From 228c50f1e6b0c35f57cd4dbc6a79170b901fceea Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 11 Dec 2024 17:36:34 +0600 Subject: [PATCH 01/11] Switch to tab implementation --- .../OpenTabSuggestion.imageset/Contents.json | 15 ++++++++ .../OpenTabSuggestion.imageset/History-16.pdf | Bin 0 -> 2371 bytes .../View/AddressBarTextField.swift | 35 +++++++++++++----- .../Model/SuggestionContainer.swift | 34 +++++++++++++---- .../ViewModel/SuggestionViewModel.swift | 9 +++-- DuckDuckGo/Tab/Model/TabContent.swift | 7 ++-- .../Tab/View/BrowserTabViewController.swift | 1 + DuckDuckGo/Tab/ViewModel/TabViewModel.swift | 1 + .../ViewModel/TabCollectionViewModel.swift | 10 +++++ .../View/WindowControllersManager.swift | 21 +++++++++++ 10 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/History-16.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Contents.json new file mode 100644 index 0000000000..a7907c18a6 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "History-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/History-16.pdf b/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/History-16.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8e4b129761c4b5feaeddfdbd3d5dc61e000778e0 GIT binary patch literal 2371 zcmY!laB}Ha0g>$Suu5*prx& z0?|!-4YdlHX$l}~-~mm@6Tq-BF*dNYgoh1I9@uP9NYrwr=A}SG7svvo04`9hLz05A zu_Z_zENToF1tpMx#FCQKqC9Y72`~gxzKJD8nH9jO1qB^6iCF3xS(=%dnJVZSndt$e z&D`7q5hq55dd5b^7G@?2x@LL?CT3>Fh6>O`08VnKgnn>FVoGLSI@m7f{QRPnVsJ7t zg;-^v2WLPnM-qWcK^eGhLAF!hIX@@AD7YXoIaNX5CABOwIW@@L36hYJ)nQ@l!xFIq zC|^Od5EgaFLi&XB2C{N2Y*-M1bB-}_4t1?40md*eBuk1)Qy~Nqp##j4SnLN2!IB2p z5k|O`f#qR}u`9E)1Ll2D_J^fG6O=UQmYD-gg9`d?If*5yE~&}+DX9>1V8xdyH0%)t zNCji+C_piI&!-cU~8|K7THs^>N7+?OdDvKa30Y z=F9*8cT0YM?N15&TK?&uv+McxGpkDpy$rD1vF**vwy8;WQ~rwS@!QA6$^E~hd+urD zcF?`H^$xAD^=kVWsSlT#iOF}Jo^s1)#eutS-+{fodRHeB*R9WkJyP-{~kiTJ@ z?}r74nWd6?G)h?Bxo;DGyKrB7xWl5D4C9FC^>dRwTeqy1dKA~Od#*?Lxv+T|qPZOH zlV+!E{8ceIC}F;pO8D#S;LtYtO%CV2o#oeNc^20y|5tijuFu-JJwbw6mdzn^98W9E z+`Z!GQ@5oNH8E?-l0H9f@@*B}qZnXS^@NFazJP}DN4ayIGiJ6w&R#L|rcAn1xX!~H zTe9xduo$p@R0v3uQ{UkuuiP%t_i3HmI&bCmoDFIFy=5EfbQUj4DpPL0**mGglY7?7 z_SXTt=e{@?&zi4%;mj@>^9LK>^4)8Wm%ZTgEQ8;Fp^Us=zWV2xJLj!Y70RqUJR#{x z0JBHt-dxcon&n$x3Wu;uOIM~n(n|1U*)145^-xxDcZGmJ=$daCANU*P@13vbF>k)^ za6(s|WmToh_a6=#6B4#gVZ6b4o3|jWd%BOP;k0RfPv#!Jx={XNkqmb=m;GVe8s7g5 zTg84Yz*5425(cD%0~QGI)MW-21(k4)d3pIIz#;}#Ey2=jIxLa-rlw>jI^}~(Ja~Zz zXBYxYG-w$JXTeMgN-fSWElN%;RsfZ$u=>?GKM$A_fi{8?IZQAhu?Q%pU=9^R$_gMM zh!;SP_09yBh9DmV!OBoz@l^`cYY5E~2=^BQMUYGamdHpZ1tg{e9R{-yn4|R}8l4kM z5_9s?QMFbSrKWKiC|Ghq{0br!%uG# Void) { let finalUrl: URL? let userEnteredValue: String @@ -1037,14 +1054,14 @@ extension AddressBarTextField: NSTextFieldDelegate { return true case #selector(NSResponder.deleteBackward(_:)), - #selector(NSResponder.deleteForward(_:)), - #selector(NSResponder.deleteToMark(_:)), - #selector(NSResponder.deleteWordForward(_:)), - #selector(NSResponder.deleteWordBackward(_:)), - #selector(NSResponder.deleteToEndOfLine(_:)), - #selector(NSResponder.deleteToEndOfParagraph(_:)), - #selector(NSResponder.deleteToBeginningOfLine(_:)), - #selector(NSResponder.deleteBackwardByDecomposingPreviousCharacter(_:)): + #selector(NSResponder.deleteForward(_:)), + #selector(NSResponder.deleteToMark(_:)), + #selector(NSResponder.deleteWordForward(_:)), + #selector(NSResponder.deleteWordBackward(_:)), + #selector(NSResponder.deleteToEndOfLine(_:)), + #selector(NSResponder.deleteToEndOfParagraph(_:)), + #selector(NSResponder.deleteToBeginningOfLine(_:)), + #selector(NSResponder.deleteBackwardByDecomposingPreviousCharacter(_:)): suggestionContainerViewModel?.clearSelection() return false diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 69d05a370c..add2a4d266 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -16,12 +16,13 @@ // limitations under the License. // -import Foundation -import Suggestions +import Combine import Common +import Foundation import History -import PixelKit import os.log +import PixelKit +import Suggestions final class SuggestionContainer { @@ -29,6 +30,8 @@ final class SuggestionContainer { @PublishedAfter var result: SuggestionResult? + typealias OpenTabsProvider = @MainActor () -> [any Suggestions.BrowserTab] + private let openTabsProvider: OpenTabsProvider private let historyCoordinating: HistoryCoordinating private let bookmarkManager: BookmarkManager private let startupPreferences: StartupPreferences @@ -41,7 +44,8 @@ final class SuggestionContainer { fileprivate let suggestionsURLSession = URLSession(configuration: .ephemeral) - init(suggestionLoading: SuggestionLoading, historyCoordinating: HistoryCoordinating, bookmarkManager: BookmarkManager, startupPreferences: StartupPreferences = .shared) { + init(openTabsProvider: @escaping OpenTabsProvider, suggestionLoading: SuggestionLoading, historyCoordinating: HistoryCoordinating, bookmarkManager: BookmarkManager, startupPreferences: StartupPreferences = .shared) { + self.openTabsProvider = openTabsProvider self.bookmarkManager = bookmarkManager self.historyCoordinating = historyCoordinating self.startupPreferences = startupPreferences @@ -52,8 +56,16 @@ final class SuggestionContainer { let urlFactory = { urlString in return URL.makeURL(fromSuggestionPhrase: urlString) } - - self.init(suggestionLoading: SuggestionLoader(urlFactory: urlFactory), + let openTabsProvider: OpenTabsProvider = { @MainActor in + let selectedTab = WindowControllersManager.shared.selectedTab + return WindowControllersManager.shared.allTabViewModels.compactMap { model in + guard model.tab !== selectedTab, model.tab.content.isUrl else { return nil } + return model.tab.content.userEditableUrl.map { url in + OpenTab(title: model.title, url: url) + } + } + } + self.init(openTabsProvider: openTabsProvider, suggestionLoading: SuggestionLoader(urlFactory: urlFactory), historyCoordinating: HistoryCoordinator.shared, bookmarkManager: LocalBookmarkManager.shared) } @@ -92,6 +104,13 @@ final class SuggestionContainer { } +struct OpenTab: BrowserTab { + + let title: String + let url: URL + +} + extension SuggestionContainer: SuggestionLoadingDataSource { var platform: Platform { @@ -123,8 +142,7 @@ extension SuggestionContainer: SuggestionLoadingDataSource { } @MainActor func openTabs(for suggestionLoading: any Suggestions.SuggestionLoading) -> [any Suggestions.BrowserTab] { - // Support for this on macOS will come later. - [] + openTabsProvider() } func suggestionLoading(_ suggestionLoading: SuggestionLoading, diff --git a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift index 5d91c6394a..882fe1b36d 100644 --- a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift +++ b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift @@ -95,7 +95,8 @@ struct SuggestionViewModel: Equatable { return title ?? url.toString(forUserInput: userStringValue) } case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _), - .internalPage(title: let title, url: _), .openTab(title: let title, url: _): + .internalPage(title: let title, url: _), + .openTab(title: let title, url: _): return title case .unknown(value: let value): return value @@ -115,7 +116,8 @@ struct SuggestionViewModel: Equatable { return title } case .bookmark(title: let title, url: _, isFavorite: _, allowedInTopHits: _), - .internalPage(title: let title, url: _), .openTab(title: let title, url: _): + .internalPage(title: let title, url: _), + .openTab(title: let title, url: _): return title } } @@ -187,8 +189,7 @@ struct SuggestionViewModel: Equatable { guard url == URL(string: StartupPreferences.shared.formattedCustomHomePageURL) else { return nil } return .home16 case .openTab: - assertionFailure("specify an icon") - return nil + return .openTabSuggestion } } diff --git a/DuckDuckGo/Tab/Model/TabContent.swift b/DuckDuckGo/Tab/Model/TabContent.swift index 2be0067319..73fc58bad1 100644 --- a/DuckDuckGo/Tab/Model/TabContent.swift +++ b/DuckDuckGo/Tab/Model/TabContent.swift @@ -52,6 +52,7 @@ extension TabContent { case link case appOpenUrl case reload + case switchToOpenTab case webViewUpdated @@ -71,7 +72,7 @@ extension TabContent { switch self { case .userEntered(_, downloadRequested: true): .custom(.userRequestedPageDownload) - case .userEntered: + case .userEntered, .switchToOpenTab /* fallback */: .custom(.userEnteredUrl) case .pendingStateRestoration: .sessionRestoration @@ -100,7 +101,7 @@ extension TabContent { .returnCacheDataElseLoad case .reload, .loadedByStateRestoration: .reloadIgnoringCacheData - case .userEntered, .bookmark, .ui, .link, .appOpenUrl, .webViewUpdated: + case .userEntered, .bookmark, .ui, .link, .appOpenUrl, .webViewUpdated, .switchToOpenTab: .useProtocolCachePolicy } } @@ -262,7 +263,7 @@ extension TabContent { case .url(_, _, source: let source): return source case .newtab, .settings, .bookmarks, .onboardingDeprecated, .onboarding, .releaseNotes, .dataBrokerProtection, - .subscription, .identityTheftRestoration, .none: + .subscription, .identityTheftRestoration, .none: return .ui } } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 88d3928293..f94892e063 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -667,6 +667,7 @@ final class BrowserTabViewController: NSViewController { .url(_, _, source: .ui), .url(_, _, source: .link), .url(_, _, source: .appOpenUrl), + .url(_, _, source: .switchToOpenTab), .url(_, _, source: .reload): return true diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index 5d4645a228..40b4ee146c 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -171,6 +171,7 @@ final class TabViewModel { .url(_, _, source: .bookmark), .url(_, _, source: .ui), .url(_, _, source: .appOpenUrl), + .url(_, _, source: .switchToOpenTab), .url(_, _, source: .reload), .newtab, .settings, diff --git a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift index 50a1a61738..1083a70041 100644 --- a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift +++ b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift @@ -787,6 +787,16 @@ extension TabCollectionViewModel { return nil } + func indexInAllTabs(where condition: (Tab) -> Bool) -> TabIndex? { + if let index = pinnedTabsCollection?.tabs.firstIndex(where: condition) { + return .pinned(index) + } + if let index = tabCollection.tabs.firstIndex(where: condition) { + return .unpinned(index) + } + return nil + } + private func tab(at tabIndex: TabIndex) -> Tab? { switch tabIndex { case .pinned(let index): diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index ac1d5dd876..d971ab720f 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -192,6 +192,12 @@ extension WindowControllersManager { // If there is any non-popup window available, open the URL in it ?? nonPopupMainWindowControllers.first { + // Switch to already open tab if present + if [.appOpenUrl, .switchToOpenTab].contains(source), + let url, switchToOpenTab(with: url, preferring: windowController) == true { + return + } + show(url: url, in: windowController, source: source, newTab: newTab) return } @@ -204,6 +210,21 @@ extension WindowControllersManager { } } + private func switchToOpenTab(with url: URL, preferring mainWindowController: MainWindowController) -> Bool { + for (windowIdx, windowController) in ([mainWindowController] + mainWindowControllers).enumerated() { + // prefer current main window + guard windowIdx == 0 || windowController !== mainWindowController else { continue } + let tabCollectionViewModel = windowController.mainViewController.tabCollectionViewModel + guard let index = tabCollectionViewModel.indexInAllTabs(where: { $0.content.urlForWebView == url }) else { continue } + + windowController.window?.makeKeyAndOrderFront(self) + tabCollectionViewModel.select(at: index) + + return true + } + return false + } + private func show(url: URL?, in windowController: MainWindowController, source: Tab.TabContent.URLSource, newTab: Bool) { let viewController = windowController.mainViewController windowController.window?.makeKeyAndOrderFront(self) From 8dc9c3f455aa51e17a1a9ee2bbac6d5d11f0620a Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 16 Dec 2024 20:22:21 +0600 Subject: [PATCH 02/11] separate tab search for fire windows --- .../Model/HomePageAddressBarModel.swift | 2 +- .../MainWindow/MainViewController.swift | 1 - .../View/AddressBarViewController.swift | 8 +++--- .../View/NavigationBarViewController.swift | 12 ++++----- .../Model/SuggestionContainer.swift | 27 +++++++++++-------- .../View/WindowControllersManager.swift | 12 ++++++++- .../Tab/SearchNonexistentDomainTests.swift | 5 +++- .../Model/SuggestionContainerTests.swift | 9 ++++--- .../SuggestionContainerViewModelTests.swift | 7 ++--- 9 files changed, 51 insertions(+), 32 deletions(-) diff --git a/DuckDuckGo/HomePage/Model/HomePageAddressBarModel.swift b/DuckDuckGo/HomePage/Model/HomePageAddressBarModel.swift index 9d5f2ed60d..763a33d514 100644 --- a/DuckDuckGo/HomePage/Model/HomePageAddressBarModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageAddressBarModel.swift @@ -146,7 +146,7 @@ extension HomePage.Models { return AddressBarViewController( coder: coder, tabCollectionViewModel: tabCollectionViewModel, - isBurner: tabCollectionViewModel.isBurner, + burnerMode: tabCollectionViewModel.burnerMode, popovers: nil, isSearchBox: true ) diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index d0674088c2..c58cc019d9 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -119,7 +119,6 @@ final class MainViewController: NSViewController { }() navigationBarViewController = NavigationBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, - isBurner: isBurner, networkProtectionPopoverManager: networkProtectionPopoverManager, networkProtectionStatusReporter: networkProtectionStatusReporter, autofillPopoverPresenter: autofillPopoverPresenter, diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 2d98d5f052..8b7db93af4 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -88,7 +88,7 @@ final class AddressBarViewController: NSViewController, ObservableObject { init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, - isBurner: Bool, + burnerMode: BurnerMode, popovers: NavigationBarPopovers?, isSearchBox: Bool = false, onboardingPixelReporter: OnboardingAddressBarReporting = OnboardingPixelReporter()) { @@ -96,9 +96,9 @@ final class AddressBarViewController: NSViewController, ObservableObject { self.popovers = popovers self.suggestionContainerViewModel = SuggestionContainerViewModel( isHomePage: tabViewModel?.tab.content == .newtab, - isBurner: isBurner, - suggestionContainer: SuggestionContainer()) - self.isBurner = isBurner + isBurner: burnerMode.isBurner, + suggestionContainer: SuggestionContainer(burnerMode: burnerMode)) + self.isBurner = burnerMode.isBurner self.onboardingPixelReporter = onboardingPixelReporter self.isSearchBox = isSearchBox diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index ef178b6eed..c3c2e0605d 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -85,7 +85,7 @@ final class NavigationBarViewController: NSViewController { var addressBarViewController: AddressBarViewController? private var tabCollectionViewModel: TabCollectionViewModel - private let isBurner: Bool + private var burnerMode: BurnerMode { tabCollectionViewModel.burnerMode } // swiftlint:disable weak_delegate private let goBackButtonMenuDelegate: NavigationButtonMenuDelegate @@ -124,7 +124,6 @@ final class NavigationBarViewController: NSViewController { private let networkProtectionFeatureActivation: NetworkProtectionFeatureActivation static func create(tabCollectionViewModel: TabCollectionViewModel, - isBurner: Bool, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation = NetworkProtectionKeychainTokenStore(), downloadListCoordinator: DownloadListCoordinator = .shared, dragDropManager: BookmarkDragDropManager = .shared, @@ -134,17 +133,16 @@ final class NavigationBarViewController: NSViewController { aiChatMenuConfig: AIChatMenuVisibilityConfigurable, brokenSitePromptLimiter: BrokenSitePromptLimiter) -> NavigationBarViewController { NSStoryboard(name: "NavigationBar", bundle: nil).instantiateInitialController { coder in - self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel, isBurner: isBurner, networkProtectionFeatureActivation: networkProtectionFeatureActivation, downloadListCoordinator: downloadListCoordinator, dragDropManager: dragDropManager, networkProtectionPopoverManager: networkProtectionPopoverManager, networkProtectionStatusReporter: networkProtectionStatusReporter, autofillPopoverPresenter: autofillPopoverPresenter, aiChatMenuConfig: aiChatMenuConfig, brokenSitePromptLimiter: brokenSitePromptLimiter) + self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel, networkProtectionFeatureActivation: networkProtectionFeatureActivation, downloadListCoordinator: downloadListCoordinator, dragDropManager: dragDropManager, networkProtectionPopoverManager: networkProtectionPopoverManager, networkProtectionStatusReporter: networkProtectionStatusReporter, autofillPopoverPresenter: autofillPopoverPresenter, aiChatMenuConfig: aiChatMenuConfig, brokenSitePromptLimiter: brokenSitePromptLimiter) }! } - init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, isBurner: Bool, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation, downloadListCoordinator: DownloadListCoordinator, dragDropManager: BookmarkDragDropManager, networkProtectionPopoverManager: NetPPopoverManager, networkProtectionStatusReporter: NetworkProtectionStatusReporter, autofillPopoverPresenter: AutofillPopoverPresenter, + init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, networkProtectionFeatureActivation: NetworkProtectionFeatureActivation, downloadListCoordinator: DownloadListCoordinator, dragDropManager: BookmarkDragDropManager, networkProtectionPopoverManager: NetPPopoverManager, networkProtectionStatusReporter: NetworkProtectionStatusReporter, autofillPopoverPresenter: AutofillPopoverPresenter, aiChatMenuConfig: AIChatMenuVisibilityConfigurable, brokenSitePromptLimiter: BrokenSitePromptLimiter) { - self.popovers = NavigationBarPopovers(networkProtectionPopoverManager: networkProtectionPopoverManager, autofillPopoverPresenter: autofillPopoverPresenter, isBurner: isBurner) + self.popovers = NavigationBarPopovers(networkProtectionPopoverManager: networkProtectionPopoverManager, autofillPopoverPresenter: autofillPopoverPresenter, isBurner: tabCollectionViewModel.isBurner) self.tabCollectionViewModel = tabCollectionViewModel self.networkProtectionButtonModel = NetworkProtectionNavBarButtonModel(popoverManager: networkProtectionPopoverManager, statusReporter: networkProtectionStatusReporter) - self.isBurner = isBurner self.networkProtectionFeatureActivation = networkProtectionFeatureActivation self.downloadListCoordinator = downloadListCoordinator self.dragDropManager = dragDropManager @@ -234,7 +232,7 @@ final class NavigationBarViewController: NSViewController { let onboardingPixelReporter = OnboardingPixelReporter() guard let addressBarViewController = AddressBarViewController(coder: coder, tabCollectionViewModel: tabCollectionViewModel, - isBurner: isBurner, + burnerMode: burnerMode, popovers: popovers, onboardingPixelReporter: onboardingPixelReporter) else { fatalError("NavigationBarViewController: Failed to init AddressBarViewController") diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index add2a4d266..99ccdfd576 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -52,20 +52,12 @@ final class SuggestionContainer { self.loading = suggestionLoading } - convenience init () { + convenience init (burnerMode: BurnerMode) { let urlFactory = { urlString in return URL.makeURL(fromSuggestionPhrase: urlString) } - let openTabsProvider: OpenTabsProvider = { @MainActor in - let selectedTab = WindowControllersManager.shared.selectedTab - return WindowControllersManager.shared.allTabViewModels.compactMap { model in - guard model.tab !== selectedTab, model.tab.content.isUrl else { return nil } - return model.tab.content.userEditableUrl.map { url in - OpenTab(title: model.title, url: url) - } - } - } - self.init(openTabsProvider: openTabsProvider, suggestionLoading: SuggestionLoader(urlFactory: urlFactory), + self.init(openTabsProvider: Self.defaultOpenTabsProvider(burnerMode: burnerMode), + suggestionLoading: SuggestionLoader(urlFactory: urlFactory), historyCoordinating: HistoryCoordinator.shared, bookmarkManager: LocalBookmarkManager.shared) } @@ -102,6 +94,19 @@ final class SuggestionContainer { latestQuery = nil } + private static func defaultOpenTabsProvider(burnerMode: BurnerMode) -> OpenTabsProvider { + { @MainActor in + let selectedTab = WindowControllersManager.shared.selectedTab + let openTabViewModels = WindowControllersManager.shared.allTabViewModels(for: burnerMode) + return openTabViewModels.compactMap { model in + guard model.tab !== selectedTab, model.tab.content.isUrl else { return nil } + return model.tab.content.userEditableUrl.map { url in + OpenTab(title: model.title, url: url) + } + } + } + } + } struct OpenTab: BrowserTab { diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index d971ab720f..b6eb66d113 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -356,10 +356,20 @@ extension WindowControllersManager { var allTabViewModels: [TabViewModel] { return allTabCollectionViewModels.flatMap { - Array($0.tabViewModels.values) + $0.tabViewModels.values } } + func allTabViewModels(for burnerMode: BurnerMode) -> [TabViewModel] { + allTabCollectionViewModels + .filter { tabCollectionViewModel in + tabCollectionViewModel.burnerMode == burnerMode + } + .flatMap { + $0.tabViewModels.values + } + } + func windowController(for tabCollectionViewModel: TabCollectionViewModel) -> MainWindowController? { return mainWindowControllers.first(where: { tabCollectionViewModel === $0.mainViewController.tabCollectionViewModel diff --git a/IntegrationTests/Tab/SearchNonexistentDomainTests.swift b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift index 9daff96090..ace26d6ac5 100644 --- a/IntegrationTests/Tab/SearchNonexistentDomainTests.swift +++ b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift @@ -238,7 +238,10 @@ final class SearchNonexistentDomainTests: XCTestCase { addressBar.stringValue = enteredString let suggestionLoadingMock = SuggestionLoadingMock() - let suggestionContainer = SuggestionContainer(suggestionLoading: suggestionLoadingMock, historyCoordinating: HistoryCoordinator.shared, bookmarkManager: LocalBookmarkManager.shared) + let suggestionContainer = SuggestionContainer(openTabsProvider: { [] }, + suggestionLoading: suggestionLoadingMock, + historyCoordinating: HistoryCoordinator.shared, + bookmarkManager: LocalBookmarkManager.shared) addressBar.suggestionContainerViewModel = SuggestionContainerViewModel(isHomePage: true, isBurner: false, suggestionContainer: suggestionContainer) suggestionContainer.getSuggestions(for: enteredString) diff --git a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift index c2f35053c2..faa8e308ec 100644 --- a/UnitTests/Suggestions/Model/SuggestionContainerTests.swift +++ b/UnitTests/Suggestions/Model/SuggestionContainerTests.swift @@ -25,7 +25,8 @@ final class SuggestionContainerTests: XCTestCase { func testWhenGetSuggestionsIsCalled_ThenContainerAsksAndHoldsSuggestionsFromLoader() { let suggestionLoadingMock = SuggestionLoadingMock() let historyCoordinatingMock = HistoryCoordinatingMock() - let suggestionContainer = SuggestionContainer(suggestionLoading: suggestionLoadingMock, + let suggestionContainer = SuggestionContainer(openTabsProvider: { [] }, + suggestionLoading: suggestionLoadingMock, historyCoordinating: historyCoordinatingMock, bookmarkManager: LocalBookmarkManager.shared) @@ -50,7 +51,8 @@ final class SuggestionContainerTests: XCTestCase { func testWhenStopGettingSuggestionsIsCalled_ThenNoSuggestionsArePublished() { let suggestionLoadingMock = SuggestionLoadingMock() let historyCoordinatingMock = HistoryCoordinatingMock() - let suggestionContainer = SuggestionContainer(suggestionLoading: suggestionLoadingMock, + let suggestionContainer = SuggestionContainer(openTabsProvider: { [] }, + suggestionLoading: suggestionLoadingMock, historyCoordinating: historyCoordinatingMock, bookmarkManager: LocalBookmarkManager.shared) @@ -65,7 +67,8 @@ final class SuggestionContainerTests: XCTestCase { func testSuggestionLoadingCacheClearing() { let suggestionLoadingMock = SuggestionLoadingMock() let historyCoordinatingMock = HistoryCoordinatingMock() - let suggestionContainer = SuggestionContainer(suggestionLoading: suggestionLoadingMock, + let suggestionContainer = SuggestionContainer(openTabsProvider: { [] }, + suggestionLoading: suggestionLoadingMock, historyCoordinating: historyCoordinatingMock, bookmarkManager: LocalBookmarkManager.shared) diff --git a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift index 50ac3dce1a..9bf1d116bd 100644 --- a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift +++ b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift @@ -34,7 +34,8 @@ final class SuggestionContainerViewModelTests: XCTestCase { SearchPreferences.shared.showAutocompleteSuggestions = true suggestionLoadingMock = SuggestionLoadingMock() historyCoordinatingMock = HistoryCoordinatingMock() - suggestionContainer = SuggestionContainer(suggestionLoading: suggestionLoadingMock, + suggestionContainer = SuggestionContainer(openTabsProvider: { [] }, + suggestionLoading: suggestionLoadingMock, historyCoordinating: historyCoordinatingMock, bookmarkManager: LocalBookmarkManager.shared) suggestionContainerViewModel = SuggestionContainerViewModel(suggestionContainer: suggestionContainer) @@ -59,7 +60,7 @@ final class SuggestionContainerViewModelTests: XCTestCase { // MARK: - Tests func testWhenSelectionIndexIsNilThenSelectedSuggestionViewModelIsNil() { - let suggestionContainer = SuggestionContainer() + let suggestionContainer = SuggestionContainer(burnerMode: .regular) let suggestionContainerViewModel = SuggestionContainerViewModel(suggestionContainer: suggestionContainer) XCTAssertNil(suggestionContainerViewModel.selectionIndex) @@ -87,7 +88,7 @@ final class SuggestionContainerViewModelTests: XCTestCase { } func testWhenSelectCalledWithIndexOutOfBoundsThenSelectedSuggestionViewModelIsNil() { - let suggestionContainer = SuggestionContainer() + let suggestionContainer = SuggestionContainer(burnerMode: .regular) let suggestionListViewModel = SuggestionContainerViewModel(suggestionContainer: suggestionContainer) suggestionListViewModel.select(at: 0) From 1da7874fbfcc4d5dd6604950e6d3a9c5392a5c3b Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 18 Dec 2024 19:54:30 +0600 Subject: [PATCH 03/11] Add FeatureFlag --- DuckDuckGo/Suggestions/Model/SuggestionContainer.swift | 3 ++- .../FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 99ccdfd576..802a36b66a 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -147,7 +147,8 @@ extension SuggestionContainer: SuggestionLoadingDataSource { } @MainActor func openTabs(for suggestionLoading: any Suggestions.SuggestionLoading) -> [any Suggestions.BrowserTab] { - openTabsProvider() + guard featureFlagger.isFeatureOn(.autcompleteTabs) else { return [] } + return openTabsProvider() } func suggestionLoading(_ suggestionLoading: SuggestionLoading, diff --git a/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift b/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift index ac3c5669bc..7992b497bf 100644 --- a/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift +++ b/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift @@ -51,6 +51,7 @@ public enum FeatureFlag: String, CaseIterable { case isPrivacyProLaunchedROWOverride case autofillPartialFormSaves + case autcompleteTabs } extension FeatureFlag: FeatureFlagDescribing { @@ -63,6 +64,8 @@ extension FeatureFlag: FeatureFlagDescribing { return true case .autofillPartialFormSaves: return true + case .autcompleteTabs: + return true case .debugMenu, .sslCertificatesBypass, .appendAtbToSerpQueries, @@ -107,6 +110,8 @@ extension FeatureFlag: FeatureFlagDescribing { return .remoteReleasable(.subfeature(PrivacyProSubfeature.isLaunchedROWOverride)) case .autofillPartialFormSaves: return .remoteReleasable(.subfeature(AutofillSubfeature.partialFormSaves)) + case .autcompleteTabs: + return .remoteReleasable(.feature(.autocompleteTabs)) } } } From d44ab67d5813dd63349a98a2100a252d6f5a7fe1 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Wed, 18 Dec 2024 19:55:12 +0600 Subject: [PATCH 04/11] Deduplicate open tab suggestions and internal pages --- .../View/AddressBarTextField.swift | 15 +++++++++++---- .../Model/SuggestionContainer.swift | 19 ++++++++++++++----- .../ViewModel/SuggestionViewModel.swift | 6 ++++-- .../View/WindowControllersManager.swift | 9 ++++++++- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 56b7d1cdd9..57ae483378 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -370,7 +370,11 @@ final class AddressBarTextField: NSTextField { PixelKit.fire(autocompletePixel) } - if case .openTab(let title, url: let url) = suggestion { + if case .internalPage(title: let title, url: let url) = suggestion, + url == .bookmarks || url.isSettingsURL { + // when choosing an internal page suggestion preffer already open matching tab + switchTo(OpenTab(title: title, url: url)) + } else if case .openTab(let title, url: let url) = suggestion { switchTo(OpenTab(title: title, url: url)) } else if NSApp.isCommandPressed { openNew(NSApp.isOptionPressed ? .window : .tab, selected: NSApp.isShiftPressed, suggestion: suggestion) @@ -489,8 +493,12 @@ final class AddressBarTextField: NSTextField { } private func switchTo(_ tab: OpenTab) { - if let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel, - let selectionIndex = tabCollectionViewModel.selectionIndex, + let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel + let selectionIndex = tabCollectionViewModel.selectionIndex + + WindowControllersManager.shared.show(url: tab.url, source: .switchToOpenTab, newTab: true /* in case not found */) + + if let selectedTabViewModel, let selectionIndex, case .newtab = selectedTabViewModel.tab.content { // close tab with "new tab" page open tabCollectionViewModel.remove(at: selectionIndex) @@ -500,7 +508,6 @@ final class AddressBarTextField: NSTextField { window.performClose(self) } } - WindowControllersManager.shared.show(url: tab.url, source: .switchToOpenTab, newTab: false) } private func makeUrl(suggestion: Suggestion?, stringValueWithoutSuffix: String, completion: @escaping (URL?, String, Bool) -> Void) { diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 802a36b66a..581498286b 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import BrowserServicesKit import Combine import Common import Foundation @@ -35,6 +36,7 @@ final class SuggestionContainer { private let historyCoordinating: HistoryCoordinating private let bookmarkManager: BookmarkManager private let startupPreferences: StartupPreferences + private let featureFlagger: FeatureFlagger private let loading: SuggestionLoading // Used for presenting the same suggestions after the removal of the local suggestion @@ -44,11 +46,12 @@ final class SuggestionContainer { fileprivate let suggestionsURLSession = URLSession(configuration: .ephemeral) - init(openTabsProvider: @escaping OpenTabsProvider, suggestionLoading: SuggestionLoading, historyCoordinating: HistoryCoordinating, bookmarkManager: BookmarkManager, startupPreferences: StartupPreferences = .shared) { + init(openTabsProvider: @escaping OpenTabsProvider, suggestionLoading: SuggestionLoading, historyCoordinating: HistoryCoordinating, bookmarkManager: BookmarkManager, startupPreferences: StartupPreferences = .shared, featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger) { self.openTabsProvider = openTabsProvider self.bookmarkManager = bookmarkManager self.historyCoordinating = historyCoordinating self.startupPreferences = startupPreferences + self.featureFlagger = featureFlagger self.loading = suggestionLoading } @@ -98,11 +101,17 @@ final class SuggestionContainer { { @MainActor in let selectedTab = WindowControllersManager.shared.selectedTab let openTabViewModels = WindowControllersManager.shared.allTabViewModels(for: burnerMode) + var usedUrls = Set() // deduplicate return openTabViewModels.compactMap { model in - guard model.tab !== selectedTab, model.tab.content.isUrl else { return nil } - return model.tab.content.userEditableUrl.map { url in - OpenTab(title: model.title, url: url) - } + guard model.tab !== selectedTab, + model.tab.content.isUrl + || model.tab.content.urlForWebView?.isSettingsURL == true + || model.tab.content.urlForWebView == .bookmarks, + let url = model.tab.content.userEditableUrl, + url != selectedTab?.content.userEditableUrl, // doesn‘t match currently selected + usedUrls.insert(url.absoluteString).inserted == true /* if did not contain */ else { return nil } + + return OpenTab(title: model.title, url: url) } } } diff --git a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift index 882fe1b36d..a6f4d14776 100644 --- a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift +++ b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift @@ -181,9 +181,11 @@ struct SuggestionViewModel: Equatable { return .favoritedBookmarkSuggestion case .unknown: return .web - case .internalPage(title: _, url: let url) where url == .bookmarks: + case .internalPage(title: _, url: let url) where url == .bookmarks, + .openTab(title: _, url: let url) where url == .bookmarks: return .bookmarksFolder - case .internalPage(title: _, url: let url) where url.isSettingsURL: + case .internalPage(title: _, url: let url) where url.isSettingsURL, + .openTab(title: _, url: let url) where url.isSettingsURL: return .settingsMulticolor16 case .internalPage(title: _, url: let url): guard url == URL(string: StartupPreferences.shared.formattedCustomHomePageURL) else { return nil } diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index b6eb66d113..53170a2820 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -215,10 +215,17 @@ extension WindowControllersManager { // prefer current main window guard windowIdx == 0 || windowController !== mainWindowController else { continue } let tabCollectionViewModel = windowController.mainViewController.tabCollectionViewModel - guard let index = tabCollectionViewModel.indexInAllTabs(where: { $0.content.urlForWebView == url }) else { continue } + guard let index = tabCollectionViewModel.indexInAllTabs(where: { + $0.content.urlForWebView == url || (url.isSettingsURL && $0.content.urlForWebView?.isSettingsURL == true) + }) else { continue } windowController.window?.makeKeyAndOrderFront(self) tabCollectionViewModel.select(at: index) + if let tab = tabCollectionViewModel.tabViewModel(at: index)?.tab, + tab.content.urlForWebView != url { + // navigate to another settings pane + tab.setContent(.contentFromURL(url, source: .switchToOpenTab)) + } return true } From d3764b16a43c2d49c263dc94cb3a96b541d5c420 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 20 Dec 2024 16:44:10 +0600 Subject: [PATCH 05/11] update icon --- .../OpenTabSuggestion.imageset/Contents.json | 2 +- .../OpenTabSuggestion.imageset/History-16.pdf | Bin 2371 -> 0 bytes .../Window-Tabbed-16D 1.pdf | Bin 0 -> 10940 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/History-16.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Window-Tabbed-16D 1.pdf diff --git a/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Contents.json index a7907c18a6..bb34ab7673 100644 --- a/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "History-16.pdf", + "filename" : "Window-Tabbed-16D 1.pdf", "idiom" : "universal" } ], diff --git a/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/History-16.pdf b/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/History-16.pdf deleted file mode 100644 index 8e4b129761c4b5feaeddfdbd3d5dc61e000778e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2371 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 literal 0 HcmV?d00001 From eeb286639d089afa71ab41afbf08a878c5399913 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 20 Dec 2024 16:45:33 +0600 Subject: [PATCH 06/11] move switchToTab logics from AddressBarTextField to WindowControllersManager --- .../NavigationBar/View/AddressBarTextField.swift | 14 -------------- .../Suggestions/Model/SuggestionContainer.swift | 14 +++++++++----- .../Windows/View/WindowControllersManager.swift | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 57ae483378..8ee7d915f0 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -493,21 +493,7 @@ final class AddressBarTextField: NSTextField { } private func switchTo(_ tab: OpenTab) { - let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel - let selectionIndex = tabCollectionViewModel.selectionIndex - WindowControllersManager.shared.show(url: tab.url, source: .switchToOpenTab, newTab: true /* in case not found */) - - if let selectedTabViewModel, let selectionIndex, - case .newtab = selectedTabViewModel.tab.content { - // close tab with "new tab" page open - tabCollectionViewModel.remove(at: selectionIndex) - - // close the window if no more non-pinned tabs are open - if tabCollectionViewModel.tabs.isEmpty, let window, window.isVisible { - window.performClose(self) - } - } } private func makeUrl(suggestion: Suggestion?, stringValueWithoutSuffix: String, completion: @escaping (URL?, String, Bool) -> Void) { diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 581498286b..7f1df51d70 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -55,11 +55,15 @@ final class SuggestionContainer { self.loading = suggestionLoading } - convenience init (burnerMode: BurnerMode) { + @MainActor + convenience init (burnerMode: BurnerMode, + windowControllersManager: WindowControllersManagerProtocol? = nil) { let urlFactory = { urlString in return URL.makeURL(fromSuggestionPhrase: urlString) } - self.init(openTabsProvider: Self.defaultOpenTabsProvider(burnerMode: burnerMode), + let windowControllersManager = windowControllersManager ?? WindowControllersManager.shared + self.init(openTabsProvider: Self.defaultOpenTabsProvider(burnerMode: burnerMode, + windowControllersManager: windowControllersManager), suggestionLoading: SuggestionLoader(urlFactory: urlFactory), historyCoordinating: HistoryCoordinator.shared, bookmarkManager: LocalBookmarkManager.shared) @@ -97,10 +101,10 @@ final class SuggestionContainer { latestQuery = nil } - private static func defaultOpenTabsProvider(burnerMode: BurnerMode) -> OpenTabsProvider { + private static func defaultOpenTabsProvider(burnerMode: BurnerMode, windowControllersManager: WindowControllersManagerProtocol) -> OpenTabsProvider { { @MainActor in - let selectedTab = WindowControllersManager.shared.selectedTab - let openTabViewModels = WindowControllersManager.shared.allTabViewModels(for: burnerMode) + let selectedTab = windowControllersManager.selectedTab + let openTabViewModels = windowControllersManager.allTabViewModels(for: burnerMode) var usedUrls = Set() // deduplicate return openTabViewModels.compactMap { model in guard model.tab !== selectedTab, diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index 53170a2820..c95e86316f 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -192,9 +192,24 @@ extension WindowControllersManager { // If there is any non-popup window available, open the URL in it ?? nonPopupMainWindowControllers.first { + let tabCollectionViewModel = windowController.mainViewController.tabCollectionViewModel + let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel + let selectionIndex = tabCollectionViewModel.selectionIndex + // Switch to already open tab if present if [.appOpenUrl, .switchToOpenTab].contains(source), let url, switchToOpenTab(with: url, preferring: windowController) == true { + + if let selectedTabViewModel, let selectionIndex, + case .newtab = selectedTabViewModel.tab.content { + // close tab with "new tab" page open + tabCollectionViewModel.remove(at: selectionIndex) + + // close the window if no more non-pinned tabs are open + if tabCollectionViewModel.tabs.isEmpty, let window = windowController.window, window.isVisible { + window.performClose(nil) + } + } return } From f59ee98442dd30398e55b6805d2ea8c1297adc29 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Fri, 20 Dec 2024 16:48:13 +0600 Subject: [PATCH 07/11] use "naked" url for dedup --- DuckDuckGo/Suggestions/Model/SuggestionContainer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 7f1df51d70..19730cbeb0 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -113,7 +113,7 @@ final class SuggestionContainer { || model.tab.content.urlForWebView == .bookmarks, let url = model.tab.content.userEditableUrl, url != selectedTab?.content.userEditableUrl, // doesn‘t match currently selected - usedUrls.insert(url.absoluteString).inserted == true /* if did not contain */ else { return nil } + usedUrls.insert(url.nakedString ?? "").inserted == true /* if did not contain */ else { return nil } return OpenTab(title: model.title, url: url) } From 8d3cf899ef291573a88737886ba36fe84b516ca6 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 23 Dec 2024 16:11:46 +0600 Subject: [PATCH 08/11] Design Review adjustments; Add Switch to Tab Address Bar box --- .../Arrow-Right-12.pdf | Bin 0 -> 4229 bytes .../Arrow-Right-12.imageset/Contents.json | 15 +++ DuckDuckGo/Common/Localizables/UserText.swift | 3 + DuckDuckGo/Localizable.xcstrings | 12 +++ .../AddressBarButtonsViewController.swift | 6 +- .../View/AddressBarTextField.swift | 27 +++-- .../View/AddressBarViewController.swift | 56 +++++++++-- .../View/NavigationBar.storyboard | 94 ++++++++++++++---- .../AppStateChangedPublisher.swift | 1 + .../Suggestions/View/Suggestion.storyboard | 27 +++-- .../ViewModel/SuggestionViewModel.swift | 4 + .../View/WindowControllersManager.swift | 28 +++--- .../AppKitExtensions/NSColorExtension.swift | 25 +++++ 13 files changed, 242 insertions(+), 56 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/Arrow-Right-12.imageset/Arrow-Right-12.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Arrow-Right-12.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Images/Arrow-Right-12.imageset/Arrow-Right-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Arrow-Right-12.imageset/Arrow-Right-12.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2879577530dc4be2a73ce0d48c332398520fd885 GIT binary patch literal 4229 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 YH{g-Yp?Pv NSAttributedString { let suffixColor = isBurner ? NSColor.burnerAccent : NSColor.addressBarSuffix @@ -932,6 +933,8 @@ extension AddressBarTextField { } static let searchSuffix = " – \(UserText.searchDuckDuckGoSuffix)" + static let searchOpenTabSuffix = " – \(UserText.duckDuckGoSearchSuffix)" + static let internalPageOpenTabSuffix = " – \(UserText.duckDuckGo)" static let visitSuffix = " – \(UserText.addressBarVisitSuffix)" var string: String { @@ -940,14 +943,16 @@ extension AddressBarTextField { return Self.searchSuffix case .visit(host: let host): return "\(Self.visitSuffix) \(host)" - case .url(let url): - if url.isDuckDuckGoSearch { - return Self.searchSuffix - } else { - return " – " + url.toString(decodePunycode: false, - dropScheme: true, - dropTrailingSlash: false) - } + case .openTab(let url) where url.isDuckDuckGoSearch: + return Self.searchOpenTabSuffix + case .openTab(let url) where url.isDuckURLScheme: + return Self.internalPageOpenTabSuffix + case .url(let url) where url.isDuckDuckGoSearch: + return Self.searchSuffix + case .url(let url), .openTab(let url): + return " – " + url.toString(decodePunycode: false, + dropScheme: true, + dropTrailingSlash: false) case .title(let title): return " – " + title } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 8b7db93af4..95bf62994e 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -33,6 +33,8 @@ final class AddressBarViewController: NSViewController, ObservableObject { @IBOutlet var passiveTextFieldMinXConstraint: NSLayoutConstraint! @IBOutlet var activeTextFieldMinXConstraint: NSLayoutConstraint! @IBOutlet var buttonsContainerView: NSView! + @IBOutlet var switchToTabBox: ColorView! + @IBOutlet var switchToTabBoxMinXConstraint: NSLayoutConstraint! private static let defaultActiveTextFieldMinX: CGFloat = 40 private let popovers: NavigationBarPopovers? @@ -46,7 +48,13 @@ final class AddressBarViewController: NSViewController, ObservableObject { let isSearchBox: Bool enum Mode: Equatable { - case editing(isUrl: Bool) + enum EditingMode { + case text + case url + case openTabSuggestion + } + + case editing(EditingMode) case browsing var isEditing: Bool { @@ -54,7 +62,11 @@ final class AddressBarViewController: NSViewController, ObservableObject { } } - private var mode: Mode = .editing(isUrl: false) { + private enum Constants { + static let switchToTabMinXPadding: CGFloat = 34 + } + + private var mode: Mode = .editing(.text) { didSet { addressBarButtonsViewController?.controllerMode = mode } @@ -63,6 +75,7 @@ final class AddressBarViewController: NSViewController, ObservableObject { private var isFirstResponder = false { didSet { updateView() + updateSwitchToTabBoxAppearance() self.addressBarButtonsViewController?.isTextFieldEditorFirstResponder = isFirstResponder self.clickPoint = nil // reset click point if the address bar activated during click } @@ -115,6 +128,8 @@ final class AddressBarViewController: NSViewController, ObservableObject { addressBarTextField.placeholderString = UserText.addressBarPlaceholder addressBarTextField.setAccessibilityIdentifier("AddressBarViewController.addressBarTextField") + switchToTabBox.isHidden = true + updateView() // only activate active text field leading constraint on its appearance to avoid constraint conflicts activeTextFieldMinXConstraint.isActive = false @@ -224,6 +239,7 @@ final class AddressBarViewController: NSViewController, ObservableObject { updateMode(value: value) addressBarButtonsViewController?.textFieldValue = value updateView() + updateSwitchToTabBoxAppearance() } .store(in: &cancellables) } @@ -360,6 +376,25 @@ final class AddressBarViewController: NSViewController, ObservableObject { addressBarTextField.placeholderString = tabViewModel?.tab.content == .newtab ? UserText.addressBarPlaceholder : "" } + private func updateSwitchToTabBoxAppearance() { + guard case .editing(.openTabSuggestion) = mode, + addressBarTextField.isVisible, let editor = addressBarTextField.editor else { + switchToTabBox.isHidden = true + switchToTabBox.alphaValue = 0 + return + } + + if !switchToTabBox.isVisible { + switchToTabBox.isShown = true + switchToTabBox.alphaValue = 0 + } + // update box position on the next pass after text editor layout is updated + DispatchQueue.main.async { + self.switchToTabBox.alphaValue = 1 + self.switchToTabBoxMinXConstraint.constant = editor.textSize.width + Constants.switchToTabMinXPadding + } + } + private func updateShadowViewPresence(_ isFirstResponder: Bool) { guard isFirstResponder, view.window?.isPopUpWindow == false else { shadowView.removeFromSuperview() @@ -377,7 +412,7 @@ final class AddressBarViewController: NSViewController, ObservableObject { shadowView.shadowColor = isSuggestionsWindowVisible ? .suggestionsShadow : .clear shadowView.shadowRadius = isSuggestionsWindowVisible ? 8.0 : 0.0 - activeOuterBorderView.isHidden = isSuggestionsWindowVisible + activeOuterBorderView.isHidden = isSuggestionsWindowVisible || view.window?.isKeyWindow != true activeBackgroundView.isHidden = isSuggestionsWindowVisible activeBackgroundViewWithSuggestions.isHidden = !isSuggestionsWindowVisible if isSearchBox { @@ -395,12 +430,16 @@ final class AddressBarViewController: NSViewController, ObservableObject { private func updateMode(value: AddressBarTextField.Value? = nil) { switch value ?? self.addressBarTextField.value { - case .text: self.mode = .editing(isUrl: false) - case .url(urlString: _, url: _, userTyped: let userTyped): self.mode = userTyped ? .editing(isUrl: true) : .browsing + case .text: self.mode = .editing(.text) + case .url(urlString: _, url: _, userTyped: let userTyped): self.mode = userTyped ? .editing(.url) : .browsing case .suggestion(let suggestionViewModel): switch suggestionViewModel.suggestion { - case .phrase, .unknown: self.mode = .editing(isUrl: false) - case .website, .bookmark, .openTab, .historyEntry, .internalPage: self.mode = .editing(isUrl: true) + case .phrase, .unknown: + self.mode = .editing(.text) + case .website, .bookmark, .historyEntry, .internalPage: + self.mode = .editing(.url) + case .openTab: + self.mode = .editing(.openTabSuggestion) } } } @@ -420,6 +459,7 @@ final class AddressBarViewController: NSViewController, ObservableObject { activeBackgroundView.backgroundColor = NSColor.homePageAddressBarBackground activeBackgroundViewWithSuggestions.borderColor = NSColor.homePageAddressBarBorder activeBackgroundViewWithSuggestions.backgroundColor = NSColor.homePageAddressBarBackground + switchToTabBox.backgroundColor = NSColor.homePageAddressBarBackground } } else { @@ -428,12 +468,14 @@ final class AddressBarViewController: NSViewController, ObservableObject { activeBackgroundView.borderWidth = 2.0 activeBackgroundView.borderColor = accentColor.withAlphaComponent(0.6) activeBackgroundView.backgroundColor = NSColor.addressBarBackground + switchToTabBox.backgroundColor = NSColor.navigationBarBackground.blended(with: .addressBarBackground) activeOuterBorderView.isHidden = !isHomePage } else { activeBackgroundView.borderWidth = 0 activeBackgroundView.borderColor = nil activeBackgroundView.backgroundColor = NSColor.inactiveSearchBarBackground + switchToTabBox.backgroundColor = NSColor.navigationBarBackground.blended(with: .inactiveSearchBarBackground) activeOuterBorderView.isHidden = true } diff --git a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard index 2fb09b93d1..7be556b0c9 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard +++ b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard @@ -1,7 +1,7 @@ - + - + @@ -408,14 +408,14 @@ - + - + - + @@ -436,7 +436,7 @@ - + @@ -447,7 +447,7 @@ - + @@ -464,10 +464,10 @@ - + - + @@ -504,7 +504,7 @@ - + @@ -512,7 +512,7 @@ - + @@ -520,11 +520,64 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -549,12 +602,14 @@ - + + + @@ -573,7 +628,9 @@ + + @@ -591,6 +648,8 @@ + + @@ -621,7 +680,7 @@ + @@ -257,7 +271,7 @@ - + @@ -318,6 +332,7 @@ + diff --git a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift index a6f4d14776..1733afa699 100644 --- a/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift +++ b/DuckDuckGo/Suggestions/ViewModel/SuggestionViewModel.swift @@ -150,6 +150,10 @@ struct SuggestionViewModel: Equatable { case .phrase, .unknown, .website: return "" + case .openTab(title: _, url: let url) where url.isDuckURLScheme: + return " – " + UserText.duckDuckGo + case .openTab(title: _, url: let url) where url.isDuckDuckGoSearch: + return " – " + UserText.duckDuckGoSearchSuffix case .historyEntry(title: _, url: let url, allowedInTopHits: _), .bookmark(title: _, url: let url, isFavorite: _, allowedInTopHits: _), .openTab(title: _, url: let url): diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index c95e86316f..b0ac64f5b9 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -25,6 +25,8 @@ import BrowserServicesKit @MainActor protocol WindowControllersManagerProtocol { + var mainWindowControllers: [MainWindowController] { get } + var lastKeyMainWindowController: MainWindowController? { get } var pinnedTabsManager: PinnedTabsManager { get } @@ -94,18 +96,6 @@ final class WindowControllersManager: WindowControllersManagerProtocol { } } - private var mainWindowController: MainWindowController? { - return mainWindowControllers.first(where: { - let isMain = $0.window?.isMainWindow ?? false - let hasMainChildWindow = $0.window?.childWindows?.contains { $0.isMainWindow } ?? false - return $0.window?.isPopUpWindow == false && (isMain || hasMainChildWindow) - }) - } - - var selectedTab: Tab? { - return mainWindowController?.mainViewController.tabCollectionViewModel.selectedTab - } - let didChangeKeyWindowController = PassthroughSubject() let didRegisterWindowController = PassthroughSubject<(MainWindowController), Never>() let didUnregisterWindowController = PassthroughSubject<(MainWindowController), Never>() @@ -368,7 +358,19 @@ extension Tab { } // MARK: - Accessing all TabCollectionViewModels -extension WindowControllersManager { +extension WindowControllersManagerProtocol { + + var mainWindowController: MainWindowController? { + return mainWindowControllers.first(where: { + let isMain = $0.window?.isMainWindow ?? false + let hasMainChildWindow = $0.window?.childWindows?.contains { $0.isMainWindow } ?? false + return $0.window?.isPopUpWindow == false && (isMain || hasMainChildWindow) + }) + } + + var selectedTab: Tab? { + return mainWindowController?.mainViewController.tabCollectionViewModel.selectedTab + } var allTabCollectionViewModels: [TabCollectionViewModel] { return mainWindowControllers.map { diff --git a/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSColorExtension.swift b/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSColorExtension.swift index 5fc2bb1d03..843e5fff28 100644 --- a/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSColorExtension.swift +++ b/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSColorExtension.swift @@ -66,4 +66,29 @@ extension NSColor { } } + public func blended(with color: NSColor) -> NSColor { + // Get the RGBA components of both colors + guard let components1 = self.usingColorSpace(.sRGB)?.cgColor.components, + let components2 = color.usingColorSpace(.sRGB)?.cgColor.components else { return self } + + // Extract the individual RGBA values + let r1 = components1[0] + let g1 = components1[1] + let b1 = components1[2] + let a1 = components1[3] + + let r2 = components2[0] + let g2 = components2[1] + let b2 = components2[2] + let a2 = components2[3] + + // Calculate the resulting color components + let r = r1 * (1 - a2) + r2 * a2 + let g = g1 * (1 - a2) + g2 * a2 + let b = b1 * (1 - a2) + b2 * a2 + + // Return the blended color + return NSColor(red: r, green: g, blue: b, alpha: a1) + } + } From 4c2c83813837815bcfaf8411333a8eef32f3827d Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 24 Dec 2024 15:19:13 +0600 Subject: [PATCH 09/11] fix switch to tab colors updating on theme switch --- .../NavigationBar/View/AddressBarViewController.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 95bf62994e..954ed2d839 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -169,6 +169,13 @@ final class AddressBarViewController: NSViewController, ObservableObject { selector: #selector(textFieldFirstReponderNotification(_:)), name: .firstResponder, object: nil) + NSApp.publisher(for: \.effectiveAppearance) + .dropFirst() + .sink { [weak self] _ in + self?.refreshAddressBarAppearance(nil) + } + .store(in: &cancellables) + addMouseMonitors() } subscribeToSelectedTabViewModel() @@ -444,7 +451,7 @@ final class AddressBarViewController: NSViewController, ObservableObject { } } - @objc private func refreshAddressBarAppearance(_ sender: Any) { + @objc private func refreshAddressBarAppearance(_ sender: Any?) { self.updateMode() self.addressBarButtonsViewController?.updateButtons() From 18f13cf6f93e59683f9e152e18b7cdca155840af Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 24 Dec 2024 15:22:25 +0600 Subject: [PATCH 10/11] add Switch to Tab box to suggestions cell --- DuckDuckGo/Common/Localizables/UserText.swift | 2 + DuckDuckGo/Localizable.xcstrings | 12 ++ .../View/AddressBarViewController.swift | 2 + .../View/NavigationBar.storyboard | 1 + .../Suggestions/View/Suggestion.storyboard | 67 ++++++++--- .../View/SuggestionTableCellView.swift | 110 +++++++++++++++--- .../View/SuggestionViewController.swift | 12 +- .../DBP/FreemiumDBPPresenterTests.swift | 1 + .../RecentlyClosedCoordinatorTests.swift | 2 + .../SuggestionContainerViewModelTests.swift | 2 + 10 files changed, 167 insertions(+), 44 deletions(-) diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 2890627918..15e9c5cdd9 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1438,4 +1438,6 @@ struct UserText { static let homePagePromotionFreemiumDBPPostScanEngagementButtonTitle = "View Results" static let removeSuggestionTooltip = NSLocalizedString("remove.suggestion.tooltip", value: "Remove from browsing history", comment: "Tooltip for the button which removes the history entry from the history") + + static let switchToTab = NSLocalizedString("switch.to.tab", value: "Switch to Tab", comment: "Suggestion to switch to an open tab button title") } diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 066d59a04d..98c11e2ca2 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -63967,6 +63967,18 @@ } } }, + "switch.to.tab" : { + "comment" : "Suggestion to switch to an open tab button title", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Switch to Tab" + } + } + } + }, "sync.promo.bookmarks.message" : { "comment" : "Message for the Sync Promotion banner when user has bookmarks that can be synced", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 954ed2d839..b7686702eb 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -34,6 +34,7 @@ final class AddressBarViewController: NSViewController, ObservableObject { @IBOutlet var activeTextFieldMinXConstraint: NSLayoutConstraint! @IBOutlet var buttonsContainerView: NSView! @IBOutlet var switchToTabBox: ColorView! + @IBOutlet var switchToTabLabel: NSTextField! @IBOutlet var switchToTabBoxMinXConstraint: NSLayoutConstraint! private static let defaultActiveTextFieldMinX: CGFloat = 40 @@ -129,6 +130,7 @@ final class AddressBarViewController: NSViewController, ObservableObject { addressBarTextField.setAccessibilityIdentifier("AddressBarViewController.addressBarTextField") switchToTabBox.isHidden = true + switchToTabLabel.attributedStringValue = SuggestionTableCellView.switchToTabAttributedString updateView() // only activate active text field leading constraint on its appearance to avoid constraint conflicts diff --git a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard index 7be556b0c9..142b992105 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard +++ b/DuckDuckGo/NavigationBar/View/NavigationBar.storyboard @@ -650,6 +650,7 @@ + diff --git a/DuckDuckGo/Suggestions/View/Suggestion.storyboard b/DuckDuckGo/Suggestions/View/Suggestion.storyboard index 4d6a3ebd32..3213648e90 100644 --- a/DuckDuckGo/Suggestions/View/Suggestion.storyboard +++ b/DuckDuckGo/Suggestions/View/Suggestion.storyboard @@ -163,6 +163,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + @@ -224,6 +251,11 @@ + + + + + @@ -271,7 +303,7 @@ - + @@ -332,7 +364,7 @@ - + @@ -346,6 +378,9 @@ + + + diff --git a/DuckDuckGo/Suggestions/View/SuggestionTableCellView.swift b/DuckDuckGo/Suggestions/View/SuggestionTableCellView.swift index a66ac1a3af..c07fe98bc3 100644 --- a/DuckDuckGo/Suggestions/View/SuggestionTableCellView.swift +++ b/DuckDuckGo/Suggestions/View/SuggestionTableCellView.swift @@ -23,24 +23,49 @@ import Suggestions final class SuggestionTableCellView: NSTableCellView { - static let identifier = "SuggestionTableCellView" + static let identifier = NSUserInterfaceItemIdentifier("SuggestionTableCellView") - static let textColor: NSColor = .suggestionText - static let suffixColor: NSColor = .addressBarSuffix - static let burnerSuffixColor: NSColor = .burnerAccent - static let iconColor: NSColor = .suggestionIcon - static let selectedTintColor: NSColor = .selectedSuggestionTint + private enum Constants { + static let textColor: NSColor = .suggestionText + static let suffixColor: NSColor = .addressBarSuffix + static let burnerSuffixColor: NSColor = .burnerAccent + static let iconColor: NSColor = .suggestionIcon + static let selectedTintColor: NSColor = .selectedSuggestionTint - @IBOutlet weak var iconImageView: NSImageView! - @IBOutlet weak var removeButton: NSButton! - @IBOutlet weak var suffixTextField: NSTextField! - @IBOutlet weak var suffixTrailingConstraint: NSLayoutConstraint! + static let switchToTabExtraSpace: CGFloat = 12 + 6 + 9 + 12 + static let switchToTabSuffixPadding: CGFloat = 8 + + static let trailingSpace: CGFloat = 8 + } + + @IBOutlet var iconImageView: NSImageView! + @IBOutlet var removeButton: NSButton! + @IBOutlet var suffixTextField: NSTextField! + @IBOutlet var suffixTrailingConstraint: NSLayoutConstraint! + @IBOutlet var switchToTabBox: ColorView! + @IBOutlet var switchToTabLabel: NSTextField! + @IBOutlet var switchToTabArrowView: NSImageView! + @IBOutlet var switchToTabBoxLeadingConstraint: NSLayoutConstraint! + @IBOutlet var switchToTabBoxTrailingConstraint: NSLayoutConstraint! var suggestion: Suggestion? + static let switchToTabAttributedString: NSAttributedString = { + let text = UserText.switchToTab + let attributes: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: 11, weight: .regular), + .kern: 0.06, + ] + + return NSAttributedString(string: text, attributes: attributes) + }() + private static let switchToTabTextWidth: CGFloat = switchToTabAttributedString.size().width + private static let switchToTabBoxWidth: CGFloat = switchToTabTextWidth + Constants.switchToTabExtraSpace + override func awakeFromNib() { - suffixTextField.textColor = Self.suffixColor + suffixTextField.textColor = Constants.suffixColor removeButton.toolTip = UserText.removeSuggestionTooltip + switchToTabLabel.attributedStringValue = Self.switchToTabAttributedString } override func viewDidMoveToWindow() { @@ -59,12 +84,19 @@ final class SuggestionTableCellView: NSTableCellView { var isBurner: Bool = false - func display(_ suggestionViewModel: SuggestionViewModel) { + func display(_ suggestionViewModel: SuggestionViewModel, isBurner: Bool) { + self.isBurner = isBurner self.suggestion = suggestionViewModel.suggestion + attributedString = suggestionViewModel.tableCellViewAttributedString iconImageView.image = suggestionViewModel.icon suffixTextField.stringValue = suggestionViewModel.suffix setRemoveButtonHidden(true) + if case .openTab = suggestionViewModel.suggestion { + switchToTabBox.isHidden = false + } else { + switchToTabBox.isHidden = true + } updateTextField() } @@ -78,22 +110,28 @@ final class SuggestionTableCellView: NSTableCellView { } if isSelected { textField?.attributedStringValue = attributedString - textField?.textColor = Self.selectedTintColor - suffixTextField.textColor = Self.selectedTintColor + textField?.textColor = Constants.selectedTintColor + suffixTextField.textColor = Constants.selectedTintColor + switchToTabLabel.textColor = Constants.selectedTintColor + switchToTabArrowView.contentTintColor = Constants.selectedTintColor + switchToTabBox.backgroundColor = .white.withAlphaComponent(0.09) } else { textField?.attributedStringValue = attributedString - textField?.textColor = Self.textColor + textField?.textColor = Constants.textColor + switchToTabLabel.textColor = Constants.textColor + switchToTabArrowView.contentTintColor = Constants.textColor + switchToTabBox.backgroundColor = .buttonMouseOver if isBurner { - suffixTextField.textColor = Self.burnerSuffixColor + suffixTextField.textColor = Constants.burnerSuffixColor } else { - suffixTextField.textColor = Self.suffixColor + suffixTextField.textColor = Constants.suffixColor } } } private func updateImageViews() { - iconImageView.contentTintColor = isSelected ? Self.selectedTintColor : Self.iconColor - removeButton.contentTintColor = isSelected ? Self.selectedTintColor : Self.iconColor + iconImageView.contentTintColor = isSelected ? Constants.selectedTintColor : Constants.iconColor + removeButton.contentTintColor = isSelected ? Constants.selectedTintColor : Constants.iconColor } func updateDeleteImageViewVisibility() { @@ -115,4 +153,38 @@ final class SuggestionTableCellView: NSTableCellView { suffixTrailingConstraint.priority = hidden ? .required : .defaultLow } + override func layout() { + if switchToTabBox.isHidden { + switchToTabBoxLeadingConstraint.isActive = false + switchToTabBoxTrailingConstraint.isActive = false + suffixTrailingConstraint.constant = Constants.trailingSpace + } else { + var textWidth = attributedString?.boundingRect(with: bounds.size).width ?? 0 + if textWidth < bounds.width { + textWidth += suffixTextField.attributedStringValue.boundingRect(with: bounds.size).width + } + if textField!.frame.minX + + textWidth + + Constants.switchToTabSuffixPadding + + Self.switchToTabBoxWidth + + Constants.trailingSpace > bounds.width { + + // when cropping title+suffix to fit the Switch to Tab box + // tie the box to the right boundary + switchToTabBoxLeadingConstraint.isActive = false + switchToTabBoxTrailingConstraint.isActive = true + // crop title+suffix to fit the Switch to Tab box + suffixTrailingConstraint.constant = Self.switchToTabBoxWidth + Constants.trailingSpace + Constants.switchToTabSuffixPadding + } else { + switchToTabBoxTrailingConstraint.isActive = false + // we can fit everything: align Switch to Tab box left edge after the suffix + switchToTabBoxLeadingConstraint.constant = textField!.frame.minX + textWidth + Constants.switchToTabSuffixPadding + switchToTabBoxLeadingConstraint.isActive = true + suffixTrailingConstraint.constant = Constants.trailingSpace + } + } + + super.layout() + } + } diff --git a/DuckDuckGo/Suggestions/View/SuggestionViewController.swift b/DuckDuckGo/Suggestions/View/SuggestionViewController.swift index f7a6765e40..4c61a44c0e 100644 --- a/DuckDuckGo/Suggestions/View/SuggestionViewController.swift +++ b/DuckDuckGo/Suggestions/View/SuggestionViewController.swift @@ -278,21 +278,15 @@ extension SuggestionViewController: NSTableViewDataSource { extension SuggestionViewController: NSTableViewDelegate { func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { - guard let suggestionTableCellView = tableView.makeView( - withIdentifier: NSUserInterfaceItemIdentifier(rawValue: SuggestionTableCellView.identifier), owner: self) - as? SuggestionTableCellView else { - assertionFailure("SuggestionViewController: Making of table cell view failed") - return nil - } + let cell = tableView.makeView(withIdentifier: SuggestionTableCellView.identifier, owner: self) as? SuggestionTableCellView ?? SuggestionTableCellView() guard let suggestionViewModel = suggestionContainerViewModel.suggestionViewModel(at: row) else { assertionFailure("SuggestionViewController: Failed to get suggestion") return nil } - suggestionTableCellView.isBurner = self.isBurner - suggestionTableCellView.display(suggestionViewModel) - return suggestionTableCellView + cell.display(suggestionViewModel, isBurner: self.isBurner) + return cell } func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { diff --git a/UnitTests/Freemium/DBP/FreemiumDBPPresenterTests.swift b/UnitTests/Freemium/DBP/FreemiumDBPPresenterTests.swift index 3a77f81b39..53f1decaa0 100644 --- a/UnitTests/Freemium/DBP/FreemiumDBPPresenterTests.swift +++ b/UnitTests/Freemium/DBP/FreemiumDBPPresenterTests.swift @@ -41,6 +41,7 @@ final class FreemiumDBPPresenterTests: XCTestCase { } private final class MockWindowControllerManager: WindowControllersManagerProtocol { + var mainWindowControllers: [DuckDuckGo_Privacy_Browser.MainWindowController] = [] var lastKeyMainWindowController: DuckDuckGo_Privacy_Browser.MainWindowController? diff --git a/UnitTests/RecentlyClosed/RecentlyClosedCoordinatorTests.swift b/UnitTests/RecentlyClosed/RecentlyClosedCoordinatorTests.swift index 9ff00af06c..d2f8036014 100644 --- a/UnitTests/RecentlyClosed/RecentlyClosedCoordinatorTests.swift +++ b/UnitTests/RecentlyClosed/RecentlyClosedCoordinatorTests.swift @@ -88,6 +88,8 @@ private extension RecentlyClosedWindow { } final class WindowControllersManagerMock: WindowControllersManagerProtocol { + var mainWindowControllers: [DuckDuckGo_Privacy_Browser.MainWindowController] = [] + var pinnedTabsManager = PinnedTabsManager(tabCollection: .init()) var didRegisterWindowController = PassthroughSubject<(MainWindowController), Never>() diff --git a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift index 9bf1d116bd..244a14b343 100644 --- a/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift +++ b/UnitTests/Suggestions/ViewModel/SuggestionContainerViewModelTests.swift @@ -59,6 +59,7 @@ final class SuggestionContainerViewModelTests: XCTestCase { // MARK: - Tests + @MainActor func testWhenSelectionIndexIsNilThenSelectedSuggestionViewModelIsNil() { let suggestionContainer = SuggestionContainer(burnerMode: .regular) let suggestionContainerViewModel = SuggestionContainerViewModel(suggestionContainer: suggestionContainer) @@ -87,6 +88,7 @@ final class SuggestionContainerViewModelTests: XCTestCase { waitForExpectations(timeout: 0, handler: nil) } + @MainActor func testWhenSelectCalledWithIndexOutOfBoundsThenSelectedSuggestionViewModelIsNil() { let suggestionContainer = SuggestionContainer(burnerMode: .regular) let suggestionListViewModel = SuggestionContainerViewModel(suggestionContainer: suggestionContainer) From 04e7a4798c6bf65ca5139ebce146c9e38cc01e28 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 24 Dec 2024 15:46:51 +0600 Subject: [PATCH 11/11] deduplicate internal page suggestions --- .../Model/SuggestionContainer.swift | 42 +++++++++++++++---- .../View/WindowControllersManager.swift | 8 +++- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 19730cbeb0..f409d42cd0 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -38,6 +38,8 @@ final class SuggestionContainer { private let startupPreferences: StartupPreferences private let featureFlagger: FeatureFlagger private let loading: SuggestionLoading + private let burnerMode: BurnerMode + private let windowControllersManager: WindowControllersManagerProtocol // Used for presenting the same suggestions after the removal of the local suggestion private(set) var suggestionDataCache: Data? @@ -46,13 +48,16 @@ final class SuggestionContainer { fileprivate let suggestionsURLSession = URLSession(configuration: .ephemeral) - init(openTabsProvider: @escaping OpenTabsProvider, suggestionLoading: SuggestionLoading, historyCoordinating: HistoryCoordinating, bookmarkManager: BookmarkManager, startupPreferences: StartupPreferences = .shared, featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger) { + init(openTabsProvider: @escaping OpenTabsProvider, suggestionLoading: SuggestionLoading, historyCoordinating: HistoryCoordinating, bookmarkManager: BookmarkManager, startupPreferences: StartupPreferences = .shared, featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger, burnerMode: BurnerMode, + windowControllersManager: WindowControllersManagerProtocol? = nil) { self.openTabsProvider = openTabsProvider self.bookmarkManager = bookmarkManager self.historyCoordinating = historyCoordinating self.startupPreferences = startupPreferences self.featureFlagger = featureFlagger self.loading = suggestionLoading + self.burnerMode = burnerMode + self.windowControllersManager = windowControllersManager ?? WindowControllersManager.shared } @MainActor @@ -66,7 +71,9 @@ final class SuggestionContainer { windowControllersManager: windowControllersManager), suggestionLoading: SuggestionLoader(urlFactory: urlFactory), historyCoordinating: HistoryCoordinator.shared, - bookmarkManager: LocalBookmarkManager.shared) + bookmarkManager: LocalBookmarkManager.shared, + burnerMode: burnerMode, + windowControllersManager: windowControllersManager) } func getSuggestions(for query: String, useCachedData: Bool = false) { @@ -104,7 +111,7 @@ final class SuggestionContainer { private static func defaultOpenTabsProvider(burnerMode: BurnerMode, windowControllersManager: WindowControllersManagerProtocol) -> OpenTabsProvider { { @MainActor in let selectedTab = windowControllersManager.selectedTab - let openTabViewModels = windowControllersManager.allTabViewModels(for: burnerMode) + let openTabViewModels = windowControllersManager.allTabViewModels(for: burnerMode, includingPinnedTabs: true) var usedUrls = Set() // deduplicate return openTabViewModels.compactMap { model in guard model.tab !== selectedTab, @@ -140,19 +147,36 @@ extension SuggestionContainer: SuggestionLoadingDataSource { } @MainActor func internalPages(for suggestionLoading: Suggestions.SuggestionLoading) -> [Suggestions.InternalPage] { - [ - // suggestions for Bookmarks&Settings - .init(title: UserText.bookmarks, url: .bookmarks), - .init(title: UserText.settings, url: .settings), - ] + PreferencePaneIdentifier.allCases.map { + var result = [Suggestions.InternalPage]() + let openTabs = windowControllersManager.allTabViewModels(for: burnerMode, includingPinnedTabs: true) + var isSettingsOpened = false + var isBookmarksOpened = false + // suggestions for Bookmarks&Settings if not Switch to Tab suggestions + for tab in openTabs { + if tab.tabContent == .bookmarks { + isBookmarksOpened = true + } else if case .settings = tab.tabContent { + isSettingsOpened = true + } + if isBookmarksOpened && isSettingsOpened { break } + } + if !isBookmarksOpened { + result.append(.init(title: UserText.bookmarks, url: .bookmarks)) + } + if !isSettingsOpened { + result.append(.init(title: UserText.settings, url: .settings)) + } + result += PreferencePaneIdentifier.allCases.map { // preference panes URLs .init(title: UserText.settings + " → " + $0.displayName, url: .settingsPane($0)) - } + { + } + result += { guard startupPreferences.launchToCustomHomePage, let homePage = URL(string: startupPreferences.formattedCustomHomePageURL) else { return [] } // home page suggestion return [.init(title: UserText.homePage, url: homePage)] }() + return result } @MainActor func bookmarks(for suggestionLoading: SuggestionLoading) -> [Suggestions.Bookmark] { diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift index b0ac64f5b9..d9b3c8e741 100644 --- a/DuckDuckGo/Windows/View/WindowControllersManager.swift +++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift @@ -384,14 +384,18 @@ extension WindowControllersManagerProtocol { } } - func allTabViewModels(for burnerMode: BurnerMode) -> [TabViewModel] { - allTabCollectionViewModels + func allTabViewModels(for burnerMode: BurnerMode, includingPinnedTabs: Bool = false) -> [TabViewModel] { + var result = allTabCollectionViewModels .filter { tabCollectionViewModel in tabCollectionViewModel.burnerMode == burnerMode } .flatMap { $0.tabViewModels.values } + if includingPinnedTabs { + result += pinnedTabsManager.tabViewModels.values + } + return result } func windowController(for tabCollectionViewModel: TabCollectionViewModel) -> MainWindowController? {