Skip to content

feat: configurable mouse gestures with dedicated settings page - #390

Draft
knipknap wants to merge 3 commits into
mainfrom
feat/configurable-mouse-gestures
Draft

feat: configurable mouse gestures with dedicated settings page#390
knipknap wants to merge 3 commits into
mainfrom
feat/configurable-mouse-gestures

Conversation

@knipknap

@knipknap knipknap commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Closes #241 (provides the workaround; see note below)

Summary

Mouse navigation gestures are now configurable in a dedicated Mouse Gestures page in the settings dialog. The architecture is context-based so it covers the 2D canvas, the 3D canvas, the sketcher addon, and gestures contributed by future addons.

Architecture

New package rayforge/ui_gtk/gestures/:

  • GestureSpec / GestureSlot / GestureContext (model.py) — a declarative model of rebindable gestures. A spec identifies a physical input (click/drag/scroll + button + modifier mask, serialized e.g. as "drag+shift+middle"); a slot is a rebindable meaning ("Pan the view") with a default binding; a context groups slots per interaction area (canvas2d, canvas3d, sketcher).
  • gesture_registry (registry.py) — an addon-capable registry following the existing SettingsPageRegistry/ContextMenuExtensionRegistry pattern, registered in REGISTRY_TABLE so addon contributions are cleaned up automatically on unload. Resolves the effective binding per slot from Config.gesture_bindings with default fallback, and detects binding conflicts within a context.
  • GestureRouter (router.py) — attaches one any-button drag gesture, one any-button click gesture, and a scroll controller to a widget. On each input it resolves the actual (button, modifiers) against the context's bindings: a match claims the sequence and dispatches to the slot's handlers; no match denies the sequence, leaving existing canvas behavior (element selection, tools, etc.) untouched.

Wiring

  • WorldSurface routes pan/zoom/context-menu/reset-view through a router keyed by a context_id class attribute; WorkSurface uses canvas2d, SketchCanvas overrides it to sketcher.
  • CameraController routes orbit/pan/Z-rotate/zoom through the router. The previous Shift-modifier distinction became two explicit slots (orbit = middle-drag, pan = Shift+middle-drag), each independently rebindable.
  • The sketcher addon registers the sketcher context (pan, zoom, tool menu, reset view) via its existing registration path — no new hookspec needed.
  • Space+drag panning on the 2D canvas remains hard-coded for now (it is intertwined with element-drag handling).

Settings page

GesturePreferencesPage (built into SettingsWindow, position after General) renders one group per registered context and rebuilds live when the registry changes, so addon-contributed gesture contexts appear/disappear automatically. Each slot row opens a capture popover ("press a mouse button now, or scroll"), with conflict rejection, per-slot unassign/reset-to-default, and row labels that update live.

Persistence

Config.gesture_bindings (context_id -> slot_id -> spec string | None for unassigned), auto-saved through the existing config mechanism; malformed or unknown entries are dropped on load and unknown slots are legal (e.g. written by a disabled addon).

Notes

  • Pan not working. #241: the reported pan breakage itself needs its own root-cause fix, but this feature gives affected users a workaround by remapping pan to a button/modifier combination that their hardware/driver delivers.
  • Editing/selection gestures (left-click select, drag-move, sketch tools) intentionally remain hard-coded; the model leaves room to add them as slots later.
  • Docs updated in website/docs/ui/settings.md.

Test plan

  • New unit/UI tests: gesture spec serialization + validation, registry resolution/conflict/cleanup, router dispatch (claim/deny, modifier discrimination, unassigned slots), preferences page rebuild and live label updates, config round-trip + malformed-entry handling (tests/ui_gtk/gestures/, tests/core/test_config.py).
  • Updated existing CameraController tests to the new per-slot handler API (33/33 pass).
  • Full suites: tests (4867 passed), tests/ui_gtk -m ui under xvfb (627 passed), builtin addon tests (same result as main).
  • Lint: ruff check, flake8 clean; translation compile step verified.

Navigation and menu gestures on the 2D canvas, 3D canvas, and in the
sketch editor can now be rebound in a dedicated Mouse Gestures page in
the settings dialog.

The new rayforge.ui_gtk.gestures package provides:

- GestureSpec/GestureSlot/GestureContext: a declarative model of
  rebindable gestures (button + modifier combinations for click, drag,
  and scroll input).
- gesture_registry: an addon-capable registry (wired into
  REGISTRY_TABLE for automatic cleanup) that resolves the effective
  binding per slot from config with default fallback.
- GestureRouter: attaches generic any-button drag/click and scroll
  controllers to a widget and dispatches input to the slot handlers
  configured for its context, claiming matched sequences and denying
  everything else so existing canvas behavior is unaffected.

WorldSurface and CameraController route their pan/zoom/orbit/Z-rotate/
context-menu input through the router instead of hard-coded
button-bound controllers. The sketcher addon registers its own
"sketcher" context with a rebindable tool-menu gesture.

Bindings persist as gesture_bindings in config.yaml. This also gives
users a workaround for hardware/driver setups where specific buttons
are not delivered (#241).

Documented in website/docs/ui/settings.md.
- Use a module-level singleton instead of a function call in the
  GestureSpec dataclass default (RUF009).
- Import Callable from collections.abc (UP035).
- Replace function-call argument defaults in test fakes with a
  module-level constant (B008) and reuse the existing fake config
  instead of a mutable class attribute (RUF012).
- Assert button presence before use in the preferences page test.
- Narrow the button type in GestureSpec.display_label before dict
  lookup.
- Drop the gesture/controller parameter annotations on the router's
  private GTK signal handlers, matching the untyped pattern already
  used by CameraController's handlers and keeping the test fakes
  assignable.
- Handle the Optional str returned by get_label() in the preferences
  page tests.
@knipknap
knipknap marked this pull request as draft September 6, 2026 13:32
@knipknap knipknap mentioned this pull request Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pan not working.

1 participant