Skip to content

Fix stuck buttons, display detection, double-click, capture safety and calibration - #4

Merged
modfox merged 9 commits into
mainfrom
fix/evaluation-items
Nov 4, 2026
Merged

modfox merged 9 commits into
mainfrom
fix/evaluation-items

Conversation

@modfox

@modfox modfox commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Fixes the issues from a codebase evaluation. One commit per item.

User-facing fixes

  • Stuck mouse button: EventEmitter.reset() releases the tip and side buttons and ends proximity at the pen's last position. It runs on proximity exit (which previously dropped the tip without a mouse-up), on unplug, and before calibration.
  • Pen mapped to the wrong screen: the tablet's display is found by EDID vendor/model (0x5c23/0x1056 for the DTH2420), then by native panel size, instead of the current mode's 3840×2160. This fixes another 4K monitor winning and scaled modes failing to match. The calibration overlay uses the same lookup.
  • Double-click: synthesized pen, side-button and tap clicks now set .mouseEventClickState (ClickCounter, using the system double-click interval).

Before accepting a second device profile

  • Watch-only HID managers: the vendor-wide hotplug watcher no longer registers report callbacks, so it doesn't copy and discard every pen report. Each matched device gets its own input buffer, and allocation failures are handled.
  • Profile-driven emitter: the vendor ID and pressure maximum come from the active DeviceProfile.
  • nib capture is passive by default: DTH2420 vendor reports are only sent with --send-enable. docs/ADDING-A-DEVICE.md is updated to match.

Calibration

  • CalibrationSolver (NibCore) replaces the duplicated state machines in the CLI and the overlay. It rejects point pairs that are too close or reversed and restarts at point 1. DisplayMapping no longer divides by zero.

Cleanups

  • Synthesized events go through an injectable EventSink; gestures take an injectable clock and swipe action.
  • Pressure-curve points are sorted once instead of on every sample (the saved config format is unchanged).
  • The menu and preferences window share one natural-scrolling helper.
  • ROADMAP.md marks signing and notarization as done.

Testing

  • swift test: 42 tests pass (up from 17). New suites: emitter, touch gestures, click counter, display locator, calibration solver, pressure-curve Codable, config.
  • Clean swift build -c release with no warnings.
  • Checked on the Cintiq Pro 24: unplug mid-drag, mapping with a second monitor attached, pen and finger double-click, rejected calibration, passive vs --send-enable capture.

🤖 Generated with Claude Code

modfox and others added 9 commits November 4, 2026 15:04
… gesture tests

Synthesized events now go through an EventSink (SystemEventSink in production) and
TouchGestureEngine takes an injectable clock and onSwipe closure, so the state machines
can be tested without Accessibility permission or osascript.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EventEmitter.reset() posts leftMouseUp / side-button ups / proximity-exit at the last
in-range point. Proximity exit now uses it (it previously dropped the tip without a
mouse-up), and the app calls it on profile change (unplug) and before calibration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Matching on the current mode's 3840x2160 pixel size picked any 4K monitor and missed
the Cintiq in scaled modes. DisplayLocator matches the profile's EDID vendor/model
(DTH2420: 0x5c23/0x1056), then the native mode's pixel size. Both pen mapping and the
calibration overlay use it, removing the overlay's hard-coded 3840x2160.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The window server doesn't derive .mouseEventClickState for posted events, so every pen
or tap click arrived as a single click. ClickCounter tracks multi-clicks using the system
double-click interval and a small distance threshold; pen tip, side buttons and
tap-to-click all set it (on downs, drags and ups).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vendor-wide hotplug watcher registered an input-report callback on every Wacom
device and copied each pen report into a Swift array only to discard it. HIDTransport's
onReport is now optional; with none, the C shim registers no report callback. Each matched
device also gets its own buffer (sized from kIOHIDMaxInputReportSizeKey) instead of all
devices sharing one 64-byte array, writes fall back to another matched device on removal,
and allocation failures are handled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EventEmitter hard-coded Wacom's vendor ID and the DTH2420's 8191 pressure max, so a
model with a different pressure resolution would be scaled wrongly. setProfile() adopts
them from the DeviceProfile whenever mappings are rebuilt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Capture used to send the DTH2420's vendor feature/output reports to every interface of
every Wacom device, where they may mean something else. It now only listens unless
--send-enable is given; ADDING-A-DEVICE.md describes running passive first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CLI calibrate command and the in-app overlay each had a copy of the hold-detection
state machine and solved without checks, so tapping the same spot twice gave xMin == xMax
and a divide-by-zero in the mapping. CalibrationSolver now holds the state machine and
rejects pairs that are too close (<10% of the logical range) or reversed, restarting at
point 1. DisplayMapping also maps a degenerate range to 0 instead of NaN.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…roadmap

- PressureCurve keeps its points sorted once instead of sorting on every pen sample;
  the encoded format (just "points") is unchanged.
- NibConfig.effectiveNaturalScrolling() replaces identical copies in the menu and the
  preferences window; remove the leftover var/_ = def in NibConfig.init(from:).
- ROADMAP: Developer ID signing + notarization is done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@modfox
modfox merged commit d4ea367 into main Nov 4, 2026
1 check passed
@modfox
modfox deleted the fix/evaluation-items branch November 4, 2026 21:59
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.

1 participant