The panel is a full-height strip that sits beside your work, where the system grey reads as a half-lit window next to a dark editor. Dark mode is now pure black, with card and row colours in one Theme file so both the panel background and the strip under the titlebar match.
158 lines
5.7 KiB
Swift
158 lines
5.7 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.isOpaque = true
|
|
// Match the SwiftUI content so the titlebar strip and any resize flash
|
|
// do not show the system grey through.
|
|
panel.backgroundColor = Theme.background
|
|
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)
|
|
}
|
|
}
|