Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to prefer new tabs to windows when opening links #2687

Merged
merged 7 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ struct UserText {
static let addressLabel = NSLocalizedString("preferences-homepage-address", value: "Address:", comment: "Homepage address field label")

static let tabs = NSLocalizedString("preferences-tabs.title", value: "Tabs", comment: "Title for tabs section in settings")
static let preferNewTabsToWindows = NSLocalizedString("preferences-tabs.prefer.new.tabs.to.windows", value: "Prefer new tabs instead of new windows when opening links", comment: "Option to prefer opening new tabs instead of windows when opening links")
static let switchToNewTabWhenOpened = NSLocalizedString("preferences-tabs.switch.tab.when.opened", value: "Always switch to a new tab when opened", comment: "Option to switch to a new tab when it is opened")

static func homeButtonMode(for position: HomeButtonPosition) -> String {
Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public struct UserDefaultsWrapper<T> {
case currentThemeName = "com.duckduckgo.macos.currentThemeNameKey"
case showFullURL = "preferences.appearance.show-full-url"
case showAutocompleteSuggestions = "preferences.appearance.show-autocomplete-suggestions"
case preferNewTabsToWindows = "preferences.tabs.prefer-new-tabs-to-windows"
case switchToNewTabWhenOpened = "preferences.tabs.switch-to-new-tab-when-opened"
case defaultPageZoom = "preferences.appearance.default-page-zoom"
case bookmarksBarAppearance = "preferences.appearance.bookmarks-bar"
Expand Down
12 changes: 12 additions & 0 deletions DuckDuckGo/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -43249,6 +43249,18 @@
}
}
},
"preferences-tabs.prefer.new.tabs.to.windows" : {
"comment" : "Option to prefer opening new tabs instead of windows when opening links",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Prefer new tabs instead of new windows when opening links"
}
}
}
},
"preferences-tabs.switch.tab.when.opened" : {
"comment" : "Option to switch to a new tab when it is opened",
"extractionState" : "extracted_with_value",
Expand Down
11 changes: 11 additions & 0 deletions DuckDuckGo/Preferences/Model/TabsPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ import Foundation

protocol TabsPreferencesPersistor {
var switchToNewTabWhenOpened: Bool { get set }
var preferNewTabsToWindows: Bool { get set }
}

struct TabsPreferencesUserDefaultsPersistor: TabsPreferencesPersistor {
@UserDefaultsWrapper(key: .preferNewTabsToWindows, defaultValue: true)
var preferNewTabsToWindows: Bool

@UserDefaultsWrapper(key: .switchToNewTabWhenOpened, defaultValue: false)
var switchToNewTabWhenOpened: Bool
}
Expand All @@ -31,6 +35,12 @@ final class TabsPreferences: ObservableObject, PreferencesTabOpening {

static let shared = TabsPreferences()

@Published var preferNewTabsToWindows: Bool {
didSet {
persistor.preferNewTabsToWindows = preferNewTabsToWindows
}
}

@Published var switchToNewTabWhenOpened: Bool {
didSet {
persistor.switchToNewTabWhenOpened = switchToNewTabWhenOpened
Expand All @@ -39,6 +49,7 @@ final class TabsPreferences: ObservableObject, PreferencesTabOpening {

init(persistor: TabsPreferencesPersistor = TabsPreferencesUserDefaultsPersistor()) {
self.persistor = persistor
preferNewTabsToWindows = persistor.preferNewTabsToWindows
switchToNewTabWhenOpened = persistor.switchToNewTabWhenOpened
}

Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/Preferences/View/PreferencesGeneralView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ extension Preferences {
// SECTION 2: Tabs
PreferencePaneSection(UserText.tabs) {
PreferencePaneSubSection {
ToggleMenuItem(UserText.preferNewTabsToWindows, isOn: $tabsModel.preferNewTabsToWindows)
ToggleMenuItem(UserText.switchToNewTabWhenOpened, isOn: $tabsModel.switchToNewTabWhenOpened)
}
}
Expand Down
31 changes: 26 additions & 5 deletions DuckDuckGo/Tab/Model/NewWindowPolicy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ import Foundation
import WebKit

enum NewWindowPolicy {
case tab(selected: Bool, burner: Bool)
case tab(selected: Bool, burner: Bool, contextMenuInitiated: Bool = false)
case popup(origin: NSPoint?, size: NSSize?)
case window(active: Bool, burner: Bool)

init(_ windowFeatures: WKWindowFeatures, shouldSelectNewTab: Bool = false, isBurner: Bool) {
init(_ windowFeatures: WKWindowFeatures, shouldSelectNewTab: Bool = false, isBurner: Bool, contextMenuInitiated: Bool = false) {
if windowFeatures.toolbarsVisibility?.boolValue == true {
self = .tab(selected: shouldSelectNewTab,
burner: isBurner)
burner: isBurner,
contextMenuInitiated: contextMenuInitiated)
} else if windowFeatures.width != nil {
self = .popup(origin: windowFeatures.origin, size: windowFeatures.size)

Expand All @@ -37,7 +38,7 @@ enum NewWindowPolicy {
// See https://app.asana.com/0/1177771139624306/1205690527704551/f.
if #available(macOS 14.1, *),
windowFeatures.statusBarVisibility == nil && windowFeatures.menuBarVisibility == nil {
self = .tab(selected: shouldSelectNewTab, burner: isBurner)
self = .tab(selected: shouldSelectNewTab, burner: isBurner, contextMenuInitiated: contextMenuInitiated)

} else {
self = .window(active: true, burner: isBurner)
Expand All @@ -49,10 +50,30 @@ enum NewWindowPolicy {
return false
}
var isSelectedTab: Bool {
if case .tab(selected: true, burner: _) = self { return true }
if case .tab(selected: true, burner: _, contextMenuInitiated: _) = self { return true }
return false
}

/**
* Replaces `.tab` with `.window` when user prefers windows over tabs.
*/
func preferringTabsToWindows(_ prefersTabsToWindows: Bool) -> NewWindowPolicy {
guard case .tab(_, let isBurner, contextMenuInitiated: false) = self, !prefersTabsToWindows else {
return self
}
return .window(active: true, burner: isBurner)
}

/**
* Forces selecting a tab if `true` is passed as argument.
*/
func preferringSelectedTabs(_ prefersSelectedTabs: Bool) -> NewWindowPolicy {
guard case .tab(selected: false, burner: let isBurner, contextMenuInitiated: let contextMenuInitiated) = self, prefersSelectedTabs else {
return self
}
return .tab(selected: true, burner: isBurner, contextMenuInitiated: contextMenuInitiated)
}

}

extension WKWindowFeatures {
Expand Down
11 changes: 4 additions & 7 deletions DuckDuckGo/Tab/Model/Tab+UIDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,12 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate {
windowFeatures: WKWindowFeatures,
completionHandler: @escaping (WKWebView?) -> Void) {

switch newWindowPolicy(for: navigationAction) {
switch newWindowPolicy(for: navigationAction)?.preferringTabsToWindows(tabsPreferences.preferNewTabsToWindows) {
// popup kind is known, action doesn‘t require Popup Permission
case .allow(.tab(selected: false, let burner)):
// update selected flag based on tab preferences
let targetKind = NewWindowPolicy.tab(selected: tabsPreferences.switchToNewTabWhenOpened, burner: burner)
completionHandler(self.createWebView(from: webView, with: configuration, for: navigationAction, of: targetKind))
return
case .allow(let targetKind):
// proceed to web view creation
completionHandler(self.createWebView(from: webView, with: configuration, for: navigationAction, of: targetKind))
completionHandler(self.createWebView(from: webView, with: configuration,
for: navigationAction, of: targetKind.preferringSelectedTabs(tabsPreferences.switchToNewTabWhenOpened)))
return
case .cancel:
// navigation action was handled before and cancelled
Expand All @@ -107,6 +103,7 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate {
let shouldSelectNewTab = !NSApp.isCommandPressed || tabsPreferences.switchToNewTabWhenOpened // this is actually not correct, to be fixed later
// try to guess popup kind from provided windowFeatures
let targetKind = NewWindowPolicy(windowFeatures, shouldSelectNewTab: shouldSelectNewTab, isBurner: burnerMode.isBurner)
.preferringTabsToWindows(tabsPreferences.preferNewTabsToWindows)

// action doesn‘t require Popup Permission as it‘s user-initiated
// TO BE FIXED: this also opens a new window when a popup ad is shown on click simultaneously with the main frame navigation:
Expand Down
12 changes: 11 additions & 1 deletion DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ import WebKit
enum NavigationDecision {
case allow(NewWindowPolicy)
case cancel

/**
* Replaces `.tab` with `.window` when user prefers windows over tabs.
*/
func preferringTabsToWindows(_ prefersTabsToWindows: Bool) -> NavigationDecision {
guard case .allow(let targetKind) = self, !prefersTabsToWindows else {
return self
}
return .allow(targetKind.preferringTabsToWindows(prefersTabsToWindows))
}
}

@MainActor
Expand Down Expand Up @@ -365,7 +375,7 @@ private extension ContextMenuManager {
}

onNewWindow = { [weak self] _ in
.allow(.tab(selected: self?.tabsPreferences.switchToNewTabWhenOpened ?? false, burner: burner))
.allow(.tab(selected: self?.tabsPreferences.switchToNewTabWhenOpened ?? false, burner: burner, contextMenuInitiated: true))
}
NSApp.sendAction(action, to: originalItem.target, from: originalItem)
}
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/Tab/View/BrowserTabViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,7 @@ extension BrowserTabViewController: TabDelegate {
case .window(active: let active, let isBurner):
assert(isBurner == childTab.burnerMode.isBurner)
WindowsManager.openNewWindow(with: childTab, showWindow: active)
case .tab(selected: let selected, _):
case .tab(selected: let selected, _, _):
self.tabCollectionViewModel.insert(childTab, after: parentTab, selected: selected)
}
}
Expand Down
7 changes: 6 additions & 1 deletion UnitTests/Preferences/TabsPreferencesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,29 @@ import XCTest
@testable import DuckDuckGo_Privacy_Browser

class MockTabsPreferencesPersistor: TabsPreferencesPersistor {
var preferNewTabsToWindows: Bool = false
var switchToNewTabWhenOpened: Bool = false
}

final class TabsPreferencesTests: XCTestCase {

func testWhenInitializedThenItLoadsPersistedValues() {
let mockPersistor = MockTabsPreferencesPersistor()
mockPersistor.preferNewTabsToWindows = true
mockPersistor.switchToNewTabWhenOpened = true
let tabsPreferences = TabsPreferences(persistor: mockPersistor)

XCTAssertTrue(tabsPreferences.preferNewTabsToWindows)
XCTAssertTrue(tabsPreferences.switchToNewTabWhenOpened)
}

func testWhenSwitchToNewTabUpdatedThenPersistorUpdates() {
func testWhenPreferencesUpdatedThenPersistorUpdates() {
let mockPersistor = MockTabsPreferencesPersistor()
let tabsPreferences = TabsPreferences(persistor: mockPersistor)
tabsPreferences.preferNewTabsToWindows = true
tabsPreferences.switchToNewTabWhenOpened = true

XCTAssertTrue(mockPersistor.preferNewTabsToWindows)
XCTAssertTrue(mockPersistor.switchToNewTabWhenOpened)
}
}
Loading