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 0000000000..2879577530 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/Arrow-Right-12.imageset/Arrow-Right-12.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/Arrow-Right-12.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Arrow-Right-12.imageset/Contents.json new file mode 100644 index 0000000000..37afaa8445 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Arrow-Right-12.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Arrow-Right-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} 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..bb34ab7673 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Window-Tabbed-16D 1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Window-Tabbed-16D 1.pdf b/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Window-Tabbed-16D 1.pdf new file mode 100644 index 0000000000..15c3bc9bb4 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/OpenTabSuggestion.imageset/Window-Tabbed-16D 1.pdf differ diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index c1ff976972..15e9c5cdd9 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -253,6 +253,9 @@ struct UserText { static let searchDuckDuckGoSuffix = NSLocalizedString("address.bar.search.suffix", value: "Search DuckDuckGo", comment: "Suffix of searched terms in address bar. Example: best watching machine . Search DuckDuckGo") + static let duckDuckGoSearchSuffix = NSLocalizedString("address.bar.search.open.tab.suffix", + value: "DuckDuckGo Search", + comment: "Suffix of DuckDuckGo Search open tab suggestion. Example: cats – DuckDuckGo Search") static let addressBarVisitSuffix = NSLocalizedString("address.bar.visit.suffix", value: "Visit", comment: "Address bar suffix of possibly visited website. Example: spreadprivacy.com . Visit spreadprivacy.com") @@ -1435,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/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/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index b90ea9b9b0..98c11e2ca2 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -1229,6 +1229,18 @@ } } }, + "address.bar.search.open.tab.suffix" : { + "comment" : "Suffix of DuckDuckGo Search open tab suggestion. Example: cats – DuckDuckGo Search", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "DuckDuckGo Search" + } + } + } + }, "address.bar.search.suffix" : { "comment" : "Suffix of searched terms in address bar. Example: best watching machine . Search DuckDuckGo", "extractionState" : "extracted_with_value", @@ -63955,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/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/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 663ffa6019..2966804cc8 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -747,10 +747,12 @@ final class AddressBarButtonsViewController: NSViewController { imageButton.image = .web case .browsing: imageButton.image = tabViewModel.favicon - case .editing(isUrl: true): + case .editing(.url): imageButton.image = .web - case .editing(isUrl: false): + case .editing(.text): imageButton.image = .search + case .editing(.openTabSuggestion): + imageButton.image = .openTabSuggestion default: imageButton.image = nil } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index 2ebc439811..acba5587ac 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -370,7 +370,13 @@ final class AddressBarTextField: NSTextField { PixelKit.fire(autocompletePixel) } - if NSApp.isCommandPressed { + 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) } else { hideSuggestionWindow() @@ -486,6 +492,10 @@ final class AddressBarTextField: NSTextField { } } + private func switchTo(_ tab: OpenTab) { + WindowControllersManager.shared.show(url: tab.url, source: .switchToOpenTab, newTab: true /* in case not found */) + } + private func makeUrl(suggestion: Suggestion?, stringValueWithoutSuffix: String, completion: @escaping (URL?, String, Bool) -> Void) { let finalUrl: URL? let userEnteredValue: String @@ -890,8 +900,7 @@ extension AddressBarTextField { case .bookmark(title: _, url: let url, isFavorite: _, allowedInTopHits: _), .historyEntry(title: _, url: let url, allowedInTopHits: _), - .internalPage(title: _, url: let url), - .openTab(title: _, url: let url): + .internalPage(title: _, url: let url): if let title = suggestionViewModel.title, !title.isEmpty, suggestionViewModel.autocompletionString != title { @@ -901,7 +910,8 @@ extension AddressBarTextField { } else { self = .url(url) } - + case .openTab(title: _, url: let url): + self = .openTab(url) case .unknown: self = Suffix.search } @@ -911,6 +921,7 @@ extension AddressBarTextField { case visit(host: String) case url(URL) case title(String) + case openTab(URL) func toAttributedString(size: CGFloat, isBurner: Bool) -> NSAttributedString { let suffixColor = isBurner ? NSColor.burnerAccent : NSColor.addressBarSuffix @@ -922,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 { @@ -930,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 } @@ -1037,14 +1052,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/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index 2d98d5f052..b7686702eb 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -33,6 +33,9 @@ final class AddressBarViewController: NSViewController, ObservableObject { @IBOutlet var passiveTextFieldMinXConstraint: NSLayoutConstraint! @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 private let popovers: NavigationBarPopovers? @@ -46,7 +49,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 +63,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 +76,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 } @@ -88,7 +102,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 +110,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 @@ -115,6 +129,9 @@ final class AddressBarViewController: NSViewController, ObservableObject { addressBarTextField.placeholderString = UserText.addressBarPlaceholder 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 activeTextFieldMinXConstraint.isActive = false @@ -154,6 +171,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() @@ -224,6 +248,7 @@ final class AddressBarViewController: NSViewController, ObservableObject { updateMode(value: value) addressBarButtonsViewController?.textFieldValue = value updateView() + updateSwitchToTabBoxAppearance() } .store(in: &cancellables) } @@ -360,6 +385,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 +421,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,17 +439,21 @@ 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) } } } - @objc private func refreshAddressBarAppearance(_ sender: Any) { + @objc private func refreshAddressBarAppearance(_ sender: Any?) { self.updateMode() self.addressBarButtonsViewController?.updateButtons() @@ -420,6 +468,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 +477,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..142b992105 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,9 @@ + + + @@ -621,7 +681,7 @@