Skip to content
Draft
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
7 changes: 7 additions & 0 deletions Snapzy/Features/Recording/RecordingCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ final class RecordingCoordinator: ObservableObject {
"isActive": "\(isActive)",
"recorderState": "\(recorder.state)",
])

guard recorder.state != .idle else {
cleanup()
return
}

Task {
await recorder.cancelRecording()
cleanup()
Expand Down Expand Up @@ -281,6 +287,7 @@ final class RecordingCoordinator: ObservableObject {
toolbarWindow = toolbar

showRegionOverlay(for: rect, interactionEnabled: captureMode != .application)
toolbar.bringToFrontForInteraction()
setupEscapeMonitors()
}

Expand Down
67 changes: 64 additions & 3 deletions Snapzy/Features/Recording/RecordingToolbarWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,36 @@
//

import AppKit
import Carbon.HIToolbox
import Combine
import SwiftUI

@MainActor
private final class RecordingToolbarHostingView<Content: View>: NSHostingView<Content> {
var onEscape: (() -> Void)?

override var acceptsFirstResponder: Bool {
true
}

override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
true
}

override func keyDown(with event: NSEvent) {
guard event.keyCode == UInt16(kVK_Escape) else {
super.keyDown(with: event)
return
}

onEscape?()
}

override func cancelOperation(_ sender: Any?) {
onEscape?()
}
}

enum RecordingToolbarMode {
case preRecord
case recording
Expand Down Expand Up @@ -150,11 +177,11 @@ final class RecordingToolbarState: ObservableObject {
// MARK: - Toolbar Window

@MainActor
final class RecordingToolbarWindow: NSWindow {
final class RecordingToolbarWindow: NSPanel {

private var anchorRect: CGRect
private var mode: RecordingToolbarMode = .preRecord
private var hostingView: NSHostingView<AnyView>?
private var hostingView: RecordingToolbarHostingView<AnyView>?
private var effectView: NSVisualEffectView?
private var cachedContentSize: CGSize?

Expand Down Expand Up @@ -234,7 +261,9 @@ final class RecordingToolbarWindow: NSWindow {
level = .popUpMenu
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
hasShadow = true
isFloatingPanel = true
isReleasedWhenClosed = false
hidesOnDeactivate = false

// Apply theme appearance at window level (mirrors AnnotateWindow.applyTheme)
appearance = ThemeManager.shared.nsAppearance
Expand Down Expand Up @@ -281,7 +310,11 @@ final class RecordingToolbarWindow: NSWindow {

private func setContent(_ view: AnyView) {
let themedView = view.preferredColorScheme(ThemeManager.shared.systemAppearance)
let hosting = NSHostingView(rootView: AnyView(themedView))
let hosting = RecordingToolbarHostingView(rootView: AnyView(themedView))
hosting.onEscape = { [weak self] in
guard self?.mode == .preRecord else { return }
self?.onCancel?()
}
hosting.translatesAutoresizingMaskIntoConstraints = false

// NSVisualEffectView provides native wallpaper-tinted material backing,
Expand Down Expand Up @@ -339,10 +372,38 @@ final class RecordingToolbarWindow: NSWindow {
/// Position and order the window to the front (initial show only).
private func showBelowRect(_ rect: CGRect) {
positionBelowRect(rect)
bringToFrontForInteraction()
}

func bringToFrontForInteraction() {
NSApp.activate(ignoringOtherApps: true)
orderFrontRegardless()
makeKey()
if let hostingView {
makeFirstResponder(hostingView)
}
}

override var canBecomeKey: Bool { true }
override var canBecomeMain: Bool { false }

override func keyDown(with event: NSEvent) {
guard mode == .preRecord, event.keyCode == UInt16(kVK_Escape) else {
super.keyDown(with: event)
return
}

onCancel?()
}

override func cancelOperation(_ sender: Any?) {
guard mode == .preRecord else {
super.cancelOperation(sender)
return
}

onCancel?()
}

func updateAnchorRect(_ rect: CGRect) {
anchorRect = rect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,45 @@ import XCTest

final class RecordingToolbarShortcutsTests: XCTestCase {

@MainActor
func testPreRecordToolbarWindow_usesActivatingPanelForButtonClicks() {
let window = RecordingToolbarWindow(anchorRect: CGRect(x: 0, y: 0, width: 640, height: 360))
defer { window.close() }

XCTAssertTrue(window.styleMask.contains(.borderless))
XCTAssertFalse(window.styleMask.contains(.nonactivatingPanel))
XCTAssertTrue(window.canBecomeKey)
XCTAssertFalse(window.canBecomeMain)
}

@MainActor
func testPreRecordToolbarWindow_escapeInvokesCancel() throws {
let window = RecordingToolbarWindow(anchorRect: CGRect(x: 0, y: 0, width: 640, height: 360))
defer { window.close() }

var didCancel = false
window.onCancel = {
didCancel = true
}

let event = try XCTUnwrap(NSEvent.keyEvent(
with: .keyDown,
location: .zero,
modifierFlags: [],
timestamp: 0,
windowNumber: window.windowNumber,
context: nil,
characters: "\u{1b}",
charactersIgnoringModifiers: "\u{1b}",
isARepeat: false,
keyCode: UInt16(kVK_Escape)
))

window.keyDown(with: event)

XCTAssertTrue(didCancel)
}

// MARK: - GlobalShortcutKind & ShortcutAction Case Matching

func testRecordingToolbarShortcuts_kindsArePresent() {
Expand Down
Loading