Closing the panel only hid it, and launching the app again did nothing because the running instance ignored the reopen, so it looked like the app refused to exit. Reopening now shows the panel, and the main menu makes Cmd+Q and Cmd+W work while the panel has focus. Termination also tears down the event tap, hotkeys and sync timers instead of leaving them live.
148 lines
5.1 KiB
Swift
148 lines
5.1 KiB
Swift
import SwiftUI
|
|
import AppKit
|
|
|
|
extension Notification.Name {
|
|
static let promptPanelDidShow = Notification.Name("PromptPanelDidShowNotification")
|
|
}
|
|
|
|
private final class KeyablePanel: NSPanel {
|
|
override var canBecomeKey: Bool { true }
|
|
override var canBecomeMain: Bool { false }
|
|
}
|
|
|
|
@MainActor
|
|
final class PanelController: NSObject {
|
|
static let shared = PanelController()
|
|
|
|
private var panel: KeyablePanel?
|
|
private var pasteMonitor: Any?
|
|
let editController = EditWindowController()
|
|
|
|
var isVisible: Bool { panel?.isVisible ?? false }
|
|
|
|
func toggle() {
|
|
isVisible ? hide() : show()
|
|
}
|
|
|
|
func show() {
|
|
let panel = panel ?? makePanel()
|
|
if UserDefaults.standard.string(forKey: "NSWindow Frame PromptPanel") == nil {
|
|
positionAtRightEdge(panel)
|
|
}
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
panel.makeKeyAndOrderFront(nil)
|
|
NSLog("Prompt: panel shown")
|
|
NotificationCenter.default.post(name: .promptPanelDidShow, object: nil)
|
|
}
|
|
|
|
func hide() {
|
|
panel?.orderOut(nil)
|
|
NSApp.deactivate()
|
|
NSLog("Prompt: panel hidden (app keeps running for the Shift-Shift hotkeys)")
|
|
}
|
|
|
|
private func makePanel() -> KeyablePanel {
|
|
let panel = KeyablePanel(
|
|
contentRect: NSRect(x: 0, y: 0, width: 360, height: 640),
|
|
styleMask: [.titled, .closable, .resizable, .nonactivatingPanel, .fullSizeContentView],
|
|
backing: .buffered,
|
|
defer: false
|
|
)
|
|
panel.titleVisibility = .hidden
|
|
panel.titlebarAppearsTransparent = true
|
|
panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
|
panel.standardWindowButton(.zoomButton)?.isHidden = true
|
|
panel.level = .floating
|
|
panel.isMovableByWindowBackground = true
|
|
panel.hidesOnDeactivate = false
|
|
panel.isReleasedWhenClosed = false
|
|
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
|
|
panel.contentView = NSHostingView(rootView: PanelView(store: Store.shared))
|
|
panel.setFrameAutosaveName("PromptPanel")
|
|
self.panel = panel
|
|
installPasteMonitor()
|
|
return panel
|
|
}
|
|
|
|
private func positionAtRightEdge(_ panel: NSPanel) {
|
|
guard let screen = NSScreen.main?.visibleFrame else { return }
|
|
let size = NSSize(width: 360, height: screen.height - 24)
|
|
panel.setContentSize(size)
|
|
panel.setFrameOrigin(NSPoint(x: screen.maxX - size.width - 12, y: screen.minY + 12))
|
|
}
|
|
|
|
// Intercept Cmd+V when the panel is key and the clipboard holds an image
|
|
// (and no text, so normal text paste into the composer is unaffected).
|
|
private func installPasteMonitor() {
|
|
guard pasteMonitor == nil else { return }
|
|
pasteMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
|
|
guard let self, let panel = self.panel, event.window == panel else { return event }
|
|
let modifiers = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
|
|
if modifiers == [.command], event.charactersIgnoringModifiers == "v" {
|
|
let pasteboard = NSPasteboard.general
|
|
if pasteboard.string(forType: .string) == nil,
|
|
let images = pasteboard.readObjects(forClasses: [NSImage.self], options: nil) as? [NSImage],
|
|
let image = images.first {
|
|
Task { @MainActor in Store.shared.addImage(image) }
|
|
return nil
|
|
}
|
|
}
|
|
return event
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class EditWindowController: NSObject {
|
|
private var window: NSWindow?
|
|
|
|
func edit(itemID: UUID, text: String) {
|
|
let view = EditView(
|
|
text: text,
|
|
onSave: { [weak self] newText in
|
|
Store.shared.updateText(itemID, newText: newText)
|
|
self?.window?.close()
|
|
},
|
|
onCancel: { [weak self] in self?.window?.close() }
|
|
)
|
|
let window = NSWindow(
|
|
contentRect: NSRect(x: 0, y: 0, width: 480, height: 320),
|
|
styleMask: [.titled, .closable, .resizable],
|
|
backing: .buffered,
|
|
defer: false
|
|
)
|
|
window.title = "Edit Note"
|
|
window.contentView = NSHostingView(rootView: view)
|
|
window.isReleasedWhenClosed = false
|
|
window.center()
|
|
window.makeKeyAndOrderFront(nil)
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
self.window = window
|
|
}
|
|
}
|
|
|
|
private struct EditView: View {
|
|
@State var text: String
|
|
var onSave: (String) -> Void
|
|
var onCancel: () -> Void
|
|
|
|
var body: some View {
|
|
VStack(spacing: 12) {
|
|
TextEditor(text: $text)
|
|
.font(.body)
|
|
.padding(4)
|
|
.background(Color.primary.opacity(0.05))
|
|
.cornerRadius(6)
|
|
HStack {
|
|
Spacer()
|
|
Button("Cancel", action: onCancel)
|
|
.keyboardShortcut(.cancelAction)
|
|
Button("Save") { onSave(text) }
|
|
.keyboardShortcut(.defaultAction)
|
|
.keyboardShortcut("s", modifiers: .command)
|
|
}
|
|
}
|
|
.padding(16)
|
|
}
|
|
}
|