|
@@ -40,6 +40,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
private var moreButton: NSButton!
|
|
private var moreButton: NSButton!
|
|
|
private var urlField: NSTextField!
|
|
private var urlField: NSTextField!
|
|
|
private var isUpdatingURLField = false
|
|
private var isUpdatingURLField = false
|
|
|
|
|
+ private var didSubmitURLFromAction = false
|
|
|
private var isPinnedOnTop = false
|
|
private var isPinnedOnTop = false
|
|
|
private var isGestureModeEnabled = true
|
|
private var isGestureModeEnabled = true
|
|
|
|
|
|
|
@@ -101,30 +102,40 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
toolbarBlur.layer?.cornerRadius = 14
|
|
toolbarBlur.layer?.cornerRadius = 14
|
|
|
toolbarBlur.layer?.masksToBounds = true
|
|
toolbarBlur.layer?.masksToBounds = true
|
|
|
|
|
|
|
|
- func makeIconButton(symbolName: String, accessibility: String, action: Selector) -> NSButton {
|
|
|
|
|
|
|
+ func makeIconButton(symbolName: String, accessibility: String, action: Selector, fallbackSymbolName: String? = nil) -> NSButton {
|
|
|
let button = NSButton()
|
|
let button = NSButton()
|
|
|
- button.image = NSImage(
|
|
|
|
|
- systemSymbolName: symbolName,
|
|
|
|
|
- accessibilityDescription: accessibility
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ let primary = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibility)
|
|
|
|
|
+ let fallback = fallbackSymbolName.flatMap { NSImage(systemSymbolName: $0, accessibilityDescription: accessibility) }
|
|
|
|
|
+ button.image = primary ?? fallback
|
|
|
button.image?.isTemplate = true
|
|
button.image?.isTemplate = true
|
|
|
button.isBordered = false
|
|
button.isBordered = false
|
|
|
button.contentTintColor = .white
|
|
button.contentTintColor = .white
|
|
|
|
|
+ button.imagePosition = .imageOnly
|
|
|
|
|
+ button.imageScaling = .scaleProportionallyDown
|
|
|
button.setButtonType(.momentaryChange)
|
|
button.setButtonType(.momentaryChange)
|
|
|
button.action = action
|
|
button.action = action
|
|
|
button.setButtonType(.momentaryChange)
|
|
button.setButtonType(.momentaryChange)
|
|
|
|
|
|
|
|
// Subtle circular hit target like the screenshot.
|
|
// Subtle circular hit target like the screenshot.
|
|
|
button.wantsLayer = true
|
|
button.wantsLayer = true
|
|
|
- button.layer?.cornerRadius = 11
|
|
|
|
|
|
|
+ button.layer?.cornerRadius = 12
|
|
|
button.layer?.masksToBounds = true
|
|
button.layer?.masksToBounds = true
|
|
|
button.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.06).cgColor
|
|
button.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.06).cgColor
|
|
|
button.layer?.borderColor = NSColor.white.withAlphaComponent(0.10).cgColor
|
|
button.layer?.borderColor = NSColor.white.withAlphaComponent(0.10).cgColor
|
|
|
button.layer?.borderWidth = 1
|
|
button.layer?.borderWidth = 1
|
|
|
|
|
|
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- button.widthAnchor.constraint(equalToConstant: 22).isActive = true
|
|
|
|
|
- button.heightAnchor.constraint(equalToConstant: 22).isActive = true
|
|
|
|
|
|
|
+ button.widthAnchor.constraint(equalToConstant: 26).isActive = true
|
|
|
|
|
+ button.heightAnchor.constraint(equalToConstant: 26).isActive = true
|
|
|
|
|
+
|
|
|
|
|
+ if button.image == nil {
|
|
|
|
|
+ // Last-resort fallback if SF Symbols aren’t available for some reason.
|
|
|
|
|
+ button.title = "⋮"
|
|
|
|
|
+ button.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
|
|
|
|
|
+ button.contentTintColor = NSColor.white.withAlphaComponent(0.9)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ button.title = ""
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
button.toolTip = accessibility
|
|
button.toolTip = accessibility
|
|
|
return button
|
|
return button
|
|
@@ -172,28 +183,54 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
action: #selector(showLauncher)
|
|
action: #selector(showLauncher)
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- // URL field
|
|
|
|
|
|
|
+ // URL "pill" wrapper.
|
|
|
|
|
+ let urlWrapperView = NSView()
|
|
|
|
|
+ urlWrapperView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ urlWrapperView.wantsLayer = true
|
|
|
|
|
+ urlWrapperView.layer?.cornerRadius = 12
|
|
|
|
|
+ urlWrapperView.layer?.masksToBounds = true
|
|
|
|
|
+ urlWrapperView.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.07).cgColor
|
|
|
|
|
+ urlWrapperView.layer?.borderColor = NSColor.white.withAlphaComponent(0.18).cgColor
|
|
|
|
|
+ urlWrapperView.layer?.borderWidth = 1
|
|
|
|
|
+
|
|
|
|
|
+ // URL field (inside pill)
|
|
|
urlField = NSTextField()
|
|
urlField = NSTextField()
|
|
|
urlField.translatesAutoresizingMaskIntoConstraints = false
|
|
urlField.translatesAutoresizingMaskIntoConstraints = false
|
|
|
urlField.action = #selector(urlFieldDidSubmit)
|
|
urlField.action = #selector(urlFieldDidSubmit)
|
|
|
- urlField.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
|
|
|
|
|
|
+ urlField.placeholderString = "Search or enter website name"
|
|
|
|
|
+ urlField.font = NSFont.systemFont(ofSize: 13, weight: .medium)
|
|
|
urlField.textColor = .white
|
|
urlField.textColor = .white
|
|
|
urlField.alignment = .left
|
|
urlField.alignment = .left
|
|
|
urlField.isEditable = true
|
|
urlField.isEditable = true
|
|
|
urlField.isBordered = false
|
|
urlField.isBordered = false
|
|
|
urlField.drawsBackground = false
|
|
urlField.drawsBackground = false
|
|
|
- urlField.wantsLayer = true
|
|
|
|
|
- urlField.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
|
|
-
|
|
|
|
|
- // URL "pill" wrapper.
|
|
|
|
|
- let urlWrapperView = NSView()
|
|
|
|
|
- urlWrapperView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
- urlWrapperView.wantsLayer = true
|
|
|
|
|
- urlWrapperView.layer?.cornerRadius = 10
|
|
|
|
|
- urlWrapperView.layer?.masksToBounds = true
|
|
|
|
|
- urlWrapperView.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.06).cgColor
|
|
|
|
|
- urlWrapperView.layer?.borderColor = NSColor.white.withAlphaComponent(0.12).cgColor
|
|
|
|
|
- urlWrapperView.layer?.borderWidth = 1
|
|
|
|
|
|
|
+ urlField.focusRingType = .none
|
|
|
|
|
+
|
|
|
|
|
+ // Optional "lock" glyph like browsers (purely visual).
|
|
|
|
|
+ let lockImageView = NSImageView()
|
|
|
|
|
+ lockImageView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ lockImageView.image = NSImage(systemSymbolName: "lock.fill", accessibilityDescription: "Secure")
|
|
|
|
|
+ lockImageView.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
|
|
|
|
|
+ lockImageView.contentTintColor = NSColor.white.withAlphaComponent(0.7)
|
|
|
|
|
+ lockImageView.imageScaling = .scaleProportionallyDown
|
|
|
|
|
+
|
|
|
|
|
+ let urlInnerStack = NSStackView(views: [lockImageView, urlField])
|
|
|
|
|
+ urlInnerStack.orientation = .horizontal
|
|
|
|
|
+ urlInnerStack.alignment = .centerY
|
|
|
|
|
+ urlInnerStack.spacing = 8
|
|
|
|
|
+ urlInnerStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ urlWrapperView.addSubview(urlInnerStack)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ lockImageView.widthAnchor.constraint(equalToConstant: 14),
|
|
|
|
|
+ lockImageView.heightAnchor.constraint(equalToConstant: 14),
|
|
|
|
|
+
|
|
|
|
|
+ urlInnerStack.leadingAnchor.constraint(equalTo: urlWrapperView.leadingAnchor, constant: 12),
|
|
|
|
|
+ urlInnerStack.trailingAnchor.constraint(equalTo: urlWrapperView.trailingAnchor, constant: -12),
|
|
|
|
|
+ urlInnerStack.topAnchor.constraint(equalTo: urlWrapperView.topAnchor, constant: 6),
|
|
|
|
|
+ urlInnerStack.bottomAnchor.constraint(equalTo: urlWrapperView.bottomAnchor, constant: -6),
|
|
|
|
|
+ urlWrapperView.heightAnchor.constraint(equalToConstant: 30),
|
|
|
|
|
+ ])
|
|
|
|
|
|
|
|
// Open in external browser
|
|
// Open in external browser
|
|
|
openInBrowserButton = makeIconButton(
|
|
openInBrowserButton = makeIconButton(
|
|
@@ -209,18 +246,19 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
action: #selector(toggleGestureMode)
|
|
action: #selector(toggleGestureMode)
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- // Download (save as PDF)
|
|
|
|
|
|
|
+ // Downloads (open Downloads folder)
|
|
|
downloadButton = makeIconButton(
|
|
downloadButton = makeIconButton(
|
|
|
symbolName: "arrow.down.circle",
|
|
symbolName: "arrow.down.circle",
|
|
|
- accessibility: "Download PDF",
|
|
|
|
|
- action: #selector(downloadPDF)
|
|
|
|
|
|
|
+ accessibility: "Downloads",
|
|
|
|
|
+ action: #selector(openDownloads)
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
// More menu
|
|
// More menu
|
|
|
moreButton = makeIconButton(
|
|
moreButton = makeIconButton(
|
|
|
symbolName: "ellipsis.vertical",
|
|
symbolName: "ellipsis.vertical",
|
|
|
accessibility: "More",
|
|
accessibility: "More",
|
|
|
- action: #selector(showMoreMenu)
|
|
|
|
|
|
|
+ action: #selector(showMoreMenu),
|
|
|
|
|
+ fallbackSymbolName: "ellipsis"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
toolbarView.addArrangedSubview(backButton)
|
|
toolbarView.addArrangedSubview(backButton)
|
|
@@ -239,14 +277,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
urlWrapperView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
urlWrapperView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
urlWrapperView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
urlWrapperView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
|
|
|
|
|
|
- urlWrapperView.addSubview(urlField)
|
|
|
|
|
- NSLayoutConstraint.activate([
|
|
|
|
|
- urlField.leadingAnchor.constraint(equalTo: urlWrapperView.leadingAnchor, constant: 12),
|
|
|
|
|
- urlField.trailingAnchor.constraint(equalTo: urlWrapperView.trailingAnchor, constant: -12),
|
|
|
|
|
- urlField.topAnchor.constraint(equalTo: urlWrapperView.topAnchor, constant: 6),
|
|
|
|
|
- urlField.bottomAnchor.constraint(equalTo: urlWrapperView.bottomAnchor, constant: -6),
|
|
|
|
|
- urlWrapperView.heightAnchor.constraint(equalToConstant: 28),
|
|
|
|
|
- ])
|
|
|
|
|
|
|
+ // (URL layout constraints are defined via urlInnerStack above)
|
|
|
|
|
|
|
|
// Web view below toolbar.
|
|
// Web view below toolbar.
|
|
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
@@ -302,6 +333,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
// Initialize toolbar state.
|
|
// Initialize toolbar state.
|
|
|
setToggledStyle(pinButton, isOn: isPinnedOnTop)
|
|
setToggledStyle(pinButton, isOn: isPinnedOnTop)
|
|
|
setToggledStyle(handButton, isOn: isGestureModeEnabled)
|
|
setToggledStyle(handButton, isOn: isGestureModeEnabled)
|
|
|
|
|
+ urlField.stringValue = initialURL.absoluteString
|
|
|
updateToolbarState()
|
|
updateToolbarState()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -372,39 +404,12 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- @objc private func downloadPDF() {
|
|
|
|
|
- guard let url = webView.url else { return }
|
|
|
|
|
-
|
|
|
|
|
- let host = (url.host?.isEmpty == false) ? (url.host ?? "page") : "page"
|
|
|
|
|
- let dateFormatter = DateFormatter()
|
|
|
|
|
- dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
|
|
|
|
|
- let fileName = "\(host)_\(dateFormatter.string(from: Date())).pdf"
|
|
|
|
|
-
|
|
|
|
|
|
|
+ @objc private func openDownloads() {
|
|
|
guard let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
|
|
guard let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
|
|
|
- showAlert(title: "Download Failed", message: "Couldn’t find your Downloads folder.")
|
|
|
|
|
|
|
+ showAlert(title: "Unavailable", message: "Couldn’t find your Downloads folder.")
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- let targetURL = downloads.appendingPathComponent(fileName)
|
|
|
|
|
-
|
|
|
|
|
- if #available(macOS 11.0, *) {
|
|
|
|
|
- webView.createPDF(configuration: WKPDFConfiguration()) { [weak self] result in
|
|
|
|
|
- guard let self else { return }
|
|
|
|
|
- switch result {
|
|
|
|
|
- case .success(let data):
|
|
|
|
|
- do {
|
|
|
|
|
- try data.write(to: targetURL, options: .atomic)
|
|
|
|
|
- self.showAlert(title: "Downloaded", message: "Saved PDF to Downloads:\n\(fileName)")
|
|
|
|
|
- } catch {
|
|
|
|
|
- self.showAlert(title: "Download Failed", message: "Couldn’t save the PDF.")
|
|
|
|
|
- }
|
|
|
|
|
- case .failure:
|
|
|
|
|
- self.showAlert(title: "Download Failed", message: "Couldn’t generate a PDF for this page.")
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- showAlert(title: "Unavailable", message: "PDF download requires macOS 11 or newer.")
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ NSWorkspace.shared.open(downloads)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@objc private func showMoreMenu() {
|
|
@objc private func showMoreMenu() {
|
|
@@ -424,6 +429,10 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
home.keyEquivalentModifierMask = [.command]
|
|
home.keyEquivalentModifierMask = [.command]
|
|
|
menu.addItem(home)
|
|
menu.addItem(home)
|
|
|
|
|
|
|
|
|
|
+ let downloads = NSMenuItem(title: "Open Downloads", action: #selector(openDownloads), keyEquivalent: "d")
|
|
|
|
|
+ downloads.keyEquivalentModifierMask = [.command]
|
|
|
|
|
+ menu.addItem(downloads)
|
|
|
|
|
+
|
|
|
let openExternal = NSMenuItem(title: "Open in Default Browser", action: #selector(openInBrowser), keyEquivalent: "o")
|
|
let openExternal = NSMenuItem(title: "Open in Default Browser", action: #selector(openInBrowser), keyEquivalent: "o")
|
|
|
openExternal.keyEquivalentModifierMask = [.command]
|
|
openExternal.keyEquivalentModifierMask = [.command]
|
|
|
menu.addItem(openExternal)
|
|
menu.addItem(openExternal)
|
|
@@ -451,6 +460,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@objc private func urlFieldDidSubmit() {
|
|
@objc private func urlFieldDidSubmit() {
|
|
|
|
|
+ didSubmitURLFromAction = true
|
|
|
loadFromURLField()
|
|
loadFromURLField()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -470,10 +480,12 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Avoid the URL field update triggering other edits while we load.
|
|
|
|
|
|
|
+ // Keep normalized URL visible immediately after submit.
|
|
|
isUpdatingURLField = true
|
|
isUpdatingURLField = true
|
|
|
- webView.load(URLRequest(url: url))
|
|
|
|
|
|
|
+ urlField.stringValue = normalized
|
|
|
isUpdatingURLField = false
|
|
isUpdatingURLField = false
|
|
|
|
|
+
|
|
|
|
|
+ webView.load(URLRequest(url: url))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func revertURLFieldToCurrentWebURL() {
|
|
private func revertURLFieldToCurrentWebURL() {
|
|
@@ -489,12 +501,24 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
|
|
|
|
|
private func setURLFieldToCurrentWebURL() {
|
|
private func setURLFieldToCurrentWebURL() {
|
|
|
guard let url = webView.url?.absoluteString else { return }
|
|
guard let url = webView.url?.absoluteString else { return }
|
|
|
- guard !isURLFieldBeingEdited else { return }
|
|
|
|
|
guard !isUpdatingURLField else { return }
|
|
guard !isUpdatingURLField else { return }
|
|
|
|
|
|
|
|
|
|
+ // Avoid overriding manual typing unless this update follows a submit.
|
|
|
|
|
+ // However, right after navigation starts the text field can still be
|
|
|
|
|
+ // considered "editing" (first responder), which would otherwise keep
|
|
|
|
|
+ // the URL pill blank/stale. If it's empty, we should populate it.
|
|
|
|
|
+ if isURLFieldBeingEdited && !didSubmitURLFromAction {
|
|
|
|
|
+ if urlField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
|
|
|
+ // proceed
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
isUpdatingURLField = true
|
|
isUpdatingURLField = true
|
|
|
urlField.stringValue = url
|
|
urlField.stringValue = url
|
|
|
isUpdatingURLField = false
|
|
isUpdatingURLField = false
|
|
|
|
|
+ didSubmitURLFromAction = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func updateToolbarState() {
|
|
private func updateToolbarState() {
|
|
@@ -541,15 +565,25 @@ extension InAppBrowserWindowController: WKNavigationDelegate {
|
|
|
updateToolbarState()
|
|
updateToolbarState()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
|
|
|
|
+ updateToolbarState()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
|
|
|
|
+ updateToolbarState()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
func webView(
|
|
func webView(
|
|
|
_ webView: WKWebView,
|
|
_ webView: WKWebView,
|
|
|
didFailProvisionalNavigation navigation: WKNavigation!,
|
|
didFailProvisionalNavigation navigation: WKNavigation!,
|
|
|
withError error: Error
|
|
withError error: Error
|
|
|
) {
|
|
) {
|
|
|
|
|
+ updateToolbarState()
|
|
|
showLoadError(error)
|
|
showLoadError(error)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
|
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
|
|
|
|
+ updateToolbarState()
|
|
|
showLoadError(error)
|
|
showLoadError(error)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -572,6 +606,12 @@ extension InAppBrowserWindowController: NSTextFieldDelegate {
|
|
|
func controlTextDidEndEditing(_ obj: Notification) {
|
|
func controlTextDidEndEditing(_ obj: Notification) {
|
|
|
guard obj.object as? NSTextField === urlField else { return }
|
|
guard obj.object as? NSTextField === urlField else { return }
|
|
|
guard !isUpdatingURLField else { return }
|
|
guard !isUpdatingURLField else { return }
|
|
|
|
|
+ // Return key triggers both action and end-editing callbacks.
|
|
|
|
|
+ // Prevent duplicate loads so history/back-forward stays stable.
|
|
|
|
|
+ if didSubmitURLFromAction {
|
|
|
|
|
+ didSubmitURLFromAction = false
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
loadFromURLField()
|
|
loadFromURLField()
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|