From d46ae4df33d06a9d26d29e55cd340d6695343c4c Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Fri, 9 Feb 2018 23:16:12 -0800 Subject: [PATCH] Add contextual menu to timeline. --- Evergreen.xcodeproj/project.pbxproj | 4 + Evergreen/Base.lproj/MainWindow.storyboard | 6 +- .../MainWindow/Timeline/ArticleArray.swift | 27 ++- .../TimelineContextualMenuDelegate.swift | 23 ++- ...melineViewController+ContextualMenus.swift | 162 ++++++++++++++++++ .../Timeline/TimelineViewController.swift | 20 +-- 6 files changed, 217 insertions(+), 25 deletions(-) create mode 100644 Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 2c3b71a7b..ad0dbe098 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -136,6 +136,7 @@ 84DAEE321F870B390058304B /* DockBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAEE311F870B390058304B /* DockBadge.swift */; }; 84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */; }; 84E850861FCB60CE0072EA88 /* AuthorAvatarDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */; }; + 84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */; }; 84E95CF71FABB3C800552D99 /* FeedList.plist in Resources */ = {isa = PBXBuildFile; fileRef = 84E95CF61FABB3C800552D99 /* FeedList.plist */; }; 84E95D241FB1087500552D99 /* ArticlePasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */; }; 84EB381F1FBA8B9F000D2111 /* KeyboardShortcuts.html in Resources */ = {isa = PBXBuildFile; fileRef = 84EB38101FBA8B9F000D2111 /* KeyboardShortcuts.html */; }; @@ -629,6 +630,7 @@ 84DAEE311F870B390058304B /* DockBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DockBadge.swift; path = Evergreen/DockBadge.swift; sourceTree = ""; }; 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDefaults.swift; path = Evergreen/AppDefaults.swift; sourceTree = ""; }; 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorAvatarDownloader.swift; sourceTree = ""; }; + 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineViewController+ContextualMenus.swift"; sourceTree = ""; }; 84E95CF61FABB3C800552D99 /* FeedList.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = FeedList.plist; sourceTree = ""; }; 84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlePasteboardWriter.swift; sourceTree = ""; }; 84EB38101FBA8B9F000D2111 /* KeyboardShortcuts.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = KeyboardShortcuts.html; sourceTree = ""; }; @@ -940,6 +942,7 @@ isa = PBXGroup; children = ( 849A976B1ED9EBC8007D329B /* TimelineViewController.swift */, + 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */, 84F204DF1FAACBB30076E152 /* ArticleArray.swift */, 849A97691ED9EBC8007D329B /* TimelineTableRowView.swift */, 849A976A1ED9EBC8007D329B /* TimelineTableView.swift */, @@ -1878,6 +1881,7 @@ 84A1500520048DDF0046AD9A /* SendToMarsEditCommand.swift in Sources */, D5907DB22004BB37005947E5 /* ScriptingObjectContainer.swift in Sources */, 849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */, + 84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */, 849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */, 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */, 84F204CE1FAACB660076E152 /* FeedListViewController.swift in Sources */, diff --git a/Evergreen/Base.lproj/MainWindow.storyboard b/Evergreen/Base.lproj/MainWindow.storyboard index 455531086..a5adb6bbf 100644 --- a/Evergreen/Base.lproj/MainWindow.storyboard +++ b/Evergreen/Base.lproj/MainWindow.storyboard @@ -650,7 +650,11 @@ - + + + + + diff --git a/Evergreen/MainWindow/Timeline/ArticleArray.swift b/Evergreen/MainWindow/Timeline/ArticleArray.swift index 2c30fa99d..b1a8d7512 100644 --- a/Evergreen/MainWindow/Timeline/ArticleArray.swift +++ b/Evergreen/MainWindow/Timeline/ArticleArray.swift @@ -78,13 +78,38 @@ extension Array where Element == Article { func canMarkAllAsRead() -> Bool { + return anyArticleIsUnread() + } + + func anyArticlePassesTest(_ test: ((Article) -> Bool)) -> Bool { + for article in self { - if !article.status.read { + if test(article) { return true } } return false } + + func anyArticleIsRead() -> Bool { + + return anyArticlePassesTest { $0.status.read } + } + + func anyArticleIsUnread() -> Bool { + + return anyArticlePassesTest { !$0.status.read } + } + + func anyArticleIsStarred() -> Bool { + + return anyArticlePassesTest { $0.status.starred } + } + + func anyArticleIsUnstarred() -> Bool { + + return anyArticlePassesTest { !$0.status.starred } + } } private extension Array where Element == Article { diff --git a/Evergreen/MainWindow/Timeline/TimelineContextualMenuDelegate.swift b/Evergreen/MainWindow/Timeline/TimelineContextualMenuDelegate.swift index d1d6ac2b6..2510f5d77 100644 --- a/Evergreen/MainWindow/Timeline/TimelineContextualMenuDelegate.swift +++ b/Evergreen/MainWindow/Timeline/TimelineContextualMenuDelegate.swift @@ -7,6 +7,7 @@ // import AppKit +import RSCore @objc final class TimelineContextualMenuDelegate: NSObject, NSMenuDelegate { @@ -14,21 +15,17 @@ import AppKit public func menuNeedsUpdate(_ menu: NSMenu) { -// guard let timelineViewController = timelineViewController else { -// return -// } + guard let timelineViewController = timelineViewController else { + return + } -// menu.removeAllItems() + menu.removeAllItems() -// guard let contextualMenu = sidebarViewController.contextualMenuForClickedRows() else { -// return -// } -// -// let items = contextualMenu.items -// contextualMenu.removeAllItems() -// for menuItem in items { -// menu.addItem(menuItem) -// } + guard let contextualMenu = timelineViewController.contextualMenuForClickedRows() else { + return + } + + menu.takeItems(from: contextualMenu) } } diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift b/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift new file mode 100644 index 000000000..3a0df2348 --- /dev/null +++ b/Evergreen/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift @@ -0,0 +1,162 @@ +// +// TimelineViewController+ContextualMenus.swift +// Evergreen +// +// Created by Brent Simmons on 2/9/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import AppKit +import Data +import Account + +extension TimelineViewController { + + func contextualMenuForClickedRows() -> NSMenu? { + + let row = tableView.clickedRow + guard row != -1, let article = articles.articleAtRow(row) else { + return nil + } + + if selectedArticles.contains(article) { + // If the clickedRow is part of the selected rows, then do a contextual menu for all the selected rows. + return menu(for: selectedArticles) + } + return menu(for: [article]) + } +} + +// MARK: Contextual Menu Actions + +extension TimelineViewController { + + @objc func markArticlesReadFromContextualMenu(_ sender: Any?) { + + guard let articles = articles(from: sender) else { + return + } + markArticles(articles, read: true) + } + + @objc func markArticlesUnreadFromContextualMenu(_ sender: Any?) { + + guard let articles = articles(from: sender) else { + return + } + markArticles(articles, read: false) + } + + @objc func markArticlesStarredFromContextualMenu(_ sender: Any?) { + + } + + @objc func markArticlesUnstarredFromContextualMenu(_ sender: Any?) { + + } + + @objc func openInBrowserFromContextualMenu(_ sender: Any?) { + + guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else { + return + } + Browser.open(urlString, inBackground: false) + } +} + + +private extension TimelineViewController { + + func markArticles(_ articles: [Article], read: Bool) { + + guard let articlesToMark = read ? unreadArticles(from: articles) : readArticles(from: articles) else { + return + } + guard let undoManager = undoManager, let markReadCommand = MarkReadOrUnreadCommand(initialArticles: Array(articlesToMark), markingRead: read, undoManager: undoManager) else { + return + } + + runCommand(markReadCommand) + } + + func unreadArticles(from articles: [Article]) -> [Article]? { + + let filteredArticles = articles.filter { !$0.status.read } + return filteredArticles.isEmpty ? nil : filteredArticles + } + + func readArticles(from articles: [Article]) -> [Article]? { + + let filteredArticles = articles.filter { $0.status.read } + return filteredArticles.isEmpty ? nil : filteredArticles + } + + func articles(from sender: Any?) -> [Article]? { + + return (sender as? NSMenuItem)?.representedObject as? [Article] + } + + func menu(for articles: [Article]) -> NSMenu? { + + let menu = NSMenu(title: "") + + if articles.anyArticleIsUnread() { + menu.addItem(markReadMenuItem(articles)) + } + if articles.anyArticleIsRead() { + menu.addItem(markUnreadMenuItem(articles)) + } + if menu.items.count > 0 { + menu.addItem(NSMenuItem.separator()) + } + +// if articles.anyArticleIsUnstarred() { +// menu.addItem(markStarredMenuItem(articles)) +// } +// if articles.anyArticleIsStarred() { +// menu.addItem(markUnstarredMenuItem(articles)) +// } + if menu.items.count > 0 && !menu.items.last!.isSeparatorItem { + menu.addItem(NSMenuItem.separator()) + } + + if articles.count == 1, let link = articles.first!.preferredLink { + menu.addItem(openInBrowserMenuItem(link)) + } + + return menu + } + + func markReadMenuItem(_ articles: [Article]) -> NSMenuItem { + + return menuItem(NSLocalizedString("Mark as Read", comment: "Command"), #selector(markArticlesReadFromContextualMenu(_:)), articles) + } + + func markUnreadMenuItem(_ articles: [Article]) -> NSMenuItem { + + return menuItem(NSLocalizedString("Mark as Unread", comment: "Command"), #selector(markArticlesUnreadFromContextualMenu(_:)), articles) + } + + func markStarredMenuItem(_ articles: [Article]) -> NSMenuItem { + + return menuItem(NSLocalizedString("Mark as Starred", comment: "Command"), #selector(markArticlesStarredFromContextualMenu(_:)), articles) + } + + func markUnstarredMenuItem(_ articles: [Article]) -> NSMenuItem { + + return menuItem(NSLocalizedString("Mark as Unstarred", comment: "Command"), #selector(markArticlesUnstarredFromContextualMenu(_:)), articles) + } + + func openInBrowserMenuItem(_ urlString: String) -> NSMenuItem { + + return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlString) + } + + func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem { + + let item = NSMenuItem(title: title, action: action, keyEquivalent: "") + item.representedObject = representedObject + item.target = self + return item + } +} diff --git a/Evergreen/MainWindow/Timeline/TimelineViewController.swift b/Evergreen/MainWindow/Timeline/TimelineViewController.swift index e97061e16..9fad68058 100644 --- a/Evergreen/MainWindow/Timeline/TimelineViewController.swift +++ b/Evergreen/MainWindow/Timeline/TimelineViewController.swift @@ -29,6 +29,16 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { } } + var articles = ArticleArray() { + didSet { + if articles != oldValue { + clearUndoableCommands() + updateShowAvatars() + tableView.reloadData() + } + } + } + var undoableCommands = [UndoableCommand]() private var cellAppearance: TimelineCellAppearance! private var cellAppearanceWithAvatar: TimelineCellAppearance! @@ -61,16 +71,6 @@ class TimelineViewController: NSViewController, UndoableCommandRunner { } } } - private var articles = ArticleArray() { - didSet { - if articles != oldValue { - clearUndoableCommands() - updateShowAvatars() - tableView.reloadData() - } - } - } - private var fontSize: FontSize = AppDefaults.shared.timelineFontSize { didSet { if fontSize != oldValue {