Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions Sources/ZoomItMacCore/App/AppController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,136 @@ final class AppController: NSObject {
presentPermissionsDialog()
}

@objc func showShortcuts() {
presentShortcutsDialog()
}

/// Shows a summary of every global keyboard shortcut ZoomIt currently
/// responds to. Values are read live from the settings store so any user
/// customization is reflected. The list also documents the in-mode keys
/// (colors, shapes, undo, etc.) that are handled once an overlay is active.
private func presentShortcutsDialog() {
let settings = settingsStore.load()

func describe(code: Int, modifiers: UInt) -> String {
guard code != 0 else { return "None" }
return SettingsWindowController.describe(
keyCode: code,
modifiers: NSEvent.ModifierFlags(rawValue: modifiers)
)
}

let zoom = describe(code: settings.hotKeyCode, modifiers: settings.hotKeyModifiers)
let draw = describe(code: settings.drawHotKeyCode, modifiers: settings.drawHotKeyModifiers)
let live = describe(code: settings.liveHotKeyCode, modifiers: settings.liveHotKeyModifiers)
let snipCopy = describe(code: settings.snipHotKeyCode, modifiers: settings.snipHotKeyModifiers)
let snipSave = describe(
code: settings.snipHotKeyCode,
modifiers: settings.snipHotKeyModifiers ^ NSEvent.ModifierFlags.shift.rawValue
)
let snipOcr = describe(code: settings.snipOcrHotKeyCode, modifiers: settings.snipOcrHotKeyModifiers)
let record = describe(code: settings.recordHotKeyCode, modifiers: settings.recordHotKeyModifiers)
let recordRegion = describe(
code: settings.recordHotKeyCode,
modifiers: settings.recordHotKeyModifiers ^ NSEvent.ModifierFlags.shift.rawValue
)
let demo = describe(code: settings.demoTypeHotKeyCode, modifiers: settings.demoTypeHotKeyModifiers)
let demoReset = describe(
code: settings.demoTypeHotKeyCode,
modifiers: settings.demoTypeHotKeyModifiers ^ NSEvent.ModifierFlags.shift.rawValue
)
let panoramaCopy = describe(code: settings.panoramaHotKeyCode, modifiers: settings.panoramaHotKeyModifiers)
let panoramaSave = describe(
code: settings.panoramaHotKeyCode,
modifiers: settings.panoramaHotKeyModifiers ^ NSEvent.ModifierFlags.shift.rawValue
)
let breakTimer = describe(code: settings.breakHotKeyCode, modifiers: settings.breakHotKeyModifiers)

let global = """
Global shortcuts
Static Zoom: \(zoom)
Live Zoom: \(live)
Draw w/out Zoom: \(draw)
Snip → Clipboard: \(snipCopy)
Snip → File: \(snipSave)
Snip → OCR: \(snipOcr)
Record Screen: \(record)
Record Region: \(recordRegion)
Panorama → Clipbd: \(panoramaCopy)
Panorama → File: \(panoramaSave)
DemoType Start: \(demo)
DemoType Reset: \(demoReset)
Break Timer: \(breakTimer)
"""

let inMode = """
While zoomed / drawing
Zoom in / out: Option+Up / Option+Down (Live Zoom)
Draw: Left mouse button
Exit draw: Right mouse button
Undo: Command+Z or Control+Z
Erase all: E
Pen width: Mouse wheel, [ / ], or Shift+Up/Down
Colors: R G B O Y P W K
Highlighter: Shift + color key
Line: Hold Shift while dragging
Rectangle: Hold Control while dragging
Ellipse: Hold Tab while dragging
Arrow: Hold Shift+Control while dragging
Blank screen: Control+W (white) / Control+K (black)
Type text: T (left) / Shift+T (right)
Font size: + / − while typing
Exit: Esc
"""

let alert = NSAlert()
alert.messageText = "ZoomIt Keyboard Shortcuts"
alert.informativeText = "Customize global shortcuts in Settings…"
alert.addButton(withTitle: "Done")
alert.addButton(withTitle: "Open Settings…")
if let icon = ZoomItAppIcon.standardIcon() {
alert.icon = icon
}

// Render the shortcut tables in a monospaced font inside an accessory
// view so the two columns line up. NSAlert's informativeText uses the
// proportional system font, which makes space-padded columns wobble.
let body = global + "\n\n" + inMode
let font = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)
let attributed = NSAttributedString(
string: body,
attributes: [
.font: font,
.foregroundColor: NSColor.labelColor
]
)

