Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

File tree

12 files changed

+175
-45
lines changed

12 files changed

+175
-45
lines changed

DuckDuckGo/Bookmarks/View/BookmarkPopover.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,24 @@ final class BookmarkPopover: NSPopover {
2222

2323
var isNew = false
2424

25+
private weak var addressBar: NSView?
26+
27+
/// prefferred bounding box for the popover positioning
28+
override var boundingFrame: NSRect {
29+
guard let addressBar,
30+
let window = addressBar.window else { return .infinite }
31+
var frame = window.convertToScreen(addressBar.convert(addressBar.bounds, to: nil))
32+
33+
frame = frame.insetBy(dx: -36, dy: -window.frame.size.height)
34+
35+
return frame
36+
}
37+
2538
override init() {
2639
super.init()
2740

28-
behavior = .transient
41+
self.animates = false
42+
self.behavior = .transient
2943
setupContentController()
3044
}
3145

@@ -46,6 +60,11 @@ final class BookmarkPopover: NSPopover {
4660
}
4761
// swiftlint:enable force_cast
4862

63+
override func show(relativeTo positioningRect: NSRect, of positioningView: NSView, preferredEdge: NSRectEdge) {
64+
self.addressBar = positioningView.superview
65+
super.show(relativeTo: positioningRect, of: positioningView, preferredEdge: preferredEdge)
66+
}
67+
4968
}
5069

