Files
UnrealEngine/Engine/Plugins/Editor/StylusInput/Source/StylusInputWintab/Private/WintabInstance.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

882 lines
34 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "WintabInstance.h"
#include <StylusInput.h>
#include <StylusInputUtils.h>
#include <Framework/Application/SlateApplication.h>
#include "WintabAPI.h"
#define LOCTEXT_NAMESPACE "WintabInstance"
#define LOG_PREAMBLE "WintabInstance"
using namespace UE::StylusInput::Private;
namespace UE::StylusInput::Wintab
{
bool SetupWintabContext(const FWintabAPI& WintabAPI, UINT WintabContextIndex, LOGCONTEXT& WintabContext)
{
bool bSuccess = true;
WintabContext.lcPktData = PACKETDATA;
WintabContext.lcPktMode = PACKETMODE;
WintabContext.lcOptions |= CXO_CSRMESSAGES | CXO_MESSAGES | CXO_SYSTEM;
AXIS AxisX = {};
AXIS AxisY = {};
AXIS AxisZ = {};
bSuccess &= WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_X, &AxisX) == sizeof(AxisX);
bSuccess &= WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_Y, &AxisY) == sizeof(AxisY);
bSuccess &= WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_Z, &AxisZ) == sizeof(AxisZ);
if (!bSuccess)
{
LogError(LOG_PREAMBLE, FString::Format(TEXT("Failed to query device properties for Wintab context with index {0}."), {WintabContextIndex}));
return false;
}
WintabContext.lcOutExtX = AxisX.axMax - AxisX.axMin + 1;
WintabContext.lcOutExtY = AxisY.axMax - AxisY.axMin + 1;
WintabContext.lcOutExtZ = AxisZ.axMax - AxisZ.axMin + 1;
// In Wintab, the tablet origin is lower left. Move origin to upper left so that it corresponds to screen origin.
WintabContext.lcOutExtY = -WintabContext.lcOutExtY;
return bSuccess;
}
bool SetupTabletContextMetadata(const FWintabAPI& WintabAPI, UINT WintabContextIndex, HCTX WintabContextHandle,
const LOGCONTEXT& WintabContext, FTabletContext& TabletContext)
{
bool bSuccess = true;
TabletContext.WintabContextIndex = WintabContextIndex;
TabletContext.WintabContextHandle = WintabContextHandle;
TabletContext.InputRectangle = {
WintabContext.lcOutOrgX, WintabContext.lcOutOrgY,
WintabContext.lcOutOrgX + WintabContext.lcOutExtX, WintabContext.lcOutOrgY - WintabContext.lcOutExtY
};
FWintabInfoOutputBuffer OutputBuffer;
TabletContext.Name = WintabContext.lcName;
if (TabletContext.Name.IsEmpty())
{
TCHAR *const OutputBufferPtr = OutputBuffer.Allocate(WTI_DEVICES + WintabContextIndex, DVC_NAME);
if (WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_NAME, OutputBufferPtr) <= OutputBuffer.SizeInBytes())
{
TabletContext.Name = OutputBufferPtr;
}
else
{
LogWarning(LOG_PREAMBLE, FString::Format(TEXT("Failed to query device name for Wintab context with index {0}."), {WintabContextIndex}));
}
}
TCHAR *const OutputBufferPtr = OutputBuffer.Allocate(WTI_DEVICES + WintabContextIndex, DVC_PNPID);
if (WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_PNPID, OutputBufferPtr) <= OutputBuffer.SizeInBytes())
{
TabletContext.PlugAndPlayID = OutputBufferPtr;
}
else
{
TabletContext.PlugAndPlayID.Empty();
LogWarning(LOG_PREAMBLE, FString::Format(TEXT("Failed to query Plug and Play ID for Wintab context with index {0}."), {WintabContextIndex}));
}
UINT HardwareCapabilities;
if (WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_HARDWARE, &HardwareCapabilities) != sizeof(HardwareCapabilities))
{
LogError(LOG_PREAMBLE, FString::Format(TEXT("Failed to query hardware capabilities for Wintab context with index {0}."), {WintabContextIndex}));
return false;
}
TabletContext.HardwareCapabilities =
(HardwareCapabilities & HWC_INTEGRATED ? ETabletHardwareCapabilities::Integrated : ETabletHardwareCapabilities::None)
| (HardwareCapabilities & HWC_TOUCH ? ETabletHardwareCapabilities::CursorMustTouch : ETabletHardwareCapabilities::None)
| (HardwareCapabilities & HWC_HARDPROX ? ETabletHardwareCapabilities::HardProximity : ETabletHardwareCapabilities::None)
| (HardwareCapabilities & HWC_PHYSID_CURSORS ? ETabletHardwareCapabilities::CursorsHavePhysicalIds : ETabletHardwareCapabilities::None);
/* CURSORS */
UINT FirstCursor, NumCursors;
if (WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_FIRSTCSR, &FirstCursor) == sizeof(FirstCursor)
&& WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_NCSRTYPES, &NumCursors) == sizeof(NumCursors))
{
TabletContext.WintabFirstCursor = FirstCursor;
TabletContext.WintabNumCursors = NumCursors;
}
else
{
LogError(LOG_PREAMBLE, FString::Format(TEXT("Failed to query cursor range for Wintab context with index {0}."), {WintabContextIndex}));
return false;
}
return bSuccess;
}
void SetPropertyStatus(FStylusInputPacket& Packet, const PACKET& WintabPacket, const int8* Data)
{
// Todo incorporate if tablet context is actually able to support Z values (or proximity?); if not, probably any packet is touching the tablet
Packet.PenStatus = (WintabPacket.pkStatus & TPS_INVERT ? EPenStatus::CursorIsInverted : EPenStatus::None)
| (WintabPacket.pkZ <= 0 ? EPenStatus::CursorIsTouching : EPenStatus::None);
}
static_assert(std::is_same_v<FPacketPropertyHandler::FFuncSetProperty, decltype(SetPropertyStatus)>);
void SetPropertyTime(FStylusInputPacket& Packet, const PACKET& WintabPacket, const int8* Data)
{
Packet.TimerTick = WintabPacket.pkTime;
}
static_assert(std::is_same_v<FPacketPropertyHandler::FFuncSetProperty, decltype(SetPropertyTime)>);
void SetPropertySerialNumber(FStylusInputPacket& Packet, const PACKET& WintabPacket, const int8* Data)
{
Packet.SerialNumber = WintabPacket.pkSerialNumber;
}
static_assert(std::is_same_v<FPacketPropertyHandler::FFuncSetProperty, decltype(SetPropertySerialNumber)>);
void SetPropertyCursor(FStylusInputPacket& Packet, const PACKET& WintabPacket, const int8* Data)
{
const FWintabMessageHandler* MessageHandler = *reinterpret_cast<FWintabMessageHandler *const *>(Data);
Packet.CursorID = MessageHandler->GetCurrentStylusID();
}
static_assert(std::is_same_v<FPacketPropertyHandler::FFuncSetProperty, decltype(SetPropertyCursor)>);
void AssignSetPropertyCursorData(int8 *const Data, const FWintabMessageHandler* MessageHandler)
{
*reinterpret_cast<const FWintabMessageHandler**>(Data) = MessageHandler;
}
void SetPropertyButtons(FStylusInputPacket& Packet, const PACKET& WintabPacket, const int8* Data)
{
// Todo
/*const WORD ButtonNumber = LOWORD(WintabPacket.pkButtons);
const WORD ButtonState = HIWORD(WintabPacket.pkButtons);*/
}
static_assert(std::is_same_v<FPacketPropertyHandler::FFuncSetProperty, decltype(SetPropertyButtons)>);
void SetPropertyX(FStylusInputPacket& Packet, const PACKET& WintabPacket, const int8* Data)
{
const int32& TabletContextInputWidth = reinterpret_cast<const int*>(Data)[0];
const int32& VirtualScreenWidth = reinterpret_cast<const int*>(Data)[1];
const RECT *const * WindowRect = reinterpret_cast<const RECT *const *>(Data + sizeof(int32) * 2);
const int32& WindowOffsetX = (*WindowRect)->left;
Packet.X = static_cast<float>(WintabPacket.pkX) * VirtualScreenWidth / TabletContextInputWidth - WindowOffsetX;
}
static_assert(std::is_same_v<FPacketPropertyHandler::FFuncSetProperty, decltype(SetPropertyX)>);
void AssignSetPropertyXData(int8 *const Data, const int32 TabletContextInputWidth, const int32 VirtualScreenWidth, const RECT* WindowRect)
{
int32* DataInt = reinterpret_cast<int32*>(Data);
*DataInt++ = TabletContextInputWidth;
*DataInt++ = VirtualScreenWidth;
const RECT** DataRect = reinterpret_cast<const RECT**>(DataInt);
*DataRect = WindowRect;
}
void SetPropertyY(FStylusInputPacket& Packet, const PACKET& WintabPacket, const int8* Data)
{
const int32& TabletContextInputHeight = reinterpret_cast<const int*>(Data)[0];
const int32& VirtualScreenHeight = reinterpret_cast<const int*>(Data)[1];
const RECT *const * WindowRect = reinterpret_cast<const RECT *const *>(Data + sizeof(int32) * 2);
const int32& WindowOffsetY = (*WindowRect)->top;
Packet.Y = static_cast<float>(WintabPacket.pkY) * VirtualScreenHeight / TabletContextInputHeight - WindowOffsetY;
}
static_assert(std::is_same_v<FPacketPropertyHandler::FFuncSetProperty, decltype(SetPropertyY)>);
void AssignSetPropertyYData(int8 *const Data, const int32 TabletContextInputHeight, const int32 VirtualScreenHeight, const RECT* WindowRect)
{
int32* DataInt = reinterpret_cast<int32*>(Data);
*DataInt++ = TabletContextInputHeight;
*DataInt++ = VirtualScreenHeight;
const RECT** DataRect = reinterpret_cast<const RECT**>(DataInt);
*DataRect = WindowRect;
}
void SetPropertyZ(FStylusInputPacket& Packet, const PACKET& WintabPacket, const int8* Data)
{
const float& InvResolution = *reinterpret_cast<const float*>(Data);
Packet.Z = static_cast<float>(WintabPacket.pkZ) * InvResolution;
}
static_assert(std::is_same_v<FPacketPropertyHandler::FFuncSetProperty, decltype(SetPropertyZ)>);
void AssignSetPropertyZData(int8 *const Data, const float Resolution)
{
*reinterpret_cast<float*>(Data) = 1.0f / Resolution;
}
void SetPropertyNormalPressure(FStylusInputPacket& Packet, const PACKET& WintabPacket, const int8* Data)
{
const float& InvExtent = *reinterpret_cast<const float*>(Data);
Packet.NormalPressure = static_cast<float>(WintabPacket.pkNormalPressure) * InvExtent;
}
static_assert(std::is_same_v<FPacketPropertyHandler::FFuncSetProperty, decltype(SetPropertyNormalPressure)>);
void AssignSetPropertyNormalPressureData(int8 *const Data, const int32 Extent)
{
*reinterpret_cast<float*>(Data) = 1.0f / Extent;
}
void SetPropertyTangentPressure(FStylusInputPacket& Packet, const PACKET& WintabPacket, const int8* Data)
{
const float& InvExtent = *reinterpret_cast<const float*>(Data);
Packet.TangentPressure = static_cast<float>(WintabPacket.pkTangentPressure) * InvExtent;
}
static_assert(std::is_same_v<FPacketPropertyHandler::FFuncSetProperty, decltype(SetPropertyTangentPressure)>);
void AssignSetPropertyTangentPressureData(int8 *const Data, const int32 Extent)
{
*reinterpret_cast<float*>(Data) = 1.0f / Extent;
}
void SetPropertyOrientation(FStylusInputPacket& Packet, const PACKET& WintabPacket, const int8* Data)
{
const float& InvScaleAzimuth = reinterpret_cast<const float*>(Data)[0];
const float& InvScaleAltitude = reinterpret_cast<const float*>(Data)[1];
const float& InvScaleTwist = reinterpret_cast<const float*>(Data)[2];
Packet.AzimuthOrientation = WintabPacket.pkOrientation.orAzimuth * InvScaleAzimuth;
Packet.AltitudeOrientation = WintabPacket.pkOrientation.orAltitude * InvScaleAltitude;
Packet.TwistOrientation = WintabPacket.pkOrientation.orTwist * InvScaleTwist;
const float AzimuthRadians = FMath::DegreesToRadians(Packet.AzimuthOrientation);
const float AltitudeRadians = FMath::DegreesToRadians(Packet.AltitudeOrientation);
const float X = FMath::Sin(AzimuthRadians) * FMath::Cos(AltitudeRadians);
const float Y = FMath::Cos(AzimuthRadians) * FMath::Cos(AltitudeRadians);
Packet.XTiltOrientation = FMath::RadiansToDegrees(FMath::Asin(-X));
Packet.YTiltOrientation = FMath::RadiansToDegrees(FMath::Asin(-Y));
}
static_assert(std::is_same_v<FPacketPropertyHandler::FFuncSetProperty, decltype(SetPropertyOrientation)>);
void AssignSetPropertyOrientationData(int8 *const Data, const float AzimuthResolution, const float AltitudeResolution, const float TwistResolution)
{
float* DataFloat = reinterpret_cast<float*>(Data);
*DataFloat++ = 1.0f / AzimuthResolution * 360.0f;
*DataFloat++ = 1.0f / AltitudeResolution * 360.0f;
*DataFloat = 1.0f / TwistResolution * 360.0f;
}
void SetPropertyRotation(FStylusInputPacket& Packet, const PACKET& WintabPacket, const int8* Data)
{
const float& InvScalePitch = reinterpret_cast<const float*>(Data)[0];
const float& InvScaleRoll = reinterpret_cast<const float*>(Data)[1];
const float& InvScaleYaw = reinterpret_cast<const float*>(Data)[2];
Packet.PitchRotation = WintabPacket.pkRotation.roPitch * InvScalePitch;
Packet.RollRotation = WintabPacket.pkRotation.roRoll * InvScaleRoll;
Packet.YawRotation = WintabPacket.pkRotation.roYaw * InvScaleYaw;
}
static_assert(std::is_same_v<FPacketPropertyHandler::FFuncSetProperty, decltype(SetPropertyRotation)>);
void AssignSetPropertyRotationData(int8 *const Data, const float PitchResolution, const float RollResolution, const float YawResolution)
{
float* DataFloat = reinterpret_cast<float*>(Data);
*DataFloat++ = 1.0f / PitchResolution * 360.0f;
*DataFloat++ = 1.0f / RollResolution * 360.0f;
*DataFloat = 1.0f / YawResolution * 360.0f;
}
bool SetupTabletContextPacketDescriptionData(const FWintabAPI& WintabAPI, UINT WintabContextIndex, const LOGCONTEXT& WintabContext,
const RECT& WindowRect, FTabletContext& TabletContext, const FWintabMessageHandler& MessageHandler)
{
bool bSuccess = true;
TabletContext.SupportedProperties = ETabletSupportedProperties::None;
TabletContext.NumPacketProperties = 0;
WTPKT DataAvailableForAllCursors;
WTPKT DataAvailableForSomeCursors;
bSuccess &= WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_PKTDATA, &DataAvailableForAllCursors) == sizeof(DataAvailableForAllCursors);
bSuccess &= WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_CSRDATA, &DataAvailableForSomeCursors) == sizeof(DataAvailableForSomeCursors);
if (!bSuccess)
{
LogError(LOG_PREAMBLE, FString::Format(TEXT("Failed to query available packet data for Wintab context with index {0}."), {WintabContextIndex}));
return false;
}
const WTPKT DataAvailable = DataAvailableForAllCursors | DataAvailableForSomeCursors;
auto AddProperty = [&TabletContext](EPacketPropertyType Type, ETabletPropertyMetricUnit MetricUnit, int32 Minimum, int32 Maximum,
float Resolution) -> FPacketPropertyHandler&
{
FPacketProperty& Property = TabletContext.PacketProperties[TabletContext.NumPacketProperties];
Property.Type = Type;
Property.MetricUnit = MetricUnit;
Property.Minimum = Minimum;
Property.Maximum = Maximum;
Property.Resolution = Resolution;
FPacketPropertyHandler& PropertyHandler = TabletContext.PacketPropertyHandlers[TabletContext.NumPacketProperties];
++TabletContext.NumPacketProperties;
return PropertyHandler;
};
if (DataAvailable & PK_STATUS)
{
TabletContext.SupportedProperties = ETabletSupportedProperties::PacketStatus;
FPacketPropertyHandler& PropertyHandler = AddProperty(EPacketPropertyType::Status, ETabletPropertyMetricUnit::Default, 0, 0, 0.0f);
PropertyHandler.SetProperty = &SetPropertyStatus;
}
if (DataAvailable & PK_TIME)
{
TabletContext.SupportedProperties = TabletContext.SupportedProperties | ETabletSupportedProperties::TimerTick;
FPacketPropertyHandler& PropertyHandler = AddProperty(EPacketPropertyType::Time, ETabletPropertyMetricUnit::Default, 0, 0, 0.0f);
PropertyHandler.SetProperty = &SetPropertyTime;
}
if (DataAvailable & PK_SERIAL_NUMBER)
{
TabletContext.SupportedProperties = TabletContext.SupportedProperties | ETabletSupportedProperties::SerialNumber;
FPacketPropertyHandler& PropertyHandler = AddProperty(EPacketPropertyType::SerialNumber, ETabletPropertyMetricUnit::Default, 0, 0, 0.0f);
PropertyHandler.SetProperty = &SetPropertySerialNumber;
}
if (DataAvailable & PK_CURSOR)
{
FPacketPropertyHandler& PropertyHandler = AddProperty(EPacketPropertyType::Cursor, ETabletPropertyMetricUnit::Default, 0, 0, 0.0f);
PropertyHandler.SetProperty = &SetPropertyCursor;
AssignSetPropertyCursorData(PropertyHandler.SetPropertyData, &MessageHandler);
}
if (DataAvailable & PK_BUTTONS)
{
FPacketPropertyHandler& PropertyHandler = AddProperty(EPacketPropertyType::Buttons, ETabletPropertyMetricUnit::Default, 0, 0, 0.0f);
PropertyHandler.SetProperty = &SetPropertyButtons;
}
if (DataAvailable & PK_X)
{
AXIS Axis;
if (WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_X, &Axis) == sizeof(Axis))
{
TabletContext.SupportedProperties = TabletContext.SupportedProperties | ETabletSupportedProperties::X;
FPacketPropertyHandler& PropertyHandler = AddProperty(EPacketPropertyType::X, static_cast<ETabletPropertyMetricUnit>(Axis.axUnits), Axis.axMin,
Axis.axMax, Fix32ToFloat(Axis.axResolution));
PropertyHandler.SetProperty = &SetPropertyX;
AssignSetPropertyXData(PropertyHandler.SetPropertyData, WintabContext.lcInExtX, WintabAPI.GetSystemMetrics(SM_CXVIRTUALSCREEN), &WindowRect);
}
else
{
bSuccess = false;
}
}
if (DataAvailable & PK_Y)
{
AXIS Axis;
if (WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_Y, &Axis) == sizeof(Axis))
{
TabletContext.SupportedProperties = TabletContext.SupportedProperties | ETabletSupportedProperties::Y;
FPacketPropertyHandler& PropertyHandler = AddProperty(EPacketPropertyType::Y, static_cast<ETabletPropertyMetricUnit>(Axis.axUnits), Axis.axMin,
Axis.axMax, Fix32ToFloat(Axis.axResolution));
PropertyHandler.SetProperty = &SetPropertyY;
AssignSetPropertyYData(PropertyHandler.SetPropertyData, WintabContext.lcInExtY, WintabAPI.GetSystemMetrics(SM_CYVIRTUALSCREEN), &WindowRect);
}
else
{
bSuccess = false;
}
}
if (DataAvailable & PK_Z)
{
AXIS Axis;
if (WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_Z, &Axis) == sizeof(Axis))
{
TabletContext.SupportedProperties = TabletContext.SupportedProperties | ETabletSupportedProperties::Z;
FPacketPropertyHandler& PropertyHandler = AddProperty(EPacketPropertyType::Z, static_cast<ETabletPropertyMetricUnit>(Axis.axUnits), Axis.axMin,
Axis.axMax, Fix32ToFloat(Axis.axResolution));
const auto& Property = TabletContext.PacketProperties[TabletContext.NumPacketProperties - 1];
const float Resolution = [&Property]
{
switch (Property.MetricUnit)
{
case ETabletPropertyMetricUnit::Inches:
return Property.Resolution * 2.54f;
case ETabletPropertyMetricUnit::Centimeters:
return Property.Resolution;
default:
return 1.0f;
}
}();
PropertyHandler.SetProperty = &SetPropertyZ;
AssignSetPropertyZData(PropertyHandler.SetPropertyData, Resolution);
}
else
{
bSuccess = false;
}
}
if (DataAvailable & PK_NORMAL_PRESSURE)
{
AXIS Axis;
if (WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_NPRESSURE, &Axis) == sizeof(Axis))
{
TabletContext.SupportedProperties = TabletContext.SupportedProperties | ETabletSupportedProperties::NormalPressure;
FPacketPropertyHandler& PropertyHandler = AddProperty(EPacketPropertyType::NormalPressure, static_cast<ETabletPropertyMetricUnit>(Axis.axUnits), Axis.axMin,
Axis.axMax, Fix32ToFloat(Axis.axResolution));
PropertyHandler.SetProperty = &SetPropertyNormalPressure;
AssignSetPropertyNormalPressureData(PropertyHandler.SetPropertyData, Axis.axMax - Axis.axMin + 1);
}
else
{
bSuccess = false;
}
}
if (DataAvailable & PK_TANGENT_PRESSURE)
{
AXIS Axis;
if (WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_TPRESSURE, &Axis) == sizeof(Axis))
{
TabletContext.SupportedProperties = TabletContext.SupportedProperties | ETabletSupportedProperties::TangentPressure;
FPacketPropertyHandler& PropertyHandler = AddProperty(EPacketPropertyType::TangentPressure, static_cast<ETabletPropertyMetricUnit>(Axis.axUnits), Axis.axMin,
Axis.axMax, Fix32ToFloat(Axis.axResolution));
PropertyHandler.SetProperty = &SetPropertyTangentPressure;
AssignSetPropertyTangentPressureData(PropertyHandler.SetPropertyData, Axis.axMax - Axis.axMin + 1);
}
else
{
bSuccess = false;
}
}
if (DataAvailable & PK_ORIENTATION)
{
AXIS Orientation[3];
if (WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_ORIENTATION, &Orientation) == sizeof(Orientation))
{
if (Orientation[0].axResolution && Orientation[1].axResolution)
{
TabletContext.SupportedProperties = TabletContext.SupportedProperties | ETabletSupportedProperties::AzimuthOrientation |
ETabletSupportedProperties::AltitudeOrientation | ETabletSupportedProperties::TwistOrientation |
ETabletSupportedProperties::XTiltOrientation | ETabletSupportedProperties::YTiltOrientation;
if (Orientation[0].axUnits != TU_CIRCLE || Orientation[1].axUnits != TU_CIRCLE || Orientation[2].axUnits != TU_CIRCLE)
{
LogWarning(LOG_PREAMBLE, FString::Format(
TEXT("Units for orientation are not all TU_CIRCLE for Wintab context with index {0}."), {WintabContextIndex}));
}
FPacketPropertyHandler& PropertyHandler = AddProperty(EPacketPropertyType::Orientation, static_cast<ETabletPropertyMetricUnit>(Orientation[0].axUnits), 0,
0, Fix32ToFloat(Orientation[0].axResolution));
PropertyHandler.SetProperty = &SetPropertyOrientation;
AssignSetPropertyOrientationData(PropertyHandler.SetPropertyData, Fix32ToFloat(Orientation[0].axResolution),
Fix32ToFloat(Orientation[1].axResolution),
Fix32ToFloat(Orientation[2].axResolution));
}
}
else
{
bSuccess = false;
}
}
if (DataAvailable & PK_ROTATION)
{
AXIS Rotation[3];
if (WintabAPI.WtInfo(WTI_DEVICES + WintabContextIndex, DVC_ROTATION, &Rotation) == sizeof(Rotation))
{
TabletContext.SupportedProperties = TabletContext.SupportedProperties | ETabletSupportedProperties::PitchRotation |
ETabletSupportedProperties::RollRotation | ETabletSupportedProperties::YawRotation;
if (Rotation[0].axUnits != TU_CIRCLE || Rotation[1].axUnits != TU_CIRCLE || Rotation[2].axUnits != TU_CIRCLE)
{
LogWarning(LOG_PREAMBLE, FString::Format(
TEXT("Units for rotation are not all TU_CIRCLE for Wintab context with index {0}."), {WintabContextIndex}));
}
FPacketPropertyHandler& PropertyHandler = AddProperty(EPacketPropertyType::Rotation, static_cast<ETabletPropertyMetricUnit>(Rotation[0].axUnits), 0, 0,
Fix32ToFloat(Rotation[0].axResolution));
PropertyHandler.SetProperty = &SetPropertyRotation;
AssignSetPropertyRotationData(PropertyHandler.SetPropertyData, Fix32ToFloat(Rotation[0].axResolution), Fix32ToFloat(Rotation[1].axResolution),
Fix32ToFloat(Rotation[2].axResolution));
}
else
{
bSuccess = false;
}
}
if (!bSuccess)
{
LogError(LOG_PREAMBLE, FString::Format(TEXT("Failed to query packet property information for Wintab context with index {0}."), {WintabContextIndex}));
}
return bSuccess;
}
bool SetupTabletContext(const FWintabAPI& WintabAPI, UINT WintabContextIndex, HCTX WintabContextHandle, const LOGCONTEXT& WintabContext,
const RECT& WindowRect, FTabletContext& TabletContext, const FWintabMessageHandler& MessageHandler)
{
bool bSuccess = true;
bSuccess &= SetupTabletContextMetadata(WintabAPI, WintabContextIndex, WintabContextHandle, WintabContext, TabletContext);
bSuccess &= SetupTabletContextPacketDescriptionData(WintabAPI, WintabContextIndex, WintabContext, WindowRect, TabletContext, MessageHandler);
return bSuccess;
}
FWintabInstance::FWintabInstance(const uint32 ID, const HWND OSWindowHandle)
: ID(ID)
, WintabAPI(FWintabAPI::GetInstance())
, OSWindowHandle(OSWindowHandle)
, MessageHandler(this,
FGetTabletContextCallback::CreateRaw(this, &FWintabInstance::GetTabletContextInternal),
FGetStylusIDCallback::CreateRaw(this, &FWintabInstance::GetStylusID),
FUpdateWindowRectCallback::CreateRaw(this, &FWintabInstance::UpdateWindowRect),
FUpdateTabletContextsCallback::CreateRaw(this, &FWintabInstance::UpdateTabletContexts))
{
if (FSlateApplication::IsInitialized())
{
if (const TSharedPtr<GenericApplication> WindowsApplication = FSlateApplication::Get().GetPlatformApplication())
{
static_cast<FWindowsApplication*>(WindowsApplication.Get())->AddMessageHandler(MessageHandler);
}
}
UpdateWindowRect();
UpdateTabletContexts();
}
FWintabInstance::~FWintabInstance()
{
ClearTabletContexts();
if (FSlateApplication::IsInitialized())
{
if (const TSharedPtr<GenericApplication> WindowsApplication = FSlateApplication::Get().GetPlatformApplication())
{
static_cast<FWindowsApplication*>(WindowsApplication.Get())->RemoveMessageHandler(MessageHandler);
}
}
const FPacketStats& PacketStats = MessageHandler.GetPacketStats();
if (const uint32 NumInvalidPackets = PacketStats.GetNumInvalidPackets())
{
Log(LOG_PREAMBLE, FString::Format(
TEXT("Wintab instance '{0}' encountered {1} invalid packets."), {FWintabInstance::GetName().ToString(), NumInvalidPackets}));
}
if (const uint32 NumLostPackets = PacketStats.GetNumLostPackets())
{
Log(LOG_PREAMBLE, FString::Format(
TEXT("Wintab instance '{0}' encountered {1} lost packets."), {FWintabInstance::GetName().ToString(), NumLostPackets}));
}
}
bool FWintabInstance::AddEventHandler(IStylusInputEventHandler* EventHandler, const EEventHandlerThread Thread)
{
if (!EventHandler)
{
LogWarning(LOG_PREAMBLE, "Tried to add nullptr as event handler.");
return false;
}
if (Thread == EEventHandlerThread::OnGameThread)
{
return MessageHandler.AddEventHandler(EventHandler);
}
if (Thread == EEventHandlerThread::Asynchronous)
{
// TODO Handle asynchronous thread
LogError(LOG_PREAMBLE, "Asynchronous event handler is not supported yet.");
return false;
}
return false;
}
bool FWintabInstance::RemoveEventHandler(IStylusInputEventHandler* EventHandler)
{
if (!EventHandler)
{
LogWarning(LOG_PREAMBLE, "Tried to remove nullptr event handler.");
return false;
}
if (MessageHandler.RemoveEventHandler(EventHandler))
{
return true;
}
// TODO Handle asynchronous thread
return false;
}
const TSharedPtr<IStylusInputTabletContext> FWintabInstance::GetTabletContext(const uint32 TabletContextID)
{
return TabletContexts.Get(TabletContextID);
}
const TSharedPtr<IStylusInputStylusInfo> FWintabInstance::GetStylusInfo(const uint32 StylusID)
{
return StylusInfos.Get(StylusID);
}
float FWintabInstance::GetPacketsPerSecond(const EEventHandlerThread EventHandlerThread) const
{
return MessageHandler.GetPacketStats().GetPacketsPerSecond();
}
FName FWintabInstance::GetInterfaceName()
{
return FWintabAPI::GetName();
}
FText FWintabInstance::GetName()
{
return FText::Format(LOCTEXT("Wintab", "Wintab #{0}"), ID);
}
bool FWintabInstance::WasInitializedSuccessfully() const
{
return true;
}
const FTabletContext* FWintabInstance::GetTabletContextInternal(const uint32 TabletContextID) const
{
return TabletContexts.Get(TabletContextID).Get();
}
uint32 FWintabInstance::GetStylusID(const uint32 TabletContextID, uint32 CursorIndex)
{
DWORD CursorPhysicalID;
UINT CursorType;
if (WintabAPI.WtInfo(WTI_CURSORS + CursorIndex, CSR_PHYSID, &CursorPhysicalID) == sizeof(CursorPhysicalID) &&
WintabAPI.WtInfo(WTI_CURSORS + CursorIndex, CSR_TYPE, &CursorType) == sizeof(CursorType))
{
const uint16 MaskedCursorId = MaskCursorId(CursorType);
const uint16 MaskedCursorType = MaskCursorType(CursorType);
const uint64 CursorID = static_cast<uint64>(MaskedCursorId) << 32 | CursorPhysicalID;
const TTuple<uint64, uint32>* Mapping = CursorIDToStylusIDMappings.FindByPredicate(
[CursorID](const TTuple<uint64, uint32>& Tuple)
{
return Tuple.Get<0>() == CursorID;
});
if (!Mapping)
{
uint32 StylusID = CursorPhysicalID;
// Resolve any collisions of physical IDs, which are only guaranteed to be unique within a (masked) cursor type.
while (StylusInfos.Contains(StylusID))
{
++StylusID;
}
Mapping = &CursorIDToStylusIDMappings.Emplace_GetRef(CursorID, StylusID);
const TSharedRef<FStylusInfo> StylusInfo = StylusInfos.Add(StylusID);
StylusInfo->WintabPhysicalID = CursorPhysicalID;
StylusInfo->WintabCursorType = MaskedCursorType;
const ECursorIndexType CursorIndexType = [&TabletContexts = TabletContexts, TabletContextID, CursorIndex]
{
if (const TSharedPtr<FTabletContext> TabletContextPtr = TabletContexts.Get(TabletContextID))
{
const FTabletContext& TabletContext = *TabletContextPtr;
if (TabletContext.WintabFirstCursor <= CursorIndex && CursorIndex < static_cast<uint8>(TabletContext.WintabFirstCursor + TabletContext.WintabNumCursors))
{
const int8 CursorIndexTypeInt = CursorIndex - TabletContext.WintabFirstCursor;
if (0 <= CursorIndexTypeInt && CursorIndexTypeInt < static_cast<int8>(ECursorIndexType::Num_Enumerators))
{
return static_cast<ECursorIndexType>(CursorIndexTypeInt);
}
}
}
return ECursorIndexType::Invalid_Enumerator;
}();
// If this is the inverted side of a pen, populate the stylus data with the non-inverted side instead.
if (CursorIndexType != ECursorIndexType::Invalid_Enumerator && CursorIsInverted(CursorIndexType))
{
--CursorIndex;
}
FWintabInfoOutputBuffer OutputBuffer;
TCHAR* OutputBufferPtr = OutputBuffer.Allocate(WTI_CURSORS + CursorIndex, CSR_NAME);
if (WintabAPI.WtInfo(WTI_CURSORS + CursorIndex, CSR_NAME, OutputBufferPtr) <= OutputBuffer.SizeInBytes())
{
StylusInfo->Name = FString::Format(TEXT("{0} ({1})"), {OutputBufferPtr, MaskedCursorTypeToString(MaskedCursorType)});
}
else
{
LogWarning(LOG_PREAMBLE, FString::Format(TEXT("Failed to query name for Wintab cursor with index {0}."), {CursorIndex}));
}
BYTE NumButtons;
if (WintabAPI.WtInfo(WTI_CURSORS + CursorIndex, CSR_BUTTONS, &NumButtons) == sizeof(NumButtons))
{
StylusInfo->Buttons.SetNum(NumButtons);
OutputBufferPtr = OutputBuffer.Allocate(WTI_CURSORS + CursorIndex, CSR_BTNNAMES);
if (WintabAPI.WtInfo(WTI_CURSORS + CursorIndex, CSR_BTNNAMES, OutputBufferPtr) <= OutputBuffer.SizeInBytes())
{
const TCHAR* Name = OutputBufferPtr;
int32 ButtonIndex = 0;
while (*Name)
{
StylusInfo->Buttons[ButtonIndex].Name = Name;
Name += StylusInfo->Buttons[ButtonIndex].Name.Len() + 1;
++ButtonIndex;
}
}
else
{
LogWarning(LOG_PREAMBLE, FString::Format(TEXT("Failed to query button names for Wintab cursor with index {0}."), {CursorIndex}));
}
BYTE ButtonMap[32] = {};
if (WintabAPI.WtInfo(WTI_CURSORS + CursorIndex, CSR_BUTTONMAP, &ButtonMap) == sizeof(ButtonMap))
{
for (int32 ButtonIndex = 0; ButtonIndex < NumButtons; ++ButtonIndex)
{
// Todo
}
}
else
{
LogWarning(LOG_PREAMBLE, FString::Format(TEXT("Failed to query button map for Wintab cursor with index {0}."), {CursorIndex}));
}
}
else
{
LogWarning(LOG_PREAMBLE, FString::Format(TEXT("Failed to query number of buttons for Wintab cursor with index {0}."), {CursorIndex}));
}
}
// Make sure a StylusInfo with this ID exists, and the cursor type and physical ID match.
checkSlow(Mapping && StylusInfos.Contains(Mapping->Get<1>()) && StylusInfos.Get(Mapping->Get<1>()).IsValid()
&& StylusInfos.Get(Mapping->Get<1>())->WintabCursorType == MaskedCursorType
&& StylusInfos.Get(Mapping->Get<1>())->WintabPhysicalID == CursorPhysicalID);
return Mapping->Get<1>();
}
else
{
LogWarning(LOG_PREAMBLE, FString::Format(TEXT("Failed to query cursor metadata for cursor index {0}."), {CursorIndex}));
}
return 0;
}
void FWintabInstance::ClearTabletContexts()
{
for (int32 Index = 0, NumIndices = TabletContexts.Num(); Index < NumIndices; ++Index)
{
const HCTX WintabContextHandle = TabletContexts[Index]->WintabContextHandle;
if (WintabAPI.WtClose(WintabContextHandle))
{
LogVerbose(LOG_PREAMBLE, FString::Format(TEXT("Closed Wintab context with handle {0}."),
{HctxToUint32(WintabContextHandle)}));
}
else
{
LogError(LOG_PREAMBLE, FString::Format(TEXT("Could not close Wintab context with handle {0}."),
{HctxToUint32(WintabContextHandle)}));
}
}
TabletContexts.Clear();
}
void FWintabInstance::UpdateTabletContexts()
{
ClearTabletContexts();
UINT WintabContextIndex = 0;
LOGCONTEXTW WintabContext;
while (WintabAPI.WtInfo(WTI_DDCTXS + WintabContextIndex, 0, &WintabContext) == sizeof(WintabContext))
{
if (!SetupWintabContext(WintabAPI, WintabContextIndex, WintabContext))
{
LogError(LOG_PREAMBLE, FString::Format(TEXT("Failed to setup Wintab context with index {0}}."), {WintabContextIndex}));
++WintabContextIndex;
continue;
}
const HCTX WintabContextHandle = WintabAPI.WtOpen(OSWindowHandle, &WintabContext, Windows::TRUE);
if (!WintabContextHandle)
{
LogError(LOG_PREAMBLE, FString::Format(TEXT("Failed to open Wintab context with index {0}."), {WintabContextIndex}));
++WintabContextIndex;
continue;
}
const uint32 TabletContextID = HctxToUint32(WintabContextHandle);
TSharedRef<FTabletContext> TabletContextPtr = [&TabletContexts = TabletContexts, TabletContextID]
{
if (TabletContexts.Contains(TabletContextID))
{
return TabletContexts.Get(TabletContextID).ToSharedRef();
}
return TabletContexts.Add(TabletContextID);
}();
FTabletContext& TabletContext = *TabletContextPtr;
if (!SetupTabletContext(WintabAPI, WintabContextIndex, WintabContextHandle, WintabContext, WindowRect, TabletContext, MessageHandler))
{
LogError(LOG_PREAMBLE, FString::Format(TEXT("Failed setting up tablet context with ID {0}."), {TabletContextID}));
TabletContexts.Remove(TabletContextID);
if (!WintabAPI.WtClose(WintabContextHandle))
{
LogError(LOG_PREAMBLE, FString::Format(TEXT("Could not close Wintab context with handle {0}."), {HctxToUint32(WintabContextHandle)}));
}
++WintabContextIndex;
continue;
}
LogVerbose(LOG_PREAMBLE, FString::Format(TEXT("Added tablet context for ID {0} [{1}, {2}]."), {
TabletContext.ID, TabletContext.Name , TabletContext.PlugAndPlayID
}));
++WintabContextIndex;
}
}
void FWintabInstance::UpdateWindowRect()
{
if (!ensure(WintabAPI.GetWindowRect(OSWindowHandle, &WindowRect)))
{
LogError(LOG_PREAMBLE, "Could not retrieve window rectangle; coordinates mapping will be incorrect!");
}
}
}
#undef LOG_PREAMBLE
#undef LOCTEXT_NAMESPACE