diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6bd2547ffb..0fdbfa2120 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -824,7 +824,6 @@ 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 */; }; - 3706FCCA293F65D500E42796 /* FirePopoverCollectionViewHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = AAE246F5270A3D3000BEEAEE /* FirePopoverCollectionViewHeader.xib */; }; 3706FCCC293F65D500E42796 /* TabBar.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA80EC7B256C46AA007083E7 /* TabBar.storyboard */; }; 3706FCCD293F65D500E42796 /* shield-dot.json in Resources */ = {isa = PBXBuildFile; fileRef = AA34396B2754D4E300B241FA /* shield-dot.json */; }; 3706FCD0293F65D500E42796 /* BookmarksBarCollectionViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BE53369286912D40019DBFD /* BookmarksBarCollectionViewItem.xib */; }; @@ -838,7 +837,6 @@ 3706FCE1293F65D500E42796 /* PermissionAuthorization.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B64C84DD2692D7400048FEBE /* PermissionAuthorization.storyboard */; }; 3706FCE2293F65D500E42796 /* dark-trackers-3.json in Resources */ = {isa = PBXBuildFile; fileRef = AA3439772754D55100B241FA /* dark-trackers-3.json */; }; 3706FCE3293F65D500E42796 /* dark-trackers-2.json in Resources */ = {isa = PBXBuildFile; fileRef = AA3439722754D55100B241FA /* dark-trackers-2.json */; }; - 3706FCE4293F65D500E42796 /* Fire.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAB7320626DD0C37002FACF9 /* Fire.storyboard */; }; 3706FCE6293F65D500E42796 /* social_images in Resources */ = {isa = PBXBuildFile; fileRef = EA18D1C9272F0DC8006DC101 /* social_images */; }; 3706FCE7293F65D500E42796 /* shield-dot-mouse-over.json in Resources */ = {isa = PBXBuildFile; fileRef = AA7EB6E827E880A600036718 /* shield-dot-mouse-over.json */; }; 3706FCEA293F65D500E42796 /* PasswordManager.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85625993269C8F9600EE44BC /* PasswordManager.storyboard */; }; @@ -848,7 +846,7 @@ 3706FCEE293F65D500E42796 /* trackers-3.json in Resources */ = {isa = PBXBuildFile; fileRef = AA3439752754D55100B241FA /* trackers-3.json */; }; 3706FCEF293F65D500E42796 /* macos-config.json in Resources */ = {isa = PBXBuildFile; fileRef = 026ADE1326C3010C002518EE /* macos-config.json */; }; 3706FCF0293F65D500E42796 /* httpsMobileV2BloomSpec.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B677427255DBEB800025BD8 /* httpsMobileV2BloomSpec.json */; }; - 3706FCF3293F65D500E42796 /* FirePopoverCollectionViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = AAE246F22709EF3B00BEEAEE /* FirePopoverCollectionViewItem.xib */; }; + 3706FCF1293F65D500E42796 /* TabBarFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = AA2CB12C2587BB5600AA6FBE /* TabBarFooter.xib */; }; 3706FCF5293F65D500E42796 /* dark-shield-dot.json in Resources */ = {isa = PBXBuildFile; fileRef = AA34396E2754D4E900B241FA /* dark-shield-dot.json */; }; 3706FCF6293F65D500E42796 /* trackers-2.json in Resources */ = {isa = PBXBuildFile; fileRef = AA3439742754D55100B241FA /* trackers-2.json */; }; 3706FDDA293F661700E42796 /* EmbeddedTrackerDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9833913227AAAEEE00DAF119 /* EmbeddedTrackerDataTests.swift */; }; @@ -2370,7 +2368,6 @@ AAAB9114288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAB9113288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift */; }; AAAB9116288EB46B00A057A9 /* VisitMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAB9115288EB46B00A057A9 /* VisitMenuItem.swift */; }; AAB549DF25DAB8F80058460B /* BookmarkViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB549DE25DAB8F80058460B /* BookmarkViewModel.swift */; }; - AAB7320726DD0C37002FACF9 /* Fire.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AAB7320626DD0C37002FACF9 /* Fire.storyboard */; }; AAB7320926DD0CD9002FACF9 /* FireViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB7320826DD0CD9002FACF9 /* FireViewController.swift */; }; AAB8203C26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB8203B26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift */; }; AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AABAF59B260A7D130085060C /* FaviconManagerMock.swift */; }; @@ -2404,8 +2401,6 @@ AAD86E52267A0DFF005C11BE /* UpdateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD86E51267A0DFF005C11BE /* UpdateController.swift */; }; AADCBF3A26F7C2CE00EF67A8 /* LottieAnimationCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = AADCBF3926F7C2CE00EF67A8 /* LottieAnimationCache.swift */; }; AAE246F32709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE246F12709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift */; }; - AAE246F42709EF3B00BEEAEE /* FirePopoverCollectionViewItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = AAE246F22709EF3B00BEEAEE /* FirePopoverCollectionViewItem.xib */; }; - AAE246F6270A3D3000BEEAEE /* FirePopoverCollectionViewHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = AAE246F5270A3D3000BEEAEE /* FirePopoverCollectionViewHeader.xib */; }; AAE246F8270A406200BEEAEE /* FirePopoverCollectionViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE246F7270A406200BEEAEE /* FirePopoverCollectionViewHeader.swift */; }; AAE39D1B24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE39D1A24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift */; }; AAE7527A263B046100B973F8 /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AAE75278263B046100B973F8 /* History.xcdatamodeld */; }; @@ -4512,7 +4507,6 @@ AAAB9113288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleanThisHistoryMenuItem.swift; sourceTree = ""; }; AAAB9115288EB46B00A057A9 /* VisitMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitMenuItem.swift; sourceTree = ""; }; AAB549DE25DAB8F80058460B /* BookmarkViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkViewModel.swift; sourceTree = ""; }; - AAB7320626DD0C37002FACF9 /* Fire.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Fire.storyboard; sourceTree = ""; }; AAB7320826DD0CD9002FACF9 /* FireViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireViewController.swift; sourceTree = ""; }; AAB8203B26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionListCharacteristics.swift; sourceTree = ""; }; AABAF59B260A7D130085060C /* FaviconManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconManagerMock.swift; sourceTree = ""; }; @@ -4547,8 +4541,6 @@ AAD86E51267A0DFF005C11BE /* UpdateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateController.swift; sourceTree = ""; }; AADCBF3926F7C2CE00EF67A8 /* LottieAnimationCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieAnimationCache.swift; sourceTree = ""; }; AAE246F12709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverCollectionViewItem.swift; sourceTree = ""; }; - AAE246F22709EF3B00BEEAEE /* FirePopoverCollectionViewItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FirePopoverCollectionViewItem.xib; sourceTree = ""; }; - AAE246F5270A3D3000BEEAEE /* FirePopoverCollectionViewHeader.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FirePopoverCollectionViewHeader.xib; sourceTree = ""; }; AAE246F7270A406200BEEAEE /* FirePopoverCollectionViewHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverCollectionViewHeader.swift; sourceTree = ""; }; AAE39D1A24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCollectionViewModelDelegateMock.swift; sourceTree = ""; }; AAE75279263B046100B973F8 /* History.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = History.xcdatamodel; sourceTree = ""; }; @@ -9053,7 +9045,6 @@ AAFCB38325E546FF00859DD4 /* View */ = { isa = PBXGroup; children = ( - AAB7320626DD0C37002FACF9 /* Fire.storyboard */, AAEEC6A827088ADB008445F7 /* FireCoordinator.swift */, AAB7320826DD0CD9002FACF9 /* FireViewController.swift */, AAE99B8827088A19008B6BD9 /* FirePopover.swift */, @@ -9061,9 +9052,7 @@ AA61C0CF2722159B00E6B681 /* FireInfoViewController.swift */, AA6AD95A2704B6DB00159F8A /* FirePopoverViewController.swift */, AAE246F7270A406200BEEAEE /* FirePopoverCollectionViewHeader.swift */, - AAE246F5270A3D3000BEEAEE /* FirePopoverCollectionViewHeader.xib */, AAE246F12709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift */, - AAE246F22709EF3B00BEEAEE /* FirePopoverCollectionViewItem.xib */, ); path = View; sourceTree = ""; @@ -10716,7 +10705,6 @@ 3706FCC6293F65D500E42796 /* README.md in Resources */, 3706FCC8293F65D500E42796 /* Assets.xcassets in Resources */, 3706FCC9293F65D500E42796 /* NavigationBar.storyboard in Resources */, - 3706FCCA293F65D500E42796 /* FirePopoverCollectionViewHeader.xib in Resources */, 9FBB0C012CB94FC10006B6A6 /* view_highlight.json in Resources */, 3706FCCC293F65D500E42796 /* TabBar.storyboard in Resources */, 3706FCCD293F65D500E42796 /* shield-dot.json in Resources */, @@ -10733,7 +10721,6 @@ 3706FCE1293F65D500E42796 /* PermissionAuthorization.storyboard in Resources */, 3706FCE2293F65D500E42796 /* dark-trackers-3.json in Resources */, 3706FCE3293F65D500E42796 /* dark-trackers-2.json in Resources */, - 3706FCE4293F65D500E42796 /* Fire.storyboard in Resources */, CD2AB5C62C8222FE0019EB49 /* filterSet.json in Resources */, 3706FCE6293F65D500E42796 /* social_images in Resources */, 3706FCE7293F65D500E42796 /* shield-dot-mouse-over.json in Resources */, @@ -10746,7 +10733,7 @@ 3706FCEE293F65D500E42796 /* trackers-3.json in Resources */, 3706FCEF293F65D500E42796 /* macos-config.json in Resources */, 3706FCF0293F65D500E42796 /* httpsMobileV2BloomSpec.json in Resources */, - 3706FCF3293F65D500E42796 /* FirePopoverCollectionViewItem.xib in Resources */, + 3706FCF1293F65D500E42796 /* TabBarFooter.xib in Resources */, 3706FCF5293F65D500E42796 /* dark-shield-dot.json in Resources */, 3706FCF6293F65D500E42796 /* trackers-2.json in Resources */, ); @@ -10918,7 +10905,6 @@ AA68C3D72490F821001B8783 /* README.md in Resources */, AA585D86248FD31400E9A3E2 /* Assets.xcassets in Resources */, 85589E8D27BBBB870038AD11 /* NavigationBar.storyboard in Resources */, - AAE246F6270A3D3000BEEAEE /* FirePopoverCollectionViewHeader.xib in Resources */, 9FBB0C002CB94FC10006B6A6 /* view_highlight.json in Resources */, AA80EC79256C46AA007083E7 /* TabBar.storyboard in Resources */, AA34396D2754D4E300B241FA /* shield-dot.json in Resources */, @@ -10935,7 +10921,6 @@ B64C84DE2692D7400048FEBE /* PermissionAuthorization.storyboard in Resources */, AA34397D2754D55100B241FA /* dark-trackers-3.json in Resources */, AA3439782754D55100B241FA /* dark-trackers-2.json in Resources */, - AAB7320726DD0C37002FACF9 /* Fire.storyboard in Resources */, CD2AB5C52C8222FE0019EB49 /* filterSet.json in Resources */, EA18D1CA272F0DC8006DC101 /* social_images in Resources */, AA7EB6E927E880A600036718 /* shield-dot-mouse-over.json in Resources */, @@ -10948,7 +10933,7 @@ AA34397B2754D55100B241FA /* trackers-3.json in Resources */, 026ADE1426C3010C002518EE /* macos-config.json in Resources */, 4B677432255DBEB800025BD8 /* httpsMobileV2BloomSpec.json in Resources */, - AAE246F42709EF3B00BEEAEE /* FirePopoverCollectionViewItem.xib in Resources */, + AA2CB12D2587BB5600AA6FBE /* TabBarFooter.xib in Resources */, AA3439702754D4E900B241FA /* dark-shield-dot.json in Resources */, AA34397A2754D55100B241FA /* trackers-2.json in Resources */, ); diff --git a/DuckDuckGo/Common/Extensions/NSViewControllerExtension.swift b/DuckDuckGo/Common/Extensions/NSViewControllerExtension.swift index cfe7953fdf..7a924e8c86 100644 --- a/DuckDuckGo/Common/Extensions/NSViewControllerExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSViewControllerExtension.swift @@ -93,8 +93,8 @@ extension NSViewController { } /// #Preview helper to hide Window controls on View Controller appearance - func _preview_hidingWindowControlsOnAppear() -> Self { // swiftlint:disable:this identifier_name - Preview_ViewControllerWindowObserver().attach(to: self) + func _preview_hidingWindowControlsOnAppear(sizeToFit: Bool = false) -> Self { // swiftlint:disable:this identifier_name + Preview_ViewControllerWindowObserver().attach(to: self, sizeToFit: sizeToFit) return self } @@ -109,7 +109,14 @@ func withoutAnimation(_ closure: () -> Void) { /// #Preview helper to hide Window controls on View Controller appearance final class Preview_ViewControllerWindowObserver: NSObject { - func attach(to viewController: NSViewController) { + + weak var sizeToFitViewController: NSViewController? + + func attach(to viewController: NSViewController, sizeToFit: Bool) { + if sizeToFit { + self.sizeToFitViewController = viewController + } + // Start observing the view.window property viewController.addObserver(self, forKeyPath: #keyPath(NSViewController.view.window), options: [.initial, .new], context: nil) viewController.onDeinit { @@ -123,5 +130,9 @@ final class Preview_ViewControllerWindowObserver: NSObject { window.titlebarAppearsTransparent = true window.titleVisibility = .hidden window.styleMask = [] + + if let vc = sizeToFitViewController { + window.setFrame(NSRect(origin: .zero, size: vc.view.bounds.size), display: true) + } } } diff --git a/DuckDuckGo/Common/Extensions/NSViewExtension.swift b/DuckDuckGo/Common/Extensions/NSViewExtension.swift index c17edf489f..da941b4da7 100644 --- a/DuckDuckGo/Common/Extensions/NSViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSViewExtension.swift @@ -21,6 +21,12 @@ import Combine import Common import os.log +extension NSButton { + var isFirstResponder: Bool { + window?.firstResponder === self + } +} + extension NSView { // Since macOS 14 Sonoma view has clipsToBound == false by default diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 14fa14eb82..83543a4756 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -328,6 +328,7 @@ struct UserText { static let fireDialogTabWillClose = NSLocalizedString("fire.dialog.tab-will-close", value: "Current tab will close", comment: "Warning label shown in an expanded view of the fire popover") static let fireDialogPinnedTabWillReload = NSLocalizedString("fire.dialog.tab-will-reload", value: "Pinned tab will reload", comment: "Warning label shown in an expanded view of the fire popover") static let fireDialogAllWindowsWillClose = NSLocalizedString("fire.dialog.all-windows-will-close", value: "All windows will close", comment: "Warning label shown in an expanded view of the fire popover") + static let fireDialogCloseAllTabsAndClear = NSLocalizedString("fire.dialog.close-all-tabs-and-clear", value: "Close all tabs and clear site data", comment: "Title of the Clear button in the fire popover") static let fireproofSite = NSLocalizedString("options.menu.fireproof-site", value: "Fireproof This Site", comment: "Context menu item") static let removeFireproofing = NSLocalizedString("options.menu.remove-fireproofing", value: "Remove Fireproofing", comment: "Context menu item") static let fireproof = NSLocalizedString("fireproof", value: "Fireproof", comment: "Fireproof button") diff --git a/DuckDuckGo/Common/View/AppKit/MouseOverButton.swift b/DuckDuckGo/Common/View/AppKit/MouseOverButton.swift index 49a2aef2c1..67988e7ccb 100644 --- a/DuckDuckGo/Common/View/AppKit/MouseOverButton.swift +++ b/DuckDuckGo/Common/View/AppKit/MouseOverButton.swift @@ -84,6 +84,22 @@ internal class MouseOverButton: NSButton, Hoverable { clipsToBounds = true } + init(title: String? = nil, target: AnyObject? = nil, action: Selector? = nil) { + super.init(frame: .zero) + self.title = title ?? "" + self.target = target + self.action = action + self.clipsToBounds = true + } + + init(image: NSImage, target: AnyObject? = nil, action: Selector? = nil) { + super.init(frame: .zero) + self.image = image + self.target = target + self.action = action + self.clipsToBounds = true + } + required init?(coder: NSCoder) { super.init(coder: coder) clipsToBounds = true diff --git a/DuckDuckGo/Fire/View/Fire.storyboard b/DuckDuckGo/Fire/View/Fire.storyboard deleted file mode 100644 index 4f5649eeb9..0000000000 --- a/DuckDuckGo/Fire/View/Fire.storyboard +++ /dev/null @@ -1,756 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DuckDuckGo/Fire/View/FireInfoViewController.swift b/DuckDuckGo/Fire/View/FireInfoViewController.swift index de6b42f190..a5e2d02c09 100644 --- a/DuckDuckGo/Fire/View/FireInfoViewController.swift +++ b/DuckDuckGo/Fire/View/FireInfoViewController.swift @@ -26,22 +26,96 @@ protocol FireInfoViewControllerDelegate: AnyObject { final class FireInfoViewController: NSViewController { - @IBOutlet weak var titleLabel: NSTextField! - @IBOutlet weak var descriptionLabel: NSTextField! - @IBOutlet weak var gotItButton: NSButton! + private lazy var titleLabel = NSTextField(string: UserText.fireInfoDialogTitle) + private lazy var descriptionLabel = NSTextField(wrappingLabelWithString: UserText.fireInfoDialogDescription) + private lazy var gotItButton = NSButton(title: UserText.gotIt, target: self, action: #selector(gotItAction)) + private lazy var imageView = NSImageView(image: .fireHeader) weak var delegate: FireInfoViewControllerDelegate? - override func viewDidLoad() { - titleLabel.stringValue = UserText.fireInfoDialogTitle - descriptionLabel.stringValue = UserText.fireInfoDialogDescription - gotItButton.title = UserText.gotIt + init() { + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("FireInfoViewController: Bad initializer") + } + + override func loadView() { + view = ColorView(frame: .zero, backgroundColor: .interfaceBackground) + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.alignment = .center + titleLabel.isEditable = false + titleLabel.isSelectable = false + titleLabel.isBordered = false + titleLabel.drawsBackground = false + titleLabel.font = .systemFont(ofSize: 15, weight: .semibold) + titleLabel.textColor = .greyText + + descriptionLabel.translatesAutoresizingMaskIntoConstraints = false + descriptionLabel.alignment = .center + descriptionLabel.isEditable = false + descriptionLabel.isSelectable = false + descriptionLabel.isBordered = false + descriptionLabel.drawsBackground = false + descriptionLabel.usesSingleLineMode = false + descriptionLabel.lineBreakMode = .byWordWrapping + descriptionLabel.font = .systemFont(ofSize: 13) + descriptionLabel.textColor = .greyText + + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.alignment = .left + imageView.imageScaling = .scaleProportionallyDown + + gotItButton.translatesAutoresizingMaskIntoConstraints = false + gotItButton.alignment = .center + gotItButton.bezelStyle = .rounded + gotItButton.controlSize = .large + gotItButton.font = .systemFont(ofSize: 13) + gotItButton.imageScaling = .scaleProportionallyDown + gotItButton.isBordered = true + gotItButton.keyEquivalent = "\r" + + view.addSubview(gotItButton) + view.addSubview(imageView) + view.addSubview(descriptionLabel) + view.addSubview(titleLabel) + + setupLayout() + } + + private func setupLayout() { + NSLayoutConstraint.activate([ + descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), + descriptionLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + gotItButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + imageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 80), + view.bottomAnchor.constraint(equalTo: gotItButton.bottomAnchor, constant: 16), + titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 20), + + titleLabel.widthAnchor.constraint(equalToConstant: 280), + + descriptionLabel.widthAnchor.constraint(equalToConstant: 280), + + imageView.heightAnchor.constraint(equalToConstant: 64), + imageView.widthAnchor.constraint(equalToConstant: 128), + + gotItButton.widthAnchor.constraint(equalToConstant: 280), + ]) } override func mouseDown(with event: NSEvent) {} - @IBAction func gotItAction(_ sender: Any) { + @objc func gotItAction(_ sender: Any) { delegate?.fireInfoViewControllerDidConfirm(self) } } + +@available(macOS 14.0, *) +#Preview(traits: .fixedLayout(width: 320, height: 363)) { + FireInfoViewController() +} diff --git a/DuckDuckGo/Fire/View/FirePopover.swift b/DuckDuckGo/Fire/View/FirePopover.swift index ab64ee7eb2..c328848044 100644 --- a/DuckDuckGo/Fire/View/FirePopover.swift +++ b/DuckDuckGo/Fire/View/FirePopover.swift @@ -63,11 +63,7 @@ final class FirePopover: NSPopover { // swiftlint:enable force_cast private func setupContentController(fireViewModel: FireViewModel, tabCollectionViewModel: TabCollectionViewModel) { - let storyboard = NSStoryboard(name: "Fire", bundle: nil) - let controller = storyboard.instantiateController(identifier: "FirePopoverWrapperViewController") { coder -> FirePopoverWrapperViewController? in - return FirePopoverWrapperViewController(coder: coder, fireViewModel: fireViewModel, tabCollectionViewModel: tabCollectionViewModel) - } - contentViewController = controller + contentViewController = FirePopoverWrapperViewController(fireViewModel: fireViewModel, tabCollectionViewModel: tabCollectionViewModel) } } diff --git a/DuckDuckGo/Fire/View/FirePopoverCollectionViewHeader.swift b/DuckDuckGo/Fire/View/FirePopoverCollectionViewHeader.swift index 4c626ea4a1..44d3e97516 100644 --- a/DuckDuckGo/Fire/View/FirePopoverCollectionViewHeader.swift +++ b/DuckDuckGo/Fire/View/FirePopoverCollectionViewHeader.swift @@ -20,10 +20,59 @@ import Cocoa final class FirePopoverCollectionViewHeader: NSView { - static let identifier = NSUserInterfaceItemIdentifier(rawValue: "FirePopoverCollectionViewHeader") + static let identifier = NSUserInterfaceItemIdentifier(rawValue: FirePopoverCollectionViewHeader.className()) + fileprivate static let size = NSSize(width: 200, height: 28) - @IBOutlet weak var title: NSTextField! + private(set) lazy var title = NSTextField(string: UserText.fireproofSites) + + override init(frame: NSRect = NSRect(origin: .zero, size: FirePopoverCollectionViewHeader.size)) { + super.init(frame: frame) + identifier = Self.identifier + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("FirePopoverCollectionViewHeader: Bad initializer") + } + + private func setupUI() { + addSubview(title) + + title.translatesAutoresizingMaskIntoConstraints = false + title.setContentHuggingPriority(.defaultHigh, for: .vertical) + title.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .horizontal) + title.isEditable = false + title.isSelectable = false + title.isBordered = false + title.drawsBackground = false + title.alignment = .left + title.font = .systemFont(ofSize: NSFont.smallSystemFontSize, weight: .light) + title.textColor = .secondaryLabelColor + + setupLayout() + } + + private func setupLayout() { + NSLayoutConstraint.activate([ + title.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 4), + title.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), + trailingAnchor.constraint(equalTo: title.trailingAnchor, constant: 8), + ]) + } override func mouseDown(with event: NSEvent) {} } + +@available(macOS 14.0, *) +#Preview(traits: FirePopoverCollectionViewHeader.size.scaled(by: 1.5).fixedLayout) { { + let vc = NSViewController() + vc.view = NSView(frame: NSRect(origin: .zero, size: FirePopoverCollectionViewHeader.size.scaled(by: 1.5))) + let header = FirePopoverCollectionViewHeader() + header.translatesAutoresizingMaskIntoConstraints = true + header.frame = NSRect(origin: NSPoint(x: (vc.view.frame.size.width - FirePopoverCollectionViewHeader.size.width) / 2, y: (vc.view.frame.size.height - FirePopoverCollectionViewHeader.size.height) / 2), size: FirePopoverCollectionViewHeader.size) + header.wantsLayer = true + header.layer!.backgroundColor = NSColor.fireBackground.cgColor + vc.view.addSubview(header) + return vc._preview_hidingWindowControlsOnAppear() +}() } diff --git a/DuckDuckGo/Fire/View/FirePopoverCollectionViewHeader.xib b/DuckDuckGo/Fire/View/FirePopoverCollectionViewHeader.xib deleted file mode 100644 index 84890c505f..0000000000 --- a/DuckDuckGo/Fire/View/FirePopoverCollectionViewHeader.xib +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DuckDuckGo/Fire/View/FirePopoverCollectionViewItem.swift b/DuckDuckGo/Fire/View/FirePopoverCollectionViewItem.swift index 6605521d21..4a10fcb7f5 100644 --- a/DuckDuckGo/Fire/View/FirePopoverCollectionViewItem.swift +++ b/DuckDuckGo/Fire/View/FirePopoverCollectionViewItem.swift @@ -26,17 +26,80 @@ protocol FirePopoverCollectionViewItemDelegate: AnyObject { final class FirePopoverCollectionViewItem: NSCollectionViewItem { - static let identifier = NSUserInterfaceItemIdentifier(rawValue: "FirePopoverCollectionViewItem") + static let identifier = NSUserInterfaceItemIdentifier(rawValue: FirePopoverCollectionViewItem.className()) + fileprivate static let size = NSSize(width: 265, height: 24) weak var delegate: FirePopoverCollectionViewItemDelegate? - @IBOutlet weak var domainTextField: NSTextField! - @IBOutlet weak var checkButton: NSButton! - @IBOutlet weak var faviconImageView: NSImageView! { - didSet { - faviconImageView.applyFaviconStyle() - } - } + private lazy var domainTextField = NSTextField(string: "exampledomain.com") + private lazy var checkButton = NSButton(title: "", target: self, action: #selector(checkButtonAction)) + private lazy var faviconImageView = NSImageView(image: .web) + private lazy var stackView = NSStackView() + + override init(nibName: String? = nil, bundle: Bundle? = nil) { + super.init(nibName: nil, bundle: nil) + identifier = Self.identifier + } + + required init?(coder: NSCoder) { + fatalError("FirePopoverCollectionViewItem: Bad initializer") + } + + override func loadView() { + view = NSView(frame: NSRect(origin: .zero, size: Self.size)) + + stackView.addArrangedSubview(checkButton) + stackView.addArrangedSubview(faviconImageView) + stackView.addArrangedSubview(domainTextField) + + stackView.alignment = .centerY + stackView.detachesHiddenViews = true + stackView.distribution = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + + domainTextField.translatesAutoresizingMaskIntoConstraints = false + domainTextField.setContentHuggingPriority(.defaultHigh, for: .vertical) + domainTextField.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .horizontal) + domainTextField.isEditable = false + domainTextField.isSelectable = false + domainTextField.isBordered = false + domainTextField.drawsBackground = false + domainTextField.font = .systemFont(ofSize: 13) + domainTextField.lineBreakMode = .byClipping + domainTextField.textColor = .labelColor + + faviconImageView.translatesAutoresizingMaskIntoConstraints = false + faviconImageView.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .horizontal) + faviconImageView.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .vertical) + faviconImageView.alignment = .left + faviconImageView.imageScaling = .scaleProportionallyDown + faviconImageView.applyFaviconStyle() + + checkButton.translatesAutoresizingMaskIntoConstraints = false + checkButton.setContentHuggingPriority(.defaultHigh, for: .vertical) + checkButton.setButtonType(.switch) + checkButton.bezelStyle = .regularSquare + checkButton.font = .systemFont(ofSize: 13) + + view.addSubview(stackView) + + setupLayout(stackView: stackView) + } + + private func setupLayout(stackView: NSStackView) { + NSLayoutConstraint.activate([ + view.trailingAnchor.constraint(equalTo: stackView.trailingAnchor), + stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + stackView.topAnchor.constraint(equalTo: view.topAnchor), + view.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), + + faviconImageView.widthAnchor.constraint(equalToConstant: 16), + faviconImageView.heightAnchor.constraint(equalToConstant: 16), + + checkButton.heightAnchor.constraint(equalToConstant: 14), + checkButton.widthAnchor.constraint(equalToConstant: 14), + ]) + } func setItem(_ item: FirePopoverViewModel.Item, isFireproofed: Bool) { domainTextField.stringValue = item.domain @@ -44,7 +107,7 @@ final class FirePopoverCollectionViewItem: NSCollectionViewItem { checkButton.isHidden = isFireproofed } - @IBAction func checkButtonAction(_ sender: Any) { + @objc func checkButtonAction(_ sender: Any) { delegate?.firePopoverCollectionViewItemDidToggle(self) } @@ -59,3 +122,17 @@ final class FirePopoverCollectionViewItem: NSCollectionViewItem { } } + +@available(macOS 14.0, *) +#Preview(traits: FirePopoverCollectionViewItem.size.scaled(by: 1.5).fixedLayout) { { + let vc = NSViewController() + vc.view = NSView(frame: NSRect(origin: .zero, size: FirePopoverCollectionViewItem.size.scaled(by: 1.5))) + let cell = FirePopoverCollectionViewItem() + cell.view.translatesAutoresizingMaskIntoConstraints = true + cell.view.frame = NSRect(origin: NSPoint(x: (vc.view.frame.size.width - FirePopoverCollectionViewItem.size.width) / 2, y: (vc.view.frame.size.height - FirePopoverCollectionViewItem.size.height) / 2), size: FirePopoverCollectionViewItem.size) + cell.view.wantsLayer = true + cell.view.layer!.backgroundColor = NSColor.fireBackground.cgColor + vc.view.addSubview(cell.view) + vc.addChild(cell) + return vc._preview_hidingWindowControlsOnAppear() +}() } diff --git a/DuckDuckGo/Fire/View/FirePopoverCollectionViewItem.xib b/DuckDuckGo/Fire/View/FirePopoverCollectionViewItem.xib deleted file mode 100644 index d68d46a100..0000000000 --- a/DuckDuckGo/Fire/View/FirePopoverCollectionViewItem.xib +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DuckDuckGo/Fire/View/FirePopoverViewController.swift b/DuckDuckGo/Fire/View/FirePopoverViewController.swift index 5b13ccd6ad..c8934fecac 100644 --- a/DuckDuckGo/Fire/View/FirePopoverViewController.swift +++ b/DuckDuckGo/Fire/View/FirePopoverViewController.swift @@ -44,29 +44,31 @@ final class FirePopoverViewController: NSViewController { private var firePopoverViewModel: FirePopoverViewModel private let historyCoordinating: HistoryCoordinating - @IBOutlet weak var closeTabsLabel: NSTextField! - @IBOutlet weak var openFireWindowsTitleLabel: NSTextField! - @IBOutlet weak var fireWindowDescriptionLabel: NSTextField! - @IBOutlet weak var headerWrapperView: NSView! - @IBOutlet weak var mainButtonsToBurnerWindowContraint: NSLayoutConstraint! - @IBOutlet weak var infoLabel: NSTextField! - @IBOutlet weak var optionsButton: NSPopUpButton! - @IBOutlet weak var optionsButtonWidthConstraint: NSLayoutConstraint! - @IBOutlet weak var openDetailsButton: NSButton! - @IBOutlet weak var openDetailsButtonImageView: NSImageView! - @IBOutlet weak var closeDetailsButton: NSButton! - @IBOutlet weak var detailsWrapperView: NSView! - @IBOutlet weak var contentHeightConstraint: NSLayoutConstraint! - @IBOutlet weak var detailsWrapperViewHeightContraint: NSLayoutConstraint! - @IBOutlet weak var openWrapperView: NSView! - @IBOutlet weak var closeWrapperView: NSView! - @IBOutlet weak var collectionView: NSCollectionView! - @IBOutlet weak var collectionViewBottomConstraint: NSLayoutConstraint! - @IBOutlet weak var warningWrapperView: NSView! - @IBOutlet weak var warningButton: NSButton! - @IBOutlet weak var clearButton: NSButton! - @IBOutlet weak var cancelButton: NSButton! - @IBOutlet weak var closeBurnerWindowButton: NSButton! + private lazy var viewMainButtonsWrapperView = ColorView(frame: .zero, backgroundColor: .interfaceBackground) + private lazy var closeTabsLabel = NSTextField(string: UserText.fireDialogCloseTabs) + private lazy var openFireWindowsTitleLabel = NSTextField(string: UserText.fireDialogFireWindowTitle) + private lazy var fireWindowDescriptionLabel = NSTextField(string: UserText.fireDialogFireWindowDescription) + private lazy var headerWrapperView = NSView() + private lazy var infoLabel = NSTextField() + private lazy var optionsButton = NSPopUpButton(title: UserText.allData, target: self, action: #selector(optionsButtonAction)) + private var optionsButtonWidthConstraint: NSLayoutConstraint! + private lazy var openDetailsButton = MouseOverButton(title: " " + UserText.details, target: self, action: #selector(openDetailsButtonAction)) + private lazy var openDetailsButtonImageView = NSImageView() + private lazy var closeDetailsButton = MouseOverButton(title: " " + UserText.fireDialogCloseAllTabsAndClear, target: self, action: #selector(closeDetailsButtonAction)) + private lazy var detailsWrapperView = NSView() + private var contentHeightConstraint: NSLayoutConstraint! + private var detailsWrapperViewHeightContraint: NSLayoutConstraint! + private lazy var openWrapperView = NSView() + private lazy var closeWrapperView = ColorView(frame: NSRect(x: 0, y: 0, width: 344, height: 42), backgroundColor: .firePopoverPanelBackground) + private lazy var scrollView = NSScrollView() + private lazy var collectionView = NSCollectionView() + private var collectionViewBottomConstraint: NSLayoutConstraint! + private lazy var warningWrapperView = ColorView(frame: NSRect(x: 0, y: 0, width: 344, height: 32), backgroundColor: .firePopoverPanelBackground) + private lazy var warningButton = NSButton(image: .warningTriangle, target: nil, action: nil) + private lazy var clearButton = NSButton(title: UserText.clear, target: self, action: #selector(clearButtonAction)) + private lazy var cancelButton = NSButton(title: UserText.cancel, target: self, action: #selector(cancelButtonAction)) + private var mainButtonsToBurnerWindowContraint: NSLayoutConstraint! + private lazy var closeBurnerWindowButton = NSButton(title: UserText.fireDialogBurnWindowButton, target: self, action: #selector(closeBurnerWindowButtonAction)) private var viewModelCancellable: AnyCancellable? private var selectedCancellable: AnyCancellable? @@ -75,12 +77,11 @@ final class FirePopoverViewController: NSViewController { fatalError("FirePopoverViewController: Bad initializer") } - init?(coder: NSCoder, - fireViewModel: FireViewModel, - tabCollectionViewModel: TabCollectionViewModel, - historyCoordinating: HistoryCoordinating = HistoryCoordinator.shared, - fireproofDomains: FireproofDomains = FireproofDomains.shared, - faviconManagement: FaviconManagement = FaviconManager.shared) { + init(fireViewModel: FireViewModel, + tabCollectionViewModel: TabCollectionViewModel, + historyCoordinating: HistoryCoordinating = HistoryCoordinator.shared, + fireproofDomains: FireproofDomains = FireproofDomains.shared, + faviconManagement: FaviconManagement = FaviconManager.shared) { self.fireViewModel = fireViewModel self.historyCoordinating = historyCoordinating self.firePopoverViewModel = FirePopoverViewModel(fireViewModel: fireViewModel, @@ -91,14 +92,436 @@ final class FirePopoverViewController: NSViewController { tld: ContentBlocking.shared.tld, contextualOnboardingStateMachine: Application.appDelegate.onboardingStateMachine) - super.init(coder: coder) + super.init(nibName: nil, bundle: nil) + } + + override func loadView() { + // MARK: Open New Fire Window (header) + + let openFireWindowContainerView = NSView() + openFireWindowContainerView.translatesAutoresizingMaskIntoConstraints = false + + let iconView = NSImageView(image: .burnerWindowButtonIcon) + iconView.translatesAutoresizingMaskIntoConstraints = false + iconView.contentTintColor = .labelColor + iconView.imageScaling = .scaleProportionallyDown + iconView.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + iconView.setContentHuggingPriority(.init(rawValue: 251), for: .vertical) + + openFireWindowsTitleLabel.translatesAutoresizingMaskIntoConstraints = false + openFireWindowsTitleLabel.isEditable = false + openFireWindowsTitleLabel.isBordered = false + openFireWindowsTitleLabel.isSelectable = false + openFireWindowsTitleLabel.drawsBackground = false + openFireWindowsTitleLabel.setContentCompressionResistancePriority(.required, for: .vertical) + openFireWindowsTitleLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) + openFireWindowsTitleLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + openFireWindowsTitleLabel.font = .systemFont(ofSize: 13) + openFireWindowsTitleLabel.textColor = .labelColor + + fireWindowDescriptionLabel.translatesAutoresizingMaskIntoConstraints = false + fireWindowDescriptionLabel.isEditable = false + fireWindowDescriptionLabel.isBordered = false + fireWindowDescriptionLabel.isSelectable = false + fireWindowDescriptionLabel.drawsBackground = false + fireWindowDescriptionLabel.setContentCompressionResistancePriority(.required, for: .vertical) + fireWindowDescriptionLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + fireWindowDescriptionLabel.font = .systemFont(ofSize: NSFont.smallSystemFontSize, weight: .regular) + fireWindowDescriptionLabel.textColor = .secondaryLabelColor + + let fireWindowMouseOverButton = MouseOverButton(target: self, action: #selector(openNewBurnerWindowAction)) + fireWindowMouseOverButton.translatesAutoresizingMaskIntoConstraints = false + fireWindowMouseOverButton.cornerRadius = 4 + fireWindowMouseOverButton.mouseDownColor = .buttonMouseDown + fireWindowMouseOverButton.mouseOverColor = .buttonMouseOver + + openFireWindowContainerView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + openFireWindowContainerView.setContentHuggingPriority(.defaultHigh, for: .vertical) + + openFireWindowContainerView.addSubview(iconView) + openFireWindowContainerView.addSubview(openFireWindowsTitleLabel) + openFireWindowContainerView.addSubview(fireWindowDescriptionLabel) + openFireWindowContainerView.addSubview(fireWindowMouseOverButton) + + // MARK: Header Image, Options View + + let headerSeparator = NSBox() + headerSeparator.translatesAutoresizingMaskIntoConstraints = false + headerSeparator.boxType = .separator + headerSeparator.setContentHuggingPriority(.defaultHigh, for: .vertical) + + let fireHeaderImageView = NSImageView(image: .fireHeader) + fireHeaderImageView.translatesAutoresizingMaskIntoConstraints = false + fireHeaderImageView.imageScaling = .scaleProportionallyDown + fireHeaderImageView.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + fireHeaderImageView.setContentHuggingPriority(.init(rawValue: 251), for: .vertical) + + infoLabel.translatesAutoresizingMaskIntoConstraints = false + infoLabel.isEditable = false + infoLabel.isBordered = false + infoLabel.isSelectable = false + infoLabel.drawsBackground = false + infoLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) + infoLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + infoLabel.alignment = .center + infoLabel.font = .systemFont(ofSize: NSFont.smallSystemFontSize, + weight: .regular) + infoLabel.textColor = .secondaryLabelColor + + optionsButton.translatesAutoresizingMaskIntoConstraints = false + optionsButton.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + optionsButton.setContentHuggingPriority(.required, for: .horizontal) + optionsButton.alignment = .center + optionsButton.bezelStyle = .regularSquare + optionsButton.font = .menuFont(ofSize: 13) + optionsButton.lineBreakMode = .byTruncatingTail + optionsButton.cell?.isBordered = true + optionsButton.cell?.state = .on + optionsButton.cell?.tag = 2 + + closeTabsLabel.translatesAutoresizingMaskIntoConstraints = false + closeTabsLabel.isEditable = false + closeTabsLabel.isBordered = false + closeTabsLabel.isSelectable = false + closeTabsLabel.drawsBackground = false + closeTabsLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) + closeTabsLabel.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + closeTabsLabel.font = .systemFont(ofSize: 13) + closeTabsLabel.lineBreakMode = .byClipping + closeTabsLabel.textColor = .labelColor + + headerWrapperView.translatesAutoresizingMaskIntoConstraints = false + headerWrapperView.addSubview(headerSeparator) + headerWrapperView.addSubview(fireHeaderImageView) + headerWrapperView.addSubview(closeTabsLabel) + headerWrapperView.addSubview(optionsButton) + headerWrapperView.addSubview(infoLabel) + + // MARK: Open Details button + + openDetailsButtonImageView.translatesAutoresizingMaskIntoConstraints = false + openDetailsButtonImageView.contentTintColor = .greyText + openDetailsButtonImageView.setContentHuggingPriority(.init(rawValue: 251), for: .horizontal) + openDetailsButtonImageView.setContentHuggingPriority(.init(rawValue: 251), for: .vertical) + openDetailsButtonImageView.alignment = .left + openDetailsButtonImageView.image = .expandDownPadding + openDetailsButtonImageView.imageScaling = .scaleProportionallyDown + + openDetailsButton.translatesAutoresizingMaskIntoConstraints = false + openDetailsButton.normalTintColor = .greyText + openDetailsButton.alignment = .center + openDetailsButton.bezelStyle = .shadowlessSquare + openDetailsButton.font = .systemFont(ofSize: NSFont.smallSystemFontSize, + weight: .regular) + openDetailsButton.mouseDownColor = .buttonMouseDownColorLight + openDetailsButton.mouseOverColor = .buttonMouseOverColorLight + + openWrapperView.translatesAutoresizingMaskIntoConstraints = false + openWrapperView.addSubview(openDetailsButtonImageView) + openWrapperView.addSubview(openDetailsButton) + + // MARK: Details (Collection View) + + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.autohidesScrollers = true + scrollView.borderType = .noBorder + scrollView.hasHorizontalScroller = false + scrollView.horizontalLineScroll = 10 + scrollView.horizontalPageScroll = 10 + scrollView.usesPredominantAxisScrolling = false + scrollView.verticalLineScroll = 10 + scrollView.verticalPageScroll = 10 + scrollView.wantsLayer = true + + let clipView = NSClipView() + clipView.documentView = collectionView + + clipView.autoresizingMask = [.width, .height] + clipView.backgroundColor = .interfaceBackground + clipView.drawsBackground = false + clipView.frame = CGRect(x: 0, y: 0, width: 344, height: 221) + + let collectionViewLayout = NSCollectionViewFlowLayout() + collectionViewLayout.itemSize = CGSize(width: 318, height: 24) + + collectionView.collectionViewLayout = collectionViewLayout + collectionView.allowsMultipleSelection = true + collectionView.autoresizingMask = [.width] + collectionView.backgroundColors = [.firePopoverListBackground] + collectionView.frame = CGRect(x: 0, y: 0, width: 344, height: 221) + collectionView.isSelectable = true + + scrollView.contentView = clipView + + detailsWrapperView.translatesAutoresizingMaskIntoConstraints = false + detailsWrapperView.addSubview(scrollView) + + detailsWrapperView.isHidden = true + + // MARK: Close Details button + + let closeDetailsSeparator1 = NSBox(frame: CGRect(x: 0, y: 39, width: 344, height: 5)) + closeDetailsSeparator1.autoresizingMask = [.maxXMargin, .minYMargin] + closeDetailsSeparator1.boxType = .separator + closeDetailsSeparator1.setContentHuggingPriority(.defaultHigh, for: .vertical) + + let closeDetailsSeparator2 = NSBox(frame: CGRect(x: 0, y: -2, width: 344, height: 5)) + closeDetailsSeparator2.autoresizingMask = [.maxXMargin, .minYMargin] + closeDetailsSeparator2.boxType = .separator + closeDetailsSeparator2.setContentHuggingPriority(.defaultHigh, for: .vertical) + + closeDetailsButton.normalTintColor = .greyText + closeDetailsButton.translatesAutoresizingMaskIntoConstraints = false + closeDetailsButton.alignment = .left + closeDetailsButton.bezelStyle = .shadowlessSquare + closeDetailsButton.font = .systemFont(ofSize: NSFont.smallSystemFontSize, + weight: .regular) + closeDetailsButton.image = .condenseUpPadding + closeDetailsButton.imagePosition = .imageTrailing + closeDetailsButton.backgroundInset = CGPoint(x: -6, y: 0.0) + closeDetailsButton.mouseDownColor = .buttonMouseDownColorLight + closeDetailsButton.mouseOverColor = .buttonMouseOverColorLight + + closeWrapperView.translatesAutoresizingMaskIntoConstraints = false + closeWrapperView.addSubview(closeDetailsSeparator1) + closeWrapperView.addSubview(closeDetailsSeparator2) + closeWrapperView.addSubview(closeDetailsButton) + + detailsWrapperView.addSubview(closeWrapperView) + + // MARK: Warning Wrapper View + + let warningSeparator1 = NSBox(frame: CGRect(x: 0, y: 29, width: 320, height: 5)) + warningSeparator1.autoresizingMask = [.maxXMargin, .minYMargin] + warningSeparator1.boxType = .separator + warningSeparator1.setContentHuggingPriority(.defaultHigh, for: .vertical) + + let warningSeparator2 = NSBox(frame: CGRect(x: 0, y: -12, width: 320, height: 5)) + warningSeparator2.autoresizingMask = [.maxXMargin, .minYMargin] + warningSeparator2.boxType = .separator + warningSeparator2.setContentHuggingPriority(.defaultHigh, for: .vertical) + + warningButton.autoresizingMask = [.maxXMargin, .minYMargin] + warningButton.contentTintColor = .greyText + warningButton.frame = CGRect(x: 20, y: 0, width: 304, height: 32) + warningButton.alignment = .left + warningButton.bezelStyle = .shadowlessSquare + warningButton.isBordered = false + warningButton.font = .systemFont(ofSize: NSFont.smallSystemFontSize, + weight: .regular) + warningButton.image = .warningTriangle + warningButton.imagePosition = .imageLeading + + warningWrapperView.translatesAutoresizingMaskIntoConstraints = false + warningWrapperView.addSubview(warningSeparator1) + warningWrapperView.addSubview(warningSeparator2) + warningWrapperView.addSubview(warningButton) + + // MARK: View Main Buttons (footer) + + let separator = NSBox() + separator.translatesAutoresizingMaskIntoConstraints = false + separator.boxType = .separator + separator.setContentHuggingPriority(.defaultHigh, for: .vertical) + + cancelButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.setContentHuggingPriority(.defaultHigh, for: .vertical) + cancelButton.alignment = .center + cancelButton.bezelStyle = .rounded + cancelButton.controlSize = .large + cancelButton.font = .systemFont(ofSize: 13) + cancelButton.imageScaling = .scaleProportionallyDown + cancelButton.cell?.isBordered = true + + clearButton.translatesAutoresizingMaskIntoConstraints = false + clearButton.setContentHuggingPriority(.defaultHigh, for: .vertical) + clearButton.alignment = .center + clearButton.bezelStyle = .rounded + clearButton.controlSize = .large + clearButton.font = .systemFont(ofSize: 13) + clearButton.imageScaling = .scaleProportionallyDown + clearButton.cell?.isBordered = true + + closeBurnerWindowButton.translatesAutoresizingMaskIntoConstraints = false + closeBurnerWindowButton.setContentHuggingPriority(.defaultHigh, for: .vertical) + closeBurnerWindowButton.isHidden = true + closeBurnerWindowButton.alignment = .center + closeBurnerWindowButton.bezelStyle = .rounded + closeBurnerWindowButton.controlSize = .large + closeBurnerWindowButton.font = .systemFont(ofSize: 13) + closeBurnerWindowButton.imageScaling = .scaleProportionallyDown + closeBurnerWindowButton.cell?.isBordered = true + + viewMainButtonsWrapperView.translatesAutoresizingMaskIntoConstraints = false + viewMainButtonsWrapperView.addSubview(separator) + viewMainButtonsWrapperView.addSubview(closeBurnerWindowButton) + viewMainButtonsWrapperView.addSubview(clearButton) + viewMainButtonsWrapperView.addSubview(cancelButton) + + // MARK: Container View + + view = ColorView(frame: NSRect(x: 0, y: 0, width: 344, height: 388), + backgroundColor: .interfaceBackground) + + view.addSubview(openFireWindowContainerView) + view.addSubview(headerWrapperView) + view.addSubview(openWrapperView) + view.addSubview(detailsWrapperView) + view.addSubview(warningWrapperView) + view.addSubview(viewMainButtonsWrapperView) + + setupOpenFireWindowLayout(openFireWindowContainerView: openFireWindowContainerView, iconView: iconView, fireWindowMouseOverButton: fireWindowMouseOverButton) + setupHeaderLayout(openFireWindowContainerView: openFireWindowContainerView, fireHeaderImageView: fireHeaderImageView, headerSeparator: headerSeparator) + setupButtonsLayout(openFireWindowContainerView: openFireWindowContainerView, separator: separator) + setupDetailsLayout() + } + + private func setupOpenFireWindowLayout(openFireWindowContainerView: NSView, iconView: NSImageView, fireWindowMouseOverButton: MouseOverButton) { + NSLayoutConstraint.activate([ + openFireWindowContainerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 84), + openFireWindowContainerView.topAnchor.constraint(equalTo: view.topAnchor), + openFireWindowContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + view.trailingAnchor.constraint(equalTo: openFireWindowContainerView.trailingAnchor), + + iconView.widthAnchor.constraint(equalToConstant: 32), + iconView.heightAnchor.constraint(equalToConstant: 32), + iconView.leadingAnchor.constraint(equalTo: openFireWindowContainerView.leadingAnchor, constant: 24), + iconView.topAnchor.constraint(equalTo: openFireWindowContainerView.topAnchor, constant: 26), + + openFireWindowsTitleLabel.topAnchor.constraint(equalTo: openFireWindowContainerView.topAnchor, constant: 25), + openFireWindowsTitleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 6), + openFireWindowContainerView.trailingAnchor.constraint(equalTo: openFireWindowsTitleLabel.trailingAnchor, constant: 20), + + fireWindowDescriptionLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 6), + openFireWindowContainerView.bottomAnchor.constraint(greaterThanOrEqualTo: fireWindowDescriptionLabel.bottomAnchor, constant: 16), + fireWindowDescriptionLabel.topAnchor.constraint(equalTo: openFireWindowsTitleLabel.bottomAnchor, constant: 2), + openFireWindowContainerView.trailingAnchor.constraint(equalTo: fireWindowDescriptionLabel.trailingAnchor, constant: 20), + + fireWindowMouseOverButton.topAnchor.constraint(equalTo: openFireWindowContainerView.topAnchor, constant: 16), + fireWindowMouseOverButton.leadingAnchor.constraint(equalTo: openFireWindowContainerView.leadingAnchor, constant: 20), + openFireWindowContainerView.trailingAnchor.constraint(equalTo: fireWindowMouseOverButton.trailingAnchor, constant: 20), + openFireWindowContainerView.bottomAnchor.constraint(equalTo: fireWindowMouseOverButton.bottomAnchor, constant: 8), + fireWindowMouseOverButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 52), + ]) + } + + private func setupHeaderLayout(openFireWindowContainerView: NSView, fireHeaderImageView: NSImageView, headerSeparator: NSBox) { + contentHeightConstraint = viewMainButtonsWrapperView.topAnchor.constraint(equalTo: headerWrapperView.bottomAnchor, constant: 42) + + NSLayoutConstraint.activate([ + view.trailingAnchor.constraint(equalTo: headerWrapperView.trailingAnchor), + headerWrapperView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + + headerWrapperView.topAnchor.constraint(equalTo: openFireWindowContainerView.bottomAnchor), + optionsButton.leadingAnchor.constraint(equalTo: headerWrapperView.leadingAnchor, constant: 55), + optionsButton.topAnchor.constraint(equalTo: closeTabsLabel.bottomAnchor, constant: 8), + headerWrapperView.heightAnchor.constraint(equalToConstant: 202), + infoLabel.centerXAnchor.constraint(equalTo: headerWrapperView.centerXAnchor), + headerWrapperView.trailingAnchor.constraint(equalTo: optionsButton.trailingAnchor, constant: 54), + fireHeaderImageView.topAnchor.constraint(equalTo: headerWrapperView.topAnchor, constant: 20), + infoLabel.topAnchor.constraint(equalTo: optionsButton.bottomAnchor, constant: 8), + closeTabsLabel.topAnchor.constraint(equalTo: fireHeaderImageView.bottomAnchor, constant: 15), + headerWrapperView.trailingAnchor.constraint(equalTo: headerSeparator.trailingAnchor, constant: 20), + headerWrapperView.trailingAnchor.constraint(equalTo: infoLabel.trailingAnchor, constant: 20), + fireHeaderImageView.centerXAnchor.constraint(equalTo: headerWrapperView.centerXAnchor), + headerSeparator.leadingAnchor.constraint(equalTo: headerWrapperView.leadingAnchor, constant: 20), + headerSeparator.topAnchor.constraint(equalTo: headerWrapperView.topAnchor), + infoLabel.leadingAnchor.constraint(equalTo: headerWrapperView.leadingAnchor, constant: 20), + closeTabsLabel.centerXAnchor.constraint(equalTo: headerWrapperView.centerXAnchor), + + infoLabel.widthAnchor.constraint(equalToConstant: 304), + infoLabel.heightAnchor.constraint(equalToConstant: 32), + + optionsButton.heightAnchor.constraint(equalToConstant: 30), + + fireHeaderImageView.widthAnchor.constraint(equalToConstant: 128), + fireHeaderImageView.heightAnchor.constraint(equalToConstant: 64), + + contentHeightConstraint, + ]) + } + + private func setupButtonsLayout(openFireWindowContainerView: NSView, separator: NSBox) { + mainButtonsToBurnerWindowContraint = viewMainButtonsWrapperView.topAnchor.constraint(equalTo: openFireWindowContainerView.bottomAnchor).priority(250) + + NSLayoutConstraint.activate([ + view.trailingAnchor.constraint(equalTo: viewMainButtonsWrapperView.trailingAnchor), + viewMainButtonsWrapperView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + view.bottomAnchor.constraint(equalTo: viewMainButtonsWrapperView.bottomAnchor), + + closeBurnerWindowButton.leadingAnchor.constraint(equalTo: viewMainButtonsWrapperView.leadingAnchor, constant: 20), + cancelButton.leadingAnchor.constraint(equalTo: viewMainButtonsWrapperView.leadingAnchor, constant: 20), + viewMainButtonsWrapperView.trailingAnchor.constraint(equalTo: closeBurnerWindowButton.trailingAnchor, constant: 20), + separator.topAnchor.constraint(equalTo: viewMainButtonsWrapperView.topAnchor), + cancelButton.centerYAnchor.constraint(equalTo: viewMainButtonsWrapperView.centerYAnchor), + viewMainButtonsWrapperView.trailingAnchor.constraint(equalTo: separator.trailingAnchor), + clearButton.centerYAnchor.constraint(equalTo: viewMainButtonsWrapperView.centerYAnchor), + clearButton.widthAnchor.constraint(equalTo: cancelButton.widthAnchor), + viewMainButtonsWrapperView.trailingAnchor.constraint(equalTo: clearButton.trailingAnchor, constant: 20), + viewMainButtonsWrapperView.bottomAnchor.constraint(equalTo: closeBurnerWindowButton.bottomAnchor, constant: 16), + clearButton.leadingAnchor.constraint(equalTo: cancelButton.trailingAnchor, constant: 8), + separator.leadingAnchor.constraint(equalTo: viewMainButtonsWrapperView.leadingAnchor), + viewMainButtonsWrapperView.heightAnchor.constraint(equalToConstant: 60), + + mainButtonsToBurnerWindowContraint, + ]) + } + + private func setupDetailsLayout() { + optionsButtonWidthConstraint = optionsButton.widthAnchor.constraint(equalToConstant: 235).priority(250) + detailsWrapperViewHeightContraint = detailsWrapperView.heightAnchor.constraint(equalToConstant: 263) + collectionViewBottomConstraint = detailsWrapperView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) + + NSLayoutConstraint.activate([ + openWrapperView.topAnchor.constraint(equalTo: headerWrapperView.bottomAnchor), + detailsWrapperView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + view.trailingAnchor.constraint(equalTo: detailsWrapperView.trailingAnchor), + detailsWrapperView.topAnchor.constraint(equalTo: headerWrapperView.bottomAnchor), + view.trailingAnchor.constraint(equalTo: openWrapperView.trailingAnchor), + openWrapperView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + + warningWrapperView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + viewMainButtonsWrapperView.topAnchor.constraint(equalTo: warningWrapperView.bottomAnchor), + view.trailingAnchor.constraint(equalTo: warningWrapperView.trailingAnchor), + warningWrapperView.heightAnchor.constraint(equalToConstant: 32), + + closeWrapperView.leadingAnchor.constraint(equalTo: detailsWrapperView.leadingAnchor), + scrollView.topAnchor.constraint(equalTo: closeWrapperView.bottomAnchor), + closeWrapperView.topAnchor.constraint(equalTo: detailsWrapperView.topAnchor), + detailsWrapperView.trailingAnchor.constraint(equalTo: closeWrapperView.trailingAnchor), + detailsWrapperView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + scrollView.leadingAnchor.constraint(equalTo: detailsWrapperView.leadingAnchor), + + closeDetailsButton.topAnchor.constraint(equalTo: closeWrapperView.topAnchor), + closeWrapperView.bottomAnchor.constraint(equalTo: closeDetailsButton.bottomAnchor), + closeDetailsButton.leadingAnchor.constraint(equalTo: closeWrapperView.leadingAnchor), + closeWrapperView.trailingAnchor.constraint(equalTo: closeDetailsButton.trailingAnchor), + closeWrapperView.heightAnchor.constraint(equalToConstant: 42), + + openWrapperView.bottomAnchor.constraint(equalTo: openDetailsButton.bottomAnchor), + openWrapperView.heightAnchor.constraint(equalToConstant: 42), + openWrapperView.trailingAnchor.constraint(equalTo: openDetailsButtonImageView.trailingAnchor), + openDetailsButton.leadingAnchor.constraint(equalTo: openWrapperView.leadingAnchor), + openWrapperView.trailingAnchor.constraint(equalTo: openDetailsButton.trailingAnchor), + openDetailsButtonImageView.centerYAnchor.constraint(equalTo: openWrapperView.centerYAnchor), + openDetailsButton.topAnchor.constraint(equalTo: openWrapperView.topAnchor), + + openDetailsButton.heightAnchor.constraint(equalToConstant: 42), + + openDetailsButtonImageView.heightAnchor.constraint(equalToConstant: 16), + openDetailsButtonImageView.widthAnchor.constraint(equalToConstant: 38), + + collectionViewBottomConstraint, + detailsWrapperViewHeightContraint, + optionsButtonWidthConstraint, + ]) } override func viewDidLoad() { super.viewDidLoad() - let nib = NSNib(nibNamed: "FirePopoverCollectionViewItem", bundle: nil) - collectionView.register(nib, forItemWithIdentifier: FirePopoverCollectionViewItem.identifier) + collectionView.register(FirePopoverCollectionViewItem.self, forItemWithIdentifier: FirePopoverCollectionViewItem.identifier) + collectionView.register(FirePopoverCollectionViewHeader.self, forSupplementaryViewOfKind: NSCollectionView.elementKindSectionHeader, withIdentifier: FirePopoverCollectionViewHeader.identifier) collectionView.delegate = self collectionView.dataSource = self @@ -107,8 +530,8 @@ final class FirePopoverViewController: NSViewController { return } - setUpStrings() updateClearButtonAppearance() + closeDetailsButton.isHidden = true setupOptionsButton() setupOpenCloseDetailsButton() updateWarningWrapperView() @@ -117,22 +540,19 @@ final class FirePopoverViewController: NSViewController { subscribeToSelected() } + override func viewDidLayout() { + super.viewDidLayout() + collectionView.nextKeyView = cancelButton + + } + override func viewWillAppear() { super.viewWillAppear() firePopoverViewModel.refreshItems() } - private func setUpStrings() { - openFireWindowsTitleLabel.stringValue = UserText.fireDialogFireWindowTitle - fireWindowDescriptionLabel.stringValue = UserText.fireDialogFireWindowDescription - closeTabsLabel.stringValue = UserText.fireDialogCloseTabs - closeBurnerWindowButton.title = UserText.fireDialogBurnWindowButton - clearButton.title = UserText.clear - cancelButton.title = UserText.cancel - } - - @IBAction func optionsButtonAction(_ sender: NSPopUpButton) { + @objc func optionsButtonAction(_ sender: NSPopUpButton) { guard let tag = sender.selectedItem?.tag, let clearingOption = FirePopoverViewModel.ClearingOption(rawValue: tag) else { assertionFailure("Clearing option for not found for the selected menu item") return @@ -141,19 +561,28 @@ final class FirePopoverViewController: NSViewController { updateWarningWrapperView() } - @IBAction func openNewBurnerWindowAction(_ sender: Any) { + @objc func openNewBurnerWindowAction(_ sender: Any) { NSApp.delegateTyped.newBurnerWindow(self) } - @IBAction func openDetailsButtonAction(_ sender: Any) { + @objc func openDetailsButtonAction(_ sender: NSButton) { + let isButtonFirstResponder = sender.isFirstResponder toggleDetails() + if isButtonFirstResponder { + closeDetailsButton.makeMeFirstResponder() + } } - @IBAction func closeDetailsButtonAction(_ sender: Any) { + @objc func closeDetailsButtonAction(_ sender: NSButton) { + let isButtonFirstResponder = sender.isFirstResponder toggleDetails() + collectionView.selectionIndexPaths = [] + if isButtonFirstResponder { + openDetailsButton.makeMeFirstResponder() + } } - @IBAction func closeBurnerWindowButtonAction(_ sender: Any) { + @objc func closeBurnerWindowButtonAction(_ sender: Any) { let windowControllersManager = WindowControllersManager.shared guard let tabCollectionViewModel = firePopoverViewModel.tabCollectionViewModel, let windowController = windowControllersManager.windowController(for: tabCollectionViewModel) else { @@ -257,13 +686,12 @@ final class FirePopoverViewController: NSViewController { collectionViewBottomConstraint.constant = warningWrapperView.isHidden ? 0 : 32 } - @IBAction func clearButtonAction(_ sender: Any) { + @objc func clearButtonAction(_ sender: Any) { delegate?.firePopoverViewControllerDidClear(self) firePopoverViewModel.burn() - } - @IBAction func cancelButtonAction(_ sender: Any) { + @objc func cancelButtonAction(_ sender: Any) { delegate?.firePopoverViewControllerDidCancel(self) } @@ -297,6 +725,7 @@ final class FirePopoverViewController: NSViewController { private func toggleDetails() { let showDetails = detailsWrapperView.isHidden openWrapperView.isHidden = showDetails + closeDetailsButton.isHidden = !showDetails detailsWrapperView.isHidden = !showDetails updateWarningWrapperView() @@ -304,6 +733,7 @@ final class FirePopoverViewController: NSViewController { } private func adjustContentHeight() { + // TODO: bug! when expanding and then selecting "current tab" with no history – layout breaks NSAnimationContext.runAnimationGroup { [self, contentHeight = contentHeight()] context in context.duration = 1/3 context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) @@ -369,14 +799,18 @@ extension FirePopoverViewController: NSCollectionViewDataSource { return section == firePopoverViewModel.selectableSectionIndex ? firePopoverViewModel.selectable.count: firePopoverViewModel.fireproofed.count } + private func modelItem(at indexPath: IndexPath) -> FirePopoverViewModel.Item { + let isSelectableSection = indexPath.section == firePopoverViewModel.selectableSectionIndex + let sectionList = isSelectableSection ? firePopoverViewModel.selectable : firePopoverViewModel.fireproofed + return sectionList[indexPath.item] + } + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { let item = collectionView.makeItem(withIdentifier: FirePopoverCollectionViewItem.identifier, for: indexPath) guard let firePopoverItem = item as? FirePopoverCollectionViewItem else { return item } firePopoverItem.delegate = self - let isSelectableSection = indexPath.section == firePopoverViewModel.selectableSectionIndex - let sectionList = isSelectableSection ? firePopoverViewModel.selectable: firePopoverViewModel.fireproofed - let listItem = sectionList[indexPath.item] + let listItem = self.modelItem(at: indexPath) firePopoverItem.setItem(listItem, isFireproofed: indexPath.section == firePopoverViewModel.fireproofedSectionIndex) return firePopoverItem } @@ -384,11 +818,8 @@ extension FirePopoverViewController: NSCollectionViewDataSource { func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, at indexPath: IndexPath) -> NSView { - // swiftlint:disable force_cast - let view = collectionView.makeSupplementaryView(ofKind: NSCollectionView.elementKindSectionHeader, - withIdentifier: FirePopoverCollectionViewHeader.identifier, - for: indexPath) as! FirePopoverCollectionViewHeader - // swiftlint:enable force_cast + // swiftlint:disable:next force_cast + let view = collectionView.makeSupplementaryView(ofKind: NSCollectionView.elementKindSectionHeader, withIdentifier: FirePopoverCollectionViewHeader.identifier, for: indexPath) as! FirePopoverCollectionViewHeader if indexPath.section == firePopoverViewModel.selectableSectionIndex { view.title.stringValue = UserText.fireDialogClearSites @@ -402,7 +833,6 @@ extension FirePopoverViewController: NSCollectionViewDataSource { } extension FirePopoverViewController: NSCollectionViewDelegate { - } extension FirePopoverViewController: NSCollectionViewDelegateFlowLayout { @@ -449,3 +879,86 @@ extension FirePopoverViewController: FirePopoverCollectionViewItemDelegate { } } +#if DEBUG +final class HistoryTabExtensionMock: TabExtension, HistoryExtensionProtocol { + + var historyEntries: [HistoryEntry] { + [ + .init(identifier: UUID(), url: .duckDuckGo, failedToLoad: false, numberOfTotalVisits: 1, lastVisit: .init(), visits: [], numberOfTrackersBlocked: 0, blockedTrackingEntities: [], trackersFound: false), + .init(identifier: UUID(), url: URL(string: "http://anothersearch.com")!, failedToLoad: false, numberOfTotalVisits: 1, lastVisit: .init(), visits: [], numberOfTrackersBlocked: 0, blockedTrackingEntities: [], trackersFound: false), + .init(identifier: UUID(), url: URL(string: "http://bit.li")!, failedToLoad: false, numberOfTotalVisits: 1, lastVisit: .init(), visits: [], numberOfTrackersBlocked: 0, blockedTrackingEntities: [], trackersFound: false), + ] + } + var localHistory: [Visit] { + historyEntries.map { Visit(date: .init(), identifier: nil, historyEntry: $0) } + } + func getPublicProtocol() -> HistoryExtensionProtocol { self } + +} + +@available(macOS 14.0, *) +#Preview("With History", traits: .fixedLayout(width: 344, height: 650)) { { + let historyExtensionMock = HistoryTabExtensionMock() + let extensionBuilder = TestTabExtensionsBuilder(load: [HistoryTabExtensionMock.self]) { builder in { _, _ in + builder.override { + historyExtensionMock + } + }} + + let tab = Tab(content: .newtab, extensionsBuilder: extensionBuilder) + let tabCollection = TabCollection(tabs: [tab]) + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) + + let vc = FirePopoverViewController(fireViewModel: FireViewModel(), tabCollectionViewModel: tabCollectionViewModel) + vc.onDeinit { + withExtendedLifetime(tabCollectionViewModel) {} + } + + return vc._preview_hidingWindowControlsOnAppear() + +}() } +// TODO: adjust +@available(macOS 14.0, *) +#Preview("Empty", traits: .fixedLayout(width: 344, height: 650)) { { + let historyExtensionMock = HistoryTabExtensionMock() + let extensionBuilder = TestTabExtensionsBuilder(load: [HistoryTabExtensionMock.self]) { builder in { _, _ in + builder.override { + historyExtensionMock + } + }} + + let tab = Tab(content: .newtab, extensionsBuilder: extensionBuilder) + let tabCollection = TabCollection(tabs: [tab]) + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) + + let vc = FirePopoverViewController(fireViewModel: FireViewModel(), tabCollectionViewModel: tabCollectionViewModel) + vc.onDeinit { + withExtendedLifetime(tabCollectionViewModel) {} + } + + return vc._preview_hidingWindowControlsOnAppear() + +}() } + +@available(macOS 14.0, *) +#Preview("Burner", traits: .fixedLayout(width: 344, height: 650)) { { + let historyExtensionMock = HistoryTabExtensionMock() + let extensionBuilder = TestTabExtensionsBuilder(load: [HistoryTabExtensionMock.self]) { builder in { _, _ in + builder.override { + historyExtensionMock + } + }} + + let tab = Tab(content: .newtab, extensionsBuilder: extensionBuilder) + let tabCollection = TabCollection(tabs: [tab]) + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: tabCollection) + + let vc = FirePopoverViewController(fireViewModel: FireViewModel(), tabCollectionViewModel: tabCollectionViewModel) + vc.onDeinit { + withExtendedLifetime(tabCollectionViewModel) {} + } + + return vc._preview_hidingWindowControlsOnAppear() + +}() } +#endif diff --git a/DuckDuckGo/Fire/View/FirePopoverWrapperViewController.swift b/DuckDuckGo/Fire/View/FirePopoverWrapperViewController.swift index a9fe010d06..5b508ef7a4 100644 --- a/DuckDuckGo/Fire/View/FirePopoverWrapperViewController.swift +++ b/DuckDuckGo/Fire/View/FirePopoverWrapperViewController.swift @@ -20,8 +20,21 @@ import Foundation final class FirePopoverWrapperViewController: NSViewController { - @IBOutlet weak var infoView: NSView! - @IBOutlet weak var popoverView: NSView! + private lazy var infoViewController: FireInfoViewController = { + let fireInfoViewController = FireInfoViewController() + fireInfoViewController.delegate = self + return fireInfoViewController + }() + + private lazy var popoverViewController: FirePopoverViewController? = { + guard let tabCollectionViewModel = tabCollectionViewModel else { + assertionFailure("Attempted to display Fire Popover without an associated TabCollectionViewModel") + return nil + } + let firePopoverViewController = FirePopoverViewController(fireViewModel: fireViewModel, tabCollectionViewModel: tabCollectionViewModel) + firePopoverViewController.delegate = self + return firePopoverViewController + }() @UserDefaultsWrapper(key: .fireInfoPresentedOnce, defaultValue: false) var infoPresentedOnce: Bool @@ -33,49 +46,24 @@ final class FirePopoverWrapperViewController: NSViewController { fatalError("FirePopoverWrapperViewController: Bad initializer") } - init?(coder: NSCoder, - fireViewModel: FireViewModel, - tabCollectionViewModel: TabCollectionViewModel) { + init(fireViewModel: FireViewModel, tabCollectionViewModel: TabCollectionViewModel) { self.fireViewModel = fireViewModel self.tabCollectionViewModel = tabCollectionViewModel - super.init(coder: coder) + super.init(nibName: nil, bundle: nil) } - @IBSegueAction func createFirePopoverViewController(_ coder: NSCoder) -> FirePopoverViewController? { - guard let tabCollectionViewModel = tabCollectionViewModel else { - assertionFailure("Attempted to display Fire Popover without an associated TabCollectionViewModel") - return nil - } - - let firePopoverViewController = FirePopoverViewController(coder: coder, - fireViewModel: fireViewModel, - tabCollectionViewModel: tabCollectionViewModel) - firePopoverViewController?.delegate = self - return firePopoverViewController - } - - @IBSegueAction func createFireInfoViewController(_ coder: NSCoder) -> FireInfoViewController? { - let fireInfoViewController = FireInfoViewController(coder: coder) - fireInfoViewController?.delegate = self - return fireInfoViewController + override func loadView() { + view = NSView() + guard let tabCollectionViewModel, let popoverViewController else { return } - } - - override func viewDidLoad() { - super.viewDidLoad() - - hideInfoContainerViewIfNeeded() - } + let infoIsVisible = !infoPresentedOnce && !tabCollectionViewModel.isBurner + self.addAndLayoutChild(popoverViewController) + popoverViewController.view.isHidden = infoIsVisible - private func hideInfoContainerViewIfNeeded() { - guard let tabCollectionViewModel else { - return + if infoIsVisible { + self.addAndLayoutChild(infoViewController) } - - let infoIsVisible = !infoPresentedOnce && !tabCollectionViewModel.isBurner - infoView.isHidden = !infoIsVisible - popoverView.isHidden = infoIsVisible } } @@ -84,7 +72,11 @@ extension FirePopoverWrapperViewController: FireInfoViewControllerDelegate { func fireInfoViewControllerDidConfirm(_ fireInfoViewController: FireInfoViewController) { infoPresentedOnce = true - hideInfoContainerViewIfNeeded() + + fireInfoViewController.removeFromParent() + fireInfoViewController.view.removeFromSuperview() + + popoverViewController?.view.isHidden = false } } @@ -100,3 +92,29 @@ extension FirePopoverWrapperViewController: FirePopoverViewControllerDelegate { } } + +@available(macOS 14.0, *) +#Preview("First time", traits: .fixedLayout(width: 344, height: 650)) { { + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [Tab(content: .newtab)])) + let vc = FirePopoverWrapperViewController(fireViewModel: FireViewModel(), tabCollectionViewModel: tabCollectionViewModel) + vc.infoPresentedOnce = false + + vc.onDeinit { + withExtendedLifetime(tabCollectionViewModel) {} + } + + return vc._preview_hidingWindowControlsOnAppear() +}() } + +@available(macOS 14.0, *) +#Preview("Info presented once", traits: .fixedLayout(width: 344, height: 650)) { { + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [Tab(content: .newtab)])) + let vc = FirePopoverWrapperViewController(fireViewModel: FireViewModel(), tabCollectionViewModel: tabCollectionViewModel) + vc.infoPresentedOnce = true + + vc.onDeinit { + withExtendedLifetime(tabCollectionViewModel) {} + } + + return vc._preview_hidingWindowControlsOnAppear() +}() } diff --git a/DuckDuckGo/Fire/View/FireViewController.swift b/DuckDuckGo/Fire/View/FireViewController.swift index fee0006b48..20cf5ab02e 100644 --- a/DuckDuckGo/Fire/View/FireViewController.swift +++ b/DuckDuckGo/Fire/View/FireViewController.swift @@ -31,34 +31,107 @@ final class FireViewController: NSViewController { private let tabCollectionViewModel: TabCollectionViewModel private var cancellables = Set() - private lazy var fireDialogViewController: FirePopoverViewController = { - let storyboard = NSStoryboard(name: "Fire", bundle: nil) - return storyboard.instantiateController(identifier: "FirePopoverViewController") - }() - - @IBOutlet weak var deletingDataLabel: NSTextField! - @IBOutlet weak var fakeFireButton: NSButton! - @IBOutlet weak var progressIndicatorWrapper: NSView! - @IBOutlet weak var progressIndicator: NSProgressIndicator! - @IBOutlet weak var progressIndicatorWrapperBG: NSView! + private lazy var deletingDataLabel = NSTextField(string: UserText.fireDialogDelitingData) + private lazy var fakeFireButton = NSButton(image: .burn, target: nil, action: nil) + private lazy var progressIndicatorWrapper = NSView() + private lazy var progressIndicator = NSProgressIndicator() + private lazy var progressIndicatorWrapperBG = ColorView(frame: .zero, backgroundColor: .fireBackground, cornerRadius: 8) + private var fireAnimationView: LottieAnimationView? private var fireAnimationViewLoadingTask: Task<(), Never>? - static func create(tabCollectionViewModel: TabCollectionViewModel, fireViewModel: FireViewModel? = nil) -> FireViewController { - NSStoryboard(name: "Fire", bundle: nil).instantiateInitialController { coder in - self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel, fireViewModel: fireViewModel) - }! + init(tabCollectionViewModel: TabCollectionViewModel, fireViewModel: FireViewModel? = nil) { + self.tabCollectionViewModel = tabCollectionViewModel + self.fireViewModel = fireViewModel ?? FireCoordinator.fireViewModel + + super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("TabBarViewController: Bad initializer") } - init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, fireViewModel: FireViewModel? = nil) { - self.tabCollectionViewModel = tabCollectionViewModel - self.fireViewModel = fireViewModel ?? FireCoordinator.fireViewModel + override func loadView() { + view = ColorView(frame: .zero, backgroundColor: .fireBackground) + + fakeFireButton.translatesAutoresizingMaskIntoConstraints = false + fakeFireButton.contentTintColor = .button + fakeFireButton.alignment = .center + fakeFireButton.bezelStyle = .shadowlessSquare + fakeFireButton.isBordered = false + fakeFireButton.imagePosition = .imageOnly + fakeFireButton.imageScaling = .scaleProportionallyDown + fakeFireButton.wantsLayer = true + fakeFireButton.layer?.backgroundColor = NSColor.buttonMouseDown.cgColor + fakeFireButton.layer?.cornerRadius = 4 + fakeFireButton.setAccessibilityIdentifier("FireViewController.fakeFireButton") + + progressIndicatorWrapperBG.translatesAutoresizingMaskIntoConstraints = false + + let imageView = NSImageView(image: .burnAlert) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .horizontal) + imageView.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .vertical) + imageView.imageScaling = .scaleProportionallyDown + + deletingDataLabel.translatesAutoresizingMaskIntoConstraints = false + deletingDataLabel.setContentHuggingPriority(.defaultHigh, for: .vertical) + deletingDataLabel.setContentHuggingPriority(NSLayoutConstraint.Priority(rawValue: 251), for: .horizontal) + deletingDataLabel.isEditable = false + deletingDataLabel.isBordered = false + deletingDataLabel.isSelectable = false + deletingDataLabel.drawsBackground = false + deletingDataLabel.font = .systemFont(ofSize: NSFont.smallSystemFontSize, weight: .regular) + deletingDataLabel.lineBreakMode = .byClipping + deletingDataLabel.textColor = .labelColor + + progressIndicator.isIndeterminate = true + progressIndicator.maxValue = 100 + progressIndicator.style = .bar + progressIndicator.translatesAutoresizingMaskIntoConstraints = false + + progressIndicatorWrapper.translatesAutoresizingMaskIntoConstraints = false + progressIndicatorWrapper.setCornerRadius(8) + progressIndicatorWrapper.addSubview(progressIndicatorWrapperBG) + progressIndicatorWrapper.addSubview(progressIndicator) + progressIndicatorWrapper.addSubview(imageView) + progressIndicatorWrapper.addSubview(deletingDataLabel) + + view.addSubview(progressIndicatorWrapper) + view.addSubview(fakeFireButton) + + setupLayout(imageView: imageView) + } - super.init(coder: coder) + private func setupLayout(imageView: NSImageView) { + NSLayoutConstraint.activate([ + fakeFireButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 6), + progressIndicatorWrapper.centerYAnchor.constraint(equalTo: view.centerYAnchor), + progressIndicatorWrapper.centerXAnchor.constraint(equalTo: view.centerXAnchor), + view.trailingAnchor.constraint(equalTo: fakeFireButton.trailingAnchor, constant: 12), + + fakeFireButton.heightAnchor.constraint(equalToConstant: 28), + fakeFireButton.widthAnchor.constraint(equalToConstant: 28), + + imageView.centerXAnchor.constraint(equalTo: progressIndicatorWrapper.centerXAnchor), + progressIndicator.centerYAnchor.constraint(equalTo: progressIndicatorWrapper.centerYAnchor, constant: 13), + deletingDataLabel.centerXAnchor.constraint(equalTo: progressIndicatorWrapper.centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: progressIndicatorWrapper.centerYAnchor, constant: -40), + progressIndicatorWrapper.bottomAnchor.constraint(equalTo: progressIndicatorWrapperBG.bottomAnchor, constant: 10), + progressIndicatorWrapper.heightAnchor.constraint(equalToConstant: 220), + progressIndicator.centerXAnchor.constraint(equalTo: progressIndicatorWrapper.centerXAnchor), + progressIndicatorWrapper.widthAnchor.constraint(equalToConstant: 320), + deletingDataLabel.centerYAnchor.constraint(equalTo: progressIndicatorWrapper.centerYAnchor, constant: 34), + progressIndicatorWrapperBG.leadingAnchor.constraint(equalTo: progressIndicatorWrapper.leadingAnchor, constant: 10), + progressIndicatorWrapperBG.topAnchor.constraint(equalTo: progressIndicatorWrapper.topAnchor, constant: 10), + progressIndicatorWrapper.trailingAnchor.constraint(equalTo: progressIndicatorWrapperBG.trailingAnchor, constant: 10), + + imageView.widthAnchor.constraint(equalToConstant: 48), + imageView.heightAnchor.constraint(equalToConstant: 48), + + progressIndicator.widthAnchor.constraint(equalToConstant: 210), + progressIndicator.heightAnchor.constraint(equalToConstant: 18), + ]) } deinit { @@ -67,7 +140,6 @@ final class FireViewController: NSViewController { override func viewDidLoad() { super.viewDidLoad() - deletingDataLabel.stringValue = UserText.fireDialogDelitingData if case .normal = NSApp.runType { fireAnimationViewLoadingTask = Task.detached(priority: .userInitiated) { await self.setupFireAnimationView() @@ -116,10 +188,6 @@ final class FireViewController: NSViewController { animationView.animationSpeed = fireAnimationSpeed - fakeFireButton.wantsLayer = true - fakeFireButton.layer?.backgroundColor = NSColor.buttonMouseDown.cgColor - - fakeFireButton.setAccessibilityIdentifier("FireViewController.fakeFireButton") subscribeToIsBurning() } @@ -143,10 +211,6 @@ final class FireViewController: NSViewController { .store(in: &cancellables) } - func showDialog() { - presentAsModalWindow(fireDialogViewController) - } - private let fireAnimationSpeed = 1.2 private let fireAnimationBeginning = 0.1 private let fireAnimationEnd = 0.63 @@ -245,3 +309,14 @@ private actor FireAnimationViewLoader { LottieAnimation.named(animationName, animationCache: LottieAnimationCache.shared) } } + +@available(macOS 14.0, *) +#Preview { { + let tabCollectionViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [Tab(content: .newtab)])) + let vc = FireViewController(tabCollectionViewModel: tabCollectionViewModel, fireViewModel: FireViewModel()) + vc.onDeinit { + withExtendedLifetime(tabCollectionViewModel) {} + } + return vc + +}() } diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index a96916fab8..ff0d023e39 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -24356,6 +24356,18 @@ } } }, + "fire.dialog.close-all-tabs-and-clear" : { + "comment" : "Title of the Clear button in the fire popover", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Close all tabs and clear site data" + } + } + } + }, "fire.dialog.close-burner-window" : { "comment" : "Button that allows the user to close and burn the browser burner window", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index d0674088c2..325bd5d444 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -128,7 +128,7 @@ final class MainViewController: NSViewController { browserTabViewController = BrowserTabViewController(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager) findInPageViewController = FindInPageViewController.create() - fireViewController = FireViewController.create(tabCollectionViewModel: tabCollectionViewModel) + fireViewController = FireViewController(tabCollectionViewModel: tabCollectionViewModel) bookmarksBarViewController = BookmarksBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager) super.init(nibName: nil, bundle: nil) @@ -639,14 +639,7 @@ extension MainViewController: BrowserTabViewControllerDelegate { ])) bkman.loadBookmarks() - let vc = MainViewController(bookmarkManager: bkman, autofillPopoverPresenter: DefaultAutofillPopoverPresenter()) - var c: AnyCancellable! - c = vc.publisher(for: \.view.window).sink { window in - window?.titlebarAppearsTransparent = true - window?.titleVisibility = .hidden - withExtendedLifetime(c) {} - } - - return vc + return MainViewController(bookmarkManager: bkman, autofillPopoverPresenter: DefaultAutofillPopoverPresenter()) + ._preview_hidingWindowControlsOnAppear() } #endif diff --git a/DuckDuckGo/TabPreview/TabPreviewViewController.swift b/DuckDuckGo/TabPreview/TabPreviewViewController.swift index ff40cd8ed1..e102b92167 100644 --- a/DuckDuckGo/TabPreview/TabPreviewViewController.swift +++ b/DuckDuckGo/TabPreview/TabPreviewViewController.swift @@ -204,25 +204,31 @@ extension TabPreviewViewController { import Combine @available(macOS 14.0, *) -#Preview(traits: .fixedLayout(width: 280, height: 220)) { { +#Preview("Not selected", traits: .fixedLayout(width: 280, height: 220)) { { 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) + isSelected: false) - 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._preview_hidingWindowControlsOnAppear(sizeToFit: true) + +}() } + +@available(macOS 14.0, *) +#Preview("Selected", traits: .fixedLayout(width: 280, height: 220)) { { + + 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) - return vc + return vc._preview_hidingWindowControlsOnAppear(sizeToFit: true) }() } + #endif