/*
* This file is based on the Windows Pointer Input Message support code files by
* Alvin Wong.
* Notwithstanding the license specified in this repository, this file is
* redistributed under BSD 2-Clause license written below in order to keep
* backporting available.
*
* All contributions by Alvin Wong:
* Copyright (c) 2017 Alvin Wong <alvinhochun@gmail.com>
* All rights reserved.
*
* All other contributions:
* Copyright (c) 2018, the respective contributors.
* All rights reserved.
*
* Each contributor holds copyright over their respective contributions.
* The project versioning (Git) records all such contribution source information.
*
* Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions
- are met:
-
- 1. Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
- 2. Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
-
- THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// Get Windows 8 API prototypes and types
#ifdef WINVER
#undef WINVER
#endif
#ifdef _WIN32_WINNT
#undef _WIN32_WINNT
#endif
#define WINVER 0x0602
#define _WIN32_WINNT 0x0602
#include "kis_tablet_support_win8.h"
#include <QApplication>
#include <QDebug>
#include <QHash>
#include <QLibrary>
#include <QPointer>
#include <QTabletEvent>
#include <QVector>
#include <QWidget>
#include <QWindow>
#include <utility>
#ifdef _WIN32
#ifdef KRITA
#include <kis_debug.h>
#endif
#include <windows.h>
#include <tpcshrd.h>
#ifndef Q_OS_WIN
#error This file must not be compiled for non-Windows systems
#endif
namespace {
class Win8PointerInputApi {
#define WIN8_POINTER_INPUT_API_LIST(FUNC) \
/* Pointer Input Functions */ \
FUNC(GetPointerPenInfo) \
FUNC(GetPointerPenInfoHistory) \
FUNC(GetPointerType) \
/* Pointer Device Functions */ \
FUNC(GetPointerDevices) \
/*FUNC(GetPointerDeviceProperties)*/ \
FUNC(GetPointerDevice) \
FUNC(GetPointerDeviceRects) \
/*FUNC(RegisterPointerDeviceNotifications)*/ \
/* end */
bool m_loaded;
public:
#define DEFINE_FP_FROM_WINAPI(func) \
\
public: \
using p##func##_t = std::add_pointer<decltype(func)>::type; \
\
private: \
p##func##_t m_p##func = nullptr; \
\
public: \
const p##func##_t &func = m_p##func; // const fp ref to member
WIN8_POINTER_INPUT_API_LIST(DEFINE_FP_FROM_WINAPI)
#undef DEFINE_FP_FROM_WINAPI
public:
Win8PointerInputApi() : m_loaded(false) {}
bool init() {
if (m_loaded) {
return true;
}
QLibrary user32Lib("user32");
if (!user32Lib.load()) {
qWarning() << "Failed to load user32.dll! This really should not happen.";
return false;
}
#ifdef KRITA
#define LOAD_AND_CHECK_FP_FROM_WINAPI(func) \
m_p##func = reinterpret_cast<p##func##_t>(user32Lib.resolve(#func)); \
if (!m_p##func) { \
dbgTablet << "Failed to load function " #func " from user32.dll"; \
return false; \
}
#else
#define LOAD_AND_CHECK_FP_FROM_WINAPI(func) \
m_p##func = reinterpret_cast<p##func##_t>(user32Lib.resolve(#func)); \
if (!m_p##func) { \
return false; \
}
#endif
WIN8_POINTER_INPUT_API_LIST(LOAD_AND_CHECK_FP_FROM_WINAPI)
#undef LOAD_AND_CHECK_FP_FROM_WINAPI
#ifdef KRITA
dbgTablet << "Loaded Windows 8 Pointer Input API functions";
#endif
m_loaded = true;
return true;
}
bool isLoaded() { return m_loaded; }
#undef WIN8_POINTER_INPUT_API_LIST
}; // class Win8PointerInputApi
Win8PointerInputApi api;
class PointerFlagsWrapper {
const POINTER_FLAGS f;
public:
PointerFlagsWrapper(POINTER_FLAGS flags) : f(flags) {}
static PointerFlagsWrapper fromPointerInfo(const POINTER_INFO &pointerInfo) {
return PointerFlagsWrapper(pointerInfo.pointerFlags);
}
static PointerFlagsWrapper fromPenInfo(const POINTER_PEN_INFO &penInfo) {
return fromPointerInfo(penInfo.pointerInfo);
}
bool isNew() const { return f & POINTER_FLAG_NEW; }
bool isInRange() const { return f & POINTER_FLAG_INRANGE; }
bool isInContact() const { return f & POINTER_FLAG_INCONTACT; }
bool isFirstButtonDown() const { return f & POINTER_FLAG_FIRSTBUTTON; }
bool isSecondButtonDown() const { return f & POINTER_FLAG_SECONDBUTTON; }
bool isThirdButtonDown() const { return f & POINTER_FLAG_THIRDBUTTON; }
bool isForthButtonDown() const { return f & POINTER_FLAG_FOURTHBUTTON; }
bool isFifthButtonDown() const { return f & POINTER_FLAG_FIFTHBUTTON; }
bool isPrimary() const { return f & POINTER_FLAG_PRIMARY; }
bool isConfidence() const { return f & POINTER_FLAG_CONFIDENCE; }
bool isCancelled() const { return f & POINTER_FLAG_CANCELED; }
bool isDown() const { return f & POINTER_FLAG_DOWN; }
bool isUpdate() const { return f & POINTER_FLAG_UPDATE; }
bool isUp() const { return f & POINTER_FLAG_UP; }
bool isWheel() const { return f & POINTER_FLAG_WHEEL; }
bool isHWheel() const { return f & POINTER_FLAG_HWHEEL; }
bool isCaptureChanged() const { return f & POINTER_FLAG_CAPTURECHANGED; }
bool hasTransform() const {
// mingw-w64 headers is missing this flag
// return f & POINTER_FLAG_HASTRANSFORM;
return f & 0x00400000;
}
}; // class PointerFlagsWrapper
class PenFlagsWrapper {
const PEN_FLAGS f;
public:
PenFlagsWrapper(PEN_FLAGS flags) : f(flags) {}
static PenFlagsWrapper fromPenInfo(const POINTER_PEN_INFO &penInfo) {
return PenFlagsWrapper(penInfo.penFlags);
}
bool isBarrelPressed() const { return f & PEN_FLAG_BARREL; }
bool isInverted() const { return f & PEN_FLAG_INVERTED; }
bool isEraserPressed() const { return f & PEN_FLAG_ERASER; }
}; // class PenFlagsWrapper
class PenMaskWrapper {
const PEN_MASK f;
public:
PenMaskWrapper(PEN_MASK mask) : f(mask) {}
static PenMaskWrapper fromPenInfo(const POINTER_PEN_INFO &penInfo) {
return PenMaskWrapper(penInfo.penMask);
}
bool pressureValid() const { return f & PEN_MASK_PRESSURE; }
bool rotationValid() const { return f & PEN_MASK_ROTATION; }
bool tiltXValid() const { return f & PEN_MASK_TILT_X; }
bool tiltYValid() const { return f & PEN_MASK_TILT_Y; }
}; // class PenMaskWrapper
struct PointerDeviceItem {
// HANDLE handle;
// RECT pointerDeviceRect;
// RECT displayRect;
qreal himetricToPixelX;
qreal himetricToPixelY;
qreal pixelOffsetX;
qreal pixelOffsetY;
DISPLAYCONFIG_ROTATION deviceOrientation; // This is needed to fix tilt
};
QHash<HANDLE, PointerDeviceItem> penDevices;
struct PenPointerItem {
// int pointerId;
// POINTER_PEN_INFO penInfo;
HWND hwnd;
HANDLE deviceHandle;
QPointer<QWidget> activeWidget; // Current widget receiving events
qreal oneOverDpr; // 1 / devicePixelRatio of activeWidget
bool widgetIsCaptured; // Current widget is capturing a pen cown event
bool widgetIsIgnored; // Pen events should be ignored until pen up
bool widgetAcceptsPenEvent; // Whether the widget accepts pen events
bool isCaptured() const { return widgetIsCaptured; }
};
QHash<int, PenPointerItem> penPointers;
// int primaryPenPointerId;
bool handlePointerMsg(const MSG &msg);
// extern "C" {
//
// LRESULT CALLBACK pointerDeviceNotificationsWndProc(HWND hwnd, UINT uMsg,
// WPARAM wParam, LPARAM lParam)
// {
// switch (uMsg) {
// case WM_POINTERDEVICECHANGE:
// dbgTablet << "I would want to handle this WM_POINTERDEVICECHANGE
// event, but ms just doesn't want me to use it";
// dbgTablet << " wParam:" << wParam;
// dbgTablet << " lParam:" << lParam;
// return 0;
// case WM_POINTERDEVICEINRANGE:
// dbgTablet << "I would want to handle this WM_POINTERDEVICEINRANGE
// event, but ms just doesn't want me to use it";
// dbgTablet << " wParam:" << wParam;
// dbgTablet << " lParam:" << lParam;
// return 0;
// case WM_POINTERDEVICEOUTOFRANGE:
// dbgTablet << "I would want to handle this WM_POINTERDEVICEOUTOFRANGE
// event, but ms just doesn't want me to use it";
// dbgTablet << " wParam:" << wParam;
// dbgTablet << " lParam:" << lParam;
// return 0;
// }
// return DefWindowProcW(hwnd, uMsg, wParam, lParam);
// }
//
// } // extern "C"
} // namespace
bool KisTabletSupportWin8::isAvailable() {
// Just try loading the APIs
return api.init();
}
bool KisTabletSupportWin8::isPenDeviceAvailable() {
if (!api.init()) {
return false;
}
UINT32 deviceCount = 0;
if (!api.GetPointerDevices(&deviceCount, nullptr)) {
#ifdef KRITA
dbgTablet << "GetPointerDevices failed";
#endif
return false;
}
if (deviceCount == 0) {
#ifdef KRITA
dbgTablet << "No pointer devices";
#endif
return false;
}
QVector<POINTER_DEVICE_INFO> devices(deviceCount);
if (!api.GetPointerDevices(&deviceCount, devices.data())) {
#ifdef KRITA
dbgTablet << "GetPointerDevices failed";
#endif
return false;
}
bool hasPenDevice = false;
for (const POINTER_DEVICE_INFO &device : devices) {
#ifdef KRITA
dbgTablet << "Found pointer device" << static_cast<void *>(device.device)
<< QString::fromWCharArray(device.productString)
<< "type:" << device.pointerDeviceType;
#endif
if (device.pointerDeviceType == POINTER_DEVICE_TYPE_INTEGRATED_PEN ||
device.pointerDeviceType == POINTER_DEVICE_TYPE_EXTERNAL_PEN) {
hasPenDevice = true;
}
}
#ifdef KRITA
dbgTablet << "hasPenDevice:" << hasPenDevice;
#endif
return hasPenDevice;
}
bool KisTabletSupportWin8::init() { return api.init(); }
// void KisTabletSupportWin8::registerPointerDeviceNotifications()
// {
// const wchar_t *className = L"w8PointerMsgWindow";
// HINSTANCE hInst = static_cast<HINSTANCE>(GetModuleHandleW(nullptr));
// WNDCLASSEXW wc;
// wc.cbSize = sizeof(WNDCLASSEXW);
// wc.style = 0;
// wc.lpfnWndProc = pointerDeviceNotificationsWndProc;
// wc.cbClsExtra = 0;
// wc.cbWndExtra = 0;
// wc.hInstance = hInst;
// wc.hCursor = 0;
// wc.hbrBackground = GetSysColorBrush(COLOR_WINDOW);
// wc.hIcon = 0;
// wc.hIconSm = 0;
// wc.lpszMenuName = 0;
// wc.lpszClassName = className;
//
// if (RegisterClassEx(&wc)) {
// HWND hwnd = CreateWindowEx(0, className, nullptr, 0, 0, 0, 0, 0,
// HWND_MESSAGE, nullptr, hInst, nullptr);
// api.RegisterPointerDeviceNotifications(hwnd, TRUE);
// } else {
// dbgTablet << "Cannot register dummy window";
// }
// }
bool KisTabletSupportWin8::nativeEventFilter(const QByteArray &eventType,
void *message, long *result) {
if (!result) {
// I don't know why this even happens, but it actually does
// And the same event is sent in again with result != nullptr
return false;
}
// This is only installed on Windows so there is no reason to check eventType
MSG &msg = *static_cast<MSG *>(message);
switch (msg.message) {
case WM_POINTERDOWN:
case WM_POINTERUP:
case WM_POINTERENTER:
case WM_POINTERLEAVE:
case WM_POINTERUPDATE:
case WM_POINTERCAPTURECHANGED: {
bool handled = handlePointerMsg(msg);
if (handled) {
*result = 0;
return true;
}
break;
}
case WM_TABLET_QUERYSYSTEMGESTURESTATUS:
*result = 0;
return true;
}
Q_UNUSED(eventType)
return false;
}
namespace {
QDebug operator<<(QDebug debug, const POINT &pt) {
QDebugStateSaver saver(debug);
debug.nospace() << '(' << pt.x << ", " << pt.y << ')';
return debug;
}
QDebug operator<<(QDebug debug, const RECT &rect) {
QDebugStateSaver saver(debug);
debug.nospace() << '(' << rect.left << ", " << rect.top << ", " << rect.right
<< ", " << rect.bottom << ')';
return debug;
}
bool registerOrUpdateDevice(HANDLE deviceHandle, const RECT &pointerDeviceRect,
const RECT &displayRect,
const DISPLAYCONFIG_ROTATION deviceOrientation) {
bool isPreviouslyRegistered = penDevices.contains(deviceHandle);
PointerDeviceItem &deviceItem = penDevices[deviceHandle];
PointerDeviceItem oldDeviceItem = deviceItem;
// deviceItem.handle = deviceHandle;
deviceItem.himetricToPixelX =
static_cast<qreal>(displayRect.right - displayRect.left) /
(pointerDeviceRect.right - pointerDeviceRect.left);
deviceItem.himetricToPixelY =
static_cast<qreal>(displayRect.bottom - displayRect.top) /
(pointerDeviceRect.bottom - pointerDeviceRect.top);
deviceItem.pixelOffsetX =
static_cast<qreal>(displayRect.left) -
deviceItem.himetricToPixelX * pointerDeviceRect.left;
deviceItem.pixelOffsetY = static_cast<qreal>(displayRect.top) -
deviceItem.himetricToPixelY * pointerDeviceRect.top;
deviceItem.deviceOrientation = deviceOrientation;
if (!isPreviouslyRegistered) {
#ifdef KRITA
dbgTablet << "Registered pen device" << deviceHandle << "with displayRect"
<< displayRect << "and deviceRect" << pointerDeviceRect << "scale"
<< deviceItem.himetricToPixelX << deviceItem.himetricToPixelY
<< "offset" << deviceItem.pixelOffsetX << deviceItem.pixelOffsetY
<< "orientation" << deviceItem.deviceOrientation;
#endif
} else if (deviceItem.himetricToPixelX != oldDeviceItem.himetricToPixelX ||
deviceItem.himetricToPixelY != oldDeviceItem.himetricToPixelY ||
deviceItem.pixelOffsetX != oldDeviceItem.pixelOffsetX ||
deviceItem.pixelOffsetY != oldDeviceItem.pixelOffsetY ||
deviceItem.deviceOrientation != oldDeviceItem.deviceOrientation) {
#ifdef KRITA
dbgTablet << "Updated pen device" << deviceHandle << "with displayRect"
<< displayRect << "and deviceRect" << pointerDeviceRect << "scale"
<< deviceItem.himetricToPixelX << deviceItem.himetricToPixelY
<< "offset" << deviceItem.pixelOffsetX << deviceItem.pixelOffsetY
<< "orientation" << deviceItem.deviceOrientation;
#endif
}
return true;
}
bool registerOrUpdateDevice(HANDLE deviceHandle) {
RECT pointerDeviceRect, displayRect;
if (!api.GetPointerDeviceRects(deviceHandle, &pointerDeviceRect,
&displayRect)) {
#ifdef KRITA
dbgTablet << "GetPointerDeviceRects failed";
#endif
return false;
}
POINTER_DEVICE_INFO pointerDeviceInfo;
if (!api.GetPointerDevice(deviceHandle, &pointerDeviceInfo)) {
#ifdef KRITA
dbgTablet << "GetPointerDevice failed";
#endif
return false;
}
return registerOrUpdateDevice(deviceHandle, pointerDeviceRect, displayRect,
static_cast<DISPLAYCONFIG_ROTATION>(
pointerDeviceInfo.displayOrientation));
}
QTabletEvent makeProximityTabletEvent(const QEvent::Type eventType,
const POINTER_PEN_INFO &penInfo) {
PenFlagsWrapper penFlags = PenFlagsWrapper::fromPenInfo(penInfo);
QTabletEvent::PointerType pointerType =
penFlags.isInverted() ? QTabletEvent::Eraser : QTabletEvent::Pen;
const QPointF emptyPoint;
return QTabletEvent(
eventType, // type
emptyPoint, // pos
emptyPoint, // globalPos
QTabletEvent::Stylus, // device
pointerType, // pointerType
0, // pressure
0, // xTilt
0, // yTilt
0, // tangentialPressure
0, // rotation
0, // z
Qt::NoModifier, // keyState
reinterpret_cast<qint64>(penInfo.pointerInfo.sourceDevice), // uniqueID
Qt::NoButton, // button
(Qt::MouseButtons)0 // buttons
);
}
// void rotateTiltAngles(int &tiltX, int &tiltY, const DISPLAYCONFIG_ROTATION
// orientation) {
// int newTiltX, newTiltY;
// switch (orientation) {
// case DISPLAYCONFIG_ROTATION_ROTATE90:
// newTiltX = -tiltY;
// newTiltY = tiltX;
// break;
// case DISPLAYCONFIG_ROTATION_ROTATE180:
// newTiltX = -tiltX;
// newTiltY = -tiltY;
// break;
// case DISPLAYCONFIG_ROTATION_ROTATE270:
// newTiltX = tiltY;
// newTiltY = -tiltX;
// break;
// case DISPLAYCONFIG_ROTATION_IDENTITY:
// default:
// newTiltX = tiltX;
// newTiltY = tiltY;
// break;
// }
// tiltX = newTiltX;
// tiltY = newTiltY;
// }
QTabletEvent makePositionalTabletEvent(const QWidget *targetWidget,
const QEvent::Type eventType,
const POINTER_PEN_INFO &penInfo,
const PointerDeviceItem &deviceItem,
const PenPointerItem &penPointerItem) {
PenFlagsWrapper penFlags = PenFlagsWrapper::fromPenInfo(penInfo);
PointerFlagsWrapper pointerFlags = PointerFlagsWrapper::fromPenInfo(penInfo);
PenMaskWrapper penMask = PenMaskWrapper::fromPenInfo(penInfo);
const QPointF globalPosF((deviceItem.himetricToPixelX *
penInfo.pointerInfo.ptHimetricLocationRaw.x +
deviceItem.pixelOffsetX) *
penPointerItem.oneOverDpr,
(deviceItem.himetricToPixelY *
penInfo.pointerInfo.ptHimetricLocationRaw.y +
deviceItem.pixelOffsetY) *
penPointerItem.oneOverDpr);
const QPoint globalPos = globalPosF.toPoint();
const QPoint localPos = targetWidget->mapFromGlobal(globalPos);
const QPointF delta = globalPosF - globalPos;
const QPointF localPosF = localPos + delta;
const QTabletEvent::PointerType pointerType =
penFlags.isInverted() ? QTabletEvent::Eraser : QTabletEvent::Pen;
Qt::MouseButton mouseButton;
if (eventType == QEvent::TabletPress) {
if (penInfo.pointerInfo.ButtonChangeType ==
POINTER_CHANGE_SECONDBUTTON_DOWN) {
mouseButton = Qt::RightButton;
} else {
#ifdef KRITA
KIS_SAFE_ASSERT_RECOVER(penInfo.pointerInfo.ButtonChangeType ==
POINTER_CHANGE_FIRSTBUTTON_DOWN) {
#else
if (!(penInfo.pointerInfo.ButtonChangeType ==
POINTER_CHANGE_FIRSTBUTTON_DOWN)) {
#endif
qWarning() << "WM_POINTER* sent unknown ButtonChangeType"
<< penInfo.pointerInfo.ButtonChangeType;
}
mouseButton = Qt::LeftButton;
}
} else if (eventType == QEvent::TabletRelease) {
if (penInfo.pointerInfo.ButtonChangeType ==
POINTER_CHANGE_SECONDBUTTON_UP) {
mouseButton = Qt::RightButton;
} else {
#ifdef KRITA
KIS_SAFE_ASSERT_RECOVER(penInfo.pointerInfo.ButtonChangeType ==
POINTER_CHANGE_FIRSTBUTTON_UP) {
#else
if (!(penInfo.pointerInfo.ButtonChangeType ==
POINTER_CHANGE_FIRSTBUTTON_UP)) {
#endif
qWarning() << "WM_POINTER* sent unknown ButtonChangeType"
<< penInfo.pointerInfo.ButtonChangeType;
}
mouseButton = Qt::LeftButton;
}
} else {
mouseButton = Qt::NoButton;
}
Qt::MouseButtons mouseButtons;
if (pointerFlags.isFirstButtonDown()) {
mouseButtons |= Qt::LeftButton;
}
if (pointerFlags.isSecondButtonDown()) {
mouseButtons |= Qt::RightButton;
}
int tiltX = 0, tiltY = 0;
if (penMask.tiltXValid()) {
tiltX = qBound(-60, penInfo.tiltX, 60);
}
if (penMask.tiltYValid()) {
tiltY = qBound(-60, penInfo.tiltY, 60);
}
// rotateTiltAngles(tiltX, tiltY, deviceItem.deviceOrientation);
int rotation = 0;
if (penMask.rotationValid()) {
rotation =
360 - penInfo.rotation; // Flip direction and convert to signed int
if (rotation > 180) {
rotation -= 360;
}
}
return QTabletEvent(
eventType, // type
localPosF, // pos
globalPosF, // globalPos
QTabletEvent::Stylus, // device
pointerType, // pointerType
penMask.pressureValid() ? static_cast<qreal>(penInfo.pressure) / 1024
: 0, // pressure
tiltX, // xTilt
tiltY, // yTilt
0, // tangentialPressure
rotation, // rotation
0, // z
QApplication::queryKeyboardModifiers(), // keyState
reinterpret_cast<qint64>(penInfo.pointerInfo.sourceDevice), // uniqueID
mouseButton, // button
mouseButtons // buttons
);
}
bool sendProximityTabletEvent(const QEvent::Type eventType,
const POINTER_PEN_INFO &penInfo) {
#ifdef KRITA
KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(
eventType == QEvent::TabletEnterProximity ||
eventType == QEvent::TabletLeaveProximity,
false);
#else
if (!(eventType == QEvent::TabletEnterProximity ||
eventType == QEvent::TabletLeaveProximity)) {
return false;
}
#endif
QTabletEvent ev = makeProximityTabletEvent(eventType, penInfo);
ev.setAccepted(false);
ev.setTimestamp(penInfo.pointerInfo.dwTime);
QCoreApplication::sendEvent(qApp, &ev);
return ev.isAccepted();
}
void synthesizeMouseEvent(const QTabletEvent &ev,
const POINTER_PEN_INFO &penInfo) {
// Update the cursor position
BOOL result = SetCursorPos(penInfo.pointerInfo.ptPixelLocationRaw.x,
penInfo.pointerInfo.ptPixelLocationRaw.y);
if (!result) {
#ifdef KRITA
dbgInput << "SetCursorPos failed, err" << GetLastError();
#endif
return;
}
// Send mousebutton down/up events. Windows stores the button state.
DWORD inputDataFlags = 0;
switch (ev.type()) {
case QEvent::TabletPress:
switch (ev.button()) {
case Qt::LeftButton:
inputDataFlags = MOUSEEVENTF_LEFTDOWN;
break;
case Qt::RightButton:
inputDataFlags = MOUSEEVENTF_RIGHTDOWN;
break;
default:
return;
}
break;
case QEvent::TabletRelease:
switch (ev.button()) {
case Qt::LeftButton:
inputDataFlags = MOUSEEVENTF_LEFTUP;
break;
case Qt::RightButton:
inputDataFlags = MOUSEEVENTF_RIGHTUP;
break;
default:
return;
}
break;
case QEvent::TabletMove:
default:
return;
}
INPUT inputData = {};
inputData.type = INPUT_MOUSE;
inputData.mi.dwFlags = inputDataFlags;
inputData.mi.dwExtraInfo =
0xFF515700 |
0x01; // https://msdn.microsoft.com/en-us/library/windows/desktop/ms703320%28v=vs.85%29.aspx
UINT result2 = SendInput(1, &inputData, sizeof(inputData));
if (result2 != 1) {
#ifdef KRITA
dbgInput << "SendInput failed, err" << GetLastError();
#endif
return;
}
}
bool sendPositionalTabletEvent(QWidget *target, const QEvent::Type eventType,
const POINTER_PEN_INFO &penInfo,
const PointerDeviceItem &device,
const PenPointerItem &penPointerItem,
const bool shouldSynthesizeMouseEvent) {
#ifdef KRITA
KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(eventType == QEvent::TabletMove ||
eventType == QEvent::TabletPress ||
eventType == QEvent::TabletRelease,
false);
#else
if (!(eventType == QEvent::TabletMove || eventType == QEvent::TabletPress ||
eventType == QEvent::TabletRelease)) {
return false;
}
#endif
QTabletEvent ev = makePositionalTabletEvent(target, eventType, penInfo,
device, penPointerItem);
ev.setAccepted(false);
ev.setTimestamp(penInfo.pointerInfo.dwTime);
QCoreApplication::sendEvent(target, &ev);
if (!shouldSynthesizeMouseEvent) {
// For pen update with multiple updates, only the last update should
// be used to synthesize a mouse event.
return false;
}
// This is some specialized code to handle synthesizing of mouse events from
// the pen events. Issues being:
// 1. We must accept the pointer down/up and the intermediate update events
// to indicate that we want all the pen pointer events for painting,
// otherwise Windows may do weird stuff and skip passing pointer events.
// 2. Some Qt and Krita code uses QCursor::pos() which calls GetCursorPos to
// get the cursor position. This doesn't work nicely before ver 1709 and
// doesn't work at all on ver 1709 if the pen events are handled, so we
// need some way to nudge the cursor on the OS level.
// It appears that using the same way (as in synthesizeMouseEvent) to nudge
// the cursor does work fine for when painting on canvas (which handles
// the QTabletEvent), but not for other widgets because it introduces a lag
// with mouse move events on move start and immediately after mouse down.
// The resolution is to simulate mouse movement with our own code only for
// handled pen events, which is what the following code does.
if (ev.type() == QEvent::TabletMove && ev.buttons() == Qt::NoButton) {
// Let Windows synthesize mouse hover events
return false;
}
if (ev.type() == QEvent::TabletPress && !ev.isAccepted()) {
// On pen down event, if the widget doesn't handle the event, let
// Windows translate the event to touch, mouse or whatever
return false;
}
if (ev.type() != QEvent::TabletPress &&
!penPointerItem.widgetAcceptsPenEvent) {
// For other events, if the previous pen down event wasn't handled by
// the widget, continue to let Windows translate the event
return false;
}
// Otherwise, we synthesize our mouse events
synthesizeMouseEvent(ev, penInfo);
return true; // and tell Windows that we do want the pen events.
}
bool handlePenEnterMsg(const POINTER_PEN_INFO &penInfo) {
PointerFlagsWrapper pointerFlags = PointerFlagsWrapper::fromPenInfo(penInfo);
if (!pointerFlags.isPrimary()) {
// Don't handle non-primary pointer messages for now
#ifdef KRITA
dbgTablet << "Pointer" << penInfo.pointerInfo.pointerId << "of device"
<< penInfo.pointerInfo.sourceDevice << "is not flagged PRIMARY";
#endif
return false;
}
// Update the device scaling factors here
// It doesn't cost much to recalculate anyway
// This ensures that the screen resolution changes are reflected
// WM_POINTERDEVICECHANGE might be useful for this, but its docs are too
// unclear to use
registerOrUpdateDevice(penInfo.pointerInfo.sourceDevice);
// TODO: Need a way to remove from device registration when devices are
// changed
// We now only handle one pointer at a time, so just clear the pointer
// registration
penPointers.clear();
int pointerId = penInfo.pointerInfo.pointerId;
PenPointerItem penPointerItem;
penPointerItem.hwnd = penInfo.pointerInfo.hwndTarget;
penPointerItem.deviceHandle = penInfo.pointerInfo.sourceDevice;
penPointerItem.activeWidget = nullptr;
penPointerItem.oneOverDpr = 1.0;
penPointerItem.widgetIsCaptured = false;
penPointerItem.widgetIsIgnored = false;
penPointerItem.widgetAcceptsPenEvent = false;
// penPointerItem.pointerId = pointerId;
penPointers.insert(pointerId, penPointerItem);
// primaryPenPointerId = pointerId;
// penEnter
sendProximityTabletEvent(QEvent::TabletEnterProximity, penInfo);
return false;
}
bool handlePenLeaveMsg(const POINTER_PEN_INFO &penInfo) {
if (!penPointers.contains(penInfo.pointerInfo.pointerId)) {
#ifdef KRITA
dbgTablet << "Pointer" << penInfo.pointerInfo.pointerId
<< "wasn't being handled";
#endif
return false;
}
if (!penDevices.contains(penInfo.pointerInfo.sourceDevice)) {
#ifdef KRITA
dbgTablet << "Device is gone from the registration???";
#endif
// TODO: re-register device?
penPointers.remove(penInfo.pointerInfo.pointerId);
return false;
}
// penLeave
sendProximityTabletEvent(QEvent::TabletLeaveProximity, penInfo);
penPointers.remove(penInfo.pointerInfo.pointerId);
return false;
}
bool handleSinglePenUpdate(PenPointerItem &penPointerItem,
const POINTER_PEN_INFO &penInfo,
const PointerDeviceItem &device,
const bool shouldSynthesizeMouseEvent) {
QWidget *targetWidget;
if (penPointerItem.isCaptured()) {
if (penPointerItem.widgetIsIgnored) {
return false;
}
targetWidget = penPointerItem.activeWidget;
if (!targetWidget) {
return false;
}
} else {
QWidget *hwndWidget =
QWidget::find(reinterpret_cast<WId>(penInfo.pointerInfo.hwndTarget));
if (!hwndWidget) {
#ifdef KRITA
dbgTablet << "HWND cannot be mapped to QWidget (what?)";
#endif
return false;
}
{
// Check popup / modal widget
QWidget *modalWidget = QApplication::activePopupWidget();
if (!modalWidget) {
modalWidget = QApplication::activeModalWidget();
}
if (modalWidget && modalWidget != hwndWidget &&
!modalWidget->isAncestorOf(hwndWidget)) {
return false;
}
}
{
QWindow *topLevelWindow = hwndWidget->windowHandle();
if (topLevelWindow) {
penPointerItem.oneOverDpr = 1.0 / topLevelWindow->devicePixelRatio();
} else {
penPointerItem.oneOverDpr = 1.0 / qApp->devicePixelRatio();
}
}
QPoint posInHwndWidget = hwndWidget->mapFromGlobal(
QPoint(static_cast<int>(penInfo.pointerInfo.ptPixelLocationRaw.x *
penPointerItem.oneOverDpr),
static_cast<int>(penInfo.pointerInfo.ptPixelLocationRaw.y *
penPointerItem.oneOverDpr)));
targetWidget = hwndWidget->childAt(posInHwndWidget);
if (!targetWidget) {
// dbgTablet << "No childQWidget at cursor position";
targetWidget = hwndWidget;
}
// penPointerItem.activeWidget = targetWidget;
}
bool handled = sendPositionalTabletEvent(targetWidget, QEvent::TabletMove,
penInfo, device, penPointerItem,
shouldSynthesizeMouseEvent);
return handled;
}
bool handlePenUpdateMsg(const POINTER_PEN_INFO &penInfo) {
auto currentPointerIt = penPointers.find(penInfo.pointerInfo.pointerId);
if (currentPointerIt == penPointers.end()) {
// dbgTablet << "Pointer" << penInfo.pointerInfo.pointerId << "wasn't being
// handled";
return false;
}
const auto devIt = penDevices.find(penInfo.pointerInfo.sourceDevice);
if (devIt == penDevices.end()) {
#ifdef KRITA
dbgTablet << "Device not registered???";
#endif
return false;
}
// UINT32 entriesCount = 0;
// if (!api.GetPointerPenInfoHistory(penInfo.pointerInfo.pointerId,
// &entriesCount, nullptr)) {
// dbgTablet << "GetPointerPenInfoHistory (getting count) failed";
// return false;
// }
UINT32 entriesCount = penInfo.pointerInfo.historyCount;
// dbgTablet << "entriesCount:" << entriesCount;
bool handled = false;
if (entriesCount != 1) {
QVector<POINTER_PEN_INFO> penInfoArray(entriesCount);
if (!api.GetPointerPenInfoHistory(penInfo.pointerInfo.pointerId,
&entriesCount, penInfoArray.data())) {
#ifdef KRITA
dbgTablet << "GetPointerPenInfoHistory failed";
#endif
return false;
}
bool handled = false;
// The returned array is in reverse chronological order
const auto rbegin = penInfoArray.rbegin();
const auto rend = penInfoArray.rend();
const auto rlast =
rend - 1; // Only synthesize mouse event for the last one
for (auto it = rbegin; it != rend; ++it) {
handled |= handleSinglePenUpdate(
*currentPointerIt, *it, *devIt,
it == rlast); // Bitwise OR doesn't short circuit
}
} else {
handled = handleSinglePenUpdate(*currentPointerIt, penInfo, *devIt, true);
}
return handled;
}
bool handlePenDownMsg(const POINTER_PEN_INFO &penInfo) {
// PointerFlagsWrapper pointerFlags =
// PointerFlagsWrapper::fromPenInfo(penInfo);
// if (!pointerFlags.isPrimary()) {
// // Don't handle non-primary pointer messages for now
// return false;
// }
auto currentPointerIt = penPointers.find(penInfo.pointerInfo.pointerId);
if (currentPointerIt == penPointers.end()) {
#ifdef KRITA
dbgTablet << "Pointer" << penInfo.pointerInfo.pointerId
<< "wasn't being handled";
#endif
return false;
}
currentPointerIt->hwnd =
penInfo.pointerInfo
.hwndTarget; // They *should* be the same, but just in case
QWidget *hwndWidget =
QWidget::find(reinterpret_cast<WId>(penInfo.pointerInfo.hwndTarget));
if (!hwndWidget) {
#ifdef KRITA
dbgTablet << "HWND cannot be mapped to QWidget (what?)";
#endif
return false;
}
{
QWindow *topLevelWindow = hwndWidget->windowHandle();
if (topLevelWindow) {
currentPointerIt->oneOverDpr = 1.0 / topLevelWindow->devicePixelRatio();
} else {
currentPointerIt->oneOverDpr = 1.0 / qApp->devicePixelRatio();
}
}
QPoint posInHwndWidget = hwndWidget->mapFromGlobal(
QPoint(static_cast<int>(penInfo.pointerInfo.ptPixelLocationRaw.x *
currentPointerIt->oneOverDpr),
static_cast<int>(penInfo.pointerInfo.ptPixelLocationRaw.y *
currentPointerIt->oneOverDpr)));
QWidget *targetWidget = hwndWidget->childAt(posInHwndWidget);
if (!targetWidget) {
#ifdef KRITA
dbgTablet << "No childQWidget at cursor position";
#endif
targetWidget = hwndWidget;
}
currentPointerIt->activeWidget = targetWidget;
currentPointerIt->widgetIsCaptured = true;
// dbgTablet << "QWidget" << targetWidget->windowTitle() << "is capturing
// pointer" << penInfo.pointerInfo.pointerId;
{
// Check popup / modal widget
QWidget *modalWidget = QApplication::activePopupWidget();
if (!modalWidget) {
modalWidget = QApplication::activeModalWidget();
}
if (modalWidget && modalWidget != hwndWidget &&
!modalWidget->isAncestorOf(hwndWidget)) {
currentPointerIt->widgetIsIgnored = true;
#ifdef KRITA
dbgTablet << "Pointer" << penInfo.pointerInfo.pointerId
<< "is being captured but will be ignored";
#endif
return false;
}
}
// penDown
const auto devIt = penDevices.find(penInfo.pointerInfo.sourceDevice);
if (devIt == penDevices.end()) {
#ifdef KRITA
dbgTablet << "Device not registered???";
#endif
return false;
}
bool handled =
sendPositionalTabletEvent(targetWidget, QEvent::TabletPress, penInfo,
*devIt, *currentPointerIt, true);
currentPointerIt->widgetAcceptsPenEvent = handled;
if (!handled) {
// dbgTablet << "QWidget did not handle tablet down event";
}
return handled;
}
bool handlePenUpMsg(const POINTER_PEN_INFO &penInfo) {
auto currentPointerIt = penPointers.find(penInfo.pointerInfo.pointerId);
if (currentPointerIt == penPointers.end()) {
#ifdef KRITA
dbgTablet << "Pointer" << penInfo.pointerInfo.pointerId
<< "wasn't being handled";
#endif
return false;
}
PenPointerItem &penPointerItem = *currentPointerIt;
if (!penPointerItem.isCaptured()) {
#ifdef KRITA
dbgTablet << "Pointer wasn't captured";
#endif
return false;
}
if (penPointerItem.widgetIsIgnored) {
penPointerItem.widgetIsCaptured = false;
penPointerItem.widgetIsIgnored = false;
return false;
}
// penUp
QWidget *targetWidget = penPointerItem.activeWidget;
if (!targetWidget) {
#ifdef KRITA
dbgTablet << "Previously captured target has been deleted";
#endif
penPointerItem.widgetIsCaptured = false;
return false;
}
const auto devIt = penDevices.find(penInfo.pointerInfo.sourceDevice);
if (devIt == penDevices.end()) {
#ifdef KRITA
dbgTablet << "Device not registered???";
#endif
return false;
}
bool handled =
sendPositionalTabletEvent(targetWidget, QEvent::TabletRelease, penInfo,
*devIt, penPointerItem, true);
// dbgTablet << "QWidget" << currentPointerIt->activeWidget->windowTitle() <<
// "is releasing capture to pointer" << penInfo.pointerInfo.pointerId;
penPointerItem.widgetIsCaptured = false;
penPointerItem.widgetAcceptsPenEvent = false;
return handled;
}
bool handlePointerMsg(const MSG &msg) {
if (!api.isLoaded()) {
qWarning() << "Windows 8 Pointer Input API functions not loaded";
return false;
}
int pointerId = GET_POINTERID_WPARAM(msg.wParam);
POINTER_INPUT_TYPE pointerType;
if (!api.GetPointerType(pointerId, &pointerType)) {
#ifdef KRITA
dbgTablet << "GetPointerType failed";
#endif
return false;
}
if (pointerType != PT_PEN) {
// dbgTablet << "pointerType" << pointerType << "is not PT_PEN";
return false;
}
POINTER_PEN_INFO penInfo;
if (!api.GetPointerPenInfo(pointerId, &penInfo)) {
#ifdef KRITA
dbgTablet << "GetPointerPenInfo failed";
#endif
return false;
}
switch (msg.message) {
case WM_POINTERDOWN:
// dbgTablet << "WM_POINTERDOWN";
break;
case WM_POINTERUP:
// dbgTablet << "WM_POINTERUP";
break;
case WM_POINTERENTER:
// dbgTablet << "WM_POINTERENTER";
break;
case WM_POINTERLEAVE:
// dbgTablet << "WM_POINTERLEAVE";
break;
case WM_POINTERUPDATE:
// dbgTablet << "WM_POINTERUPDATE";
break;
case WM_POINTERCAPTURECHANGED:
// dbgTablet << "WM_POINTERCAPTURECHANGED";
break;
default:
#ifdef KRITA
dbgTablet << "I missed this message: " << msg.message;
#endif
break;
}
// dbgTablet << " hwnd: " << penInfo.pointerInfo.hwndTarget;
// dbgTablet << " msg hwnd: " << msg.hwnd;
// dbgTablet << " pointerId: " << pointerId;
// dbgTablet << " sourceDevice:" << penInfo.pointerInfo.sourceDevice;
// dbgTablet << " pointerFlags:" << penInfo.pointerInfo.pointerFlags;
// dbgTablet << " btnChgType: " << penInfo.pointerInfo.ButtonChangeType;
// dbgTablet << " penFlags: " << penInfo.penFlags;
// dbgTablet << " penMask: " << penInfo.penMask;
// dbgTablet << " pressure: " << penInfo.pressure;
// dbgTablet << " rotation: " << penInfo.rotation;
// dbgTablet << " tiltX: " << penInfo.tiltX;
// dbgTablet << " tiltY: " << penInfo.tiltY;
// dbgTablet << " ptPixelLocationRaw: " <<
// penInfo.pointerInfo.ptPixelLocationRaw;
// dbgTablet << " ptHimetricLocationRaw:" <<
// penInfo.pointerInfo.ptHimetricLocationRaw;
// RECT pointerDeviceRect, displayRect;
// if (!api.GetPointerDeviceRects(penInfo.pointerInfo.sourceDevice,
// &pointerDeviceRect, &displayRect)) {
// dbgTablet << "GetPointerDeviceRects failed";
// return false;
// }
// dbgTablet << " pointerDeviceRect:" << pointerDeviceRect;
// dbgTablet << " displayRect:" << displayRect;
// dbgTablet << " scaled X:" <<
// static_cast<qreal>(penInfo.pointerInfo.ptHimetricLocationRaw.x) /
// (pointerDeviceRect.right - pointerDeviceRect.left) * (displayRect.right -
// displayRect.left);
// dbgTablet << " scaled Y:" <<
// static_cast<qreal>(penInfo.pointerInfo.ptHimetricLocationRaw.y) /
// (pointerDeviceRect.bottom - pointerDeviceRect.top) * (displayRect.bottom -
// displayRect.top);
switch (msg.message) {
case WM_POINTERDOWN:
return handlePenDownMsg(penInfo);
case WM_POINTERUP:
return handlePenUpMsg(penInfo);
case WM_POINTERENTER:
return handlePenEnterMsg(penInfo);
case WM_POINTERLEAVE:
return handlePenLeaveMsg(penInfo);
case WM_POINTERUPDATE:
return handlePenUpdateMsg(penInfo);
case WM_POINTERCAPTURECHANGED:
// TODO: Should this event be handled?
#ifdef KRITA
dbgTablet << "FIXME: WM_POINTERCAPTURECHANGED isn't handled";
#endif
break;
}
return false;
}
} // namespace
#endif // _WIN32