Windows: add support for Windows Ink.

Before this Blender always needed the Wintab driver. This adds support for the
native pressure API in Windows 8+, making it possible to get pressure sensitivity
on e.g. Microsoft Surface hardware without any extra drivers.

By default Blender will automatically use Wintab if available, and if not use
Windows Ink instead. There is also a new user preference to explicitly specify
which API to use if automatic detection fails.

Fixes T57869: no pressure sensitivity with Surface pen or laptop.

Code by Christopher Peerman with some tweaks by Brecht Van Lommel.

Differential Revision: https://developer.blender.org/D4165
This commit is contained in:
Christopher Peerman 2019-01-14 17:46:49 +01:00 committed by Brecht Van Lommel
parent eaf282b375
commit 4693207918
Notes: blender-bot 2023-02-14 09:33:11 +01:00
Referenced by issue #57869, No pressure sensitivity with Surface Pen on Surface Laptop
14 changed files with 277 additions and 7 deletions

View File

@ -742,6 +742,13 @@ extern GHOST_TSuccess GHOST_ActivateOpenGLContext(GHOST_ContextHandle contexthan
*/
extern GHOST_TSuccess GHOST_ReleaseOpenGLContext(GHOST_ContextHandle contexthandle);
/**
* Set which tablet API to use. Only affects Windows, other platforms have a single API.
* \param systemhandle The handle to the system
* \param api Enum indicating which API to use.
*/
extern void GHOST_SetTabletAPI(GHOST_SystemHandle systemhandle, GHOST_TTabletAPI api);
/**
* Returns the status of the tablet
* \param windowhandle The handle to the window

View File

@ -397,6 +397,12 @@ public:
*/
virtual GHOST_TSuccess getButtonState(GHOST_TButtonMask mask, bool& isDown) const = 0;
/**
* Set which tablet API to use. Only affects Windows, other platforms have a single API.
* \param api Enum indicating which API to use.
*/
virtual void setTabletAPI(GHOST_TTabletAPI api) = 0;
#ifdef WITH_INPUT_NDOF
/**
* Sets 3D mouse deadzone

View File

@ -90,6 +90,12 @@ typedef enum {
GHOST_kTabletModeEraser
} GHOST_TTabletMode;
typedef enum {
GHOST_kTabletAutomatic = 0,
GHOST_kTabletNative,
GHOST_kTabletWintab,
} GHOST_TTabletAPI;
typedef struct GHOST_TabletData {
GHOST_TTabletMode Active; /* 0=None, 1=Stylus, 2=Eraser */
float Pressure; /* range 0.0 (not touching) to 1.0 (full pressure) */

View File

@ -750,8 +750,13 @@ GHOST_TSuccess GHOST_InvalidateWindow(GHOST_WindowHandle windowhandle)
return window->invalidate();
}
void GHOST_SetTabletAPI(GHOST_SystemHandle systemhandle, GHOST_TTabletAPI api)
{
GHOST_ISystem *system = (GHOST_ISystem *) systemhandle;
system->setTabletAPI(api);
}
extern const GHOST_TabletData *GHOST_GetTabletData(GHOST_WindowHandle windowhandle)
const GHOST_TabletData *GHOST_GetTabletData(GHOST_WindowHandle windowhandle)
{
return ((GHOST_IWindow *)windowhandle)->GetTabletData();
}

View File

@ -52,10 +52,11 @@ GHOST_System::GHOST_System()
m_displayManager(NULL),
m_timerManager(NULL),
m_windowManager(NULL),
m_eventManager(NULL)
m_eventManager(NULL),
#ifdef WITH_INPUT_NDOF
, m_ndofManager(0)
m_ndofManager(0),
#endif
m_tabletAPI(GHOST_kTabletAutomatic)
{
}
@ -297,6 +298,16 @@ GHOST_TSuccess GHOST_System::getButtonState(GHOST_TButtonMask mask, bool& isDown
return success;
}
void GHOST_System::setTabletAPI(GHOST_TTabletAPI api)
{
m_tabletAPI = api;
}
bool GHOST_System::useTabletAPI(GHOST_TTabletAPI api) const
{
return (m_tabletAPI == GHOST_kTabletAutomatic || m_tabletAPI == api);
}
#ifdef WITH_INPUT_NDOF
void GHOST_System::setNDOFDeadZone(float deadzone)
{

View File

@ -247,6 +247,17 @@ public:
*/
GHOST_TSuccess getButtonState(GHOST_TButtonMask mask, bool& isDown) const;
/**
* Set which tablet API to use. Only affects Windows, other platforms have a single API.
* \param api Enum indicating which API to use.
*/
void setTabletAPI(GHOST_TTabletAPI api);
/**
* Test if given tablet API should be used by event handling.
*/
bool useTabletAPI(GHOST_TTabletAPI api) const;
#ifdef WITH_INPUT_NDOF
/***************************************************************************************
* Access to 3D mouse.
@ -380,6 +391,8 @@ protected:
/** Settings of the display before the display went fullscreen. */
GHOST_DisplaySetting m_preFullScreenSetting;
/** Which tablet API to use. */
GHOST_TTabletAPI m_tabletAPI;
};
inline GHOST_TimerManager *GHOST_System::getTimerManager() const

