From 81615c306415e1a3bc5654decb8bbff62d5734bb Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 5 Sep 2024 18:45:21 -0300 Subject: [PATCH 1/5] Support dragging strings to new tab button --- .../TabBar/View/TabBarViewController.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 89abfcf544..c2bd31ea93 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -559,6 +559,8 @@ final class TabBarViewController: NSViewController { } private func setupAddTabButton() { + addTabButton.delegate = self + addTabButton.registerForDraggedTypes([.string]) addTabButton.target = self addTabButton.action = #selector(addButtonAction(_:)) addTabButton.toolTip = UserText.newTabTooltip @@ -625,6 +627,29 @@ final class TabBarViewController: NSViewController { } +extension TabBarViewController: MouseOverButtonDelegate { + + func mouseOverButton(_ sender: MouseOverButton, draggingEntered info: any NSDraggingInfo, isMouseOver: UnsafeMutablePointer) -> NSDragOperation { + assert(sender === addTabButton || sender === footerAddButton) + let pasteboard = info.draggingPasteboard + + if let types = pasteboard.types, types.contains(.string) { + return .copy + } + return .none + } + + func mouseOverButton(_ sender: MouseOverButton, performDragOperation info: any NSDraggingInfo) -> Bool { + assert(sender === addTabButton || sender === footerAddButton) + if let string = info.draggingPasteboard.string(forType: .string), let url = URL.makeSearchUrl(from: string) { + tabCollectionViewModel.insertOrAppendNewTab(.url(url, credential: nil, source: .ui)) + return true + } + + return true + } +} + extension TabBarViewController: TabCollectionViewModelDelegate { func tabCollectionViewModelDidAppend(_ tabCollectionViewModel: TabCollectionViewModel, selected: Bool) { @@ -882,6 +907,8 @@ extension TabBarViewController: NSCollectionViewDataSource { footer.addButton?.action = #selector(addButtonAction(_:)) footer.toolTip = UserText.newTabTooltip self.footerAddButton = footer.addButton + self.footerAddButton?.delegate = self + self.footerAddButton?.registerForDraggedTypes([.string]) } return view From 42b934321d9f9a9156d4ac1282d612b2ba96a39f Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Fri, 6 Sep 2024 11:47:55 -0300 Subject: [PATCH 2/5] Replace current tab with search when string is dragged to tab --- DuckDuckGo/TabBar/View/TabBarCollectionView.swift | 2 +- DuckDuckGo/TabBar/View/TabBarViewController.swift | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/TabBar/View/TabBarCollectionView.swift b/DuckDuckGo/TabBar/View/TabBarCollectionView.swift index df3222decc..d36b293f9a 100644 --- a/DuckDuckGo/TabBar/View/TabBarCollectionView.swift +++ b/DuckDuckGo/TabBar/View/TabBarCollectionView.swift @@ -33,7 +33,7 @@ final class TabBarCollectionView: NSCollectionView { register(nib, forItemWithIdentifier: TabBarViewItem.identifier) // Register for the dropped object types we can accept. - registerForDraggedTypes([.URL, .fileURL, TabBarViewItemPasteboardWriter.utiInternalType]) + registerForDraggedTypes([.URL, .fileURL, TabBarViewItemPasteboardWriter.utiInternalType, .string]) // Enable dragging items within and into our CollectionView. setDraggingSourceOperationMask([.private], forLocal: true) } diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index c2bd31ea93..4dbf4a6796 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -969,6 +969,11 @@ extension TabBarViewController: NSCollectionViewDelegate { // allow dropping URLs or files guard draggingInfo.draggingPasteboard.url == nil else { return .copy } + // Check if the pasteboard contains string data + if draggingInfo.draggingPasteboard.availableType(from: [.string]) != nil { + return .copy + } + // dragging a tab guard case .private = draggingInfo.draggingSourceOperationMask, draggingInfo.draggingPasteboard.types == [TabBarViewItemPasteboardWriter.utiInternalType] else { return .none } @@ -988,6 +993,13 @@ extension TabBarViewController: NSCollectionViewDelegate { selected: true) return true + } else if let string = draggingInfo.draggingPasteboard.string(forType: .string), + let url = URL.makeSearchUrl(from: string), + let index = tabCollectionViewModel.selectionIndex { + let tab = Tab(content: .url(url, credential: nil, source: .reload), title: "\(string) at DuckDuckGo", shouldLoadInBackground: true, burnerMode: tabCollectionViewModel.burnerMode) + tabCollectionViewModel.replaceTab(at: index, with: tab, forceChange: true) + collectionView.reloadData() + return true } guard case .private = draggingInfo.draggingSourceOperationMask, From e4190d4cbed089d9069bad46987a72913f08b948 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Sun, 8 Sep 2024 09:52:28 -0300 Subject: [PATCH 3/5] - Adds support to drop strings for pinned tabs - Inserts new tab when string gets dropped in the tab bar - Replaces current tab with string search when strings gets dropped on regular tab --- .../PinnedTabs/View/PinnedTabView.swift | 18 +++++++++++++- .../TabBar/View/TabBarViewController.swift | 24 +++++++++++++------ DuckDuckGo/TabBar/View/TabBarViewItem.swift | 18 ++++++++++++++ 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 7bc278f80e..055ce02f08 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -19,7 +19,7 @@ import SwiftUI import SwiftUIExtensions -struct PinnedTabView: View { +struct PinnedTabView: View, DropDelegate { enum Const { static let dimension: CGFloat = 34 static let cornerRadius: CGFloat = 10 @@ -49,6 +49,7 @@ struct PinnedTabView: View { .buttonStyle(TouchDownButtonStyle()) .cornerRadius(Const.cornerRadius, corners: [.topLeft, .topRight]) .contextMenu { contextMenu } + .onDrop(of: ["public.text"], delegate: self) BorderView(isSelected: isSelected, cornerRadius: Const.cornerRadius, @@ -62,7 +63,22 @@ struct PinnedTabView: View { } else { stack } + } + + func performDrop(info: DropInfo) -> Bool { + if let item = info.itemProviders(for: ["public.utf8-plain-text"]).first { + item.loadItem(forTypeIdentifier: "public.utf8-plain-text", options: nil) { (data, _) in + if let data = data as? Data, let droppedString = NSString(data: data, encoding: NSUTF8StringEncoding) { + print(droppedString) + let url = URL.makeSearchUrl(from: droppedString as String)! + DispatchQueue.main.async { + model.setUrl(url, source: .ui) + } + } + } + } + return true } private var isSelected: Bool { diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 4dbf4a6796..931c332c3b 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -991,14 +991,9 @@ extension TabBarViewController: NSCollectionViewDelegate { tabCollectionViewModel.insert(Tab(content: .url(url, source: .appOpenUrl), burnerMode: tabCollectionViewModel.burnerMode), at: .unpinned(newIndex), selected: true) - return true - } else if let string = draggingInfo.draggingPasteboard.string(forType: .string), - let url = URL.makeSearchUrl(from: string), - let index = tabCollectionViewModel.selectionIndex { - let tab = Tab(content: .url(url, credential: nil, source: .reload), title: "\(string) at DuckDuckGo", shouldLoadInBackground: true, burnerMode: tabCollectionViewModel.burnerMode) - tabCollectionViewModel.replaceTab(at: index, with: tab, forceChange: true) - collectionView.reloadData() + } else if let string = draggingInfo.draggingPasteboard.string(forType: .string), let url = URL.makeSearchUrl(from: string) { + tabCollectionViewModel.insertOrAppendNewTab(.url(url, credential: nil, source: .reload)) return true } @@ -1261,6 +1256,21 @@ extension TabBarViewController: TabBarViewItemDelegate { return tab.audioState } + func tabBarViewItem(_ tabBarViewItem: TabBarViewItem, replaceWithStringSearch: String) { + guard let indexPath = collectionView.indexPath(for: tabBarViewItem), + let tab = tabCollectionViewModel.tabCollection.tabs[safe: indexPath.item], + let tabIndex = tabCollectionViewModel.indexInAllTabs(of: tab) else { return } + + if let url = URL.makeSearchUrl(from: replaceWithStringSearch) { + let tab = Tab(content: .url(url, credential: nil, source: .reload), + shouldLoadInBackground: true, + burnerMode: tabCollectionViewModel.burnerMode) + tabCollectionViewModel.replaceTab(at: tabIndex, with: tab, forceChange: true) + tabCollectionViewModel.select(tab: tab) + collectionView.reloadData() + } + } + func otherTabBarViewItemsState(for tabBarViewItem: TabBarViewItem) -> OtherTabBarViewItemsState { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index d4448ab4b1..c554cae927 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -52,6 +52,7 @@ protocol TabBarViewItemDelegate: AnyObject { func tabBarViewItemMuteUnmuteSite(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemRemoveFireproofing(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState? + func tabBarViewItem(_ tabBarViewItem: TabBarViewItem, replaceWithStringSearch: String) func otherTabBarViewItemsState(for tabBarViewItem: TabBarViewItem) -> OtherTabBarViewItemsState @@ -393,6 +394,9 @@ final class TabBarViewItem: NSCollectionViewItem { } else { faviconImageView.contentTintColor = nil } + + mouseOverView.registerForDraggedTypes([.string]) + mouseOverView.delegate = self } private var usedPermissions = Permissions() { @@ -664,6 +668,20 @@ extension TabBarViewItem: MouseClickViewDelegate { delegate?.tabBarViewItemCloseAction(self) } + func mouseOverView(_ sender: MouseOverView, performDragOperation info: any NSDraggingInfo) -> Bool { + if let droppedString = info.draggingPasteboard.string(forType: .string) { + delegate?.tabBarViewItem(self, replaceWithStringSearch: droppedString) + return true + } + return false + } + + func mouseOverView(_ sender: MouseOverView, draggingEntered info: any NSDraggingInfo, isMouseOver: UnsafeMutablePointer) -> NSDragOperation { + if info.draggingPasteboard.availableType(from: [.string]) != nil { + return .copy + } + return [] + } } extension TabBarViewItem { From 182de12f813290bad6d66c02ac0a87281369c649 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Sun, 8 Sep 2024 18:14:17 -0300 Subject: [PATCH 4/5] Fix tests --- UnitTests/TabBar/View/MockTabViewItemDelegate.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index 680f840d9c..2eae908c1c 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -125,6 +125,10 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { OtherTabBarViewItemsState(hasItemsToTheLeft: hasItemsToTheLeft, hasItemsToTheRight: hasItemsToTheRight) } + func tabBarViewItem(_ tabBarViewItem: TabBarViewItem, replaceWithStringSearch: String) { + + } + func clear() { self.audioState = nil } From 0a96e4b82e25f6b1a14298017ab837a3b251f592 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 12 Sep 2024 14:30:06 -0300 Subject: [PATCH 5/5] - Fix regular tabs losing history when dragging string to them - Fix URLs not being loaded correctly when dropping them into the tabs --- DuckDuckGo/PinnedTabs/View/PinnedTabView.swift | 2 +- .../TabBar/View/TabBarViewController.swift | 18 ++++++------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 055ce02f08..09b24a5385 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -70,7 +70,7 @@ struct PinnedTabView: View, DropDelegate { item.loadItem(forTypeIdentifier: "public.utf8-plain-text", options: nil) { (data, _) in if let data = data as? Data, let droppedString = NSString(data: data, encoding: NSUTF8StringEncoding) { print(droppedString) - let url = URL.makeSearchUrl(from: droppedString as String)! + let url = URL.makeURL(from: droppedString as String)! DispatchQueue.main.async { model.setUrl(url, source: .ui) diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 931c332c3b..2cc455158c 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -641,7 +641,7 @@ extension TabBarViewController: MouseOverButtonDelegate { func mouseOverButton(_ sender: MouseOverButton, performDragOperation info: any NSDraggingInfo) -> Bool { assert(sender === addTabButton || sender === footerAddButton) - if let string = info.draggingPasteboard.string(forType: .string), let url = URL.makeSearchUrl(from: string) { + if let string = info.draggingPasteboard.string(forType: .string), let url = URL.makeURL(from: string) { tabCollectionViewModel.insertOrAppendNewTab(.url(url, credential: nil, source: .ui)) return true } @@ -992,7 +992,7 @@ extension TabBarViewController: NSCollectionViewDelegate { at: .unpinned(newIndex), selected: true) return true - } else if let string = draggingInfo.draggingPasteboard.string(forType: .string), let url = URL.makeSearchUrl(from: string) { + } else if let string = draggingInfo.draggingPasteboard.string(forType: .string), let url = URL.makeURL(from: string) { tabCollectionViewModel.insertOrAppendNewTab(.url(url, credential: nil, source: .reload)) return true } @@ -1258,16 +1258,10 @@ extension TabBarViewController: TabBarViewItemDelegate { func tabBarViewItem(_ tabBarViewItem: TabBarViewItem, replaceWithStringSearch: String) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem), - let tab = tabCollectionViewModel.tabCollection.tabs[safe: indexPath.item], - let tabIndex = tabCollectionViewModel.indexInAllTabs(of: tab) else { return } - - if let url = URL.makeSearchUrl(from: replaceWithStringSearch) { - let tab = Tab(content: .url(url, credential: nil, source: .reload), - shouldLoadInBackground: true, - burnerMode: tabCollectionViewModel.burnerMode) - tabCollectionViewModel.replaceTab(at: tabIndex, with: tab, forceChange: true) - tabCollectionViewModel.select(tab: tab) - collectionView.reloadData() + let tab = tabCollectionViewModel.tabCollection.tabs[safe: indexPath.item] else { return } + + if let url = URL.makeURL(from: replaceWithStringSearch) { + tab.setContent(.url(url, credential: nil, source: .reload)) } }