diff --git a/Sources/Visto/App/VistoApp.swift b/Sources/Visto/App/VistoApp.swift index 2bce880..f475007 100644 --- a/Sources/Visto/App/VistoApp.swift +++ b/Sources/Visto/App/VistoApp.swift @@ -29,9 +29,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { @objc private func openNewWindow(_ sender: Any?) { let session = BrowserSession() - let window = NSWindow( + let window = VistoWindow( contentRect: NSRect(origin: .zero, size: windowSize), - styleMask: [.borderless], + styleMask: [.titled, .closable, .miniaturizable, .utilityWindow], backing: .buffered, defer: false ) @@ -40,6 +40,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { window.backgroundColor = .white window.isOpaque = true window.hasShadow = true + window.isFloatingPanel = false + window.hidesOnDeactivate = false + window.isMovableByWindowBackground = false window.contentMinSize = windowSize window.contentMaxSize = windowSize window.isReleasedWhenClosed = false @@ -140,6 +143,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { editMenu.addItem(menuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")) editMenu.addItem(menuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")) editMenu.addItem(menuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")) + editMenu.addItem(menuItem(title: "Delete", action: #selector(NSText.delete(_:)), keyEquivalent: "")) editMenu.addItem(menuItem(title: "Select All", action: #selector(NSText.selectAll(_:)), keyEquivalent: "a")) editMenuItem.submenu = editMenu mainMenu.addItem(editMenuItem) diff --git a/Sources/Visto/Support/VistoWindow.swift b/Sources/Visto/Support/VistoWindow.swift new file mode 100644 index 0000000..3d4607e --- /dev/null +++ b/Sources/Visto/Support/VistoWindow.swift @@ -0,0 +1,8 @@ +import AppKit + +/// Utility-style panel so Visto gets the standard compact title bar +/// (traffic lights + title) used by macOS utility apps. +final class VistoWindow: NSPanel { + override var canBecomeKey: Bool { true } + override var canBecomeMain: Bool { true } +} diff --git a/Sources/Visto/Views/BrowserChromeView.swift b/Sources/Visto/Views/BrowserChromeView.swift index 847ce48..9140a59 100644 --- a/Sources/Visto/Views/BrowserChromeView.swift +++ b/Sources/Visto/Views/BrowserChromeView.swift @@ -15,18 +15,6 @@ struct BrowserChromeView: NSViewRepresentable { } final class BrowserChromeNSView: NSView, NSTextFieldDelegate { - private let closeButton = NSWindow.standardWindowButton( - .closeButton, - for: [.titled, .closable] - )! - private let minimizeButton = NSWindow.standardWindowButton( - .miniaturizeButton, - for: [.titled, .miniaturizable] - )! - private let zoomButton = NSWindow.standardWindowButton( - .zoomButton, - for: [.titled, .resizable] - )! private let addressField = NSTextField() private let refreshButton = NSButton() private let separator = NSBox() @@ -41,7 +29,6 @@ final class BrowserChromeNSView: NSView, NSTextFieldDelegate { wantsLayer = true layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor - configureWindowButtons() configureAddressField() configureRefreshButton() @@ -60,33 +47,23 @@ final class BrowserChromeNSView: NSView, NSTextFieldDelegate { override func layout() { super.layout() - let buttonSize = NSSize(width: 14, height: 14) - let buttonY = floor((bounds.height - buttonSize.height) / 2) - closeButton.frame = NSRect(x: 12, y: buttonY, width: buttonSize.width, height: buttonSize.height) - minimizeButton.frame = NSRect(x: 32, y: buttonY, width: buttonSize.width, height: buttonSize.height) - zoomButton.frame = NSRect(x: 52, y: buttonY, width: buttonSize.width, height: buttonSize.height) - let controlHeight: CGFloat = 22 let controlY = floor((bounds.height - controlHeight) / 2) - let addressX: CGFloat = 78 + let leading: CGFloat = 10 let refreshWidth: CGFloat = 24 let trailing: CGFloat = 7 let gap: CGFloat = 5 let refreshX = bounds.width - trailing - refreshWidth addressField.frame = NSRect( - x: addressX, + x: leading, y: controlY, - width: max(80, refreshX - gap - addressX), + width: max(80, refreshX - gap - leading), height: controlHeight ) refreshButton.frame = NSRect(x: refreshX, y: controlY, width: refreshWidth, height: controlHeight) separator.frame = NSRect(x: 0, y: 0, width: bounds.width, height: 1) } - override func mouseDown(with event: NSEvent) { - window?.performDrag(with: event) - } - func updateSession(_ session: BrowserSession) { guard self.session !== session else { return @@ -101,22 +78,12 @@ final class BrowserChromeNSView: NSView, NSTextFieldDelegate { session.inputURL = addressField.stringValue } - private func configureWindowButtons() { - closeButton.target = self - closeButton.action = #selector(closeWindow) - minimizeButton.target = self - minimizeButton.action = #selector(minimizeWindow) - zoomButton.isEnabled = false - - addSubview(closeButton) - addSubview(minimizeButton) - addSubview(zoomButton) - } - private func configureAddressField() { addressField.placeholderString = "localhost:3000" addressField.controlSize = .small addressField.font = .systemFont(ofSize: 12) + addressField.isEditable = true + addressField.isSelectable = true addressField.delegate = self addressField.target = self addressField.action = #selector(loadAddress) @@ -161,14 +128,6 @@ final class BrowserChromeNSView: NSView, NSTextFieldDelegate { } } - @objc private func closeWindow() { - window?.close() - } - - @objc private func minimizeWindow() { - window?.miniaturize(nil) - } - @objc private func loadAddress() { session.inputURL = addressField.stringValue session.loadInput() diff --git a/Sources/Visto/Views/WebViewContainer.swift b/Sources/Visto/Views/WebViewContainer.swift index 97e2221..b3e5f04 100644 --- a/Sources/Visto/Views/WebViewContainer.swift +++ b/Sources/Visto/Views/WebViewContainer.swift @@ -1,3 +1,4 @@ +import AppKit import SwiftUI import WebKit @@ -17,7 +18,7 @@ struct WebViewContainer: NSViewRepresentable { configuration.preferences.javaScriptCanOpenWindowsAutomatically = true configuration.defaultWebpagePreferences.preferredContentMode = .mobile - let webView = WKWebView(frame: .zero, configuration: configuration) + let webView = FocusableWebView(frame: .zero, configuration: configuration) webView.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 26_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1" webView.allowsBackForwardNavigationGestures = true webView.navigationDelegate = context.coordinator @@ -93,3 +94,14 @@ struct WebViewContainer: NSViewRepresentable { var hardReloadToken = 0 } } + +/// Ensures clicks in the preview claim first responder so Delete and +/// Command-C/V reach page inputs through the AppKit responder chain. +private final class FocusableWebView: WKWebView { + override var acceptsFirstResponder: Bool { true } + + override func mouseDown(with event: NSEvent) { + window?.makeFirstResponder(self) + super.mouseDown(with: event) + } +}