Changeset View
Standalone View
intern/ghost/intern/GHOST_TrackpadWin32.cpp
- This file was added.
| /* SPDX-License-Identifier: GPL-2.0-or-later */ | |||||
| /** \file | |||||
| * \ingroup GHOST | |||||
| */ | |||||
| #include <cmath> | |||||
| #include "GHOST_Debug.h" | |||||
| #include "GHOST_TrackpadWin32.h" | |||||
| GHOST_DirectManipulationHelper::GHOST_DirectManipulationHelper( | |||||
| HWND hWnd, | |||||
| Microsoft::WRL::ComPtr<IDirectManipulationManager> directManipulationManager, | |||||
| Microsoft::WRL::ComPtr<IDirectManipulationUpdateManager> directManipulationUpdateManager, | |||||
| Microsoft::WRL::ComPtr<IDirectManipulationViewport> directManipulationViewport, | |||||
| Microsoft::WRL::ComPtr<GHOST_DirectManipulationViewportEventHandler> | |||||
| directManipulationEventHandler, | |||||
| DWORD directManipulationViewportHandlerCookie, | |||||
| bool isScrollDirectionInverted) | |||||
| : m_hWnd(hWnd), | |||||
nicholas_rishel: So after a bit more research into this, I think it's impossible to move the `HWND` from this… | |||||
| m_scrollDirectionRegKey(NULL), | |||||
| m_scrollDirectionChangeEvent(NULL), | |||||
| m_directManipulationManager(directManipulationManager), | |||||
| m_directManipulationUpdateManager(directManipulationUpdateManager), | |||||
| m_directManipulationViewport(directManipulationViewport), | |||||
| m_directManipulationEventHandler(directManipulationEventHandler), | |||||
| m_directManipulationViewportHandlerCookie(directManipulationViewportHandlerCookie), | |||||
| m_isScrollDirectionInverted(isScrollDirectionInverted) | |||||
| { | |||||
Done Inline ActionsDM_ASSERT is a tad misleading, assert is expected to be a noop in release. DM_CHECK_RESULT_AND_EXIT_EARLY was suggested as better but not ideal alternative when I asked other maintainers. I tend to prefer not using macros, but in this case I think it's the right call given the constraints. nicholas_rishel: `DM_ASSERT` is a tad misleading, assert is expected to be a noop in release. | |||||
| } | |||||
| GHOST_DirectManipulationHelper *GHOST_DirectManipulationHelper::create(HWND hWnd, uint16_t dpi) | |||||
| { | |||||
| #define DM_CHECK_RESULT_AND_EXIT_EARLY(hr, failMessage) \ | |||||
| { \ | |||||
| if (!SUCCEEDED(hr)) { \ | |||||
| GHOST_PRINT(failMessage); \ | |||||
| return nullptr; \ | |||||
| } \ | |||||
| } | |||||
| Microsoft::WRL::ComPtr<IDirectManipulationManager> directManipulationManager; | |||||
| HRESULT hr = ::CoCreateInstance(CLSID_DirectManipulationManager, | |||||
| nullptr, | |||||
| CLSCTX_INPROC_SERVER, | |||||
| IID_PPV_ARGS(&directManipulationManager)); | |||||
| DM_CHECK_RESULT_AND_EXIT_EARLY(hr, "DirectManipulationManager create failed\n"); | |||||
| /* Since we want to use fake viewport, we need to send fake updates to UpdateManager. */ | |||||
| Microsoft::WRL::ComPtr<IDirectManipulationUpdateManager> directManipulationUpdateManager; | |||||
| hr = directManipulationManager->GetUpdateManager(IID_PPV_ARGS(&directManipulationUpdateManager)); | |||||
| DM_CHECK_RESULT_AND_EXIT_EARLY(hr, "Get UpdateManager failed\n"); | |||||
| Microsoft::WRL::ComPtr<IDirectManipulationViewport> directManipulationViewport; | |||||
| hr = directManipulationManager->CreateViewport( | |||||
| nullptr, hWnd, IID_PPV_ARGS(&directManipulationViewport)); | |||||
| DM_CHECK_RESULT_AND_EXIT_EARLY(hr, "Viewport create failed\n"); | |||||
| DIRECTMANIPULATION_CONFIGURATION configuration = | |||||
| DIRECTMANIPULATION_CONFIGURATION_INTERACTION | | |||||
| DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_X | | |||||
| DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_Y | | |||||
| DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_INERTIA | | |||||
| DIRECTMANIPULATION_CONFIGURATION_SCALING; | |||||
| hr = directManipulationViewport->ActivateConfiguration(configuration); | |||||
| DM_CHECK_RESULT_AND_EXIT_EARLY(hr, "Viewport set ActivateConfiguration failed\n"); | |||||
| /* Since we are using fake viewport and only want to use Direct Manipulation for touchpad, we | |||||
| * need to use MANUALUPDATE option. */ | |||||
| hr = directManipulationViewport->SetViewportOptions( | |||||
| DIRECTMANIPULATION_VIEWPORT_OPTIONS_MANUALUPDATE); | |||||
| DM_CHECK_RESULT_AND_EXIT_EARLY(hr, "Viewport set ViewportOptions failed\n"); | |||||
| /* We receive Direct Manipulation transform updates in IDirectManipulationViewportEventHandler | |||||
| * callbacks. */ | |||||
| Microsoft::WRL::ComPtr<GHOST_DirectManipulationViewportEventHandler> | |||||
| directManipulationEventHandler = | |||||
| Microsoft::WRL::Make<GHOST_DirectManipulationViewportEventHandler>(dpi); | |||||
Done Inline ActionsI'd pull out the "could fail" dmanip setup code into a factory function and make this constructor private. This cleans up following code that could assume everything is initialized if the function can be called. nicholas_rishel: I'd pull out the "could fail" dmanip setup code into a factory function and make this… | |||||
| DWORD directManipulationViewportHandlerCookie; | |||||
| directManipulationViewport->AddEventHandler( | |||||
| hWnd, directManipulationEventHandler.Get(), &directManipulationViewportHandlerCookie); | |||||
| DM_CHECK_RESULT_AND_EXIT_EARLY(hr, "Viewport add EventHandler failed\n"); | |||||
| /* Set default rect for viewport before activating. */ | |||||
| RECT rect = {0, 0, 10000, 10000}; | |||||
| hr = directManipulationViewport->SetViewportRect(&rect); | |||||
| DM_CHECK_RESULT_AND_EXIT_EARLY(hr, "Viewport set rect failed\n"); | |||||
Done Inline ActionsExample of a check that could be made obsolete with the above factory function suggestion. nicholas_rishel: Example of a check that could be made obsolete with the above factory function suggestion. | |||||
| hr = directManipulationManager->Activate(hWnd); | |||||
| DM_CHECK_RESULT_AND_EXIT_EARLY(hr, "DirectManipulationManager activate failed\n"); | |||||
| hr = directManipulationViewport->Enable(); | |||||
| DM_CHECK_RESULT_AND_EXIT_EARLY(hr, "Viewport enable failed\n"); | |||||
| directManipulationEventHandler->resetViewport(directManipulationViewport.Get()); | |||||
| bool isScrollDirectionInverted = getScrollDirectionFromReg(); | |||||
| auto instance = new GHOST_DirectManipulationHelper(hWnd, | |||||
| directManipulationManager, | |||||
| directManipulationUpdateManager, | |||||
| directManipulationViewport, | |||||
| directManipulationEventHandler, | |||||
| directManipulationViewportHandlerCookie, | |||||
| isScrollDirectionInverted); | |||||
| instance->registerScrollDirectionChangeListener(); | |||||
| return instance; | |||||
Done Inline ActionsSince this loop is called every frame, I think we should track the viewport's DIRECTMANIPULATION_STATUS and only call UPDATE when the status is DIRECTMANIPULATION_RUNNING or DIRECTMANIPULATION_INERTIA. nicholas_rishel: Since this loop is called every frame, I think we should track the viewport's… | |||||
| #undef DM_CHECK_RESULT_AND_EXIT_EARLY | |||||
| } | |||||
| bool GHOST_DirectManipulationHelper::getScrollDirectionFromReg() | |||||
| { | |||||
| DWORD scrollDirectionRegValue, pcbData; | |||||
| HRESULT hr = HRESULT_FROM_WIN32( | |||||
| RegGetValueW(HKEY_CURRENT_USER, | |||||
| L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\PrecisionTouchPad\\", | |||||
| L"ScrollDirection", | |||||
| RRF_RT_REG_DWORD, | |||||
| NULL, | |||||
| &scrollDirectionRegValue, | |||||
| &pcbData)); | |||||
| if (!SUCCEEDED(hr)) { | |||||
| GHOST_PRINT("Failed to get scroll direction from registry\n"); | |||||
| return false; | |||||
| } | |||||
| return scrollDirectionRegValue == 0; | |||||
| } | |||||
| void GHOST_DirectManipulationHelper::registerScrollDirectionChangeListener() | |||||
| { | |||||
| if (!m_scrollDirectionRegKey) { | |||||
| HRESULT hr = HRESULT_FROM_WIN32( | |||||
Done Inline ActionsIn GHOST_DirectManipulationHelper::create the current pattern is to create the Helper, and the populate it. Because of this it may be in a partially initialized state during destruction, and these cleanup calls would be null pointer dereferences. My preferred fix here would be to not allow Helper to ever be in a partially initialized state. What this would mean is that in create instead of passing references to the helper's members, you would pass local variables to initialize with and initialize helper with those. Just as an example (removing error checking for clarity). // Before:
GHOST_DirectManipulationHelper *GHOST_DirectManipulationHelper::create(HWND hWnd, uint16_t dpi)
{
GHOST_DirectManipulationHelper *helper = new GHOST_DirectManipulationHelper(hWnd);
HRESULT hr = ::CoCreateInstance(CLSID_DirectManipulationManager,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&helper->m_directManipulationManager));
hr = helper->m_directManipulationManager->GetUpdateManager(
IID_PPV_ARGS(&helper->m_directManipulationUpdateManager));
...
return helper;
}
// After:
GHOST_DirectManipulationHelper *GHOST_DirectManipulationHelper::create(HWND hWnd, uint16_t dpi)
{
Microsoft::WRL::ComPtr<IDirectManipulationManager> directManipulationManager;
HRESULT hr = ::CoCreateInstance(CLSID_DirectManipulationManager,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&directManipulationManager));
Microsoft::WRL::ComPtr<IDirectManipulationUpdateManager> directManipulationUpdateManager;
hr = directManipulationManager->GetUpdateManager(
IID_PPV_ARGS(&directManipulationUpdateManager));
...
return new GHOST_DirectManipulationHelper(hWnd, directManipulationManager, directManipulationUpdateManager, ...);
}nicholas_rishel: In `GHOST_DirectManipulationHelper::create` the current pattern is to create the Helper, and… | |||||
| RegOpenKeyExW(HKEY_CURRENT_USER, | |||||
| L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\PrecisionTouchPad\\", | |||||
Done Inline ActionsI'd remove this, no need to repeat what should be in Window's documentation. nicholas_rishel: I'd remove this, no need to repeat what should be in Window's documentation. | |||||
| 0, | |||||
| KEY_NOTIFY, | |||||
| &m_scrollDirectionRegKey)); | |||||
| if (!SUCCEEDED(hr)) { | |||||
| GHOST_PRINT("Failed to open scroll direction registry key\n"); | |||||
| return; | |||||
| } | |||||
| } | |||||
| if (!m_scrollDirectionChangeEvent) { | |||||
| m_scrollDirectionChangeEvent = CreateEventW(NULL, true, false, NULL); | |||||
| } | |||||
| else { | |||||
| ResetEvent(m_scrollDirectionChangeEvent); | |||||
| } | |||||
| HRESULT hr = HRESULT_FROM_WIN32(RegNotifyChangeKeyValue(m_scrollDirectionRegKey, | |||||
| true, | |||||
| REG_NOTIFY_CHANGE_LAST_SET, | |||||
| m_scrollDirectionChangeEvent, | |||||
| true)); | |||||
| if (!SUCCEEDED(hr)) { | |||||
| GHOST_PRINT("Failed to register scroll direction change listener\n"); | |||||
| return; | |||||
| } | |||||
| } | |||||
| void GHOST_DirectManipulationHelper::onPointerHitTest(UINT32 pointerId) | |||||
| { | |||||
| [[maybe_unused]] HRESULT hr = m_directManipulationViewport->SetContact(pointerId); | |||||
| GHOST_ASSERT(SUCCEEDED(hr), "Viewport set contact failed\n"); | |||||
| if (WaitForSingleObject(m_scrollDirectionChangeEvent, 0) == WAIT_OBJECT_0) { | |||||
| m_isScrollDirectionInverted = getScrollDirectionFromReg(); | |||||
| registerScrollDirectionChangeListener(); | |||||
| } | |||||
| } | |||||
| void GHOST_DirectManipulationHelper::update() | |||||
Done Inline ActionsInstead of polling this every frame, pass in the dpi during initialization and update it when WM_DPICHANGED occurs. This gets us halfway to removing the GHOST_Window dependency. nicholas_rishel: Instead of polling this every frame, pass in the dpi during initialization and update it when… | |||||
| { | |||||
| if (m_directManipulationEventHandler->dm_status == DIRECTMANIPULATION_RUNNING || | |||||
| m_directManipulationEventHandler->dm_status == DIRECTMANIPULATION_INERTIA) { | |||||
| [[maybe_unused]] HRESULT hr = m_directManipulationUpdateManager->Update(nullptr); | |||||
| GHOST_ASSERT(SUCCEEDED(hr), "DirectManipulationUpdateManager update failed\n"); | |||||
| } | |||||
| } | |||||
| void GHOST_DirectManipulationHelper::setDPI(uint16_t dpi) | |||||
| { | |||||
| m_directManipulationEventHandler->dpi = dpi; | |||||
| } | |||||
| GHOST_TTrackpadInfo GHOST_DirectManipulationHelper::getTrackpadInfo() | |||||
| { | |||||
| GHOST_TTrackpadInfo result = m_directManipulationEventHandler->accumulated_values; | |||||
Done Inline ActionsThis state machine, and why it's necessary, should be commented. Your explanation in the prior inline comment should work here. nicholas_rishel: This state machine, and why it's necessary, should be commented. Your explanation in the prior… | |||||
| result.isScrollDirectionInverted = m_isScrollDirectionInverted; | |||||
| m_directManipulationEventHandler->accumulated_values = {0, 0, 0}; | |||||
| return result; | |||||
| } | |||||
| GHOST_DirectManipulationHelper::~GHOST_DirectManipulationHelper() | |||||
| { | |||||
| HRESULT hr; | |||||
| hr = m_directManipulationViewport->Stop(); | |||||
| GHOST_ASSERT(SUCCEEDED(hr), "Viewport stop failed\n"); | |||||
| hr = m_directManipulationViewport->RemoveEventHandler(m_directManipulationViewportHandlerCookie); | |||||
| GHOST_ASSERT(SUCCEEDED(hr), "Viewport remove event handler failed\n"); | |||||
| hr = m_directManipulationViewport->Abandon(); | |||||
| GHOST_ASSERT(SUCCEEDED(hr), "Viewport abandon failed\n"); | |||||
| hr = m_directManipulationManager->Deactivate(m_hWnd); | |||||
| GHOST_ASSERT(SUCCEEDED(hr), "DirectManipulationManager deactivate failed\n"); | |||||
| if (m_scrollDirectionChangeEvent) { | |||||
| CloseHandle(m_scrollDirectionChangeEvent); | |||||
Done Inline ActionsNitpick: phrasing "next updates" is a bit awkward, I'd use "following updates". nicholas_rishel: Nitpick: phrasing "next updates" is a bit awkward, I'd use "following updates". | |||||
| m_scrollDirectionChangeEvent = NULL; | |||||
| } | |||||
| if (m_scrollDirectionRegKey) { | |||||
| RegCloseKey(m_scrollDirectionRegKey); | |||||
| m_scrollDirectionRegKey = NULL; | |||||
| } | |||||
| } | |||||
| GHOST_DirectManipulationViewportEventHandler::GHOST_DirectManipulationViewportEventHandler( | |||||
| uint16_t dpi) | |||||
| : accumulated_values({0, 0, 0}), dpi(dpi), dm_status(DIRECTMANIPULATION_BUILDING) | |||||
| { | |||||
| } | |||||
| void GHOST_DirectManipulationViewportEventHandler::resetViewport( | |||||
| IDirectManipulationViewport *viewport) | |||||
| { | |||||
| if (gesture_state != GESTURE_NONE) { | |||||
| [[maybe_unused]] HRESULT hr = viewport->ZoomToRect(0.0f, 0.0f, 10000.0f, 10000.0f, FALSE); | |||||
| GHOST_ASSERT(SUCCEEDED(hr), "Viewport reset failed\n"); | |||||
| } | |||||
| gesture_state = GESTURE_NONE; | |||||
| last_scale = PINCH_SCALE_FACTOR; | |||||
| last_x = 0.0f; | |||||
| last_y = 0.0f; | |||||
| } | |||||
| HRESULT GHOST_DirectManipulationViewportEventHandler::OnViewportStatusChanged( | |||||
| IDirectManipulationViewport *viewport, | |||||
| DIRECTMANIPULATION_STATUS current, | |||||
| DIRECTMANIPULATION_STATUS previous) | |||||
Done Inline ActionsInstead of pushing event here, save the accumulated state. Add a function GetTouchpadInfo to the helper which grabs the accumulated state, returns e.g. a TOUCHPAD_INFO struct. When that state is pulled, it should be zeroed sans the fractional part. This allows us to remove GHOST_System and the remainder of GHOST_Window from the touchpad handler. nicholas_rishel: Instead of pushing event here, save the accumulated state. Add a function `GetTouchpadInfo` to… | |||||
| { | |||||
| dm_status = current; | |||||
| if (current == previous) { | |||||
| return S_OK; | |||||
| } | |||||
| if (previous == DIRECTMANIPULATION_ENABLED || current == DIRECTMANIPULATION_READY || | |||||
| (previous == DIRECTMANIPULATION_INERTIA && current != DIRECTMANIPULATION_INERTIA)) { | |||||
| resetViewport(viewport); | |||||
| } | |||||
| return S_OK; | |||||
| } | |||||
| HRESULT GHOST_DirectManipulationViewportEventHandler::OnViewportUpdated( | |||||
| IDirectManipulationViewport *viewport) | |||||
| { | |||||
| /* Nothing to do here. */ | |||||
| return S_OK; | |||||
| } | |||||
| HRESULT GHOST_DirectManipulationViewportEventHandler::OnContentUpdated( | |||||
| IDirectManipulationViewport *viewport, IDirectManipulationContent *content) | |||||
| { | |||||
| float transform[6]; | |||||
| HRESULT hr = content->GetContentTransform(transform, ARRAYSIZE(transform)); | |||||
| GHOST_ASSERT(SUCCEEDED(hr), "DirectManipulationContent get transform failed\n"); | |||||
| const float device_scale_factor = dpi / 96.0f; | |||||
| const float scale = transform[0] * PINCH_SCALE_FACTOR; | |||||
| const float x = transform[4] / device_scale_factor; | |||||
| const float y = transform[5] / device_scale_factor; | |||||
| const float EPS = 3e-5; | |||||
| /* Ignore repeating or incorrect input. */ | |||||
| if ((fabs(scale - last_scale) <= EPS && fabs(x - last_x) <= EPS && fabs(y - last_y) <= EPS) || | |||||
| scale == 0.0f) { | |||||
| GHOST_PRINT("Ignoring touchpad input\n"); | |||||
| return hr; | |||||
| } | |||||
| /* Assume that every gesture is a pan in the beginning. | |||||
| * If it's a pinch, the gesture will be changed below. */ | |||||
| if (gesture_state == GESTURE_NONE) { | |||||
| gesture_state = GESTURE_PAN; | |||||
| } | |||||
| /* DM doesn't always immediately recognize pinch gestures, | |||||
| * so allow transition from pan to pinch. */ | |||||
| if (gesture_state == GESTURE_PAN) { | |||||
| if (fabs(scale - PINCH_SCALE_FACTOR) > EPS) { | |||||
| gesture_state = GESTURE_PINCH; | |||||
| } | |||||
| } | |||||
| /* This state machine is used here because: | |||||
| * 1. Pinch and pan gestures must be differentiated and cannot be proccessed at the same time | |||||
| * because XY transform values become nonsensical during pinch gesture. | |||||
| * 2. GHOST requires delta values for events while DM provides transformation matrix of the | |||||
| * current gesture. | |||||
| * 3. GHOST events accept integer values while DM values are non-integer. | |||||
| * Truncated fractional parts are accumulated and accounted for in following updates. | |||||
| */ | |||||
| switch (gesture_state) { | |||||
| case GESTURE_PINCH: { | |||||
| int32_t dscale = roundf(scale - last_scale); | |||||
| last_scale += dscale; | |||||
| accumulated_values.scale += dscale; | |||||
| break; | |||||
| } | |||||
| case GESTURE_PAN: { | |||||
| int32_t dx = roundf(x - last_x); | |||||
| int32_t dy = roundf(y - last_y); | |||||
| last_x += dx; | |||||
| last_y += dy; | |||||
| accumulated_values.x += dx; | |||||
| accumulated_values.y += dy; | |||||
| break; | |||||
| } | |||||
| case GESTURE_NONE: | |||||
| break; | |||||
| } | |||||
| return hr; | |||||
| } | |||||
So after a bit more research into this, I think it's impossible to move the HWND from this class while keeping RAII cleanup, but we can remove GHOST_WindowWin32 and GHOST_SystemWin32. I think there are a lot of maintainability benefits of removing the latter two as it makes pointer ownership much cleaner, and between the two HWND is the less risky thing to use after it's resource has been freed.
Sorry for asking for the removal of passing in a HWND just to ask you to add it back in to instead remove the GHOST_ dependencies, the former was a lower hanging fruit to understand at the time.
Note removing the GHOST_System dependency will also remove the need for GHOST_EventTrackpad as the event generation would be moved out too.