2018-01-28 22:28:33 +01:00
//
2018-02-04 20:19:24 +01:00
// S i d e b a r V i e w C o n t r o l l e r + C o n t e x t u a l M e n u s . s w i f t
2018-08-29 07:18:24 +02:00
// N e t N e w s W i r e
2018-01-28 22:28:33 +01:00
//
// C r e a t e d b y B r e n t S i m m o n s o n 1 / 2 8 / 1 8 .
// C o p y r i g h t © 2 0 1 8 R a n c h e r o S o f t w a r e . A l l r i g h t s r e s e r v e d .
//
import AppKit
2018-07-24 03:29:08 +02:00
import Articles
2018-01-28 22:28:33 +01:00
import Account
2021-02-02 04:54:47 +01:00
import UserNotifications
2024-03-21 04:49:15 +01:00
import AppKitExtras
import Core
2018-01-28 22:28:33 +01:00
2021-02-02 03:26:34 +01:00
extension Notification . Name {
public static let DidUpdateFeedPreferencesFromContextMenu = Notification . Name ( rawValue : " DidUpdateFeedPreferencesFromContextMenu " )
}
2018-02-04 20:19:24 +01:00
extension SidebarViewController {
2018-01-28 22:28:33 +01:00
func menu ( for objects : [ Any ] ? ) -> NSMenu ? {
guard let objects = objects , objects . count > 0 else {
2018-02-04 06:30:30 +01:00
return menuForNoSelection ( )
2018-01-28 22:28:33 +01:00
}
2018-02-11 21:59:35 +01:00
if objects . count > 1 {
return menuForMultipleObjects ( objects )
2018-01-28 22:28:33 +01:00
}
2018-02-11 21:59:35 +01:00
let object = objects . first !
switch object {
2024-02-26 06:41:18 +01:00
case is Feed :
2024-02-26 08:12:21 +01:00
return menuForFeed ( object as ! Feed )
2018-02-11 21:59:35 +01:00
case is Folder :
return menuForFolder ( object as ! Folder )
case is PseudoFeed :
return menuForSmartFeed ( object as ! PseudoFeed )
default :
return nil
}
2018-01-28 22:28:33 +01:00
}
}
// MARK: C o n t e x t u a l M e n u A c t i o n s
2018-02-04 20:19:24 +01:00
extension SidebarViewController {
2018-01-28 22:28:33 +01:00
@objc func openHomePageFromContextualMenu ( _ sender : Any ? ) {
2018-01-29 01:09:18 +01:00
guard let menuItem = sender as ? NSMenuItem , let urlString = menuItem . representedObject as ? String else {
return
}
Browser . open ( urlString , inBackground : false )
2018-01-28 22:28:33 +01:00
}
2018-01-29 01:09:18 +01:00
@objc func copyURLFromContextualMenu ( _ sender : Any ? ) {
2018-01-28 22:28:33 +01:00
2018-01-29 01:09:18 +01: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 22:28:33 +01:00
}
@objc func markObjectsReadFromContextualMenu ( _ sender : Any ? ) {
2018-02-04 20:45:51 +01:00
guard let menuItem = sender as ? NSMenuItem , let objects = menuItem . representedObject as ? [ Any ] else {
return
}
2024-03-19 05:08:37 +01: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 20:45:51 +01:00
}
2018-01-28 22:28:33 +01:00
}
@objc func deleteFromContextualMenu ( _ sender : Any ? ) {
2018-09-26 04:10:54 +02: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 23:54:20 +02: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 22:28:33 +01:00
}
@objc func renameFromContextualMenu ( _ sender : Any ? ) {
2024-02-26 06:41:18 +01: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-04 03:49:29 +01:00
return
}
2018-02-04 06:04:28 +01:00
renameWindowController = RenameWindowController ( originalTitle : object . nameForDisplay , representedObject : object , delegate : self )
guard let renameSheet = renameWindowController ? . window else {
return
2018-02-04 03:49:29 +01:00
}
2018-02-04 06:04:28 +01:00
window . beginSheet ( renameSheet )
}
2021-02-02 01:16:45 +01:00
@objc func toggleNotificationsFromContextMenu ( _ sender : Any ? ) {
guard let item = sender as ? NSMenuItem ,
2024-02-26 06:41:18 +01:00
let feed = item . representedObject as ? Feed else {
2021-02-02 01:16:45 +01:00
return
}
2021-02-02 04:54:47 +01:00
UNUserNotificationCenter . current ( ) . getNotificationSettings { ( settings ) in
if settings . authorizationStatus = = . denied {
self . showNotificationsNotEnabledAlert ( )
} else if settings . authorizationStatus = = . authorized {
DispatchQueue . main . async {
2021-02-02 06:25:06 +01:00
if feed . isNotifyAboutNewArticles = = nil { feed . isNotifyAboutNewArticles = false }
2021-02-02 04:54:47 +01:00
feed . isNotifyAboutNewArticles ? . toggle ( )
NotificationCenter . default . post ( Notification ( name : . DidUpdateFeedPreferencesFromContextMenu ) )
}
} else {
UNUserNotificationCenter . current ( ) . requestAuthorization ( options : [ . badge , . sound , . alert ] ) { ( granted , error ) in
if granted {
DispatchQueue . main . async {
2021-02-02 06:25:06 +01:00
if feed . isNotifyAboutNewArticles = = nil { feed . isNotifyAboutNewArticles = false }
2021-02-02 04:54:47 +01:00
feed . isNotifyAboutNewArticles ? . toggle ( )
NotificationCenter . default . post ( Notification ( name : . DidUpdateFeedPreferencesFromContextMenu ) )
NSApplication . shared . registerForRemoteNotifications ( )
}
} else {
self . showNotificationsNotEnabledAlert ( )
}
}
}
}
2021-02-02 01:16:45 +01:00
}
@objc func toggleArticleExtractorFromContextMenu ( _ sender : Any ? ) {
guard let item = sender as ? NSMenuItem ,
2024-02-26 06:41:18 +01:00
let feed = item . representedObject as ? Feed else {
2021-02-02 01:16:45 +01:00
return
}
2021-02-02 06:25:06 +01:00
if feed . isArticleExtractorAlwaysOn = = nil { feed . isArticleExtractorAlwaysOn = false }
2021-02-02 01:16:45 +01:00
feed . isArticleExtractorAlwaysOn ? . toggle ( )
2021-02-02 03:26:34 +01:00
NotificationCenter . default . post ( Notification ( name : . DidUpdateFeedPreferencesFromContextMenu ) )
2021-02-02 01:16:45 +01:00
}
2021-02-02 04:54:47 +01: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
// I f S y s t e m P r e f e r e n c e s i s a l r e a d y o p e n , a n d n o d e l a y i s p r o v i d e d h e r e , t h e n i t a p p e a r s i n t h e f o r e g r o u n d a n d i m m e d i a t e l y d i s a p p e a r s .
DispatchQueue . main . asyncAfter ( wallDeadline : . now ( ) + 0.2 , execute : {
NSWorkspace . shared . open ( URL ( string : " x-apple.systempreferences:com.apple.preference.notifications " ) ! , configuration : config )
} )
}
}
}
2018-02-04 06:04:28 +01:00
}
2018-02-04 20:19:24 +01:00
extension SidebarViewController : RenameWindowControllerDelegate {
2018-02-04 06:04:28 +01:00
func renameWindowController ( _ windowController : RenameWindowController , didRenameObject object : Any , withNewName name : String ) {
2018-02-04 03:49:29 +01:00
2024-02-26 06:41:18 +01:00
if let feed = object as ? Feed {
2019-05-09 00:55:53 +02:00
feed . rename ( to : name ) { result in
switch result {
case . success :
break
case . failure ( let error ) :
NSApplication . shared . presentError ( error )
}
}
} else if let folder = object as ? Folder {
2019-05-06 17:53:20 +02:00
folder . rename ( to : name ) { result in
switch result {
case . success :
break
case . failure ( let error ) :
NSApplication . shared . presentError ( error )
}
}
2018-02-04 06:04:28 +01:00
}
2018-01-28 22:28:33 +01:00
}
}
// MARK: B u i l d C o n t e x t u a l M e n u s
2018-02-04 20:19:24 +01:00
private extension SidebarViewController {
2018-01-28 22:28:33 +01:00
2018-02-04 06:30:30 +01:00
func menuForNoSelection ( ) -> NSMenu {
let menu = NSMenu ( title : " " )
2024-02-26 08:12:21 +01:00
menu . addItem ( withTitle : NSLocalizedString ( " New Feed " , comment : " Command " ) , action : #selector ( AppDelegate . showAddFeedWindow ( _ : ) ) , keyEquivalent : " " )
2019-05-19 22:27:58 +02:00
menu . addItem ( withTitle : NSLocalizedString ( " New Folder " , comment : " Command " ) , action : #selector ( AppDelegate . showAddFolderWindow ( _ : ) ) , keyEquivalent : " " )
2018-02-04 06:30:30 +01:00
return menu
}
2024-02-26 08:12:21 +01:00
func menuForFeed ( _ feed : Feed ) -> NSMenu ? {
2018-01-28 22:28:33 +01:00
let menu = NSMenu ( title : " " )
2024-02-26 08:12:21 +01:00
if feed . unreadCount > 0 {
menu . addItem ( markAllReadMenuItem ( [ feed ] ) )
2018-01-28 22:28:33 +01:00
menu . addItem ( NSMenuItem . separator ( ) )
}
2024-02-26 08:12:21 +01:00
if let homePageURL = feed . homePageURL , let _ = URL ( string : homePageURL ) {
2020-07-06 17:06:12 +02:00
let item = menuItem ( NSLocalizedString ( " Open Home Page " , comment : " Command " ) , #selector ( openHomePageFromContextualMenu ( _ : ) ) , homePageURL . decodedURLString ? ? homePageURL )
2018-01-28 22:28:33 +01:00
menu . addItem ( item )
menu . addItem ( NSMenuItem . separator ( ) )
}
2024-02-26 08:12:21 +01:00
let copyFeedURLItem = menuItem ( NSLocalizedString ( " Copy Feed URL " , comment : " Command " ) , #selector ( copyURLFromContextualMenu ( _ : ) ) , feed . url . decodedURLString ? ? feed . url )
2018-01-28 22:28:33 +01:00
menu . addItem ( copyFeedURLItem )
2024-02-26 08:12:21 +01:00
if let homePageURL = feed . homePageURL {
2020-07-06 17:06:12 +02:00
let item = menuItem ( NSLocalizedString ( " Copy Home Page URL " , comment : " Command " ) , #selector ( copyURLFromContextualMenu ( _ : ) ) , homePageURL . decodedURLString ? ? homePageURL )
2018-01-28 22:28:33 +01:00
menu . addItem ( item )
}
menu . addItem ( NSMenuItem . separator ( ) )
2021-02-02 01:16:45 +01:00
2024-02-26 08:12:21 +01:00
let notificationText = feed . notificationDisplayName . capitalized
2021-02-02 03:26:34 +01:00
2024-02-26 08:12:21 +01:00
let notificationMenuItem = menuItem ( notificationText , #selector ( toggleNotificationsFromContextMenu ( _ : ) ) , feed )
if feed . isNotifyAboutNewArticles = = nil || feed . isNotifyAboutNewArticles ! = = false {
2021-02-02 03:26:34 +01:00
notificationMenuItem . state = . off
2021-02-02 01:16:45 +01:00
} else {
2021-02-02 03:26:34 +01:00
notificationMenuItem . state = . on
2021-02-02 01:16:45 +01:00
}
2021-02-02 03:26:34 +01:00
menu . addItem ( notificationMenuItem )
2023-06-26 01:15:21 +02:00
let articleExtractorText = NSLocalizedString ( " Always Use Reader View " , comment : " Always Use Reader View " )
2024-02-26 08:12:21 +01:00
let articleExtractorMenuItem = menuItem ( articleExtractorText , #selector ( toggleArticleExtractorFromContextMenu ( _ : ) ) , feed )
2023-06-26 01:15:21 +02:00
2024-02-26 08:12:21 +01:00
if feed . isArticleExtractorAlwaysOn = = nil || feed . isArticleExtractorAlwaysOn ! = = false {
2023-06-26 01:15:21 +02:00
articleExtractorMenuItem . state = . off
} else {
articleExtractorMenuItem . state = . on
2021-02-02 01:16:45 +01:00
}
2023-06-26 01:15:21 +02:00
menu . addItem ( articleExtractorMenuItem )
2021-02-02 01:16:45 +01:00
menu . addItem ( NSMenuItem . separator ( ) )
2024-02-26 08:12:21 +01:00
menu . addItem ( renameMenuItem ( feed ) )
menu . addItem ( deleteMenuItem ( [ feed ] ) )
2018-01-28 22:28:33 +01: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-29 01:09:18 +01:00
menu . addItem ( renameMenuItem ( folder ) )
2018-09-26 04:10:54 +02:00
menu . addItem ( deleteMenuItem ( [ folder ] ) )
2018-01-29 01:09:18 +01:00
2018-01-28 22:28:33 +01:00
return menu . numberOfItems > 0 ? menu : nil
}
2018-02-11 21:59:35 +01:00
func menuForSmartFeed ( _ smartFeed : PseudoFeed ) -> NSMenu ? {
2018-01-28 22:28:33 +01:00
2018-02-11 21:59:35 +01:00
let menu = NSMenu ( title : " " )
if smartFeed . unreadCount > 0 {
menu . addItem ( markAllReadMenuItem ( [ smartFeed ] ) )
2018-01-29 01:09:18 +01:00
}
2018-02-11 21:59:35 +01:00
return menu . numberOfItems > 0 ? menu : nil
}
func menuForMultipleObjects ( _ objects : [ Any ] ) -> NSMenu ? {
2018-01-28 22:28:33 +01:00
let menu = NSMenu ( title : " " )
if anyObjectInArrayHasNonZeroUnreadCount ( objects ) {
menu . addItem ( markAllReadMenuItem ( objects ) )
}
2018-09-26 04:10:54 +02:00
if allObjectsAreFeedsAndOrFolders ( objects ) {
menu . addSeparatorIfNeeded ( )
menu . addItem ( deleteMenuItem ( objects ) )
}
2018-01-28 22:28:33 +01: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-29 01:09:18 +01:00
func allObjectsAreFeedsAndOrFolders ( _ objects : [ Any ] ) -> Bool {
for object in objects {
if ! objectIsFeedOrFolder ( object ) {
return false
}
}
return true
}
func objectIsFeedOrFolder ( _ object : Any ) -> Bool {
2024-02-26 06:41:18 +01:00
return object is Feed || object is Folder
2018-01-29 01:09:18 +01:00
}
2018-01-28 22:28:33 +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-02-04 20:45:51 +01:00
2024-03-19 05:08:37 +01:00
@ MainActor func unreadArticles ( for objects : [ Any ] ) async -> Set < Article > {
2018-02-04 20:45:51 +01:00
var articles = Set < Article > ( )
for object in objects {
if let articleFetcher = object as ? ArticleFetcher {
2024-03-19 05:08:37 +01:00
if let unreadArticles = try ? await articleFetcher . fetchUnreadArticles ( ) {
2019-12-17 07:45:59 +01:00
articles . formUnion ( unreadArticles )
}
2018-02-04 20:45:51 +01:00
}
}
return articles
}
2018-01-28 22:28:33 +01:00
}