Skip to content

Commit

Permalink
Show in folder menu action bookmarks (#2988)
Browse files Browse the repository at this point in the history
  • Loading branch information
jotaemepereira committed Jul 23, 2024
1 parent 13e8377 commit 832d72d
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 17 deletions.
54 changes: 39 additions & 15 deletions DuckDuckGo/Bookmarks/Services/ContextualMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@ import AppKit

enum ContextualMenu {

static func menu(for objects: [Any]?) -> NSMenu? {
menu(for: objects, target: nil)
static func menu(for objects: [Any]?, forSearch: Bool = false) -> NSMenu? {
menu(for: objects, target: nil, forSearch: forSearch)
}

/// Creates an instance of NSMenu for the specified Objects and target.
/// - Parameters:
/// - objects: The objects to create the menu for.
/// - target: The target to associate to the `NSMenuItem`
/// - forSearch: Boolean that indicates if a bookmark search is currently happening.
/// - Returns: An instance of NSMenu or nil if `objects` is not a `Bookmark` or a `Folder`.
static func menu(for objects: [Any]?, target: AnyObject?) -> NSMenu? {
static func menu(for objects: [Any]?, target: AnyObject?, forSearch: Bool = false) -> NSMenu? {

guard let objects = objects, objects.count > 0 else {
return menuForNoSelection()
Expand All @@ -45,7 +46,7 @@ enum ContextualMenu {

guard let object else { return nil }

let menu = menu(for: object, parentFolder: parentFolder)
let menu = menu(for: object, parentFolder: parentFolder, forSearch: forSearch)

menu?.items.forEach { item in
item.target = target
Expand All @@ -59,14 +60,15 @@ enum ContextualMenu {
/// - Parameters:
/// - entity: The bookmark entity to create the menu for.
/// - parentFolder: An optional `BookmarkFolder`.
/// - forSearch: Boolean that indicates if a bookmark search is currently happening.
/// - Returns: An instance of NSMenu or nil if `entity` is not a `Bookmark` or a `Folder`.
static func menu(for entity: BaseBookmarkEntity, parentFolder: BookmarkFolder?) -> NSMenu? {
static func menu(for entity: BaseBookmarkEntity, parentFolder: BookmarkFolder?, forSearch: Bool = false) -> NSMenu? {
let menu: NSMenu?
if let bookmark = entity as? Bookmark {
menu = self.menu(for: bookmark, parent: parentFolder, isFavorite: bookmark.isFavorite)
menu = self.menu(for: bookmark, parent: parentFolder, isFavorite: bookmark.isFavorite, forSearch: forSearch)
} else if let folder = entity as? BookmarkFolder {
// When the user edits a folder we need to show the parent in the folder picker. Folders directly child of PseudoFolder `Bookmarks` have nil parent because their parent is not an instance of `BookmarkFolder`
menu = self.menu(for: folder, parent: parentFolder)
menu = self.menu(for: folder, parent: parentFolder, forSearch: forSearch)
} else {
menu = nil
}
Expand Down Expand Up @@ -101,16 +103,16 @@ private extension ContextualMenu {
NSMenu(items: [addFolderMenuItem(folder: nil)])
}

static func menu(for bookmark: Bookmark?, parent: BookmarkFolder?, isFavorite: Bool) -> NSMenu {
NSMenu(items: menuItems(for: bookmark, parent: parent, isFavorite: isFavorite))
static func menu(for bookmark: Bookmark?, parent: BookmarkFolder?, isFavorite: Bool, forSearch: Bool = false) -> NSMenu {
NSMenu(items: menuItems(for: bookmark, parent: parent, isFavorite: isFavorite, forSearch: forSearch))
}

static func menu(for folder: BookmarkFolder?, parent: BookmarkFolder?) -> NSMenu {
NSMenu(items: menuItems(for: folder, parent: parent))
static func menu(for folder: BookmarkFolder?, parent: BookmarkFolder?, forSearch: Bool) -> NSMenu {
NSMenu(items: menuItems(for: folder, parent: parent, forSearch: forSearch))
}

static func menuItems(for bookmark: Bookmark?, parent: BookmarkFolder?, isFavorite: Bool) -> [NSMenuItem] {
[
static func menuItems(for bookmark: Bookmark?, parent: BookmarkFolder?, isFavorite: Bool, forSearch: Bool = false) -> [NSMenuItem] {
var items = [
openBookmarkInNewTabMenuItem(bookmark: bookmark),
openBookmarkInNewWindowMenuItem(bookmark: bookmark),
NSMenuItem.separator(),
Expand All @@ -124,10 +126,17 @@ private extension ContextualMenu {
addFolderMenuItem(folder: parent),
manageBookmarksMenuItem(),
]

if forSearch {
let showInFolderItem = showInFolderMenuItem(bookmark: bookmark, parent: parent)
items.insert(showInFolderItem, at: 5)
}

return items
}

static func menuItems(for folder: BookmarkFolder?, parent: BookmarkFolder?) -> [NSMenuItem] {
[
static func menuItems(for folder: BookmarkFolder?, parent: BookmarkFolder?, forSearch: Bool = false) -> [NSMenuItem] {
var items = [
openInNewTabsMenuItem(folder: folder),
openAllInNewWindowMenuItem(folder: folder),
NSMenuItem.separator(),
Expand All @@ -138,6 +147,13 @@ private extension ContextualMenu {
addFolderMenuItem(folder: folder),
manageBookmarksMenuItem(),
]

if forSearch {
let showInFolderItem = showInFolderMenuItem(folder: folder, parent: parent)
items.insert(showInFolderItem, at: 3)
}

return items
}

static func menuItem(_ title: String, _ action: Selector, _ representedObject: Any? = nil) -> NSMenuItem {
Expand Down Expand Up @@ -192,6 +208,10 @@ private extension ContextualMenu {
return menuItem(UserText.bookmarksBarContextMenuMoveToEnd, #selector(BookmarkMenuItemSelectors.moveToEnd(_:)), bookmarkEntityInfo)
}

static func showInFolderMenuItem(bookmark: Bookmark?, parent: BookmarkFolder?) -> NSMenuItem {
return menuItem(UserText.showInFolder, #selector(BookmarkSearchMenuItemSelectors.showInFolder(_:)), bookmark)
}

// MARK: - Bookmark Folder Menu Items

static func openInNewTabsMenuItem(folder: BookmarkFolder?) -> NSMenuItem {
Expand All @@ -206,6 +226,10 @@ private extension ContextualMenu {
menuItem(UserText.addFolder, #selector(FolderMenuItemSelectors.newFolder(_:)), folder)
}

static func showInFolderMenuItem(folder: BookmarkFolder?, parent: BookmarkFolder?) -> NSMenuItem {
return menuItem(UserText.showInFolder, #selector(BookmarkSearchMenuItemSelectors.showInFolder(_:)), folder)
}

static func editFolderMenuItem(folder: BookmarkFolder?, parent: BookmarkFolder?) -> NSMenuItem {
let folderEntityInfo = folder.flatMap { BookmarkEntityInfo(entity: $0, parent: parent) }
return menuItem(UserText.editBookmark, #selector(FolderMenuItemSelectors.editFolder(_:)), folderEntityInfo)
Expand Down
5 changes: 5 additions & 0 deletions DuckDuckGo/Bookmarks/Services/MenuItemSelectors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ import AppKit
func openAllInNewWindow(_ sender: NSMenuItem)

}

@objc protocol BookmarkSearchMenuItemSelectors {

func showInFolder(_ sender: NSMenuItem)
}
24 changes: 22 additions & 2 deletions DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ final class BookmarkListViewController: NSViewController {
let row = outlineView.row(for: cell)
guard
let item = outlineView.item(atRow: row),
let contextMenu = ContextualMenu.menu(for: [item], target: self)
let contextMenu = ContextualMenu.menu(for: [item], target: self, forSearch: dataSource.isSearching)
else {
return
}
Expand Down Expand Up @@ -649,7 +649,7 @@ extension BookmarkListViewController: NSMenuDelegate {
}

if let item = outlineView.item(atRow: row) {
return ContextualMenu.menu(for: [item])
return ContextualMenu.menu(for: [item], forSearch: dataSource.isSearching)
} else {
return nil
}
Expand Down Expand Up @@ -810,6 +810,26 @@ extension BookmarkListViewController: FolderMenuItemSelectors {

}

extension BookmarkListViewController: BookmarkSearchMenuItemSelectors {
func showInFolder(_ sender: NSMenuItem) {
guard let baseBookmark = sender.representedObject as? BaseBookmarkEntity else {
assertionFailure("Failed to retrieve Bookmark from Show in Folder context menu item")
return
}

hideSearchBar()
showTreeView()

guard let node = dataSource.treeController.node(representing: baseBookmark) else {
return
}

expandFoldersUntil(node: node)
outlineView.scrollTo(node)
outlineView.highlight(node)
}
}

// MARK: - Search field delegate

extension BookmarkListViewController: NSSearchFieldDelegate {
Expand Down
41 changes: 41 additions & 0 deletions DuckDuckGo/Bookmarks/View/BookmarkOutlineCellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ final class BookmarkOutlineCellView: NSTableCellView {
NSTrackingArea(rect: .zero, options: [.inVisibleRect, .activeAlways, .mouseEnteredAndExited], owner: self, userInfo: nil)
}()
private var leadingConstraint = NSLayoutConstraint()
private var isHighlighted: Bool = false {
didSet {
updateHighlightAppearance()
}
}
private var flashTimer: Timer?
private var flashDuration: TimeInterval = 0

var shouldShowMenuButton = false

Expand Down Expand Up @@ -71,6 +78,32 @@ final class BookmarkOutlineCellView: NSTableCellView {
favoriteImageView.isHidden = false
}

func setHighlighted(_ highlighted: Bool) {
isHighlighted = highlighted
}

// Method to start the flash highlight effect
func startFlashHighlighting(duration: TimeInterval, interval: TimeInterval) {
self.flashDuration = duration
self.flashTimer?.invalidate()
self.flashTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(toggleHighlight), userInfo: nil, repeats: true)

DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
self.stopFlashHighlighting()
}
}

// Method to stop the flash highlight effect
func stopFlashHighlighting() {
self.flashTimer?.invalidate()
self.flashTimer = nil
self.setHighlighted(false) // Ensure the cell is not highlighted when stopping the flash
}

@objc private func toggleHighlight() {
self.setHighlighted(!isHighlighted)
}

// MARK: - Private

private func setupUI() {
Expand Down Expand Up @@ -159,6 +192,14 @@ final class BookmarkOutlineCellView: NSTableCellView {
countLabel.setContentHuggingPriority(.required, for: .horizontal)
}

private func updateHighlightAppearance() {
if isHighlighted {
self.layer?.backgroundColor = NSColor.selectedControlColor.cgColor
} else {
self.layer?.backgroundColor = NSColor.clear.cgColor
}
}

@objc private func cellMenuButtonClicked() {
delegate?.outlineCellViewRequestedMenu(self)
}
Expand Down
9 changes: 9 additions & 0 deletions DuckDuckGo/Bookmarks/View/BookmarksOutlineView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,13 @@ final class BookmarksOutlineView: NSOutlineView {
}
}

func highlight(_ item: Any) {
let row = row(forItem: item)

guard row != -1, let cellView = view(atColumn: 0, row: row, makeIfNecessary: false) as? BookmarkOutlineCellView else {
return
}

cellView.startFlashHighlighting(duration: 2.0, interval: 0.5)
}
}
1 change: 1 addition & 0 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ struct UserText {
static let showFolderContents = NSLocalizedString("show.folder.contents", value: "Show Folder Contents", comment: "Menu item that shows the content of a folder ")
static let editBookmark = NSLocalizedString("menu.bookmarks.edit", value: "Edit…", comment: "Menu item to edit a bookmark or a folder")
static let addFolder = NSLocalizedString("menu.add.folder", value: "Add Folder…", comment: "Menu item to add a folder")
static let showInFolder = NSLocalizedString("menu.show.in.folder", value: "Show in Folder", comment: "Menu item to show where a bookmark is located")

static let tabHomeTitle = NSLocalizedString("tab.home.title", value: "New Tab", comment: "Tab home title")
static let tabUntitledTitle = NSLocalizedString("tab.empty.title", value: "Untitled", comment: "Title for an empty tab without a title")
Expand Down
12 changes: 12 additions & 0 deletions DuckDuckGo/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -30660,6 +30660,18 @@
}
}
},
"menu.show.in.folder" : {
"comment" : "Menu item to show where a bookmark is located",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Show in Folder"
}
}
}
},
"Merge All Windows" : {
"comment" : "Main Menu Window item",
"localizations" : {
Expand Down
55 changes: 55 additions & 0 deletions UnitTests/Bookmarks/Model/ContextualMenuTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,61 @@ final class ContextualMenuTests: XCTestCase {
assertMenu(item: items[2], withTitle: UserText.bookmarksBarContextMenuDelete, selector: #selector(BookmarkMenuItemSelectors.deleteEntities(_:)), representedObject: [bookmark, folder])
}

func testWhenSearchIsHappeningThenMenuForBookmarksReturnsShowInFolder() throws {
// GIVEN
let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DDG", isFavorite: false)

// WHEN
let menu = ContextualMenu.menu(for: [bookmark], forSearch: true)

// THEN
let items = try XCTUnwrap(menu?.items)
XCTAssertEqual(items.count, 13)
assertMenu(item: items[5], withTitle: UserText.showInFolder, selector: #selector(BookmarkSearchMenuItemSelectors.showInFolder(_:)), representedObject: bookmark)
}

func testWhenSearchIsHappeningThenMenuForFoldersReturnsShowInFolder() throws {
// GIVEN
let folder = BookmarkFolder(id: "1", title: "Folder")

// WHEN
let menu = ContextualMenu.menu(for: [folder], forSearch: true)

// THEN
let items = try XCTUnwrap(menu?.items)
XCTAssertEqual(items.count, 10)
assertMenu(item: items[3], withTitle: UserText.showInFolder, selector: #selector(BookmarkSearchMenuItemSelectors.showInFolder(_:)), representedObject: folder)
}

func testWhenGettingContextalMenuForMoreThanOneBookmarkThenShowInFolderIsNotReturned() throws {
// GIVEN
let bookmark = Bookmark(id: "1", url: "", title: "Bookmark", isFavorite: true)
let folder = BookmarkFolder(id: "1", title: "Folder")

// WHEN
let menu = ContextualMenu.menu(for: [bookmark, folder])

// THEN
let items = try XCTUnwrap(menu?.items)
XCTAssertEqual(items.count, 3)

for menuItem in items {
XCTAssertNotEqual(menuItem.title, UserText.showInFolder)
XCTAssertNotEqual(menuItem.action, #selector(BookmarkSearchMenuItemSelectors.showInFolder(_:)))
}
}

func testWhenGettingContextualMenuForItemThenShowInFolderIsNotReturned() throws {
// WHEN
let menu = ContextualMenu.menu(for: [])

// THEN
XCTAssertEqual(menu?.items.count, 1)
let menuItem = try XCTUnwrap(menu?.items.first)
XCTAssertNotEqual(menuItem.title, UserText.showInFolder)
XCTAssertNotEqual(menuItem.action, #selector(BookmarkSearchMenuItemSelectors.showInFolder(_:)))
}

}

private extension ContextualMenuTests {
Expand Down

0 comments on commit 832d72d

Please sign in to comment.