View File

@ -122,6 +122,10 @@
#define WM_DPICHANGED 0x02E0
#endif // WM_DPICHANGED
#ifndef WM_POINTERUPDATE
#define WM_POINTERUPDATE 0x0245
#endif // WM_POINTERUPDATE
/* Workaround for some laptop touchpads, some of which seems to
* have driver issues which makes it so window function receives
* the message, but PeekMessage doesn't pick those messages for
@ -1233,6 +1237,9 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, UINT msg, WPARAM wParam,
case WT_PROXIMITY:
window->processWin32TabletInitEvent();
break;
case WM_POINTERUPDATE:
window->processWin32PointerEvent(wParam);
break;
////////////////////////////////////////////////////////////////////////
// Mouse events, processed
////////////////////////////////////////////////////////////////////////
@ -1450,8 +1457,6 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, UINT msg, WPARAM wParam,
* change such as when the window is moved to a monitor with a different DPI.
*/
{
WORD newYAxisDPI = HIWORD(wParam);
WORD newXAxisDPI = LOWORD(wParam);
// The suggested new size and position of the window.
RECT* const suggestedWindowRect = (RECT*)lParam;

View File

@ -51,7 +51,9 @@
#include <string.h>
#include <assert.h>
#ifndef GET_POINTERID_WPARAM
#define GET_POINTERID_WPARAM(wParam) (LOWORD(wParam))
#endif // GET_POINTERID_WPARAM
const wchar_t *GHOST_WindowWin32::s_windowClassName = L"GHOST_WindowClass";
const int GHOST_WindowWin32::s_maxTitleLength = 128;
@ -89,6 +91,8 @@ GHOST_WindowWin32::GHOST_WindowWin32(GHOST_SystemWin32 *system,
m_wantAlphaBackground(alphaBackground),
m_normal_state(GHOST_kWindowStateNormal),
m_user32(NULL),
m_fpGetPointerInfo(NULL),
m_fpGetPointerPenInfo(NULL),
m_parentWindowHwnd(parentwindowhwnd),
m_debug_context(is_debug)
{
@ -284,6 +288,12 @@ GHOST_WindowWin32::GHOST_WindowWin32(GHOST_SystemWin32 *system,
RegisterRawInputDevices(&device, 1, sizeof(device));
}
// Initialize Windows Ink
if (m_user32) {
m_fpGetPointerInfo = (GHOST_WIN32_GetPointerInfo) ::GetProcAddress(m_user32, "GetPointerInfo");
m_fpGetPointerPenInfo = (GHOST_WIN32_GetPointerPenInfo) ::GetProcAddress(m_user32, "GetPointerPenInfo");
}
// Initialize Wintab
m_wintab.handle = ::LoadLibrary("Wintab32.dll");
if (m_wintab.handle) {
@ -353,6 +363,7 @@ GHOST_WindowWin32::~GHOST_WindowWin32()
if (m_Bar) {
m_Bar->SetProgressState(m_hWnd, TBPF_NOPROGRESS);
m_Bar->Release();
m_Bar = NULL;
}
if (m_wintab.handle) {
@ -364,6 +375,13 @@ GHOST_WindowWin32::~GHOST_WindowWin32()
memset(&m_wintab, 0, sizeof(m_wintab));
}
if (m_user32) {
FreeLibrary(m_user32);
m_user32 = NULL;
m_fpGetPointerInfo = NULL;
m_fpGetPointerPenInfo = NULL;
}
if (m_customCursor) {
DestroyCursor(m_customCursor);
m_customCursor = NULL;
@ -371,6 +389,7 @@ GHOST_WindowWin32::~GHOST_WindowWin32()
if (m_hWnd != NULL && m_hDC != NULL && releaseNativeHandles()) {
::ReleaseDC(m_hWnd, m_hDC);
m_hDC = NULL;
}
if (m_hWnd) {
@ -379,6 +398,7 @@ GHOST_WindowWin32::~GHOST_WindowWin32()
RevokeDragDrop(m_hWnd);
// Release our reference of the DropTarget and it will delete itself eventually.
m_dropTarget->Release();
m_dropTarget = NULL;
}
::SetWindowLongPtr(m_hWnd, GWLP_USERDATA, NULL);
::DestroyWindow(m_hWnd);
@ -866,8 +886,62 @@ GHOST_TSuccess GHOST_WindowWin32::setWindowCursorShape(GHOST_TStandardCursor cur
return GHOST_kSuccess;
}
void GHOST_WindowWin32::processWin32PointerEvent(WPARAM wParam)
{
if (!m_system->useTabletAPI(GHOST_kTabletNative)) {
return; // Other tablet API specified by user
}
if (!m_fpGetPointerInfo || !m_fpGetPointerPenInfo) {
return; // OS version does not support pointer API
}
UINT32 pointerId = GET_POINTERID_WPARAM(wParam);
POINTER_INFO pointerInfo;
if (!m_fpGetPointerInfo(pointerId, &pointerInfo)) {
return; // Invalid pointer info
}
m_tabletData.Active = GHOST_kTabletModeNone;
m_tabletData.Pressure = 1.0f;
m_tabletData.Xtilt = 0.0f;
m_tabletData.Ytilt = 0.0f;
if (pointerInfo.pointerType & PT_POINTER) {
POINTER_PEN_INFO pointerPenInfo;
if (!m_fpGetPointerPenInfo(pointerId, &pointerPenInfo)) {
return;
}
// With the Microsoft Surface Pen if you hover the within 1cm of the screen the WM_POINTERUPDATE
// event will fire with PEN_MASK_PRESSURE mask set and zero pressure. In this case we disable
// tablet mode until the pen is physically touching. This enables the user to switch to the
// mouse and draw at full pressure.
if (pointerPenInfo.penMask & PEN_MASK_PRESSURE && pointerPenInfo.pressure > 0) {
m_tabletData.Active = GHOST_kTabletModeStylus;
m_tabletData.Pressure = pointerPenInfo.pressure / 1024.0f;
}
if (pointerPenInfo.penFlags & PEN_FLAG_ERASER) {
m_tabletData.Active = GHOST_kTabletModeEraser;
}
if (pointerPenInfo.penFlags & PEN_MASK_TILT_X) {
m_tabletData.Xtilt = fmin(fabs(pointerPenInfo.tiltX / 90), 1.0f);
}
if (pointerPenInfo.penFlags & PEN_MASK_TILT_Y) {
m_tabletData.Ytilt = fmin(fabs(pointerPenInfo.tiltY / 90), 1.0f);
}
}
}
void GHOST_WindowWin32::processWin32TabletActivateEvent(WORD state)
{
if (!m_system->useTabletAPI(GHOST_kTabletWintab)) {
return;
}
if (m_wintab.enable && m_wintab.tablet) {
m_wintab.enable(m_wintab.tablet, state);
@ -879,6 +953,10 @@ void GHOST_WindowWin32::processWin32TabletActivateEvent(WORD state)
void GHOST_WindowWin32::processWin32TabletInitEvent()
{
if (!m_system->useTabletAPI(GHOST_kTabletWintab)) {
return;
}
// Let's see if we can initialize tablet here
if (m_wintab.info && m_wintab.tablet) {
AXIS Pressure, Orientation[3]; /* The maximum tablet size */
@ -903,10 +981,16 @@ void GHOST_WindowWin32::processWin32TabletInitEvent()
m_tabletData.Active = GHOST_kTabletModeNone;
}
m_tabletData.Active = GHOST_kTabletModeNone;
}
void GHOST_WindowWin32::processWin32TabletEvent(WPARAM wParam, LPARAM lParam)
{
if (!m_system->useTabletAPI(GHOST_kTabletWintab)) {
return;
}
if (m_wintab.packet && m_wintab.tablet) {
PACKET pkt;
if (m_wintab.packet((HCTX)lParam, wParam, &pkt)) {
@ -972,6 +1056,10 @@ void GHOST_WindowWin32::processWin32TabletEvent(WPARAM wParam, LPARAM lParam)
void GHOST_WindowWin32::bringTabletContextToFront()
{
if (!m_system->useTabletAPI(GHOST_kTabletWintab)) {
return;
}
if (m_wintab.overlap && m_wintab.tablet) {
m_wintab.overlap(m_wintab.tablet, TRUE);
}

View File

@ -68,6 +68,79 @@ typedef UINT(API * GHOST_WIN32_GetDpiForWindow)(HWND);
#define USER_DEFAULT_SCREEN_DPI 96
#endif // USER_DEFAULT_SCREEN_DPI
// typedefs for user32 functions to allow pointer functions
enum tagPOINTER_INPUT_TYPE {
PT_POINTER = 1, // Generic pointer
PT_TOUCH = 2, // Touch
PT_PEN = 3, // Pen
PT_MOUSE = 4, // Mouse
#if(WINVER >= 0x0603)
PT_TOUCHPAD = 5, // Touchpad
#endif /* WINVER >= 0x0603 */
};
typedef enum tagPOINTER_BUTTON_CHANGE_TYPE {
POINTER_CHANGE_NONE,
POINTER_CHANGE_FIRSTBUTTON_DOWN,
POINTER_CHANGE_FIRSTBUTTON_UP,
POINTER_CHANGE_SECONDBUTTON_DOWN,
POINTER_CHANGE_SECONDBUTTON_UP,
POINTER_CHANGE_THIRDBUTTON_DOWN,
POINTER_CHANGE_THIRDBUTTON_UP,
POINTER_CHANGE_FOURTHBUTTON_DOWN,
POINTER_CHANGE_FOURTHBUTTON_UP,
POINTER_CHANGE_FIFTHBUTTON_DOWN,
POINTER_CHANGE_FIFTHBUTTON_UP,
} POINTER_BUTTON_CHANGE_TYPE;
typedef DWORD POINTER_INPUT_TYPE;
typedef UINT32 POINTER_FLAGS;
typedef struct tagPOINTER_INFO {
POINTER_INPUT_TYPE pointerType;
UINT32 pointerId;
UINT32 frameId;
POINTER_FLAGS pointerFlags;
HANDLE sourceDevice;
HWND hwndTarget;
POINT ptPixelLocation;
POINT ptHimetricLocation;
POINT ptPixelLocationRaw;
POINT ptHimetricLocationRaw;
DWORD dwTime;
UINT32 historyCount;
INT32 InputData;
DWORD dwKeyStates;
UINT64 PerformanceCount;
POINTER_BUTTON_CHANGE_TYPE ButtonChangeType;
} POINTER_INFO;
typedef UINT32 PEN_FLAGS;
#define PEN_FLAG_NONE 0x00000000 // Default
#define PEN_FLAG_BARREL 0x00000001 // The barrel button is pressed
#define PEN_FLAG_INVERTED 0x00000002 // The pen is inverted
#define PEN_FLAG_ERASER 0x00000004 // The eraser button is pressed
typedef UINT32 PEN_MASK;
#define PEN_MASK_NONE 0x00000000 // Default - none of the optional fields are valid
#define PEN_MASK_PRESSURE 0x00000001 // The pressure field is valid
#define PEN_MASK_ROTATION 0x00000002 // The rotation field is valid
#define PEN_MASK_TILT_X 0x00000004 // The tiltX field is valid
#define PEN_MASK_TILT_Y 0x00000008 // The tiltY field is valid
typedef struct tagPOINTER_PEN_INFO {
POINTER_INFO pointerInfo;
PEN_FLAGS penFlags;
PEN_MASK penMask;
UINT32 pressure;
UINT32 rotation;
INT32 tiltX;
INT32 tiltY;
} POINTER_PEN_INFO;
typedef BOOL (API * GHOST_WIN32_GetPointerInfo)(UINT32 pointerId, POINTER_INFO *pointerInfo);
typedef BOOL (API * GHOST_WIN32_GetPointerPenInfo)(UINT32 pointerId, POINTER_PEN_INFO *penInfo);
/**
* GHOST window on M$ Windows OSs.
* \author Maarten Gribnau
@ -253,6 +326,7 @@ public:
return &m_tabletData;
}
void processWin32PointerEvent(WPARAM wParam);
void processWin32TabletActivateEvent(WORD state);
void processWin32TabletInitEvent();
void processWin32TabletEvent(WPARAM wParam, LPARAM lParam);
@ -377,6 +451,8 @@ private:
/** user32 dll handle*/
HMODULE m_user32;
GHOST_WIN32_GetPointerInfo m_fpGetPointerInfo;
GHOST_WIN32_GetPointerPenInfo m_fpGetPointerPenInfo;
/** Hwnd to parent window */
GHOST_TEmbedderWindowID m_parentWindowHwnd;

View File

@ -1504,6 +1504,11 @@ class USERPREF_PT_input_devices_tablet(PreferencePanel):
prefs = context.preferences
inputs = prefs.inputs
import sys
if sys.platform[:3] == "win":
layout.prop(inputs, "tablet_api")
layout.separator()
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
flow.prop(inputs, "pressure_threshold_max")

View File

@ -694,13 +694,16 @@ typedef struct UserDef {
/** Seconds to zoom around current frame. */
float view_frame_seconds;
char _pad1[4];
char _pad1[2];
/** Private, defaults to 20 for 72 DPI setting. */
short widget_unit;
short anisotropic_filter;
short use_16bit_textures, use_gpu_mipmap;
/** Tablet API to use (Windows only). */
short tablet_api;
/** Raw tablet pressure that maps to 100%. */
float pressure_threshold_max;
/** Curve non-linearity parameter. */
@ -915,6 +918,13 @@ typedef enum eUserpref_UI_Flag2 {
USER_TRACKPAD_NATURAL = (1 << 2),
} eUserpref_UI_Flag2;
/* UserDef.tablet_api */
typedef enum eUserpref_TableAPI {
USER_TABLET_AUTOMATIC = 0,
USER_TABLET_NATIVE = 1,
USER_TABLET_WINTAB = 2,
} eUserpref_TabletAPI;
/* UserDef.app_flag */
typedef enum eUserpref_APP_Flag {
USER_APP_LOCK_UI_LAYOUT = (1 << 0),

View File

@ -236,6 +236,11 @@ static void rna_userdef_autokeymode_set(PointerRNA *ptr, int value)
}
}
static void rna_userdef_tablet_api_update(Main *UNUSED(bmain), Scene *UNUSED(scene), PointerRNA *UNUSED(ptr))
{
WM_init_tablet_api();
}
#ifdef WITH_INPUT_NDOF
static void rna_userdef_ndof_deadzone_update(Main *UNUSED(bmain), Scene *UNUSED(scene), PointerRNA *ptr)
{
@ -4608,6 +4613,13 @@ static void rna_def_userdef_input(BlenderRNA *brna)
};
#endif /* WITH_INPUT_NDOF */
static const EnumPropertyItem tablet_api[] = {
{USER_TABLET_AUTOMATIC, "AUTOMATIC", 0, "Automatic", "Automatically choose Wintab or Windows Ink depending on the device"},
{USER_TABLET_NATIVE, "WINDOWS_INK", 0, "Windows Ink", "Use native Windows Ink API, for modern tablet and pen devices. Requires Windows 8 or newer"},
{USER_TABLET_WINTAB, "WINTAB", 0, "Wintab", "Use Wintab driver for older tablets and Windows versions"},
{0, NULL, 0, NULL, NULL}
};
static const EnumPropertyItem view_zoom_styles[] = {
{USER_ZOOM_CONT, "CONTINUE", 0, "Continue", "Old style zoom, continues while moving mouse up or down"},
{USER_ZOOM_DOLLY, "DOLLY", 0, "Dolly", "Zoom in and out based on vertical mouse movement"},
@ -4733,6 +4745,11 @@ static void rna_def_userdef_input(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Softness",
"Adjusts softness of the low pressure response onset using a gamma curve");
prop = RNA_def_property(srna, "tablet_api", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_items(prop, tablet_api);
RNA_def_property_ui_text(prop, "Tablet API", "Select the tablet API to use for pressure sensitivity");
RNA_def_property_update(prop, 0, "rna_userdef_tablet_api_update");
#ifdef WITH_INPUT_NDOF
/* 3D mouse settings */
/* global options */

View File

@ -91,6 +91,7 @@ void WM_init_state_fullscreen_set(void);
void WM_init_state_normal_set(void);
void WM_init_window_focus_set(bool do_it);
void WM_init_native_pixels(bool do_it);
void WM_init_tablet_api(void);
void WM_init (struct bContext *C, int argc, const char **argv);
void WM_exit_ext (struct bContext *C, const bool do_python);

View File

@ -1690,6 +1690,8 @@ void wm_ghost_init(bContext *C)
}
GHOST_UseWindowFocus(wm_init_state.window_focus);
WM_init_tablet_api();
}
}
@ -1979,6 +1981,24 @@ void WM_init_native_pixels(bool do_it)
wm_init_state.native_pixels = do_it;
}
void WM_init_tablet_api(void)
{
if (g_system) {
switch(U.tablet_api) {
case USER_TABLET_NATIVE:
GHOST_SetTabletAPI(g_system, GHOST_kTabletNative);
break;
case USER_TABLET_WINTAB:
GHOST_SetTabletAPI(g_system, GHOST_kTabletWintab);
break;
case USER_TABLET_AUTOMATIC:
default:
GHOST_SetTabletAPI(g_system, GHOST_kTabletAutomatic);
break;
}
}
}
/* This function requires access to the GHOST_SystemHandle (g_system) */
void WM_cursor_warp(wmWindow *win, int x, int y)
{