5170
extension BookmarkPopover: BookmarkPopoverViewControllerDelegate {

DuckDuckGo/Common/Extensions/NSPopoverExtension.swift

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,77 @@ import AppKit
2020

2121
extension NSPopover {
2222

23-
/// Shows the popover below the specified button with the popover's pin positioned in the middle of the button, and a specified y-offset for the pin.
24-
///
25-
/// - Parameters:
26-
/// - view: The button below which the popover should appear.
27-
/// - yOffset: The y-offset for the popover's pin position relative to the bottom of the button. Default is 5.0 points.
28-
func showBelow(_ view: NSView) {
29-
// Set the preferred edge to be the bottom edge of the button
30-
let preferredEdge: NSRectEdge = .maxY
31-
32-
// Calculate the positioning rect
33-
let viewFrame = view.bounds
34-
let pinPositionX = viewFrame.midX
35-
let positioningRect = NSRect(x: pinPositionX, y: 0, width: 0, height: 0)
36-
37-
// Show the popover
38-
self.show(relativeTo: positioningRect, of: view, preferredEdge: preferredEdge)
23+
static let defaultMainWindowMargin = 6.0
24+
25+
/// maximum margin from window edge
26+
@objc var mainWindowMargin: CGFloat {
27+
Self.defaultMainWindowMargin
28+
}
29+
30+
/// temporary value used to get popover‘s owner window while it‘s not yet set
31+
@TaskLocal
32+
private static var mainWindow: NSWindow?
33+
34+
var mainWindow: NSWindow? {
35+
self.contentViewController?.view.window?.parent ?? Self.mainWindow
36+
}
37+
38+
/// prefferred bounding box for the popover positioning
39+
@objc var boundingFrame: NSRect {
40+
guard let mainWindow else { return .infinite }
41+
42+
return mainWindow.frame.insetBy(dx: mainWindowMargin, dy: 0)
43+
.intersection(mainWindow.screen?.visibleFrame ?? .infinite)
44+
}
45+
46+
@objc func adjustFrame(_ frame: NSRect) -> NSRect {
47+
var frame = frame
48+
let boundingFrame = self.boundingFrame
49+
if !boundingFrame.isInfinite, boundingFrame.width > frame.width {
50+
frame.origin.x = min(max(frame.minX, boundingFrame.minX), boundingFrame.maxX - frame.width)
51+
}
52+
return frame
53+
}
54+
55+
/// Shows the popover below the specified rect inside the view bounds with the popover's pin positioned in the middle of the rect
56+
public func show(positionedBelow positioningRect: NSRect, in positioningView: NSView) {
57+
assert(!positioningView.isHidden && positioningView.alphaValue > 0)
58+
59+
// We tap into `_currentFrameOnScreenWithContentSize:outAnchorEdge:` to adjust popover position
60+
// inside bounds of its owner Main Window.
61+
// https://app.asana.com/0/1177771139624306/1202217488822824/f
62+
_=Self.swizzleCurrentFrameOnScreenOnce
63+
64+
// position popover at the middle of the positioningView
65+
let positioningRect = NSRect(x: positioningRect.midX - 1, y: positioningRect.origin.y, width: 2, height: positioningRect.height)
66+
let preferredEdge: NSRectEdge = positioningView.isFlipped ? .maxY : .minY
67+
68+
Self.$mainWindow.withValue(positioningView.window) {
69+
self.show(relativeTo: positioningRect, of: positioningView, preferredEdge: preferredEdge)
70+
}
71+
}
72+
73+
/// Shows the popover below the specified view with the popover's pin positioned in the middle of the view
74+
func show(positionedBelow view: NSView) {
75+
self.show(positionedBelow: view.bounds, in: view)
76+
}
77+
78+
static let currentFrameOnScreenWithContentSizeSelector = NSSelectorFromString("_currentFrameOnScreenWithContentSize:outAnchorEdge:")
79+
80+
private static let swizzleCurrentFrameOnScreenOnce: () = {
81+
guard let originalMethod = class_getInstanceMethod(NSPopover.self, currentFrameOnScreenWithContentSizeSelector),
82+
let swizzledMethod = class_getInstanceMethod(NSPopover.self, #selector(currentFrameOnScreenWithContentSize)) else {
83+
assertionFailure("Methods not available")
84+
return
85+
}
86+
87+
method_exchangeImplementations(originalMethod, swizzledMethod)
88+
}()
89+
90+
// place popover inside bounds of its owner Main Window
91+
@objc(swizzled_currentFrameOnScreenWithContentSize:outAnchorEdge:)
92+
private dynamic func currentFrameOnScreenWithContentSize(size: NSSize, outAnchorEdge: UnsafeRawPointer?) -> NSRect {
93+
self.adjustFrame(currentFrameOnScreenWithContentSize(size: size, outAnchorEdge: outAnchorEdge))
3994
}
4095

4196
}

DuckDuckGo/Common/Extensions/NSRectExtension.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ extension NSRect {
3737
}
3838

3939
// Apply an offset so that we don't get caught by the "Line of Death" https://textslashplain.com/2017/01/14/the-line-of-death/
40-
func insetFromLineOfDeath() -> NSRect {
41-
return insetBy(dx: 0, dy: -15)
40+
func insetFromLineOfDeath(flipped: Bool) -> NSRect {
41+
let offset = 3.0
42+
assert(height > offset * 2)
43+
return insetBy(dx: 0, dy: offset)
4244
}
4345

4446
}

DuckDuckGo/FileDownload/View/DownloadsPopover.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ final class DownloadsPopover: NSPopover {
2323
override init() {
2424
super.init()
2525

26+
self.animates = false
2627
self.behavior = .semitransient
2728

2829
setupContentController()

DuckDuckGo/Fire/View/FireCoordinator.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@ final class FireCoordinator {
5656
}
5757

5858
static func showFirePopover(relativeTo positioningView: NSView, tabCollectionViewModel: TabCollectionViewModel) {
59-
if !(firePopover?.isShown ?? false) {
60-
firePopover = FirePopover(fireViewModel: fireViewModel, tabCollectionViewModel: tabCollectionViewModel)
61-
firePopover?.showBelow(positioningView)
59+
guard !(firePopover?.isShown ?? false) else {
60+
firePopover?.close()
61+
return
6262
}
63+
firePopover = FirePopover(fireViewModel: fireViewModel, tabCollectionViewModel: tabCollectionViewModel)
64+
firePopover?.show(positionedBelow: positioningView.bounds.insetBy(dx: 0, dy: 3), in: positioningView)
6365
}
6466

6567
}

DuckDuckGo/Fire/View/FirePopover.swift

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,36 @@ import AppKit
2020

2121
final class FirePopover: NSPopover {
2222

23+
override var mainWindowMargin: CGFloat { -14 }
24+
25+
private static let minScreenEdgeMargin = 10.0
26+
private static let defaultScreenEdgeCorrection = 12.0
27+
28+
// always position the Fire popover by the right edge
29+
override func adjustFrame(_ frame: NSRect) -> NSRect {
30+
let boundingFrame = self.boundingFrame
31+
guard !boundingFrame.isInfinite else { return frame }
32+
guard let popoverWindow = self.contentViewController?.view.window else {
33+
assertionFailure("no popover window")
34+
return frame
35+
}
36+
37+
var frame = frame
38+
frame.origin.x = boundingFrame.maxX - popoverWindow.frame.width
39+
if let mainWindow = popoverWindow.parent,
40+
let screen = mainWindow.screen,
41+
mainWindow.frame.maxX > screen.visibleFrame.maxX - Self.defaultScreenEdgeCorrection {
42+
// close to the screen edge the Popover gets shifted to the left
43+
frame.origin.x += Self.defaultScreenEdgeCorrection - (screen.visibleFrame.maxX - mainWindow.frame.maxX)
44+
}
45+
return frame
46+
}
47+
2348
init(fireViewModel: FireViewModel, tabCollectionViewModel: TabCollectionViewModel) {
2449
super.init()
2550

26-
self.behavior = .transient
51+
self.animates = false
52+
self.behavior = .semitransient
2753

2854
setupContentController(fireViewModel: fireViewModel, tabCollectionViewModel: tabCollectionViewModel)
2955
}
@@ -38,10 +64,9 @@ final class FirePopover: NSPopover {
3864

3965
private func setupContentController(fireViewModel: FireViewModel, tabCollectionViewModel: TabCollectionViewModel) {
4066
let storyboard = NSStoryboard(name: "Fire", bundle: nil)
41-
let controller = storyboard.instantiateController(
42-
identifier: "FirePopoverWrapperViewController") { coder -> FirePopoverWrapperViewController? in
43-
return FirePopoverWrapperViewController(coder: coder, fireViewModel: fireViewModel, tabCollectionViewModel: tabCollectionViewModel)
44-
}
67+
let controller = storyboard.instantiateController(identifier: "FirePopoverWrapperViewController") { coder -> FirePopoverWrapperViewController? in
68+
return FirePopoverWrapperViewController(coder: coder, fireViewModel: fireViewModel, tabCollectionViewModel: tabCollectionViewModel)
69+
}
4570
contentViewController = controller
4671
}
4772

DuckDuckGo/Menus/MainMenuActions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ extension AppDelegate {
240240
assertionFailure("No reference to main window controller")
241241
return
242242
}
243-
windowController.mainViewController.browserTabViewController.openNewTab(with: .url(URL.duckDuckGoEmailLogin))
243+
windowController.mainViewController.browserTabViewController.openNewTab(with: .url(URL.duckDuckGoEmailLogin))
244244
}
245245

246246
}

DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ final class AddressBarButtonsViewController: NSViewController {
292292
bookmarkButton.isHidden = false
293293
bookmarkPopover.isNew = result.isNew
294294
bookmarkPopover.viewController.bookmark = bookmark
295-
bookmarkPopover.show(relativeTo: bookmarkButton.bounds, of: bookmarkButton, preferredEdge: .maxY)
295+
bookmarkPopover.show(positionedBelow: bookmarkButton)
296296
} else {
297297
updateBookmarkButtonVisibility()
298298
bookmarkPopover.close()
@@ -356,7 +356,7 @@ final class AddressBarButtonsViewController: NSViewController {
356356

357357
let positioningViewInWindow = privacyDashboardPositioningView.convert(privacyDashboardPositioningView.bounds, to: view.window?.contentView)
358358
privacyDashboardPopover.setPreferredMaxHeight(positioningViewInWindow.origin.y)
359-
privacyDashboardPopover.show(relativeTo: privacyDashboardPositioningView.bounds, of: privacyDashboardPositioningView, preferredEdge: .maxY)
359+
privacyDashboardPopover.show(positionedBelow: privacyDashboardPositioningView)
360360

361361
privacyEntryPointButton.state = .on
362362

DuckDuckGo/NavigationBar/View/NavigationBarPopovers.swift

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ final class NavigationBarPopovers {
9797
@available(macOS 11.4, *)
9898
func toggleNetworkProtectionPopover(usingView view: NSView, withDelegate delegate: NSPopoverDelegate) {
9999
#if NETWORK_PROTECTION
100-
if let networkProtectionPopover = networkProtectionPopover,
101-
networkProtectionPopover.isShown {
100+
if let networkProtectionPopover, networkProtectionPopover.isShown {
102101
networkProtectionPopover.close()
103102
} else {
104103
showNetworkProtectionPopover(usingView: view, withDelegate: delegate)
@@ -121,7 +120,7 @@ final class NavigationBarPopovers {
121120
popover.viewController.delegate = downloadsDelegate
122121
downloadsPopover = popover
123122

124-
show(popover: popover, usingView: view, preferredEdge: .maxY)
123+
show(popover, positionedBelow: view)
125124
}
126125

127126
private var downloadsPopoverTimer: Timer?
@@ -161,6 +160,12 @@ final class NavigationBarPopovers {
161160
downloadsPopover?.close()
162161
}
163162

163+
#if NETWORK_PROTECTION
164+
if networkProtectionPopover?.isShown ?? false {
165+
networkProtectionPopover?.close()
166+
}
167+
#endif
168+
164169
return true
165170
}
166171

@@ -176,7 +181,7 @@ final class NavigationBarPopovers {
176181
}
177182

178183
LocalBookmarkManager.shared.requestSync()
179-
show(popover: popover, usingView: view)
184+
show(popover, positionedBelow: view)
180185
}
181186

182187
func showPasswordManagementPopover(selectedCategory: SecureVaultSorting.Category?, usingView view: NSView, withDelegate delegate: NSPopoverDelegate) {
@@ -186,15 +191,15 @@ final class NavigationBarPopovers {
186191
passwordManagementPopover = popover
187192
popover.viewController.domain = passwordManagementDomain
188193
popover.delegate = delegate
189-
show(popover: popover, usingView: view)
194+
show(popover, positionedBelow: view)
190195
popover.select(category: selectedCategory)
191196
}
192197

193198
func showPasswordManagerPopover(selectedWebsiteAccount: SecureVaultModels.WebsiteAccount, usingView view: NSView, withDelegate delegate: NSPopoverDelegate) {
194199
let popover = passwordManagementPopover ?? PasswordManagementPopover()
195200
passwordManagementPopover = popover
196201
popover.delegate = delegate
197-
show(popover: popover, usingView: view)
202+
show(popover, positionedBelow: view)
198203
popover.select(websiteAccount: selectedWebsiteAccount)
199204
}
200205

@@ -251,27 +256,27 @@ final class NavigationBarPopovers {
251256
let popover = SaveCredentialsPopover()
252257
popover.delegate = delegate
253258
saveCredentialsPopover = popover
254-
show(popover: popover, usingView: view)
259+
show(popover, positionedBelow: view)
255260
}
256261

257262
private func showSavePaymentMethodPopover(usingView view: NSView, withDelegate delegate: NSPopoverDelegate) {
258263
let popover = SavePaymentMethodPopover()
259264
popover.delegate = delegate
260265
savePaymentMethodPopover = popover
261-
show(popover: popover, usingView: view)
266+
show(popover, positionedBelow: view)
262267
}
263268

264269
private func showSaveIdentityPopover(usingView view: NSView, withDelegate delegate: NSPopoverDelegate) {
265270
let popover = SaveIdentityPopover()
266271
popover.delegate = delegate
267272
saveIdentityPopover = popover
268-
show(popover: popover, usingView: view)
273+
show(popover, positionedBelow: view)
269274
}
270275

271-
private func show(popover: NSPopover, usingView view: NSView, preferredEdge edge: NSRectEdge = .minY) {
276+
private func show(_ popover: NSPopover, positionedBelow view: NSView) {
272277
view.isHidden = false
273278

274-
popover.show(relativeTo: view.bounds.insetFromLineOfDeath(), of: view, preferredEdge: edge)
279+
popover.show(positionedBelow: view.bounds.insetFromLineOfDeath(flipped: view.isFlipped), in: view)
275280
}
276281

277282
// MARK: - Network Protection
@@ -312,7 +317,7 @@ final class NavigationBarPopovers {
312317
networkProtectionPopover = popover
313318
return popover
314319
}()
315-
show(popover: popover, usingView: view, preferredEdge: .maxY)
320+
show(popover, positionedBelow: view)
316321
}
317322
#endif
318323
}

DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,8 @@ final class NavigationBarViewController: NSViewController {
256256
passwordManagerCoordinator: PasswordManagerCoordinator.shared,
257257
internalUserDecider: internalUserDecider)
258258
menu.actionDelegate = self
259-
menu.popUp(positioning: nil, at: NSPoint(x: 0, y: sender.bounds.height + 4), in: sender)
259+
let location = NSPoint(x: -menu.size.width + sender.bounds.width, y: sender.bounds.height + 4)
260+
menu.popUp(positioning: nil, at: location, in: sender)
260261
}
261262
// swiftlint:enable force_cast
262263

0 commit comments

Comments
 (0)