diff --git a/libs/ui/input/wintab/kis_tablet_support_win.cpp b/libs/ui/input/wintab/kis_tablet_support_win.cpp index 83bd63981c..f977bdfa5e 100644 --- a/libs/ui/input/wintab/kis_tablet_support_win.cpp +++ b/libs/ui/input/wintab/kis_tablet_support_win.cpp @@ -1,1001 +1,990 @@ /* * Copyright (c) 2013 Digia Plc and/or its subsidiary(-ies). * Copyright (c) 2013 Boudewijn Rempt * Copyright (c) 2013 Dmitry Kazakov * Copyright (c) 2015 Michael Abrahams * Copyright (c) 2015 The Qt Company Ltd. * * 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. */ #include "kis_tablet_support_win_p.h" #include "kis_tablet_support_win.h" #include -#include #include #include #include -#include // #include // #include // #include #include #include #include #include #include #define Q_PI M_PI #include #include // For "inline tool switches" #include #include #include "kis_screen_size_choice_dialog.h" // NOTE: we stub out qwindowcontext.cpp::347 to disable Qt's own tablet support. // Note: The definition of the PACKET structure in pktdef.h depends on this define. #define PACKETDATA (PK_X | PK_Y | PK_BUTTONS | PK_TIME | PK_NORMAL_PRESSURE | PK_TANGENT_PRESSURE | PK_ORIENTATION | PK_CURSOR | PK_Z) #include "pktdef.h" QT_BEGIN_NAMESPACE enum { PacketMode = 0, TabletPacketQSize = 128, DeviceIdMask = 0xFF6, // device type mask && device color mask CursorTypeBitMask = 0x0F06 // bitmask to find the specific cursor type (see Wacom FAQ) }; /* * * Krita extensions begin here * * */ QWindowsTabletSupport *QTAB = 0; static QPointer targetWindow = 0; //< Window receiving last tablet event static QPointer qt_tablet_target = 0; //< Widget receiving last tablet event static bool dialogOpen = false; //< KisTabletSupportWin is not a Q_OBJECT and can't accept dialog signals HWND createDummyWindow(const QString &className, const wchar_t *windowName, WNDPROC wndProc) { if (!wndProc) wndProc = DefWindowProc; WNDCLASSEX wc; wc.cbSize = sizeof(WNDCLASSEX); wc.style = 0; wc.lpfnWndProc = wndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = (HINSTANCE)GetModuleHandle(0); wc.hCursor = 0; wc.hbrBackground = GetSysColorBrush(COLOR_WINDOW); wc.hIcon = 0; wc.hIconSm = 0; wc.lpszMenuName = 0; wc.lpszClassName = (wchar_t*)className.utf16(); ATOM atom = RegisterClassEx(&wc); if (!atom) qErrnoWarning("Registering tablet fake window class failed."); return CreateWindowEx(0, (wchar_t*)className.utf16(), windowName, WS_OVERLAPPED, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HWND_MESSAGE, NULL, (HINSTANCE)GetModuleHandle(0), NULL); } void printContext(const LOGCONTEXT &lc) { - QByteArray ba; - QBuffer buf(&ba); - buf.open(QBuffer::WriteOnly); - - buf << "\nTablet context data:\n "; - buf << ppVar(lc.lcName) << "\n "; - buf << ppVar(lc.lcDevice) << "\n "; - buf << ppVar(lc.lcInOrgX) << "\n "; - buf << ppVar(lc.lcInOrgY) << "\n "; - buf << ppVar(lc.lcInExtX) << "\n "; - buf << ppVar(lc.lcInExtY) << "\n "; - buf << ppVar(lc.lcOutOrgX) << "\n "; - buf << ppVar(lc.lcOutOrgY) << "\n "; - buf << ppVar(lc.lcOutExtX) << "\n "; - buf << ppVar(lc.lcOutExtY) << "\n "; - buf << ppVar(lc.lcSysOrgX) << "\n "; - buf << ppVar(lc.lcSysOrgY) << "\n "; - buf << ppVar(lc.lcSysExtX) << "\n "; - buf << ppVar(lc.lcSysExtY) << "\n "; - - buf << "Qt Desktop Geometry" << QApplication::desktop()->geometry() << "\n "; - - buf.close(); - - dbgTablet << buf; - KisUsageLogger::write(QString::fromUtf8(ba)); + dbgTablet << "# Getting current context data:"; + dbgTablet << ppVar(lc.lcName); + dbgTablet << ppVar(lc.lcDevice); + dbgTablet << ppVar(lc.lcInOrgX); + dbgTablet << ppVar(lc.lcInOrgY); + dbgTablet << ppVar(lc.lcInExtX); + dbgTablet << ppVar(lc.lcInExtY); + dbgTablet << ppVar(lc.lcOutOrgX); + dbgTablet << ppVar(lc.lcOutOrgY); + dbgTablet << ppVar(lc.lcOutExtX); + dbgTablet << ppVar(lc.lcOutExtY); + dbgTablet << ppVar(lc.lcSysOrgX); + dbgTablet << ppVar(lc.lcSysOrgY); + dbgTablet << ppVar(lc.lcSysExtX); + dbgTablet << ppVar(lc.lcSysExtY); + + dbgTablet << "Qt Desktop Geometry" << QApplication::desktop()->geometry(); } static QRect mapToNative(const QRect &qRect, int m_factor) { return QRect(qRect.x() * m_factor, qRect.y() * m_factor, qRect.width() * m_factor, qRect.height() * m_factor); } static inline QEvent::Type mouseEventType(QEvent::Type t) { return (t == QEvent::TabletMove ? QEvent::MouseMove : t == QEvent::TabletPress ? QEvent::MouseButtonPress : t == QEvent::TabletRelease ? QEvent::MouseButtonRelease : QEvent::None); } static inline bool isMouseEventType(QEvent::Type t) { return (t == QEvent::MouseMove || t == QEvent::MouseButtonPress || t == QEvent::MouseButtonRelease); } static QPoint mousePosition() { POINT p; GetCursorPos(&p); return QPoint(p.x, p.y); } QWindowsWinTab32DLL QWindowsTabletSupport::m_winTab32DLL; bool KisTabletSupportWin::init() { if (!QWindowsTabletSupport::m_winTab32DLL.init()) { qWarning() << "Failed to initialize Wintab"; return false; } QTAB = QWindowsTabletSupport::create(); // Refresh tablet context after tablet rotated, screen added, etc. QObject::connect(qApp->primaryScreen(), &QScreen::geometryChanged, [=](const QRect & geometry){ delete QTAB; QTAB = QWindowsTabletSupport::create(); }); return true; } // Derived from qwidgetwindow. // // The work done by processTabletEvent from qguiapplicationprivate is divided // between here and translateTabletPacketEvent. static void handleTabletEvent(QWidget *windowWidget, const QPointF &local, const QPointF &global, int device, int pointerType, Qt::MouseButton button, Qt::MouseButtons buttons, qreal pressure,int xTilt, int yTilt, qreal tangentialPressure, qreal rotation, int z, qint64 uniqueId, Qt::KeyboardModifiers modifiers, QEvent::Type type, LONG time) { // Lock in target window if (type == QEvent::TabletPress) { targetWindow = windowWidget; dbgInput << "Locking target window" << targetWindow; } else if ((type == QEvent::TabletRelease || buttons == Qt::NoButton) && (targetWindow != 0)) { dbgInput << "Releasing target window" << targetWindow; targetWindow = 0; } if (!windowWidget) // Should never happen return; // We do this instead of constructing the event e beforehand const QPoint localPos = local.toPoint(); const QPoint globalPos = global.toPoint(); if (type == QEvent::TabletPress) { QWidget *widget = windowWidget->childAt(localPos); if (!widget) widget = windowWidget; qt_tablet_target = widget; } QWidget *finalDestination = qt_tablet_target; if (!finalDestination) { finalDestination = windowWidget->childAt(localPos); } if ((type == QEvent::TabletRelease || buttons == Qt::NoButton) && (qt_tablet_target != 0)) { dbgInput << "releasing tablet target" << qt_tablet_target; qt_tablet_target = 0; } if (finalDestination) { // The event was specified relative to windowWidget, so we remap it QPointF delta = global - globalPos; QPointF mapped = finalDestination->mapFromGlobal(global.toPoint()) + delta; QTabletEvent ev(type, mapped, global, device, pointerType, pressure, xTilt, yTilt, tangentialPressure, rotation, z, modifiers, uniqueId, button, buttons); ev.setTimestamp(time); QGuiApplication::sendEvent(finalDestination, &ev); if (ev.isAccepted()) { // dbgTablet << "Tablet event" << type << "accepted" << "by target widget" << finalDestination; } else { // Turn off eventEater send a synthetic mouse event. // dbgTablet << "Tablet event" << type << "rejected; sending mouse event to" << finalDestination; qt_tablet_target = 0; // We shouldn't ever get a widget accepting a tablet event from this // call, so we won't worry about any interactions with our own // widget-locking code. // QWindow *target = platformScreen->topLevelAt(globalPos); // if (!target) return; // QPointF windowLocal = global - QPointF(target->mapFromGlobal(QPoint())) + delta; // QWindowSystemInterface::handleTabletEvent(target, ev.timestamp(), windowLocal, // global, device, pointerType, // buttons, pressure, xTilt, yTilt, // tangentialPressure, rotation, z, // uniqueId, modifiers); } } else { qt_tablet_target = 0; targetWindow = 0; } } /** * This is a default implementation of a class for converting the * WinTab value of the buttons pressed to the Qt buttons. This class * may be substituted from the UI. */ struct DefaultButtonsConverter { void convert(DWORD btnOld, DWORD btnNew, Qt::MouseButton *button, Qt::MouseButtons *buttons, const QWindowsTabletDeviceData &tdd) { int pressedButtonValue = btnNew ^ btnOld; *button = buttonValueToEnum(pressedButtonValue, tdd); *buttons = Qt::NoButton; for (int i = 0; i < 3; i++) { int btn = 0x1 << i; if (btn & btnNew) { Qt::MouseButton convertedButton = buttonValueToEnum(btn, tdd); *buttons |= convertedButton; /** * If a button that is present in hardware input is * mapped to a Qt::NoButton, it means that it is going * to be eaten by the driver, for example by its * "Pan/Scroll" feature. Therefore we shouldn't handle * any of the events associated to it. So just return * Qt::NoButton here. */ if (convertedButton == Qt::NoButton) { /** * Sometimes the driver-handled shortcuts are just * keyboard modifiers, so ideally we should handle * them as well. The problem is that we cannot * know if the shortcut was a pan/zoom action or a * shortcut. So here we use a "hackish" approach. * We just check if any modifier has been pressed * and, if so, pass the button to Krita. Of * course, if the driver uses some really complex * shortcuts like "Shift + stylus btn" to generate * some recorded shortcut, it will not work. But I * guess it will be ok for the most of the * usecases. * * WARNING: this hack will *not* work if you bind * any non-modifier key to the stylus * button, e.g. Space. */ const Qt::KeyboardModifiers keyboardModifiers = QApplication::queryKeyboardModifiers(); if (KisTabletDebugger::instance()->shouldEatDriverShortcuts() || keyboardModifiers == Qt::NoModifier) { *button = Qt::NoButton; *buttons = Qt::NoButton; break; } } } } } private: Qt::MouseButton buttonValueToEnum(DWORD button, const QWindowsTabletDeviceData &tdd) { const int leftButtonValue = 0x1; const int middleButtonValue = 0x2; const int rightButtonValue = 0x4; const int doubleClickButtonValue = 0x7; button = tdd.buttonsMap.value(button); return button == leftButtonValue ? Qt::LeftButton : button == rightButtonValue ? Qt::RightButton : button == doubleClickButtonValue ? Qt::MiddleButton : button == middleButtonValue ? Qt::MiddleButton : button ? Qt::LeftButton /* fallback item */ : Qt::NoButton; } }; static DefaultButtonsConverter *globalButtonsConverter = new DefaultButtonsConverter(); /* * * Krita extensions end here * * */ // This is the WndProc for a single additional hidden window used to collect tablet events. extern "C" LRESULT QT_WIN_CALLBACK qWindowsTabletSupportWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WT_PROXIMITY: if (QTAB->translateTabletProximityEvent(wParam, lParam)) return 0; break; case WT_PACKET: if (QTAB->translateTabletPacketEvent()) return 0; break; } return DefWindowProc(hwnd, message, wParam, lParam); } // Scale tablet coordinates to screen coordinates. static inline int sign(int x) { return x >= 0 ? 1 : -1; } inline QPointF QWindowsTabletDeviceData::scaleCoordinates(int coordX, int coordY, const QRect &targetArea) const { const int targetX = targetArea.x(); const int targetY = targetArea.y(); const int targetWidth = targetArea.width(); const int targetHeight = targetArea.height(); const qreal x = sign(targetWidth) == sign(maxX) ? ((coordX - minX) * qAbs(targetWidth) / qAbs(qreal(maxX - minX))) + targetX : ((qAbs(maxX) - (coordX - minX)) * qAbs(targetWidth) / qAbs(qreal(maxX - minX))) + targetX; const qreal y = sign(targetHeight) == sign(maxY) ? ((coordY - minY) * qAbs(targetHeight) / qAbs(qreal(maxY - minY))) + targetY : ((qAbs(maxY) - (coordY - minY)) * qAbs(targetHeight) / qAbs(qreal(maxY - minY))) + targetY; return QPointF(x, y); } /*! \class QWindowsWinTab32DLL \brief Functions from wintabl32.dll shipped with WACOM tablets used by QWindowsTabletSupport. \internal \ingroup qt-lighthouse-win */ bool QWindowsWinTab32DLL::init() { if (wTInfo) return true; QLibrary library(QStringLiteral("wintab32")); if (!library.load()) { qWarning() << QString("Could not load wintab32 dll: %1").arg(library.errorString()); return false; } wTOpen = (PtrWTOpen) library.resolve("WTOpenW"); wTClose = (PtrWTClose) library.resolve("WTClose"); wTInfo = (PtrWTInfo) library.resolve("WTInfoW"); wTEnable = (PtrWTEnable) library.resolve("WTEnable"); wTOverlap = (PtrWTOverlap) library.resolve("WTOverlap"); wTPacketsGet = (PtrWTPacketsGet) library.resolve("WTPacketsGet"); wTGet = (PtrWTGet) library.resolve("WTGetW"); wTQueueSizeGet = (PtrWTQueueSizeGet) library.resolve("WTQueueSizeGet"); wTQueueSizeSet = (PtrWTQueueSizeSet) library.resolve("WTQueueSizeSet"); if (wTOpen && wTClose && wTInfo && wTEnable && wTOverlap && wTPacketsGet && wTQueueSizeGet && wTQueueSizeSet) { return true; } qWarning() << "Could not resolve the following symbols:\n" << "\t wTOpen" << wTOpen << "\n" << "\t wtClose" << wTClose << "\n" << "\t wtInfo" << wTInfo << "\n" << "\t wTEnable" << wTEnable << "\n" << "\t wTOverlap" << wTOverlap << "\n" << "\t wTPacketsGet" << wTPacketsGet << "\n" << "\t wTQueueSizeGet" << wTQueueSizeGet << "\n" << "\t wTQueueSizeSet" << wTQueueSizeSet << "\n"; return false; } /*! \class QWindowsTabletSupport \brief Tablet support for Windows. Support for WACOM tablets. \sa http://www.wacomeng.com/windows/docs/Wintab_v140.htm \internal \since 5.2 \ingroup qt-lighthouse-win */ QWindowsTabletSupport::QWindowsTabletSupport(HWND window, HCTX context) : m_window(window) , m_context(context) , m_absoluteRange(20) , m_tiltSupport(false) , m_currentDevice(-1) { AXIS orientation[3]; // Some tablets don't support tilt, check if it is possible, if (QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_DEVICES, DVC_ORIENTATION, &orientation)) m_tiltSupport = orientation[0].axResolution && orientation[1].axResolution; } QWindowsTabletSupport::~QWindowsTabletSupport() { QWindowsTabletSupport::m_winTab32DLL.wTClose(m_context); DestroyWindow(m_window); } QWindowsTabletSupport *QWindowsTabletSupport::create() { const HWND window = createDummyWindow(QStringLiteral("TabletDummyWindow"), L"TabletDummyWindow", qWindowsTabletSupportWndProc); LOGCONTEXT lcMine; // build our context from the default context QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_DEFSYSCTX, 0, &lcMine); // Go for the raw coordinates, the tablet event will return good stuff. The // defaults for lcOut rectangle are the desktop dimensions in pixels, which // means Wintab will do lossy rounding. Instead we specify this trivial // scaling for the output context, then do the scaling ourselves later to // obtain higher resolution coordinates. lcMine.lcOptions |= CXO_MESSAGES | CXO_CSRMESSAGES; lcMine.lcPktData = lcMine.lcMoveMask = PACKETDATA; lcMine.lcPktMode = PacketMode; lcMine.lcOutOrgX = 0; lcMine.lcOutExtX = lcMine.lcInExtX; lcMine.lcOutOrgY = 0; lcMine.lcOutExtY = -lcMine.lcInExtY; const HCTX context = QWindowsTabletSupport::m_winTab32DLL.wTOpen(window, &lcMine, true); if (!context) { dbgTablet << __FUNCTION__ << "Unable to open tablet."; DestroyWindow(window); return 0; } // Set the size of the Packet Queue to the correct size const int currentQueueSize = QWindowsTabletSupport::m_winTab32DLL.wTQueueSizeGet(context); if (currentQueueSize != TabletPacketQSize) { if (!QWindowsTabletSupport::m_winTab32DLL.wTQueueSizeSet(context, TabletPacketQSize)) { if (!QWindowsTabletSupport::m_winTab32DLL.wTQueueSizeSet(context, currentQueueSize)) { qWarning() << "Unable to set queue size on tablet. The tablet will not work."; QWindowsTabletSupport::m_winTab32DLL.wTClose(context); DestroyWindow(window); return 0; } // cannot restore old size } // cannot set } // mismatch dbgTablet << "Opened tablet context " << context << " on window " << window << "changed packet queue size " << currentQueueSize << "->" << TabletPacketQSize; return new QWindowsTabletSupport(window, context); } unsigned QWindowsTabletSupport::options() const { UINT result = 0; m_winTab32DLL.wTInfo(WTI_INTERFACE, IFC_CTXOPTIONS, &result); return result; } QString QWindowsTabletSupport::description() const { const unsigned size = m_winTab32DLL.wTInfo(WTI_INTERFACE, IFC_WINTABID, 0); if (!size) return QString(); QVarLengthArray winTabId(size + 1); m_winTab32DLL.wTInfo(WTI_INTERFACE, IFC_WINTABID, winTabId.data()); WORD implementationVersion = 0; m_winTab32DLL.wTInfo(WTI_INTERFACE, IFC_IMPLVERSION, &implementationVersion); WORD specificationVersion = 0; m_winTab32DLL.wTInfo(WTI_INTERFACE, IFC_SPECVERSION, &specificationVersion); const unsigned opts = options(); QString result = QString::fromLatin1("%1 specification: v%2.%3 implementation: v%4.%5 options: 0x%6") .arg(QString::fromWCharArray(winTabId.data())) .arg(specificationVersion >> 8).arg(specificationVersion & 0xFF) .arg(implementationVersion >> 8).arg(implementationVersion & 0xFF) .arg(opts, 0, 16); if (opts & CXO_MESSAGES) result += QStringLiteral(" CXO_MESSAGES"); if (opts & CXO_CSRMESSAGES) result += QStringLiteral(" CXO_CSRMESSAGES"); if (m_tiltSupport) result += QStringLiteral(" tilt"); return result; } void QWindowsTabletSupport::notifyActivate() { // Cooperate with other tablet applications, but when we get focus, I want to use the tablet. const bool result = QWindowsTabletSupport::m_winTab32DLL.wTEnable(m_context, true) && QWindowsTabletSupport::m_winTab32DLL.wTOverlap(m_context, true); dbgTablet << __FUNCTION__ << result; } static inline int indexOfDevice(const QVector &devices, qint64 uniqueId) { for (int i = 0; i < devices.size(); ++i) if (devices.at(i).uniqueId == uniqueId) return i; return -1; } static inline QTabletEvent::TabletDevice deviceType(const UINT cursorType) { if (((cursorType & 0x0006) == 0x0002) && ((cursorType & CursorTypeBitMask) != 0x0902)) return QTabletEvent::Stylus; if (cursorType == 0x4020) // Surface Pro 2 tablet device return QTabletEvent::Stylus; switch (cursorType & CursorTypeBitMask) { case 0x0802: return QTabletEvent::Stylus; case 0x0902: return QTabletEvent::Airbrush; case 0x0004: return QTabletEvent::FourDMouse; case 0x0006: return QTabletEvent::Puck; case 0x0804: return QTabletEvent::RotationStylus; default: break; } return QTabletEvent::NoDevice; }; static inline QTabletEvent::PointerType pointerType(unsigned pkCursor) { switch (pkCursor % 3) { // %3 for dual track case 0: return QTabletEvent::Cursor; case 1: return QTabletEvent::Pen; case 2: return QTabletEvent::Eraser; default: break; } return QTabletEvent::UnknownPointer; } QDebug operator<<(QDebug d, const QWindowsTabletDeviceData &t) { d << "TabletDevice id:" << t.uniqueId << " pressure: " << t.minPressure << ".." << t.maxPressure << " tan pressure: " << t.minTanPressure << ".." << t.maxTanPressure << " area:" << t.minX << t.minY << t.minZ << ".." << t.maxX << t.maxY << t.maxZ << " device " << t.currentDevice << " pointer " << t.currentPointerType; return d; } QWindowsTabletDeviceData QWindowsTabletSupport::tabletInit(const quint64 uniqueId, const UINT cursorType) const { QWindowsTabletDeviceData result; result.uniqueId = uniqueId; /* browse WinTab's many info items to discover pressure handling. */ AXIS axis; LOGCONTEXT lc; /* get the current context for its device variable. */ QWindowsTabletSupport::m_winTab32DLL.wTGet(m_context, &lc); if (KisTabletDebugger::instance()->initializationDebugEnabled()) { printContext(lc); } /* get the size of the pressure axis. */ QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_DEVICES + lc.lcDevice, DVC_NPRESSURE, &axis); result.minPressure = int(axis.axMin); result.maxPressure = int(axis.axMax); QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_DEVICES + lc.lcDevice, DVC_TPRESSURE, &axis); result.minTanPressure = int(axis.axMin); result.maxTanPressure = int(axis.axMax); result.minX = int(lc.lcOutOrgX); result.maxX = int(lc.lcOutExtX) + int(lc.lcOutOrgX); result.minY = int(lc.lcOutOrgY); result.maxY = -int(lc.lcOutExtY) + int(lc.lcOutOrgY); // These are set to 0 when we opened the tablet context in QWindowsTabletSupport::create KIS_SAFE_ASSERT_RECOVER_NOOP(lc.lcOutOrgX == 0); KIS_SAFE_ASSERT_RECOVER_NOOP(lc.lcOutOrgY == 0); result.maxZ = int(lc.lcOutExtZ) - int(lc.lcOutOrgZ); result.currentDevice = deviceType(cursorType); // Define a rectangle representing the whole screen as seen by Wintab. QRect qtDesktopRect = QApplication::desktop()->geometry(); QRect wintabDesktopRect(lc.lcSysOrgX, lc.lcSysOrgY, lc.lcSysExtX, lc.lcSysExtY); qDebug() << ppVar(qtDesktopRect); qDebug() << ppVar(wintabDesktopRect); // Show screen choice dialog if (!dialogOpen) { KisScreenSizeChoiceDialog dlg(0, wintabDesktopRect, qtDesktopRect); KisExtendedModifiersMapper mapper; KisExtendedModifiersMapper::ExtendedModifiers modifiers = mapper.queryExtendedModifiers(); if (modifiers.contains(Qt::Key_Shift) || (!dlg.canUseDefaultSettings() && qtDesktopRect != wintabDesktopRect)) { dialogOpen = true; dlg.exec(); } result.virtualDesktopArea = dlg.screenRect(); dialogOpen = false; } else { // This branch should've been explicitly prevented. KIS_SAFE_ASSERT_RECOVER_NOOP(!dialogOpen); warnTablet << "Trying to init a WinTab device while screen resolution dialog is active, this should not happen!"; warnTablet << "Tablet coordinates could be wrong as a result."; result.virtualDesktopArea = qtDesktopRect; } return result; }; bool QWindowsTabletSupport::translateTabletProximityEvent(WPARAM /* wParam */, LPARAM lParam) { if (dialogOpen) { // tabletInit(...) may show the screen resolution dialog and is blocking. // During this period, don't process any tablet events at all. dbgTablet << "WinTab screen resolution dialog is active, ignoring WinTab proximity event"; return false; } auto sendProximityEvent = [&](QEvent::Type type) { QPointF emptyPos; qreal zero = 0.0; QTabletEvent e(type, emptyPos, emptyPos, 0, m_devices.at(m_currentDevice).currentPointerType, zero, 0, 0, zero, zero, 0, Qt::NoModifier, m_devices.at(m_currentDevice).uniqueId, Qt::NoButton, (Qt::MouseButtons)0); qApp->sendEvent(qApp, &e); }; if (!LOWORD(lParam)) { // dbgTablet << "leave proximity for device #" << m_currentDevice; sendProximityEvent(QEvent::TabletLeaveProximity); return true; } PACKET proximityBuffer[1]; // we are only interested in the first packet in this case const int totalPacks = QWindowsTabletSupport::m_winTab32DLL.wTPacketsGet(m_context, 1, proximityBuffer); if (!totalPacks) return false; UINT pkCursor = proximityBuffer[0].pkCursor; // initializing and updating the cursor should be done in response to // WT_CSRCHANGE. We do it in WT_PROXIMITY because some wintab never send // the event WT_CSRCHANGE even if asked with CXO_CSRMESSAGES tabletUpdateCursor(pkCursor); // dbgTablet << "enter proximity for device #" << m_currentDevice << m_devices.at(m_currentDevice); sendProximityEvent(QEvent::TabletEnterProximity); return true; } bool QWindowsTabletSupport::translateTabletPacketEvent() { static PACKET localPacketBuf[TabletPacketQSize]; // our own tablet packet queue. const int packetCount = QWindowsTabletSupport::m_winTab32DLL.wTPacketsGet(m_context, TabletPacketQSize, &localPacketBuf); if (!packetCount || m_currentDevice < 0 || dialogOpen) return false; // In contrast to Qt, these will not be "const" during our loop. // This is because the Surface Pro 3 may cause us to switch devices. QWindowsTabletDeviceData tabletData = m_devices.at(m_currentDevice); int currentDevice = tabletData.currentDevice; int currentPointerType = tabletData.currentPointerType; // static Qt::MouseButtons buttons = Qt::NoButton, btnOld, btnChange; static DWORD btnNew, btnOld, btnChange; // The tablet can be used in 2 different modes, depending on its settings: // 1) Absolute (pen) mode: // The coordinates are scaled to the virtual desktop (by default). The user // can also choose to scale to the monitor or a region of the screen. // When entering proximity, the tablet driver snaps the mouse pointer to the // tablet position scaled to that area and keeps it in sync. // 2) Relative (mouse) mode: // The pen follows the mouse. The constant 'absoluteRange' specifies the // manhattanLength difference for detecting if a tablet input device is in this mode, // in which case we snap the position to the mouse position. // It seems there is no way to find out the mode programmatically, the LOGCONTEXT orgX/Y/Ext // area is always the virtual desktop. static qreal dpr = 1.0; auto activeWindow = qApp->activeWindow(); if (activeWindow) { dpr = activeWindow->devicePixelRatio(); } const Qt::KeyboardModifiers keyboardModifiers = QApplication::queryKeyboardModifiers(); for (int i = 0; i < packetCount; ++i) { const PACKET &packet = localPacketBuf[i]; btnOld = btnNew; btnNew = localPacketBuf[i].pkButtons; btnChange = btnOld ^ btnNew; bool buttonPressed = btnChange && btnNew > btnOld; bool buttonReleased = btnChange && btnNew < btnOld; bool anyButtonsStillPressed = btnNew; Qt::MouseButton button = Qt::NoButton; Qt::MouseButtons buttons; globalButtonsConverter->convert(btnOld, btnNew, &button, &buttons, tabletData); QEvent::Type type = QEvent::TabletMove; if (buttonPressed && button != Qt::NoButton) { type = QEvent::TabletPress; } else if (buttonReleased && button != Qt::NoButton) { type = QEvent::TabletRelease; } const int z = currentDevice == QTabletEvent::FourDMouse ? int(packet.pkZ) : 0; // NOTE: we shouldn't postpone the tablet events like Qt does, because we // don't support mouse mode (which was the reason for introducing this // postponing). See bug 363284. QPointF globalPosF = tabletData.scaleCoordinates(packet.pkX, packet.pkY, tabletData.virtualDesktopArea); globalPosF /= dpr; // Convert from "native" to "device independent pixels." QPoint globalPos = globalPosF.toPoint(); // Find top-level window QWidget *w = targetWindow; // If we had a target already, use it. if (!w) { w = qApp->activePopupWidget(); if (!w) w = qApp->activeModalWidget(); if (!w) w = qApp->topLevelAt(globalPos); if (!w) continue; w = w->window(); } const QPoint localPos = w->mapFromGlobal(globalPos); const QPointF delta = globalPosF - globalPos; const QPointF localPosF = globalPosF + QPointF(localPos - globalPos) + delta; const qreal pressureNew = packet.pkButtons && (currentPointerType == QTabletEvent::Pen || currentPointerType == QTabletEvent::Eraser) ? m_devices.at(m_currentDevice).scalePressure(packet.pkNormalPressure) : qreal(0); const qreal tangentialPressure = currentDevice == QTabletEvent::Airbrush ? m_devices.at(m_currentDevice).scaleTangentialPressure(packet.pkTangentPressure) : qreal(0); int tiltX = 0; int tiltY = 0; qreal rotation = 0; if (m_tiltSupport) { // Convert from azimuth and altitude to x tilt and y tilt. What // follows is the optimized version. Here are the equations used: // X = sin(azimuth) * cos(altitude) // Y = cos(azimuth) * cos(altitude) // Z = sin(altitude) // X Tilt = arctan(X / Z) // Y Tilt = arctan(Y / Z) const double radAzim = (packet.pkOrientation.orAzimuth / 10.0) * (M_PI / 180); const double tanAlt = std::tan((std::abs(packet.pkOrientation.orAltitude / 10.0)) * (M_PI / 180)); const double degX = std::atan(std::sin(radAzim) / tanAlt); const double degY = std::atan(std::cos(radAzim) / tanAlt); tiltX = int(degX * (180 / M_PI)); tiltY = int(-degY * (180 / M_PI)); rotation = 360.0 - (packet.pkOrientation.orTwist / 10.0); if (rotation > 180.0) rotation -= 360.0; } // This is adds *a lot* of noise to the output log if (false) { dbgTablet << "Packet #" << (i+1) << '/' << packetCount << "button:" << packet.pkButtons << globalPosF << z << "to:" << w << localPos << "(packet" << packet.pkX << packet.pkY << ") dev:" << currentDevice << "pointer:" << currentPointerType << "P:" << pressureNew << "tilt:" << tiltX << ',' << tiltY << "tanP:" << tangentialPressure << "rotation:" << rotation; } // Reusable helper function. Better than compiler macros! auto sendTabletEvent = [&](QTabletEvent::Type t){ handleTabletEvent(w, localPosF, globalPosF, currentDevice, currentPointerType, button, buttons, pressureNew, tiltX, tiltY, tangentialPressure, rotation, z, m_devices.at(m_currentDevice).uniqueId, keyboardModifiers, t, packet.pkTime); }; /** * Workaround to deal with "inline" tool switches. * These are caused by the eraser trigger button on the Surface Pro 3. * We shoot out a tabletUpdateCursor request and a switchInputDevice request. */ if (isSurfacePro3 && (packet.pkCursor != currentPkCursor)) { dbgTablet << "Got an inline tool switch."; // Send tablet release event. sendTabletEvent(QTabletEvent::TabletRelease); // Read the new cursor info. UINT pkCursor = packet.pkCursor; tabletUpdateCursor(pkCursor); // Update the local loop variables. tabletData = m_devices.at(m_currentDevice); currentDevice = deviceType(tabletData.currentDevice); currentPointerType = pointerType(pkCursor); sendTabletEvent(QTabletEvent::TabletPress); } sendTabletEvent(type); } // Loop over packets return true; } void QWindowsTabletSupport::tabletUpdateCursor(const int pkCursor) { UINT physicalCursorId; QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_CURSORS + pkCursor, CSR_PHYSID, &physicalCursorId); UINT cursorType; QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_CURSORS + pkCursor, CSR_TYPE, &cursorType); const qint64 uniqueId = (qint64(cursorType & DeviceIdMask) << 32L) | qint64(physicalCursorId); m_currentDevice = indexOfDevice(m_devices, uniqueId); if (m_currentDevice < 0) { m_currentDevice = m_devices.size(); m_devices.push_back(tabletInit(uniqueId, cursorType)); // Note: ideally we might check this button map for changes every // update. However there seems to be an issue with Wintab altering the // button map when the user right-clicks in Krita while another // application has focus. This forces Krita to load button settings only // once, when the tablet is first detected. // // See https://bugs.kde.org/show_bug.cgi?id=359561 BYTE logicalButtons[32]; memset(logicalButtons, 0, 32); m_winTab32DLL.wTInfo(WTI_CURSORS + pkCursor, CSR_SYSBTNMAP, &logicalButtons); m_devices[m_currentDevice].buttonsMap[0x1] = logicalButtons[0]; m_devices[m_currentDevice].buttonsMap[0x2] = logicalButtons[1]; m_devices[m_currentDevice].buttonsMap[0x4] = logicalButtons[2]; } m_devices[m_currentDevice].currentPointerType = pointerType(pkCursor); currentPkCursor = pkCursor; // Check tablet name to enable Surface Pro 3 workaround. #ifdef UNICODE if (!isSurfacePro3) { /** * Some really "nice" tablet drivers don't know that they are * supposed to return their name length when the buffer is * null and they try to write into it effectively causing a * suicide. So we cannot rely on it :( * * We workaround it by just allocating a big array and hoping * for the best. * * Failing tablets: * - Adesso Cybertablet M14 * - Peritab-302 * - Trust Tablet TB7300 * - VisTablet Realm Pro * - Aiptek 14000u (latest driver: v5.03, 2013-10-21) * - Genius G-Pen F350 * - Genius G-Pen 560 (supported on win7 only) */ // we cannot use the correct api :( // UINT nameLength = QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_DEVICES, DVC_NAME, 0); // 1024 chars should be enough for everyone! (c) UINT nameLength = 1024; TCHAR* dvcName = new TCHAR[nameLength + 1]; memset(dvcName, 0, sizeof(TCHAR) * nameLength); UINT writtenBytes = QWindowsTabletSupport::m_winTab32DLL.wTInfo(WTI_DEVICES, DVC_NAME, dvcName); if (writtenBytes > sizeof(TCHAR) * nameLength) { qWarning() << "WINTAB WARNING: tablet name is too long!" << writtenBytes; // avoid crash when trying to read it dvcName[nameLength - 1] = (TCHAR)0; } QString qDvcName = QString::fromWCharArray((const wchar_t*)dvcName); dbgInput << "DVC_NAME =" << qDvcName; // Name changed between older and newer Surface Pro 3 drivers if (qDvcName == QString::fromLatin1("N-trig DuoSense device") || qDvcName == QString::fromLatin1("Microsoft device")) { dbgInput << "This looks like a Surface Pro 3. Enabling eraser workaround."; isSurfacePro3 = true; } delete[] dvcName; } #endif } QT_END_NAMESPACE