mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-16 15:51:38 +00:00
666 lines
24 KiB
Swift
666 lines
24 KiB
Swift
import AppKit
|
|
import Foundation
|
|
import WebKit
|
|
|
|
@MainActor
|
|
private final class DashboardLinkBrowserTab {
|
|
/// WebKit preserves one WKNavigation identity across a redirect chain.
|
|
/// A different identity means the opened-link alias is no longer current.
|
|
private enum RequestAliasPhase {
|
|
case awaitingNavigation
|
|
case loading(AnyObject)
|
|
case retained
|
|
case retired
|
|
}
|
|
|
|
let id = UUID()
|
|
let webView: WKWebView
|
|
let requestedURL: URL
|
|
var representedURL: URL?
|
|
var title: String?
|
|
var observations: [NSKeyValueObservation] = []
|
|
private var requestAliasPhase: RequestAliasPhase = .awaitingNavigation
|
|
|
|
init(webView: WKWebView, requestedURL: URL) {
|
|
self.webView = webView
|
|
self.requestedURL = requestedURL
|
|
self.representedURL = requestedURL
|
|
}
|
|
|
|
var requestedURLAlias: URL? {
|
|
if case .retired = self.requestAliasPhase {
|
|
nil
|
|
} else {
|
|
self.requestedURL
|
|
}
|
|
}
|
|
|
|
func startNavigation(_ navigation: AnyObject) {
|
|
switch self.requestAliasPhase {
|
|
case .awaitingNavigation:
|
|
self.requestAliasPhase = .loading(navigation)
|
|
case let .loading(initial) where initial !== navigation:
|
|
self.requestAliasPhase = .retired
|
|
case .retained:
|
|
self.requestAliasPhase = .retired
|
|
case .loading, .retired:
|
|
break
|
|
}
|
|
}
|
|
|
|
func updateRepresentedURL(_ url: URL?) {
|
|
// Initial redirects keep the opened link reusable. Once that chain
|
|
// finishes, a distinct navigation retires the now-stale alias.
|
|
if case .retained = self.requestAliasPhase, let url, url != self.representedURL {
|
|
self.requestAliasPhase = .retired
|
|
}
|
|
self.representedURL = url
|
|
}
|
|
|
|
func finishNavigation(_ navigation: AnyObject?, at url: URL?, title: String?) {
|
|
self.updateRepresentedURL(url)
|
|
self.title = title
|
|
guard let navigation else { return }
|
|
switch self.requestAliasPhase {
|
|
case .awaitingNavigation:
|
|
self.requestAliasPhase = .retained
|
|
case let .loading(initial) where initial === navigation:
|
|
self.requestAliasPhase = .retained
|
|
case .loading:
|
|
self.requestAliasPhase = .retired
|
|
case .retained, .retired:
|
|
break
|
|
}
|
|
}
|
|
|
|
func failNavigation() {
|
|
// A failed initial chain has no reusable page. Retire its alias so
|
|
// opening the original link again starts a fresh load.
|
|
self.requestAliasPhase = .retired
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class DashboardLinkBrowserView: NSView {
|
|
var activeWebView: WKWebView? {
|
|
self.activeTab?.webView
|
|
}
|
|
|
|
weak var webViewNavigationDelegate: WKNavigationDelegate? {
|
|
didSet {
|
|
self.tabs.forEach { $0.webView.navigationDelegate = self.webViewNavigationDelegate }
|
|
}
|
|
}
|
|
|
|
weak var webViewUIDelegate: WKUIDelegate? {
|
|
didSet {
|
|
self.tabs.forEach { $0.webView.uiDelegate = self.webViewUIDelegate }
|
|
}
|
|
}
|
|
|
|
var onClose: (() -> Void)?
|
|
var onOpenExternal: ((URL) -> Void)?
|
|
|
|
private let websiteDataStore: WKWebsiteDataStore
|
|
private var tabs: [DashboardLinkBrowserTab] = []
|
|
private var activeTabID: UUID?
|
|
private let toolbar = NSVisualEffectView()
|
|
private let tabBar = DashboardLinkBrowserTabBar()
|
|
private let backButton = DashboardLinkBrowserView.makeButton(symbol: "chevron.left", label: "Back")
|
|
private let forwardButton = DashboardLinkBrowserView.makeButton(symbol: "chevron.right", label: "Forward")
|
|
private let reloadButton = DashboardLinkBrowserView.makeButton(symbol: "arrow.clockwise", label: "Reload")
|
|
private let externalButton = DashboardLinkBrowserView.makeButton(
|
|
symbol: "arrow.up.right.square",
|
|
label: "Open in Default Browser")
|
|
private let closeButton = DashboardLinkBrowserView.makeButton(symbol: "xmark", label: "Close Sidebar")
|
|
private let addressLabel: NSTextField = {
|
|
let label = NSTextField(labelWithString: "")
|
|
label.font = .systemFont(ofSize: 12, weight: .medium)
|
|
label.textColor = .secondaryLabelColor
|
|
label.lineBreakMode = .byTruncatingMiddle
|
|
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
return label
|
|
}()
|
|
|
|
private var activeTab: DashboardLinkBrowserTab? {
|
|
guard let activeTabID else { return nil }
|
|
return self.tabs.first { $0.id == activeTabID }
|
|
}
|
|
|
|
init(websiteDataStore: WKWebsiteDataStore) {
|
|
self.websiteDataStore = websiteDataStore
|
|
super.init(frame: .zero)
|
|
|
|
self.configureActions()
|
|
self.buildView()
|
|
self.updateChrome()
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder _: NSCoder) {
|
|
fatalError("init(coder:) is not supported")
|
|
}
|
|
|
|
func owns(_ webView: WKWebView) -> Bool {
|
|
self.tabs.contains { $0.webView === webView }
|
|
}
|
|
|
|
func open(_ url: URL) {
|
|
// Keep the original request as a stable dedupe key across redirects while
|
|
// also reusing a tab that has since navigated to the requested target.
|
|
let tab = self.tabs.first(where: { $0.representedURL == url }) ??
|
|
self.tabs.first(where: { $0.requestedURLAlias == url })
|
|
if let tab {
|
|
self.activateTab(id: tab.id)
|
|
return
|
|
}
|
|
self.openInNewTab(url)
|
|
}
|
|
|
|
func openInNewTab(_ url: URL) {
|
|
let tab = self.makeTab(requestedURL: url)
|
|
self.tabs.append(tab)
|
|
self.tabBar.appendTab(id: tab.id, title: self.displayTitle(for: tab), toolTip: url.absoluteString)
|
|
self.activateTab(id: tab.id)
|
|
let webView = tab.webView
|
|
// Let the installed tab become observable before navigation starts.
|
|
// An immediate close then cancels the pending load without retaining it.
|
|
Task { @MainActor [weak self, weak webView] in
|
|
guard let self, let webView, self.owns(webView) else { return }
|
|
webView.load(URLRequest(url: url))
|
|
}
|
|
}
|
|
|
|
func closeTab(id: UUID) {
|
|
guard let index = self.tabs.firstIndex(where: { $0.id == id }) else { return }
|
|
let wasActive = self.activeTabID == id
|
|
let tab = self.tabs.remove(at: index)
|
|
self.dispose(tab)
|
|
self.tabBar.removeTab(id: id)
|
|
|
|
guard !self.tabs.isEmpty else {
|
|
self.activeTabID = nil
|
|
self.tabBar.setActiveTab(id: nil)
|
|
self.updateChrome()
|
|
self.onClose?()
|
|
return
|
|
}
|
|
if wasActive {
|
|
let nextIndex = min(index, self.tabs.count - 1)
|
|
self.activateTab(id: self.tabs[nextIndex].id)
|
|
}
|
|
}
|
|
|
|
func closeOtherTabs(keeping id: UUID) {
|
|
guard let keptTab = self.tabs.first(where: { $0.id == id }) else { return }
|
|
let removedTabs = self.tabs.filter { $0.id != id }
|
|
for tab in removedTabs {
|
|
self.dispose(tab)
|
|
self.tabBar.removeTab(id: tab.id)
|
|
}
|
|
self.tabs = [keptTab]
|
|
self.activateTab(id: id)
|
|
}
|
|
|
|
func moveTab(fromIndex: Int, toIndex: Int) {
|
|
guard self.tabs.indices.contains(fromIndex), toIndex >= 0, toIndex < self.tabs.count else { return }
|
|
let tab = self.tabs.remove(at: fromIndex)
|
|
self.tabs.insert(tab, at: toIndex)
|
|
self.tabBar.moveTab(id: tab.id, toIndex: toIndex)
|
|
}
|
|
|
|
func closeBrowser() {
|
|
let tabs = self.tabs
|
|
self.tabs.removeAll()
|
|
self.activeTabID = nil
|
|
for tab in tabs {
|
|
self.dispose(tab)
|
|
self.tabBar.removeTab(id: tab.id)
|
|
}
|
|
self.tabBar.setActiveTab(id: nil)
|
|
self.updateChrome()
|
|
}
|
|
|
|
func updateChrome() {
|
|
let tab = self.activeTab
|
|
let url = tab?.representedURL
|
|
self.addressLabel.stringValue = url?.host(percentEncoded: false) ?? url?.absoluteString ?? ""
|
|
self.addressLabel.toolTip = url?.absoluteString
|
|
self.backButton.isEnabled = tab?.webView.canGoBack == true
|
|
self.forwardButton.isEnabled = tab?.webView.canGoForward == true
|
|
self.reloadButton.isEnabled = url != nil
|
|
self.externalButton.isEnabled = url.flatMap(Self.httpURL) != nil
|
|
}
|
|
|
|
func navigationWillStart(_ url: URL, in webView: WKWebView) {
|
|
guard let tab = self.tab(owning: webView) else { return }
|
|
tab.updateRepresentedURL(url)
|
|
self.refreshTab(tab)
|
|
}
|
|
|
|
func navigationDidStart(_ navigation: WKNavigation?, in webView: WKWebView) {
|
|
guard let navigation, let tab = self.tab(owning: webView) else { return }
|
|
tab.startNavigation(navigation)
|
|
}
|
|
|
|
func navigationDidFinish(_ navigation: WKNavigation?, for webView: WKWebView) {
|
|
self.finishNavigation(navigation, at: webView.url, title: webView.title, in: webView)
|
|
}
|
|
|
|
func navigationDidFail(for webView: WKWebView) {
|
|
guard let tab = self.tab(owning: webView) else { return }
|
|
tab.failNavigation()
|
|
self.updateChrome()
|
|
}
|
|
|
|
func navigationURLDidChange(for webView: WKWebView) {
|
|
guard let tab = self.tab(owning: webView) else { return }
|
|
tab.updateRepresentedURL(webView.url)
|
|
self.refreshTab(tab)
|
|
}
|
|
|
|
private func finishNavigation(
|
|
_ navigation: AnyObject?,
|
|
at url: URL?,
|
|
title: String?,
|
|
in webView: WKWebView)
|
|
{
|
|
guard let tab = self.tab(owning: webView) else { return }
|
|
tab.finishNavigation(navigation, at: url, title: title)
|
|
self.refreshTab(tab)
|
|
}
|
|
|
|
func contextMenu(forTabAt index: Int) -> NSMenu? {
|
|
guard self.tabs.indices.contains(index) else { return nil }
|
|
return self.contextMenu(for: self.tabs[index])
|
|
}
|
|
|
|
private static func makeWebView(websiteDataStore: WKWebsiteDataStore) -> WKWebView {
|
|
// Every tab shares persisted browser sessions, but never inherits the
|
|
// dashboard's auth scripts, user scripts, or privileged message handler.
|
|
let configuration = WKWebViewConfiguration()
|
|
configuration.websiteDataStore = websiteDataStore
|
|
configuration.preferences.isElementFullscreenEnabled = true
|
|
configuration.preferences.javaScriptCanOpenWindowsAutomatically = false
|
|
let webView = WKWebView(frame: .zero, configuration: configuration)
|
|
webView.setValue(true, forKey: "drawsBackground")
|
|
return webView
|
|
}
|
|
|
|
private func makeTab(requestedURL: URL) -> DashboardLinkBrowserTab {
|
|
let webView = Self.makeWebView(websiteDataStore: self.websiteDataStore)
|
|
webView.navigationDelegate = self.webViewNavigationDelegate
|
|
webView.uiDelegate = self.webViewUIDelegate
|
|
let tab = DashboardLinkBrowserTab(webView: webView, requestedURL: requestedURL)
|
|
self.installWebView(webView)
|
|
self.observeNavigationState(for: tab)
|
|
return tab
|
|
}
|
|
|
|
private func dispose(_ tab: DashboardLinkBrowserTab) {
|
|
tab.observations.forEach { $0.invalidate() }
|
|
tab.observations.removeAll()
|
|
tab.webView.navigationDelegate = nil
|
|
tab.webView.uiDelegate = nil
|
|
tab.webView.stopLoading()
|
|
tab.webView.removeFromSuperview()
|
|
}
|
|
|
|
private func tab(owning webView: WKWebView) -> DashboardLinkBrowserTab? {
|
|
self.tabs.first { $0.webView === webView }
|
|
}
|
|
|
|
private func activateTab(id: UUID) {
|
|
guard self.tabs.contains(where: { $0.id == id }) else { return }
|
|
self.activeTabID = id
|
|
for tab in self.tabs {
|
|
tab.webView.isHidden = tab.id != id
|
|
}
|
|
self.tabBar.setActiveTab(id: id)
|
|
self.updateChrome()
|
|
if self.window != nil {
|
|
self.window?.makeFirstResponder(self.activeWebView)
|
|
}
|
|
}
|
|
|
|
private func refreshTab(_ tab: DashboardLinkBrowserTab) {
|
|
self.tabBar.updateTab(
|
|
id: tab.id,
|
|
title: self.displayTitle(for: tab),
|
|
toolTip: tab.representedURL?.absoluteString)
|
|
if tab.id == self.activeTabID {
|
|
self.updateChrome()
|
|
}
|
|
}
|
|
|
|
private func displayTitle(for tab: DashboardLinkBrowserTab) -> String {
|
|
if let title = tab.title?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty {
|
|
return title
|
|
}
|
|
return tab.representedURL?.host(percentEncoded: false) ?? "New Tab"
|
|
}
|
|
|
|
private func configureActions() {
|
|
self.backButton.target = self
|
|
self.backButton.action = #selector(self.goBack)
|
|
self.forwardButton.target = self
|
|
self.forwardButton.action = #selector(self.goForward)
|
|
self.reloadButton.target = self
|
|
self.reloadButton.action = #selector(self.reload)
|
|
self.externalButton.target = self
|
|
self.externalButton.action = #selector(self.openExternal)
|
|
self.closeButton.target = self
|
|
self.closeButton.action = #selector(self.close)
|
|
self.tabBar.delegate = self
|
|
}
|
|
|
|
private func observeNavigationState(for tab: DashboardLinkBrowserTab) {
|
|
let webView = tab.webView
|
|
// WebKit updates these properties after some navigation delegate callbacks.
|
|
// KVO also catches same-document SPA URL changes that skip didFinish.
|
|
tab.observations = [
|
|
webView.observe(\.canGoBack, options: [.new]) { [weak self, weak tab] _, _ in
|
|
Task { @MainActor in
|
|
guard let self, let tab else { return }
|
|
self.refreshTab(tab)
|
|
}
|
|
},
|
|
webView.observe(\.canGoForward, options: [.new]) { [weak self, weak tab] _, _ in
|
|
Task { @MainActor in
|
|
guard let self, let tab else { return }
|
|
self.refreshTab(tab)
|
|
}
|
|
},
|
|
webView.observe(\.url, options: [.new]) { [weak self, weak webView] _, _ in
|
|
Task { @MainActor in
|
|
guard let self, let webView else { return }
|
|
self.navigationURLDidChange(for: webView)
|
|
}
|
|
},
|
|
webView.observe(\.title, options: [.new]) { [weak self, weak tab, weak webView] _, _ in
|
|
Task { @MainActor in
|
|
guard let self, let tab, let webView else { return }
|
|
tab.title = webView.title
|
|
self.refreshTab(tab)
|
|
}
|
|
},
|
|
]
|
|
}
|
|
|
|
private func buildView() {
|
|
self.toolbar.material = .headerView
|
|
self.toolbar.blendingMode = .withinWindow
|
|
self.toolbar.state = .active
|
|
self.toolbar.translatesAutoresizingMaskIntoConstraints = false
|
|
addSubview(self.toolbar)
|
|
|
|
let controls = NSStackView(views: [
|
|
backButton,
|
|
forwardButton,
|
|
reloadButton,
|
|
addressLabel,
|
|
externalButton,
|
|
closeButton,
|
|
])
|
|
controls.orientation = .horizontal
|
|
controls.alignment = .centerY
|
|
controls.distribution = .fill
|
|
controls.spacing = 4
|
|
controls.setCustomSpacing(10, after: self.reloadButton)
|
|
controls.setCustomSpacing(10, after: self.addressLabel)
|
|
controls.translatesAutoresizingMaskIntoConstraints = false
|
|
self.toolbar.addSubview(controls)
|
|
|
|
self.tabBar.translatesAutoresizingMaskIntoConstraints = false
|
|
self.toolbar.addSubview(self.tabBar)
|
|
|
|
let separator = NSBox()
|
|
separator.boxType = .separator
|
|
separator.translatesAutoresizingMaskIntoConstraints = false
|
|
self.toolbar.addSubview(separator)
|
|
|
|
NSLayoutConstraint.activate([
|
|
self.toolbar.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
self.toolbar.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
self.toolbar.topAnchor.constraint(equalTo: topAnchor),
|
|
self.toolbar.heightAnchor.constraint(equalToConstant: 78),
|
|
// Browser-style header: tabs on top, navigation controls below.
|
|
// 12pt top inset clears the window's 12pt drag overlay (it reaches
|
|
// into wide sidebars), keeping every tab pixel clickable.
|
|
self.tabBar.leadingAnchor.constraint(equalTo: self.toolbar.leadingAnchor),
|
|
self.tabBar.trailingAnchor.constraint(equalTo: self.toolbar.trailingAnchor),
|
|
self.tabBar.topAnchor.constraint(equalTo: self.toolbar.topAnchor, constant: 12),
|
|
self.tabBar.heightAnchor.constraint(equalToConstant: DashboardWindowLayout.linkBrowserTabBarHeight),
|
|
controls.leadingAnchor.constraint(equalTo: self.toolbar.leadingAnchor, constant: 10),
|
|
controls.trailingAnchor.constraint(equalTo: self.toolbar.trailingAnchor, constant: -10),
|
|
controls.bottomAnchor.constraint(equalTo: self.toolbar.bottomAnchor, constant: -4),
|
|
controls.heightAnchor.constraint(equalToConstant: 28),
|
|
separator.leadingAnchor.constraint(equalTo: self.toolbar.leadingAnchor),
|
|
separator.trailingAnchor.constraint(equalTo: self.toolbar.trailingAnchor),
|
|
separator.bottomAnchor.constraint(equalTo: self.toolbar.bottomAnchor),
|
|
])
|
|
}
|
|
|
|
private func installWebView(_ webView: WKWebView) {
|
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
webView.isHidden = true
|
|
addSubview(webView)
|
|
NSLayoutConstraint.activate([
|
|
webView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
webView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
webView.topAnchor.constraint(equalTo: self.toolbar.bottomAnchor),
|
|
webView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
])
|
|
}
|
|
|
|
private func contextMenu(for tab: DashboardLinkBrowserTab) -> NSMenu {
|
|
let menu = NSMenu()
|
|
menu.autoenablesItems = false
|
|
let externalURL = tab.representedURL.flatMap(Self.httpURL)
|
|
menu.addItem(self.menuItem(
|
|
title: "Open in Default Browser",
|
|
action: #selector(self.openTabExternal(_:)),
|
|
tabID: tab.id,
|
|
isEnabled: externalURL != nil))
|
|
menu.addItem(self.menuItem(
|
|
title: "Copy Link",
|
|
action: #selector(self.copyTabLink(_:)),
|
|
tabID: tab.id,
|
|
isEnabled: tab.representedURL != nil))
|
|
menu.addItem(self.menuItem(
|
|
title: "Reload",
|
|
action: #selector(self.reloadTab(_:)),
|
|
tabID: tab.id,
|
|
isEnabled: tab.representedURL != nil))
|
|
menu.addItem(.separator())
|
|
menu.addItem(self.menuItem(
|
|
title: "Close Tab",
|
|
action: #selector(self.closeTabFromMenu(_:)),
|
|
tabID: tab.id,
|
|
isEnabled: true))
|
|
menu.addItem(self.menuItem(
|
|
title: "Close Other Tabs",
|
|
action: #selector(self.closeOtherTabsFromMenu(_:)),
|
|
tabID: tab.id,
|
|
isEnabled: self.tabs.count > 1))
|
|
return menu
|
|
}
|
|
|
|
private func menuItem(
|
|
title: String,
|
|
action: Selector,
|
|
tabID: UUID,
|
|
isEnabled: Bool) -> NSMenuItem
|
|
{
|
|
let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
|
|
item.target = self
|
|
item.representedObject = tabID as NSUUID
|
|
item.isEnabled = isEnabled
|
|
return item
|
|
}
|
|
|
|
private func tab(for menuItem: NSMenuItem) -> DashboardLinkBrowserTab? {
|
|
guard let id = menuItem.representedObject as? UUID else { return nil }
|
|
return self.tabs.first { $0.id == id }
|
|
}
|
|
|
|
private static func makeButton(symbol: String, label: String) -> NSButton {
|
|
let configuration = NSImage.SymbolConfiguration(pointSize: 13, weight: .medium)
|
|
let image = NSImage(systemSymbolName: symbol, accessibilityDescription: label)?
|
|
.withSymbolConfiguration(configuration) ?? NSImage(size: NSSize(width: 16, height: 16))
|
|
let button = NSButton(image: image, target: nil, action: nil)
|
|
button.isBordered = false
|
|
button.bezelStyle = .regularSquare
|
|
button.imageScaling = .scaleProportionallyDown
|
|
button.toolTip = label
|
|
button.setAccessibilityLabel(label)
|
|
button.widthAnchor.constraint(equalToConstant: 26).isActive = true
|
|
button.heightAnchor.constraint(equalToConstant: 26).isActive = true
|
|
return button
|
|
}
|
|
|
|
private static func httpURL(_ url: URL) -> URL? {
|
|
guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else {
|
|
return nil
|
|
}
|
|
return url
|
|
}
|
|
|
|
@objc private func goBack() {
|
|
self.activeWebView?.goBack()
|
|
}
|
|
|
|
@objc private func goForward() {
|
|
self.activeWebView?.goForward()
|
|
}
|
|
|
|
@objc private func reload() {
|
|
self.activeWebView?.reload()
|
|
}
|
|
|
|
@objc private func openExternal() {
|
|
guard let url = self.activeTab?.representedURL.flatMap(Self.httpURL) else { return }
|
|
self.onOpenExternal?(url)
|
|
}
|
|
|
|
@objc private func close() {
|
|
self.onClose?()
|
|
}
|
|
|
|
@objc private func openTabExternal(_ sender: NSMenuItem) {
|
|
guard let url = self.tab(for: sender)?.representedURL.flatMap(Self.httpURL) else { return }
|
|
self.onOpenExternal?(url)
|
|
}
|
|
|
|
@objc private func copyTabLink(_ sender: NSMenuItem) {
|
|
guard let url = self.tab(for: sender)?.representedURL else { return }
|
|
let pasteboard = NSPasteboard.general
|
|
pasteboard.clearContents()
|
|
pasteboard.writeObjects([url as NSURL])
|
|
pasteboard.setString(url.absoluteString, forType: .string)
|
|
}
|
|
|
|
@objc private func reloadTab(_ sender: NSMenuItem) {
|
|
self.tab(for: sender)?.webView.reload()
|
|
}
|
|
|
|
@objc private func closeTabFromMenu(_ sender: NSMenuItem) {
|
|
guard let id = (sender.representedObject as? UUID) else { return }
|
|
self.closeTab(id: id)
|
|
}
|
|
|
|
@objc private func closeOtherTabsFromMenu(_ sender: NSMenuItem) {
|
|
guard let id = (sender.representedObject as? UUID) else { return }
|
|
self.closeOtherTabs(keeping: id)
|
|
}
|
|
}
|
|
|
|
extension DashboardLinkBrowserView: DashboardLinkBrowserTabBarDelegate {
|
|
func tabBar(_: DashboardLinkBrowserTabBar, didSelectTab id: UUID) {
|
|
self.activateTab(id: id)
|
|
}
|
|
|
|
func tabBar(_: DashboardLinkBrowserTabBar, didCloseTab id: UUID) {
|
|
self.closeTab(id: id)
|
|
}
|
|
|
|
func tabBar(_: DashboardLinkBrowserTabBar, didMoveTab id: UUID, toIndex: Int) {
|
|
guard let fromIndex = self.tabs.firstIndex(where: { $0.id == id }) else { return }
|
|
self.moveTab(fromIndex: fromIndex, toIndex: toIndex)
|
|
}
|
|
|
|
func tabBar(_: DashboardLinkBrowserTabBar, contextMenuForTab id: UUID) -> NSMenu? {
|
|
guard let tab = self.tabs.first(where: { $0.id == id }) else { return nil }
|
|
return self.contextMenu(for: tab)
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
extension DashboardLinkBrowserView {
|
|
var _testTabCount: Int {
|
|
self.tabs.count
|
|
}
|
|
|
|
var _testTabURLs: [URL?] {
|
|
self.tabs.map(\.representedURL)
|
|
}
|
|
|
|
var _testActiveTabIndex: Int? {
|
|
guard let activeTabID else { return nil }
|
|
return self.tabs.firstIndex { $0.id == activeTabID }
|
|
}
|
|
|
|
var _testActiveWebView: WKWebView? {
|
|
self.activeWebView
|
|
}
|
|
|
|
var _testRepresentedURL: URL? {
|
|
self.activeTab?.representedURL
|
|
}
|
|
|
|
var _testNavigationObservationCount: Int {
|
|
self.activeTab?.observations.count ?? 0
|
|
}
|
|
|
|
var _testWebsiteDataStore: WKWebsiteDataStore {
|
|
self.websiteDataStore
|
|
}
|
|
|
|
var _testAllWebViews: [WKWebView] {
|
|
self.tabs.map(\.webView)
|
|
}
|
|
|
|
func _testSelectTab(at index: Int) {
|
|
guard self.tabs.indices.contains(index) else { return }
|
|
self.activateTab(id: self.tabs[index].id)
|
|
}
|
|
|
|
func _testCloseTab(at index: Int) {
|
|
guard self.tabs.indices.contains(index) else { return }
|
|
self.closeTab(id: self.tabs[index].id)
|
|
}
|
|
|
|
func _testMoveTab(from fromIndex: Int, to toIndex: Int) {
|
|
self.moveTab(fromIndex: fromIndex, toIndex: toIndex)
|
|
}
|
|
|
|
func _testOpenInNewTab(_ url: URL) {
|
|
self.openInNewTab(url)
|
|
}
|
|
|
|
func _testStartNavigation(_ navigation: AnyObject, in webView: WKWebView) {
|
|
guard let tab = self.tab(owning: webView) else { return }
|
|
tab.startNavigation(navigation)
|
|
}
|
|
|
|
func _testFinishNavigation(_ navigation: AnyObject, at url: URL?, in webView: WKWebView) {
|
|
self.finishNavigation(navigation, at: url, title: webView.title, in: webView)
|
|
}
|
|
|
|
func _testContextMenu(forTabAt index: Int) -> NSMenu? {
|
|
self.contextMenu(forTabAt: index)
|
|
}
|
|
}
|
|
#endif
|