diff --git a/Mac/Base.lproj/MainWindow.storyboard b/Mac/Base.lproj/MainWindow.storyboard index 2a537adbc..a816f6226 100644 --- a/Mac/Base.lproj/MainWindow.storyboard +++ b/Mac/Base.lproj/MainWindow.storyboard @@ -364,14 +364,25 @@ + + + + + + + + + + + diff --git a/Mac/MainWindow/Detail/DetailContainerView.swift b/Mac/MainWindow/Detail/DetailContainerView.swift index ffc330b37..7e6ae710a 100644 --- a/Mac/MainWindow/Detail/DetailContainerView.swift +++ b/Mac/MainWindow/Detail/DetailContainerView.swift @@ -29,10 +29,61 @@ final class DetailContainerView: NSView { if let contentView = contentView { contentView.translatesAutoresizingMaskIntoConstraints = false addSubview(contentView, positioned: .below, relativeTo: detailStatusBarView) - let constraints = constraintsToMakeSubViewFullSize(contentView) + + // Constrain the content view to fill the available space on all sides except the top, which we'll constrain to the find bar + var constraints = constraintsToMakeSubViewFullSize(contentView).filter { $0.firstAttribute != .top } + + constraints.append(findBarContainerView.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor)) + constraints.append(findBarContainerView.bottomAnchor.constraint(equalTo: contentView.topAnchor)) NSLayoutConstraint.activate(constraints) contentViewConstraints = constraints } } } + + // MARK: NSTextFinderBarContainer + + @IBOutlet var findBarContainerView: NSView! + @IBOutlet var findBarHeightConstraint: NSLayoutConstraint! + + + public var findBarView: NSView? = nil { + didSet { + oldValue?.removeFromSuperview() + } + } + + public var isFindBarVisible = false { + didSet { + // It seems AppKit assumes the findBarView will be removed from its superview when it's + // not being shown, so we have to fulfill that expectation in addition to hiding the stack view + // container we embed it in. + if + self.isFindBarVisible, + let view = findBarView + { + view.layoutSubtreeIfNeeded() + view.frame.origin = NSZeroPoint + view.frame.size.width = self.findBarContainerView.bounds.width + findBarContainerView.frame = view.bounds + findBarHeightConstraint.constant = view.frame.size.height + 1.0 + findBarContainerView.addSubview(view) + } + else { + if let view = findBarView { + view.removeFromSuperview() + findBarHeightConstraint.constant = 0 + } + } + } + } + + func findBarViewDidChangeHeight() { + if let height = findBarView?.frame.size.height { + findBarHeightConstraint.constant = height + 1.0 + findBarContainerView.layoutSubtreeIfNeeded() + findBarView?.setFrameOrigin(NSPoint.zero) + } + } + } diff --git a/Mac/MainWindow/Detail/DetailViewController.swift b/Mac/MainWindow/Detail/DetailViewController.swift index f8758a852..bcfd2533a 100644 --- a/Mac/MainWindow/Detail/DetailViewController.swift +++ b/Mac/MainWindow/Detail/DetailViewController.swift @@ -41,6 +41,7 @@ final class DetailViewController: NSViewController, WKUIDelegate { } statusBarView.mouseoverLink = nil containerView.contentView = webview + resetTextFinder() } } @@ -52,6 +53,7 @@ final class DetailViewController: NSViewController, WKUIDelegate { func setState(_ state: DetailState, mode: TimelineSourceMode) { webViewController(for: mode).state = state + resetTextFinder() } func showDetail(for mode: TimelineSourceMode) { @@ -92,7 +94,33 @@ final class DetailViewController: NSViewController, WKUIDelegate { func saveState(to state: inout [AnyHashable : Any]) { currentWebViewController.saveState(to: &state) } - + + // MARK: Find in Article + + private var didLoadTextFinder = false + lazy private var textFinder: NSTextFinder = { + let finder = NSTextFinder() + finder.isIncrementalSearchingEnabled = true + finder.incrementalSearchingShouldDimContentView = false + finder.client = self.currentWebViewController.webView + finder.findBarContainer = self.containerView + didLoadTextFinder = true + return finder + }() + + private func resetTextFinder() { + if didLoadTextFinder { + self.textFinder.performAction(.hideFindInterface) + self.textFinder.client = currentWebViewController.webView + } + } + + @IBAction func performFindPanelAction(_ sender: Any?) { + if let menuItem = sender as? NSMenuItem, let findAction = NSTextFinder.Action(rawValue: menuItem.tag) { + self.textFinder.performAction(findAction) + } + } + } // MARK: - DetailWebViewControllerDelegate diff --git a/Mac/MainWindow/Detail/DetailWebView.swift b/Mac/MainWindow/Detail/DetailWebView.swift index 9c7a618b9..f66555500 100644 --- a/Mac/MainWindow/Detail/DetailWebView.swift +++ b/Mac/MainWindow/Detail/DetailWebView.swift @@ -99,6 +99,12 @@ final class DetailWebView: WKWebView { window!.setFrame(frame, display: false) } } + + // MARK: NSTextFinderClient + + // Returning false here prevents the "Replace" checkbox from appearing in the find bar + override var isEditable: Bool { return false } + } // MARK: - Private diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index 0c3465f03..6d669cf5f 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -186,7 +186,18 @@ final class DetailWebViewController: NSViewController { state[UserInfoKey.isShowingExtractedArticle] = isShowingExtractedArticle state[UserInfoKey.articleWindowScrollY] = windowScrollY } - + + // MARK: Find in Article + + var canFindInArticle: Bool { + switch state { + case .article(_, _), .extracted(_, _, _): + return true + default: + return false + } + } + } // MARK: - WKScriptMessageHandler diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index 10320b8c5..17aba7c5b 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -276,6 +276,9 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { return true } + if item.action == #selector(performFindPanelAction(_:)) { + return self.detailViewController?.currentWebViewController.canFindInArticle ?? false + } return true } @@ -536,6 +539,10 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { @IBAction func toggleReadArticlesFilter(_ sender: Any?) { timelineContainerViewController?.toggleReadFilter() } + + @IBAction func performFindPanelAction(_ sender: Any?) { + self.detailViewController?.performFindPanelAction(sender) + } @objc func selectArticleTheme(_ menuItem: NSMenuItem) { ArticleThemesManager.shared.currentThemeName = menuItem.title