let label = NSTextField(labelWithAttributedString: attributed)
label.isSelectable = true
label.lineBreakMode = .byClipping
label.usesSingleLineMode = false
label.translatesAutoresizingMaskIntoConstraints = true

// Size the accessory view to fit the intrinsic text size so NSAlert
// grows the whole dialog around it instead of clipping it.
let fitting = label.sizeThatFits(
NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
)
let container = NSView(frame: NSRect(origin: .zero, size: fitting))
container.translatesAutoresizingMaskIntoConstraints = true
container.addSubview(label)
label.frame = container.bounds
label.autoresizingMask = [.width, .height]
alert.accessoryView = container

alert.window.animationBehavior = .none
NSApp.activate(ignoringOtherApps: true)

if alert.runModal() == .alertSecondButtonReturn {
settingsWindowController.show()
}
}

/// Shows the permission status dialog and acts on the chosen button, then
/// re-presents itself so the user can grant or open settings for several
/// permissions in one sitting and watch the status refresh. For the
Expand Down
1 change: 1 addition & 0 deletions Sources/ZoomItMacCore/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ public final class AppDelegate: NSObject, NSApplicationDelegate {
StatusMenuEntry(title: "Panorama Capture", action: #selector(AppController.startPanorama), keyEquivalent: ""),
StatusMenuEntry(title: "Break Timer", action: #selector(AppController.toggleBreakTimer), keyEquivalent: ""),
.separator,
StatusMenuEntry(title: "Keyboard Shortcuts", action: #selector(AppController.showShortcuts), keyEquivalent: ""),
StatusMenuEntry(title: "Check Permissions", action: #selector(AppController.checkPermissions), keyEquivalent: ""),
StatusMenuEntry(title: "Quit", action: #selector(AppController.quit), keyEquivalent: "q")
]
Expand Down
16 changes: 16 additions & 0 deletions Sources/ZoomItMacCore/SelfTest/SelfTestRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public enum SelfTestRunner {
try testPanoramaEscapeCancel()
try testIdleSleepAssertionLifecycle()
try testStatusMenuOrderMatchesWindows()
try testStatusMenuActionsAreWired()
try testClipTransitionUpdatesOnChange()
try testWebcamOverlayDragOrigin()
try testTrimSavePreservesOriginal()
Expand Down Expand Up @@ -570,6 +571,7 @@ public enum SelfTestRunner {
"Record Screen", // Record
"Panorama Capture", // macOS-only, after Record
"Break Timer", // moved below Panorama Capture
"Keyboard Shortcuts",
"Check Permissions",
"Quit"
]
Expand All @@ -596,6 +598,20 @@ public enum SelfTestRunner {
try expect(titles.last == "Quit", "Expected Quit to be the last menu item")
}

/// Every non-separator status-menu entry must have a non-nil action that
/// AppController actually responds to; otherwise the menu item would be
/// permanently disabled at runtime. Regression guard for wiring up new
/// entries such as "Keyboard Shortcuts".
private static func testStatusMenuActionsAreWired() throws {
for entry in AppDelegate.statusMenuEntries() where !entry.isSeparator {
guard let action = entry.action else {
throw SelfTestError.failure("Expected menu entry '\(entry.title)' to have an action")
}
try expect(AppController.instancesRespond(to: action),
"Expected AppController to respond to \(action) for menu entry '\(entry.title)'")
}
}

/// Changing the clip transition popup from Fade to Black to Fade to White
/// must update the existing append boundary (previously it stayed black
/// because the transition was captured only at append time). Delete-seam
Expand Down