Changeset View
Changeset View
Standalone View
Standalone View
intern/ghost/intern/GHOST_Wintab.cpp
- This file was added.
| /* | |||||
| * 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.org[0] = lc.lcInOrgX; | |||||
| tablet.org[1] = lc.lcInOrgY; | |||||
| tablet.ext[0] = lc.lcInExtX; | |||||
| tablet.ext[1] = lc.lcInExtY; | |||||
| system.org[0] = lc.lcSysOrgX; | |||||
| system.org[1] = lc.lcSysOrgY; | |||||
| system.ext[0] = lc.lcSysExtX; | |||||
| /* Wintab maps y origin to the tablet's bottom; invert y to match Windows y origin mapping to the | |||||
| * screen top. */ | |||||
| system.ext[1] = -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; | |||||
| } | |||||
| GHOST_TabletData GHOST_Wintab::getLastTabletData() | |||||
| { | |||||
| return m_lastTabletData; | |||||
| } | |||||
| void GHOST_Wintab::getInput(std::vector<GHOST_WintabInfoWin32> &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(LONG x_in, LONG y_in, int &x_out, int &y_out) | |||||
| { | |||||
| /* If sign(InExtent) == sign(OutExtent) | |||||
| * Out = (In - OutOrgin) * abs(OutExtent) / abs(InExtent) + OutOrgin | |||||
LazyDodo: There's a lot going on here, and this comment is only moderately more readable than the code… | |||||
| * Else | |||||
Done Inline Actionsif i'm reading the code right, is this what is happening here? double x_normalized = ((double)x_in) - tab.org[0]) / tab.ext[0]; double y_normalized = ((double)y_in) - tab.org[1]) / tab.ext[1]; x_out = (int) ((x_normalized * sys.ext[0]) + sys.org[0]); y_out = (int) ((y_normalized * sys.ext[1]) + sys.org[1]); LazyDodo: if i'm reading the code right, is this what is happening here?
```
double x_normalized =… | |||||
Done Inline ActionsEssentially yes, the else case being similar except it moves the origin to origin+extent and maps in reverse over the same range. Reversing over the range initially tripped me up so I had some resistance to straying much from the documentation. I opt to compute in integers given that's how the documentation presents it, see comments under this section, and that I couldn't convince myself it would not reduce rounding error. If you wanted to break it into it's parts while staying in integers it would go:
nicholas_rishel: Essentially yes, the else case being similar except it moves the origin to origin+extent and… | |||||
| * Map over the same range [origin, origin+exent] in reverse. | |||||
| * Out = (abs(InExtent) - (In - OutOrgin)) * abs(OutExtent) / abs(InExtent) + OutOrgin | |||||
| */ | |||||
| Coord &tab = m_tabletCoord, &sys = m_systemCoord; | |||||
| if ((tab.ext[0] < 0) == (sys.ext[0] < 0)) { | |||||
| x_out = (x_in - tab.org[0]) * abs(sys.ext[0]) / abs(tab.ext[0]) + sys.org[0]; | |||||
| } | |||||
| else { | |||||
| x_out = (abs(tab.ext[0]) - (x_in - tab.org[0])) * abs(sys.ext[0]) / abs(tab.ext[0]) + | |||||
| sys.org[0]; | |||||
| } | |||||
| if ((tab.ext[1] < 0) == (sys.ext[1] < 0)) { | |||||
| y_out = (y_in - tab.org[1]) * abs(sys.ext[1]) / abs(tab.ext[1]) + sys.org[1]; | |||||
| } | |||||
| else { | |||||
| y_out = (abs(tab.ext[1]) - (y_in - tab.org[1])) * abs(sys.ext[1]) / abs(tab.ext[1]) + | |||||
| sys.org[1]; | |||||
| } | |||||
| } | |||||
| 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; | |||||
Done Inline ActionsRemove debug print? brecht: Remove debug print? | |||||
| return false; | |||||
| } | |||||
| } | |||||
There's a lot going on here, and this comment is only moderately more readable than the code down below
Given there seems to be a fair bit of repetition as well is there any chance this could be decomposed further in perhaps some functions with a more descriptive names?