-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Added Wintab tablet api support #20366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
| public (float xTilt, float yTilt) ConvertWintabToStandardTilt(double rawAzimuth, double rawAltitude) | ||
| { | ||
| float azimuthDeg = (float)rawAzimuth / 10f; | ||
| float altitudeDeg = (float)rawAltitude / 10f; | ||
|
|
||
| float azimuthRad = azimuthDeg * MathF.PI / 180f; | ||
|
|
||
| float tiltFromVertical = 90f - altitudeDeg; | ||
|
|
||
| float tiltX, tiltY; | ||
|
|
||
| tiltX = tiltFromVertical * MathF.Sin(azimuthRad); | ||
| tiltY = tiltFromVertical * MathF.Cos(azimuthRad); | ||
|
|
||
| tiltY = -tiltY; | ||
|
|
||
| tiltX = Math.Clamp(tiltX, -90f, 90f); | ||
| tiltY = Math.Clamp(tiltY, -90f, 90f); | ||
|
|
||
| return (tiltX, tiltY); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: Windows Ink tilt values are rounded, this returns decimal values
| private void FlushIntermediatePackets(uint count) | ||
| { | ||
| uint actualSize = 0; | ||
| var packets = _wnData.GetDataPackets(count, true, ref actualSize); | ||
| foreach (var packet in packets) | ||
| { | ||
| s_lastWintabPackets[packet.pkSerialNumber] = packet; | ||
| } | ||
|
|
||
| if (s_lastWintabPackets.Count > MaxWintabPacketHistorySize) | ||
| { | ||
| var keysToRemove = s_lastWintabPackets.Keys | ||
| .OrderBy(k => k) | ||
| .Take(s_lastWintabPackets.Count - MaxWintabPacketHistorySize) | ||
| .ToList(); | ||
|
|
||
| foreach (var key in keysToRemove) | ||
| { | ||
| s_lastWintabPackets.Remove(key); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is not ideal, but Wintab API does not have anything like Ink to return packets with starting index argument. Buffering it into local dictionary on each movement also prevents buffer overflows within Wintab itself.
| /*/// <summary> | ||
| /// Returns a string containing device name. | ||
| /// </summary> | ||
| /// <returns></returns> | ||
| public static string GetDeviceInfo() | ||
| { | ||
| string devInfo = null; | ||
| IntPtr buf = CMemUtils.AllocUnmanagedBuf(MAX_STRING_SIZE); | ||
| try | ||
| { | ||
| int size = (int)CWintabFuncs.WTInfoA( | ||
| (uint)EWTICategoryIndex.WTI_DEVICES, | ||
| (uint)EWTIDevicesIndex.DVC_NAME, buf); | ||
| if (size < 1) | ||
| { | ||
| throw new Exception("GetDeviceInfo returned empty string."); | ||
| } | ||
| // Strip off final null character before marshalling. | ||
| devInfo = CMemUtils.MarshalUnmanagedString(buf, size - 1); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| MessageBox.Show("FAILED GetDeviceInfo: " + ex.ToString()); | ||
| } | ||
| CMemUtils.FreeUnmanagedBuf(buf); | ||
| return devInfo; | ||
| } | ||
| /// <summary> | ||
| /// Returns the default digitizing context, with useful context overrides. | ||
| /// </summary> | ||
| /// <param name="options_I">caller's options; OR'd into context options</param> | ||
| /// <returns>A valid context object or null on error.</returns> | ||
| public static CWintabContext GetDefaultDigitizingContext(ECTXOptionValues options_I = 0) | ||
| { | ||
| // Send all possible data bits (not including extended data). | ||
| // This is redundant with CWintabContext initialization, which | ||
| // also inits with PK_PKTBITS_ALL. | ||
| uint PACKETDATA = (uint)EWintabPacketBit.PK_PKTBITS_ALL; // The Full Monty | ||
| uint PACKETMODE = (uint)EWintabPacketBit.PK_BUTTONS; | ||
| CWintabContext context = GetDefaultContext(EWTICategoryIndex.WTI_DEFCONTEXT); | ||
| if (context != null) | ||
| { | ||
| // Add digitizer-specific context tweaks. | ||
| context.PktMode = 0; // all data in absolute mode (set EWintabPacketBit bit(s) for relative mode) | ||
| context.SysMode = false; // system cursor tracks in absolute mode (zero) | ||
| // Add caller's options. | ||
| context.Options |= (uint)options_I; | ||
| // Set the context data bits. | ||
| context.PktData = PACKETDATA; | ||
| context.PktMode = PACKETMODE; | ||
| context.MoveMask = PACKETDATA; | ||
| context.BtnUpMask = context.BtnDnMask; | ||
| } | ||
| return context; | ||
| } | ||
| /// <summary> | ||
| /// Returns the default system context, with useful context overrides. | ||
| /// </summary> | ||
| /// <param name="options_I">caller's options; OR'd into context options</param> | ||
| /// <returns>A valid context object or null on error.</returns> | ||
| public static CWintabContext GetDefaultSystemContext(ECTXOptionValues options_I = 0) | ||
| { | ||
| // Send all possible data bits (not including extended data). | ||
| // This is redundant with CWintabContext initialization, which | ||
| // also inits with PK_PKTBITS_ALL. | ||
| uint PACKETDATA = (uint)EWintabPacketBit.PK_PKTBITS_ALL; // The Full Monty | ||
| uint PACKETMODE = (uint)EWintabPacketBit.PK_BUTTONS; | ||
| CWintabContext context = GetDefaultContext(EWTICategoryIndex.WTI_DEFSYSCTX); | ||
| if (context != null) | ||
| { | ||
| // TODO: Add system-specific context tweaks. | ||
| // Add caller's options. | ||
| context.Options |= (uint)options_I; | ||
| // Make sure we get data packet messages. | ||
| context.Options |= (uint)ECTXOptionValues.CXO_MESSAGES; | ||
| // Set the context data bits. | ||
| context.PktData = PACKETDATA; | ||
| context.PktMode = PACKETMODE; | ||
| context.MoveMask = PACKETDATA; | ||
| context.BtnUpMask = context.BtnDnMask; | ||
| context.Name = "WintabDN Event Data Context"; | ||
| } | ||
| return context; | ||
| } | ||
| /// <summary> | ||
| /// Helper function to get digitizing or system default context. | ||
| /// </summary> | ||
| /// <param name="contextType_I">Use WTI_DEFCONTEXT for digital context or WTI_DEFSYSCTX for system context</param> | ||
| /// <returns>Returns the default context or null on error.</returns> | ||
| private static CWintabContext GetDefaultContext(EWTICategoryIndex contextIndex_I) | ||
| { | ||
| CWintabContext context = new CWintabContext(); | ||
| IntPtr buf = CMemUtils.AllocUnmanagedBuf(context.LogContext); | ||
| try | ||
| { | ||
| int size = (int)CWintabFuncs.WTInfoA((uint)contextIndex_I, 0, buf); | ||
| context.LogContext = CMemUtils.MarshalUnmanagedBuf<WintabLogContext>(buf, size); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| MessageBox.Show("FAILED GetDefaultContext: " + ex.ToString()); | ||
| } | ||
| CMemUtils.FreeUnmanagedBuf(buf); | ||
| return context; | ||
| } | ||
| /// <summary> | ||
| /// Returns the default device. If this value is -1, then it also known as a "virtual device". | ||
| /// </summary> | ||
| /// <returns></returns> | ||
| public static Int32 GetDefaultDeviceIndex() | ||
| { | ||
| Int32 devIndex = 0; | ||
| IntPtr buf = CMemUtils.AllocUnmanagedBuf(devIndex); | ||
| try | ||
| { | ||
| int size = (int)CWintabFuncs.WTInfoA( | ||
| (uint)EWTICategoryIndex.WTI_DEFCONTEXT, | ||
| (uint)EWTIContextIndex.CTX_DEVICE, buf); | ||
| devIndex = CMemUtils.MarshalUnmanagedBuf<Int32>(buf, size); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| MessageBox.Show("FAILED GetDefaultDeviceIndex: " + ex.ToString()); | ||
| } | ||
| CMemUtils.FreeUnmanagedBuf(buf); | ||
| return devIndex; | ||
| } | ||
| /// <summary> | ||
| /// Returns the WintabAxis object for specified device and dimension. | ||
| /// </summary> | ||
| /// <param name="devIndex_I">Device index (-1 = virtual device)</param> | ||
| /// <param name="dim_I">Dimension: AXIS_X, AXIS_Y or AXIS_Z</param> | ||
| /// <returns></returns> | ||
| public static WintabAxis GetDeviceAxis(Int32 devIndex_I, EAxisDimension dim_I) | ||
| { | ||
| WintabAxis axis = new WintabAxis(); | ||
| IntPtr buf = CMemUtils.AllocUnmanagedBuf(axis); | ||
| try | ||
| { | ||
| int size = (int)CWintabFuncs.WTInfoA( | ||
| (uint)(EWTICategoryIndex.WTI_DEVICES + devIndex_I), | ||
| (uint)dim_I, buf); | ||
| // If size == 0, then returns a zeroed struct. | ||
| axis = CMemUtils.MarshalUnmanagedBuf<WintabAxis>(buf, size); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| MessageBox.Show("FAILED GetDeviceAxis: " + ex.ToString()); | ||
| } | ||
| CMemUtils.FreeUnmanagedBuf(buf); | ||
| return axis; | ||
| } | ||
| /// <summary> | ||
| /// Returns a 3-element array describing the tablet's rotation range and resolution capabilities | ||
| /// </summary> | ||
| /// <returns></returns> | ||
| public static WintabAxisArray GetDeviceRotation(out bool rotationSupported_O) | ||
| { | ||
| WintabAxisArray axisArray = new WintabAxisArray(); | ||
| rotationSupported_O = false; | ||
| IntPtr buf = CMemUtils.AllocUnmanagedBuf(axisArray); | ||
| try | ||
| { | ||
| int size = (int)CWintabFuncs.WTInfoA( | ||
| (uint)EWTICategoryIndex.WTI_DEVICES, | ||
| (uint)EWTIDevicesIndex.DVC_ROTATION, buf); | ||
| // If size == 0, then returns a zeroed struct. | ||
| axisArray = CMemUtils.MarshalUnmanagedBuf<WintabAxisArray>(buf, size); | ||
| rotationSupported_O = (axisArray.array[0].axResolution != 0 && axisArray.array[1].axResolution != 0); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| MessageBox.Show("FAILED GetDeviceRotation: " + ex.ToString()); | ||
| } | ||
| CMemUtils.FreeUnmanagedBuf(buf); | ||
| return axisArray; | ||
| } | ||
| /// <summary> | ||
| /// Returns the number of devices connected (attached). | ||
| /// </summary> | ||
| /// <returns>tablet count</returns> | ||
| public static UInt32 GetNumberOfDevices() | ||
| { | ||
| UInt32 numDevices = 0; | ||
| IntPtr buf = CMemUtils.AllocUnmanagedBuf(numDevices); | ||
| try | ||
| { | ||
| int size = (int)CWintabFuncs.WTInfoA( | ||
| (uint)EWTICategoryIndex.WTI_INTERFACE, | ||
| (uint)EWTIInterfaceIndex.IFC_NDEVICES, buf); | ||
| numDevices = CMemUtils.MarshalUnmanagedBuf<UInt32>(buf, size); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| MessageBox.Show("FAILED GetNumberOfDevices: " + ex.ToString()); | ||
| } | ||
| CMemUtils.FreeUnmanagedBuf(buf); | ||
| return numDevices; | ||
| } | ||
| /// <summary> | ||
| /// Returns whether a stylus is currently connected to the active cursor. | ||
| /// </summary> | ||
| /// <returns></returns> | ||
| public static bool IsStylusActive() | ||
| { | ||
| bool isStylusActive = false; | ||
| IntPtr buf = CMemUtils.AllocUnmanagedBuf(isStylusActive); | ||
| try | ||
| { | ||
| int size = (int)CWintabFuncs.WTInfoA( | ||
| (uint)EWTICategoryIndex.WTI_INTERFACE, | ||
| (uint)EWTIInterfaceIndex.IFC_NDEVICES, buf); | ||
| isStylusActive = CMemUtils.MarshalUnmanagedBuf<bool>(buf, size); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| MessageBox.Show("FAILED GetNumberOfDevices: " + ex.ToString()); | ||
| } | ||
| CMemUtils.FreeUnmanagedBuf(buf); | ||
| return isStylusActive; | ||
| } | ||
| /// <summary> | ||
| /// Returns a string containing the name of the selected stylus. | ||
| /// </summary> | ||
| /// <param name="index_I">indicates stylus type</param> | ||
| /// <returns></returns> | ||
| public static string GetStylusName(EWTICursorNameIndex index_I) | ||
| { | ||
| string stylusName = null; | ||
| IntPtr buf = CMemUtils.AllocUnmanagedBuf(MAX_STRING_SIZE); | ||
| try | ||
| { | ||
| int size = (int)CWintabFuncs.WTInfoA( | ||
| (uint)index_I, | ||
| (uint)EWTICursorsIndex.CSR_NAME, buf); | ||
| if (size < 1) | ||
| { | ||
| throw new Exception("GetStylusName returned empty string."); | ||
| } | ||
| // Strip off final null character before marshalling. | ||
| stylusName = CMemUtils.MarshalUnmanagedString(buf, size - 1); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| MessageBox.Show("FAILED GetDeviceInfo: " + ex.ToString()); | ||
| } | ||
| CMemUtils.FreeUnmanagedBuf(buf); | ||
| return stylusName; | ||
| } | ||
| /// <summary> | ||
| /// Return the WintabAxis object for the specified dimension. | ||
| /// </summary> | ||
| /// <param name="dimension_I">Dimension to fetch (eg: x, y)</param> | ||
| /// <returns></returns> | ||
| public static WintabAxis GetTabletAxis(EAxisDimension dimension_I) | ||
| { | ||
| WintabAxis axis = new WintabAxis(); | ||
| IntPtr buf = CMemUtils.AllocUnmanagedBuf(axis); | ||
| try | ||
| { | ||
| int size = (int)CWintabFuncs.WTInfoA( | ||
| (uint)EWTICategoryIndex.WTI_DEVICES, | ||
| (uint)dimension_I, buf); | ||
| axis = CMemUtils.MarshalUnmanagedBuf<WintabAxis>(buf, size); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| MessageBox.Show("FAILED GetMaxPressure: " + ex.ToString()); | ||
| } | ||
| CMemUtils.FreeUnmanagedBuf(buf); | ||
| return axis; | ||
| } | ||
| /// <summary> | ||
| /// Return the number of tablets that have at some time been attached. | ||
| /// A record of these devices is in the tablet settings. Since there | ||
| /// is no direct query for this value, we have to enumerate all of | ||
| /// the tablet settings. | ||
| /// </summary> | ||
| /// <returns>tablet count</returns> | ||
| public static UInt32 GetNumberOfConfiguredDevices() | ||
| { | ||
| UInt32 numConfiguredTablets = 0; | ||
| try | ||
| { | ||
| WintabLogContext ctx = new WintabLogContext(); | ||
| IntPtr buf = CMemUtils.AllocUnmanagedBuf(ctx); | ||
| for (Int32 idx = 0; idx < MAX_NUM_ATTACHED_TABLETS; idx++) | ||
| { | ||
| int size = (int)CWintabFuncs.WTInfoA( | ||
| (UInt32)(EWTICategoryIndex.WTI_DDCTXS + idx), 0, buf); | ||
| if (size == 0) | ||
| { | ||
| break; | ||
| } | ||
| else | ||
| { | ||
| numConfiguredTablets++; | ||
| } | ||
| } | ||
| CMemUtils.FreeUnmanagedBuf(buf); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| MessageBox.Show("FAILED GetNumberOfConfiguredDevices: " + ex.ToString()); | ||
| } | ||
| return numConfiguredTablets; | ||
| } | ||
| /// <summary> | ||
| /// Returns a list of indecies of previous or currently attached devices. | ||
| /// It is up to the caller to use the list to determine which devices are | ||
| /// actually physically device by responding to data events for those devices. | ||
| /// Devices that are not physically attached will, of course, never send | ||
| /// a data event. | ||
| /// </summary> | ||
| /// <returns></returns> | ||
| public static List<Byte> GetFoundDevicesIndexList() | ||
| { | ||
| List<Byte> list = new List<Byte>(); | ||
| try | ||
| { | ||
| WintabLogContext ctx = new WintabLogContext(); | ||
| IntPtr buf = CMemUtils.AllocUnmanagedBuf(ctx); | ||
| for (Int32 idx = 0; idx < MAX_NUM_ATTACHED_TABLETS; idx++) | ||
| { | ||
| int size = (int)CWintabFuncs.WTInfoA( | ||
| (UInt32)(EWTICategoryIndex.WTI_DDCTXS + idx), 0, buf); | ||
| if (size == 0) | ||
| { | ||
| break; | ||
| } | ||
| else | ||
| { | ||
| list.Add((Byte)idx); | ||
| } | ||
| } | ||
| CMemUtils.FreeUnmanagedBuf(buf); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| MessageBox.Show("FAILED GetNumberOfConfiguredDevices: " + ex.ToString()); | ||
| } | ||
| return list; | ||
| } | ||
| }*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left this from original Wacom sample implementation, in case it is needed later. I can remove it if needed
|
You can test this PR using the following package version. |
|
You can test this PR using the following package version. |
|
You can test this PR using the following package version. |
|
You can test this PR using the following package version. |
|
You can test this PR using the following package version. |
What does the pull request do?
Added Wintab tablet support API for Windows.
Currently, Avalonia only handles tablet support via Windows Ink. Unfortunately, Windows Ink comes with a lot of unwanted features and weird design decisions. A lot of tablet users still prefer to use Wintab for painting and navigation purposes.
Additionally, Wintab is a much older, more mature API and has broader hardware support than Windows Ink.
What is the current behavior?
Users with disabled Windows Ink cannot use tablet features like pressure and tilt.
What is the updated/expected behavior with this PR?
With this PR, users who have disabled Windows Ink will fall back to Wintab. Windows Ink is still the default and will be used when possible.
How was the solution implemented (if it's not obvious)?
Wintab is hooked to AppWndProc with 2 messages:
WT_PROXIMITYandWT_PACKET.Logic to pick correct api works like this:
WT_PACKETorWT_PROXIMITYtells the app, that tablet is present, Wintab context is closed and Windows Ink Test flag is enabledMOUSEorPOINTERevent decides whether Ink or Wintab should be used. If the next event isPOINTER, it means user has Windows Ink enabled and it will be used (Wintab context remains closed).If the next event is MOUSE without Pen flag, it means that Ink is not enabled, Wintab context is reopened.
Unfortunately, as far as I know and tested, this is the best way to determine if tablet settings have Ink enabled.
Huion tablet does not report
WM_POINTERevents if Wintab context is opened, so there's no way to know if tablet drivers have "Windows Ink" checkbox enabled. So we must enable wintab context first.If we don't open Wintab context and Windows Ink is disabled in tablet settings, the only event avalonia will get is MOUSE without any pen flags. So (as far as I know) it's impossible to tell if pen is used.
There might be a theoretical possibility, where the next event after first
WT_PROXIMITYorWT_PACKETwill be a mouse event with Ink enabled and app could falsely assume, that it is disabled. Although it is really unlikelyChecklist
Breaking changes
I don't think this can break existing solutions. Maybe only for apps, that rely heavily on Ink and scenario I described above with false positive Ink disabled happens.
Obsoletions / Deprecations
Fixed issues