Enable folder dragging between accounts
This commit is contained in:
parent
039deadb72
commit
b1bd8d2d90
|
@ -1,82 +0,0 @@
|
|||
//
|
||||
// FolderPasteboardWriter.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 2/11/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
extension Folder: PasteboardWriterOwner {
|
||||
|
||||
public var pasteboardWriter: NSPasteboardWriting {
|
||||
return FolderPasteboardWriter(folder: self)
|
||||
}
|
||||
}
|
||||
|
||||
@objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
|
||||
private let folder: Folder
|
||||
static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder"
|
||||
static let folderUTIInternalType = NSPasteboard.PasteboardType(rawValue: folderUTIInternal)
|
||||
|
||||
init(folder: Folder) {
|
||||
|
||||
self.folder = folder
|
||||
}
|
||||
|
||||
// MARK: - NSPasteboardWriting
|
||||
|
||||
func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
|
||||
|
||||
return [.string, FolderPasteboardWriter.folderUTIInternalType]
|
||||
}
|
||||
|
||||
func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
|
||||
|
||||
let plist: Any?
|
||||
|
||||
switch type {
|
||||
case .string:
|
||||
plist = folder.nameForDisplay
|
||||
case FolderPasteboardWriter.folderUTIInternalType:
|
||||
plist = internalDictionary()
|
||||
default:
|
||||
plist = nil
|
||||
}
|
||||
|
||||
return plist
|
||||
}
|
||||
}
|
||||
|
||||
private extension FolderPasteboardWriter {
|
||||
|
||||
private struct Key {
|
||||
|
||||
static let name = "name"
|
||||
|
||||
// Internal
|
||||
static let accountID = "accountID"
|
||||
static let folderID = "folderID"
|
||||
}
|
||||
|
||||
func internalDictionary() -> [String: Any] {
|
||||
|
||||
var d = [String: Any]()
|
||||
|
||||
d[Key.folderID] = folder.folderID
|
||||
if let name = folder.name {
|
||||
d[Key.name] = name
|
||||
}
|
||||
if let accountID = folder.account?.accountID {
|
||||
d[Key.accountID] = accountID
|
||||
}
|
||||
|
||||
return d
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
//
|
||||
// FolderPasteboardWriter.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 2/11/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
typealias PasteboardFolderDictionary = [String: String]
|
||||
|
||||
struct PasteboardFolder: Hashable {
|
||||
|
||||
private struct Key {
|
||||
static let name = "name"
|
||||
// Internal
|
||||
static let folderID = "folderID"
|
||||
static let accountID = "accountID"
|
||||
}
|
||||
|
||||
|
||||
let name: String
|
||||
let folderID: String?
|
||||
let accountID: String?
|
||||
|
||||
init(name: String, folderID: String?, accountID: String?) {
|
||||
self.name = name
|
||||
self.folderID = folderID
|
||||
self.accountID = accountID
|
||||
}
|
||||
|
||||
// MARK: - Reading
|
||||
|
||||
init?(dictionary: PasteboardFolderDictionary) {
|
||||
guard let name = dictionary[Key.name] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let folderID = dictionary[Key.folderID]
|
||||
let accountID = dictionary[Key.accountID]
|
||||
|
||||
self.init(name: name, folderID: folderID, accountID: accountID)
|
||||
}
|
||||
|
||||
init?(pasteboardItem: NSPasteboardItem) {
|
||||
var pasteboardType: NSPasteboard.PasteboardType?
|
||||
if pasteboardItem.types.contains(FolderPasteboardWriter.folderUTIInternalType) {
|
||||
pasteboardType = FolderPasteboardWriter.folderUTIInternalType
|
||||
}
|
||||
|
||||
if let foundType = pasteboardType {
|
||||
if let folderDictionary = pasteboardItem.propertyList(forType: foundType) as? PasteboardFeedDictionary {
|
||||
self.init(dictionary: folderDictionary)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
static func pasteboardFolders(with pasteboard: NSPasteboard) -> Set<PasteboardFolder>? {
|
||||
guard let items = pasteboard.pasteboardItems else {
|
||||
return nil
|
||||
}
|
||||
let folders = items.compactMap { PasteboardFolder(pasteboardItem: $0) }
|
||||
return folders.isEmpty ? nil : Set(folders)
|
||||
}
|
||||
|
||||
// MARK: - Writing
|
||||
|
||||
func internalDictionary() -> PasteboardFolderDictionary {
|
||||
var d = PasteboardFeedDictionary()
|
||||
d[PasteboardFolder.Key.name] = name
|
||||
if let folderID = folderID {
|
||||
d[PasteboardFolder.Key.folderID] = folderID
|
||||
}
|
||||
if let accountID = accountID {
|
||||
d[PasteboardFolder.Key.accountID] = accountID
|
||||
}
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
extension Folder: PasteboardWriterOwner {
|
||||
|
||||
public var pasteboardWriter: NSPasteboardWriting {
|
||||
return FolderPasteboardWriter(folder: self)
|
||||
}
|
||||
}
|
||||
|
||||
@objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
|
||||
private let folder: Folder
|
||||
static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder"
|
||||
static let folderUTIInternalType = NSPasteboard.PasteboardType(rawValue: folderUTIInternal)
|
||||
|
||||
init(folder: Folder) {
|
||||
|
||||
self.folder = folder
|
||||
}
|
||||
|
||||
// MARK: - NSPasteboardWriting
|
||||
|
||||
func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
|
||||
|
||||
return [.string, FolderPasteboardWriter.folderUTIInternalType]
|
||||
}
|
||||
|
||||
func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
|
||||
|
||||
let plist: Any?
|
||||
|
||||
switch type {
|
||||
case .string:
|
||||
plist = folder.nameForDisplay
|
||||
case FolderPasteboardWriter.folderUTIInternalType:
|
||||
plist = internalDictionary
|
||||
default:
|
||||
plist = nil
|
||||
}
|
||||
|
||||
return plist
|
||||
}
|
||||
}
|
||||
|
||||
private extension FolderPasteboardWriter {
|
||||
var pasteboardFolder: PasteboardFolder {
|
||||
return PasteboardFolder(name: folder.name ?? "", folderID: String(folder.folderID), accountID: folder.account?.accountID)
|
||||
}
|
||||
|
||||
var internalDictionary: PasteboardFeedDictionary {
|
||||
return pasteboardFolder.internalDictionary()
|
||||
}
|
||||
}
|
|
@ -54,11 +54,22 @@ import Account
|
|||
}
|
||||
|
||||
func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
|
||||
guard let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard), !draggedFeeds.isEmpty else {
|
||||
let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)
|
||||
let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard)
|
||||
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
|
||||
let parentNode = nodeForItem(item)
|
||||
|
||||
if let draggedFolders = draggedFolders {
|
||||
if draggedFolders.count == 1 {
|
||||
return validateLocalFolderDrop(outlineView, draggedFolders.first!, parentNode, index)
|
||||
} else {
|
||||
return validateLocalFoldersDrop(outlineView, draggedFolders, parentNode, index)
|
||||
}
|
||||
}
|
||||
|
||||
if let draggedFeeds = draggedFeeds {
|
||||
let contentsType = draggedFeedContentsType(draggedFeeds)
|
||||
|
||||
switch contentsType {
|
||||
|
@ -75,12 +86,18 @@ import Account
|
|||
}
|
||||
}
|
||||
|
||||
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
|
||||
guard let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard), !draggedFeeds.isEmpty else {
|
||||
return false
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
|
||||
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
|
||||
let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)
|
||||
let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard)
|
||||
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
|
||||
return false
|
||||
}
|
||||
let parentNode = nodeForItem(item)
|
||||
|
||||
if let draggedFeeds = draggedFeeds {
|
||||
let contentsType = draggedFeedContentsType(draggedFeeds)
|
||||
|
||||
switch contentsType {
|
||||
|
@ -95,6 +112,9 @@ import Account
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
@ -109,11 +129,10 @@ private extension SidebarOutlineDataSource {
|
|||
}
|
||||
|
||||
func nodeRepresentsDraggableItem(_ node: Node) -> Bool {
|
||||
// Don’t allow PseudoFeed or Folder to be dragged.
|
||||
// Don’t allow PseudoFeed to be dragged.
|
||||
// This will have to be revisited later. For instance,
|
||||
// user-created smart feeds should be draggable, maybe.
|
||||
// And we might allow dragging folders between accounts.
|
||||
return node.representedObject is Feed
|
||||
return node.representedObject is Folder || node.representedObject is Feed
|
||||
}
|
||||
|
||||
// MARK: - Drag and Drop
|
||||
|
@ -179,15 +198,14 @@ private extension SidebarOutlineDataSource {
|
|||
if violatesTagSpecificBehavior(dropTargetNode, draggedFeed) {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
let dragOperation: NSDragOperation = localFeedsDropOperation(dropTargetNode, Set([draggedFeed]))
|
||||
if parentNode == dropTargetNode && index == NSOutlineViewDropOnItemIndex {
|
||||
return dragOperation
|
||||
return localDragOperation()
|
||||
}
|
||||
let updatedIndex = indexWhereDraggedFeedWouldAppear(dropTargetNode, draggedFeed)
|
||||
if parentNode !== dropTargetNode || index != updatedIndex {
|
||||
outlineView.setDropItem(dropTargetNode, dropChildIndex: updatedIndex)
|
||||
}
|
||||
return dragOperation
|
||||
return localDragOperation()
|
||||
}
|
||||
|
||||
func validateLocalFeedsDrop(_ outlineView: NSOutlineView, _ draggedFeeds: Set<PasteboardFeed>, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
|
@ -204,10 +222,10 @@ private extension SidebarOutlineDataSource {
|
|||
if parentNode !== dropTargetNode || index != NSOutlineViewDropOnItemIndex {
|
||||
outlineView.setDropItem(dropTargetNode, dropChildIndex: NSOutlineViewDropOnItemIndex)
|
||||
}
|
||||
return localFeedsDropOperation(dropTargetNode, draggedFeeds)
|
||||
return localDragOperation()
|
||||
}
|
||||
|
||||
func localFeedsDropOperation(_ dropTargetNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> NSDragOperation {
|
||||
func localDragOperation() -> NSDragOperation {
|
||||
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||
return .copy
|
||||
} else {
|
||||
|
@ -240,6 +258,32 @@ private extension SidebarOutlineDataSource {
|
|||
return accounts
|
||||
}
|
||||
|
||||
func validateLocalFolderDrop(_ outlineView: NSOutlineView, _ draggedFolder: PasteboardFolder, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
guard let dropAccount = parentNode.representedObject as? Account, dropAccount.accountID != draggedFolder.accountID else {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
let updatedIndex = indexWhereDraggedFolderWouldAppear(parentNode, draggedFolder)
|
||||
if index != updatedIndex {
|
||||
outlineView.setDropItem(parentNode, dropChildIndex: updatedIndex)
|
||||
}
|
||||
return localDragOperation()
|
||||
}
|
||||
|
||||
func validateLocalFoldersDrop(_ outlineView: NSOutlineView, _ draggedFolders: Set<PasteboardFolder>, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
guard let dropAccount = parentNode.representedObject as? Account else {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
for draggedFolder in draggedFolders {
|
||||
if dropAccount.accountID == draggedFolder.accountID {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
}
|
||||
if index != NSOutlineViewDropOnItemIndex {
|
||||
outlineView.setDropItem(parentNode, dropChildIndex: NSOutlineViewDropOnItemIndex)
|
||||
}
|
||||
return localDragOperation()
|
||||
}
|
||||
|
||||
func copyInAccount(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? Feed else {
|
||||
return
|
||||
|
@ -522,6 +566,18 @@ private extension SidebarOutlineDataSource {
|
|||
let index = sortedNodes.firstIndex(of: draggedFeedNode)!
|
||||
return index
|
||||
}
|
||||
|
||||
func indexWhereDraggedFolderWouldAppear(_ parentNode: Node, _ draggedFolder: PasteboardFolder) -> Int {
|
||||
let draggedFolderWrapper = PasteboardFolderObjectWrapper(pasteboardFolder: draggedFolder)
|
||||
let draggedFolderNode = Node(representedObject: draggedFolderWrapper, parent: nil)
|
||||
draggedFolderNode.canHaveChildNodes = true
|
||||
let nodes = parentNode.childNodes + [draggedFolderNode]
|
||||
|
||||
// Revisit if the tree controller can ever be sorted in some other way.
|
||||
let sortedNodes = nodes.sortedAlphabeticallyWithFoldersAtEnd()
|
||||
let index = sortedNodes.firstIndex(of: draggedFolderNode)!
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
final class PasteboardFeedObjectWrapper: DisplayNameProvider {
|
||||
|
@ -535,3 +591,15 @@ final class PasteboardFeedObjectWrapper: DisplayNameProvider {
|
|||
self.pasteboardFeed = pasteboardFeed
|
||||
}
|
||||
}
|
||||
|
||||
final class PasteboardFolderObjectWrapper: DisplayNameProvider {
|
||||
|
||||
var nameForDisplay: String {
|
||||
return pasteboardFolder.name
|
||||
}
|
||||
let pasteboardFolder: PasteboardFolder
|
||||
|
||||
init(pasteboardFolder: PasteboardFolder) {
|
||||
self.pasteboardFolder = pasteboardFolder
|
||||
}
|
||||
}
|
||||
|
|
|
@ -233,7 +233,7 @@
|
|||
84A37CB5201ECD610087C5AF /* RenameWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A37CB4201ECD610087C5AF /* RenameWindowController.swift */; };
|
||||
84A3EE5F223B667F00557320 /* DefaultFeeds.opml in Resources */ = {isa = PBXBuildFile; fileRef = 84A3EE52223B667F00557320 /* DefaultFeeds.opml */; };
|
||||
84A3EE61223B667F00557320 /* DefaultFeeds.opml in Resources */ = {isa = PBXBuildFile; fileRef = 84A3EE52223B667F00557320 /* DefaultFeeds.opml */; };
|
||||
84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */; };
|
||||
84AD1EAA2031617300BC20B7 /* PasteboardFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EA92031617300BC20B7 /* PasteboardFolder.swift */; };
|
||||
84AD1EBA2031649C00BC20B7 /* SmartFeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */; };
|
||||
84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EBB2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift */; };
|
||||
84B7178C201E66580091657D /* SidebarViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */; };
|
||||
|
@ -839,7 +839,7 @@
|
|||
84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToMarsEditCommand.swift; sourceTree = "<group>"; };
|
||||
84A37CB4201ECD610087C5AF /* RenameWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RenameWindowController.swift; sourceTree = "<group>"; };
|
||||
84A3EE52223B667F00557320 /* DefaultFeeds.opml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = DefaultFeeds.opml; sourceTree = "<group>"; };
|
||||
84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderPasteboardWriter.swift; sourceTree = "<group>"; };
|
||||
84AD1EA92031617300BC20B7 /* PasteboardFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteboardFolder.swift; sourceTree = "<group>"; };
|
||||
84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedPasteboardWriter.swift; sourceTree = "<group>"; };
|
||||
84AD1EBB2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarOutlineDataSource.swift; sourceTree = "<group>"; };
|
||||
84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SidebarViewController+ContextualMenus.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1369,7 +1369,7 @@
|
|||
849A97601ED9EB96007D329B /* SidebarOutlineView.swift */,
|
||||
849A97631ED9EB96007D329B /* UnreadCountView.swift */,
|
||||
848D578D21543519005FFAD5 /* PasteboardFeed.swift */,
|
||||
84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */,
|
||||
84AD1EA92031617300BC20B7 /* PasteboardFolder.swift */,
|
||||
849A97821ED9EC63007D329B /* SidebarStatusBarView.swift */,
|
||||
844B5B6A1FEA224000C7C76A /* Keyboard */,
|
||||
845A29251FC928C7007B49E3 /* Cell */,
|
||||
|
@ -2468,7 +2468,7 @@
|
|||
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */,
|
||||
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
|
||||
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */,
|
||||
84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */,
|
||||
84AD1EAA2031617300BC20B7 /* PasteboardFolder.swift in Sources */,
|
||||
5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */,
|
||||
84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */,
|
||||
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue