InAppBrowserWindow.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. import AppKit
  2. import WebKit
  3. /// Titlebar icon control: white/neutral tint (no accent blue), subtle hover and selected backgrounds.
  4. private final class TitlebarIconButton: NSButton {
  5. private var isHovering = false
  6. private var hoverTrackingArea: NSTrackingArea?
  7. var isToggleActive = false
  8. override func awakeFromNib() {
  9. super.awakeFromNib()
  10. commonInit()
  11. }
  12. override init(frame frameRect: NSRect) {
  13. super.init(frame: frameRect)
  14. commonInit()
  15. }
  16. @available(*, unavailable)
  17. required init?(coder: NSCoder) {
  18. nil
  19. }
  20. private func commonInit() {
  21. wantsLayer = true
  22. layer?.cornerRadius = 5
  23. layer?.masksToBounds = true
  24. }
  25. override func updateTrackingAreas() {
  26. super.updateTrackingAreas()
  27. // Do NOT remove all tracking areas here: AppKit uses its own tracking area for tooltips.
  28. // We only manage our own hover tracking area.
  29. if let hoverTrackingArea {
  30. removeTrackingArea(hoverTrackingArea)
  31. }
  32. let area = NSTrackingArea(
  33. rect: bounds,
  34. options: [.activeInKeyWindow, .mouseEnteredAndExited, .inVisibleRect],
  35. owner: self,
  36. userInfo: nil
  37. )
  38. hoverTrackingArea = area
  39. addTrackingArea(area)
  40. }
  41. override func mouseEntered(with event: NSEvent) {
  42. isHovering = true
  43. refreshVisuals()
  44. }
  45. override func mouseExited(with event: NSEvent) {
  46. isHovering = false
  47. refreshVisuals()
  48. }
  49. override var isEnabled: Bool {
  50. didSet { refreshVisuals() }
  51. }
  52. func setToggleActive(_ on: Bool) {
  53. isToggleActive = on
  54. refreshVisuals()
  55. }
  56. func refreshVisuals() {
  57. let white = NSColor.white
  58. if !isEnabled {
  59. contentTintColor = white.withAlphaComponent(0.35)
  60. layer?.backgroundColor = NSColor.clear.cgColor
  61. return
  62. }
  63. if isToggleActive {
  64. contentTintColor = white
  65. let bg = isHovering ? white.withAlphaComponent(0.22) : white.withAlphaComponent(0.16)
  66. layer?.backgroundColor = bg.cgColor
  67. return
  68. }
  69. if isHovering {
  70. contentTintColor = white
  71. layer?.backgroundColor = white.withAlphaComponent(0.12).cgColor
  72. } else {
  73. contentTintColor = white.withAlphaComponent(0.88)
  74. layer?.backgroundColor = NSColor.clear.cgColor
  75. }
  76. }
  77. }
  78. final class InAppBrowserWindowManager {
  79. static let shared = InAppBrowserWindowManager()
  80. private var controllers: [InAppBrowserWindowController] = []
  81. private init() {}
  82. func open(url: URL, title: String? = nil) {
  83. let controller = InAppBrowserWindowController(initialURL: url, windowTitle: title)
  84. controllers.append(controller)
  85. controller.onClose = { [weak self, weak controller] in
  86. guard let self, let controller else { return }
  87. self.controllers.removeAll { $0 === controller }
  88. }
  89. controller.showWindow(nil)
  90. controller.window?.makeKeyAndOrderFront(nil)
  91. }
  92. }
  93. final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
  94. var onClose: (() -> Void)?
  95. private let webView: WKWebView
  96. private let initialURL: URL
  97. private var didLoadInitialURL = false
  98. // Titlebar controls
  99. private var backButton: TitlebarIconButton!
  100. private var forwardButton: TitlebarIconButton!
  101. private var reloadButton: TitlebarIconButton!
  102. private var pinButton: TitlebarIconButton!
  103. private var homeButton: TitlebarIconButton!
  104. private var widgetButton: TitlebarIconButton!
  105. private var handButton: TitlebarIconButton!
  106. private var downloadButton: TitlebarIconButton!
  107. private var moreButton: TitlebarIconButton!
  108. private var siteNameLabel: NSTextField!
  109. private var siteIconView: NSImageView!
  110. private var currentSiteIconURLKey: String?
  111. /// Keeps the center title in sync when `document.title` changes after load (e.g. SPAs).
  112. private var titleObservation: NSKeyValueObservation?
  113. /// Refreshes site identity when loading starts/stops.
  114. private var loadingObservation: NSKeyValueObservation?
  115. private var isPinnedOnTop = false
  116. private var isGestureModeEnabled = true
  117. private func setToggledStyle(_ button: TitlebarIconButton, isOn: Bool) {
  118. button.setToggleActive(isOn)
  119. }
  120. init(initialURL: URL, windowTitle: String?) {
  121. self.initialURL = initialURL
  122. let config = WKWebViewConfiguration()
  123. config.defaultWebpagePreferences.allowsContentJavaScript = true
  124. self.webView = WKWebView(frame: .zero, configuration: config)
  125. let window = NSWindow(
  126. contentRect: NSRect(x: 180, y: 140, width: 1200, height: 800),
  127. styleMask: [.titled, .closable, .miniaturizable, .resizable],
  128. backing: .buffered,
  129. defer: false
  130. )
  131. window.title = windowTitle ?? "Browser"
  132. window.isReleasedWhenClosed = false
  133. window.titleVisibility = .hidden
  134. window.titlebarAppearsTransparent = true
  135. window.styleMask.insert(.fullSizeContentView)
  136. // Build UI: web view fills content; controls live in titlebar.
  137. let containerView = NSView(frame: .zero)
  138. containerView.wantsLayer = true
  139. containerView.layer?.backgroundColor = NSColor.clear.cgColor
  140. window.contentView = containerView
  141. func makeIconButton(symbolName: String, accessibility: String, action: Selector, fallbackSymbolName: String? = nil) -> TitlebarIconButton {
  142. let button = TitlebarIconButton()
  143. let primary = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibility)
  144. let fallback = fallbackSymbolName.flatMap { NSImage(systemSymbolName: $0, accessibilityDescription: accessibility) }
  145. button.image = primary ?? fallback
  146. button.image?.isTemplate = true
  147. button.imagePosition = .imageOnly
  148. button.imageScaling = .scaleProportionallyDown
  149. button.isBordered = false
  150. button.bezelStyle = .regularSquare
  151. button.controlSize = .small
  152. button.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
  153. button.setButtonType(.momentaryPushIn)
  154. button.action = action
  155. button.toolTip = accessibility
  156. if button.image == nil {
  157. button.title = "⋮"
  158. } else {
  159. button.title = ""
  160. }
  161. // Consistent symbol sizing.
  162. let cfg = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
  163. button.image = button.image?.withSymbolConfiguration(cfg)
  164. button.refreshVisuals()
  165. return button
  166. }
  167. // Back
  168. backButton = makeIconButton(
  169. symbolName: "chevron.left",
  170. accessibility: "Back",
  171. action: #selector(goBack)
  172. )
  173. // Forward
  174. forwardButton = makeIconButton(
  175. symbolName: "chevron.right",
  176. accessibility: "Forward",
  177. action: #selector(goForward)
  178. )
  179. // Reload
  180. reloadButton = makeIconButton(
  181. symbolName: "arrow.clockwise",
  182. accessibility: "Reload",
  183. action: #selector(reloadPage)
  184. )
  185. // Pin (always on top)
  186. pinButton = makeIconButton(
  187. symbolName: "pin",
  188. accessibility: "Pin (Always on Top)",
  189. action: #selector(togglePinOnTop)
  190. )
  191. // Home
  192. homeButton = makeIconButton(
  193. symbolName: "house",
  194. accessibility: "Home",
  195. action: #selector(goHome)
  196. )
  197. // Widget (placeholder — hook up when widget feature ships)
  198. widgetButton = makeIconButton(
  199. symbolName: "widget.small",
  200. accessibility: "Widget",
  201. action: #selector(widgetTapped),
  202. fallbackSymbolName: "square.on.square"
  203. )
  204. // Center site identity (logo + app/site name).
  205. siteIconView = NSImageView()
  206. siteIconView.translatesAutoresizingMaskIntoConstraints = false
  207. siteIconView.image = NSImage(systemSymbolName: "globe", accessibilityDescription: "Site")?
  208. .withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold))
  209. siteIconView.contentTintColor = NSColor.secondaryLabelColor
  210. siteIconView.imageScaling = .scaleProportionallyDown
  211. siteIconView.wantsLayer = true
  212. siteIconView.layer?.cornerRadius = 3
  213. siteIconView.layer?.masksToBounds = true
  214. siteNameLabel = NSTextField(labelWithString: windowTitle ?? "Browser")
  215. siteNameLabel.font = NSFont.systemFont(ofSize: 13, weight: .semibold)
  216. siteNameLabel.textColor = NSColor.labelColor
  217. siteNameLabel.alignment = .left
  218. siteNameLabel.lineBreakMode = .byTruncatingTail
  219. let headerStack = NSStackView(views: [siteIconView, siteNameLabel])
  220. headerStack.orientation = .horizontal
  221. headerStack.alignment = .centerY
  222. headerStack.spacing = 8
  223. headerStack.translatesAutoresizingMaskIntoConstraints = false
  224. NSLayoutConstraint.activate([
  225. siteIconView.widthAnchor.constraint(equalToConstant: 14),
  226. siteIconView.heightAnchor.constraint(equalToConstant: 14),
  227. ])
  228. // Hand tool (toggle navigation gestures)
  229. handButton = makeIconButton(
  230. symbolName: "hand.raised",
  231. accessibility: "Gesture Mode",
  232. action: #selector(toggleGestureMode)
  233. )
  234. // Downloads (open Downloads folder)
  235. downloadButton = makeIconButton(
  236. symbolName: "arrow.down.circle",
  237. accessibility: "Downloads",
  238. action: #selector(openDownloads)
  239. )
  240. // More menu
  241. moreButton = makeIconButton(
  242. symbolName: "ellipsis.vertical",
  243. accessibility: "More",
  244. action: #selector(showMoreMenu),
  245. fallbackSymbolName: "ellipsis"
  246. )
  247. // Web view fills content.
  248. webView.translatesAutoresizingMaskIntoConstraints = false
  249. webView.setValue(false, forKey: "drawsBackground")
  250. containerView.addSubview(webView)
  251. // Use the window content layout guide so content starts below titlebar.
  252. let contentGuide = (window.contentLayoutGuide as? NSLayoutGuide) ?? NSLayoutGuide()
  253. NSLayoutConstraint.activate([
  254. webView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
  255. webView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
  256. webView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
  257. webView.topAnchor.constraint(equalTo: contentGuide.topAnchor),
  258. ])
  259. super.init(window: window)
  260. window.delegate = self
  261. // Build titlebar layout after super.init.
  262. configureTitlebarLayout(centerView: headerStack)
  263. // Safe to reference `self` only after super.init.
  264. backButton.target = self
  265. forwardButton.target = self
  266. reloadButton.target = self
  267. pinButton.target = self
  268. homeButton.target = self
  269. widgetButton.target = self
  270. handButton.target = self
  271. downloadButton.target = self
  272. moreButton.target = self
  273. webView.uiDelegate = self
  274. webView.navigationDelegate = self
  275. webView.allowsBackForwardNavigationGestures = true
  276. titleObservation = webView.observe(\.title, options: [.new]) { [weak self] _, _ in
  277. self?.updateSiteNameFromWebView()
  278. }
  279. loadingObservation = webView.observe(\.isLoading, options: [.new]) { [weak self] _, _ in
  280. self?.updateSiteNameFromWebView()
  281. }
  282. isGestureModeEnabled = true
  283. // Initialize toolbar state.
  284. setToggledStyle(pinButton, isOn: isPinnedOnTop)
  285. setToggledStyle(handButton, isOn: isGestureModeEnabled)
  286. siteNameLabel.stringValue = windowTitle ?? "Browser"
  287. updateToolbarState()
  288. }
  289. private func configureTitlebarLayout(centerView: NSView) {
  290. guard let window else { return }
  291. centerView.translatesAutoresizingMaskIntoConstraints = false
  292. let leftStack = NSStackView(views: [backButton, forwardButton, reloadButton, homeButton])
  293. leftStack.orientation = .horizontal
  294. leftStack.alignment = .centerY
  295. leftStack.spacing = 6
  296. let rightStack = NSStackView(views: [
  297. pinButton,
  298. widgetButton,
  299. handButton,
  300. downloadButton,
  301. moreButton,
  302. ])
  303. rightStack.orientation = .horizontal
  304. rightStack.alignment = .centerY
  305. rightStack.spacing = 6
  306. func accessory(_ view: NSView, layout: NSLayoutConstraint.Attribute) -> NSTitlebarAccessoryViewController {
  307. let vc = NSTitlebarAccessoryViewController()
  308. vc.layoutAttribute = layout
  309. vc.view = view
  310. return vc
  311. }
  312. // Single top accessory with left / center / right.
  313. let topBar = NSView(frame: NSRect(x: 0, y: 0, width: 1000, height: 34))
  314. topBar.translatesAutoresizingMaskIntoConstraints = false
  315. let leftContainer = NSView()
  316. leftContainer.translatesAutoresizingMaskIntoConstraints = false
  317. leftContainer.addSubview(leftStack)
  318. leftStack.translatesAutoresizingMaskIntoConstraints = false
  319. let centerContainer = NSView()
  320. centerContainer.translatesAutoresizingMaskIntoConstraints = false
  321. centerContainer.addSubview(centerView)
  322. let rightContainer = NSView()
  323. rightContainer.translatesAutoresizingMaskIntoConstraints = false
  324. rightContainer.addSubview(rightStack)
  325. rightStack.translatesAutoresizingMaskIntoConstraints = false
  326. let barStack = NSStackView(views: [leftContainer, centerContainer, rightContainer])
  327. barStack.orientation = .horizontal
  328. barStack.alignment = .centerY
  329. barStack.distribution = .fill
  330. barStack.spacing = 10
  331. barStack.translatesAutoresizingMaskIntoConstraints = false
  332. topBar.addSubview(barStack)
  333. NSLayoutConstraint.activate([
  334. topBar.heightAnchor.constraint(equalToConstant: 34),
  335. barStack.leadingAnchor.constraint(equalTo: topBar.leadingAnchor, constant: 12),
  336. barStack.trailingAnchor.constraint(equalTo: topBar.trailingAnchor, constant: -12),
  337. barStack.topAnchor.constraint(equalTo: topBar.topAnchor, constant: 2),
  338. barStack.bottomAnchor.constraint(equalTo: topBar.bottomAnchor, constant: -2),
  339. leftContainer.widthAnchor.constraint(equalTo: rightContainer.widthAnchor),
  340. leftStack.leadingAnchor.constraint(equalTo: leftContainer.leadingAnchor),
  341. leftStack.trailingAnchor.constraint(equalTo: leftContainer.trailingAnchor),
  342. leftStack.centerYAnchor.constraint(equalTo: leftContainer.centerYAnchor),
  343. rightStack.leadingAnchor.constraint(equalTo: rightContainer.leadingAnchor),
  344. rightStack.trailingAnchor.constraint(equalTo: rightContainer.trailingAnchor),
  345. rightStack.centerYAnchor.constraint(equalTo: rightContainer.centerYAnchor),
  346. centerView.centerXAnchor.constraint(equalTo: centerContainer.centerXAnchor),
  347. centerView.centerYAnchor.constraint(equalTo: centerContainer.centerYAnchor),
  348. centerView.leadingAnchor.constraint(greaterThanOrEqualTo: centerContainer.leadingAnchor, constant: 6),
  349. centerView.trailingAnchor.constraint(lessThanOrEqualTo: centerContainer.trailingAnchor, constant: -6),
  350. ])
  351. window.addTitlebarAccessoryViewController(accessory(topBar, layout: .top))
  352. }
  353. @available(*, unavailable)
  354. required init?(coder: NSCoder) {
  355. nil
  356. }
  357. override func showWindow(_ sender: Any?) {
  358. super.showWindow(sender)
  359. guard !didLoadInitialURL else { return }
  360. didLoadInitialURL = true
  361. webView.load(URLRequest(url: initialURL))
  362. }
  363. func windowWillClose(_ notification: Notification) {
  364. onClose?()
  365. }
  366. @objc private func goBack() {
  367. webView.goBack()
  368. }
  369. @objc private func goForward() {
  370. webView.goForward()
  371. }
  372. @objc private func reloadPage() {
  373. webView.reload()
  374. }
  375. @objc private func goHome() {
  376. webView.load(URLRequest(url: initialURL))
  377. }
  378. @objc private func widgetTapped() {
  379. let candidateTitle = siteNameLabel?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
  380. let matched = LauncherApp.sampleApps.first { $0.name.caseInsensitiveCompare(candidateTitle) == .orderedSame }
  381. NotificationCenter.default.post(
  382. name: .openWidgetsPage,
  383. object: nil,
  384. userInfo: matched.map { ["appID": $0.id] }
  385. )
  386. }
  387. @objc private func togglePinOnTop() {
  388. isPinnedOnTop.toggle()
  389. window?.level = isPinnedOnTop ? .floating : .normal
  390. if let button = pinButton {
  391. let symbol = isPinnedOnTop ? "pin.fill" : "pin"
  392. button.image = NSImage(systemSymbolName: symbol, accessibilityDescription: "Pin (Always on Top)")
  393. button.image?.isTemplate = true
  394. let cfg = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
  395. button.image = button.image?.withSymbolConfiguration(cfg)
  396. setToggledStyle(button, isOn: isPinnedOnTop)
  397. }
  398. }
  399. @objc private func toggleGestureMode() {
  400. isGestureModeEnabled.toggle()
  401. webView.allowsBackForwardNavigationGestures = isGestureModeEnabled
  402. if let button = handButton {
  403. let symbol = isGestureModeEnabled ? "hand.raised.fill" : "hand.raised"
  404. button.image = NSImage(systemSymbolName: symbol, accessibilityDescription: "Gesture Mode")
  405. button.image?.isTemplate = true
  406. let cfg = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
  407. button.image = button.image?.withSymbolConfiguration(cfg)
  408. setToggledStyle(button, isOn: isGestureModeEnabled)
  409. }
  410. }
  411. @objc private func openDownloads() {
  412. guard let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
  413. showAlert(title: "Unavailable", message: "Couldn’t find your Downloads folder.")
  414. return
  415. }
  416. NSWorkspace.shared.open(downloads)
  417. }
  418. @objc private func showMoreMenu() {
  419. let menu = NSMenu()
  420. let reload = NSMenuItem(title: "Reload", action: #selector(reloadPage), keyEquivalent: "r")
  421. reload.keyEquivalentModifierMask = [.command]
  422. menu.addItem(reload)
  423. let home = NSMenuItem(title: "Home", action: #selector(goHome), keyEquivalent: "h")
  424. home.keyEquivalentModifierMask = [.command]
  425. menu.addItem(home)
  426. let downloads = NSMenuItem(title: "Open Downloads", action: #selector(openDownloads), keyEquivalent: "d")
  427. downloads.keyEquivalentModifierMask = [.command]
  428. menu.addItem(downloads)
  429. menu.addItem(NSMenuItem.separator())
  430. let close = NSMenuItem(title: "Close Window", action: #selector(closeWindowFromMenu), keyEquivalent: "w")
  431. close.keyEquivalentModifierMask = [.command]
  432. menu.addItem(close)
  433. menu.items.forEach { $0.target = self }
  434. let location = NSPoint(x: moreButton.bounds.midX, y: moreButton.bounds.minY - 4)
  435. menu.popUp(positioning: nil, at: location, in: moreButton)
  436. }
  437. @objc private func closeWindowFromMenu() {
  438. window?.performClose(nil)
  439. }
  440. /// WKWebView often reports the requested URL as `title` until the real document title is available.
  441. private func isLikelyURLString(_ raw: String) -> Bool {
  442. let t = raw.trimmingCharacters(in: .whitespacesAndNewlines)
  443. if t.isEmpty { return false }
  444. if t.hasPrefix("http://") || t.hasPrefix("https://") { return true }
  445. if t.contains("://") { return true }
  446. if let u = URL(string: t), u.scheme != nil, u.host != nil { return true }
  447. return false
  448. }
  449. private func updateSiteNameFromWebView() {
  450. let loading = webView.isLoading
  451. let host = webView.url?.host?.trimmingCharacters(in: .whitespacesAndNewlines)
  452. let rawTitle = webView.title?.trimmingCharacters(in: .whitespacesAndNewlines)
  453. let pageTitle: String? = {
  454. guard let rawTitle, !rawTitle.isEmpty else { return nil }
  455. return isLikelyURLString(rawTitle) ? nil : rawTitle
  456. }()
  457. // Prefer document title; never show hostname or URL-like strings while a load is in progress.
  458. let preferred: String
  459. if let pageTitle {
  460. preferred = pageTitle
  461. } else if loading {
  462. preferred = "Loading…"
  463. } else if let host, !host.isEmpty {
  464. preferred = host
  465. } else {
  466. preferred = "Browser"
  467. }
  468. siteNameLabel.stringValue = preferred
  469. window?.title = preferred
  470. guard let url = webView.url else {
  471. siteIconView.image = NSImage(systemSymbolName: "globe", accessibilityDescription: "Site")?
  472. .withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold))
  473. return
  474. }
  475. let key = (url.host ?? url.absoluteString).lowercased()
  476. guard currentSiteIconURLKey != key else { return }
  477. currentSiteIconURLKey = key
  478. Task { [weak self] in
  479. guard let self else { return }
  480. let fetched = await FaviconImageCache.shared.image(for: url)
  481. await MainActor.run {
  482. guard self.currentSiteIconURLKey == key else { return }
  483. if let fetched {
  484. self.siteIconView.image = fetched
  485. } else {
  486. self.siteIconView.image = NSImage(systemSymbolName: "globe", accessibilityDescription: "Site")?
  487. .withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold))
  488. self.siteIconView.contentTintColor = NSColor.secondaryLabelColor
  489. }
  490. }
  491. }
  492. }
  493. private func updateToolbarState() {
  494. backButton.isEnabled = webView.canGoBack
  495. forwardButton.isEnabled = webView.canGoForward
  496. // Keep reload enabled; it matches typical browser behavior.
  497. reloadButton.isEnabled = true
  498. // Sync center site name.
  499. updateSiteNameFromWebView()
  500. }
  501. private func showAlert(title: String, message: String) {
  502. let alert = NSAlert()
  503. alert.messageText = title
  504. alert.informativeText = message
  505. alert.addButton(withTitle: "OK")
  506. alert.alertStyle = .informational
  507. alert.runModal()
  508. }
  509. }
  510. extension InAppBrowserWindowController: WKNavigationDelegate {
  511. func webView(
  512. _ webView: WKWebView,
  513. decidePolicyFor navigationAction: WKNavigationAction,
  514. decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
  515. ) {
  516. if navigationAction.targetFrame == nil, let url = navigationAction.request.url {
  517. InAppBrowserWindowManager.shared.open(url: url)
  518. decisionHandler(.cancel)
  519. return
  520. }
  521. decisionHandler(.allow)
  522. }
  523. func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
  524. updateToolbarState()
  525. }
  526. func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
  527. updateToolbarState()
  528. }
  529. func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
  530. updateToolbarState()
  531. }
  532. func webView(
  533. _ webView: WKWebView,
  534. didFailProvisionalNavigation navigation: WKNavigation!,
  535. withError error: Error
  536. ) {
  537. updateToolbarState()
  538. showLoadError(error)
  539. }
  540. func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
  541. updateToolbarState()
  542. showLoadError(error)
  543. }
  544. private func showLoadError(_ error: Error) {
  545. let message = """
  546. <html>
  547. <body style="background:#111;color:#EEE;font-family:-apple-system, sans-serif;padding:24px;">
  548. <h2>Page failed to load</h2>
  549. <p>Could not open <b>\(initialURL.absoluteString)</b></p>
  550. <p>\(error.localizedDescription)</p>
  551. </body>
  552. </html>
  553. """
  554. webView.loadHTMLString(message, baseURL: nil)
  555. window?.title = "Load Error"
  556. }
  557. }
  558. extension InAppBrowserWindowController: WKUIDelegate {
  559. func webView(
  560. _ webView: WKWebView,
  561. createWebViewWith configuration: WKWebViewConfiguration,
  562. for navigationAction: WKNavigationAction,
  563. windowFeatures: WKWindowFeatures
  564. ) -> WKWebView? {
  565. guard let url = navigationAction.request.url else { return nil }
  566. InAppBrowserWindowManager.shared.open(url: url)
  567. return nil
  568. }
  569. }