From 6f158f834dcfa638639391f37afcb2ca8457cb45 Mon Sep 17 00:00:00 2001 From: Nicholas Rishel Date: Thu, 4 Mar 2021 15:48:48 -0800 Subject: Refactor of Wintab to use Wintab supplied mouse movement once verified against system input. Reviewed By: brecht, LazyDodo Maniphest Tasks: T88852 Differential Revision: https://developer.blender.org/D11508 --- intern/ghost/CMakeLists.txt | 2 + intern/ghost/GHOST_Types.h | 4 +- intern/ghost/intern/GHOST_System.h | 2 +- intern/ghost/intern/GHOST_SystemWin32.cpp | 412 ++++++++++++++++++++----- intern/ghost/intern/GHOST_SystemWin32.h | 23 ++ intern/ghost/intern/GHOST_WindowWin32.cpp | 255 ++++------------ intern/ghost/intern/GHOST_WindowWin32.h | 87 +++--- intern/ghost/intern/GHOST_Wintab.cpp | 491 ++++++++++++++++++++++++++++++ intern/ghost/intern/GHOST_Wintab.h | 250 +++++++++++++++ 9 files changed, 1201 insertions(+), 325 deletions(-) create mode 100644 intern/ghost/intern/GHOST_Wintab.cpp create mode 100644 intern/ghost/intern/GHOST_Wintab.h (limited to 'intern/ghost') diff --git a/intern/ghost/CMakeLists.txt b/intern/ghost/CMakeLists.txt index 1b5cdb3cda0..40d78a22e6f 100644 --- a/intern/ghost/CMakeLists.txt +++ b/intern/ghost/CMakeLists.txt @@ -370,6 +370,7 @@ elseif(WIN32) intern/GHOST_DropTargetWin32.cpp intern/GHOST_SystemWin32.cpp intern/GHOST_WindowWin32.cpp + intern/GHOST_Wintab.cpp intern/GHOST_ContextD3D.h intern/GHOST_DisplayManagerWin32.h @@ -377,6 +378,7 @@ elseif(WIN32) intern/GHOST_SystemWin32.h intern/GHOST_TaskbarWin32.h intern/GHOST_WindowWin32.h + intern/GHOST_Wintab.h ) if(NOT WITH_GL_EGL) diff --git a/intern/ghost/GHOST_Types.h b/intern/ghost/GHOST_Types.h index 3a8d0fbfecf..f18e6f03ede 100644 --- a/intern/ghost/GHOST_Types.h +++ b/intern/ghost/GHOST_Types.h @@ -105,7 +105,9 @@ typedef enum { typedef enum { GHOST_kTabletAutomatic = 0, - GHOST_kTabletNative, + /* Show as Windows Ink to users to match "Use Windows Ink" in tablet utilities, but we use the + dependent Windows Pointer API. */ + GHOST_kTabletWinPointer, GHOST_kTabletWintab, } GHOST_TTabletAPI; diff --git a/intern/ghost/intern/GHOST_System.h b/intern/ghost/intern/GHOST_System.h index 2a7123b293e..9915520691f 100644 --- a/intern/ghost/intern/GHOST_System.h +++ b/intern/ghost/intern/GHOST_System.h @@ -239,7 +239,7 @@ class GHOST_System : public GHOST_ISystem { * Set which tablet API to use. Only affects Windows, other platforms have a single API. * \param api: Enum indicating which API to use. */ - void setTabletAPI(GHOST_TTabletAPI api); + virtual void setTabletAPI(GHOST_TTabletAPI api) override; GHOST_TTabletAPI getTabletAPI(void); #ifdef WITH_INPUT_NDOF diff --git a/intern/ghost/intern/GHOST_SystemWin32.cpp b/intern/ghost/intern/GHOST_SystemWin32.cpp index 45b9e88f884..09cfa30eca5 100644 --- a/intern/ghost/intern/GHOST_SystemWin32.cpp +++ b/intern/ghost/intern/GHOST_SystemWin32.cpp @@ -866,15 +866,151 @@ GHOST_EventButton *GHOST_SystemWin32::processButtonEvent(GHOST_TEventType type, { GHOST_SystemWin32 *system = (GHOST_SystemWin32 *)getSystem(); - if (type == GHOST_kEventButtonDown) { - window->updateMouseCapture(MousePressed); + GHOST_TabletData td = window->getTabletData(); + + /* Move mouse to button event position. */ + if (window->getTabletData().Active != GHOST_kTabletModeNone) { + /* Tablet should be handling inbetween mouse moves, only move to event position. */ + DWORD msgPos = ::GetMessagePos(); + int msgPosX = GET_X_LPARAM(msgPos); + int msgPosY = GET_Y_LPARAM(msgPos); + system->pushEvent(new GHOST_EventCursor( + ::GetMessageTime(), GHOST_kEventCursorMove, window, msgPosX, msgPosY, td)); } - else if (type == GHOST_kEventButtonUp) { - window->updateMouseCapture(MouseReleased); + + window->updateMouseCapture(type == GHOST_kEventButtonDown ? MousePressed : MouseReleased); + return new GHOST_EventButton(system->getMilliSeconds(), type, window, mask, td); +} + +void GHOST_SystemWin32::processWintabEvent(GHOST_WindowWin32 *window) +{ + GHOST_Wintab *wt = window->getWintab(); + if (!wt) { + return; + } + + GHOST_SystemWin32 *system = (GHOST_SystemWin32 *)getSystem(); + + std::vector wintabInfo; + wt->getInput(wintabInfo); + + /* Wintab provided coordinates are untrusted until a Wintab and Win32 button down event match. + * This is checked on every button down event, and revoked if there is a mismatch. This can + * happen when Wintab incorrectly scales cursor position or is in mouse mode. + * + * If Wintab was never trusted while processing this Win32 event, a fallback Ghost cursor move + * event is created at the position of the Win32 WT_PACKET event. */ + bool mouseMoveHandled; + bool useWintabPos; + mouseMoveHandled = useWintabPos = wt->trustCoordinates(); + + for (GHOST_WintabInfoWin32 &info : wintabInfo) { + switch (info.type) { + case GHOST_kEventCursorMove: { + if (!useWintabPos) { + continue; + } + + wt->mapWintabToSysCoordinates(info.x, info.y, info.x, info.y); + system->pushEvent(new GHOST_EventCursor( + info.time, GHOST_kEventCursorMove, window, info.x, info.y, info.tabletData)); + + break; + } + case GHOST_kEventButtonDown: { + UINT message; + switch (info.button) { + case GHOST_kButtonMaskLeft: + message = WM_LBUTTONDOWN; + break; + case GHOST_kButtonMaskRight: + message = WM_RBUTTONDOWN; + break; + case GHOST_kButtonMaskMiddle: + message = WM_MBUTTONDOWN; + break; + default: + continue; + } + + /* Wintab buttons are modal, but the API does not inform us what mode a pressed button is + * in. Only issue button events if we can steal an equivalent Win32 button event from the + * event queue. */ + MSG msg; + if (PeekMessage(&msg, window->getHWND(), message, message, PM_NOYIELD) && + msg.message != WM_QUIT) { + + /* Test for Win32/Wintab button down match. */ + useWintabPos = wt->testCoordinates(msg.pt.x, msg.pt.y, info.x, info.y); + if (!useWintabPos) { + continue; + } + + /* Steal the Win32 event which was previously peeked. */ + PeekMessage(&msg, window->getHWND(), message, message, PM_REMOVE | PM_NOYIELD); + + /* Move cursor to button location, to prevent incorrect cursor position when + * transitioning from unsynchronized Win32 to Wintab cursor control. */ + wt->mapWintabToSysCoordinates(info.x, info.y, info.x, info.y); + system->pushEvent(new GHOST_EventCursor( + info.time, GHOST_kEventCursorMove, window, info.x, info.y, info.tabletData)); + + window->updateMouseCapture(MousePressed); + system->pushEvent( + new GHOST_EventButton(info.time, info.type, window, info.button, info.tabletData)); + + mouseMoveHandled = true; + break; + } + } + case GHOST_kEventButtonUp: { + if (!useWintabPos) { + continue; + } + + UINT message; + switch (info.button) { + case GHOST_kButtonMaskLeft: + message = WM_LBUTTONUP; + break; + case GHOST_kButtonMaskRight: + message = WM_RBUTTONUP; + break; + case GHOST_kButtonMaskMiddle: + message = WM_MBUTTONUP; + break; + default: + continue; + } + + /* Wintab buttons are modal, but the API does not inform us what mode a pressed button is + * in. Only issue button events if we can steal an equivalent Win32 button event from the + * event queue. */ + MSG msg; + if (PeekMessage(&msg, window->getHWND(), message, message, PM_REMOVE | PM_NOYIELD) && + msg.message != WM_QUIT) { + + window->updateMouseCapture(MouseReleased); + system->pushEvent( + new GHOST_EventButton(info.time, info.type, window, info.button, info.tabletData)); + } + break; + } + default: + break; + } } - return new GHOST_EventButton( - system->getMilliSeconds(), type, window, mask, window->getTabletData()); + /* Fallback cursor movement if Wintab position were never trusted while processing this event. */ + if (!mouseMoveHandled) { + DWORD pos = GetMessagePos(); + int x = GET_X_LPARAM(pos); + int y = GET_Y_LPARAM(pos); + + /* TODO supply tablet data */ + system->pushEvent(new GHOST_EventCursor( + system->getMilliSeconds(), GHOST_kEventCursorMove, window, x, y, GHOST_TABLET_DATA_NONE)); + } } void GHOST_SystemWin32::processPointerEvent( @@ -882,7 +1018,7 @@ void GHOST_SystemWin32::processPointerEvent( { /* Pointer events might fire when changing windows for a device which is set to use Wintab, even * when when Wintab is left enabled but set to the bottom of Wintab overlap order. */ - if (!window->useTabletAPI(GHOST_kTabletNative)) { + if (!window->usingTabletAPI(GHOST_kTabletWinPointer)) { return; } @@ -893,20 +1029,21 @@ void GHOST_SystemWin32::processPointerEvent( return; } - if (!pointerInfo[0].isPrimary) { - eventHandled = true; - return; // For multi-touch displays we ignore these events - } - switch (type) { - case WM_POINTERENTER: - window->m_tabletInRange = true; - system->pushEvent(new GHOST_EventCursor(pointerInfo[0].time, - GHOST_kEventCursorMove, - window, - pointerInfo[0].pixelLocation.x, - pointerInfo[0].pixelLocation.y, - pointerInfo[0].tabletData)); + case WM_POINTERUPDATE: + /* Coalesced pointer events are reverse chronological order, reorder chronologically. + * Only contiguous move events are coalesced. */ + for (GHOST_TUns32 i = pointerInfo.size(); i-- > 0;) { + system->pushEvent(new GHOST_EventCursor(pointerInfo[i].time, + GHOST_kEventCursorMove, + window, + pointerInfo[i].pixelLocation.x, + pointerInfo[i].pixelLocation.y, + pointerInfo[i].tabletData)); + } + + /* Leave event unhandled so that system cursor is moved. */ + break; case WM_POINTERDOWN: /* Move cursor to point of contact because GHOST_EventButton does not include position. */ @@ -922,18 +1059,10 @@ void GHOST_SystemWin32::processPointerEvent( pointerInfo[0].buttonMask, pointerInfo[0].tabletData)); window->updateMouseCapture(MousePressed); - break; - case WM_POINTERUPDATE: - /* Coalesced pointer events are reverse chronological order, reorder chronologically. - * Only contiguous move events are coalesced. */ - for (GHOST_TUns32 i = pointerInfo.size(); i-- > 0;) { - system->pushEvent(new GHOST_EventCursor(pointerInfo[i].time, - GHOST_kEventCursorMove, - window, - pointerInfo[i].pixelLocation.x, - pointerInfo[i].pixelLocation.y, - pointerInfo[i].tabletData)); - } + + /* Mark event handled so that mouse button events are not generated. */ + eventHandled = true; + break; case WM_POINTERUP: system->pushEvent(new GHOST_EventButton(pointerInfo[0].time, @@ -942,16 +1071,14 @@ void GHOST_SystemWin32::processPointerEvent( pointerInfo[0].buttonMask, pointerInfo[0].tabletData)); window->updateMouseCapture(MouseReleased); - break; - case WM_POINTERLEAVE: - window->m_tabletInRange = false; + + /* Mark event handled so that mouse button events are not generated. */ + eventHandled = true; + break; default: break; } - - eventHandled = true; - system->setCursorPosition(pointerInfo[0].pixelLocation.x, pointerInfo[0].pixelLocation.y); } GHOST_EventCursor *GHOST_SystemWin32::processCursorEvent(GHOST_WindowWin32 *window) @@ -959,18 +1086,14 @@ GHOST_EventCursor *GHOST_SystemWin32::processCursorEvent(GHOST_WindowWin32 *wind GHOST_TInt32 x_screen, y_screen; GHOST_SystemWin32 *system = (GHOST_SystemWin32 *)getSystem(); - if (window->m_tabletInRange) { - if (window->useTabletAPI(GHOST_kTabletNative)) { - /* Tablet input handled in WM_POINTER* events. WM_MOUSEMOVE events in response to tablet - * input aren't normally generated when using WM_POINTER events, but manually moving the - * system cursor as we do in WM_POINTER handling does. */ - return NULL; - } + if (window->getTabletData().Active != GHOST_kTabletModeNone) { + /* While pen devices are in range, cursor movement is handled by tablet input processing. */ + return NULL; } system->getCursorPosition(x_screen, y_screen); - if (window->getCursorGrabModeIsWarp() && !window->m_tabletInRange) { + if (window->getCursorGrabModeIsWarp()) { GHOST_TInt32 x_new = x_screen; GHOST_TInt32 y_new = y_screen; GHOST_TInt32 x_accum, y_accum; @@ -982,7 +1105,7 @@ GHOST_EventCursor *GHOST_SystemWin32::processCursorEvent(GHOST_WindowWin32 *wind } /* Could also clamp to screen bounds wrap with a window outside the view will fail atm. - * Use offset of 8 in case the window is at screen bounds. */ + * Use inset in case the window is at screen bounds. */ bounds.wrapPoint(x_new, y_new, 2, window->getCursorGrabAxis()); window->getCursorGrabAccum(x_accum, y_accum); @@ -998,7 +1121,7 @@ GHOST_EventCursor *GHOST_SystemWin32::processCursorEvent(GHOST_WindowWin32 *wind window, x_screen + x_accum, y_screen + y_accum, - window->getTabletData()); + GHOST_TABLET_DATA_NONE); } } else { @@ -1007,7 +1130,7 @@ GHOST_EventCursor *GHOST_SystemWin32::processCursorEvent(GHOST_WindowWin32 *wind window, x_screen, y_screen, - window->getTabletData()); + GHOST_TABLET_DATA_NONE); } return NULL; } @@ -1117,6 +1240,23 @@ GHOST_EventKey *GHOST_SystemWin32::processKeyEvent(GHOST_WindowWin32 *window, RA return event; } +GHOST_Event *GHOST_SystemWin32::processWindowSizeEvent(GHOST_WindowWin32 *window) +{ + GHOST_SystemWin32 *system = (GHOST_SystemWin32 *)getSystem(); + GHOST_Event *sizeEvent = new GHOST_Event( + system->getMilliSeconds(), GHOST_kEventWindowSize, window); + + /* We get WM_SIZE before we fully init. Do not dispatch before we are continuously resizing. */ + if (window->m_inLiveResize) { + system->pushEvent(sizeEvent); + system->dispatchEvents(); + return NULL; + } + else { + return sizeEvent; + } +} + GHOST_Event *GHOST_SystemWin32::processWindowEvent(GHOST_TEventType type, GHOST_WindowWin32 *window) { @@ -1124,7 +1264,6 @@ GHOST_Event *GHOST_SystemWin32::processWindowEvent(GHOST_TEventType type, if (type == GHOST_kEventWindowActivate) { system->getWindowManager()->setActiveWindow(window); - window->bringTabletContextToFront(); } return new GHOST_Event(system->getMilliSeconds(), type, window); @@ -1152,6 +1291,31 @@ GHOST_TSuccess GHOST_SystemWin32::pushDragDropEvent(GHOST_TEventType eventType, system->getMilliSeconds(), eventType, draggedObjectType, window, mouseX, mouseY, data)); } +void GHOST_SystemWin32::setTabletAPI(GHOST_TTabletAPI api) +{ + GHOST_System::setTabletAPI(api); + + /* If API is set to WinPointer (Windows Ink), unload Wintab so that trouble drivers don't disable + * Windows Ink. Load Wintab when API is Automatic because decision logic relies on knowing + * whether a Wintab device is present. */ + const bool loadWintab = GHOST_kTabletWinPointer != api; + GHOST_WindowManager *wm = getWindowManager(); + + for (GHOST_IWindow *win : wm->getWindows()) { + GHOST_WindowWin32 *windowWin32 = (GHOST_WindowWin32 *)win; + if (loadWintab) { + windowWin32->loadWintab(GHOST_kWindowStateMinimized != windowWin32->getState()); + + if (windowWin32->usingTabletAPI(GHOST_kTabletWintab)) { + windowWin32->resetPointerPenInfo(); + } + } + else { + windowWin32->closeWintab(); + } + } +} + void GHOST_SystemWin32::processMinMaxInfo(MINMAXINFO *minmax) { minmax->ptMinTrackSize.x = 320; @@ -1386,33 +1550,100 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, UINT msg, WPARAM wParam, case SC_KEYMENU: eventHandled = true; break; - case SC_RESTORE: + case SC_RESTORE: { ::ShowWindow(hwnd, SW_RESTORE); window->setState(window->getState()); + + GHOST_Wintab *wt = window->getWintab(); + if (wt) { + wt->enable(); + } + eventHandled = true; break; + } + case SC_MAXIMIZE: { + GHOST_Wintab *wt = window->getWintab(); + if (wt) { + wt->enable(); + } + /* Don't report event as handled so that default handling occurs. */ + break; + } + case SC_MINIMIZE: { + GHOST_Wintab *wt = window->getWintab(); + if (wt) { + wt->disable(); + } + /* Don't report event as handled so that default handling occurs. */ + break; + } } break; //////////////////////////////////////////////////////////////////////// // Wintab events, processed //////////////////////////////////////////////////////////////////////// - case WT_PACKET: - window->processWin32TabletEvent(wParam, lParam); + case WT_CSRCHANGE: { + GHOST_Wintab *wt = window->getWintab(); + if (wt) { + wt->updateCursorInfo(); + } + eventHandled = true; + break; + } + case WT_PROXIMITY: { + GHOST_Wintab *wt = window->getWintab(); + if (wt) { + bool inRange = LOWORD(lParam); + if (inRange) { + /* Some devices don't emit WT_CSRCHANGE events, so update cursor info here. */ + wt->updateCursorInfo(); + } + else { + wt->leaveRange(); + } + } + eventHandled = true; break; - case WT_CSRCHANGE: - case WT_PROXIMITY: - window->processWin32TabletInitEvent(); + } + case WT_INFOCHANGE: { + GHOST_Wintab *wt = window->getWintab(); + if (wt) { + wt->processInfoChange(lParam); + + if (window->usingTabletAPI(GHOST_kTabletWintab)) { + window->resetPointerPenInfo(); + } + } + eventHandled = true; + break; + } + case WT_PACKET: + processWintabEvent(window); + eventHandled = true; break; //////////////////////////////////////////////////////////////////////// // Pointer events, processed //////////////////////////////////////////////////////////////////////// - case WM_POINTERENTER: - case WM_POINTERDOWN: case WM_POINTERUPDATE: + case WM_POINTERDOWN: case WM_POINTERUP: - case WM_POINTERLEAVE: processPointerEvent(msg, window, wParam, lParam, eventHandled); break; + case WM_POINTERLEAVE: { + GHOST_TUns32 pointerId = GET_POINTERID_WPARAM(wParam); + POINTER_INFO pointerInfo; + if (!GetPointerInfo(pointerId, &pointerInfo)) { + break; + } + + /* Reset pointer pen info if pen device has left tracking range. */ + if (pointerInfo.pointerType == PT_PEN && !IS_POINTER_INRANGE_WPARAM(wParam)) { + window->resetPointerPenInfo(); + eventHandled = true; + } + break; + } //////////////////////////////////////////////////////////////////////// // Mouse events, processed //////////////////////////////////////////////////////////////////////// @@ -1451,7 +1682,20 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, UINT msg, WPARAM wParam, } break; case WM_MOUSEMOVE: + if (!window->m_mousePresent) { + TRACKMOUSEEVENT tme = {sizeof(tme)}; + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = hwnd; + TrackMouseEvent(&tme); + window->m_mousePresent = true; + GHOST_Wintab *wt = window->getWintab(); + if (wt) { + wt->gainFocus(); + } + } + event = processCursorEvent(window); + break; case WM_MOUSEWHEEL: { /* The WM_MOUSEWHEEL message is sent to the focus window @@ -1486,7 +1730,17 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, UINT msg, WPARAM wParam, window->loadCursor(true, GHOST_kStandardCursorDefault); } break; - + case WM_MOUSELEAVE: { + window->m_mousePresent = false; + if (window->getTabletData().Active == GHOST_kTabletModeNone) { + processCursorEvent(window); + } + GHOST_Wintab *wt = window->getWintab(); + if (wt) { + wt->loseFocus(); + } + break; + } //////////////////////////////////////////////////////////////////////// // Mouse events, ignored //////////////////////////////////////////////////////////////////////// @@ -1534,7 +1788,7 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, UINT msg, WPARAM wParam, * will not be dispatched to OUR active window if we minimize one of OUR windows. */ if (LOWORD(wParam) == WA_INACTIVE) window->lostMouseCapture(); - window->processWin32TabletActivateEvent(GET_WM_ACTIVATE_STATE(wParam, lParam)); + lResult = ::DefWindowProc(hwnd, msg, wParam, lParam); break; } @@ -1576,6 +1830,8 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, UINT msg, WPARAM wParam, /* Let DefWindowProc handle it. */ break; case WM_SIZING: + event = processWindowSizeEvent(window); + break; case WM_SIZE: /* The WM_SIZE message is sent to a window after its size has changed. * The WM_SIZE and WM_MOVE messages are not sent if an application handles the @@ -1583,15 +1839,7 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, UINT msg, WPARAM wParam, * to perform any move or size change processing during the WM_WINDOWPOSCHANGED * message without calling DefWindowProc. */ - /* we get first WM_SIZE before we fully init. - * So, do not dispatch before we continuously resizing. */ - if (window->m_inLiveResize) { - system->pushEvent(processWindowEvent(GHOST_kEventWindowSize, window)); - system->dispatchEvents(); - } - else { - event = processWindowEvent(GHOST_kEventWindowSize, window); - } + event = processWindowSizeEvent(window); break; case WM_CAPTURECHANGED: window->lostMouseCapture(); @@ -1642,6 +1890,24 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, UINT msg, WPARAM wParam, SWP_NOZORDER | SWP_NOACTIVATE); } break; + case WM_DISPLAYCHANGE: { + GHOST_Wintab *wt = window->getWintab(); + if (wt) { + for (GHOST_IWindow *iter_win : system->getWindowManager()->getWindows()) { + GHOST_WindowWin32 *iter_win32win = (GHOST_WindowWin32 *)iter_win; + wt->remapCoordinates(); + } + } + break; + } + case WM_KILLFOCUS: + /* The WM_KILLFOCUS message is sent to a window immediately before it loses the keyboard + * focus. We want to prevent this if a window is still active and it loses focus to + * nowhere. */ + if (!wParam && hwnd == ::GetActiveWindow()) { + ::SetFocus(hwnd); + } + break; //////////////////////////////////////////////////////////////////////// // Window events, ignored //////////////////////////////////////////////////////////////////////// @@ -1678,12 +1944,6 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, UINT msg, WPARAM wParam, * object associated with the window. */ break; - case WM_KILLFOCUS: - /* The WM_KILLFOCUS message is sent to a window immediately before it loses the - * keyboard focus. We want to prevent this if a window is still active and it loses - * focus to nowhere. */ - if (!wParam && hwnd == ::GetActiveWindow()) - ::SetFocus(hwnd); case WM_SHOWWINDOW: /* The WM_SHOWWINDOW message is sent to a window when the window is * about to be hidden or shown. */ diff --git a/intern/ghost/intern/GHOST_SystemWin32.h b/intern/ghost/intern/GHOST_SystemWin32.h index 51c0c984710..7dd61421d4c 100644 --- a/intern/ghost/intern/GHOST_SystemWin32.h +++ b/intern/ghost/intern/GHOST_SystemWin32.h @@ -265,6 +265,16 @@ class GHOST_SystemWin32 : public GHOST_System { int mouseY, void *data); + /*************************************************************************************** + ** Modify tablet API + ***************************************************************************************/ + + /** + * Set which tablet API to use. + * \param api: Enum indicating which API to use. + */ + void setTabletAPI(GHOST_TTabletAPI api) override; + protected: /** * Initializes the system. @@ -308,6 +318,12 @@ class GHOST_SystemWin32 : public GHOST_System { GHOST_WindowWin32 *window, GHOST_TButtonMask mask); + /** + * Creates tablet events from Wintab events. + * \param window: The window receiving the event (the active window). + */ + static void processWintabEvent(GHOST_WindowWin32 *window); + /** * Creates tablet events from pointer events. * \param type: The type of pointer event. @@ -351,6 +367,13 @@ class GHOST_SystemWin32 : public GHOST_System { */ GHOST_TKey processSpecialKey(short vKey, short scanCode) const; + /** + * Creates a window size event. + * \param window: The window receiving the event (the active window). + * \return The event created. + */ + static GHOST_Event *processWindowSizeEvent(GHOST_WindowWin32 *window); + /** * Creates a window event. * \param type: The type of event to create. diff --git a/intern/ghost/intern/GHOST_WindowWin32.cpp b/intern/ghost/intern/GHOST_WindowWin32.cpp index eeafe333633..33c79daf11d 100644 --- a/intern/ghost/intern/GHOST_WindowWin32.cpp +++ b/intern/ghost/intern/GHOST_WindowWin32.cpp @@ -21,8 +21,6 @@ * \ingroup GHOST */ -#define _USE_MATH_DEFINES - #include "GHOST_WindowWin32.h" #include "GHOST_ContextD3D.h" #include "GHOST_ContextNone.h" @@ -72,7 +70,7 @@ GHOST_WindowWin32::GHOST_WindowWin32(GHOST_SystemWin32 *system, bool is_debug, bool dialog) : GHOST_Window(width, height, state, wantStereoVisual, false), - m_tabletInRange(false), + m_mousePresent(false), m_inLiveResize(false), m_system(system), m_hDC(0), @@ -82,6 +80,8 @@ GHOST_WindowWin32::GHOST_WindowWin32(GHOST_SystemWin32 *system, m_nPressedButtons(0), m_customCursor(0), m_wantAlphaBackground(alphaBackground), + m_wintab(NULL), + m_lastPointerTabletData(GHOST_TABLET_DATA_NONE), m_normal_state(GHOST_kWindowStateNormal), m_user32(NULL), m_parentWindowHwnd(parentwindow ? parentwindow->m_hWnd : HWND_DESKTOP), @@ -90,10 +90,6 @@ GHOST_WindowWin32::GHOST_WindowWin32(GHOST_SystemWin32 *system, wchar_t *title_16 = alloc_utf16_from_8((char *)title, 0); RECT win_rect = {left, top, (long)(left + width), (long)(top + height)}; - // Initialize tablet variables - memset(&m_wintab, 0, sizeof(m_wintab)); - m_tabletData = GHOST_TABLET_DATA_NONE; - DWORD style = parentwindow ? WS_POPUPWINDOW | WS_CAPTION | WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_SIZEBOX : WS_OVERLAPPEDWINDOW; @@ -218,65 +214,10 @@ GHOST_WindowWin32::GHOST_WindowWin32(GHOST_SystemWin32 *system, } // Initialize Wintab - m_wintab.handle = ::LoadLibrary("Wintab32.dll"); - if (m_wintab.handle && m_system->getTabletAPI() != GHOST_kTabletNative) { - // Get API functions - m_wintab.info = (GHOST_WIN32_WTInfo)::GetProcAddress(m_wintab.handle, "WTInfoA"); - m_wintab.open = (GHOST_WIN32_WTOpen)::GetProcAddress(m_wintab.handle, "WTOpenA"); - m_wintab.close = (GHOST_WIN32_WTClose)::GetProcAddress(m_wintab.handle, "WTClose"); - m_wintab.packet = (GHOST_WIN32_WTPacket)::GetProcAddress(m_wintab.handle, "WTPacket"); - m_wintab.enable = (GHOST_WIN32_WTEnable)::GetProcAddress(m_wintab.handle, "WTEnable"); - m_wintab.overlap = (GHOST_WIN32_WTOverlap)::GetProcAddress(m_wintab.handle, "WTOverlap"); - - // Let's see if we can initialize tablet here. - // Check if WinTab available by getting system context info. - LOGCONTEXT lc = {0}; - lc.lcOptions |= CXO_SYSTEM; - if (m_wintab.open && m_wintab.info && m_wintab.info(WTI_DEFSYSCTX, 0, &lc)) { - // Now init the tablet - /* The maximum tablet size, pressure and orientation (tilt) */ - AXIS TabletX, TabletY, Pressure, Orientation[3]; - - // Open a Wintab context - - // Open the context - lc.lcPktData = PACKETDATA; - lc.lcPktMode = PACKETMODE; - lc.lcOptions |= CXO_MESSAGES; - lc.lcMoveMask = PACKETDATA; - - /* Set the entire tablet as active */ - m_wintab.info(WTI_DEVICES, DVC_X, &TabletX); - m_wintab.info(WTI_DEVICES, DVC_Y, &TabletY); - - /* get the max pressure, to divide into a float */ - BOOL pressureSupport = m_wintab.info(WTI_DEVICES, DVC_NPRESSURE, &Pressure); - if (pressureSupport) - m_wintab.maxPressure = Pressure.axMax; - else - m_wintab.maxPressure = 0; - - /* get the max tilt axes, to divide into floats */ - BOOL tiltSupport = m_wintab.info(WTI_DEVICES, DVC_ORIENTATION, &Orientation); - if (tiltSupport) { - /* does the tablet support azimuth ([0]) and altitude ([1]) */ - if (Orientation[0].axResolution && Orientation[1].axResolution) { - /* all this assumes the minimum is 0 */ - m_wintab.maxAzimuth = Orientation[0].axMax; - m_wintab.maxAltitude = Orientation[1].axMax; - } - else { /* No so don't do tilt stuff. */ - m_wintab.maxAzimuth = m_wintab.maxAltitude = 0; - } - } - - // The Wintab spec says we must open the context disabled if we are using cursor masks. - m_wintab.tablet = m_wintab.open(m_hWnd, &lc, FALSE); - if (m_wintab.enable && m_wintab.tablet) { - m_wintab.enable(m_wintab.tablet, TRUE); - } - } + if (system->getTabletAPI() != GHOST_kTabletWinPointer) { + loadWintab(GHOST_kWindowStateMinimized != state); } + CoCreateInstance( CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, IID_ITaskbarList3, (LPVOID *)&m_Bar); } @@ -289,14 +230,7 @@ GHOST_WindowWin32::~GHOST_WindowWin32() m_Bar = NULL; } - if (m_wintab.handle) { - if (m_wintab.close && m_wintab.tablet) { - m_wintab.close(m_wintab.tablet); - } - - FreeLibrary(m_wintab.handle); - memset(&m_wintab, 0, sizeof(m_wintab)); - } + closeWintab(); if (m_user32) { FreeLibrary(m_user32); @@ -913,20 +847,16 @@ GHOST_TSuccess GHOST_WindowWin32::hasCursorShape(GHOST_TStandardCursor cursorSha GHOST_TSuccess GHOST_WindowWin32::getPointerInfo( std::vector &outPointerInfo, WPARAM wParam, LPARAM lParam) { - if (!useTabletAPI(GHOST_kTabletNative)) { - return GHOST_kFailure; - } - GHOST_TInt32 pointerId = GET_POINTERID_WPARAM(wParam); GHOST_TInt32 isPrimary = IS_POINTER_PRIMARY_WPARAM(wParam); GHOST_SystemWin32 *system = (GHOST_SystemWin32 *)GHOST_System::getSystem(); - GHOST_TUns32 outCount; + GHOST_TUns32 outCount = 0; - if (!(GetPointerInfoHistory(pointerId, &outCount, NULL))) { + if (!(GetPointerPenInfoHistory(pointerId, &outCount, NULL))) { return GHOST_kFailure; } - auto pointerPenInfo = std::vector(outCount); + std::vector pointerPenInfo(outCount); outPointerInfo.resize(outCount); if (!(GetPointerPenInfoHistory(pointerId, &outCount, pointerPenInfo.data()))) { @@ -988,148 +918,77 @@ GHOST_TSuccess GHOST_WindowWin32::getPointerInfo( } } + if (!outPointerInfo.empty()) { + m_lastPointerTabletData = outPointerInfo.back().tabletData; + } + return GHOST_kSuccess; } -void GHOST_WindowWin32::processWin32TabletActivateEvent(WORD state) +void GHOST_WindowWin32::resetPointerPenInfo() { - if (!useTabletAPI(GHOST_kTabletWintab)) { - return; - } - - if (m_wintab.enable && m_wintab.tablet) { - m_wintab.enable(m_wintab.tablet, state); - - if (m_wintab.overlap && state) { - m_wintab.overlap(m_wintab.tablet, TRUE); - } - } + m_lastPointerTabletData = GHOST_TABLET_DATA_NONE; } -bool GHOST_WindowWin32::useTabletAPI(GHOST_TTabletAPI api) const +GHOST_Wintab *GHOST_WindowWin32::getWintab() const { - if (m_system->getTabletAPI() == api) { - return true; - } - else if (m_system->getTabletAPI() == GHOST_kTabletAutomatic) { - if (m_wintab.tablet) - return api == GHOST_kTabletWintab; - else - return api == GHOST_kTabletNative; - } - else { - return false; - } + return m_wintab; } -void GHOST_WindowWin32::processWin32TabletInitEvent() +void GHOST_WindowWin32::loadWintab(bool enable) { - if (!useTabletAPI(GHOST_kTabletWintab)) { - return; - } - - // Let's see if we can initialize tablet here - if (m_wintab.info && m_wintab.tablet) { - AXIS Pressure, Orientation[3]; /* The maximum tablet size */ - - BOOL pressureSupport = m_wintab.info(WTI_DEVICES, DVC_NPRESSURE, &Pressure); - if (pressureSupport) - m_wintab.maxPressure = Pressure.axMax; - else - m_wintab.maxPressure = 0; - - BOOL tiltSupport = m_wintab.info(WTI_DEVICES, DVC_ORIENTATION, &Orientation); - if (tiltSupport) { - /* does the tablet support azimuth ([0]) and altitude ([1]) */ - if (Orientation[0].axResolution && Orientation[1].axResolution) { - m_wintab.maxAzimuth = Orientation[0].axMax; - m_wintab.maxAltitude = Orientation[1].axMax; - } - else { /* No so don't do tilt stuff. */ - m_wintab.maxAzimuth = m_wintab.maxAltitude = 0; + if (!m_wintab) { + if (m_wintab = GHOST_Wintab::loadWintab(m_hWnd)) { + if (enable) { + m_wintab->enable(); + + /* Focus Wintab if cursor is inside this window. This ensures Wintab is enabled when the + * tablet is used to change the Tablet API. */ + GHOST_TInt32 x, y; + if (m_system->getCursorPosition(x, y)) { + GHOST_Rect rect; + getClientBounds(rect); + + if (rect.isInside(x, y)) { + m_wintab->gainFocus(); + } + } } } } +} - m_tabletData.Active = GHOST_kTabletModeNone; +void GHOST_WindowWin32::closeWintab() +{ + delete m_wintab; + m_wintab = NULL; } -void GHOST_WindowWin32::processWin32TabletEvent(WPARAM wParam, LPARAM lParam) +bool GHOST_WindowWin32::usingTabletAPI(GHOST_TTabletAPI api) const { - if (!useTabletAPI(GHOST_kTabletWintab)) { - return; + if (m_system->getTabletAPI() == api) { + return true; } - - if (m_wintab.packet && m_wintab.tablet) { - PACKET pkt; - if (m_wintab.packet((HCTX)lParam, wParam, &pkt)) { - switch (pkt.pkCursor % 3) { /* % 3 for multiple devices ("DualTrack") */ - case 0: - m_tabletData.Active = GHOST_kTabletModeNone; /* puck - not yet supported */ - break; - case 1: - m_tabletData.Active = GHOST_kTabletModeStylus; /* stylus */ - break; - case 2: - m_tabletData.Active = GHOST_kTabletModeEraser; /* eraser */ - break; - } - - if (m_wintab.maxPressure > 0) { - m_tabletData.Pressure = (float)pkt.pkNormalPressure / (float)m_wintab.maxPressure; - } - else { - m_tabletData.Pressure = 1.0f; - } - - if ((m_wintab.maxAzimuth > 0) && (m_wintab.maxAltitude > 0)) { - ORIENTATION ort = pkt.pkOrientation; - float vecLen; - float altRad, azmRad; /* in radians */ - - /* - * from the wintab spec: - * orAzimuth Specifies the clockwise rotation of the - * cursor about the z axis through a full circular range. - * - * orAltitude Specifies the angle with the x-y plane - * through a signed, semicircular range. Positive values - * specify an angle upward toward the positive z axis; - * negative values specify an angle downward toward the negative z axis. - * - * wintab.h defines .orAltitude as a UINT but documents .orAltitude - * as positive for upward angles and negative for downward angles. - * WACOM uses negative altitude values to show that the pen is inverted; - * therefore we cast .orAltitude as an (int) and then use the absolute value. - */ - - /* convert raw fixed point data to radians */ - altRad = (float)((fabs((float)ort.orAltitude) / (float)m_wintab.maxAltitude) * M_PI / 2.0); - azmRad = (float)(((float)ort.orAzimuth / (float)m_wintab.maxAzimuth) * M_PI * 2.0); - - /* find length of the stylus' projected vector on the XY plane */ - vecLen = cos(altRad); - - /* from there calculate X and Y components based on azimuth */ - m_tabletData.Xtilt = sin(azmRad) * vecLen; - m_tabletData.Ytilt = (float)(sin(M_PI / 2.0 - azmRad) * vecLen); - } - else { - m_tabletData.Xtilt = 0.0f; - m_tabletData.Ytilt = 0.0f; - } + else if (m_system->getTabletAPI() == GHOST_kTabletAutomatic) { + if (m_wintab && m_wintab->devicesPresent()) { + return api == GHOST_kTabletWintab; + } + else { + return api == GHOST_kTabletWinPointer; } } + else { + return false; + } } -void GHOST_WindowWin32::bringTabletContextToFront() +GHOST_TabletData GHOST_WindowWin32::getTabletData() { - if (!useTabletAPI(GHOST_kTabletWintab)) { - return; + if (usingTabletAPI(GHOST_kTabletWintab)) { + return m_wintab ? m_wintab->getLastTabletData() : GHOST_TABLET_DATA_NONE; } - - if (m_wintab.overlap && m_wintab.tablet) { - m_wintab.overlap(m_wintab.tablet, TRUE); + else { + return m_lastPointerTabletData; } } diff --git a/intern/ghost/intern/GHOST_WindowWin32.h b/intern/ghost/intern/GHOST_WindowWin32.h index c261dffc578..3c1f32476de 100644 --- a/intern/ghost/intern/GHOST_WindowWin32.h +++ b/intern/ghost/intern/GHOST_WindowWin32.h @@ -30,29 +30,16 @@ #include "GHOST_TaskbarWin32.h" #include "GHOST_Window.h" +#include "GHOST_Wintab.h" #ifdef WITH_INPUT_IME # include "GHOST_ImeWin32.h" #endif #include -#include -// PACKETDATA and PACKETMODE modify structs in pktdef.h, so make sure they come first -#define PACKETDATA (PK_BUTTONS | PK_NORMAL_PRESSURE | PK_ORIENTATION | PK_CURSOR) -#define PACKETMODE PK_BUTTONS -#include - class GHOST_SystemWin32; class GHOST_DropTargetWin32; -// typedefs for WinTab functions to allow dynamic loading -typedef UINT(API *GHOST_WIN32_WTInfo)(UINT, UINT, LPVOID); -typedef HCTX(API *GHOST_WIN32_WTOpen)(HWND, LPLOGCONTEXTA, BOOL); -typedef BOOL(API *GHOST_WIN32_WTClose)(HCTX); -typedef BOOL(API *GHOST_WIN32_WTPacket)(HCTX, UINT, LPVOID); -typedef BOOL(API *GHOST_WIN32_WTEnable)(HCTX, BOOL); -typedef BOOL(API *GHOST_WIN32_WTOverlap)(HCTX, BOOL); - // typedefs for user32 functions to allow dynamic loading of Windows 10 DPI scaling functions typedef UINT(API *GHOST_WIN32_GetDpiForWindow)(HWND); @@ -62,7 +49,6 @@ struct GHOST_PointerInfoWin32 { GHOST_TButtonMask buttonMask; POINT pixelLocation; GHOST_TUns64 time; - GHOST_TabletData tabletData; }; @@ -256,16 +242,11 @@ class GHOST_WindowWin32 : public GHOST_Window { HCURSOR getStandardCursor(GHOST_TStandardCursor shape) const; void loadCursor(bool visible, GHOST_TStandardCursor cursorShape) const; - const GHOST_TabletData &getTabletData() - { - return m_tabletData; - } - /** * Query whether given tablet API should be used. * \param api: Tablet API to test. */ - bool useTabletAPI(GHOST_TTabletAPI api) const; + bool usingTabletAPI(GHOST_TTabletAPI api) const; /** * Translate WM_POINTER events into GHOST_PointerInfoWin32 structs. @@ -278,10 +259,34 @@ class GHOST_WindowWin32 : public GHOST_Window { WPARAM wParam, LPARAM lParam); - void processWin32TabletActivateEvent(WORD state); - void processWin32TabletInitEvent(); - void processWin32TabletEvent(WPARAM wParam, LPARAM lParam); - void bringTabletContextToFront(); + /** + * Resets pointer pen tablet state. + */ + void resetPointerPenInfo(); + + /** + * Retrieves pointer to Wintab if Wintab is the set Tablet API. + * \return Pointer to Wintab member. + */ + GHOST_Wintab *getWintab() const; + + /** + * Loads Wintab context for the window. + * \param enable: True if Wintab should be enabled after loading. Wintab should not be enabled if + * the window is minimzed. + */ + void loadWintab(bool enable); + + /** + * Closes Wintab for the window. + */ + void closeWintab(); + + /** + * Get the most recent Windows Pointer tablet data. + * \return Most recent pointer tablet data. + */ + GHOST_TabletData getTabletData(); GHOST_TSuccess beginFullScreen() const { @@ -295,10 +300,10 @@ class GHOST_WindowWin32 : public GHOST_Window { GHOST_TUns16 getDPIHint() override; - /** Whether a tablet stylus is being tracked. */ - bool m_tabletInRange; + /** True if the mouse is either over or captured by the window. */ + bool m_mousePresent; - /** if the window currently resizing */ + /** True if the window currently resizing. */ bool m_inLiveResize; #ifdef WITH_INPUT_IME @@ -382,27 +387,11 @@ class GHOST_WindowWin32 : public GHOST_Window { static const wchar_t *s_windowClassName; static const int s_maxTitleLength; - /** Tablet data for GHOST */ - GHOST_TabletData m_tabletData; - - /* Wintab API */ - struct { - /** `WinTab.dll` handle. */ - HMODULE handle = NULL; - - /** API functions */ - GHOST_WIN32_WTInfo info; - GHOST_WIN32_WTOpen open; - GHOST_WIN32_WTClose close; - GHOST_WIN32_WTPacket packet; - GHOST_WIN32_WTEnable enable; - GHOST_WIN32_WTOverlap overlap; - - /** Stores the Tablet context if detected Tablet features using `WinTab.dll` */ - HCTX tablet; - LONG maxPressure; - LONG maxAzimuth, maxAltitude; - } m_wintab; + /** Pointer to Wintab manager if Wintab is loaded. */ + GHOST_Wintab *m_wintab; + + /** Most recent tablet data. */ + GHOST_TabletData m_lastPointerTabletData; GHOST_TWindowState m_normal_state; diff --git a/intern/ghost/intern/GHOST_Wintab.cpp b/intern/ghost/intern/GHOST_Wintab.cpp new file mode 100644 index 00000000000..47ef484145b --- /dev/null +++ b/intern/ghost/intern/GHOST_Wintab.cpp @@ -0,0 +1,491 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** \file + * \ingroup GHOST + */ + +#define _USE_MATH_DEFINES + +#include "GHOST_Wintab.h" + +GHOST_Wintab *GHOST_Wintab::loadWintab(HWND hwnd) +{ + /* Load Wintab library if available. */ + + auto handle = unique_hmodule(::LoadLibrary("Wintab32.dll"), &::FreeLibrary); + if (!handle) { + return nullptr; + } + + /* Get Wintab functions. */ + + auto info = (GHOST_WIN32_WTInfo)::GetProcAddress(handle.get(), "WTInfoA"); + if (!info) { + return nullptr; + } + + auto open = (GHOST_WIN32_WTOpen)::GetProcAddress(handle.get(), "WTOpenA"); + if (!open) { + return nullptr; + } + + auto get = (GHOST_WIN32_WTGet)::GetProcAddress(handle.get(), "WTGetA"); + if (!get) { + return nullptr; + } + + auto set = (GHOST_WIN32_WTSet)::GetProcAddress(handle.get(), "WTSetA"); + if (!set) { + return nullptr; + } + + auto close = (GHOST_WIN32_WTClose)::GetProcAddress(handle.get(), "WTClose"); + if (!close) { + return nullptr; + } + + auto packetsGet = (GHOST_WIN32_WTPacketsGet)::GetProcAddress(handle.get(), "WTPacketsGet"); + if (!packetsGet) { + return nullptr; + } + + auto queueSizeGet = (GHOST_WIN32_WTQueueSizeGet)::GetProcAddress(handle.get(), "WTQueueSizeGet"); + if (!queueSizeGet) { + return nullptr; + } + + auto queueSizeSet = (GHOST_WIN32_WTQueueSizeSet)::GetProcAddress(handle.get(), "WTQueueSizeSet"); + if (!queueSizeSet) { + return nullptr; + } + + auto enable = (GHOST_WIN32_WTEnable)::GetProcAddress(handle.get(), "WTEnable"); + if (!enable) { + return nullptr; + } + + auto overlap = (GHOST_WIN32_WTOverlap)::GetProcAddress(handle.get(), "WTOverlap"); + if (!overlap) { + return nullptr; + } + + /* Build Wintab context. */ + + LOGCONTEXT lc = {0}; + if (!info(WTI_DEFSYSCTX, 0, &lc)) { + return nullptr; + } + + Coord tablet, system; + extractCoordinates(lc, tablet, system); + modifyContext(lc); + + /* The Wintab spec says we must open the context disabled if we are using cursor masks. */ + auto hctx = unique_hctx(open(hwnd, &lc, FALSE), close); + if (!hctx) { + return nullptr; + } + + /* Wintab provides no way to determine the maximum queue size aside from checking if attempts + * to change the queue size are successful. */ + const int maxQueue = 500; + int queueSize = queueSizeGet(hctx.get()); + + while (queueSize < maxQueue) { + int testSize = min(queueSize + 16, maxQueue); + if (queueSizeSet(hctx.get(), testSize)) { + queueSize = testSize; + } + else { + /* From Windows Wintab Documentation for WTQueueSizeSet: + * "If the return value is zero, the context has no queue because the function deletes the + * original queue before attempting to create a new one. The application must continue + * calling the function with a smaller queue size until the function returns a non - zero + * value." + * + * In our case we start with a known valid queue size and in the event of failure roll + * back to the last valid queue size. The Wintab spec dates back to 16 bit Windows, thus + * assumes memory recently deallocated may not be available, which is no longer a practical + * concern. */ + if (!queueSizeSet(hctx.get(), queueSize)) { + /* If a previously valid queue size is no longer valid, there is likely something wrong in + * the Wintab implementation and we should not use it. */ + return nullptr; + } + break; + } + } + + return new GHOST_Wintab(hwnd, + std::move(handle), + info, + get, + set, + packetsGet, + enable, + overlap, + std::move(hctx), + tablet, + system, + queueSize); +} + +void GHOST_Wintab::modifyContext(LOGCONTEXT &lc) +{ + lc.lcPktData = PACKETDATA; + lc.lcPktMode = PACKETMODE; + lc.lcMoveMask = PACKETDATA; + lc.lcOptions |= CXO_CSRMESSAGES | CXO_MESSAGES; + + /* Tablet scaling is handled manually because some drivers don't handle HIDPI or multi-display + * correctly; reset tablet scale factors to unscaled tablet coodinates. */ + lc.lcOutOrgX = lc.lcInOrgX; + lc.lcOutOrgY = lc.lcInOrgY; + lc.lcOutExtX = lc.lcInExtX; + lc.lcOutExtY = lc.lcInExtY; +} + +void GHOST_Wintab::extractCoordinates(LOGCONTEXT &lc, Coord &tablet, Coord &system) +{ + tablet.x.org = lc.lcInOrgX; + tablet.x.ext = lc.lcInExtX; + tablet.y.org = lc.lcInOrgY; + tablet.y.ext = lc.lcInExtY; + + system.x.org = lc.lcSysOrgX; + system.x.ext = lc.lcSysExtX; + system.y.org = lc.lcSysOrgY; + /* Wintab maps y origin to the tablet's bottom; invert y to match Windows y origin mapping to the + * screen top. */ + system.y.ext = -lc.lcSysExtY; +} + +GHOST_Wintab::GHOST_Wintab(HWND hwnd, + unique_hmodule handle, + GHOST_WIN32_WTInfo info, + GHOST_WIN32_WTGet get, + GHOST_WIN32_WTSet set, + GHOST_WIN32_WTPacketsGet packetsGet, + GHOST_WIN32_WTEnable enable, + GHOST_WIN32_WTOverlap overlap, + unique_hctx hctx, + Coord tablet, + Coord system, + int queueSize) + : m_handle{std::move(handle)}, + m_fpInfo{info}, + m_fpGet{get}, + m_fpSet{set}, + m_fpPacketsGet{packetsGet}, + m_fpEnable{enable}, + m_fpOverlap{overlap}, + m_context{std::move(hctx)}, + m_tabletCoord{tablet}, + m_systemCoord{system}, + m_pkts{queueSize} +{ + m_fpInfo(WTI_INTERFACE, IFC_NDEVICES, &m_numDevices); + updateCursorInfo(); +} + +void GHOST_Wintab::enable() +{ + m_fpEnable(m_context.get(), true); + m_enabled = true; +} + +void GHOST_Wintab::disable() +{ + if (m_focused) { + loseFocus(); + } + m_fpEnable(m_context.get(), false); + m_enabled = false; +} + +void GHOST_Wintab::gainFocus() +{ + m_fpOverlap(m_context.get(), true); + m_focused = true; +} + +void GHOST_Wintab::loseFocus() +{ + if (m_lastTabletData.Active != GHOST_kTabletModeNone) { + leaveRange(); + } + + /* Mouse mode of tablet or display layout may change when Wintab or Window is inactive. Don't + * trust for mouse movement until re-verified. */ + m_coordTrusted = false; + + m_fpOverlap(m_context.get(), false); + m_focused = false; +} + +void GHOST_Wintab::leaveRange() +{ + /* Button state can't be tracked while out of range, reset it. */ + m_buttons = 0; + /* Set to none to indicate tablet is inactive. */ + m_lastTabletData = GHOST_TABLET_DATA_NONE; + /* Clear the packet queue. */ + m_fpPacketsGet(m_context.get(), m_pkts.size(), m_pkts.data()); +} + +void GHOST_Wintab::remapCoordinates() +{ + LOGCONTEXT lc = {0}; + + if (m_fpInfo(WTI_DEFSYSCTX, 0, &lc)) { + extractCoordinates(lc, m_tabletCoord, m_systemCoord); + modifyContext(lc); + + m_fpSet(m_context.get(), &lc); + } +} + +void GHOST_Wintab::updateCursorInfo() +{ + AXIS Pressure, Orientation[3]; + + BOOL pressureSupport = m_fpInfo(WTI_DEVICES, DVC_NPRESSURE, &Pressure); + m_maxPressure = pressureSupport ? Pressure.axMax : 0; + + BOOL tiltSupport = m_fpInfo(WTI_DEVICES, DVC_ORIENTATION, &Orientation); + /* Check if tablet supports azimuth [0] and altitude [1], encoded in axResolution. */ + if (tiltSupport && Orientation[0].axResolution && Orientation[1].axResolution) { + m_maxAzimuth = Orientation[0].axMax; + m_maxAltitude = Orientation[1].axMax; + } + else { + m_maxAzimuth = m_maxAltitude = 0; + } +} + +void GHOST_Wintab::processInfoChange(LPARAM lParam) +{ + /* Update number of connected Wintab digitizers. */ + if (LOWORD(lParam) == WTI_INTERFACE && HIWORD(lParam) == IFC_NDEVICES) { + m_fpInfo(WTI_INTERFACE, IFC_NDEVICES, &m_numDevices); + } +} + +bool GHOST_Wintab::devicesPresent() +{ + return m_numDevices > 0; +} + +GHOST_TabletData GHOST_Wintab::getLastTabletData() +{ + return m_lastTabletData; +} + +void GHOST_Wintab::getInput(std::vector &outWintabInfo) +{ + const int numPackets = m_fpPacketsGet(m_context.get(), m_pkts.size(), m_pkts.data()); + outWintabInfo.resize(numPackets); + size_t outExtent = 0; + + for (int i = 0; i < numPackets; i++) { + PACKET pkt = m_pkts[i]; + GHOST_WintabInfoWin32 &out = outWintabInfo[i + outExtent]; + + out.tabletData = GHOST_TABLET_DATA_NONE; + /* % 3 for multiple devices ("DualTrack"). */ + switch (pkt.pkCursor % 3) { + case 0: + /* Puck - processed as mouse. */ + out.tabletData.Active = GHOST_kTabletModeNone; + break; + case 1: + out.tabletData.Active = GHOST_kTabletModeStylus; + break; + case 2: + out.tabletData.Active = GHOST_kTabletModeEraser; + break; + } + + out.x = pkt.pkX; + out.y = pkt.pkY; + + if (m_maxPressure > 0) { + out.tabletData.Pressure = (float)pkt.pkNormalPressure / (float)m_maxPressure; + } + + if ((m_maxAzimuth > 0) && (m_maxAltitude > 0)) { + ORIENTATION ort = pkt.pkOrientation; + float vecLen; + float altRad, azmRad; /* In radians. */ + + /* + * From the wintab spec: + * orAzimuth: Specifies the clockwise rotation of the cursor about the z axis through a + * full circular range. + * orAltitude: Specifies the angle with the x-y plane through a signed, semicircular range. + * Positive values specify an angle upward toward the positive z axis; negative values + * specify an angle downward toward the negative z axis. + * + * wintab.h defines orAltitude as a UINT but documents orAltitude as positive for upward + * angles and negative for downward angles. WACOM uses negative altitude values to show that + * the pen is inverted; therefore we cast orAltitude as an (int) and then use the absolute + * value. + */ + + /* Convert raw fixed point data to radians. */ + altRad = (float)((fabs((float)ort.orAltitude) / (float)m_maxAltitude) * M_PI / 2.0); + azmRad = (float)(((float)ort.orAzimuth / (float)m_maxAzimuth) * M_PI * 2.0); + + /* Find length of the stylus' projected vector on the XY plane. */ + vecLen = cos(altRad); + + /* From there calculate X and Y components based on azimuth. */ + out.tabletData.Xtilt = sin(azmRad) * vecLen; + out.tabletData.Ytilt = (float)(sin(M_PI / 2.0 - azmRad) * vecLen); + } + + out.time = pkt.pkTime; + + /* Some Wintab libraries don't handle relative button input, so we track button presses + * manually. */ + out.button = GHOST_kButtonMaskNone; + out.type = GHOST_kEventCursorMove; + + DWORD buttonsChanged = m_buttons ^ pkt.pkButtons; + WORD buttonIndex = 0; + GHOST_WintabInfoWin32 buttonRef = out; + int buttons = 0; + + while (buttonsChanged) { + if (buttonsChanged & 1) { + /* Find the index for the changed button from the button map. */ + GHOST_TButtonMask button = mapWintabToGhostButton(pkt.pkCursor, buttonIndex); + + if (button != GHOST_kButtonMaskNone) { + /* Extend output if multiple buttons are pressed. We don't extend input until we confirm + * a Wintab buttons maps to a system button. */ + if (buttons > 0) { + outWintabInfo.resize(outWintabInfo.size() + 1); + outExtent++; + GHOST_WintabInfoWin32 &out = outWintabInfo[i + outExtent]; + out = buttonRef; + } + buttons++; + + out.button = button; + if (buttonsChanged & pkt.pkButtons) { + out.type = GHOST_kEventButtonDown; + } + else { + out.type = GHOST_kEventButtonUp; + } + } + + m_buttons ^= 1 << buttonIndex; + } + + buttonsChanged >>= 1; + buttonIndex++; + } + } + + if (!outWintabInfo.empty()) { + m_lastTabletData = outWintabInfo.back().tabletData; + } +} + +GHOST_TButtonMask GHOST_Wintab::mapWintabToGhostButton(UINT cursor, WORD physicalButton) +{ + const WORD numButtons = 32; + BYTE logicalButtons[numButtons] = {0}; + BYTE systemButtons[numButtons] = {0}; + + if (!m_fpInfo(WTI_CURSORS + cursor, CSR_BUTTONMAP, &logicalButtons) || + !m_fpInfo(WTI_CURSORS + cursor, CSR_SYSBTNMAP, &systemButtons)) { + return GHOST_kButtonMaskNone; + } + + if (physicalButton >= numButtons) { + return GHOST_kButtonMaskNone; + } + + BYTE lb = logicalButtons[physicalButton]; + + if (lb >= numButtons) { + return GHOST_kButtonMaskNone; + } + + switch (systemButtons[lb]) { + case SBN_LCLICK: + return GHOST_kButtonMaskLeft; + case SBN_RCLICK: + return GHOST_kButtonMaskRight; + case SBN_MCLICK: + return GHOST_kButtonMaskMiddle; + default: + return GHOST_kButtonMaskNone; + } +} + +void GHOST_Wintab::mapWintabToSysCoordinates(int x_in, int y_in, int &x_out, int &y_out) +{ + /* Maps from range [in.org, in.org + abs(in.ext)] to [out.org, out.org + abs(out.ext)], in + * reverse if in.ext and out.ext have differing sign. */ + auto remap = [](int inPoint, Range in, Range out) -> int { + int absInExt = abs(in.ext); + int absOutExt = abs(out.ext); + + /* Translate input from range [in.org, in.org + absInExt] to [0, absInExt] */ + int inMagnitude = inPoint - in.org; + + /* If signs of extents differ, reverse input over range. */ + if ((in.ext < 0) != (out.ext < 0)) { + inMagnitude = absInExt - inMagnitude; + } + + /* Scale from [0, absInExt] to [0, absOutExt]. */ + int outMagnitude = inMagnitude * absOutExt / absInExt; + + /* Translate from range [0, absOutExt] to [out.org, out.org + absOutExt]. */ + int outPoint = outMagnitude + out.org; + + return outPoint; + }; + + x_out = remap(x_in, m_tabletCoord.x, m_systemCoord.x); + y_out = remap(y_in, m_tabletCoord.y, m_systemCoord.y); +} + +bool GHOST_Wintab::trustCoordinates() +{ + return m_coordTrusted; +} + +bool GHOST_Wintab::testCoordinates(int sysX, int sysY, int wtX, int wtY) +{ + mapWintabToSysCoordinates(wtX, wtY, wtX, wtY); + + /* Allow off by one pixel tolerance in case of rounding error. */ + if (abs(sysX - wtX) <= 1 && abs(sysY - wtY) <= 1) { + m_coordTrusted = true; + return true; + } + else { + m_coordTrusted = false; + return false; + } +} diff --git a/intern/ghost/intern/GHOST_Wintab.h b/intern/ghost/intern/GHOST_Wintab.h new file mode 100644 index 00000000000..7193049362d --- /dev/null +++ b/intern/ghost/intern/GHOST_Wintab.h @@ -0,0 +1,250 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** \file + * \ingroup GHOST + * Declaration of GHOST_WintabWin32 class. + */ + +/* Wacom's Wintab documentation is periodically offline, moved, and increasingly hidden away. You + * can find a (painstakingly) archived copy of the documentation at + * https://web.archive.org/web/20201122230125/https://developer-docs-legacy.wacom.com/display/DevDocs/Windows+Wintab+Documentation + */ + +#pragma once + +#include +#include +#include + +#include "GHOST_Types.h" + +#include +/* PACKETDATA and PACKETMODE modify structs in pktdef.h, so make sure they come first. */ +#define PACKETDATA \ + (PK_BUTTONS | PK_NORMAL_PRESSURE | PK_ORIENTATION | PK_CURSOR | PK_X | PK_Y | PK_TIME) +#define PACKETMODE 0 +#include + +/* Typedefs for Wintab functions to allow dynamic loading. */ +typedef UINT(API *GHOST_WIN32_WTInfo)(UINT, UINT, LPVOID); +typedef BOOL(API *GHOST_WIN32_WTGet)(HCTX, LPLOGCONTEXTA); +typedef BOOL(API *GHOST_WIN32_WTSet)(HCTX, LPLOGCONTEXTA); +typedef HCTX(API *GHOST_WIN32_WTOpen)(HWND, LPLOGCONTEXTA, BOOL); +typedef BOOL(API *GHOST_WIN32_WTClose)(HCTX); +typedef int(API *GHOST_WIN32_WTPacketsGet)(HCTX, int, LPVOID); +typedef int(API *GHOST_WIN32_WTQueueSizeGet)(HCTX); +typedef BOOL(API *GHOST_WIN32_WTQueueSizeSet)(HCTX, int); +typedef BOOL(API *GHOST_WIN32_WTEnable)(HCTX, BOOL); +typedef BOOL(API *GHOST_WIN32_WTOverlap)(HCTX, BOOL); + +/* Typedefs for Wintab and Windows resource management. */ +typedef std::unique_ptr, decltype(&::FreeLibrary)> unique_hmodule; +typedef std::unique_ptr, GHOST_WIN32_WTClose> unique_hctx; + +struct GHOST_WintabInfoWin32 { + GHOST_TInt32 x, y; + GHOST_TEventType type; + GHOST_TButtonMask button; + GHOST_TUns64 time; + GHOST_TabletData tabletData; +}; + +class GHOST_Wintab { + public: + /** + * Loads Wintab if available. + * \param hwnd: Window to attach Wintab context to. + */ + static GHOST_Wintab *loadWintab(HWND hwnd); + + /** + * Enables Wintab context. + */ + void enable(); + + /** + * Disables the Wintab context and unwinds Wintab state. + */ + void disable(); + + /** + * Brings Wintab context to the top of the overlap order. + */ + void gainFocus(); + + /** + * Puts Wintab context at bottom of overlap order and unwinds Wintab state. + */ + void loseFocus(); + + /** + * Clean up when Wintab leaves tracking range. + */ + void leaveRange(); + + /** + * Handle Wintab coordinate changes when DisplayChange events occur. + */ + void remapCoordinates(); + + /** + * Maps Wintab to Win32 display coordinates. + * \param x_in: The tablet x coordinate. + * \param y_in: The tablet y coordinate. + * \param x_out: Output for the Win32 mapped x coordinate. + * \param y_out: Output for the Win32 mapped y coordiante. + */ + void mapWintabToSysCoordinates(int x_in, int y_in, int &x_out, int &y_out); + + /** + * Updates cached Wintab properties for current cursor. + */ + void updateCursorInfo(); + + /** + * Handle Wintab info changes such as change in number of connected tablets. + * \param lParam: LPARAM of the event. + */ + void processInfoChange(LPARAM lParam); + + /** + * Whether Wintab devices are present. + * \return True if Wintab devices are present. + */ + bool devicesPresent(); + + /** + * Translate Wintab packets into GHOST_WintabInfoWin32 structs. + * \param outWintabInfo: Storage to return resulting GHOST_WintabInfoWin32 data. + */ + void getInput(std::vector &outWintabInfo); + + /** + * Whether Wintab coordinates should be trusted. + * \return True if Wintab coordinates should be trusted. + */ + bool trustCoordinates(); + + /** + * Tests whether Wintab coordinates can be trusted by comparing Win32 and Wintab reported curser + * position. + * \param sysX: System cursor x position. + * \param sysY: System cursor y position. + * \param wtX: Wintab cursor x position. + * \param wtY: Wintab cursor y position. + * \return True if Win32 and Wintab cursor positions match within tolerance. + * + * Note: Only test coordiantes on button press, not release. This prevents issues when async + * mismatch causes mouse movement to replay and snap back, which is only an issue while drawing. + */ + bool testCoordinates(int sysX, int sysY, int wtX, int wtY); + + /** + * Retrieve the most recent tablet data, or none if pen is not in range. + * \return Most recent tablet data, or none if pen is not in range. + */ + GHOST_TabletData getLastTabletData(); + + private: + /** Wintab dll handle. */ + unique_hmodule m_handle; + /** Wintab API functions. */ + GHOST_WIN32_WTInfo m_fpInfo = nullptr; + GHOST_WIN32_WTGet m_fpGet = nullptr; + GHOST_WIN32_WTSet m_fpSet = nullptr; + GHOST_WIN32_WTPacketsGet m_fpPacketsGet = nullptr; + GHOST_WIN32_WTEnable m_fpEnable = nullptr; + GHOST_WIN32_WTOverlap m_fpOverlap = nullptr; + + /** Stores the Wintab tablet context. */ + unique_hctx m_context; + /** Whether the context is enabled. */ + bool m_enabled = false; + /** Whether the context has focus and is at the top of overlap order. */ + bool m_focused = false; + + /** Pressed button map. */ + GHOST_TUns8 m_buttons = 0; + + /** Range of a coodinate space. */ + struct Range { + /** Origin of range. */ + int org = 0; + /** Extent of range. */ + int ext = 1; + }; + + /** 2D Coordinate space. */ + struct Coord { + /** Range of x. */ + Range x = {}; + /** Range of y. */ + Range y = {}; + }; + /** Whether Wintab coordinates are trusted. */ + bool m_coordTrusted = false; + /** Tablet input range. */ + Coord m_tabletCoord = {}; + /** System output range. */ + Coord m_systemCoord = {}; + + int m_maxPressure = 0; + int m_maxAzimuth = 0; + int m_maxAltitude = 0; + + /** Number of connected Wintab devices. */ + UINT m_numDevices = 0; + /** Reusable buffer to read in Wintab packets. */ + std::vector m_pkts; + /** Most recently received tablet data, or none if pen is not in range. */ + GHOST_TabletData m_lastTabletData = GHOST_TABLET_DATA_NONE; + + GHOST_Wintab(HWND hwnd, + unique_hmodule handle, + GHOST_WIN32_WTInfo info, + GHOST_WIN32_WTGet get, + GHOST_WIN32_WTSet set, + GHOST_WIN32_WTPacketsGet packetsGet, + GHOST_WIN32_WTEnable enable, + GHOST_WIN32_WTOverlap overlap, + unique_hctx hctx, + Coord tablet, + Coord system, + int queueSize); + + /** + * Convert Wintab system mapped (mouse) buttons into Ghost button mask. + * \param cursor: The Wintab cursor associated to the button. + * \param physicalButton: The physical button ID to inspect. + * \return The system mapped button. + */ + GHOST_TButtonMask mapWintabToGhostButton(UINT cursor, WORD physicalButton); + + /** + * Applies common modifications to Wintab context. + * \param lc: Wintab context to modify. + */ + static void modifyContext(LOGCONTEXT &lc); + + /** + * Extracts tablet and system coordinates from Wintab context. + * \param lc: Wintab context to extract coordinates from. + * \param tablet: Tablet coordinates. + * \param system: System coordinates. + */ + static void extractCoordinates(LOGCONTEXT &lc, Coord &tablet, Coord &system); +}; -- cgit v1.2.3