From 5ece0fd4d450964fa36ba310a746f9fd4cd23865 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 19 Feb 2024 15:36:47 +0600 Subject: [PATCH] drop TabPreview.storyboard --- DuckDuckGo.xcodeproj/project.pbxproj | 8 - .../TabBar/View/TabBarViewController.swift | 6 +- DuckDuckGo/TabPreview/TabPreview.storyboard | 127 ------------- .../TabPreview/TabPreviewViewController.swift | 178 ++++++++++++++++-- .../TabPreviewWindowController.swift | 27 ++- 5 files changed, 189 insertions(+), 157 deletions(-) delete mode 100644 DuckDuckGo/TabPreview/TabPreview.storyboard diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index a3bcf7326b..1ea5ddd2a9 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -666,7 +666,6 @@ 3706FCC0293F65D500E42796 /* FindInPage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85A0117325AF2EDF00FA6A0C /* FindInPage.storyboard */; }; 3706FCC3293F65D500E42796 /* userscript.js in Resources */ = {isa = PBXBuildFile; fileRef = B31055BE27A1BA1D001AC618 /* userscript.js */; }; 3706FCC4293F65D500E42796 /* fb-tds.json in Resources */ = {isa = PBXBuildFile; fileRef = EA4617EF273A28A700F110A2 /* fb-tds.json */; }; - 3706FCC5293F65D500E42796 /* TabPreview.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAE8B101258A41C000E81239 /* TabPreview.storyboard */; }; 3706FCC6293F65D500E42796 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = AA68C3D62490F821001B8783 /* README.md */; }; 3706FCC8293F65D500E42796 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA585D85248FD31400E9A3E2 /* Assets.xcassets */; }; 3706FCC9293F65D500E42796 /* NavigationBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85589E8C27BBBB870038AD11 /* NavigationBar.storyboard */; }; @@ -1950,7 +1949,6 @@ 4B957BFD2AC7AE700062CA31 /* JSAlert.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = EEC111E3294D06020086524F /* JSAlert.storyboard */; }; 4B957C002AC7AE700062CA31 /* userscript.js in Resources */ = {isa = PBXBuildFile; fileRef = B31055BE27A1BA1D001AC618 /* userscript.js */; }; 4B957C012AC7AE700062CA31 /* fb-tds.json in Resources */ = {isa = PBXBuildFile; fileRef = EA4617EF273A28A700F110A2 /* fb-tds.json */; }; - 4B957C022AC7AE700062CA31 /* TabPreview.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAE8B101258A41C000E81239 /* TabPreview.storyboard */; }; 4B957C032AC7AE700062CA31 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = AA68C3D62490F821001B8783 /* README.md */; }; 4B957C042AC7AE700062CA31 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AA585D85248FD31400E9A3E2 /* Assets.xcassets */; }; 4B957C052AC7AE700062CA31 /* NavigationBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85589E8C27BBBB870038AD11 /* NavigationBar.storyboard */; }; @@ -2554,7 +2552,6 @@ AAE7527C263B056C00B973F8 /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* HistoryStore.swift */; }; AAE7527E263B05C600B973F8 /* HistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527D263B05C600B973F8 /* HistoryEntry.swift */; }; AAE75280263B0A4D00B973F8 /* HistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */; }; - AAE8B102258A41C000E81239 /* TabPreview.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAE8B101258A41C000E81239 /* TabPreview.storyboard */; }; AAE8B110258A456C00E81239 /* TabPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */; }; AAE99B8927088A19008B6BD9 /* FirePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE99B8827088A19008B6BD9 /* FirePopover.swift */; }; AAEC74B22642C57200C2EFBC /* HistoryCoordinatingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */; }; @@ -4098,7 +4095,6 @@ AAE7527B263B056C00B973F8 /* HistoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryStore.swift; sourceTree = ""; }; AAE7527D263B05C600B973F8 /* HistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryEntry.swift; sourceTree = ""; }; AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinator.swift; sourceTree = ""; }; - AAE8B101258A41C000E81239 /* TabPreview.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = TabPreview.storyboard; sourceTree = ""; }; AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPreviewViewController.swift; sourceTree = ""; }; AAE99B8827088A19008B6BD9 /* FirePopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopover.swift; sourceTree = ""; }; AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinatingMock.swift; sourceTree = ""; }; @@ -7656,7 +7652,6 @@ AAE8B0FD258A416F00E81239 /* TabPreview */ = { isa = PBXGroup; children = ( - AAE8B101258A41C000E81239 /* TabPreview.storyboard */, AAC82C5F258B6CB5009B6B42 /* TabPreviewWindowController.swift */, AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */, 1DB67F272B6FE21D003DF243 /* Model */, @@ -8873,7 +8868,6 @@ EEC8EB3F2982CA440065AA39 /* JSAlert.storyboard in Resources */, 3706FCC3293F65D500E42796 /* userscript.js in Resources */, 3706FCC4293F65D500E42796 /* fb-tds.json in Resources */, - 3706FCC5293F65D500E42796 /* TabPreview.storyboard in Resources */, 3706FCC6293F65D500E42796 /* README.md in Resources */, 3706FCC8293F65D500E42796 /* Assets.xcassets in Resources */, 3706FCC9293F65D500E42796 /* NavigationBar.storyboard in Resources */, @@ -9000,7 +8994,6 @@ 4B957BFD2AC7AE700062CA31 /* JSAlert.storyboard in Resources */, 4B957C002AC7AE700062CA31 /* userscript.js in Resources */, 4B957C012AC7AE700062CA31 /* fb-tds.json in Resources */, - 4B957C022AC7AE700062CA31 /* TabPreview.storyboard in Resources */, 4B957C032AC7AE700062CA31 /* README.md in Resources */, 4B957C042AC7AE700062CA31 /* Assets.xcassets in Resources */, 4B957C052AC7AE700062CA31 /* NavigationBar.storyboard in Resources */, @@ -9096,7 +9089,6 @@ EEC111E4294D06020086524F /* JSAlert.storyboard in Resources */, B31055C627A1BA1D001AC618 /* userscript.js in Resources */, EA4617F0273A28A700F110A2 /* fb-tds.json in Resources */, - AAE8B102258A41C000E81239 /* TabPreview.storyboard in Resources */, AA68C3D72490F821001B8783 /* README.md in Resources */, AA585D86248FD31400E9A3E2 /* Assets.xcassets in Resources */, 85589E8D27BBBB870038AD11 /* NavigationBar.storyboard in Resources */, diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 6142ee5d3b..919cd75eff 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -558,11 +558,7 @@ final class TabBarViewController: NSViewController { // MARK: - Tab Preview - private var tabPreviewWindowController: TabPreviewWindowController = { - let storyboard = NSStoryboard(name: "TabPreview", bundle: nil) - // swiftlint:disable:next force_cast - return storyboard.instantiateController(withIdentifier: "TabPreviewWindowController") as! TabPreviewWindowController - }() + private lazy var tabPreviewWindowController = TabPreviewWindowController() private func showTabPreview(for tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem), diff --git a/DuckDuckGo/TabPreview/TabPreview.storyboard b/DuckDuckGo/TabPreview/TabPreview.storyboard deleted file mode 100644 index f8ffbfe7b0..0000000000 --- a/DuckDuckGo/TabPreview/TabPreview.storyboard +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DuckDuckGo/TabPreview/TabPreviewViewController.swift b/DuckDuckGo/TabPreview/TabPreviewViewController.swift index 67c846ba7e..e805efac9c 100644 --- a/DuckDuckGo/TabPreview/TabPreviewViewController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewViewController.swift @@ -18,34 +18,119 @@ import Cocoa -final class TabPreviewViewController: NSViewController { - - @IBOutlet weak var titleTextField: NSTextField! - @IBOutlet weak var urlTextField: NSTextField! - @IBOutlet weak var snapshotImageView: NSImageView! - @IBOutlet weak var snapshotImageViewHeightConstraint: NSLayoutConstraint! +protocol Previewable { + var shouldShowPreview: Bool { get } + var title: String { get } + var tabContent: Tab.TabContent { get } + var snapshot: NSImage? { get } } -extension TabPreviewViewController { +final class TabPreviewViewController: NSViewController { enum TextFieldMaskGradientSize: CGFloat { case width = 6 case trailingSpace = 12 } - override func viewDidLoad() { - super.viewDidLoad() + private lazy var viewColorView = ColorView(frame: .zero, backgroundColor: .controlBackgroundColor) + private lazy var titleTextField = NSTextField() + private lazy var urlTextField = NSTextField() + private lazy var box = NSBox() + private lazy var snapshotImageView = NSImageView() + + private var snapshotImageViewHeightConstraint: NSLayoutConstraint! + + init() { + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("\(Self.self): Bad initializer") + } + + override func loadView() { + view = NSView() + + view.addSubview(viewColorView) + view.addSubview(titleTextField) + view.addSubview(urlTextField) + view.addSubview(box) + view.addSubview(snapshotImageView) + + snapshotImageView.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + snapshotImageView.setContentHuggingPriority(.init(rawValue: 251), for: .vertical) + snapshotImageView.translatesAutoresizingMaskIntoConstraints = false + snapshotImageView.imageScaling = .scaleProportionallyDown + box.boxType = .separator + box.setContentHuggingPriority(.defaultHigh, for: .vertical) + box.translatesAutoresizingMaskIntoConstraints = false + + urlTextField.isEditable = false + urlTextField.isBordered = false + urlTextField.setContentHuggingPriority(.defaultHigh, for: .vertical) + urlTextField.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + urlTextField.translatesAutoresizingMaskIntoConstraints = false + urlTextField.backgroundColor = .textBackgroundColor + urlTextField.font = .systemFont(ofSize: 13) + urlTextField.lineBreakMode = .byTruncatingTail + urlTextField.textColor = .tabPreviewSecondaryTint + + titleTextField.isEditable = false + titleTextField.isBordered = false + titleTextField.setContentHuggingPriority(.defaultHigh, for: .vertical) + titleTextField.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + titleTextField.translatesAutoresizingMaskIntoConstraints = false + titleTextField.backgroundColor = .textBackgroundColor + titleTextField.font = .systemFont(ofSize: 13, weight: .medium) + titleTextField.textColor = .tabPreviewTint titleTextField.maximumNumberOfLines = 3 titleTextField.cell?.truncatesLastVisibleLine = true + + viewColorView.translatesAutoresizingMaskIntoConstraints = false + + setupLayout() + } + + private func setupLayout() { + + viewColorView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + titleTextField.topAnchor.constraint(equalTo: viewColorView.topAnchor, constant: 10).isActive = true + box.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + view.trailingAnchor.constraint(equalTo: snapshotImageView.trailingAnchor).isActive = true + view.trailingAnchor.constraint(equalTo: viewColorView.trailingAnchor).isActive = true + urlTextField.topAnchor.constraint(equalTo: titleTextField.bottomAnchor, constant: 4).isActive = true + viewColorView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + view.bottomAnchor.constraint(equalTo: snapshotImageView.bottomAnchor).isActive = true + titleTextField.topAnchor.constraint(equalTo: view.topAnchor, constant: 10).isActive = true + snapshotImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + urlTextField.bottomAnchor.constraint(equalTo: viewColorView.bottomAnchor, constant: -12).isActive = true + titleTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true + view.trailingAnchor.constraint(equalTo: box.trailingAnchor).isActive = true + urlTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16).isActive = true + view.trailingAnchor.constraint(equalTo: urlTextField.trailingAnchor, constant: 8).isActive = true + view.trailingAnchor.constraint(equalTo: titleTextField.trailingAnchor, constant: 8).isActive = true + box.bottomAnchor.constraint(equalTo: viewColorView.bottomAnchor).isActive = true + snapshotImageView.topAnchor.constraint(equalTo: viewColorView.bottomAnchor).isActive = true + + box.heightAnchor.constraint(equalToConstant: 1).isActive = true + + titleTextField.widthAnchor.constraint(equalToConstant: 256).isActive = true + + viewColorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 57).isActive = true + + snapshotImageViewHeightConstraint = snapshotImageView.heightAnchor.constraint(equalToConstant: 0) + snapshotImageViewHeightConstraint.isActive = true } - func display(tabViewModel: TabViewModel, isSelected: Bool) { + func display(tabViewModel: Previewable, isSelected: Bool) { + _=view // load view if needed + titleTextField.stringValue = tabViewModel.title titleTextField.lineBreakMode = isSelected ? .byWordWrapping : .byTruncatingTail - switch tabViewModel.tab.content { + switch tabViewModel.tabContent { case .url(let url, credential: _, source: _): urlTextField.stringValue = url.toString(decodePunycode: true, dropScheme: true, @@ -57,9 +142,9 @@ extension TabPreviewViewController { urlTextField.stringValue = "" } - if !isSelected, !tabViewModel.isShowingErrorPage, let snapshot = tabViewModel.tab.tabSnapshot { + if !isSelected, tabViewModel.shouldShowPreview, let snapshot = tabViewModel.snapshot { snapshotImageView.image = snapshot - snapshotImageViewHeightConstraint.constant = getHeight(for: tabViewModel.tab.tabSnapshot) + snapshotImageViewHeightConstraint.constant = getHeight(for: snapshot) } else { snapshotImageView.image = nil snapshotImageViewHeightConstraint.constant = 0 @@ -76,3 +161,70 @@ extension TabPreviewViewController { } } + +extension TabViewModel: Previewable { + + var shouldShowPreview: Bool { + !isShowingErrorPage + } + + var snapshot: NSImage? { + tab.tabSnapshot + } + + var tabContent: Tab.TabContent { + tab.content + } + +} + +#if DEBUG +extension TabPreviewViewController { + func displayMockPreview(of size: NSSize, withTitle title: String, content: Tab.TabContent, previewable: Bool, isSelected: Bool) { + + struct PreviewableMock: Previewable { + let size: NSSize + let title: String + var tabContent: Tab.TabContent + let shouldShowPreview: Bool + + var snapshot: NSImage? { + let image = NSImage(size: size) + image.lockFocus() + NSColor(deviceRed: 0.95, green: 0.98, blue: 0.99, alpha: 1).setFill() + NSRect(origin: .zero, size: image.size).fill() + image.unlockFocus() + return image + } + } + + self.display(tabViewModel: PreviewableMock(size: size, title: title, tabContent: content, shouldShowPreview: previewable), isSelected: isSelected) + } +} + +import Combine +private let previewSize = NSSize(width: 280, height: 220) + +@available(macOS 14.0, *) +#Preview(traits: .fixedLayout(width: previewSize.width, height: previewSize.height)) { { + + let vc = TabPreviewViewController() + vc.displayMockPreview(of: NSSize(width: 1280, height: 560), + withTitle: "Some reasonably long tab preview title that won‘t fit in one line", + content: .url(.makeSearchUrl(from: "SERP query string to search for some ducks")!, source: .ui), + previewable: true, + isSelected: true) + + var c: AnyCancellable! + c = vc.publisher(for: \.view.window).sink { window in + window?.titlebarAppearsTransparent = true + window?.titleVisibility = .hidden + window?.styleMask = [] + window?.setFrame(NSRect(origin: .zero, size: vc.view.bounds.size), display: true) + withExtendedLifetime(c) {} + } + + return vc + +}() } +#endif diff --git a/DuckDuckGo/TabPreview/TabPreviewWindowController.swift b/DuckDuckGo/TabPreview/TabPreviewWindowController.swift index 6e4e5f4456..f9fca79fcd 100644 --- a/DuckDuckGo/TabPreview/TabPreviewWindowController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewWindowController.swift @@ -34,20 +34,39 @@ final class TabPreviewWindowController: NSWindowController { // swiftlint:disable force_cast var tabPreviewViewController: TabPreviewViewController { - contentViewController as! TabPreviewViewController + return self.window!.contentViewController as! TabPreviewViewController } // swiftlint:enable force_cast - override func windowDidLoad() { - super.windowDidLoad() + init() { + super.init(window: Self.loadWindow()) - window?.animationBehavior = .utilityWindow NotificationCenter.default.addObserver(self, selector: #selector(suggestionWindowOpenNotification(_:)), name: .suggestionWindowOpen, object: nil) } + required init?(coder: NSCoder) { + fatalError("\(Self.self): Bad initializer") + } + + private static func loadWindow() -> NSWindow { + let tabPreviewViewController = TabPreviewViewController() + + let window = NSWindow(contentRect: CGRect(x: 294, y: 313, width: 280, height: 58), styleMask: [.titled, .fullSizeContentView], backing: .buffered, defer: true) + window.contentViewController = tabPreviewViewController + + window.allowsToolTipsWhenApplicationIsInactive = false + window.autorecalculatesKeyViewLoop = false + window.isReleasedWhenClosed = false + window.titleVisibility = .hidden + window.titlebarAppearsTransparent = true + window.animationBehavior = .utilityWindow + + return window + } + deinit { NotificationCenter.default.removeObserver(self) }