Enable folder dragging between accounts

This commit is contained in:
Maurice Parker 2019-05-29 15:43:33 -05:00
parent 039deadb72
commit b1bd8d2d90
4 changed files with 244 additions and 121 deletions

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -54,46 +54,66 @@ 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)
let contentsType = draggedFeedContentsType(draggedFeeds)
switch contentsType {
case .singleNonLocal:
let draggedNonLocalFeed = singleNonLocalFeed(from: draggedFeeds)!
return validateSingleNonLocalFeedDrop(outlineView, draggedNonLocalFeed, parentNode, index)
case .singleLocal:
let draggedFeed = draggedFeeds.first!
return validateSingleLocalFeedDrop(outlineView, draggedFeed, parentNode, index)
case .multipleLocal:
return validateLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
case .multipleNonLocal, .mixed, .empty:
return SidebarOutlineDataSource.dragOperationNone
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 {
case .singleNonLocal:
let draggedNonLocalFeed = singleNonLocalFeed(from: draggedFeeds)!
return validateSingleNonLocalFeedDrop(outlineView, draggedNonLocalFeed, parentNode, index)
case .singleLocal:
let draggedFeed = draggedFeeds.first!
return validateSingleLocalFeedDrop(outlineView, draggedFeed, parentNode, index)
case .multipleLocal:
return validateLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
case .multipleNonLocal, .mixed, .empty:
return SidebarOutlineDataSource.dragOperationNone
}
}
return SidebarOutlineDataSource.dragOperationNone
}
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
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 false
}
let parentNode = nodeForItem(item)
let contentsType = draggedFeedContentsType(draggedFeeds)
switch contentsType {
case .singleNonLocal:
let draggedNonLocalFeed = singleNonLocalFeed(from: draggedFeeds)!
return acceptSingleNonLocalFeedDrop(outlineView, draggedNonLocalFeed, parentNode, index)
case .singleLocal:
return acceptLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
case .multipleLocal:
return acceptLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
case .multipleNonLocal, .mixed, .empty:
return false
if let draggedFeeds = draggedFeeds {
let contentsType = draggedFeedContentsType(draggedFeeds)
switch contentsType {
case .singleNonLocal:
let draggedNonLocalFeed = singleNonLocalFeed(from: draggedFeeds)!
return acceptSingleNonLocalFeedDrop(outlineView, draggedNonLocalFeed, parentNode, index)
case .singleLocal:
return acceptLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
case .multipleLocal:
return acceptLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
case .multipleNonLocal, .mixed, .empty:
return false
}
}
return false
}
}
@ -109,11 +129,10 @@ private extension SidebarOutlineDataSource {
}
func nodeRepresentsDraggableItem(_ node: Node) -> Bool {
// Dont allow PseudoFeed or Folder to be dragged.
// Dont 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
}
}

View File

@ -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 */,