Pasting still failed because the panel is a nonactivating panel: it can be key while the app is not active, and menu key equivalents are not delivered reliably in that state. Cmd+V is now dispatched to the responder chain directly, and the app no longer replaces SwiftUI's main menu — it only adds Hide Panel to it, so the Edit items that AppKit routes text shortcuts through stay intact. Rows also gained a checkbox so a workspace works as a checklist without turning the row into a button, which would swallow the checkbox's clicks.
154 lines
5.5 KiB
Swift
154 lines
5.5 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))
|
|
}
|
|
|
|
// Cmd+V is handled here rather than left to a menu key equivalent. The
|
|
// panel is a nonactivating panel, so it can be key while the app is not
|
|
// active, and menu key equivalents are not reliably delivered in that
|
|
// state — which is what silently broke pasting. Dispatching paste: to the
|
|
// responder chain ourselves removes that dependency.
|
|
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)
|
|
guard modifiers == [.command], event.charactersIgnoringModifiers == "v" else { return event }
|
|
|
|
let pasteboard = NSPasteboard.general
|
|
if pasteboard.string(forType: .string) != nil {
|
|
NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: nil)
|
|
return nil
|
|
}
|
|
if 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)
|
|
}
|
|
}
|