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

Dismiss Pinned Tab Previews when exiting full screen or hovering the semaphore buttons #2788

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
12 changes: 12 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1822,6 +1822,10 @@
9FBD847B2BB3EC3300220859 /* MockAttributionOriginProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBD84792BB3EC3300220859 /* MockAttributionOriginProvider.swift */; };
9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; };
9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; };
9FDB93EB2BFEF71600AC50F6 /* Publishers.withLatestFrom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDB93EA2BFEF71600AC50F6 /* Publishers.withLatestFrom.swift */; };
9FDB93EC2BFEF71600AC50F6 /* Publishers.withLatestFrom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDB93EA2BFEF71600AC50F6 /* Publishers.withLatestFrom.swift */; };
9FDB93EE2BFEF84E00AC50F6 /* TabPreviewEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDB93ED2BFEF84E00AC50F6 /* TabPreviewEventsHandler.swift */; };
9FDB93EF2BFEF84E00AC50F6 /* TabPreviewEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDB93ED2BFEF84E00AC50F6 /* TabPreviewEventsHandler.swift */; };
9FEE98652B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; };
9FEE98662B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; };
9FEE98692B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; };
Expand Down Expand Up @@ -3541,6 +3545,8 @@
9FBD84762BB3E54200220859 /* InstallationAttributionPixelHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationAttributionPixelHandlerTests.swift; sourceTree = "<group>"; };
9FBD84792BB3EC3300220859 /* MockAttributionOriginProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAttributionOriginProvider.swift; sourceTree = "<group>"; };
9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFavoriteView.swift; sourceTree = "<group>"; };
9FDB93EA2BFEF71600AC50F6 /* Publishers.withLatestFrom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publishers.withLatestFrom.swift; sourceTree = "<group>"; };
9FDB93ED2BFEF84E00AC50F6 /* TabPreviewEventsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPreviewEventsHandler.swift; sourceTree = "<group>"; };
9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkView.swift; sourceTree = "<group>"; };
9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewModel.swift; sourceTree = "<group>"; };
9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogCoordinatorViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7466,6 +7472,7 @@
B6A9E46A2614618A0067D1B9 /* OperatingSystemVersionExtension.swift */,
B637273C26CCF0C200C8CB02 /* OptionalExtension.swift */,
B684592125C93BE000DC17B6 /* Publisher.asVoid.swift */,
9FDB93EA2BFEF71600AC50F6 /* Publishers.withLatestFrom.swift */,
B68C2FB127706E6A00BF2C7D /* ProcessExtension.swift */,
B684592625C93C0500DC17B6 /* Publishers.NestedObjectChanges.swift */,
B6AAAC3D26048F690029438D /* RandomAccessCollectionExtension.swift */,
Expand Down Expand Up @@ -7551,6 +7558,7 @@
children = (
AAC82C5F258B6CB5009B6B42 /* TabPreviewWindowController.swift */,
AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */,
9FDB93ED2BFEF84E00AC50F6 /* TabPreviewEventsHandler.swift */,
1DB67F272B6FE21D003DF243 /* Model */,
1DC6696E2B6CF08200AA0645 /* Services */,
);
Expand Down Expand Up @@ -9688,6 +9696,7 @@
3706FB0E293F65D500E42796 /* FaviconManager.swift in Sources */,
3706FB0F293F65D500E42796 /* ChromiumFaviconsReader.swift in Sources */,
4B0BD7B82A9FE6E600EF609D /* NetworkProtectionOnboardingMenu.swift in Sources */,
9FDB93EF2BFEF84E00AC50F6 /* TabPreviewEventsHandler.swift in Sources */,
3706FB10293F65D500E42796 /* SuggestionTableRowView.swift in Sources */,
3706FB11293F65D500E42796 /* DownloadsPreferences.swift in Sources */,
3706FB12293F65D500E42796 /* PasswordManagementItemList.swift in Sources */,
Expand All @@ -9697,6 +9706,7 @@
EE6666702B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */,
3706FB16293F65D500E42796 /* StoredPermission.swift in Sources */,
3706FB17293F65D500E42796 /* FirePopoverCollectionViewHeader.swift in Sources */,
9FDB93EC2BFEF71600AC50F6 /* Publishers.withLatestFrom.swift in Sources */,
85774B042A71CDD000DE0561 /* BlockMenuItem.swift in Sources */,
3706FB19293F65D500E42796 /* FireViewController.swift in Sources */,
B6E3E55C2BC0041A00A41922 /* DownloadListStoreMock.swift in Sources */,
Expand Down Expand Up @@ -10901,6 +10911,7 @@
B6106BAD26A7BF390013B453 /* PermissionState.swift in Sources */,
371C0A2927E33EDC0070591F /* FeedbackPresenter.swift in Sources */,
B66260DD29AC5D4300E9E3EE /* NavigationProtectionTabExtension.swift in Sources */,
9FDB93EE2BFEF84E00AC50F6 /* TabPreviewEventsHandler.swift in Sources */,
1D1A33492A6FEB170080ACED /* BurnerMode.swift in Sources */,
14505A08256084EF00272CC6 /* UserAgent.swift in Sources */,
987799F12999993C005D8EB6 /* LegacyBookmarkStore.swift in Sources */,
Expand Down Expand Up @@ -11215,6 +11226,7 @@
B69A14F22B4D6FE800B9417D /* AddBookmarkFolderPopoverViewModel.swift in Sources */,
4BE65478271FCD41008D1D63 /* PasswordManagementNoteItemView.swift in Sources */,
AA5C8F632591021700748EB7 /* NSApplicationExtension.swift in Sources */,
9FDB93EB2BFEF71600AC50F6 /* Publishers.withLatestFrom.swift in Sources */,
AA9E9A5625A3AE8400D1959D /* NSWindowExtension.swift in Sources */,
7BD3AF5D2A8E7AF1006F9F56 /* KeychainType+ClientDefault.swift in Sources */,
370A34B12AB24E3700C77F7C /* SyncDebugMenu.swift in Sources */,
Expand Down
67 changes: 67 additions & 0 deletions DuckDuckGo/Common/Extensions/Publishers.withLatestFrom.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// Publishers.withLatestFrom.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import Combine

