Files
NetNewsWire/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift
2023-07-09 22:20:58 -07:00

337 lines
12 KiB
Swift

//
// TimelineViewController+ContextualMenus.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/9/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
import RSCore
import Articles
import Account
extension TimelineViewController {
var shareMenu: NSMenu? {
return shareMenu(for: selectedArticles)
}
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, directlyMarked: true)
}
@objc func markArticlesUnreadFromContextualMenu(_ sender: Any?) {
guard let articles = articles(from: sender) else { return }
markArticles(articles, read: false, directlyMarked: true)
}
@objc func markAboveArticlesReadFromContextualMenu(_ sender: Any?) {
guard let articles = articles(from: sender) else { return }
markAboveArticlesRead(articles)
}
@objc func markBelowArticlesReadFromContextualMenu(_ sender: Any?) {
guard let articles = articles(from: sender) else { return }
markBelowArticlesRead(articles)
}
@objc func markArticlesStarredFromContextualMenu(_ sender: Any?) {
guard let articles = articles(from: sender) else { return }
markArticles(articles, starred: true, directlyMarked: true)
}
@objc func markArticlesUnstarredFromContextualMenu(_ sender: Any?) {
guard let articles = articles(from: sender) else {
return
}
markArticles(articles, starred: false, directlyMarked: true)
}
@objc func selectFeedInSidebarFromContextualMenu(_ sender: Any?) {
guard let menuItem = sender as? NSMenuItem, let feed = menuItem.representedObject as? Feed else {
return
}
delegate?.timelineRequestedFeedSelection(self, feed: feed)
}
@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,
directlyMarked: false,
undoManager: undoManager) else {
return
}
runCommand(markReadCommand)
}
@objc func openInBrowserFromContextualMenu(_ sender: Any?) {
guard let menuItem = sender as? NSMenuItem, let urlStrings = menuItem.representedObject as? [String] else {
return
}
Browser.open(urlStrings, fromWindow: self.view.window, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
}
@objc func copyURLFromContextualMenu(_ sender: Any?) {
guard let menuItem = sender as? NSMenuItem, let urlStrings = menuItem.representedObject as? [String?] else {
return
}
URLPasteboardWriter.write(urlStrings: urlStrings, alertingIn: self.view.window)
}
@objc func performShareServiceFromContextualMenu(_ sender: Any?) {
guard let menuItem = sender as? NSMenuItem, let sharingCommandInfo = menuItem.representedObject as? SharingCommandInfo else {
return
}
sharingCommandInfo.perform()
}
}
private extension TimelineViewController {
func markArticles(_ articles: [Article], read: Bool, directlyMarked: Bool) {
markArticles(articles, statusKey: .read, flag: read, directlyMarked: directlyMarked)
}
func markArticles(_ articles: [Article], starred: Bool, directlyMarked: Bool) {
markArticles(articles, statusKey: .starred, flag: starred, directlyMarked: directlyMarked)
}
func markArticles(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool) {
guard let undoManager = undoManager,
let markStatusCommand = MarkStatusCommand(initialArticles: articles,
statusKey: statusKey,
flag: flag,
directlyMarked: directlyMarked,
undoManager: undoManager) else {
return
}
runCommand(markStatusCommand)
}
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.anyArticleIsUnreadAndCanMarkRead() {
menu.addItem(markReadMenuItem(articles))
}
if articles.anyArticleIsReadAndCanMarkUnread() {
menu.addItem(markUnreadMenuItem(articles))
}
if articles.anyArticleIsUnstarred() {
menu.addItem(markStarredMenuItem(articles))
}
if articles.anyArticleIsStarred() {
menu.addItem(markUnstarredMenuItem(articles))
}
if let first = articles.first, self.articles.articlesAbove(article: first).canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) {
menu.addItem(markAboveReadMenuItem(articles))
}
if let last = articles.last, self.articles.articlesBelow(article: last).canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) {
menu.addItem(markBelowReadMenuItem(articles))
}
menu.addSeparatorIfNeeded()
if articles.count == 1, let feed = articles.first!.feed {
if !(representedObjects?.contains(where: { $0 as? Feed == feed }) ?? false) {
menu.addItem(selectFeedInSidebarMenuItem(feed))
}
if let markAllMenuItem = markAllAsReadMenuItem(feed) {
menu.addItem(markAllMenuItem)
}
}
let links = articles.map { $0.preferredLink }
let compactLinks = links.compactMap { $0 }
if compactLinks.count > 0 {
menu.addSeparatorIfNeeded()
menu.addItem(openInBrowserMenuItem(compactLinks))
menu.addItem(openInBrowserReversedMenuItem(compactLinks))
menu.addSeparatorIfNeeded()
menu.addItem(copyArticleURLsMenuItem(links))
if let externalLink = articles.first?.externalLink, externalLink != links.first {
menu.addItem(copyExternalURLMenuItem(externalLink))
}
}
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
}
let sortedArticles = articles.sortedByDate(.orderedAscending)
let items = sortedArticles.map { ArticlePasteboardWriter(article: $0) }
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("button.title.share", comment: "Share menu name"))
for service in services {
service.delegate = sharingServiceDelegate
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)
}
return menu
}
func markReadMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("button.title.mark-as-read", comment: "Mark as Read"), #selector(markArticlesReadFromContextualMenu(_:)), articles)
}
func markUnreadMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("button.title.mark-as-unread", comment: "Mark as Unread"), #selector(markArticlesUnreadFromContextualMenu(_:)), articles)
}
func markStarredMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("button.title.mark-as-starred", comment: "Mark as Starred"), #selector(markArticlesStarredFromContextualMenu(_:)), articles)
}
func markUnstarredMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("button.title.mark-as-unstarred", comment: "Mark as Unstarred"), #selector(markArticlesUnstarredFromContextualMenu(_:)), articles)
}
func markAboveReadMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("button.title-mark-above-as-read.titlecase", comment: "Mark Above as Read"), #selector(markAboveArticlesReadFromContextualMenu(_:)), articles)
}
func markBelowReadMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("button.title-mark-below-as-read.titlecase", comment: "Mark Below as Read"), #selector(markBelowArticlesReadFromContextualMenu(_:)), articles)
}
func selectFeedInSidebarMenuItem(_ feed: Feed) -> NSMenuItem {
let localizedMenuText = NSLocalizedString("button.title.select-in-sidebar.%@", comment: "Select “%@” in Sidebar")
let formattedMenuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay)
return menuItem(formattedMenuText as String, #selector(selectFeedInSidebarFromContextualMenu(_:)), feed)
}
func markAllAsReadMenuItem(_ feed: Feed) -> NSMenuItem? {
guard let articlesSet = try? feed.fetchArticles() else {
return nil
}
let articles = Array(articlesSet)
guard articles.canMarkAllAsRead() else {
return nil
}
let localizedMenuText = NSLocalizedString("button.title.mark-all-as-read.%@", comment: "Mark All as Read in “%@”")
let menuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
return menuItem(menuText, #selector(markAllInFeedAsRead(_:)), articles)
}
func openInBrowserMenuItem(_ urlStrings: [String]) -> NSMenuItem {
return menuItem(NSLocalizedString("button.title.open-in-browser", comment: "Open in Browser"), #selector(openInBrowserFromContextualMenu(_:)), urlStrings)
}
func openInBrowserReversedMenuItem(_ urlStrings: [String]) -> NSMenuItem {
let item = menuItem(Browser.titleForOpenInBrowserInverted, #selector(openInBrowserFromContextualMenu(_:)), urlStrings)
item.keyEquivalentModifierMask = .shift
item.isAlternate = true
return item;
}
func copyArticleURLsMenuItem(_ urlStrings: [String?]) -> NSMenuItem {
let format = NSLocalizedString("button.title.copy-article-urls.%ld", comment: "Copy Article URL or Copy Article URLs (if more than one)")
let title = String.localizedStringWithFormat(format, urlStrings.count)
return menuItem(title, #selector(copyURLFromContextualMenu(_:)), urlStrings)
}
func copyExternalURLMenuItem(_ urlString: String) -> NSMenuItem {
return menuItem(NSLocalizedString("button.title.copy-external-url", comment: "Copy External URL"), #selector(copyURLFromContextualMenu(_:)), 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
}
}
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)
}
}