2018-01-28 13:28:33 -08:00
|
|
|
//
|
2018-02-04 11:19:24 -08:00
|
|
|
// SidebarViewController+ContextualMenus.swift
|
2018-08-28 22:18:24 -07:00
|
|
|
// NetNewsWire
|
2018-01-28 13:28:33 -08:00
|
|
|
//
|
|
|
|
// Created by Brent Simmons on 1/28/18.
|
|
|
|
// Copyright © 2018 Ranchero Software. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import AppKit
|
2018-07-23 18:29:08 -07:00
|
|
|
import Articles
|
2018-01-28 13:28:33 -08:00
|
|
|
import Account
|
2021-02-02 11:54:47 +08:00
|
|
|
import UserNotifications
|
2024-03-20 20:49:15 -07:00
|
|
|
import AppKitExtras
|
|
|
|
import Core
|
2018-01-28 13:28:33 -08:00
|
|
|
|
2021-02-02 10:26:34 +08:00
|
|
|
extension Notification.Name {
|
|
|
|
public static let DidUpdateFeedPreferencesFromContextMenu = Notification.Name(rawValue: "DidUpdateFeedPreferencesFromContextMenu")
|
|
|
|
}
|
|
|
|
|
2018-02-04 11:19:24 -08:00
|
|
|
extension SidebarViewController {
|
2018-01-28 13:28:33 -08:00
|
|
|
|
|
|
|
func menu(for objects: [Any]?) -> NSMenu? {
|
|
|
|
|
|
|
|
guard let objects = objects, objects.count > 0 else {
|
2018-02-03 21:30:30 -08:00
|
|
|
return menuForNoSelection()
|
2018-01-28 13:28:33 -08:00
|
|
|
}
|
|
|
|
|
2018-02-11 12:59:35 -08:00
|
|
|
if objects.count > 1 {
|
|
|
|
return menuForMultipleObjects(objects)
|
2018-01-28 13:28:33 -08:00
|
|
|
}
|
|
|
|
|
2018-02-11 12:59:35 -08:00
|
|
|
let object = objects.first!
|
|
|
|
|
|
|
|
switch object {
|
2024-02-25 21:41:18 -08:00
|
|
|
case is Feed:
|
2024-02-25 23:12:21 -08:00
|
|
|
return menuForFeed(object as! Feed)
|
2018-02-11 12:59:35 -08:00
|
|
|
case is Folder:
|
|
|
|
return menuForFolder(object as! Folder)
|
|
|
|
case is PseudoFeed:
|
|
|
|
return menuForSmartFeed(object as! PseudoFeed)
|
|
|
|
default:
|
|
|
|
return nil
|
|
|
|
}
|
2018-01-28 13:28:33 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Contextual Menu Actions
|
|
|
|
|
2018-02-04 11:19:24 -08:00
|
|
|
extension SidebarViewController {
|
2018-01-28 13:28:33 -08:00
|
|
|
|
|
|
|
@objc func openHomePageFromContextualMenu(_ sender: Any?) {
|
|
|
|
|
2018-01-28 16:09:18 -08:00
|
|
|
guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
Browser.open(urlString, inBackground: false)
|
2018-01-28 13:28:33 -08:00
|
|
|
}
|
|
|
|
|
2018-01-28 16:09:18 -08:00
|
|
|
@objc func copyURLFromContextualMenu(_ sender: Any?) {
|
2018-01-28 13:28:33 -08:00
|
|
|
|
2018-01-28 16:09:18 -08:00
|
|
|
guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
URLPasteboardWriter.write(urlString: urlString, to: NSPasteboard.general)
|
2018-01-28 13:28:33 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func markObjectsReadFromContextualMenu(_ sender: Any?) {
|
|
|
|
|
2018-02-04 11:45:51 -08:00
|
|
|
guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [Any] else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-03-18 21:08:37 -07:00
|
|
|
Task {
|
|
|
|
let articles = await unreadArticles(for: objects)
|
|
|
|
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
runCommand(markReadCommand)
|
2018-02-04 11:45:51 -08:00
|
|
|
}
|
2018-01-28 13:28:33 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func deleteFromContextualMenu(_ sender: Any?) {
|
2018-09-25 21:10:54 -05:00
|
|
|
guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [AnyObject] else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let nodes = objects.compactMap { treeController.nodeInTreeRepresentingObject($0) }
|
2020-10-23 16:54:20 -05:00
|
|
|
|
|
|
|
let alert = SidebarDeleteItemsAlert.build(nodes)
|
|
|
|
alert.beginSheetModal(for: view.window!) { [weak self] result in
|
|
|
|
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
|
|
|
|
self?.deleteNodes(nodes)
|
|
|
|
}
|
|
|
|
}
|
2018-01-28 13:28:33 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func renameFromContextualMenu(_ sender: Any?) {
|
|
|
|
|
2024-02-25 21:41:18 -08:00
|
|
|
guard let window = view.window, let menuItem = sender as? NSMenuItem, let object = menuItem.representedObject as? DisplayNameProvider, object is Feed || object is Folder else {
|
2018-02-03 18:49:29 -08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2018-02-03 21:04:28 -08:00
|
|
|
renameWindowController = RenameWindowController(originalTitle: object.nameForDisplay, representedObject: object, delegate: self)
|
|
|
|
guard let renameSheet = renameWindowController?.window else {
|
|
|
|
return
|
2018-02-03 18:49:29 -08:00
|
|
|
}
|
2018-02-03 21:04:28 -08:00
|
|
|
window.beginSheet(renameSheet)
|
|
|
|
}
|
2021-02-02 08:16:45 +08:00
|
|
|
|
|
|
|
@objc func toggleNotificationsFromContextMenu(_ sender: Any?) {
|
|
|
|
guard let item = sender as? NSMenuItem,
|
2024-02-25 21:41:18 -08:00
|
|
|
let feed = item.representedObject as? Feed else {
|
2021-02-02 08:16:45 +08:00
|
|
|
return
|
|
|
|
}
|
2021-02-02 11:54:47 +08:00
|
|
|
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
|
|
|
|
if settings.authorizationStatus == .denied {
|
|
|
|
self.showNotificationsNotEnabledAlert()
|
|
|
|
} else if settings.authorizationStatus == .authorized {
|
|
|
|
DispatchQueue.main.async {
|
2024-05-25 23:05:38 -07:00
|
|
|
if feed.shouldSendUserNotificationForNewArticles == nil { feed.shouldSendUserNotificationForNewArticles = false }
|
|
|
|
feed.shouldSendUserNotificationForNewArticles?.toggle()
|
2021-02-02 11:54:47 +08:00
|
|
|
NotificationCenter.default.post(Notification(name: .DidUpdateFeedPreferencesFromContextMenu))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in
|
|
|
|
if granted {
|
|
|
|
DispatchQueue.main.async {
|
2024-05-25 23:05:38 -07:00
|
|
|
if feed.shouldSendUserNotificationForNewArticles == nil { feed.shouldSendUserNotificationForNewArticles = false }
|
|
|
|
feed.shouldSendUserNotificationForNewArticles?.toggle()
|
2021-02-02 11:54:47 +08:00
|
|
|
NotificationCenter.default.post(Notification(name: .DidUpdateFeedPreferencesFromContextMenu))
|
|
|
|
NSApplication.shared.registerForRemoteNotifications()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
self.showNotificationsNotEnabledAlert()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-02-02 08:16:45 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
@objc func toggleArticleExtractorFromContextMenu(_ sender: Any?) {
|
|
|
|
guard let item = sender as? NSMenuItem,
|
2024-02-25 21:41:18 -08:00
|
|
|
let feed = item.representedObject as? Feed else {
|
2021-02-02 08:16:45 +08:00
|
|
|
return
|
|
|
|
}
|
2021-02-02 13:25:06 +08:00
|
|
|
if feed.isArticleExtractorAlwaysOn == nil { feed.isArticleExtractorAlwaysOn = false }
|
2021-02-02 08:16:45 +08:00
|
|
|
feed.isArticleExtractorAlwaysOn?.toggle()
|
2021-02-02 10:26:34 +08:00
|
|
|
NotificationCenter.default.post(Notification(name: .DidUpdateFeedPreferencesFromContextMenu))
|
2021-02-02 08:16:45 +08:00
|
|
|
}
|
|
|
|
|
2021-02-02 11:54:47 +08:00
|
|
|
func showNotificationsNotEnabledAlert() {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
let alert = NSAlert()
|
|
|
|
alert.messageText = NSLocalizedString("Notifications are not enabled", comment: "Notifications are not enabled.")
|
|
|
|
alert.informativeText = NSLocalizedString("You can enable NetNewsWire notifications in System Preferences.", comment: "Notifications are not enabled.")
|
|
|
|
alert.addButton(withTitle: NSLocalizedString("Open System Preferences", comment: "Open System Preferences"))
|
|
|
|
alert.addButton(withTitle: NSLocalizedString("Dismiss", comment: "Dismiss"))
|
|
|
|
let userChoice = alert.runModal()
|
|
|
|
if userChoice == .alertFirstButtonReturn {
|
|
|
|
let config = NSWorkspace.OpenConfiguration()
|
|
|
|
config.activates = true
|
|
|
|
// If System Preferences is already open, and no delay is provided here, then it appears in the foreground and immediately disappears.
|
|
|
|
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 0.2, execute: {
|
|
|
|
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.notifications")!, configuration: config)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-03 21:04:28 -08:00
|
|
|
}
|
|
|
|
|
2018-02-04 11:19:24 -08:00
|
|
|
extension SidebarViewController: RenameWindowControllerDelegate {
|
2018-02-03 21:04:28 -08:00
|
|
|
|
2024-03-27 17:49:09 -07:00
|
|
|
func renameWindowController(_ windowController: RenameWindowController, didRenameObject object: Any, withNewName name: String) async {
|
|
|
|
|
|
|
|
guard let renamableItem = object as? Renamable else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
do {
|
|
|
|
try await renamableItem.rename(to: name)
|
|
|
|
} catch {
|
|
|
|
NSApplication.shared.presentError(error)
|
2018-02-03 21:04:28 -08:00
|
|
|
}
|
2018-01-28 13:28:33 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Build Contextual Menus
|
|
|
|
|
2018-02-04 11:19:24 -08:00
|
|
|
private extension SidebarViewController {
|
2018-01-28 13:28:33 -08:00
|
|
|
|
2018-02-03 21:30:30 -08:00
|
|
|
func menuForNoSelection() -> NSMenu {
|
|
|
|
|
|
|
|
let menu = NSMenu(title: "")
|
|
|
|
|
2024-02-25 23:12:21 -08:00
|
|
|
menu.addItem(withTitle: NSLocalizedString("New Feed", comment: "Command"), action: #selector(AppDelegate.showAddFeedWindow(_:)), keyEquivalent: "")
|
2019-05-19 15:27:58 -05:00
|
|
|
menu.addItem(withTitle: NSLocalizedString("New Folder", comment: "Command"), action: #selector(AppDelegate.showAddFolderWindow(_:)), keyEquivalent: "")
|
2018-02-03 21:30:30 -08:00
|
|
|
|
|
|
|
return menu
|
|
|
|
}
|
|
|
|
|
2024-02-25 23:12:21 -08:00
|
|
|
func menuForFeed(_ feed: Feed) -> NSMenu? {
|
2018-01-28 13:28:33 -08:00
|
|
|
|
|
|
|
let menu = NSMenu(title: "")
|
|
|
|
|
2024-02-25 23:12:21 -08:00
|
|
|
if feed.unreadCount > 0 {
|
|
|
|
menu.addItem(markAllReadMenuItem([feed]))
|
2018-01-28 13:28:33 -08:00
|
|
|
menu.addItem(NSMenuItem.separator())
|
|
|
|
}
|
|
|
|
|
2024-02-25 23:12:21 -08:00
|
|
|
if let homePageURL = feed.homePageURL, let _ = URL(string: homePageURL) {
|
2024-06-09 10:58:07 -07:00
|
|
|
let item = menuItem(NSLocalizedString("Open Home Page", comment: "Command"), #selector(openHomePageFromContextualMenu(_:)), homePageURL)
|
2018-01-28 13:28:33 -08:00
|
|
|
menu.addItem(item)
|
|
|
|
menu.addItem(NSMenuItem.separator())
|
|
|
|
}
|
|
|
|
|
2024-06-09 10:58:07 -07:00
|
|
|
let copyFeedURLItem = menuItem(NSLocalizedString("Copy Feed URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), feed.url)
|
2018-01-28 13:28:33 -08:00
|
|
|
menu.addItem(copyFeedURLItem)
|
|
|
|
|
2024-02-25 23:12:21 -08:00
|
|
|
if let homePageURL = feed.homePageURL {
|
2024-06-09 10:58:07 -07:00
|
|
|
let item = menuItem(NSLocalizedString("Copy Home Page URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), homePageURL)
|
2018-01-28 13:28:33 -08:00
|
|
|
menu.addItem(item)
|
|
|
|
}
|
|
|
|
menu.addItem(NSMenuItem.separator())
|
2021-02-02 08:16:45 +08:00
|
|
|
|
2024-02-25 23:12:21 -08:00
|
|
|
let notificationText = feed.notificationDisplayName.capitalized
|
2021-02-02 10:26:34 +08:00
|
|
|
|
2024-02-25 23:12:21 -08:00
|
|
|
let notificationMenuItem = menuItem(notificationText, #selector(toggleNotificationsFromContextMenu(_:)), feed)
|
2024-05-25 23:05:38 -07:00
|
|
|
if feed.shouldSendUserNotificationForNewArticles == nil || feed.shouldSendUserNotificationForNewArticles! == false {
|
2021-02-02 10:26:34 +08:00
|
|
|
notificationMenuItem.state = .off
|
2021-02-02 08:16:45 +08:00
|
|
|
} else {
|
2021-02-02 10:26:34 +08:00
|
|
|
notificationMenuItem.state = .on
|
2021-02-02 08:16:45 +08:00
|
|
|
}
|
2021-02-02 10:26:34 +08:00
|
|
|
menu.addItem(notificationMenuItem)
|
2023-06-25 16:15:21 -07:00
|
|
|
|
|
|
|
let articleExtractorText = NSLocalizedString("Always Use Reader View", comment: "Always Use Reader View")
|
2024-02-25 23:12:21 -08:00
|
|
|
let articleExtractorMenuItem = menuItem(articleExtractorText, #selector(toggleArticleExtractorFromContextMenu(_:)), feed)
|
2023-06-25 16:15:21 -07:00
|
|
|
|
2024-02-25 23:12:21 -08:00
|
|
|
if feed.isArticleExtractorAlwaysOn == nil || feed.isArticleExtractorAlwaysOn! == false {
|
2023-06-25 16:15:21 -07:00
|
|
|
articleExtractorMenuItem.state = .off
|
|
|
|
} else {
|
|
|
|
articleExtractorMenuItem.state = .on
|
2021-02-02 08:16:45 +08:00
|
|
|
}
|
2023-06-25 16:15:21 -07:00
|
|
|
menu.addItem(articleExtractorMenuItem)
|
|
|
|
|
2021-02-02 08:16:45 +08:00
|
|
|
menu.addItem(NSMenuItem.separator())
|
|
|
|
|
2024-02-25 23:12:21 -08:00
|
|
|
menu.addItem(renameMenuItem(feed))
|
|
|
|
menu.addItem(deleteMenuItem([feed]))
|
2018-01-28 13:28:33 -08:00
|
|
|
|
|
|
|
return menu
|
|
|
|
}
|
|
|
|
|
|
|
|
func menuForFolder(_ folder: Folder) -> NSMenu? {
|
|
|
|
|
|
|
|
let menu = NSMenu(title: "")
|
|
|
|
|
|
|
|
if folder.unreadCount > 0 {
|
|
|
|
menu.addItem(markAllReadMenuItem([folder]))
|
|
|
|
menu.addItem(NSMenuItem.separator())
|
|
|
|
}
|
|
|
|
|
2018-01-28 16:09:18 -08:00
|
|
|
menu.addItem(renameMenuItem(folder))
|
2018-09-25 21:10:54 -05:00
|
|
|
menu.addItem(deleteMenuItem([folder]))
|
2018-01-28 16:09:18 -08:00
|
|
|
|
2018-01-28 13:28:33 -08:00
|
|
|
return menu.numberOfItems > 0 ? menu : nil
|
|
|
|
}
|
|
|
|
|
2018-02-11 12:59:35 -08:00
|
|
|
func menuForSmartFeed(_ smartFeed: PseudoFeed) -> NSMenu? {
|
2018-01-28 13:28:33 -08:00
|
|
|
|
2018-02-11 12:59:35 -08:00
|
|
|
let menu = NSMenu(title: "")
|
|
|
|
|
|
|
|
if smartFeed.unreadCount > 0 {
|
|
|
|
menu.addItem(markAllReadMenuItem([smartFeed]))
|
2018-01-28 16:09:18 -08:00
|
|
|
}
|
2018-02-11 12:59:35 -08:00
|
|
|
return menu.numberOfItems > 0 ? menu : nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func menuForMultipleObjects(_ objects: [Any]) -> NSMenu? {
|
|
|
|
|
2018-01-28 13:28:33 -08:00
|
|
|
let menu = NSMenu(title: "")
|
|
|
|
|
|
|
|
if anyObjectInArrayHasNonZeroUnreadCount(objects) {
|
|
|
|
menu.addItem(markAllReadMenuItem(objects))
|
|
|
|
}
|
|
|
|
|
2018-09-25 21:10:54 -05:00
|
|
|
if allObjectsAreFeedsAndOrFolders(objects) {
|
|
|
|
menu.addSeparatorIfNeeded()
|
|
|
|
menu.addItem(deleteMenuItem(objects))
|
|
|
|
}
|
2018-01-28 13:28:33 -08:00
|
|
|
|
|
|
|
return menu.numberOfItems > 0 ? menu : nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func markAllReadMenuItem(_ objects: [Any]) -> NSMenuItem {
|
|
|
|
|
|
|
|
return menuItem(NSLocalizedString("Mark All as Read", comment: "Command"), #selector(markObjectsReadFromContextualMenu(_:)), objects)
|
|
|
|
}
|
|
|
|
|
|
|
|
func deleteMenuItem(_ objects: [Any]) -> NSMenuItem {
|
|
|
|
|
|
|
|
return menuItem(NSLocalizedString("Delete", comment: "Command"), #selector(deleteFromContextualMenu(_:)), objects)
|
|
|
|
}
|
|
|
|
|
|
|
|
func renameMenuItem(_ object: Any) -> NSMenuItem {
|
|
|
|
|
|
|
|
return menuItem(NSLocalizedString("Rename", comment: "Command"), #selector(renameFromContextualMenu(_:)), object)
|
|
|
|
}
|
|
|
|
|
|
|
|
func anyObjectInArrayHasNonZeroUnreadCount(_ objects: [Any]) -> Bool {
|
|
|
|
|
|
|
|
for object in objects {
|
|
|
|
if let unreadCountProvider = object as? UnreadCountProvider {
|
|
|
|
if unreadCountProvider.unreadCount > 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-01-28 16:09:18 -08:00
|
|
|
func allObjectsAreFeedsAndOrFolders(_ objects: [Any]) -> Bool {
|
|
|
|
|
|
|
|
for object in objects {
|
|
|
|
if !objectIsFeedOrFolder(object) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func objectIsFeedOrFolder(_ object: Any) -> Bool {
|
|
|
|
|
2024-02-25 21:41:18 -08:00
|
|
|
return object is Feed || object is Folder
|
2018-01-28 16:09:18 -08:00
|
|
|
}
|
|
|
|
|
2018-01-28 13:28:33 -08: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-02-04 11:45:51 -08:00
|
|
|
|
2024-03-18 21:08:37 -07:00
|
|
|
@MainActor func unreadArticles(for objects: [Any]) async -> Set<Article> {
|
2018-02-04 11:45:51 -08:00
|
|
|
|
|
|
|
var articles = Set<Article>()
|
|
|
|
for object in objects {
|
|
|
|
if let articleFetcher = object as? ArticleFetcher {
|
2024-03-18 21:08:37 -07:00
|
|
|
if let unreadArticles = try? await articleFetcher.fetchUnreadArticles() {
|
2019-12-16 22:45:59 -08:00
|
|
|
articles.formUnion(unreadArticles)
|
|
|
|
}
|
2018-02-04 11:45:51 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return articles
|
|
|
|
}
|
2018-01-28 13:28:33 -08:00
|
|
|
}
|
|
|
|
|