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

Bookmarks menu implementation #3031

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
43b4318
Bookmarks menu implementation
mallexxx Jul 26, 2024
2e7b155
cleanup, fix some issues
mallexxx Jul 30, 2024
07b2c66
cleanup
mallexxx Jul 30, 2024
65cca05
Merge branch 'main' into alex/bookmarks-menu
mallexxx Jul 31, 2024
bb354f4
fix bookmarks menu jumping to other item on items reordering
mallexxx Jul 31, 2024
46f3f3e
fix reloading of Clipped bookmark bar items
mallexxx Jul 31, 2024
4477bef
fix items reordering in the Clipped bookmarks bar menu
mallexxx Jul 31, 2024
b1fc212
add ParentFolderType: Equatable
mallexxx Jul 31, 2024
3842df8
drop ParentFolderType: Equatable from BookmarkStoreMock
mallexxx Aug 2, 2024
53b0c28
display "Empty" disabled item in empty submenu
mallexxx Aug 5, 2024
403623b
Fix submenu positioning
mallexxx Aug 5, 2024
5fbdd15
fix submenu positioning
mallexxx Aug 5, 2024
0725514
Expand folders dragged onto; Scroll when dragging over scroll buttons…
mallexxx Aug 5, 2024
8798f73
Unify bookmarks drag&drop
mallexxx Aug 7, 2024
3001de2
minor readability improvements
mallexxx Aug 7, 2024
e905e67
Close bookmarks menu on click elsewhere
mallexxx Aug 8, 2024
4e0d072
Drag&drop adjustments, delay closing submenu when cursor is moving do…
mallexxx Aug 8, 2024
5010239
commit missed file
mallexxx Aug 8, 2024
acf706b
Display scrollers in Bookmark Manager
mallexxx Aug 8, 2024
10b60c8
Fix removed items creating mess on the Bookmarks Bar
mallexxx Aug 8, 2024
8e29862
Fix cell spacing in Search mode
mallexxx Aug 8, 2024
644b9fc
Fix tests build
mallexxx Aug 8, 2024
e15d283
Adjust Bookmarks Bar spacing and max size
mallexxx Aug 8, 2024
b8cd540
fix drag-drop issues, update menu frame on reload
mallexxx Aug 14, 2024
444145f
TODOs
mallexxx Aug 15, 2024
f827684
Merge remote-tracking branch 'origin/main' into alex/bookmarks-menu
mallexxx Aug 15, 2024
40022f9
apply code adjustments from recent PRs
mallexxx Aug 16, 2024
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
54 changes: 45 additions & 9 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1530"
version = "1.8">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
Expand Down
1 change: 0 additions & 1 deletion DuckDuckGo/Application/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import Foundation
final class Application: NSApplication {

private let copyHandler = CopyHandler()
// private var _delegate: AppDelegate!
public static var appDelegate: AppDelegate!

override init() {
Expand Down
6 changes: 6 additions & 0 deletions DuckDuckGo/Bookmarks/Extensions/NSOutlineViewExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ extension NSOutlineView {
selectedNodes.compactMap { $0.representedObject as? PseudoFolder }
}

func rowIfValid(forItem item: Any?) -> Int? {
let row = row(forItem: item)
guard row >= 0, row != NSNotFound else { return nil }
return row
}

func revealAndSelect(nodePath: BookmarkNode.Path) {
let totalNodePathComponents = nodePath.components.count
if totalNodePathComponents < 2 {
Expand Down
198 changes: 198 additions & 0 deletions DuckDuckGo/Bookmarks/Model/BookmarkDragDropManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
//
// BookmarkDragDropManager.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 AppKit
import Common
import Foundation

final class BookmarkDragDropManager {

static let shared = BookmarkDragDropManager()

static let draggedTypes: [NSPasteboard.PasteboardType] = [
.string,
.URL,
BookmarkPasteboardWriter.bookmarkUTIInternalType,
FolderPasteboardWriter.folderUTIInternalType
]

private let bookmarkManager: BookmarkManager

init(bookmarkManager: BookmarkManager = LocalBookmarkManager.shared) {
self.bookmarkManager = bookmarkManager
}

func validateDrop(_ info: NSDraggingInfo, to destination: Any) -> NSDragOperation {
let bookmarks = PasteboardBookmark.pasteboardBookmarks(with: info.draggingPasteboard.pasteboardItems)
let folders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard.pasteboardItems)

let bookmarksDragOperation = bookmarks.flatMap { validateMove(for: $0, destination: destination) }
let foldersDragOperation = folders.flatMap { validateMove(for: $0, destination: destination) }

switch (bookmarksDragOperation, foldersDragOperation) {
// If the dragged values contain both folders and bookmarks, only validate the move if all objects can be moved.
case (true, true), (true, nil), (nil, true):
return .move
default:
return .none
}
}

private func validateMove(for draggedBookmarks: Set<PasteboardBookmark>, destination: Any) -> Bool? {
guard !draggedBookmarks.isEmpty else { return nil }
guard destination is BookmarkFolder || destination is PseudoFolder else { return false }

return true
}

private func validateMove(for draggedFolders: Set<PasteboardFolder>, destination: Any) -> Bool? {
guard !draggedFolders.isEmpty else { return nil }

guard let destinationFolder = destination as? BookmarkFolder else {
if destination as? PseudoFolder == .bookmarks {
return true
}
return false
}

// Folders cannot be dragged onto themselves or any of their descendants:
return draggedFolders.allSatisfy { folder in
bookmarkManager.canMoveObjectWithUUID(objectUUID: folder.id, to: destinationFolder)
}
}

@discardableResult
@MainActor
func acceptDrop(_ info: NSDraggingInfo, to destination: Any, at index: Int) -> Bool {
defer {
// prevent other drop targets accepting the dragged items twice
info.draggingPasteboard.clearContents()
}
guard let draggedObjectIdentifiers = info.draggingPasteboard.pasteboardItems?.compactMap(\.bookmarkEntityUUID), !draggedObjectIdentifiers.isEmpty else {
return createBookmarks(from: info.draggingPasteboard.pasteboardItems ?? [], in: destination, at: index, window: info.draggingDestinationWindow)
}

switch destination {
case let folder as BookmarkFolder:
if folder.id == PseudoFolder.bookmarks.id { fallthrough }

let index = (index == -1 || index == NSNotFound) ? 0 : index
let parent: ParentFolderType = (folder.id == PseudoFolder.bookmarks.id) ? .root : .parent(uuid: folder.id)
bookmarkManager.move(objectUUIDs: draggedObjectIdentifiers, toIndex: index, withinParentFolder: parent) { error in
if let error = error {
os_log("Failed to accept existing parent drop via outline view: %s", error.localizedDescription)
}
}

case is PseudoFolder where (destination as? PseudoFolder) == .bookmarks:
if index == -1 || index == NSNotFound {
bookmarkManager.add(objectsWithUUIDs: draggedObjectIdentifiers, to: nil) { error in
if let error = error {
os_log("Failed to accept nil parent drop via outline view: %s", error.localizedDescription)
}
}
} else {
bookmarkManager.move(objectUUIDs: draggedObjectIdentifiers, toIndex: index, withinParentFolder: .root) { error in
if let error = error {
os_log("Failed to accept nil parent drop via outline view: %s", error.localizedDescription)
}
}
}

case let pseudoFolder as PseudoFolder where pseudoFolder == .favorites:
if index == -1 || index == NSNotFound {
bookmarkManager.update(objectsWithUUIDs: draggedObjectIdentifiers, update: { entity in
let bookmark = entity as? Bookmark
bookmark?.isFavorite = true
}, completion: { error in
if let error = error {
os_log("Failed to update entities during drop via outline view: %s", error.localizedDescription)
}
})
} else {
bookmarkManager.moveFavorites(with: draggedObjectIdentifiers, toIndex: index) { error in
if let error = error {
os_log("Failed to update entities during drop via outline view: %s", error.localizedDescription)
}
}
}

default:
assertionFailure("Unknown destination: \(destination)")
return false
}

return true
}

@MainActor
private func createBookmarks(from pasteboardItems: [NSPasteboardItem], in destination: Any, at index: Int, window: NSWindow?) -> Bool {
var parent: BookmarkFolder?
var isFavorite = false

switch destination {
case let pseudoFolder as PseudoFolder where pseudoFolder == .favorites:
isFavorite = true
case let pseudoFolder as PseudoFolder where pseudoFolder == .bookmarks:
isFavorite = false

case let folder as BookmarkFolder:
parent = folder

default:
assertionFailure("Unknown destination: \(destination)")
return false
}

var currentIndex = index
for item in pasteboardItems {
let url: URL
let title: String
func titleFromUrlDroppingSchemeIfNeeded(_ url: URL) -> String {
let title = url.absoluteString
// drop `http[s]://` from bookmark URL used as its title
if let scheme = url.navigationalScheme, scheme.isHypertextScheme {
return title.dropping(prefix: scheme.separated())
}
return title
}
if let webViewItem = item.draggedWebViewValues() {
url = webViewItem.url
title = webViewItem.title ?? self.title(forTabWith: webViewItem.url, in: window) ?? titleFromUrlDroppingSchemeIfNeeded(url)
} else if let draggedString = item.string(forType: .string),
let draggedURL = URL(string: draggedString) {
url = draggedURL
title = self.title(forTabWith: draggedURL, in: window) ?? titleFromUrlDroppingSchemeIfNeeded(url)
} else {
continue
}

self.bookmarkManager.makeBookmark(for: url, title: title, isFavorite: isFavorite, index: currentIndex, parent: parent)
currentIndex += 1
}

return currentIndex > index
}

@MainActor
private func title(forTabWith url: URL, in window: NSWindow?) -> String? {
guard let mainViewController = window?.contentViewController as? MainViewController else { return nil }
return mainViewController.tabCollectionViewModel.title(forTabWithURL: url)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ final class BookmarkListTreeControllerSearchDataSource: BookmarkTreeControllerSe
self.bookmarkManager = bookmarkManager
}

func nodes(for searchQuery: String, sortMode: BookmarksSortMode) -> [BookmarkNode] {
func nodes(forSearchQuery searchQuery: String, sortMode: BookmarksSortMode) -> [BookmarkNode] {
let searchResults = bookmarkManager.search(by: searchQuery)

return rebuildChildNodes(for: searchResults.sorted(by: sortMode))
Expand Down
32 changes: 12 additions & 20 deletions DuckDuckGo/Bookmarks/Model/BookmarkNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ final class BookmarkNode: Hashable {
var childNodes = [BookmarkNode]()

var isRoot: Bool {
return parent == nil
return representedObject is RootNode
}

var numberOfChildNodes: Int {
Expand Down Expand Up @@ -67,6 +67,16 @@ final class BookmarkNode: Hashable {
return 0
}

var canBeHighlighted: Bool {
if representedObject is SpacerNode {
return false
} else if let menuItem = representedObject as? MenuItemNode {
return menuItem.isEnabled
} else {
return true
}
}

/// Creates an instance of a bookmark node.
/// - Parameters:
/// - representedObject: The represented object contained in the node.
Expand Down Expand Up @@ -144,11 +154,7 @@ final class BookmarkNode: Hashable {
}

func childNodeRepresenting(object: AnyObject) -> BookmarkNode? {
return findNodeRepresenting(object: object, recursively: false)
}

func descendantNodeRepresenting(object: AnyObject) -> BookmarkNode? {
return findNodeRepresenting(object: object, recursively: true)
return childNodes.first { $0.representedObjectEquals(object) }
}

func isAncestor(of node: BookmarkNode) -> Bool {
Expand All @@ -169,20 +175,6 @@ final class BookmarkNode: Hashable {
}
}

func findNodeRepresenting(object: AnyObject, recursively: Bool = false) -> BookmarkNode? {
for childNode in childNodes {
if childNode.representedObjectEquals(object) {
return childNode
}

if recursively, let foundNode = childNode.descendantNodeRepresenting(object: object) {
return foundNode
}
}

return nil
}

// MARK: - Hashable

func hash(into hasher: inout Hasher) {
Expand Down
Loading
Loading