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:
parent
66c02a6fe1
commit
e16911b363
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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]()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user