Make undo deleting feeds/folders work.

This commit is contained in:
Brent Simmons 2017-11-05 12:14:36 -08:00
parent 57296279e4
commit 9818278c9b
3 changed files with 185 additions and 131 deletions

View File

@ -14,7 +14,7 @@ import Data
final class DeleteFromSidebarCommand: UndoableCommand { final class DeleteFromSidebarCommand: UndoableCommand {
let undoManager: UndoManager let undoManager: UndoManager
let undoActionName: String let undoActionName: String
var redoActionName: String { var redoActionName: String {
get { get {
@ -22,40 +22,40 @@ final class DeleteFromSidebarCommand: UndoableCommand {
} }
} }
private let itemSpecifiers: [SidebarItemSpecifier] private let itemSpecifiers: [SidebarItemSpecifier]
init?(nodesToDelete: [Node], undoManager: UndoManager) { init?(nodesToDelete: [Node], undoManager: UndoManager) {
guard DeleteFromSidebarCommand.canDelete(nodesToDelete) else { guard DeleteFromSidebarCommand.canDelete(nodesToDelete) else {
return nil return nil
} }
guard let actionName = DeleteActionName.name(for: nodesToDelete) else { guard let actionName = DeleteActionName.name(for: nodesToDelete) else {
return nil return nil
} }
self.undoActionName = actionName self.undoActionName = actionName
self.undoManager = undoManager self.undoManager = undoManager
let itemSpecifiers = nodesToDelete.flatMap{ SidebarItemSpecifier(node: $0) } let itemSpecifiers = nodesToDelete.flatMap{ SidebarItemSpecifier(node: $0) }
guard !itemSpecifiers.isEmpty else { guard !itemSpecifiers.isEmpty else {
return nil return nil
} }
self.itemSpecifiers = itemSpecifiers self.itemSpecifiers = itemSpecifiers
} }
func perform() { func perform() {
BatchUpdate.shared.perform { BatchUpdate.shared.perform {
itemSpecifiers.forEach { $0.delete() } itemSpecifiers.forEach { $0.delete() }
} }
registerUndo() registerUndo()
} }
func undo() { func undo() {
BatchUpdate.shared.perform { BatchUpdate.shared.perform {
itemSpecifiers.forEach { $0.restore() }
} }
registerRedo() registerRedo()
} }
@ -88,133 +88,176 @@ final class DeleteFromSidebarCommand: UndoableCommand {
private struct SidebarItemSpecifier { private struct SidebarItemSpecifier {
private weak var account: Account? private weak var account: Account?
private let parentFolder: Folder? private let parentFolder: Folder?
private let folder: Folder? private let folder: Folder?
private let feed: Feed? private let feed: Feed?
private let path: ContainerPath private let path: ContainerPath
private var container: Container? { private var container: Container? {
get { get {
if let parentFolder = parentFolder { if let parentFolder = parentFolder {
return parentFolder return parentFolder
} }
if let account = account { if let account = account {
return account return account
} }
return nil return nil
} }
} }
init?(node: Node) { init?(node: Node) {
var account: Account? var account: Account?
self.parentFolder = node.parentFolder()
if let feed = node.representedObject as? Feed { self.parentFolder = node.parentFolder()
if let feed = node.representedObject as? Feed {
self.feed = feed self.feed = feed
self.folder = nil self.folder = nil
account = feed.account account = feed.account
} }
else if let folder = node.representedObject as? Folder { else if let folder = node.representedObject as? Folder {
self.feed = nil self.feed = nil
self.folder = folder self.folder = folder
account = folder.account account = folder.account
} }
else { else {
return nil return nil
} }
if account == nil { if account == nil {
return nil return nil
} }
self.account = account! self.account = account!
self.path = ContainerPath(account: account!, folders: node.containingFolders()) self.path = ContainerPath(account: account!, folders: node.containingFolders())
}
func delete() {
guard let container = container else {
return
}
if let feed = feed {
container.deleteFeed(feed)
}
else if let folder = folder {
container.deleteFolder(folder)
}
}
func restore() {
if let feed = feed {
restoreFeed()
}
else if let folder = folder {
restoreFolder()
}
}
private func restoreFeed() {
guard let account = account, let feed = feed else {
return
}
let feedToUse = uniquedFeed(feed)
account.addFeed(feedToUse, to: resolvedFolder())
}
private func restoreFolder() {
guard let account = account, let folder = folder else {
return
}
account.addFolder(folder, to: nil)
}
private func uniquedFeed(_ feed: Feed) -> Feed {
// A Feed may appear in multiple places in a given account,
// but its best if theyre the same Feed instance.
// Usually this will return the same Feed that was passed-in,
// but not necessarily always.
return account?.existingFeed(with: feed.feedID) ?? feed
}
private func resolvedFolder() -> Folder? {
return path.resolveContainer() as? Folder
} }
func delete() {
guard let container = container else {
return
}
if let feed = feed {
container.deleteFeed(feed)
}
else if let folder = folder {
container.deleteFolder(folder)
}
}
} }
private extension Node { private extension Node {
func parentFolder() -> Folder? { func parentFolder() -> Folder? {
guard let parentNode = self.parent else { guard let parentNode = self.parent else {
return nil return nil
} }
if parentNode.isRoot { if parentNode.isRoot {
return nil return nil
} }
if let folder = parentNode.representedObject as? Folder { if let folder = parentNode.representedObject as? Folder {
return folder return folder
} }
return nil return nil
} }
func containingFolders() -> [Folder] { func containingFolders() -> [Folder] {
var nomad = self.parent var nomad = self.parent
var folders = [Folder]() var folders = [Folder]()
while nomad != nil { while nomad != nil {
if let folder = nomad!.representedObject as? Folder { if let folder = nomad!.representedObject as? Folder {
folders += [folder] folders += [folder]
} }
else { else {
break break
} }
nomad = nomad!.parent nomad = nomad!.parent
} }
return folders.reversed() return folders.reversed()
} }
} }
private struct DeleteActionName { private struct DeleteActionName {
private static let deleteFeed = NSLocalizedString("Delete Feed", comment: "command") private static let deleteFeed = NSLocalizedString("Delete Feed", comment: "command")
private static let deleteFeeds = NSLocalizedString("Delete Feeds", comment: "command") private static let deleteFeeds = NSLocalizedString("Delete Feeds", comment: "command")
private static let deleteFolder = NSLocalizedString("Delete Folder", comment: "command") private static let deleteFolder = NSLocalizedString("Delete Folder", comment: "command")
private static let deleteFolders = NSLocalizedString("Delete Folders", comment: "command") private static let deleteFolders = NSLocalizedString("Delete Folders", comment: "command")
private static let deleteFeedsAndFolders = NSLocalizedString("Delete Feeds and Folders", comment: "command") private static let deleteFeedsAndFolders = NSLocalizedString("Delete Feeds and Folders", comment: "command")
static func name(for nodes: [Node]) -> String? { static func name(for nodes: [Node]) -> String? {
var numberOfFeeds = 0 var numberOfFeeds = 0
var numberOfFolders = 0 var numberOfFolders = 0
for node in nodes { for node in nodes {
if let _ = node.representedObject as? Feed { if let _ = node.representedObject as? Feed {
numberOfFeeds += 1 numberOfFeeds += 1
} }
else if let _ = node.representedObject as? Folder { else if let _ = node.representedObject as? Folder {
numberOfFolders += 1 numberOfFolders += 1
} }
else { else {
return nil // Delete only Feeds and Folders. return nil // Delete only Feeds and Folders.
} }
} }
if numberOfFolders < 1 { if numberOfFolders < 1 {
return numberOfFeeds == 1 ? deleteFeed : deleteFeeds return numberOfFeeds == 1 ? deleteFeed : deleteFeeds
} }
if numberOfFeeds < 1 { if numberOfFeeds < 1 {
return numberOfFolders == 1 ? deleteFolder : deleteFolders return numberOfFolders == 1 ? deleteFolder : deleteFolders
} }
return deleteFeedsAndFolders return deleteFeedsAndFolders
} }
} }

View File

@ -20,6 +20,7 @@ import RSCore
TreeController(delegate: treeControllerDelegate) TreeController(delegate: treeControllerDelegate)
}() }()
var undoableCommands = [UndoableCommand]() var undoableCommands = [UndoableCommand]()
private var animatingChanges = false
//MARK: NSViewController //MARK: NSViewController
@ -79,11 +80,13 @@ import RSCore
let selectedRows = outlineView.selectedRowIndexes let selectedRows = outlineView.selectedRowIndexes
animatingChanges = true
outlineView.beginUpdates() outlineView.beginUpdates()
outlineView.removeItems(at: selectedRows, inParent: nil, withAnimation: [.slideDown]) outlineView.removeItems(at: selectedRows, inParent: nil, withAnimation: [.slideDown])
outlineView.endUpdates() outlineView.endUpdates()
runCommand(deleteCommand) runCommand(deleteCommand)
animatingChanges = false
} }
// MARK: Navigation // MARK: Navigation
@ -170,7 +173,7 @@ private extension SidebarViewController {
func rebuildTreeAndReloadDataIfNeeded() { func rebuildTreeAndReloadDataIfNeeded() {
if !BatchUpdate.shared.isPerforming { if !animatingChanges && !BatchUpdate.shared.isPerforming {
treeController.rebuild() treeController.rebuild()
outlineView.reloadData() outlineView.reloadData()
} }

View File

@ -276,7 +276,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
@discardableResult @discardableResult
public func addFolder(_ folder: Folder, to parentFolder: Folder?) -> Bool { public func addFolder(_ folder: Folder, to parentFolder: Folder?) -> Bool {
return false // TODO // TODO: support subfolders, maybe, some day, if one of the sync systems
// supports subfolders. But, for now, parentFolder is ignored.
if objectIsChild(folder) {
return true
}
children += [folder]
postChildrenDidChangeNotification()
return true
} }
public func importOPML(_ opmlDocument: RSOPMLDocument) { public func importOPML(_ opmlDocument: RSOPMLDocument) {