Skip to content

Standalone Contextual Menu: Floating menu for overlay-free users #117

@malpern

Description

@malpern

Add a standalone floating contextual menu that can appear even when the keyboard overlay is not shown.

Problem

The Overlay Context Integration (previous ticket) works great for users who have the keyboard overlay visible. However, some users:

  • Don't use the overlay (prefer cleaner screen)
  • Have overlay hidden/dimmed
  • Want contextual help without full keyboard display

This ticket adds a lightweight floating menu as an alternative.

User Workflow

When overlay is hidden:

  1. User presses Leader → w

  2. Floating menu appears near cursor or screen center:

    ┌────────────────────────────────┐
    │ Window Management (Press key)  │
    ├────────────────────────────────┤
    │ [H] Left Half                  │
    │ [L] Right Half                 │
    │ [K] Maximize                   │
    │ [J] Center                     │
    │ [Y] Top-Left    [U] Top-Right  │
    │ [B] Bottom-Left [N] Bottom-Rgt │
    │ [M] Next Display [Z] Undo      │
    └────────────────────────────────┘
    
  3. User presses H → menu dismisses, window snaps

  4. OR: 5s timeout → menu auto-dismisses

  5. OR: Esc → menu dismisses

When overlay is visible:

  • Use overlay context integration (previous ticket)
  • Don't show standalone menu (avoid redundancy)

Implementation

ContextMenuWindow.swift (new, ~200 lines)

Features:

  • Floating NSPanel window (non-activating)
  • Positioned near cursor or screen center
  • Compact layout showing only active keys
  • Same data model as overlay version
  • Smooth fade in/out animations
  • Auto-dismiss on action or timeout

Architecture:

@MainActor
final class ContextMenuWindow: NSPanel {
    private var contextLayer: String?
    private var mappings: [KeyMapping] = []
    private var dismissTimer: Timer?
    
    func show(for layer: String, with mappings: [KeyMapping]) {
        // Position near cursor
        // Fade in animation
        // Start dismiss timer
    }
    
    func dismiss() {
        // Fade out animation
        // Close window
    }
}

ContextMenuContentView.swift (new, ~100 lines)

SwiftUI view for menu content:

struct ContextMenuContentView: View {
    let layerName: String
    let mappings: [KeyMapping]
    
    var body: some View {
        VStack(spacing: 8) {
            // Header
            Text(layerName)
            
            // Key grid (compact layout)
            LazyVGrid(columns: 2) {
                ForEach(mappings) { mapping in
                    KeyActionRow(
                        key: mapping.input,
                        action: mapping.actionLabel
                    )
                }
            }
        }
    }
}

Integration Points

RuntimeCoordinator.swift (+30 lines):

  • Check if overlay is visible
  • If not, show standalone menu
  • If yes, use overlay context (existing)

Settings (+20 lines):

  • enableStandaloneContextMenu: Bool
  • contextMenuTimeout: TimeInterval
  • Preference to choose overlay vs standalone

Visual Design

Compact, focused layout:

  • Only shows mapped keys (not full keyboard)
  • 2-column grid for efficiency
  • Minimal chrome (just header + content)
  • Semi-transparent background
  • Drop shadow for depth

Positioning:

  • Option 1: Near cursor (follows mouse)
  • Option 2: Screen center (consistent location)
  • Option 3: User-configurable position

Benefits

For users without overlay:

  • Still get contextual help
  • Lighter weight than full keyboard
  • Cleaner screen real estate

For users with overlay:

  • Choice of which to use
  • Can disable standalone if redundant
  • Preference for different contexts

Configuration

struct ContextMenuSettings {
    var enableOverlayContext: Bool = true
    var enableStandaloneMenu: Bool = false  // Default OFF (overlay preferred)
    var standalonePosition: MenuPosition = .nearCursor
    var contextTimeout: TimeInterval = 5.0
}

enum MenuPosition {
    case nearCursor
    case screenCenter
    case custom(CGPoint)
}

Effort Estimate

  • Total: ~300 lines across 4 files
  • Duration: 2 days
  • Complexity: Low-Medium (straightforward window management)

Dependencies

  • Blocks: None
  • Blocked by: Overlay Context Integration ticket (data model reuse)

Success Criteria

  • ✅ Menu appears when overlay is hidden
  • ✅ Shows only mapped keys for active context
  • ✅ Positioned appropriately (near cursor or center)
  • ✅ Auto-dismisses after timeout or action
  • ✅ User can choose overlay vs standalone via settings
  • ✅ Works for all context types (window, launcher, etc.)

Future Enhancements (Not in Scope)

  • Multiple menu styles/themes
  • Custom positioning per context
  • Keyboard-only navigation (arrow keys)
  • Pin menu to stay visible

References

  • Design doc: docs/features/leader-key-overlay-integration.md (Phase 2)
  • Prerequisite: Overlay Context Integration ticket
  • Related: MAL-38 (leader key visual menu)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions