Make deleting work. Can’t undo yet. But now everything is messed-up because of an AnyHashable casting bug. Don’t run this build.

This commit is contained in:
Brent Simmons 2017-11-04 22:51:14 -07:00
parent 66c02a6fe1
commit e16911b363
7 changed files with 193 additions and 88 deletions

View File

@ -14,14 +14,7 @@ import Data
final class DeleteFromSidebarCommand: UndoableCommand { final class DeleteFromSidebarCommand: UndoableCommand {
private struct ActionName { let undoManager: UndoManager
static let deleteFeed = NSLocalizedString("Delete Feed", comment: "command")
static let deleteFeeds = NSLocalizedString("Delete Feeds", comment: "command")
static let deleteFolder = NSLocalizedString("Delete Folder", comment: "command")
static let deleteFolders = NSLocalizedString("Delete Folders", comment: "command")
static let deleteFeedsAndFolders = NSLocalizedString("Delete Feeds and Folders", comment: "command")
}
let undoActionName: String let undoActionName: String
var redoActionName: String { var redoActionName: String {
get { get {
@ -29,49 +22,40 @@ final class DeleteFromSidebarCommand: UndoableCommand {
} }
} }
let undoManager: UndoManager private let itemSpecifiers: [SidebarItemSpecifier]
init?(nodesToDelete: [Node], undoManager: UndoManager) { init?(nodesToDelete: [Node], undoManager: UndoManager) {
var numberOfFeeds = 0 guard DeleteFromSidebarCommand.canDelete(nodesToDelete) else {
var numberOfFolders = 0 return nil
for node in nodesToDelete {
if let _ = node.representedObject as? Feed {
numberOfFeeds += 1
} }
else if let _ = node.representedObject as? Folder { guard let actionName = DeleteActionName.name(for: nodesToDelete) else {
numberOfFolders += 1
}
else {
return nil // Delete only Feeds and Folders.
}
}
if numberOfFeeds < 1 && numberOfFolders < 1 {
return nil return nil
} }
if numberOfFolders < 1 { self.undoActionName = actionName
self.undoActionName = numberOfFeeds == 1 ? ActionName.deleteFeed : ActionName.deleteFeeds
}
else if numberOfFeeds < 1 {
self.undoActionName = numberOfFolders == 1 ? ActionName.deleteFolder : ActionName.deleteFolders
}
else {
self.undoActionName = ActionName.deleteFeedsAndFolders
}
self.undoManager = undoManager self.undoManager = undoManager
let itemSpecifiers = nodesToDelete.flatMap{ SidebarItemSpecifier(node: $0) }
guard !itemSpecifiers.isEmpty else {
return nil
}
self.itemSpecifiers = itemSpecifiers
} }
func perform() { func perform() {
BatchUpdate.shared.perform {
itemSpecifiers.forEach { $0.delete() }
}
registerUndo() registerUndo()
} }
func undo() { func undo() {
BatchUpdate.shared.perform {
}
registerRedo() registerRedo()
} }
@ -103,15 +87,30 @@ final class DeleteFromSidebarCommand: UndoableCommand {
private struct SidebarItemSpecifier { private struct SidebarItemSpecifier {
weak var account: Account? private weak var account: Account?
let folder: Folder? private let parentFolder: Folder?
let feed: Feed? private let folder: Folder?
let path: ContainerPath private let feed: Feed?
private let path: ContainerPath
private var container: Container? {
get {
if let parentFolder = parentFolder {
return parentFolder
}
if let account = account {
return account
}
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 { if let feed = node.representedObject as? Feed {
self.feed = feed self.feed = feed
self.folder = nil self.folder = nil
@ -132,10 +131,38 @@ private struct SidebarItemSpecifier {
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)
}
}
} }
private extension Node { private extension Node {
func parentFolder() -> Folder? {
guard let parentNode = self.parent else {
return nil
}
if parentNode.isRoot {
return nil
}
if let folder = parentNode.representedObject as? Folder {
return folder
}
return nil
}
func containingFolders() -> [Folder] { func containingFolders() -> [Folder] {
var nomad = self.parent var nomad = self.parent
@ -156,3 +183,38 @@ private extension Node {
} }
private struct DeleteActionName {
private static let deleteFeed = NSLocalizedString("Delete Feed", comment: "command")
private static let deleteFeeds = NSLocalizedString("Delete Feeds", comment: "command")
private static let deleteFolder = NSLocalizedString("Delete Folder", comment: "command")
private static let deleteFolders = NSLocalizedString("Delete Folders", comment: "command")
private static let deleteFeedsAndFolders = NSLocalizedString("Delete Feeds and Folders", comment: "command")
static func name(for nodes: [Node]) -> String? {
var numberOfFeeds = 0
var numberOfFolders = 0
for node in nodes {
if let _ = node.representedObject as? Feed {
numberOfFeeds += 1
}
else if let _ = node.representedObject as? Folder {
numberOfFolders += 1
}
else {
return nil // Delete only Feeds and Folders.
}
}
if numberOfFolders < 1 {
return numberOfFeeds == 1 ? deleteFeed : deleteFeeds
}
if numberOfFeeds < 1 {
return numberOfFolders == 1 ? deleteFolder : deleteFolders
}
return deleteFeedsAndFolders
}
}

View File

@ -12,13 +12,14 @@ import Data
import Account import Account
import RSCore import RSCore
@objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSOutlineViewDataSource { @objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSOutlineViewDataSource, UndoableCommandRunner {
@IBOutlet var outlineView: SidebarOutlineView! @IBOutlet var outlineView: SidebarOutlineView!
let treeControllerDelegate = SidebarTreeControllerDelegate() let treeControllerDelegate = SidebarTreeControllerDelegate()
lazy var treeController: TreeController = { lazy var treeController: TreeController = {
TreeController(delegate: treeControllerDelegate) TreeController(delegate: treeControllerDelegate)
}() }()
var undoableCommands = [UndoableCommand]()
//MARK: NSViewController //MARK: NSViewController
@ -71,15 +72,18 @@ import RSCore
} }
let nodesToDelete = treeController.normalizedSelectedNodes(selectedNodes) let nodesToDelete = treeController.normalizedSelectedNodes(selectedNodes)
guard let undoManager = undoManager, let deleteCommand = DeleteFromSidebarCommand(nodesToDelete: nodesToDelete, undoManager: undoManager) else {
return
}
let selectedRows = outlineView.selectedRowIndexes let selectedRows = outlineView.selectedRowIndexes
outlineView.beginUpdates() outlineView.beginUpdates()
outlineView.removeItems(at: selectedRows, inParent: nil, withAnimation: [.slideDown]) outlineView.removeItems(at: selectedRows, inParent: nil, withAnimation: [.slideDown])
outlineView.endUpdates() outlineView.endUpdates()
BatchUpdate.shared.perform { runCommand(deleteCommand)
deleteItemsForNodes(nodesToDelete)
}
} }
// MARK: Navigation // MARK: Navigation

View File

@ -12,7 +12,7 @@ import RSTextDrawing
import Data import Data
import Account import Account
class TimelineViewController: NSViewController, KeyboardDelegate { class TimelineViewController: NSViewController, KeyboardDelegate, UndoableCommandRunner {
@IBOutlet var tableView: TimelineTableView! @IBOutlet var tableView: TimelineTableView!
@ -22,7 +22,7 @@ class TimelineViewController: NSViewController, KeyboardDelegate {
} }
} }
private var undoableCommands = [UndoableCommand]() var undoableCommands = [UndoableCommand]()
private var cellAppearance: TimelineCellAppearance! private var cellAppearance: TimelineCellAppearance!
private var showFeedNames = false private var showFeedNames = false
private var didRegisterForNotifications = false private var didRegisterForNotifications = false
@ -319,36 +319,6 @@ class TimelineViewController: NSViewController, KeyboardDelegate {
} }
// MARK: - Undoable Commands
private extension TimelineViewController {
func runCommand(_ undoableCommand: UndoableCommand) {
pushUndoableCommand(undoableCommand)
undoableCommand.perform()
}
func pushUndoableCommand(_ undoableCommand: UndoableCommand) {
undoableCommands += [undoableCommand]
}
func clearUndoableCommands() {
// When the timeline is reloaded and the list of articles changes,
// undoable commands should be dropped otherwise things like
// Redo Mark Read are ambiguous. (Do they apply to the previous articles
// or to the current articles?)
guard let undoManager = undoManager else {
return
}
undoableCommands.forEach { undoManager.removeAllActions(withTarget: $0) }
undoableCommands = [UndoableCommand]()
}
}
// MARK: - NSTableViewDataSource // MARK: - NSTableViewDataSource
extension TimelineViewController: NSTableViewDataSource { extension TimelineViewController: NSTableViewDataSource {

View File

@ -16,9 +16,9 @@ extension NSNotification.Name {
public static let ChildrenDidChange = Notification.Name("ChildrenDidChange") public static let ChildrenDidChange = Notification.Name("ChildrenDidChange")
} }
public protocol Container { public protocol Container: class {
var children: [AnyObject] { get } var children: [AnyObject] { get set }
func hasAtLeastOneFeed() -> Bool func hasAtLeastOneFeed() -> Bool
func objectIsChild(_ object: AnyObject) -> Bool func objectIsChild(_ object: AnyObject) -> Bool
@ -26,6 +26,9 @@ public protocol Container {
func hasChildFolder(with: String) -> Bool func hasChildFolder(with: String) -> Bool
func childFolder(with: String) -> Folder? func childFolder(with: String) -> Folder?
func deleteFeed(_ feed: Feed)
func deleteFolder(_ folder: Folder)
//Recursive //Recursive
func flattenedFeeds() -> Set<Feed> func flattenedFeeds() -> Set<Feed>
func hasFeed(with feedID: String) -> Bool func hasFeed(with feedID: String) -> Bool
@ -172,6 +175,34 @@ public extension Container {
return nil return nil
} }
func indexOf<T: Equatable>(_ object: T) -> Int? {
return children.index(where: { (child) -> Bool in
if let oneObject = child as? T {
return oneObject == object
}
return false
})
}
func delete<T: Equatable>(_ object: T) {
if let index = indexOf(object) {
children.remove(at: index)
postChildrenDidChangeNotification()
}
}
func deleteFeed(_ feed: Feed) {
return delete(feed)
}
func deleteFolder(_ folder: Folder) {
return delete(folder)
}
func postChildrenDidChangeNotification() { func postChildrenDidChangeNotification() {
NotificationCenter.default.post(name: .ChildrenDidChange, object: self) NotificationCenter.default.post(name: .ChildrenDidChange, object: self)

View File

@ -36,3 +36,41 @@ extension UndoableCommand {
} }
} }
} }
// Useful for view controllers.
public protocol UndoableCommandRunner: class {
var undoableCommands: [UndoableCommand] { get set }
var undoManager: UndoManager? { get }
func runCommand(_ undoableCommand: UndoableCommand)
func clearUndoableCommands()
}
public extension UndoableCommandRunner {
func runCommand(_ undoableCommand: UndoableCommand) {
pushUndoableCommand(undoableCommand)
undoableCommand.perform()
}
func pushUndoableCommand(_ undoableCommand: UndoableCommand) {
undoableCommands += [undoableCommand]
}
func clearUndoableCommands() {
// Useful, for example, when timeline is reloaded and the list of articles changes.
// Otherwise things like Redo Mark Read are ambiguous.
// (Do they apply to the previous articles or to the current articles?)
guard let undoManager = undoManager else {
return
}
undoableCommands.forEach { undoManager.removeAllActions(withTarget: $0) }
undoableCommands = [UndoableCommand]()
}
}