Skip to content

Add customizable mouse-click effects with new "Mouse" preferences tab - #344

Open
prossm wants to merge 1 commit into
keycastr:mainfrom
prossm:custom-click
Open

Add customizable mouse-click effects with new "Mouse" preferences tab#344
prossm wants to merge 1 commit into
keycastr:mainfrom
prossm:custom-click

Conversation

@prossm

@prossm prossm commented Apr 27, 2026

Copy link
Copy Markdown

Hi! KeyCastr user here, first contribution. I built this for my own use and figured I'd offer it upstream. Feel free to incorporate it, or not as you see fit. Full transparency: I did use a coding agent for the work, but it's fully functional.

What this adds

A new "Mouse" preferences tab with four mouse-click visual effects (Halo, Blur, Halo + Blur, Droplet) and per-effect customization for color, density, diameter, and duration. The original Halo behavior is preserved as the default, just with its color decoupled from the keystroke bezel color.

Demo: https://www.loom.com/share/1a43df69bfbf4ce09d79115e62cc0696

Design notes worth flagging for review

  • Added as a separate prefs tab rather than extending the Display tab: I tried three approaches to graft the controls onto Display and each one collided with the existing tab's autoresize-mask layout — a new tab kept the Display tab pristine and avoided risk to the visualizer plugin pref injection. Feel free to move it back if you have a layout approach in mind.
  • Namespaced preferences: All new prefs are namespaced under mouse.* keys; the existing default.bezelColor is no longer read by the mouse visualizer (its color is now mouse.effectColor1). I didn't add a migration — fresh installs get the new opaque defaults, existing users keep their bezel color for keystrokes and pick a new mouse color on first visit.
  • Programmatic UI: UI is built programmatically rather than in MainMenu.nib so the binary nib stays untouched. Easier to review as text; downside is it's not editable in Interface Builder.
  • Tests: No new tests. The existing KCMouseEventVisualizerTests still pass.

Commit

Full technical breakdown is in the single commit message — file-by-file walkthrough of methods touched, KVO wiring, droplet anchor-point fix, etc. Worth reading before reviewing the diff.

# What this changes (non-technical)

Adds four new visual effects for mouse clicks, plus a dedicated "Mouse"
tab in Preferences for configuring them. Previously the only mouse-click
indicator was a thin circle whose color was tied to the keystroke
bezel; now click visualization is its own first-class feature.

The new "Mouse" tab (placed between Display and Update in the toolbar)
exposes:

  - Mouse Effect: Halo (the original ring), Blur (a soft, expanding
    cloud), Halo + Blur (both layered), or Droplet (concentric ripples
    that fan outward from the click site, evoking a drop in water).
  - Mouse Effect Color 1: drives the halo stroke and droplet rings.
  - Mouse Effect Color 2: drives the blur fill (used in Blur and
    Halo + Blur).
  - Mouse Effect Density: stroke/core thickness, applied per effect.
  - Mouse Effect Diameter: base size of the effect at the click point.
  - Mouse Effect Duration: how long the animation runs end-to-end.

All numeric controls have both a slider and an editable rounded text
field for precise values. Halo continues to follow the cursor while
the button is held; Blur and Droplet anchor to the click site and run
to completion. Defaults preserve the original Halo look out of the box.

# What this changes (technical)

## KCMouseEventVisualizer.h / .m

