InAppBrowserWindow.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. import AppKit
  2. import WebKit
  3. final class InAppBrowserWindowManager {
  4. static let shared = InAppBrowserWindowManager()
  5. private var controllers: [InAppBrowserWindowController] = []
  6. private init() {}
  7. func open(url: URL, title: String? = nil) {
  8. let controller = InAppBrowserWindowController(initialURL: url, windowTitle: title)
  9. controllers.append(controller)
  10. controller.onClose = { [weak self, weak controller] in
  11. guard let self, let controller else { return }
  12. self.controllers.removeAll { $0 === controller }
  13. }
  14. controller.showWindow(nil)
  15. controller.window?.makeKeyAndOrderFront(nil)
  16. }
  17. }
  18. final class InAppBrowserWindowController: NSWindowController, NSWindowDelegate {
  19. var onClose: (() -> Void)?
  20. private let webView: WKWebView
  21. private let initialURL: URL
  22. private var didLoadInitialURL = false
  23. // Toolbar UI (top navigation bar)
  24. private var backButton: NSButton!
  25. private var forwardButton: NSButton!
  26. private var reloadButton: NSButton!
  27. private var pinButton: NSButton!
  28. private var homeButton: NSButton!
  29. private var gridButton: NSButton!
  30. private var openInBrowserButton: NSButton!
  31. private var handButton: NSButton!
  32. private var downloadButton: NSButton!
  33. private var moreButton: NSButton!
  34. private var urlField: NSTextField!
  35. private var isUpdatingURLField = false
  36. private var didSubmitURLFromAction = false
  37. private var isPinnedOnTop = false
  38. private var isGestureModeEnabled = true
  39. private func setToggledStyle(_ button: NSButton, isOn: Bool) {
  40. button.layer?.backgroundColor = isOn
  41. ? NSColor.systemBlue.withAlphaComponent(0.28).cgColor
  42. : NSColor.white.withAlphaComponent(0.06).cgColor
  43. button.layer?.borderColor = isOn
  44. ? NSColor.systemBlue.withAlphaComponent(0.55).cgColor
  45. : NSColor.white.withAlphaComponent(0.10).cgColor
  46. }
  47. init(initialURL: URL, windowTitle: String?) {
  48. self.initialURL = initialURL
  49. let config = WKWebViewConfiguration()
  50. config.defaultWebpagePreferences.allowsContentJavaScript = true
  51. self.webView = WKWebView(frame: .zero, configuration: config)
  52. let window = NSWindow(
  53. contentRect: NSRect(x: 180, y: 140, width: 1200, height: 800),
  54. styleMask: [.titled, .closable, .miniaturizable, .resizable],
  55. backing: .buffered,
  56. defer: false
  57. )
  58. window.title = windowTitle ?? "Browser"
  59. window.isReleasedWhenClosed = false
  60. // Build UI: top toolbar + web view underneath.
  61. let containerView = NSView(frame: .zero)
  62. containerView.wantsLayer = true
  63. containerView.layer?.backgroundColor = NSColor.clear.cgColor
  64. window.contentView = containerView
  65. let toolbarHeight: CGFloat = 46
  66. let toolbarContainer = NSView()
  67. toolbarContainer.translatesAutoresizingMaskIntoConstraints = false
  68. toolbarContainer.wantsLayer = true
  69. toolbarContainer.layer?.cornerRadius = 14
  70. toolbarContainer.layer?.masksToBounds = true
  71. containerView.addSubview(toolbarContainer)
  72. // Blurred background like a modern browser toolbar.
  73. let toolbarBlur = NSVisualEffectView()
  74. toolbarBlur.material = .hudWindow
  75. toolbarBlur.blendingMode = .behindWindow
  76. toolbarBlur.state = .active
  77. toolbarBlur.translatesAutoresizingMaskIntoConstraints = false
  78. toolbarContainer.addSubview(toolbarBlur)
  79. let toolbarView = NSStackView()
  80. toolbarView.orientation = .horizontal
  81. toolbarView.alignment = .centerY
  82. toolbarView.spacing = 10
  83. toolbarView.translatesAutoresizingMaskIntoConstraints = false
  84. toolbarContainer.addSubview(toolbarView)
  85. toolbarBlur.wantsLayer = true
  86. toolbarBlur.layer?.cornerRadius = 14
  87. toolbarBlur.layer?.masksToBounds = true
  88. func makeIconButton(symbolName: String, accessibility: String, action: Selector, fallbackSymbolName: String? = nil) -> NSButton {
  89. let button = NSButton()
  90. let primary = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibility)
  91. let fallback = fallbackSymbolName.flatMap { NSImage(systemSymbolName: $0, accessibilityDescription: accessibility) }
  92. button.image = primary ?? fallback
  93. button.image?.isTemplate = true
  94. button.isBordered = false
  95. button.contentTintColor = .white
  96. button.imagePosition = .imageOnly
  97. button.imageScaling = .scaleProportionallyDown
  98. button.setButtonType(.momentaryChange)
  99. button.action = action
  100. button.setButtonType(.momentaryChange)
  101. // Subtle circular hit target like the screenshot.
  102. button.wantsLayer = true
  103. button.layer?.cornerRadius = 12
  104. button.layer?.masksToBounds = true
  105. button.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.06).cgColor
  106. button.layer?.borderColor = NSColor.white.withAlphaComponent(0.10).cgColor
  107. button.layer?.borderWidth = 1
  108. button.translatesAutoresizingMaskIntoConstraints = false
  109. button.widthAnchor.constraint(equalToConstant: 26).isActive = true
  110. button.heightAnchor.constraint(equalToConstant: 26).isActive = true
  111. if button.image == nil {
  112. // Last-resort fallback if SF Symbols aren’t available for some reason.
  113. button.title = "⋮"
  114. button.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
  115. button.contentTintColor = NSColor.white.withAlphaComponent(0.9)
  116. } else {
  117. button.title = ""
  118. }
  119. button.toolTip = accessibility
  120. return button
  121. }
  122. // Back
  123. backButton = makeIconButton(
  124. symbolName: "chevron.left",
  125. accessibility: "Back",
  126. action: #selector(goBack)
  127. )
  128. // Forward
  129. forwardButton = makeIconButton(
  130. symbolName: "chevron.right",
  131. accessibility: "Forward",
  132. action: #selector(goForward)
  133. )
  134. // Reload
  135. reloadButton = makeIconButton(
  136. symbolName: "arrow.clockwise",
  137. accessibility: "Reload",
  138. action: #selector(reloadPage)
  139. )
  140. // Pin (always on top)
  141. pinButton = makeIconButton(
  142. symbolName: "pin",
  143. accessibility: "Pin (Always on Top)",
  144. action: #selector(togglePinOnTop)
  145. )
  146. // Home
  147. homeButton = makeIconButton(
  148. symbolName: "house",
  149. accessibility: "Home",
  150. action: #selector(goHome)
  151. )
  152. // Grid (show launcher)
  153. gridButton = makeIconButton(
  154. symbolName: "square.grid.2x2",
  155. accessibility: "Show Launcher",
  156. action: #selector(showLauncher)
  157. )
  158. // URL "pill" wrapper.
  159. let urlWrapperView = NSView()
  160. urlWrapperView.translatesAutoresizingMaskIntoConstraints = false
  161. urlWrapperView.wantsLayer = true
  162. urlWrapperView.layer?.cornerRadius = 12
  163. urlWrapperView.layer?.masksToBounds = true
  164. urlWrapperView.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.07).cgColor
  165. urlWrapperView.layer?.borderColor = NSColor.white.withAlphaComponent(0.18).cgColor
  166. urlWrapperView.layer?.borderWidth = 1
  167. // URL field (inside pill)
  168. urlField = NSTextField()
  169. urlField.translatesAutoresizingMaskIntoConstraints = false
  170. urlField.action = #selector(urlFieldDidSubmit)
  171. urlField.placeholderString = "Search or enter website name"
  172. urlField.font = NSFont.systemFont(ofSize: 13, weight: .medium)
  173. urlField.textColor = .white
  174. urlField.alignment = .left
  175. urlField.isEditable = true
  176. urlField.isBordered = false
  177. urlField.drawsBackground = false
  178. urlField.focusRingType = .none
  179. // Optional "lock" glyph like browsers (purely visual).
  180. let lockImageView = NSImageView()
  181. lockImageView.translatesAutoresizingMaskIntoConstraints = false
  182. lockImageView.image = NSImage(systemSymbolName: "lock.fill", accessibilityDescription: "Secure")
  183. lockImageView.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
  184. lockImageView.contentTintColor = NSColor.white.withAlphaComponent(0.7)
  185. lockImageView.imageScaling = .scaleProportionallyDown
  186. let urlInnerStack = NSStackView(views: [lockImageView, urlField])
  187. urlInnerStack.orientation = .horizontal
  188. urlInnerStack.alignment = .centerY
  189. urlInnerStack.spacing = 8
  190. urlInnerStack.translatesAutoresizingMaskIntoConstraints = false
  191. urlWrapperView.addSubview(urlInnerStack)
  192. NSLayoutConstraint.activate([
  193. lockImageView.widthAnchor.constraint(equalToConstant: 14),
  194. lockImageView.heightAnchor.constraint(equalToConstant: 14),
  195. urlInnerStack.leadingAnchor.constraint(equalTo: urlWrapperView.leadingAnchor, constant: 12),
  196. urlInnerStack.trailingAnchor.constraint(equalTo: urlWrapperView.trailingAnchor, constant: -12),
  197. urlInnerStack.topAnchor.constraint(equalTo: urlWrapperView.topAnchor, constant: 6),
  198. urlInnerStack.bottomAnchor.constraint(equalTo: urlWrapperView.bottomAnchor, constant: -6),
  199. urlWrapperView.heightAnchor.constraint(equalToConstant: 30),
  200. ])
  201. // Open in external browser
  202. openInBrowserButton = makeIconButton(
  203. symbolName: "arrow.up.right.square",
  204. accessibility: "Open in Browser",
  205. action: #selector(openInBrowser)
  206. )
  207. // Hand tool (toggle navigation gestures)
  208. handButton = makeIconButton(
  209. symbolName: "hand.raised",
  210. accessibility: "Gesture Mode",
  211. action: #selector(toggleGestureMode)
  212. )
  213. // Downloads (open Downloads folder)
  214. downloadButton = makeIconButton(
  215. symbolName: "arrow.down.circle",
  216. accessibility: "Downloads",
  217. action: #selector(openDownloads)
  218. )
  219. // More menu
  220. moreButton = makeIconButton(
  221. symbolName: "ellipsis.vertical",
  222. accessibility: "More",
  223. action: #selector(showMoreMenu),
  224. fallbackSymbolName: "ellipsis"
  225. )
  226. toolbarView.addArrangedSubview(backButton)
  227. toolbarView.addArrangedSubview(forwardButton)
  228. toolbarView.addArrangedSubview(reloadButton)
  229. toolbarView.addArrangedSubview(urlWrapperView)
  230. toolbarView.addArrangedSubview(pinButton)
  231. toolbarView.addArrangedSubview(homeButton)
  232. toolbarView.addArrangedSubview(gridButton)
  233. toolbarView.addArrangedSubview(openInBrowserButton)
  234. toolbarView.addArrangedSubview(handButton)
  235. toolbarView.addArrangedSubview(downloadButton)
  236. toolbarView.addArrangedSubview(moreButton)
  237. // Give the URL field more space than the icon buttons.
  238. urlWrapperView.setContentHuggingPriority(.defaultLow, for: .horizontal)
  239. urlWrapperView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
  240. // (URL layout constraints are defined via urlInnerStack above)
  241. // Web view below toolbar.
  242. webView.translatesAutoresizingMaskIntoConstraints = false
  243. webView.setValue(false, forKey: "drawsBackground")
  244. containerView.addSubview(webView)
  245. NSLayoutConstraint.activate([
  246. toolbarContainer.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10),
  247. toolbarContainer.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -10),
  248. toolbarContainer.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10),
  249. toolbarContainer.heightAnchor.constraint(equalToConstant: toolbarHeight),
  250. toolbarBlur.leadingAnchor.constraint(equalTo: toolbarContainer.leadingAnchor),
  251. toolbarBlur.trailingAnchor.constraint(equalTo: toolbarContainer.trailingAnchor),
  252. toolbarBlur.topAnchor.constraint(equalTo: toolbarContainer.topAnchor),
  253. toolbarBlur.bottomAnchor.constraint(equalTo: toolbarContainer.bottomAnchor),
  254. toolbarView.leadingAnchor.constraint(equalTo: toolbarContainer.leadingAnchor, constant: 12),
  255. toolbarView.trailingAnchor.constraint(equalTo: toolbarContainer.trailingAnchor, constant: -12),
  256. toolbarView.topAnchor.constraint(equalTo: toolbarContainer.topAnchor, constant: 6),
  257. toolbarView.bottomAnchor.constraint(equalTo: toolbarContainer.bottomAnchor, constant: -6),
  258. webView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
  259. webView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
  260. webView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
  261. webView.topAnchor.constraint(equalTo: toolbarContainer.bottomAnchor, constant: 0),
  262. ])
  263. super.init(window: window)
  264. window.delegate = self
  265. // Safe to reference `self` only after super.init.
  266. backButton.target = self
  267. forwardButton.target = self
  268. reloadButton.target = self
  269. pinButton.target = self
  270. homeButton.target = self
  271. gridButton.target = self
  272. openInBrowserButton.target = self
  273. handButton.target = self
  274. downloadButton.target = self
  275. moreButton.target = self
  276. urlField.delegate = self
  277. urlField.target = self
  278. webView.uiDelegate = self
  279. webView.navigationDelegate = self
  280. webView.allowsBackForwardNavigationGestures = true
  281. isGestureModeEnabled = true
  282. // Initialize toolbar state.
  283. setToggledStyle(pinButton, isOn: isPinnedOnTop)
  284. setToggledStyle(handButton, isOn: isGestureModeEnabled)
  285. urlField.stringValue = initialURL.absoluteString
  286. updateToolbarState()
  287. }
  288. @available(*, unavailable)
  289. required init?(coder: NSCoder) {
  290. nil
  291. }
  292. override func showWindow(_ sender: Any?) {
  293. super.showWindow(sender)
  294. guard !didLoadInitialURL else { return }
  295. didLoadInitialURL = true
  296. webView.load(URLRequest(url: initialURL))
  297. }
  298. func windowWillClose(_ notification: Notification) {
  299. onClose?()
  300. }
  301. @objc private func goBack() {
  302. webView.goBack()
  303. }
  304. @objc private func goForward() {
  305. webView.goForward()
  306. }
  307. @objc private func reloadPage() {
  308. webView.reload()
  309. }
  310. @objc private func openInBrowser() {
  311. guard let url = webView.url else { return }
  312. NSWorkspace.shared.open(url)
  313. }
  314. @objc private func goHome() {
  315. webView.load(URLRequest(url: initialURL))
  316. }
  317. @objc private func showLauncher() {
  318. // Bring the main app window (launcher) to front.
  319. if let main = NSApp.windows.first {
  320. main.makeKeyAndOrderFront(nil)
  321. }
  322. NSApp.activate(ignoringOtherApps: true)
  323. }
  324. @objc private func togglePinOnTop() {
  325. isPinnedOnTop.toggle()
  326. window?.level = isPinnedOnTop ? .floating : .normal
  327. if let button = pinButton {
  328. let symbol = isPinnedOnTop ? "pin.fill" : "pin"
  329. button.image = NSImage(systemSymbolName: symbol, accessibilityDescription: "Pin (Always on Top)")
  330. button.image?.isTemplate = true
  331. setToggledStyle(button, isOn: isPinnedOnTop)
  332. }
  333. }
  334. @objc private func toggleGestureMode() {
  335. isGestureModeEnabled.toggle()
  336. webView.allowsBackForwardNavigationGestures = isGestureModeEnabled
  337. if let button = handButton {
  338. let symbol = isGestureModeEnabled ? "hand.raised.fill" : "hand.raised"
  339. button.image = NSImage(systemSymbolName: symbol, accessibilityDescription: "Gesture Mode")
  340. button.image?.isTemplate = true
  341. setToggledStyle(button, isOn: isGestureModeEnabled)
  342. }
  343. }
  344. @objc private func openDownloads() {
  345. guard let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
  346. showAlert(title: "Unavailable", message: "Couldn’t find your Downloads folder.")
  347. return
  348. }
  349. NSWorkspace.shared.open(downloads)
  350. }
  351. @objc private func showMoreMenu() {
  352. let menu = NSMenu()
  353. let copyURL = NSMenuItem(title: "Copy URL", action: #selector(copyCurrentURL), keyEquivalent: "c")
  354. copyURL.keyEquivalentModifierMask = [.command]
  355. menu.addItem(copyURL)
  356. menu.addItem(NSMenuItem.separator())
  357. let reload = NSMenuItem(title: "Reload", action: #selector(reloadPage), keyEquivalent: "r")
  358. reload.keyEquivalentModifierMask = [.command]
  359. menu.addItem(reload)
  360. let home = NSMenuItem(title: "Home", action: #selector(goHome), keyEquivalent: "h")
  361. home.keyEquivalentModifierMask = [.command]
  362. menu.addItem(home)
  363. let downloads = NSMenuItem(title: "Open Downloads", action: #selector(openDownloads), keyEquivalent: "d")
  364. downloads.keyEquivalentModifierMask = [.command]
  365. menu.addItem(downloads)
  366. let openExternal = NSMenuItem(title: "Open in Default Browser", action: #selector(openInBrowser), keyEquivalent: "o")
  367. openExternal.keyEquivalentModifierMask = [.command]
  368. menu.addItem(openExternal)
  369. menu.addItem(NSMenuItem.separator())
  370. let close = NSMenuItem(title: "Close Window", action: #selector(closeWindowFromMenu), keyEquivalent: "w")
  371. close.keyEquivalentModifierMask = [.command]
  372. menu.addItem(close)
  373. menu.items.forEach { $0.target = self }
  374. let location = NSPoint(x: moreButton.bounds.midX, y: moreButton.bounds.minY - 4)
  375. menu.popUp(positioning: nil, at: location, in: moreButton)
  376. }
  377. @objc private func copyCurrentURL() {
  378. guard let s = webView.url?.absoluteString, !s.isEmpty else { return }
  379. NSPasteboard.general.clearContents()
  380. NSPasteboard.general.setString(s, forType: .string)
  381. }
  382. @objc private func closeWindowFromMenu() {
  383. window?.performClose(nil)
  384. }
  385. @objc private func urlFieldDidSubmit() {
  386. didSubmitURLFromAction = true
  387. loadFromURLField()
  388. }
  389. private func loadFromURLField() {
  390. let raw = urlField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
  391. guard !raw.isEmpty else { return }
  392. let normalized: String
  393. if raw.lowercased().hasPrefix("http://") || raw.lowercased().hasPrefix("https://") {
  394. normalized = raw
  395. } else {
  396. normalized = "https://\(raw)"
  397. }
  398. guard let url = URL(string: normalized) else {
  399. revertURLFieldToCurrentWebURL()
  400. return
  401. }
  402. // Keep normalized URL visible immediately after submit.
  403. isUpdatingURLField = true
  404. urlField.stringValue = normalized
  405. isUpdatingURLField = false
  406. webView.load(URLRequest(url: url))
  407. }
  408. private func revertURLFieldToCurrentWebURL() {
  409. guard let url = webView.url?.absoluteString else { return }
  410. isUpdatingURLField = true
  411. urlField.stringValue = url
  412. isUpdatingURLField = false
  413. }
  414. private var isURLFieldBeingEdited: Bool {
  415. urlField.currentEditor() != nil
  416. }
  417. private func setURLFieldToCurrentWebURL() {
  418. guard let url = webView.url?.absoluteString else { return }
  419. guard !isUpdatingURLField else { return }
  420. // Avoid overriding manual typing unless this update follows a submit.
  421. // However, right after navigation starts the text field can still be
  422. // considered "editing" (first responder), which would otherwise keep
  423. // the URL pill blank/stale. If it's empty, we should populate it.
  424. if isURLFieldBeingEdited && !didSubmitURLFromAction {
  425. if urlField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
  426. // proceed
  427. } else {
  428. return
  429. }
  430. }
  431. isUpdatingURLField = true
  432. urlField.stringValue = url
  433. isUpdatingURLField = false
  434. didSubmitURLFromAction = false
  435. }
  436. private func updateToolbarState() {
  437. backButton.isEnabled = webView.canGoBack
  438. forwardButton.isEnabled = webView.canGoForward
  439. // Keep reload enabled; it matches typical browser behavior.
  440. reloadButton.isEnabled = true
  441. // Sync URL field and window title.
  442. setURLFieldToCurrentWebURL()
  443. }
  444. private func showAlert(title: String, message: String) {
  445. let alert = NSAlert()
  446. alert.messageText = title
  447. alert.informativeText = message
  448. alert.addButton(withTitle: "OK")
  449. alert.alertStyle = .informational
  450. alert.runModal()
  451. }
  452. }
  453. extension InAppBrowserWindowController: WKNavigationDelegate {
  454. func webView(
  455. _ webView: WKWebView,
  456. decidePolicyFor navigationAction: WKNavigationAction,
  457. decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
  458. ) {
  459. if navigationAction.targetFrame == nil, let url = navigationAction.request.url {
  460. InAppBrowserWindowManager.shared.open(url: url)
  461. decisionHandler(.cancel)
  462. return
  463. }
  464. decisionHandler(.allow)
  465. }
  466. func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
  467. if let host = webView.url?.host, !host.isEmpty {
  468. window?.title = host
  469. }
  470. updateToolbarState()
  471. }
  472. func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
  473. updateToolbarState()
  474. }
  475. func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
  476. updateToolbarState()
  477. }
  478. func webView(
  479. _ webView: WKWebView,
  480. didFailProvisionalNavigation navigation: WKNavigation!,
  481. withError error: Error
  482. ) {
  483. updateToolbarState()
  484. showLoadError(error)
  485. }
  486. func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
  487. updateToolbarState()
  488. showLoadError(error)
  489. }
  490. private func showLoadError(_ error: Error) {
  491. let message = """
  492. <html>
  493. <body style="background:#111;color:#EEE;font-family:-apple-system, sans-serif;padding:24px;">
  494. <h2>Page failed to load</h2>
  495. <p>Could not open <b>\(initialURL.absoluteString)</b></p>
  496. <p>\(error.localizedDescription)</p>
  497. </body>
  498. </html>
  499. """
  500. webView.loadHTMLString(message, baseURL: nil)
  501. window?.title = "Load Error"
  502. }
  503. }
  504. extension InAppBrowserWindowController: NSTextFieldDelegate {
  505. func controlTextDidEndEditing(_ obj: Notification) {
  506. guard obj.object as? NSTextField === urlField else { return }
  507. guard !isUpdatingURLField else { return }
  508. // Return key triggers both action and end-editing callbacks.
  509. // Prevent duplicate loads so history/back-forward stays stable.
  510. if didSubmitURLFromAction {
  511. didSubmitURLFromAction = false
  512. return
  513. }
  514. loadFromURLField()
  515. }
  516. }
  517. extension InAppBrowserWindowController: WKUIDelegate {
  518. func webView(
  519. _ webView: WKWebView,
  520. createWebViewWith configuration: WKWebViewConfiguration,
  521. for navigationAction: WKNavigationAction,
  522. windowFeatures: WKWindowFeatures
  523. ) -> WKWebView? {
  524. guard let url = navigationAction.request.url else { return nil }
  525. InAppBrowserWindowManager.shared.open(url: url)
  526. return nil
  527. }
  528. }