|
@@ -27,7 +27,7 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
private let initialURL: URL
|
|
private let initialURL: URL
|
|
|
private var didLoadInitialURL = false
|
|
private var didLoadInitialURL = false
|
|
|
|
|
|
|
|
- // Toolbar UI (top navigation bar)
|
|
|
|
|
|
|
+ // Titlebar controls
|
|
|
private var backButton: NSButton!
|
|
private var backButton: NSButton!
|
|
|
private var forwardButton: NSButton!
|
|
private var forwardButton: NSButton!
|
|
|
private var reloadButton: NSButton!
|
|
private var reloadButton: NSButton!
|
|
@@ -38,19 +38,19 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
private var handButton: NSButton!
|
|
private var handButton: NSButton!
|
|
|
private var downloadButton: NSButton!
|
|
private var downloadButton: NSButton!
|
|
|
private var moreButton: NSButton!
|
|
private var moreButton: NSButton!
|
|
|
- private var urlField: NSTextField!
|
|
|
|
|
- private var isUpdatingURLField = false
|
|
|
|
|
- private var didSubmitURLFromAction = false
|
|
|
|
|
|
|
+ private var siteNameLabel: NSTextField!
|
|
|
|
|
+ private var siteSubtitleLabel: NSTextField!
|
|
|
|
|
+ private var siteIconView: NSImageView!
|
|
|
private var isPinnedOnTop = false
|
|
private var isPinnedOnTop = false
|
|
|
private var isGestureModeEnabled = true
|
|
private var isGestureModeEnabled = true
|
|
|
|
|
|
|
|
private func setToggledStyle(_ button: NSButton, isOn: Bool) {
|
|
private func setToggledStyle(_ button: NSButton, isOn: Bool) {
|
|
|
- button.layer?.backgroundColor = isOn
|
|
|
|
|
- ? NSColor.systemBlue.withAlphaComponent(0.28).cgColor
|
|
|
|
|
- : NSColor.white.withAlphaComponent(0.06).cgColor
|
|
|
|
|
- button.layer?.borderColor = isOn
|
|
|
|
|
- ? NSColor.systemBlue.withAlphaComponent(0.55).cgColor
|
|
|
|
|
- : NSColor.white.withAlphaComponent(0.10).cgColor
|
|
|
|
|
|
|
+ // Native-ish “selected” look using tint + filled variant when available.
|
|
|
|
|
+ if isOn {
|
|
|
|
|
+ button.contentTintColor = .systemBlue
|
|
|
|
|
+ } else {
|
|
|
|
|
+ button.contentTintColor = nil
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
init(initialURL: URL, windowTitle: String?) {
|
|
init(initialURL: URL, windowTitle: String?) {
|
|
@@ -68,76 +68,39 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
)
|
|
)
|
|
|
window.title = windowTitle ?? "Browser"
|
|
window.title = windowTitle ?? "Browser"
|
|
|
window.isReleasedWhenClosed = false
|
|
window.isReleasedWhenClosed = false
|
|
|
|
|
+ window.titleVisibility = .hidden
|
|
|
|
|
+ window.titlebarAppearsTransparent = true
|
|
|
|
|
+ window.styleMask.insert(.fullSizeContentView)
|
|
|
|
|
|
|
|
- // Build UI: top toolbar + web view underneath.
|
|
|
|
|
|
|
+ // Build UI: web view fills content; controls live in titlebar.
|
|
|
let containerView = NSView(frame: .zero)
|
|
let containerView = NSView(frame: .zero)
|
|
|
containerView.wantsLayer = true
|
|
containerView.wantsLayer = true
|
|
|
containerView.layer?.backgroundColor = NSColor.clear.cgColor
|
|
containerView.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
window.contentView = containerView
|
|
window.contentView = containerView
|
|
|
|
|
|
|
|
- let toolbarHeight: CGFloat = 46
|
|
|
|
|
- let toolbarContainer = NSView()
|
|
|
|
|
- toolbarContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
- toolbarContainer.wantsLayer = true
|
|
|
|
|
- toolbarContainer.layer?.cornerRadius = 14
|
|
|
|
|
- toolbarContainer.layer?.masksToBounds = true
|
|
|
|
|
- containerView.addSubview(toolbarContainer)
|
|
|
|
|
-
|
|
|
|
|
- // Blurred background like a modern browser toolbar.
|
|
|
|
|
- let toolbarBlur = NSVisualEffectView()
|
|
|
|
|
- toolbarBlur.material = .hudWindow
|
|
|
|
|
- toolbarBlur.blendingMode = .behindWindow
|
|
|
|
|
- toolbarBlur.state = .active
|
|
|
|
|
- toolbarBlur.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
- toolbarContainer.addSubview(toolbarBlur)
|
|
|
|
|
-
|
|
|
|
|
- let toolbarView = NSStackView()
|
|
|
|
|
- toolbarView.orientation = .horizontal
|
|
|
|
|
- toolbarView.alignment = .centerY
|
|
|
|
|
- toolbarView.spacing = 10
|
|
|
|
|
- toolbarView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
- toolbarContainer.addSubview(toolbarView)
|
|
|
|
|
-
|
|
|
|
|
- toolbarBlur.wantsLayer = true
|
|
|
|
|
- toolbarBlur.layer?.cornerRadius = 14
|
|
|
|
|
- toolbarBlur.layer?.masksToBounds = true
|
|
|
|
|
-
|
|
|
|
|
func makeIconButton(symbolName: String, accessibility: String, action: Selector, fallbackSymbolName: String? = nil) -> NSButton {
|
|
func makeIconButton(symbolName: String, accessibility: String, action: Selector, fallbackSymbolName: String? = nil) -> NSButton {
|
|
|
let button = NSButton()
|
|
let button = NSButton()
|
|
|
let primary = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibility)
|
|
let primary = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibility)
|
|
|
let fallback = fallbackSymbolName.flatMap { NSImage(systemSymbolName: $0, accessibilityDescription: accessibility) }
|
|
let fallback = fallbackSymbolName.flatMap { NSImage(systemSymbolName: $0, accessibilityDescription: accessibility) }
|
|
|
button.image = primary ?? fallback
|
|
button.image = primary ?? fallback
|
|
|
button.image?.isTemplate = true
|
|
button.image?.isTemplate = true
|
|
|
- button.isBordered = false
|
|
|
|
|
- button.contentTintColor = .white
|
|
|
|
|
button.imagePosition = .imageOnly
|
|
button.imagePosition = .imageOnly
|
|
|
button.imageScaling = .scaleProportionallyDown
|
|
button.imageScaling = .scaleProportionallyDown
|
|
|
- button.setButtonType(.momentaryChange)
|
|
|
|
|
|
|
+ button.isBordered = true
|
|
|
|
|
+ button.bezelStyle = .texturedRounded
|
|
|
|
|
+ button.controlSize = .small
|
|
|
|
|
+ button.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
|
|
|
|
|
+ button.setButtonType(.momentaryPushIn)
|
|
|
button.action = action
|
|
button.action = action
|
|
|
- button.setButtonType(.momentaryChange)
|
|
|
|
|
-
|
|
|
|
|
- // Subtle circular hit target like the screenshot.
|
|
|
|
|
- button.wantsLayer = true
|
|
|
|
|
- button.layer?.cornerRadius = 12
|
|
|
|
|
- button.layer?.masksToBounds = true
|
|
|
|
|
- button.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.06).cgColor
|
|
|
|
|
- button.layer?.borderColor = NSColor.white.withAlphaComponent(0.10).cgColor
|
|
|
|
|
- button.layer?.borderWidth = 1
|
|
|
|
|
-
|
|
|
|
|
- button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
- button.widthAnchor.constraint(equalToConstant: 26).isActive = true
|
|
|
|
|
- button.heightAnchor.constraint(equalToConstant: 26).isActive = true
|
|
|
|
|
-
|
|
|
|
|
|
|
+ button.toolTip = accessibility
|
|
|
if button.image == nil {
|
|
if button.image == nil {
|
|
|
- // Last-resort fallback if SF Symbols aren’t available for some reason.
|
|
|
|
|
button.title = "⋮"
|
|
button.title = "⋮"
|
|
|
- button.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
|
|
|
|
|
- button.contentTintColor = NSColor.white.withAlphaComponent(0.9)
|
|
|
|
|
} else {
|
|
} else {
|
|
|
button.title = ""
|
|
button.title = ""
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- button.toolTip = accessibility
|
|
|
|
|
|
|
+ // Consistent symbol sizing.
|
|
|
|
|
+ let cfg = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
|
|
|
|
|
+ button.image = button.image?.withSymbolConfiguration(cfg)
|
|
|
return button
|
|
return button
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -183,54 +146,37 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
action: #selector(showLauncher)
|
|
action: #selector(showLauncher)
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- // 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.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
- urlField.action = #selector(urlFieldDidSubmit)
|
|
|
|
|
- urlField.placeholderString = "Search or enter website name"
|
|
|
|
|
- urlField.font = NSFont.systemFont(ofSize: 13, weight: .medium)
|
|
|
|
|
- urlField.textColor = .white
|
|
|
|
|
- urlField.alignment = .left
|
|
|
|
|
- urlField.isEditable = true
|
|
|
|
|
- urlField.isBordered = false
|
|
|
|
|
- urlField.drawsBackground = false
|
|
|
|
|
- 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),
|
|
|
|
|
- ])
|
|
|
|
|
|
|
+ // Center capsule (icon + site + optional page title).
|
|
|
|
|
+ siteIconView = NSImageView()
|
|
|
|
|
+ siteIconView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ siteIconView.image = NSImage(systemSymbolName: "globe", accessibilityDescription: "Site")?
|
|
|
|
|
+ .withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold))
|
|
|
|
|
+ siteIconView.contentTintColor = NSColor.secondaryLabelColor
|
|
|
|
|
+ siteIconView.imageScaling = .scaleProportionallyDown
|
|
|
|
|
+
|
|
|
|
|
+ siteNameLabel = NSTextField(labelWithString: windowTitle ?? "Browser")
|
|
|
|
|
+ siteNameLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
|
|
|
|
|
+ siteNameLabel.textColor = NSColor.labelColor
|
|
|
|
|
+ siteNameLabel.alignment = .left
|
|
|
|
|
+ siteNameLabel.lineBreakMode = .byTruncatingTail
|
|
|
|
|
+
|
|
|
|
|
+ siteSubtitleLabel = NSTextField(labelWithString: "")
|
|
|
|
|
+ siteSubtitleLabel.font = NSFont.systemFont(ofSize: 10.5, weight: .medium)
|
|
|
|
|
+ siteSubtitleLabel.textColor = NSColor.secondaryLabelColor
|
|
|
|
|
+ siteSubtitleLabel.alignment = .left
|
|
|
|
|
+ siteSubtitleLabel.lineBreakMode = .byTruncatingTail
|
|
|
|
|
+
|
|
|
|
|
+ let textStack = NSStackView(views: [siteNameLabel, siteSubtitleLabel])
|
|
|
|
|
+ textStack.orientation = .vertical
|
|
|
|
|
+ textStack.alignment = .leading
|
|
|
|
|
+ textStack.spacing = 0
|
|
|
|
|
+ textStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let headerStack = NSStackView(views: [siteIconView, textStack])
|
|
|
|
|
+ headerStack.orientation = .horizontal
|
|
|
|
|
+ headerStack.alignment = .centerY
|
|
|
|
|
+ headerStack.spacing = 8
|
|
|
|
|
+ headerStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
|
|
// Open in external browser
|
|
// Open in external browser
|
|
|
openInBrowserButton = makeIconButton(
|
|
openInBrowserButton = makeIconButton(
|
|
@@ -261,56 +207,28 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
fallbackSymbolName: "ellipsis"
|
|
fallbackSymbolName: "ellipsis"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- toolbarView.addArrangedSubview(backButton)
|
|
|
|
|
- toolbarView.addArrangedSubview(forwardButton)
|
|
|
|
|
- toolbarView.addArrangedSubview(reloadButton)
|
|
|
|
|
- toolbarView.addArrangedSubview(urlWrapperView)
|
|
|
|
|
- toolbarView.addArrangedSubview(pinButton)
|
|
|
|
|
- toolbarView.addArrangedSubview(homeButton)
|
|
|
|
|
- toolbarView.addArrangedSubview(gridButton)
|
|
|
|
|
- toolbarView.addArrangedSubview(openInBrowserButton)
|
|
|
|
|
- toolbarView.addArrangedSubview(handButton)
|
|
|
|
|
- toolbarView.addArrangedSubview(downloadButton)
|
|
|
|
|
- toolbarView.addArrangedSubview(moreButton)
|
|
|
|
|
-
|
|
|
|
|
- // Give the URL field more space than the icon buttons.
|
|
|
|
|
- urlWrapperView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
|
|
- urlWrapperView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
|
|
|
|
-
|
|
|
|
|
- // (URL layout constraints are defined via urlInnerStack above)
|
|
|
|
|
-
|
|
|
|
|
- // Web view below toolbar.
|
|
|
|
|
|
|
+ // Web view fills content.
|
|
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
webView.setValue(false, forKey: "drawsBackground")
|
|
webView.setValue(false, forKey: "drawsBackground")
|
|
|
|
|
|
|
|
containerView.addSubview(webView)
|
|
containerView.addSubview(webView)
|
|
|
|
|
|
|
|
|
|
+ // Use the window content layout guide so content starts below titlebar.
|
|
|
|
|
+ let contentGuide = (window.contentLayoutGuide as? NSLayoutGuide) ?? NSLayoutGuide()
|
|
|
NSLayoutConstraint.activate([
|
|
NSLayoutConstraint.activate([
|
|
|
- toolbarContainer.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10),
|
|
|
|
|
- toolbarContainer.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -10),
|
|
|
|
|
- toolbarContainer.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10),
|
|
|
|
|
- toolbarContainer.heightAnchor.constraint(equalToConstant: toolbarHeight),
|
|
|
|
|
-
|
|
|
|
|
- toolbarBlur.leadingAnchor.constraint(equalTo: toolbarContainer.leadingAnchor),
|
|
|
|
|
- toolbarBlur.trailingAnchor.constraint(equalTo: toolbarContainer.trailingAnchor),
|
|
|
|
|
- toolbarBlur.topAnchor.constraint(equalTo: toolbarContainer.topAnchor),
|
|
|
|
|
- toolbarBlur.bottomAnchor.constraint(equalTo: toolbarContainer.bottomAnchor),
|
|
|
|
|
-
|
|
|
|
|
- toolbarView.leadingAnchor.constraint(equalTo: toolbarContainer.leadingAnchor, constant: 12),
|
|
|
|
|
- toolbarView.trailingAnchor.constraint(equalTo: toolbarContainer.trailingAnchor, constant: -12),
|
|
|
|
|
- toolbarView.topAnchor.constraint(equalTo: toolbarContainer.topAnchor, constant: 6),
|
|
|
|
|
- toolbarView.bottomAnchor.constraint(equalTo: toolbarContainer.bottomAnchor, constant: -6),
|
|
|
|
|
-
|
|
|
|
|
webView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
|
webView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
|
|
webView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
|
webView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
|
|
webView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
|
webView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
|
|
|
- webView.topAnchor.constraint(equalTo: toolbarContainer.bottomAnchor, constant: 0),
|
|
|
|
|
|
|
+ webView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
|
|
|
])
|
|
])
|
|
|
|
|
|
|
|
super.init(window: window)
|
|
super.init(window: window)
|
|
|
|
|
|
|
|
window.delegate = self
|
|
window.delegate = self
|
|
|
|
|
|
|
|
|
|
+ // Build titlebar layout after super.init.
|
|
|
|
|
+ configureTitlebarLayout(centerView: headerStack)
|
|
|
|
|
+
|
|
|
// Safe to reference `self` only after super.init.
|
|
// Safe to reference `self` only after super.init.
|
|
|
backButton.target = self
|
|
backButton.target = self
|
|
|
forwardButton.target = self
|
|
forwardButton.target = self
|
|
@@ -322,8 +240,6 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
handButton.target = self
|
|
handButton.target = self
|
|
|
downloadButton.target = self
|
|
downloadButton.target = self
|
|
|
moreButton.target = self
|
|
moreButton.target = self
|
|
|
- urlField.delegate = self
|
|
|
|
|
- urlField.target = self
|
|
|
|
|
|
|
|
|
|
webView.uiDelegate = self
|
|
webView.uiDelegate = self
|
|
|
webView.navigationDelegate = self
|
|
webView.navigationDelegate = self
|
|
@@ -333,10 +249,109 @@ 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
|
|
|
|
|
|
|
+ siteNameLabel.stringValue = windowTitle ?? (initialURL.host ?? "Browser")
|
|
|
updateToolbarState()
|
|
updateToolbarState()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private func configureTitlebarLayout(centerView: NSView) {
|
|
|
|
|
+ guard let window else { return }
|
|
|
|
|
+
|
|
|
|
|
+ let centerCapsule = NSVisualEffectView()
|
|
|
|
|
+ centerCapsule.material = .titlebar
|
|
|
|
|
+ centerCapsule.blendingMode = .withinWindow
|
|
|
|
|
+ centerCapsule.state = .active
|
|
|
|
|
+ centerCapsule.wantsLayer = true
|
|
|
|
|
+ centerCapsule.layer?.cornerRadius = 12
|
|
|
|
|
+ centerCapsule.layer?.masksToBounds = true
|
|
|
|
|
+ centerCapsule.layer?.borderWidth = 1
|
|
|
|
|
+ centerCapsule.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.6).cgColor
|
|
|
|
|
+ centerCapsule.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ centerCapsule.addSubview(centerView)
|
|
|
|
|
+ centerView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ centerView.leadingAnchor.constraint(equalTo: centerCapsule.leadingAnchor, constant: 12),
|
|
|
|
|
+ centerView.trailingAnchor.constraint(equalTo: centerCapsule.trailingAnchor, constant: -12),
|
|
|
|
|
+ centerView.topAnchor.constraint(equalTo: centerCapsule.topAnchor, constant: 5),
|
|
|
|
|
+ centerView.bottomAnchor.constraint(equalTo: centerCapsule.bottomAnchor, constant: -5),
|
|
|
|
|
+ centerCapsule.heightAnchor.constraint(equalToConstant: 30),
|
|
|
|
|
+ centerCapsule.widthAnchor.constraint(greaterThanOrEqualToConstant: 240),
|
|
|
|
|
+ centerCapsule.widthAnchor.constraint(lessThanOrEqualToConstant: 520),
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ let leftStack = NSStackView(views: [backButton, forwardButton, reloadButton, homeButton])
|
|
|
|
|
+ leftStack.orientation = .horizontal
|
|
|
|
|
+ leftStack.alignment = .centerY
|
|
|
|
|
+ leftStack.spacing = 6
|
|
|
|
|
+
|
|
|
|
|
+ let rightStack = NSStackView(views: [
|
|
|
|
|
+ pinButton,
|
|
|
|
|
+ gridButton,
|
|
|
|
|
+ openInBrowserButton,
|
|
|
|
|
+ handButton,
|
|
|
|
|
+ downloadButton,
|
|
|
|
|
+ moreButton,
|
|
|
|
|
+ ])
|
|
|
|
|
+ rightStack.orientation = .horizontal
|
|
|
|
|
+ rightStack.alignment = .centerY
|
|
|
|
|
+ rightStack.spacing = 6
|
|
|
|
|
+
|
|
|
|
|
+ func accessory(_ view: NSView, layout: NSLayoutConstraint.Attribute) -> NSTitlebarAccessoryViewController {
|
|
|
|
|
+ let vc = NSTitlebarAccessoryViewController()
|
|
|
|
|
+ vc.layoutAttribute = layout
|
|
|
|
|
+ vc.view = view
|
|
|
|
|
+ return vc
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Single top accessory with left / center / right.
|
|
|
|
|
+ let topBar = NSView(frame: NSRect(x: 0, y: 0, width: 1000, height: 34))
|
|
|
|
|
+ topBar.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let leftContainer = NSView()
|
|
|
|
|
+ leftContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ leftContainer.addSubview(leftStack)
|
|
|
|
|
+ leftStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let centerContainer = NSView()
|
|
|
|
|
+ centerContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ centerContainer.addSubview(centerCapsule)
|
|
|
|
|
+
|
|
|
|
|
+ let rightContainer = NSView()
|
|
|
|
|
+ rightContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ rightContainer.addSubview(rightStack)
|
|
|
|
|
+ rightStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ let barStack = NSStackView(views: [leftContainer, centerContainer, rightContainer])
|
|
|
|
|
+ barStack.orientation = .horizontal
|
|
|
|
|
+ barStack.alignment = .centerY
|
|
|
|
|
+ barStack.distribution = .fill
|
|
|
|
|
+ barStack.spacing = 10
|
|
|
|
|
+ barStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ topBar.addSubview(barStack)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ topBar.heightAnchor.constraint(equalToConstant: 34),
|
|
|
|
|
+
|
|
|
|
|
+ barStack.leadingAnchor.constraint(equalTo: topBar.leadingAnchor, constant: 12),
|
|
|
|
|
+ barStack.trailingAnchor.constraint(equalTo: topBar.trailingAnchor, constant: -12),
|
|
|
|
|
+ barStack.topAnchor.constraint(equalTo: topBar.topAnchor, constant: 2),
|
|
|
|
|
+ barStack.bottomAnchor.constraint(equalTo: topBar.bottomAnchor, constant: -2),
|
|
|
|
|
+
|
|
|
|
|
+ leftContainer.widthAnchor.constraint(equalTo: rightContainer.widthAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ leftStack.leadingAnchor.constraint(equalTo: leftContainer.leadingAnchor),
|
|
|
|
|
+ leftStack.centerYAnchor.constraint(equalTo: leftContainer.centerYAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ rightStack.trailingAnchor.constraint(equalTo: rightContainer.trailingAnchor),
|
|
|
|
|
+ rightStack.centerYAnchor.constraint(equalTo: rightContainer.centerYAnchor),
|
|
|
|
|
+
|
|
|
|
|
+ centerCapsule.centerXAnchor.constraint(equalTo: centerContainer.centerXAnchor),
|
|
|
|
|
+ centerCapsule.centerYAnchor.constraint(equalTo: centerContainer.centerYAnchor),
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ window.addTitlebarAccessoryViewController(accessory(topBar, layout: .top))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@available(*, unavailable)
|
|
@available(*, unavailable)
|
|
|
required init?(coder: NSCoder) {
|
|
required init?(coder: NSCoder) {
|
|
|
nil
|
|
nil
|
|
@@ -459,66 +474,15 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
window?.performClose(nil)
|
|
window?.performClose(nil)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- @objc private func urlFieldDidSubmit() {
|
|
|
|
|
- didSubmitURLFromAction = true
|
|
|
|
|
- loadFromURLField()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func loadFromURLField() {
|
|
|
|
|
- let raw = urlField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
- guard !raw.isEmpty else { return }
|
|
|
|
|
-
|
|
|
|
|
- let normalized: String
|
|
|
|
|
- if raw.lowercased().hasPrefix("http://") || raw.lowercased().hasPrefix("https://") {
|
|
|
|
|
- normalized = raw
|
|
|
|
|
- } else {
|
|
|
|
|
- normalized = "https://\(raw)"
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- guard let url = URL(string: normalized) else {
|
|
|
|
|
- revertURLFieldToCurrentWebURL()
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Keep normalized URL visible immediately after submit.
|
|
|
|
|
- isUpdatingURLField = true
|
|
|
|
|
- urlField.stringValue = normalized
|
|
|
|
|
- isUpdatingURLField = false
|
|
|
|
|
|
|
+ private func updateSiteNameFromWebView() {
|
|
|
|
|
+ let host = webView.url?.host?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
+ let pageTitle = webView.title?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
|
+ let preferred = (host?.isEmpty == false) ? host! : ((pageTitle?.isEmpty == false) ? pageTitle! : "Browser")
|
|
|
|
|
|
|
|
- webView.load(URLRequest(url: url))
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func revertURLFieldToCurrentWebURL() {
|
|
|
|
|
- guard let url = webView.url?.absoluteString else { return }
|
|
|
|
|
- isUpdatingURLField = true
|
|
|
|
|
- urlField.stringValue = url
|
|
|
|
|
- isUpdatingURLField = false
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private var isURLFieldBeingEdited: Bool {
|
|
|
|
|
- urlField.currentEditor() != nil
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func setURLFieldToCurrentWebURL() {
|
|
|
|
|
- guard let url = webView.url?.absoluteString 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
|
|
|
|
|
- urlField.stringValue = url
|
|
|
|
|
- isUpdatingURLField = false
|
|
|
|
|
- didSubmitURLFromAction = false
|
|
|
|
|
|
|
+ siteNameLabel.stringValue = preferred
|
|
|
|
|
+ siteSubtitleLabel.stringValue = (pageTitle?.isEmpty == false && pageTitle != preferred) ? pageTitle! : ""
|
|
|
|
|
+ siteSubtitleLabel.isHidden = siteSubtitleLabel.stringValue.isEmpty
|
|
|
|
|
+ window?.title = preferred
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func updateToolbarState() {
|
|
private func updateToolbarState() {
|
|
@@ -528,8 +492,8 @@ final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
|
|
|
// Keep reload enabled; it matches typical browser behavior.
|
|
// Keep reload enabled; it matches typical browser behavior.
|
|
|
reloadButton.isEnabled = true
|
|
reloadButton.isEnabled = true
|
|
|
|
|
|
|
|
- // Sync URL field and window title.
|
|
|
|
|
- setURLFieldToCurrentWebURL()
|
|
|
|
|
|
|
+ // Sync center site name.
|
|
|
|
|
+ updateSiteNameFromWebView()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private func showAlert(title: String, message: String) {
|
|
private func showAlert(title: String, message: String) {
|
|
@@ -602,20 +566,6 @@ extension InAppBrowserWindowController: WKNavigationDelegate {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-extension InAppBrowserWindowController: NSTextFieldDelegate {
|
|
|
|
|
- func controlTextDidEndEditing(_ obj: Notification) {
|
|
|
|
|
- guard obj.object as? NSTextField === urlField 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()
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
extension InAppBrowserWindowController: WKUIDelegate {
|
|
extension InAppBrowserWindowController: WKUIDelegate {
|
|
|
func webView(
|
|
func webView(
|
|
|
_ webView: WKWebView,
|
|
_ webView: WKWebView,
|