Major rewrite of the mouse-click rendering pipeline.

  - Exports new defaults keys: kKCMouseEffectKey, kKCMouseEffectColor1Key,
    kKCMouseEffectColor2Key, kKCMouseEffectDurationKey,
    kKCMouseEffectDensityKey, kKCMouseEffectDiameterKey.
  - Adds class method +defaultPreferences returning the registered
    defaults dict (consumed by KCAppController.registerDefaults). Default
    colors are fully opaque so NSColorWell renders without its
    transparency-indicator diagonal.
  - Extends the KCMouseDisplayOptionsProvider protocol with
    mouseEffectNames and currentMouseEffectName for popup binding.
  - KCMouseVisualizerWindow:
    - Resized to fit the worst-case (kKCMouseEffectMaxDiameter ×
      kKCMouseEffectMaxScale + 40pt) so any in-range diameter at any
      scale fits without clipping.
    - setIgnoresMouseEvents:YES so the overlay never intercepts clicks.
    - KVO observers on the five new defaults keys; observer callback
      tears down cached layers via -tearDownLayers, forcing a rebuild
      with fresh values on the next event.
    - -resolvedEffect, -effectColor1/2, -effectDuration, -effectDensity,
      -effectRadius read live from NSUserDefaults each event.
  - Per-effect renderers:
    - showHaloPersistent / fadeHalo: stroked CAShapeLayer, opacity
      animation. Both wrapped in CATransaction setDisableActions:YES to
      kill CALayer's implicit 0.25s opacity animation that was producing
      a visible double-blink against the explicit fade-out.
    - runBlurAnimation: CAGradientLayer (kCAGradientLayerRadial) with a
      three-stop gradient — opaque core, opaque core, clear edge — where
      the core stop position is derived from density. Bounds explicit,
      position centered on cursor; transform.scale + opacity keyframe
      run as a CAAnimationGroup.
    - runDropletAnimation: kKCDropletRingCount=3 stroked CAShapeLayers
      with bounds (diameter × diameter) and explicit position so
      anchorPoint (0.5, 0.5) sits on the cursor — fixes a bug where
      transform.scale was scaling around the parent layer's (0,0) and
      flinging rings up-and-right. Each ring carries a CGShadow glow in
      the stroke color (shadowOpacity 0.9, shadowRadius 6) for soft
      edges, plus animated lineWidth thinning to dissipate. Rings are
      staggered by duration / (ringCount + 1) via CAAnimationGroup
      beginTime.
  - updateWithMouseEvent: dispatches per effect — halo follows cursor on
    drag and fades on mouseUp; blur and droplet trigger once on
    mouseDown and ignore drag/up.

## KCAppController.h / .m

  - Forward-declares KCMouseEventVisualizer; exposes
    -mouseEventVisualizer and -availableMouseEffectNames so the prefs
    controller can drive the popup binding.
  - registerDefaults merges in [KCMouseEventVisualizer defaultPreferences].
  - Lazily registers KCColorValueTransformer (an
    NSSecureUnarchiveFromDataTransformer for NSColor) so NSColorWell
    bindings can round-trip color data through NSUserDefaultsController.

## KCPrefsWindowController.h / .m

Adds the "Mouse" preference pane programmatically — no nib edits.

  - Three earlier attempts to inject controls into the existing Display
    tab failed because of entanglement with the static subview's
    autoresize masks and the legacy s.height += subview.height * 2.0
    hack in -changeVisualizerFrom:to:. The current approach builds a
    fully self-contained tab and registers it alongside the nib-loaded
    tabs.
  - -registerMouseEffectsTab runs from -nudge before the toolbar is
    constructed, appends the content view to preferenceViews, and
    inserts the "Mouse" identifier into toolbarItemIdentifiers at
    count - 1 so the visual order is General | Display | Mouse | Update
    while the tag (= preferenceViews index) stays at the end.
  - Toolbar icon uses the SF Symbol "computermouse" on macOS 11+ with
    NSImageNameComputer as fallback.
  - -buildMouseEffectsBoxWithWidth: lays out a 6-row NSBox with absolute
    frames (NSGridView with autolayout proved fragile inside the
    NSBox + NSAtTop title-bar layout). Content height accounts for the
    ~24pt title bar reserve via -mouseEffectsContentHeight +
    -mouseEffectsBoxHeight.
  - -addSliderRowToContent:... helper builds a slider + editable
    rounded-bezel NSTextField pair, both bound via NSUserDefaultsController
    keypaths with NSContinuouslyUpdatesValueBindingOption so dragging
    the slider updates the field live and typing in the field updates
    the slider on commit. NSNumberFormatter min/max enforce range.
  - Color wells bind via NSValueBinding with
    NSValueTransformerNameBindingOption: NSStringFromClass([KCColorValueTransformer class])
    so NSData blobs in defaults round-trip to NSColor.
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