Files
test2/intern/ghost/intern/GHOST_Wintab.cpp
Campbell Barton 67ee87a6e9 Cleanup: spelling
2021-06-22 14:23:37 +10:00

492 lines
14 KiB
C++

/*
* 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 un-scaled tablet coordinates. */
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<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(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;
}
}