2018-02-10 08:16:12 +01:00
|
|
|
//
|
|
|
|
// TimelineViewController+ContextualMenus.swift
|
2018-08-29 07:18:24 +02:00
|
|
|
// NetNewsWire
|
2018-02-10 08:16:12 +01:00
|
|
|
//
|
|
|
|
// Created by Brent Simmons on 2/9/18.
|
|
|
|
// Copyright © 2018 Ranchero Software. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import AppKit
|
2019-05-21 21:57:22 +02:00
|
|
|
import RSCore
|
2018-07-24 03:29:08 +02:00
|
|
|
import Articles
|
2018-02-10 08:16:12 +01:00
|
|
|
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?) {
|
2020-03-09 00:15:17 +01:00
|
|
|
guard let articles = articles(from: sender) else { return }
|
2018-02-10 08:16:12 +01:00
|
|
|
markArticles(articles, read: true)
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func markArticlesUnreadFromContextualMenu(_ sender: Any?) {
|
2020-03-09 00:15:17 +01:00
|
|
|
guard let articles = articles(from: sender) else { return }
|
2018-09-06 17:26:57 +02:00
|
|
|
markArticles(articles, read: false)
|
2018-09-05 06:34:06 +02:00
|
|
|
}
|
|
|
|
|
2020-03-09 00:15:17 +01:00
|
|
|
@objc func markAboveArticlesReadFromContextualMenu(_ sender: Any?) {
|
|
|
|
guard let articles = articles(from: sender) else { return }
|
|
|
|
markAboveArticlesRead(articles)
|
|
|
|
}
|
2018-09-05 06:34:06 +02:00
|
|
|
|
2020-03-09 00:15:17 +01:00
|
|
|
@objc func markBelowArticlesReadFromContextualMenu(_ sender: Any?) {
|
|
|
|
guard let articles = articles(from: sender) else { return }
|
|
|
|
markBelowArticlesRead(articles)
|
2018-02-10 08:16:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func markArticlesStarredFromContextualMenu(_ sender: Any?) {
|
2020-03-09 00:15:17 +01:00
|
|
|
guard let articles = articles(from: sender) else { return }
|
2018-02-18 21:09:13 +01:00
|
|
|
markArticles(articles, starred: true)
|
2018-02-10 08:16:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func markArticlesUnstarredFromContextualMenu(_ sender: Any?) {
|
2018-02-18 21:09:13 +01:00
|
|
|
guard let articles = articles(from: sender) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
markArticles(articles, starred: false)
|
2018-02-10 08:16:12 +01:00
|
|
|
}
|
|
|
|
|
2018-09-26 02:20:43 +02:00
|
|
|
@objc func selectFeedInSidebarFromContextualMenu(_ sender: Any?) {
|
2020-03-01 00:50:13 +01:00
|
|
|
guard let menuItem = sender as? NSMenuItem, let webFeed = menuItem.representedObject as? WebFeed else {
|
2018-09-26 02:20:43 +02:00
|
|
|
return
|
|
|
|
}
|
2020-03-01 00:50:13 +01:00
|
|
|
delegate?.timelineRequestedWebFeedSelection(self, webFeed: webFeed)
|
2018-09-26 02:20:43 +02:00
|
|
|
}
|
|
|
|
|
2019-05-22 17:07:00 +02:00
|
|
|
@objc func markAllInFeedAsRead(_ sender: Any?) {
|
|
|
|
guard let menuItem = sender as? NSMenuItem, let feedArticles = menuItem.representedObject as? ArticleArray else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: feedArticles, markingRead: true, undoManager: undoManager) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
runCommand(markReadCommand)
|
2019-05-21 21:57:22 +02:00
|
|
|
}
|
|
|
|
|
2018-02-10 08:16:12 +01:00
|
|
|
@objc func openInBrowserFromContextualMenu(_ sender: Any?) {
|
|
|
|
|
|
|
|
guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
Browser.open(urlString, inBackground: false)
|
|
|
|
}
|
2021-05-01 22:47:39 +02:00
|
|
|
|
|
|
|
@objc func copyURLFromContextualMenu(_ sender: Any?) {
|
|
|
|
guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
URLPasteboardWriter.write(urlString: urlString, to: .general)
|
|
|
|
}
|
2018-09-04 02:02:10 +02:00
|
|
|
|
|
|
|
@objc func performShareServiceFromContextualMenu(_ sender: Any?) {
|
|
|
|
guard let menuItem = sender as? NSMenuItem, let sharingCommandInfo = menuItem.representedObject as? SharingCommandInfo else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sharingCommandInfo.perform()
|
|
|
|
}
|
2018-02-10 08:16:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private extension TimelineViewController {
|
|
|
|
|
|
|
|
func markArticles(_ articles: [Article], read: Bool) {
|
2018-02-18 21:09:13 +01:00
|
|
|
markArticles(articles, statusKey: .read, flag: read)
|
|
|
|
}
|
|
|
|
|
|
|
|
func markArticles(_ articles: [Article], starred: Bool) {
|
|
|
|
markArticles(articles, statusKey: .starred, flag: starred)
|
|
|
|
}
|
|
|
|
|
|
|
|
func markArticles(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool) {
|
|
|
|
guard let undoManager = undoManager, let markStatusCommand = MarkStatusCommand(initialArticles: articles, statusKey: statusKey, flag: flag, undoManager: undoManager) else {
|
2018-02-10 08:16:12 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-02-18 21:09:13 +01:00
|
|
|
runCommand(markStatusCommand)
|
2018-02-10 08:16:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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))
|
|
|
|
}
|
2020-02-29 19:30:35 +01:00
|
|
|
if articles.anyArticleIsReadAndCanMarkUnread() {
|
2018-02-10 08:16:12 +01:00
|
|
|
menu.addItem(markUnreadMenuItem(articles))
|
|
|
|
}
|
2018-02-18 21:09:13 +01:00
|
|
|
if articles.anyArticleIsUnstarred() {
|
|
|
|
menu.addItem(markStarredMenuItem(articles))
|
|
|
|
}
|
|
|
|
if articles.anyArticleIsStarred() {
|
|
|
|
menu.addItem(markUnstarredMenuItem(articles))
|
|
|
|
}
|
2020-03-09 00:15:17 +01:00
|
|
|
if let first = articles.first, self.articles.articlesAbove(article: first).canMarkAllAsRead() {
|
|
|
|
menu.addItem(markAboveReadMenuItem(articles))
|
|
|
|
}
|
|
|
|
if let last = articles.last, self.articles.articlesBelow(article: last).canMarkAllAsRead() {
|
|
|
|
menu.addItem(markBelowReadMenuItem(articles))
|
2018-09-05 06:34:06 +02:00
|
|
|
}
|
2018-02-10 08:16:12 +01:00
|
|
|
|
2018-09-26 02:20:43 +02:00
|
|
|
menu.addSeparatorIfNeeded()
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
if articles.count == 1, let feed = articles.first!.webFeed {
|
2020-03-01 20:07:24 +01:00
|
|
|
if !(representedObjects?.contains(where: { $0 as? WebFeed == feed }) ?? false) {
|
|
|
|
menu.addItem(selectFeedInSidebarMenuItem(feed))
|
|
|
|
}
|
2019-05-22 17:07:00 +02:00
|
|
|
if let markAllMenuItem = markAllAsReadMenuItem(feed) {
|
|
|
|
menu.addItem(markAllMenuItem)
|
|
|
|
}
|
2018-09-26 02:20:43 +02:00
|
|
|
}
|
|
|
|
|
2018-02-10 08:16:12 +01:00
|
|
|
if articles.count == 1, let link = articles.first!.preferredLink {
|
2018-09-26 02:20:43 +02:00
|
|
|
menu.addSeparatorIfNeeded()
|
2018-02-10 08:16:12 +01:00
|
|
|
menu.addItem(openInBrowserMenuItem(link))
|
2021-05-01 22:47:39 +02:00
|
|
|
menu.addSeparatorIfNeeded()
|
|
|
|
menu.addItem(copyArticleURLMenuItem(link))
|
|
|
|
|
2021-09-30 05:46:11 +02:00
|
|
|
if let externalLink = articles.first?.externalLink, externalLink != link {
|
2021-05-01 22:47:39 +02:00
|
|
|
menu.addItem(copyExternalURLMenuItem(externalLink))
|
|
|
|
}
|
2018-02-10 08:16:12 +01:00
|
|
|
}
|
|
|
|
|
2018-09-04 02:02:10 +02:00
|
|
|
if let sharingMenu = shareMenu(for: articles) {
|
|
|
|
menu.addSeparatorIfNeeded()
|
|
|
|
let menuItem = NSMenuItem(title: sharingMenu.title, action: nil, keyEquivalent: "")
|
|
|
|
menuItem.submenu = sharingMenu
|
|
|
|
menu.addItem(menuItem)
|
|
|
|
}
|
|
|
|
|
|
|
|
return menu
|
|
|
|
}
|
|
|
|
|
|
|
|
func shareMenu(for articles: [Article]) -> NSMenu? {
|
|
|
|
if articles.isEmpty {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-12-28 06:49:52 +01:00
|
|
|
let sortedArticles = articles.sortedByDate(.orderedAscending)
|
2018-09-26 05:20:59 +02:00
|
|
|
let items = sortedArticles.map { ArticlePasteboardWriter(article: $0) }
|
2018-09-04 02:02:10 +02:00
|
|
|
let standardServices = NSSharingService.sharingServices(forItems: items)
|
|
|
|
let customServices = SharingServicePickerDelegate.customSharingServices(for: items)
|
|
|
|
let services = standardServices + customServices
|
|
|
|
if services.isEmpty {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
let menu = NSMenu(title: NSLocalizedString("Share", comment: "Share menu name"))
|
|
|
|
services.forEach { (service) in
|
2018-09-26 05:20:59 +02:00
|
|
|
service.delegate = sharingServiceDelegate
|
2018-09-04 02:02:10 +02:00
|
|
|
let menuItem = NSMenuItem(title: service.menuItemTitle, action: #selector(performShareServiceFromContextualMenu(_:)), keyEquivalent: "")
|
|
|
|
menuItem.image = service.image
|
|
|
|
let sharingCommandInfo = SharingCommandInfo(service: service, items: items)
|
|
|
|
menuItem.representedObject = sharingCommandInfo
|
|
|
|
menu.addItem(menuItem)
|
|
|
|
}
|
|
|
|
|
2018-02-10 08:16:12 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-03-09 00:15:17 +01:00
|
|
|
func markAboveReadMenuItem(_ articles: [Article]) -> NSMenuItem {
|
|
|
|
return menuItem(NSLocalizedString("Mark Above as Read", comment: "Command"), #selector(markAboveArticlesReadFromContextualMenu(_:)), articles)
|
|
|
|
}
|
|
|
|
|
|
|
|
func markBelowReadMenuItem(_ articles: [Article]) -> NSMenuItem {
|
|
|
|
return menuItem(NSLocalizedString("Mark Below as Read", comment: "Command"), #selector(markBelowArticlesReadFromContextualMenu(_:)), articles)
|
2018-09-05 06:34:06 +02:00
|
|
|
}
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
func selectFeedInSidebarMenuItem(_ feed: WebFeed) -> NSMenuItem {
|
2019-08-13 00:41:14 +02:00
|
|
|
let localizedMenuText = NSLocalizedString("Select “%@” in Sidebar", comment: "Command")
|
2019-05-21 21:57:22 +02:00
|
|
|
let formattedMenuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay)
|
|
|
|
return menuItem(formattedMenuText as String, #selector(selectFeedInSidebarFromContextualMenu(_:)), feed)
|
2018-09-26 02:20:43 +02:00
|
|
|
}
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
func markAllAsReadMenuItem(_ feed: WebFeed) -> NSMenuItem? {
|
2019-12-17 07:45:59 +01:00
|
|
|
guard let articlesSet = try? feed.fetchArticles() else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
let articles = Array(articlesSet)
|
2019-05-22 17:07:00 +02:00
|
|
|
guard articles.canMarkAllAsRead() else {
|
2019-05-21 21:57:22 +02:00
|
|
|
return nil
|
|
|
|
}
|
2019-12-17 07:45:59 +01:00
|
|
|
|
2019-08-13 00:41:14 +02:00
|
|
|
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
2019-05-22 17:07:00 +02:00
|
|
|
let menuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
|
2019-05-21 21:57:22 +02:00
|
|
|
|
2019-05-22 17:07:00 +02:00
|
|
|
return menuItem(menuText, #selector(markAllInFeedAsRead(_:)), articles)
|
2019-05-21 21:57:22 +02:00
|
|
|
}
|
|
|
|
|
2018-02-10 08:16:12 +01:00
|
|
|
func openInBrowserMenuItem(_ urlString: String) -> NSMenuItem {
|
|
|
|
|
|
|
|
return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlString)
|
|
|
|
}
|
2021-05-01 22:47:39 +02:00
|
|
|
|
|
|
|
func copyArticleURLMenuItem(_ urlString: String) -> NSMenuItem {
|
|
|
|
return menuItem(NSLocalizedString("Copy Article URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString)
|
|
|
|
}
|
|
|
|
|
|
|
|
func copyExternalURLMenuItem(_ urlString: String) -> NSMenuItem {
|
|
|
|
return menuItem(NSLocalizedString("Copy External URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString)
|
|
|
|
}
|
|
|
|
|
2018-02-10 08:16:12 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2018-09-04 02:02:10 +02:00
|
|
|
|
|
|
|
private final class SharingCommandInfo {
|
|
|
|
|
|
|
|
let service: NSSharingService
|
|
|
|
let items: [Any]
|
|
|
|
|
|
|
|
init(service: NSSharingService, items: [Any]) {
|
|
|
|
self.service = service
|
|
|
|
self.items = items
|
|
|
|
}
|
|
|
|
|
|
|
|
func perform() {
|
|
|
|
service.perform(withItems: items)
|
|
|
|
}
|
|
|
|
}
|