// More Info:
// - RXMarbles: https://rxmarbles.com/#withLatestFrom
// - https://jasdev.me/notes/with-latest-from
extension Publisher {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also import CombineExt package which has same implementation by jasdev but if we’re not using the whole set of operators, probably no need it.


/// Upon an emission from self, emit the latest value from the
/// second publisher, if any exists.
///
/// - parameter other: A second publisher source.
///
/// - returns: A publisher containing the latest value from the second publisher, if any.
func withLatestFrom<Other: Publisher>(
_ other: Other
) -> AnyPublisher<Other.Output, Other.Failure> where Failure == Other.Failure {
withLatestFrom(other) { _, otherValue in otherValue }
}

/// Merges two publishers into a single publisher by combining each value
/// from self with the latest value from the second publisher, if any.
///
/// - parameter other: A second publisher source.
/// - parameter resultSelector: Function to invoke for each value from the self combined
/// with the latest value from the second source, if any.
///
/// - returns: A publisher containing the result of combining each value of the self
/// with the latest value from the second publisher, if any, using the
/// specified result selector function.
func withLatestFrom<Other: Publisher, Result>(
_ other: Other,
resultSelector: @escaping (Output, Other.Output) -> Result
) -> AnyPublisher<Result, Failure> where Other.Failure == Failure {
let upstream = share()

return other
.map { second in
upstream.map {
resultSelector($0, second)
}
}
.switchToLatest()
.zip(upstream) // `zip`ping and discarding `\.1` allows for upstream completions to be projected down immediately.
.map(\.0)
.eraseToAnyPublisher()
}

}
6 changes: 5 additions & 1 deletion DuckDuckGo/Common/View/AppKit/HoverTrackingArea.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ final class HoverTrackingArea: NSTrackingArea {
private var observers: [NSKeyValueObservation]?

init(owner: some Hoverable) {
super.init(rect: .zero, options: [.mouseEnteredAndExited, .activeInKeyWindow, .enabledDuringMouseDrag, .inVisibleRect], owner: owner, userInfo: nil)
super.init(rect: .zero, options: [.mouseEnteredAndExited, .mouseMoved, .activeInKeyWindow, .enabledDuringMouseDrag, .inVisibleRect], owner: owner, userInfo: nil)

observers = [
owner.observe(\.backgroundColor) { [weak self] _, _ in self?.updateLayer() },
Expand Down Expand Up @@ -115,6 +115,10 @@ final class HoverTrackingArea: NSTrackingArea {
view?.mouseExited(with: event)
}

@objc func mouseMoved(_ event: NSEvent) {
view?.mouseMoved(with: event)
}

private func mouseDownDidChange() {
guard let view else { return }

Expand Down
12 changes: 12 additions & 0 deletions DuckDuckGo/Common/View/AppKit/MouseOverView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Combine
@objc protocol MouseOverViewDelegate: AnyObject {

@objc optional func mouseOverView(_ mouseOverView: MouseOverView, isMouseOver: Bool)
@objc optional func mouseOverViewIsMoving(_ mouseOverView: MouseOverView)

@objc optional func mouseClickView(_ mouseClickView: MouseClickView, mouseDownEvent: NSEvent)
@objc optional func mouseClickView(_ mouseClickView: MouseClickView, mouseUpEvent: NSEvent)
Expand Down Expand Up @@ -59,6 +60,7 @@ internal class MouseOverView: NSControl, Hoverable {
}
}
}

@objc dynamic var isMouseDown: Bool = false

override class var cellClass: AnyClass? {
Expand Down Expand Up @@ -135,6 +137,16 @@ internal class MouseOverView: NSControl, Hoverable {
}
}

override func mouseMoved(with event: NSEvent) {
super.mouseMoved(with: event)

delegate?.mouseOverViewIsMoving?(self)

if eventTypeMask.contains(.init(type: event.type)), let action {
NSApp.sendAction(action, to: target, from: self)
}
}

override func mouseDown(with event: NSEvent) {
guard isMouseLocationInsideBounds(event.locationInWindow) else { return }

Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ final class PinnedTabsViewModel: ObservableObject {
}
}

@Published var mouseMoving: Void = ()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the beginning I was passing setting the CGPoint where the mouse location was, but I wasn’t really using it so I changed to Void. If it feels weird we can always change to CGPoint as we may need it in the future we need it


@Published var shouldDrawLastItemSeparator: Bool = true {
didSet {
updateItemsWithoutSeparator()
Expand Down
10 changes: 7 additions & 3 deletions DuckDuckGo/PinnedTabs/View/PinnedTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ struct PinnedTabView: View {
}

if controlActiveState == .key {
stack.onHover { [weak collectionModel, weak model] isHovered in
collectionModel?.hoveredItem = isHovered ? model : nil
}
stack
.onHover { [weak collectionModel, weak model] isHovered in
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An approach I tried was to make an operator called onContinuousHovering. The problem with that was that the preview would show again after exiting full screen. And removing mouseEntered(_:) from the Tracking Area view would cause issues to the overlay state of the view when exiting full screen. 😞

collectionModel?.hoveredItem = isHovered ? model : nil
}
.onMouseMoving { [weak collectionModel] in
collectionModel?.mouseMoving = ()
}
} else {
stack
}
Expand Down
61 changes: 36 additions & 25 deletions DuckDuckGo/TabBar/View/TabBarViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ final class TabBarViewController: NSViewController {
private var mouseDownCancellable: AnyCancellable?
private var cancellables = Set<AnyCancellable>()

private let tabPreviewEventsHandler: TabPreviewEventsHandler

@IBOutlet weak var shadowView: TabShadowView!

@IBOutlet weak var rightSideStackView: NSStackView!
Expand Down Expand Up @@ -89,10 +91,18 @@ final class TabBarViewController: NSViewController {
self.pinnedTabsViewModel = pinnedTabsViewModel
self.pinnedTabsView = pinnedTabsView
self.pinnedTabsHostingView = PinnedTabsHostingView(rootView: pinnedTabsView)
tabPreviewEventsHandler = TabPreviewEventsHandler(
pinnedTabHoveredIndexPublisher: pinnedTabsViewModel.$hoveredItemIndex.eraseToAnyPublisher(),
pinnedTabMouseMovingPublisher: pinnedTabsViewModel.$mouseMoving.eraseToAnyPublisher()
)
} else {
self.pinnedTabsViewModel = nil
self.pinnedTabsView = nil
self.pinnedTabsHostingView = nil
tabPreviewEventsHandler = TabPreviewEventsHandler(
pinnedTabHoveredIndexPublisher: Just(nil).eraseToAnyPublisher(),
pinnedTabMouseMovingPublisher: Just(()).eraseToAnyPublisher()
)
}

super.init(coder: coder)
Expand All @@ -107,6 +117,7 @@ final class TabBarViewController: NSViewController {
setupFireButton()
setupPinnedTabsView()
subscribeToTabModeChanges()
subscribeToTabPreviewEvents()
setupAddTabButton()
setupAsBurnerWindowIfNeeded()
}
Expand Down Expand Up @@ -245,13 +256,6 @@ final class TabBarViewController: NSViewController {
}
.store(in: &cancellables)

pinnedTabsViewModel.$hoveredItemIndex.dropFirst().removeDuplicates()
.debounce(for: 0.05, scheduler: DispatchQueue.main)
.sink { [weak self] index in
self?.pinnedTabsViewDidUpdateHoveredItem(to: index)
}
.store(in: &cancellables)

pinnedTabsViewModel.contextMenuActionPublisher
.sink { [weak self] action in
self?.handlePinnedTabContextMenuAction(action)
Expand All @@ -278,16 +282,21 @@ final class TabBarViewController: NSViewController {
.store(in: &cancellables)
}

private func pinnedTabsViewDidUpdateHoveredItem(to index: Int?) {
if let index = index {
showPinnedTabPreview(at: index)
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
if self.view.isMouseLocationInsideBounds() == false {
self.hideTabPreview(allowQuickRedisplay: true)
private func subscribeToTabPreviewEvents() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method receive events to show both pinned tabs and non pinned tabs.

tabPreviewEventsHandler.eventPublisher
.debounce(for: 0.05, scheduler: DispatchQueue.main)
.sink { [weak self] event in
guard let self else { return }
switch event {
case let .hide(allowQuickRedisplay, withDelay):
self.hideTabPreview(allowQuickRedisplay: allowQuickRedisplay, withDelay: withDelay)
case let .show(.pinned(index)):
self.showPinnedTabPreview(at: index)
case let .show(.unpinned(item)):
self.showTabPreview(for: item)
}
}
}
.store(in: &cancellables)
}

private func deselectTabAndSelectPinnedTab(at index: Int) {
Expand Down Expand Up @@ -613,8 +622,8 @@ final class TabBarViewController: NSViewController {
tabPreviewWindowController.show(parentWindow: window, topLeftPointInWindow: pointInWindow)
}

func hideTabPreview(allowQuickRedisplay: Bool = false) {
tabPreviewWindowController.hide(allowQuickRedisplay: allowQuickRedisplay)
func hideTabPreview(allowQuickRedisplay: Bool = false, withDelay: Bool = false) {
tabPreviewWindowController.hide(allowQuickRedisplay: allowQuickRedisplay, withDelay: withDelay)
}

}
Expand Down Expand Up @@ -1020,15 +1029,17 @@ extension TabBarViewController: NSCollectionViewDelegate {
extension TabBarViewController: TabBarViewItemDelegate {

func tabBarViewItem(_ tabBarViewItem: TabBarViewItem, isMouseOver: Bool) {
guard !isMouseOver else { return }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the mouse exit the area we dismiss the preview


if isMouseOver {
// Show tab preview for visible tab bar items
if collectionView.visibleRect.intersects(tabBarViewItem.view.frame) {
showTabPreview(for: tabBarViewItem)
}
} else {
tabPreviewWindowController.hide(allowQuickRedisplay: true, withDelay: true)
}
// Send an event to dismiss the preview when the mouse exits the area
tabPreviewEventsHandler.unpinnedTabMouseExited()
}

func tabBarViewItemMouseIsMoving(_ tabBarViewItem: TabBarViewItem) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the mouse moves within the tab we show the preview

guard collectionView.visibleRect.intersects(tabBarViewItem.view.frame) else { return }

// Send an event to show the preview when the mouse moves within the area
tabPreviewEventsHandler.unpinnedTabMouseEntered(tabBarViewItem: tabBarViewItem)
}

func tabBarViewItemCanBeDuplicated(_ tabBarViewItem: TabBarViewItem) -> Bool {
Expand Down
5 changes: 5 additions & 0 deletions DuckDuckGo/TabBar/View/TabBarViewItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct OtherTabBarViewItemsState {
protocol TabBarViewItemDelegate: AnyObject {

func tabBarViewItem(_ tabBarViewItem: TabBarViewItem, isMouseOver: Bool)
func tabBarViewItemMouseIsMoving(_ tabBarViewItem: TabBarViewItem)

func tabBarViewItemCanBeDuplicated(_ tabBarViewItem: TabBarViewItem) -> Bool
func tabBarViewItemCanBePinned(_ tabBarViewItem: TabBarViewItem) -> Bool
Expand Down Expand Up @@ -647,6 +648,10 @@ extension TabBarViewItem: MouseClickViewDelegate {
view.needsLayout = true
}

func mouseOverViewIsMoving(_ mouseOverView: MouseOverView) {
delegate?.tabBarViewItemMouseIsMoving(self)
}

func mouseClickView(_ mouseClickView: MouseClickView, otherMouseDownEvent: NSEvent) {
// close on middle-click
guard otherMouseDownEvent.buttonNumber == 2 else { return }
Expand Down
Loading
Loading