From afd760f2b7339f12c9d68a16091dfa468ad294de Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Wed, 14 May 2025 13:33:10 +0200 Subject: [PATCH] UI: Ghost: support horizontal scrolling for 2D editors Some mice have an additional horizontal scroll wheel. This patch adds support for receiving such events. By default it is used to scroll 2D editors left and right. I originally developed this because I was missing it in the spreadsheet, but it seems to be useful in many other editors too. It's supported on Linux (Wayland), Windows and macos. Pull Request: https://projects.blender.org/blender/blender/pulls/138758 --- intern/ghost/GHOST_Types.h | 11 ++++- intern/ghost/intern/GHOST_EventPrinter.cc | 4 +- intern/ghost/intern/GHOST_EventWheel.hh | 9 ++-- intern/ghost/intern/GHOST_SystemCocoa.mm | 17 +++---- intern/ghost/intern/GHOST_SystemSDL.cc | 3 +- intern/ghost/intern/GHOST_SystemWayland.cc | 15 ++++-- intern/ghost/intern/GHOST_SystemWin32.cc | 49 ++++++++++++++++--- intern/ghost/intern/GHOST_SystemWin32.hh | 17 +++++-- intern/ghost/intern/GHOST_SystemX11.cc | 4 +- intern/ghost/test/gears/GHOST_C-Test.c | 2 +- intern/ghost/test/gears/GHOST_Test.cpp | 2 +- .../keyconfig/keymap_data/blender_default.py | 2 + source/blender/makesrna/intern/rna_wm.cc | 4 ++ .../windowmanager/intern/wm_event_system.cc | 22 +++++++-- .../blender/windowmanager/wm_event_types.hh | 16 ++++-- 15 files changed, 131 insertions(+), 46 deletions(-) diff --git a/intern/ghost/GHOST_Types.h b/intern/ghost/GHOST_Types.h index 39d1be93135..2900bf1a6cf 100644 --- a/intern/ghost/GHOST_Types.h +++ b/intern/ghost/GHOST_Types.h @@ -260,7 +260,7 @@ typedef enum { /** Mouse button up event. */ GHOST_kEventButtonUp, /** - * Mouse wheel event. + * Vertical/Horizontal mouse wheel event. * * \note #GHOST_GetEventData returns #GHOST_TEventWheelData. */ @@ -577,9 +577,16 @@ typedef struct { GHOST_TabletData tablet; } GHOST_TEventButtonData; +typedef enum { + GHOST_kEventWheelAxisVertical = 0, + GHOST_kEventWheelAxisHorizontal = 1, +} GHOST_TEventWheelAxis; + typedef struct { + /** Which mouse wheel is used. */ + GHOST_TEventWheelAxis axis; /** Displacement of a mouse wheel. */ - int32_t z; + int32_t value; } GHOST_TEventWheelData; typedef enum { diff --git a/intern/ghost/intern/GHOST_EventPrinter.cc b/intern/ghost/intern/GHOST_EventPrinter.cc index 3aa517deab4..f7de68cd84a 100644 --- a/intern/ghost/intern/GHOST_EventPrinter.cc +++ b/intern/ghost/intern/GHOST_EventPrinter.cc @@ -80,7 +80,9 @@ bool GHOST_EventPrinter::processEvent(const GHOST_IEvent *event) } case GHOST_kEventWheel: { const GHOST_TEventWheelData *wheelData = static_cast(data); - std::cout << "GHOST_kEventWheel, z: " << wheelData->z; + std::cout << "GHOST_kEventWheel, axis: " + << (wheelData->axis == GHOST_kEventWheelAxisVertical ? "vertical" : "horizontal") + << ", value: " << wheelData->value; handled = true; break; } diff --git a/intern/ghost/intern/GHOST_EventWheel.hh b/intern/ghost/intern/GHOST_EventWheel.hh index 680f397f01f..c0a2f802535 100644 --- a/intern/ghost/intern/GHOST_EventWheel.hh +++ b/intern/ghost/intern/GHOST_EventWheel.hh @@ -22,16 +22,17 @@ class GHOST_EventWheel : public GHOST_Event { * Constructor. * \param msec: The time this event was generated. * \param window: The window of this event. - * \param z: The displacement of the mouse wheel. + * \param axis: The axis of the mouse wheel. + * \param value: The displacement of the mouse wheel. */ - GHOST_EventWheel(uint64_t msec, GHOST_IWindow *window, int32_t z) + GHOST_EventWheel(uint64_t msec, GHOST_IWindow *window, GHOST_TEventWheelAxis axis, int32_t value) : GHOST_Event(msec, GHOST_kEventWheel, window) { - m_wheelEventData.z = z; + m_wheelEventData.axis = axis; + m_wheelEventData.value = value; m_data = &m_wheelEventData; } protected: - /** The z-displacement of the mouse wheel. */ GHOST_TEventWheelData m_wheelEventData; }; diff --git a/intern/ghost/intern/GHOST_SystemCocoa.mm b/intern/ghost/intern/GHOST_SystemCocoa.mm index 0c496686ddd..ef3444e92be 100644 --- a/intern/ghost/intern/GHOST_SystemCocoa.mm +++ b/intern/ghost/intern/GHOST_SystemCocoa.mm @@ -1743,17 +1743,16 @@ GHOST_TSuccess GHOST_SystemCocoa::handleMouseEvent(void *eventPtr) /* Standard scroll-wheel case, if no swiping happened, * and no momentum (kinetic scroll) works. */ if (!m_multiTouchScroll && momentumPhase == NSEventPhaseNone) { - double deltaF = event.deltaY; - - if (deltaF == 0.0) { - deltaF = event.deltaX; /* Make blender decide if it's horizontal scroll. */ + if (event.deltaX != 0.0) { + const int32_t delta = event.deltaX > 0.0 ? 1 : -1; + pushEvent(new GHOST_EventWheel( + event.timestamp * 1000, window, GHOST_kEventWheelAxisHorizontal, delta)); } - if (deltaF == 0.0) { - break; /* Discard trackpad delta=0 events. */ + if (event.deltaY != 0.0) { + const int32_t delta = event.deltaY > 0.0 ? 1 : -1; + pushEvent(new GHOST_EventWheel( + event.timestamp * 1000, window, GHOST_kEventWheelAxisVertical, delta)); } - - const int32_t delta = deltaF > 0.0 ? 1 : -1; - pushEvent(new GHOST_EventWheel(event.timestamp * 1000, window, delta)); } else { const NSPoint mousePos = event.locationInWindow; diff --git a/intern/ghost/intern/GHOST_SystemSDL.cc b/intern/ghost/intern/GHOST_SystemSDL.cc index 17bdc6bf67e..021fdfda556 100644 --- a/intern/ghost/intern/GHOST_SystemSDL.cc +++ b/intern/ghost/intern/GHOST_SystemSDL.cc @@ -605,7 +605,8 @@ void GHOST_SystemSDL::processEvent(SDL_Event *sdl_event) GHOST_WindowSDL *window = findGhostWindow( SDL_GetWindowFromID_fallback(sdl_sub_evt.windowID)); assert(window != nullptr); - g_event = new GHOST_EventWheel(event_ms, window, sdl_sub_evt.y); + g_event = new GHOST_EventWheel( + event_ms, window, GHOST_kEventWheelAxisVertical, sdl_sub_evt.y); break; } case SDL_KEYDOWN: diff --git a/intern/ghost/intern/GHOST_SystemWayland.cc b/intern/ghost/intern/GHOST_SystemWayland.cc index 7ecba893ce0..e9f7d5edd7e 100644 --- a/intern/ghost/intern/GHOST_SystemWayland.cc +++ b/intern/ghost/intern/GHOST_SystemWayland.cc @@ -4110,13 +4110,19 @@ static void pointer_handle_frame(void *data, wl_pointer * /*wl_pointer*/) } /* Done evaluating scroll input, generate the events. */ - - /* Discrete X axis currently unsupported. */ if (ps.discrete_xy[0] || ps.discrete_xy[1]) { + if (ps.discrete_xy[0]) { + seat->system->pushEvent_maybe_pending(new GHOST_EventWheel( + ps.has_event_ms ? ps.event_ms : seat->system->getMilliSeconds(), + win, + GHOST_kEventWheelAxisHorizontal, + ps.discrete_xy[0])); + } if (ps.discrete_xy[1]) { seat->system->pushEvent_maybe_pending(new GHOST_EventWheel( ps.has_event_ms ? ps.event_ms : seat->system->getMilliSeconds(), win, + GHOST_kEventWheelAxisVertical, -ps.discrete_xy[1])); } ps.discrete_xy[0] = 0; @@ -4949,7 +4955,10 @@ static void tablet_tool_handle_frame(void *data, } case GWL_TabletTool_EventTypes::Wheel: { seat->system->pushEvent_maybe_pending( - new GHOST_EventWheel(event_ms, win, -tablet_tool->frame_pending.wheel.clicks)); + new GHOST_EventWheel(event_ms, + win, + GHOST_kEventWheelAxisVertical, + -tablet_tool->frame_pending.wheel.clicks)); break; } } diff --git a/intern/ghost/intern/GHOST_SystemWin32.cc b/intern/ghost/intern/GHOST_SystemWin32.cc index f15aff4ba9e..c4d7c77f747 100644 --- a/intern/ghost/intern/GHOST_SystemWin32.cc +++ b/intern/ghost/intern/GHOST_SystemWin32.cc @@ -1217,13 +1217,13 @@ GHOST_EventCursor *GHOST_SystemWin32::processCursorEvent(GHOST_WindowWin32 *wind GHOST_TABLET_DATA_NONE); } -void GHOST_SystemWin32::processWheelEvent(GHOST_WindowWin32 *window, - WPARAM wParam, - LPARAM /*lParam*/) +void GHOST_SystemWin32::processWheelEventVertical(GHOST_WindowWin32 *window, + WPARAM wParam, + LPARAM /*lParam*/) { GHOST_SystemWin32 *system = (GHOST_SystemWin32 *)getSystem(); - int acc = system->m_wheelDeltaAccum; + int acc = system->m_wheelDeltaAccumVertical; int delta = GET_WHEEL_DELTA_WPARAM(wParam); if (acc * delta < 0) { @@ -1235,10 +1235,37 @@ void GHOST_SystemWin32::processWheelEvent(GHOST_WindowWin32 *window, acc = abs(acc); while (acc >= WHEEL_DELTA) { - system->pushEvent(new GHOST_EventWheel(getMessageTime(system), window, direction)); + system->pushEvent(new GHOST_EventWheel( + getMessageTime(system), window, GHOST_kEventWheelAxisVertical, direction)); acc -= WHEEL_DELTA; } - system->m_wheelDeltaAccum = acc * direction; + system->m_wheelDeltaAccumVertical = acc * direction; +} + +/** This is almost the same as #processWheelEventVertical. */ +void GHOST_SystemWin32::processWheelEventHorizontal(GHOST_WindowWin32 *window, + WPARAM wParam, + LPARAM /*lParam*/) +{ + GHOST_SystemWin32 *system = (GHOST_SystemWin32 *)getSystem(); + + int acc = system->m_wheelDeltaAccumHorizontal; + int delta = GET_WHEEL_DELTA_WPARAM(wParam); + + if (acc * delta < 0) { + /* Scroll direction reversed. */ + acc = 0; + } + acc += delta; + int direction = (acc >= 0) ? 1 : -1; + acc = abs(acc); + + while (acc >= WHEEL_DELTA) { + system->pushEvent(new GHOST_EventWheel( + getMessageTime(system), window, GHOST_kEventWheelAxisHorizontal, direction)); + acc -= WHEEL_DELTA; + } + system->m_wheelDeltaAccumHorizontal = acc * direction; } GHOST_EventKey *GHOST_SystemWin32::processKeyEvent(GHOST_WindowWin32 *window, RAWINPUT const &raw) @@ -1994,13 +2021,18 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, uint msg, WPARAM wParam, * since DefWindowProc propagates it up the parent chain * until it finds a window that processes it. */ - processWheelEvent(window, wParam, lParam); + processWheelEventVertical(window, wParam, lParam); eventHandled = true; #ifdef BROKEN_PEEK_TOUCHPAD PostMessage(hwnd, WM_USER, 0, 0); #endif break; } + case WM_MOUSEHWHEEL: { + processWheelEventHorizontal(window, wParam, lParam); + eventHandled = true; + break; + } case WM_SETCURSOR: { /* The WM_SETCURSOR message is sent to a window if the mouse causes the cursor * to move within a window and mouse input is not captured. @@ -2072,7 +2104,8 @@ LRESULT WINAPI GHOST_SystemWin32::s_wndProc(HWND hwnd, uint msg, WPARAM wParam, * If the windows use different input queues, the message is sent asynchronously, * so the window is activated immediately. */ - system->m_wheelDeltaAccum = 0; + system->m_wheelDeltaAccumVertical = 0; + system->m_wheelDeltaAccumHorizontal = 0; event = processWindowEvent( LOWORD(wParam) ? GHOST_kEventWindowActivate : GHOST_kEventWindowDeactivate, window); /* WARNING: Let DefWindowProc handle WM_ACTIVATE, otherwise WM_MOUSEWHEEL diff --git a/intern/ghost/intern/GHOST_SystemWin32.hh b/intern/ghost/intern/GHOST_SystemWin32.hh index 923d86d3def..1845f89daaa 100644 --- a/intern/ghost/intern/GHOST_SystemWin32.hh +++ b/intern/ghost/intern/GHOST_SystemWin32.hh @@ -369,12 +369,20 @@ class GHOST_SystemWin32 : public GHOST_System { const int32_t screen_co[2]); /** - * Handles a mouse wheel event. + * Handles a vertical mouse wheel event. * \param window: The window receiving the event (the active window). * \param wParam: The wParam from the `wndproc`. * \param lParam: The lParam from the `wndproc`. */ - static void processWheelEvent(GHOST_WindowWin32 *window, WPARAM wParam, LPARAM lParam); + static void processWheelEventVertical(GHOST_WindowWin32 *window, WPARAM wParam, LPARAM lParam); + + /** + * Handles a horizontal mouse wheel event. + * \param window: The window receiving the event (the active window). + * \param wParam: The wParam from the `wndproc`. + * \param lParam: The lParam from the `wndproc`. + */ + static void processWheelEventHorizontal(GHOST_WindowWin32 *window, WPARAM wParam, LPARAM lParam); /** * Creates a key event and updates the key data stored locally (m_modifierKeys). @@ -479,8 +487,9 @@ class GHOST_SystemWin32 : public GHOST_System { /** Console status. */ bool m_consoleStatus; - /** Wheel delta accumulator. */ - int m_wheelDeltaAccum; + /** Wheel delta accumulators. */ + int m_wheelDeltaAccumVertical; + int m_wheelDeltaAccumHorizontal; }; inline void GHOST_SystemWin32::handleKeyboardChange() diff --git a/intern/ghost/intern/GHOST_SystemX11.cc b/intern/ghost/intern/GHOST_SystemX11.cc index cd08c6eec40..7d6b072d2cf 100644 --- a/intern/ghost/intern/GHOST_SystemX11.cc +++ b/intern/ghost/intern/GHOST_SystemX11.cc @@ -1268,13 +1268,13 @@ void GHOST_SystemX11::processEvent(XEvent *xe) /* process wheel mouse events and break, only pass on press events */ if (xbe.button == Button4) { if (xbe.type == ButtonPress) { - g_event = new GHOST_EventWheel(event_ms, window, 1); + g_event = new GHOST_EventWheel(event_ms, window, GHOST_kEventWheelAxisVertical, 1); } break; } if (xbe.button == Button5) { if (xbe.type == ButtonPress) { - g_event = new GHOST_EventWheel(event_ms, window, -1); + g_event = new GHOST_EventWheel(event_ms, window, GHOST_kEventWheelAxisVertical, -1); } break; } diff --git a/intern/ghost/test/gears/GHOST_C-Test.c b/intern/ghost/test/gears/GHOST_C-Test.c index 1ec4cbede5e..eefd1d65f96 100644 --- a/intern/ghost/test/gears/GHOST_C-Test.c +++ b/intern/ghost/test/gears/GHOST_C-Test.c @@ -289,7 +289,7 @@ bool processEvent(GHOST_EventHandle hEvent, GHOST_TUserDataPtr userData) #endif case GHOST_kEventWheel: { wheelData = (GHOST_TEventWheelData *)GHOST_GetEventData(hEvent); - if (wheelData->z > 0) { + if (wheelData->value > 0) { view_rotz += 5.f; } else { diff --git a/intern/ghost/test/gears/GHOST_Test.cpp b/intern/ghost/test/gears/GHOST_Test.cpp index 0ea87408360..a6b5184fe2d 100644 --- a/intern/ghost/test/gears/GHOST_Test.cpp +++ b/intern/ghost/test/gears/GHOST_Test.cpp @@ -459,7 +459,7 @@ bool Application::processEvent(const GHOST_IEvent *event) #endif case GHOST_kEventWheel: { GHOST_TEventWheelData *wheelData = (GHOST_TEventWheelData *)event->getData(); - if (wheelData->z > 0) { + if (wheelData->value > 0) { view_rotz += 5.f; } else { diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index ac28f8d05ae..d06ac87c214 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -939,7 +939,9 @@ def km_view2d(_params): ("view2d.pan", {"type": 'MIDDLEMOUSE', "value": 'PRESS', "shift": True}, None), ("view2d.pan", {"type": 'TRACKPADPAN', "value": 'ANY'}, None), ("view2d.scroll_right", {"type": 'WHEELDOWNMOUSE', "value": 'PRESS', "ctrl": True}, None), + ("view2d.scroll_right", {"type": 'WHEELRIGHTMOUSE', "value": 'PRESS'}, None), ("view2d.scroll_left", {"type": 'WHEELUPMOUSE', "value": 'PRESS', "ctrl": True}, None), + ("view2d.scroll_left", {"type": 'WHEELLEFTMOUSE', "value": 'PRESS'}, None), ("view2d.scroll_down", {"type": 'WHEELDOWNMOUSE', "value": 'PRESS', "shift": True}, None), ("view2d.scroll_up", {"type": 'WHEELUPMOUSE', "value": 'PRESS', "shift": True}, None), ("view2d.ndof", {"type": 'NDOF_MOTION', "value": 'ANY'}, None), diff --git a/source/blender/makesrna/intern/rna_wm.cc b/source/blender/makesrna/intern/rna_wm.cc index f38c9ef78dd..bb100496e29 100644 --- a/source/blender/makesrna/intern/rna_wm.cc +++ b/source/blender/makesrna/intern/rna_wm.cc @@ -71,6 +71,8 @@ static const EnumPropertyItem event_mouse_type_items[] = { {WHEELDOWNMOUSE, "WHEELDOWNMOUSE", 0, CTX_N_(BLT_I18NCONTEXT_UI_EVENTS, "Wheel Down"), ""}, {WHEELINMOUSE, "WHEELINMOUSE", 0, CTX_N_(BLT_I18NCONTEXT_UI_EVENTS, "Wheel In"), ""}, {WHEELOUTMOUSE, "WHEELOUTMOUSE", 0, CTX_N_(BLT_I18NCONTEXT_UI_EVENTS, "Wheel Out"), ""}, + {WHEELLEFTMOUSE, "WHEELLEFTMOUSE", 0, CTX_N_(BLT_I18NCONTEXT_UI_EVENTS, "Wheel Left"), ""}, + {WHEELRIGHTMOUSE, "WHEELRIGHTMOUSE", 0, CTX_N_(BLT_I18NCONTEXT_UI_EVENTS, "Wheel Right"), ""}, {0, nullptr, 0, nullptr, nullptr}, }; @@ -236,6 +238,8 @@ const EnumPropertyItem rna_enum_event_type_items[] = { {WHEELDOWNMOUSE, "WHEELDOWNMOUSE", 0, "Wheel Down", "WhDown"}, {WHEELINMOUSE, "WHEELINMOUSE", 0, "Wheel In", "WhIn"}, {WHEELOUTMOUSE, "WHEELOUTMOUSE", 0, "Wheel Out", "WhOut"}, + {WHEELLEFTMOUSE, "WHEELLEFTMOUSE", 0, "Wheel Left", "WhLeft"}, + {WHEELRIGHTMOUSE, "WHEELRIGHTMOUSE", 0, "Wheel Right", "WhRight"}, RNA_ENUM_ITEM_SEPR, {EVT_AKEY, "A", 0, "A", ""}, {EVT_BKEY, "B", 0, "B", ""}, diff --git a/source/blender/windowmanager/intern/wm_event_system.cc b/source/blender/windowmanager/intern/wm_event_system.cc index 5a947fa5b69..90952c1ad9e 100644 --- a/source/blender/windowmanager/intern/wm_event_system.cc +++ b/source/blender/windowmanager/intern/wm_event_system.cc @@ -6257,13 +6257,25 @@ void wm_event_add_ghostevent(wmWindowManager *wm, customdata); int click_step; - if (wheelData->z > 0) { - event.type = WHEELUPMOUSE; - click_step = wheelData->z; + if (wheelData->axis == GHOST_kEventWheelAxisVertical) { + if (wheelData->value > 0) { + event.type = WHEELUPMOUSE; + click_step = wheelData->value; + } + else { + event.type = WHEELDOWNMOUSE; + click_step = -wheelData->value; + } } else { - event.type = WHEELDOWNMOUSE; - click_step = -wheelData->z; + if (wheelData->value > 0) { + event.type = WHEELRIGHTMOUSE; + click_step = wheelData->value; + } + else { + event.type = WHEELLEFTMOUSE; + click_step = -wheelData->value; + } } BLI_assert(click_step != 0); diff --git a/source/blender/windowmanager/wm_event_types.hh b/source/blender/windowmanager/wm_event_types.hh index cab34ba4bda..bc4a29669ed 100644 --- a/source/blender/windowmanager/wm_event_types.hh +++ b/source/blender/windowmanager/wm_event_types.hh @@ -76,16 +76,20 @@ enum wmEventType : int16_t { * ignore all but the most recent MOUSEMOVE (for better performance), * paint and drawing tools however will want to handle these. */ INBETWEEN_MOUSEMOVE = 0x0011, + /* Horizontal scrolling events. */ + WHEELLEFTMOUSE = 0x0014, /* 20 */ + WHEELRIGHTMOUSE = 0x0015, /* 21 */ /* Maximum keyboard value (inclusive). */ -#define _EVT_MOUSE_MAX 0x0011 /* 17 */ +#define _EVT_MOUSE_MAX 0x0015 /* 21 */ /* IME event, GHOST_kEventImeCompositionStart in ghost. */ - WM_IME_COMPOSITE_START = 0x0014, + WM_IME_COMPOSITE_START = 0x0016, + /* 0x0017 is MOUSESMARTZOOM. */ /* IME event, GHOST_kEventImeComposition in ghost. */ - WM_IME_COMPOSITE_EVENT = 0x0015, + WM_IME_COMPOSITE_EVENT = 0x0018, /* IME event, GHOST_kEventImeCompositionEnd in ghost. */ - WM_IME_COMPOSITE_END = 0x0016, + WM_IME_COMPOSITE_END = 0x0019, /* Tablet/Pen Specific Events. */ TABLET_STYLUS = 0x001a, @@ -424,7 +428,9 @@ enum wmEventType : int16_t { BUTTON6MOUSE, \ BUTTON7MOUSE)) /** Test whether the event is a mouse wheel. */ -#define ISMOUSE_WHEEL(event_type) ((event_type) >= WHEELUPMOUSE && (event_type) <= WHEELOUTMOUSE) +#define ISMOUSE_WHEEL(event_type) \ + (((event_type) >= WHEELUPMOUSE && (event_type) <= WHEELOUTMOUSE) || \ + ELEM((event_type), WHEELLEFTMOUSE, WHEELRIGHTMOUSE)) /** Test whether the event is a mouse (trackpad) gesture. */ #define ISMOUSE_GESTURE(event_type) ((event_type) >= MOUSEPAN && (event_type) <= MOUSESMARTZOOM)