2017-05-27 19:43:27 +02:00
|
|
|
|
//
|
|
|
|
|
// MainWindowController.swift
|
2018-08-29 07:18:24 +02:00
|
|
|
|
// NetNewsWire
|
2017-05-27 19:43:27 +02:00
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 8/1/15.
|
|
|
|
|
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
2018-02-03 07:51:32 +01:00
|
|
|
|
import AppKit
|
2019-10-03 18:39:48 +02:00
|
|
|
|
import UserNotifications
|
2018-07-24 03:29:08 +02:00
|
|
|
|
import Articles
|
2017-10-07 23:40:14 +02:00
|
|
|
|
import Account
|
2018-02-18 02:45:05 +01:00
|
|
|
|
import RSCore
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2019-02-14 06:33:50 +01:00
|
|
|
|
enum TimelineSourceMode {
|
|
|
|
|
case regular, search
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-26 07:25:29 +01:00
|
|
|
|
class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
2018-02-08 06:42:33 +01:00
|
|
|
|
|
2019-10-03 22:49:27 +02:00
|
|
|
|
private var activityManager = ActivityManager()
|
|
|
|
|
|
2019-09-20 00:25:36 +02:00
|
|
|
|
private var isShowingExtractedArticle = false
|
2019-09-19 01:15:55 +02:00
|
|
|
|
private var articleExtractor: ArticleExtractor? = nil
|
2018-09-08 03:22:13 +02:00
|
|
|
|
private var sharingServicePickerDelegate: NSSharingServicePickerDelegate?
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2020-03-06 01:42:17 +01:00
|
|
|
|
private let windowAutosaveName = NSWindow.FrameAutosaveName("MainWindow")
|
2020-03-03 20:26:33 +01:00
|
|
|
|
private static let mainWindowWidthsStateKey = "mainWindowWidthsStateKey"
|
2018-02-18 05:45:15 +01:00
|
|
|
|
|
2019-02-18 03:46:28 +01:00
|
|
|
|
private var currentFeedOrFolder: AnyObject? {
|
|
|
|
|
// Nil for none or multiple selection.
|
|
|
|
|
guard let selectedObjects = selectedObjectsInSidebar(), selectedObjects.count == 1 else {
|
|
|
|
|
return nil
|
2017-10-19 06:53:45 +02:00
|
|
|
|
}
|
2019-02-18 03:46:28 +01:00
|
|
|
|
return selectedObjects.first
|
2017-10-19 06:53:45 +02:00
|
|
|
|
}
|
2018-09-19 04:57:38 +02:00
|
|
|
|
|
2018-02-18 05:45:15 +01:00
|
|
|
|
private var shareToolbarItem: NSToolbarItem? {
|
2020-08-09 17:47:56 +02:00
|
|
|
|
return window?.toolbar?.existingItem(withIdentifier: .share)
|
2018-02-18 05:45:15 +01:00
|
|
|
|
}
|
2018-02-18 05:33:30 +01:00
|
|
|
|
|
2018-02-27 07:31:07 +01:00
|
|
|
|
private static var detailViewMinimumThickness = 384
|
2019-02-18 22:10:16 +01:00
|
|
|
|
private var sidebarViewController: SidebarViewController?
|
|
|
|
|
private var timelineContainerViewController: TimelineContainerViewController?
|
|
|
|
|
private var detailViewController: DetailViewController?
|
2019-02-25 07:39:57 +01:00
|
|
|
|
private var currentSearchField: NSSearchField? = nil
|
|
|
|
|
private var searchString: String? = nil
|
|
|
|
|
private var lastSentSearchString: String? = nil
|
|
|
|
|
private var timelineSourceMode: TimelineSourceMode = .regular {
|
|
|
|
|
didSet {
|
|
|
|
|
timelineContainerViewController?.showTimeline(for: timelineSourceMode)
|
|
|
|
|
detailViewController?.showDetail(for: timelineSourceMode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private var searchSmartFeed: SmartFeed? = nil
|
2018-02-27 07:31:07 +01:00
|
|
|
|
|
2018-02-18 05:45:15 +01:00
|
|
|
|
// MARK: - NSWindowController
|
2017-12-21 23:47:12 +01:00
|
|
|
|
|
2018-02-08 06:42:33 +01:00
|
|
|
|
override func windowDidLoad() {
|
|
|
|
|
super.windowDidLoad()
|
2018-02-18 05:45:15 +01:00
|
|
|
|
|
2018-09-08 03:22:13 +02:00
|
|
|
|
sharingServicePickerDelegate = SharingServicePickerDelegate(self.window)
|
|
|
|
|
|
2020-08-10 18:43:18 +02:00
|
|
|
|
if #available(macOS 11.0, *) {
|
2021-02-16 00:49:28 +01:00
|
|
|
|
let toolbar = NSToolbar(identifier: "MainWindowToolbar")
|
|
|
|
|
toolbar.allowsUserCustomization = true
|
|
|
|
|
toolbar.autosavesConfiguration = true
|
|
|
|
|
toolbar.displayMode = .iconOnly
|
|
|
|
|
toolbar.delegate = self
|
|
|
|
|
self.window?.toolbar = toolbar
|
2020-08-09 04:28:43 +02:00
|
|
|
|
} else {
|
|
|
|
|
if !AppDefaults.shared.showTitleOnMainWindow {
|
|
|
|
|
window?.titleVisibility = .hidden
|
|
|
|
|
}
|
2017-12-02 23:54:12 +01:00
|
|
|
|
}
|
2020-03-06 01:42:17 +01:00
|
|
|
|
|
2020-03-03 20:26:33 +01:00
|
|
|
|
if let window = window {
|
|
|
|
|
let point = NSPoint(x: 128, y: 64)
|
2020-08-21 03:00:31 +02:00
|
|
|
|
let size = NSSize(width: 1345, height: 900)
|
2020-03-03 20:26:33 +01:00
|
|
|
|
let minSize = NSSize(width: 600, height: 600)
|
|
|
|
|
window.setPointAndSizeAdjustingForScreen(point: point, size: size, minimumSize: minSize)
|
2017-12-21 23:43:29 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-27 07:31:07 +01:00
|
|
|
|
detailSplitViewItem?.minimumThickness = CGFloat(MainWindowController.detailViewMinimumThickness)
|
2019-02-18 04:38:17 +01:00
|
|
|
|
|
2019-02-18 03:46:28 +01:00
|
|
|
|
sidebarViewController = splitViewController?.splitViewItems[0].viewController as? SidebarViewController
|
2019-02-18 22:10:16 +01:00
|
|
|
|
sidebarViewController!.delegate = self
|
2018-02-27 07:31:07 +01:00
|
|
|
|
|
2019-02-18 04:38:17 +01:00
|
|
|
|
timelineContainerViewController = splitViewController?.splitViewItems[1].viewController as? TimelineContainerViewController
|
2019-02-18 22:10:16 +01:00
|
|
|
|
timelineContainerViewController!.delegate = self
|
2019-02-18 04:38:17 +01:00
|
|
|
|
|
2019-02-18 07:00:04 +01:00
|
|
|
|
detailViewController = splitViewController?.splitViewItems[2].viewController as? DetailViewController
|
|
|
|
|
|
2017-10-07 23:40:14 +02:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(refreshProgressDidChange(_:)), name: .AccountRefreshDidBegin, object: nil)
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(refreshProgressDidChange(_:)), name: .AccountRefreshDidFinish, object: nil)
|
2017-10-08 02:43:10 +02:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(refreshProgressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
|
2017-10-19 06:53:45 +02:00
|
|
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
2018-09-19 04:57:38 +02:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
2020-08-10 10:58:19 +02:00
|
|
|
|
|
2017-10-19 06:53:45 +02:00
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.updateWindowTitle()
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2020-05-19 03:35:06 +02:00
|
|
|
|
|
2017-10-19 06:53:45 +02:00
|
|
|
|
}
|
2018-01-21 20:35:50 +01:00
|
|
|
|
|
2018-02-18 05:58:30 +01:00
|
|
|
|
// MARK: - API
|
|
|
|
|
|
2018-01-21 20:35:50 +01:00
|
|
|
|
func selectedObjectsInSidebar() -> [AnyObject]? {
|
|
|
|
|
return sidebarViewController?.selectedObjects
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-03 18:39:48 +02:00
|
|
|
|
func handle(_ response: UNNotificationResponse) {
|
|
|
|
|
let userInfo = response.notification.request.content.userInfo
|
2019-11-14 22:35:19 +01:00
|
|
|
|
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
|
|
|
|
|
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
|
|
|
|
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
2019-10-03 18:39:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-10-03 22:49:27 +02:00
|
|
|
|
func handle(_ activity: NSUserActivity) {
|
|
|
|
|
guard let userInfo = activity.userInfo else { return }
|
2019-11-14 22:35:19 +01:00
|
|
|
|
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return }
|
|
|
|
|
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
|
|
|
|
|
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
|
2019-10-03 22:49:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-06 01:42:17 +01:00
|
|
|
|
func saveStateToUserDefaults() {
|
2020-07-02 05:17:38 +02:00
|
|
|
|
AppDefaults.shared.windowState = savableState()
|
2020-03-06 01:42:17 +01:00
|
|
|
|
window?.saveFrame(usingName: windowAutosaveName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func restoreStateFromUserDefaults() {
|
2020-07-02 05:17:38 +02:00
|
|
|
|
if let state = AppDefaults.shared.windowState {
|
2020-03-06 01:42:17 +01:00
|
|
|
|
restoreState(from: state)
|
|
|
|
|
window?.setFrameUsingName(windowAutosaveName, force: true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-18 05:58:30 +01:00
|
|
|
|
// MARK: - Notifications
|
2018-02-08 06:42:33 +01:00
|
|
|
|
|
2017-09-17 21:54:08 +02:00
|
|
|
|
@objc func refreshProgressDidChange(_ note: Notification) {
|
2018-02-18 02:45:05 +01:00
|
|
|
|
CoalescingQueue.standard.add(self, #selector(makeToolbarValidate))
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2017-10-19 06:53:45 +02:00
|
|
|
|
|
|
|
|
|
@objc func unreadCountDidChange(_ note: Notification) {
|
2018-09-19 04:57:38 +02:00
|
|
|
|
updateWindowTitleIfNecessary(note.object)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func displayNameDidChange(_ note: Notification) {
|
|
|
|
|
updateWindowTitleIfNecessary(note.object)
|
|
|
|
|
}
|
2017-10-19 06:53:45 +02:00
|
|
|
|
|
2018-09-19 18:15:04 +02:00
|
|
|
|
private func updateWindowTitleIfNecessary(_ noteObject: Any?) {
|
2018-09-19 04:57:38 +02:00
|
|
|
|
|
2018-09-19 18:15:04 +02:00
|
|
|
|
if let folder = currentFeedOrFolder as? Folder, let noteObject = noteObject as? Folder {
|
2018-09-19 06:10:11 +02:00
|
|
|
|
if folder == noteObject {
|
2018-09-19 04:57:38 +02:00
|
|
|
|
updateWindowTitle()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
if let feed = currentFeedOrFolder as? WebFeed, let noteObject = noteObject as? WebFeed {
|
2018-09-19 06:10:11 +02:00
|
|
|
|
if feed == noteObject {
|
2018-09-19 04:57:38 +02:00
|
|
|
|
updateWindowTitle()
|
2018-09-19 06:10:11 +02:00
|
|
|
|
return
|
2018-09-19 04:57:38 +02:00
|
|
|
|
}
|
2017-10-19 06:53:45 +02:00
|
|
|
|
}
|
2018-09-19 04:57:38 +02:00
|
|
|
|
|
2018-09-19 18:15:04 +02:00
|
|
|
|
// If we don't recognize the changed object, we will test it for identity instead
|
2018-09-19 18:26:19 +02:00
|
|
|
|
// of equality. This works well for us if the window title is displaying a
|
2018-09-19 18:15:04 +02:00
|
|
|
|
// PsuedoFeed object.
|
|
|
|
|
if let currentObject = currentFeedOrFolder, let noteObject = noteObject {
|
|
|
|
|
if currentObject === noteObject as AnyObject {
|
|
|
|
|
updateWindowTitle()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-19 06:53:45 +02:00
|
|
|
|
}
|
2020-05-19 03:35:06 +02:00
|
|
|
|
|
2018-02-18 05:58:30 +01:00
|
|
|
|
// MARK: - Toolbar
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2018-02-18 00:29:55 +01:00
|
|
|
|
@objc func makeToolbarValidate() {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
|
|
|
|
window?.toolbar?.validateVisibleItems()
|
|
|
|
|
}
|
2017-12-20 00:24:38 +01:00
|
|
|
|
|
2018-02-18 05:58:30 +01:00
|
|
|
|
// MARK: - NSUserInterfaceValidations
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
|
|
|
|
public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
|
|
|
|
|
|
|
|
|
if item.action == #selector(openArticleInBrowser(_:)) {
|
2020-05-21 02:41:23 +02:00
|
|
|
|
if let item = item as? NSMenuItem, item.keyEquivalentModifierMask.contains(.shift) {
|
|
|
|
|
item.title = Browser.titleForOpenInBrowserInverted
|
|
|
|
|
}
|
2020-05-21 02:54:25 +02:00
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
|
return currentLink != nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if item.action == #selector(nextUnread(_:)) {
|
2021-03-29 21:33:08 +02:00
|
|
|
|
return canGoToNextUnread(wrappingToTop: true)
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if item.action == #selector(markAllAsRead(_:)) {
|
|
|
|
|
return canMarkAllAsRead()
|
|
|
|
|
}
|
2017-11-06 06:27:34 +01:00
|
|
|
|
|
2018-09-13 22:00:33 +02:00
|
|
|
|
if item.action == #selector(toggleRead(_:)) {
|
|
|
|
|
return validateToggleRead(item)
|
2017-11-06 06:27:34 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-17 07:35:04 +01:00
|
|
|
|
if item.action == #selector(toggleStarred(_:)) {
|
|
|
|
|
return validateToggleStarred(item)
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-09 00:15:17 +01:00
|
|
|
|
if item.action == #selector(markAboveArticlesAsRead(_:)) {
|
|
|
|
|
return canMarkAboveArticlesAsRead()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if item.action == #selector(markBelowArticlesAsRead(_:)) {
|
|
|
|
|
return canMarkBelowArticlesAsRead()
|
2017-12-25 21:21:44 +01:00
|
|
|
|
}
|
2017-12-26 21:50:34 +01:00
|
|
|
|
|
2019-09-19 01:15:55 +02:00
|
|
|
|
if item.action == #selector(toggleArticleExtractor(_:)) {
|
|
|
|
|
return validateToggleArticleExtractor(item)
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-26 21:50:34 +01:00
|
|
|
|
if item.action == #selector(toolbarShowShareMenu(_:)) {
|
|
|
|
|
return canShowShareMenu()
|
|
|
|
|
}
|
2018-02-08 06:42:33 +01:00
|
|
|
|
|
2019-02-25 08:03:32 +01:00
|
|
|
|
if item.action == #selector(moveFocusToSearchField(_:)) {
|
|
|
|
|
return currentSearchField != nil
|
|
|
|
|
}
|
2018-02-08 06:42:33 +01:00
|
|
|
|
|
2020-03-16 00:02:55 +01:00
|
|
|
|
if item.action == #selector(cleanUp(_:)) {
|
|
|
|
|
return validateCleanUp(item)
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-22 18:47:03 +01:00
|
|
|
|
if item.action == #selector(toggleReadFeedsFilter(_:)) {
|
2019-11-24 11:29:15 +01:00
|
|
|
|
return validateToggleReadFeeds(item)
|
2019-11-22 17:55:54 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-22 18:47:03 +01:00
|
|
|
|
if item.action == #selector(toggleReadArticlesFilter(_:)) {
|
2019-11-24 11:29:15 +01:00
|
|
|
|
return validateToggleReadArticles(item)
|
2019-11-22 18:47:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-14 20:36:18 +01:00
|
|
|
|
if item.action == #selector(toggleTheSidebar(_:)) {
|
2018-02-08 06:42:33 +01:00
|
|
|
|
guard let splitViewItem = sidebarSplitViewItem else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let sidebarIsShowing = !splitViewItem.isCollapsed
|
|
|
|
|
if let menuItem = item as? NSMenuItem {
|
|
|
|
|
let title = sidebarIsShowing ? NSLocalizedString("Hide Sidebar", comment: "Menu item") : NSLocalizedString("Show Sidebar", comment: "Menu item")
|
|
|
|
|
menuItem.title = title
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
2019-05-19 16:10:19 +02:00
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-20 21:59:31 +01:00
|
|
|
|
// MARK: - Actions
|
|
|
|
|
|
|
|
|
|
@IBAction func scrollOrGoToNextUnread(_ sender: Any?) {
|
2019-02-18 22:10:16 +01:00
|
|
|
|
guard let detailViewController = detailViewController else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-12-20 22:39:31 +01:00
|
|
|
|
detailViewController.canScrollDown { (canScroll) in
|
2018-09-02 02:25:29 +02:00
|
|
|
|
NSCursor.setHiddenUntilMouseMoves(true)
|
2019-02-18 22:10:16 +01:00
|
|
|
|
canScroll ? detailViewController.scrollPageDown(sender) : self.nextUnread(sender)
|
2017-12-20 22:39:31 +01:00
|
|
|
|
}
|
2017-12-20 21:59:31 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-10 20:33:25 +02:00
|
|
|
|
@IBAction func scrollUp(_ sender: Any?) {
|
|
|
|
|
guard let detailViewController = detailViewController else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
detailViewController.canScrollUp { (canScroll) in
|
|
|
|
|
if (canScroll) {
|
|
|
|
|
NSCursor.setHiddenUntilMouseMoves(true)
|
|
|
|
|
detailViewController.scrollPageUp(sender)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-20 21:59:31 +01:00
|
|
|
|
@IBAction func openArticleInBrowser(_ sender: Any?) {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
if let link = currentLink {
|
2020-05-21 02:59:05 +02:00
|
|
|
|
Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-12-20 21:59:31 +01:00
|
|
|
|
|
|
|
|
|
@IBAction func openInBrowser(_ sender: Any?) {
|
2020-09-24 01:30:34 +02:00
|
|
|
|
if AppDefaults.shared.openInBrowserInBackground {
|
|
|
|
|
window?.makeKeyAndOrderFront(self)
|
|
|
|
|
}
|
2017-12-20 21:59:31 +01:00
|
|
|
|
openArticleInBrowser(sender)
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-29 15:56:20 +02:00
|
|
|
|
@objc func openInAppBrowser(_ sender: Any?) {
|
2020-05-29 12:14:32 +02:00
|
|
|
|
// There is no In-App Browser for mac - so we use safari
|
|
|
|
|
openArticleInBrowser(sender)
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-22 07:28:21 +02:00
|
|
|
|
@IBAction func openInBrowserUsingOppositeOfSettings(_ sender: Any?) {
|
2020-09-24 01:30:34 +02:00
|
|
|
|
if !AppDefaults.shared.openInBrowserInBackground {
|
|
|
|
|
window?.makeKeyAndOrderFront(self)
|
|
|
|
|
}
|
2020-05-21 06:51:41 +02:00
|
|
|
|
if let link = currentLink {
|
2020-07-02 05:17:38 +02:00
|
|
|
|
Browser.open(link, inBackground: !AppDefaults.shared.openInBrowserInBackground)
|
2020-05-21 06:51:41 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-20 21:59:31 +01:00
|
|
|
|
@IBAction func nextUnread(_ sender: Any?) {
|
2019-02-18 22:10:16 +01:00
|
|
|
|
guard let timelineViewController = currentTimelineViewController, let sidebarViewController = sidebarViewController else {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
2018-09-05 06:18:59 +02:00
|
|
|
|
|
|
|
|
|
NSCursor.setHiddenUntilMouseMoves(true)
|
2019-02-18 22:10:16 +01:00
|
|
|
|
|
|
|
|
|
// TODO: handle search mode
|
2020-11-01 23:33:48 +01:00
|
|
|
|
if timelineViewController.canGoToNextUnread(wrappingToTop: false) {
|
|
|
|
|
goToNextUnreadInTimeline(wrappingToTop: false)
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2020-11-01 23:33:48 +01:00
|
|
|
|
else if sidebarViewController.canGoToNextUnread(wrappingToTop: true) {
|
|
|
|
|
sidebarViewController.goToNextUnread(wrappingToTop: true)
|
|
|
|
|
|
|
|
|
|
// If we ended up on the same timelineViewController, we may need to wrap
|
|
|
|
|
// around to the top of its contents.
|
|
|
|
|
if timelineViewController.canGoToNextUnread(wrappingToTop: true) {
|
|
|
|
|
goToNextUnreadInTimeline(wrappingToTop: true)
|
2017-12-24 07:18:42 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-20 21:59:31 +01:00
|
|
|
|
@IBAction func markAllAsRead(_ sender: Any?) {
|
2019-02-18 22:10:16 +01:00
|
|
|
|
currentTimelineViewController?.markAllAsRead()
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2017-11-06 06:27:34 +01:00
|
|
|
|
|
2018-09-13 22:00:33 +02:00
|
|
|
|
@IBAction func toggleRead(_ sender: Any?) {
|
2019-02-18 22:10:16 +01:00
|
|
|
|
currentTimelineViewController?.toggleReadStatusForSelectedArticles()
|
|
|
|
|
}
|
2017-11-06 06:27:34 +01:00
|
|
|
|
|
2019-02-18 22:10:16 +01:00
|
|
|
|
@IBAction func markRead(_ sender: Any?) {
|
|
|
|
|
currentTimelineViewController?.markSelectedArticlesAsRead(sender)
|
2017-11-06 06:27:34 +01:00
|
|
|
|
}
|
2017-12-20 21:59:31 +01:00
|
|
|
|
|
|
|
|
|
@IBAction func markUnread(_ sender: Any?) {
|
2019-02-18 22:10:16 +01:00
|
|
|
|
currentTimelineViewController?.markSelectedArticlesAsUnread(sender)
|
2017-12-20 21:59:31 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-17 07:35:04 +01:00
|
|
|
|
@IBAction func toggleStarred(_ sender: Any?) {
|
2019-02-18 22:10:16 +01:00
|
|
|
|
currentTimelineViewController?.toggleStarredStatusForSelectedArticles()
|
2018-02-17 07:35:04 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-09-19 01:15:55 +02:00
|
|
|
|
@IBAction func toggleArticleExtractor(_ sender: Any?) {
|
|
|
|
|
|
|
|
|
|
guard let currentLink = currentLink, let article = oneSelectedArticle else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-09-20 00:25:36 +02:00
|
|
|
|
|
|
|
|
|
defer {
|
|
|
|
|
makeToolbarValidate()
|
|
|
|
|
}
|
2019-09-19 01:15:55 +02:00
|
|
|
|
|
2019-10-04 14:55:42 +02:00
|
|
|
|
if articleExtractor?.state == .failedToParse {
|
|
|
|
|
startArticleExtractorForCurrentLink()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-20 00:25:36 +02:00
|
|
|
|
guard articleExtractor?.state != .processing else {
|
|
|
|
|
articleExtractor?.cancel()
|
|
|
|
|
articleExtractor = nil
|
|
|
|
|
isShowingExtractedArticle = false
|
|
|
|
|
detailViewController?.setState(DetailState.article(article), mode: timelineSourceMode)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard !isShowingExtractedArticle else {
|
|
|
|
|
isShowingExtractedArticle = false
|
|
|
|
|
detailViewController?.setState(DetailState.article(article), mode: timelineSourceMode)
|
2019-09-19 01:15:55 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let articleExtractor = articleExtractor, let extractedArticle = articleExtractor.article {
|
|
|
|
|
if currentLink == articleExtractor.articleLink {
|
2019-09-20 00:25:36 +02:00
|
|
|
|
isShowingExtractedArticle = true
|
2019-09-19 01:15:55 +02:00
|
|
|
|
let detailState = DetailState.extracted(article, extractedArticle)
|
|
|
|
|
detailViewController?.setState(detailState, mode: timelineSourceMode)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2019-09-21 22:03:42 +02:00
|
|
|
|
startArticleExtractorForCurrentLink()
|
2019-09-19 01:15:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-21 06:23:48 +01:00
|
|
|
|
@IBAction func markAllAsReadAndGoToNextUnread(_ sender: Any?) {
|
2021-04-15 21:29:49 +02:00
|
|
|
|
currentTimelineViewController?.markAllAsRead() {
|
|
|
|
|
self.nextUnread(sender)
|
|
|
|
|
}
|
2017-12-21 06:23:48 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-12-20 21:59:31 +01:00
|
|
|
|
@IBAction func markUnreadAndGoToNextUnread(_ sender: Any?) {
|
|
|
|
|
markUnread(sender)
|
|
|
|
|
nextUnread(sender)
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-21 05:51:17 +01:00
|
|
|
|
@IBAction func markReadAndGoToNextUnread(_ sender: Any?) {
|
|
|
|
|
markUnread(sender)
|
|
|
|
|
nextUnread(sender)
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-14 20:36:18 +01:00
|
|
|
|
@IBAction func toggleTheSidebar(_ sender: Any?) {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
splitViewController!.toggleSidebar(sender)
|
2020-03-14 20:36:18 +01:00
|
|
|
|
guard let splitViewItem = sidebarSplitViewItem else { return }
|
|
|
|
|
if splitViewItem.isCollapsed {
|
|
|
|
|
currentTimelineViewController?.focus()
|
|
|
|
|
} else {
|
|
|
|
|
sidebarViewController?.focus()
|
|
|
|
|
}
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2020-09-23 02:42:14 +02:00
|
|
|
|
|
|
|
|
|
@IBAction func markOlderArticlesAsRead(_ sender: Any?) {
|
|
|
|
|
currentTimelineViewController?.markOlderArticlesRead()
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-09 00:15:17 +01:00
|
|
|
|
@IBAction func markAboveArticlesAsRead(_ sender: Any?) {
|
|
|
|
|
currentTimelineViewController?.markAboveArticlesRead()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@IBAction func markBelowArticlesAsRead(_ sender: Any?) {
|
|
|
|
|
currentTimelineViewController?.markBelowArticlesRead()
|
2017-11-20 01:28:26 +01:00
|
|
|
|
}
|
2017-12-21 06:23:48 +01:00
|
|
|
|
|
|
|
|
|
@IBAction func navigateToTimeline(_ sender: Any?) {
|
2019-02-18 22:10:16 +01:00
|
|
|
|
currentTimelineViewController?.focus()
|
2017-12-21 06:23:48 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@IBAction func navigateToSidebar(_ sender: Any?) {
|
|
|
|
|
sidebarViewController?.focus()
|
|
|
|
|
}
|
2017-12-21 23:43:29 +01:00
|
|
|
|
|
2019-03-01 23:56:40 +01:00
|
|
|
|
@IBAction func navigateToDetail(_ sender: Any?) {
|
|
|
|
|
detailViewController?.focus()
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-21 23:43:29 +01:00
|
|
|
|
@IBAction func goToPreviousSubscription(_ sender: Any?) {
|
|
|
|
|
sidebarViewController?.outlineView.selectPreviousRow(sender)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@IBAction func goToNextSubscription(_ sender: Any?) {
|
|
|
|
|
sidebarViewController?.outlineView.selectNextRow(sender)
|
|
|
|
|
}
|
2017-12-26 21:50:34 +01:00
|
|
|
|
|
2018-01-27 21:39:07 +01:00
|
|
|
|
@IBAction func gotoToday(_ sender: Any?) {
|
|
|
|
|
sidebarViewController?.gotoToday(sender)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@IBAction func gotoAllUnread(_ sender: Any?) {
|
|
|
|
|
sidebarViewController?.gotoAllUnread(sender)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@IBAction func gotoStarred(_ sender: Any?) {
|
|
|
|
|
sidebarViewController?.gotoStarred(sender)
|
|
|
|
|
}
|
2018-01-28 00:04:45 +01:00
|
|
|
|
|
2017-12-26 21:50:34 +01:00
|
|
|
|
@IBAction func toolbarShowShareMenu(_ sender: Any?) {
|
|
|
|
|
guard let selectedArticles = selectedArticles, !selectedArticles.isEmpty else {
|
|
|
|
|
assertionFailure("Expected toolbarShowShareMenu to be called only when there are selected articles.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
guard let shareToolbarItem = shareToolbarItem else {
|
|
|
|
|
assertionFailure("Expected toolbarShowShareMenu to be called only by the Share item in the toolbar.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
guard let view = shareToolbarItem.view else {
|
|
|
|
|
// TODO: handle menu form representation
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-26 05:20:59 +02:00
|
|
|
|
let sortedArticles = selectedArticles.sortedByDate(.orderedAscending)
|
|
|
|
|
let items = sortedArticles.map { ArticlePasteboardWriter(article: $0) }
|
2017-12-26 21:50:34 +01:00
|
|
|
|
let sharingServicePicker = NSSharingServicePicker(items: items)
|
2018-02-18 05:33:30 +01:00
|
|
|
|
sharingServicePicker.delegate = sharingServicePickerDelegate
|
2017-12-26 21:50:34 +01:00
|
|
|
|
sharingServicePicker.show(relativeTo: view.bounds, of: view, preferredEdge: .minY)
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-25 08:03:32 +01:00
|
|
|
|
@IBAction func moveFocusToSearchField(_ sender: Any?) {
|
|
|
|
|
guard let searchField = currentSearchField else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
window?.makeFirstResponder(searchField)
|
|
|
|
|
}
|
2019-11-22 17:55:54 +01:00
|
|
|
|
|
2020-03-16 00:02:55 +01:00
|
|
|
|
@IBAction func cleanUp(_ sender: Any?) {
|
|
|
|
|
timelineContainerViewController?.cleanUp()
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-22 18:47:03 +01:00
|
|
|
|
@IBAction func toggleReadFeedsFilter(_ sender: Any?) {
|
2019-11-22 17:55:54 +01:00
|
|
|
|
sidebarViewController?.toggleReadFilter()
|
|
|
|
|
}
|
2019-11-22 18:47:03 +01:00
|
|
|
|
|
|
|
|
|
@IBAction func toggleReadArticlesFilter(_ sender: Any?) {
|
|
|
|
|
timelineContainerViewController?.toggleReadFilter()
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-26 21:50:34 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-02-27 21:15:41 +01:00
|
|
|
|
// MARK: NSWindowDelegate
|
|
|
|
|
|
|
|
|
|
extension MainWindowController: NSWindowDelegate {
|
2020-03-03 02:46:31 +01:00
|
|
|
|
|
2020-03-03 19:54:37 +01:00
|
|
|
|
func window(_ window: NSWindow, willEncodeRestorableState coder: NSCoder) {
|
2020-03-06 00:42:54 +01:00
|
|
|
|
coder.encode(savableState(), forKey: UserInfoKey.windowState)
|
2020-03-03 02:46:31 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-03 19:54:37 +01:00
|
|
|
|
func window(_ window: NSWindow, didDecodeRestorableState coder: NSCoder) {
|
2020-03-06 00:42:54 +01:00
|
|
|
|
guard let state = try? coder.decodeTopLevelObject(forKey: UserInfoKey.windowState) as? [AnyHashable : Any] else { return }
|
|
|
|
|
restoreState(from: state)
|
2020-03-03 02:46:31 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-02-27 21:15:41 +01:00
|
|
|
|
func windowWillClose(_ notification: Notification) {
|
|
|
|
|
detailViewController?.stopMediaPlayback()
|
2020-03-03 03:06:55 +01:00
|
|
|
|
appDelegate.removeMainWindow(self)
|
2020-02-27 21:15:41 +01:00
|
|
|
|
}
|
2020-03-03 02:46:31 +01:00
|
|
|
|
|
2020-02-27 21:15:41 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-18 03:46:28 +01:00
|
|
|
|
// MARK: - SidebarDelegate
|
|
|
|
|
|
|
|
|
|
extension MainWindowController: SidebarDelegate {
|
|
|
|
|
|
2019-02-18 06:43:51 +01:00
|
|
|
|
func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?) {
|
2019-07-29 01:00:43 +02:00
|
|
|
|
// Don’t update the timeline if it already has those objects.
|
|
|
|
|
let representedObjectsAreTheSame = timelineContainerViewController?.regularTimelineViewControllerHasRepresentedObjects(selectedObjects) ?? false
|
|
|
|
|
if !representedObjectsAreTheSame {
|
|
|
|
|
timelineContainerViewController?.setRepresentedObjects(selectedObjects, mode: .regular)
|
|
|
|
|
forceSearchToEnd()
|
|
|
|
|
}
|
2019-02-18 03:46:28 +01:00
|
|
|
|
updateWindowTitle()
|
|
|
|
|
NotificationCenter.default.post(name: .InspectableObjectsDidChange, object: nil)
|
|
|
|
|
}
|
2019-07-28 07:53:27 +02:00
|
|
|
|
|
|
|
|
|
func unreadCount(for representedObject: AnyObject) -> Int {
|
|
|
|
|
guard let timelineViewController = regularTimelineViewController else {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
guard timelineViewController.representsThisObjectOnly(representedObject) else {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
return timelineViewController.unreadCount
|
|
|
|
|
}
|
2020-03-03 02:46:31 +01:00
|
|
|
|
|
2020-03-03 19:54:37 +01:00
|
|
|
|
func sidebarInvalidatedRestorationState(_: SidebarViewController) {
|
|
|
|
|
invalidateRestorableState()
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-18 03:46:28 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-18 06:43:51 +01:00
|
|
|
|
// MARK: - TimelineContainerViewControllerDelegate
|
|
|
|
|
|
|
|
|
|
extension MainWindowController: TimelineContainerViewControllerDelegate {
|
|
|
|
|
|
|
|
|
|
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) {
|
2019-10-03 22:49:27 +02:00
|
|
|
|
activityManager.invalidateReading()
|
|
|
|
|
|
2019-09-20 00:25:36 +02:00
|
|
|
|
articleExtractor?.cancel()
|
|
|
|
|
articleExtractor = nil
|
|
|
|
|
isShowingExtractedArticle = false
|
|
|
|
|
makeToolbarValidate()
|
|
|
|
|
|
2019-02-18 06:43:51 +01:00
|
|
|
|
let detailState: DetailState
|
|
|
|
|
if let articles = articles {
|
2019-09-21 22:03:42 +02:00
|
|
|
|
if articles.count == 1 {
|
2019-11-15 13:19:14 +01:00
|
|
|
|
activityManager.reading(feed: nil, article: articles.first)
|
2019-11-15 03:11:41 +01:00
|
|
|
|
if articles.first?.webFeed?.isArticleExtractorAlwaysOn ?? false {
|
2019-09-21 22:03:42 +02:00
|
|
|
|
detailState = .loading
|
|
|
|
|
startArticleExtractorForCurrentLink()
|
|
|
|
|
} else {
|
|
|
|
|
detailState = .article(articles.first!)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
detailState = .multipleSelection
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2019-02-18 06:43:51 +01:00
|
|
|
|
detailState = .noSelection
|
|
|
|
|
}
|
2019-09-19 01:15:55 +02:00
|
|
|
|
|
2019-02-18 06:43:51 +01:00
|
|
|
|
detailViewController?.setState(detailState, mode: mode)
|
|
|
|
|
}
|
2020-03-01 00:50:13 +01:00
|
|
|
|
|
|
|
|
|
func timelineRequestedWebFeedSelection(_: TimelineContainerViewController, webFeed: WebFeed) {
|
2020-03-01 01:24:14 +01:00
|
|
|
|
sidebarViewController?.selectFeed(webFeed)
|
2020-03-01 00:50:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-03 19:54:37 +01:00
|
|
|
|
func timelineInvalidatedRestorationState(_: TimelineContainerViewController) {
|
|
|
|
|
invalidateRestorableState()
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-18 06:43:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-19 04:02:40 +01:00
|
|
|
|
// MARK: - NSSearchFieldDelegate
|
|
|
|
|
|
|
|
|
|
extension MainWindowController: NSSearchFieldDelegate {
|
|
|
|
|
|
|
|
|
|
func searchFieldDidStartSearching(_ sender: NSSearchField) {
|
2019-02-25 07:39:57 +01:00
|
|
|
|
startSearchingIfNeeded()
|
2019-02-19 04:02:40 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func searchFieldDidEndSearching(_ sender: NSSearchField) {
|
2019-02-25 07:39:57 +01:00
|
|
|
|
stopSearchingIfNeeded()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@IBAction func runSearch(_ sender: NSSearchField) {
|
|
|
|
|
if sender.stringValue == "" {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
startSearchingIfNeeded()
|
|
|
|
|
handleSearchFieldTextChange(sender)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func handleSearchFieldTextChange(_ searchField: NSSearchField) {
|
|
|
|
|
let s = searchField.stringValue
|
|
|
|
|
if s == searchString {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
searchString = s
|
|
|
|
|
updateSmartFeed()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateSmartFeed() {
|
|
|
|
|
guard timelineSourceMode == .search, let searchString = searchString else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if searchString == lastSentSearchString {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
lastSentSearchString = searchString
|
|
|
|
|
let smartFeed = SmartFeed(delegate: SearchFeedDelegate(searchString: searchString))
|
|
|
|
|
timelineContainerViewController?.setRepresentedObjects([smartFeed], mode: .search)
|
|
|
|
|
searchSmartFeed = smartFeed
|
2020-11-19 23:21:51 +01:00
|
|
|
|
updateWindowTitle()
|
2019-02-25 07:39:57 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func forceSearchToEnd() {
|
|
|
|
|
timelineSourceMode = .regular
|
|
|
|
|
searchString = nil
|
|
|
|
|
lastSentSearchString = nil
|
|
|
|
|
if let searchField = currentSearchField {
|
|
|
|
|
searchField.stringValue = ""
|
|
|
|
|
}
|
2020-11-19 23:21:51 +01:00
|
|
|
|
updateWindowTitle()
|
2019-02-25 07:39:57 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func startSearchingIfNeeded() {
|
|
|
|
|
timelineSourceMode = .search
|
2020-11-19 23:21:51 +01:00
|
|
|
|
updateWindowTitle()
|
2019-02-25 07:39:57 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func stopSearchingIfNeeded() {
|
|
|
|
|
searchString = nil
|
|
|
|
|
lastSentSearchString = nil
|
|
|
|
|
timelineSourceMode = .regular
|
|
|
|
|
timelineContainerViewController?.setRepresentedObjects(nil, mode: .search)
|
2020-11-19 23:21:51 +01:00
|
|
|
|
updateWindowTitle()
|
2019-02-19 04:02:40 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-19 01:15:55 +02:00
|
|
|
|
// MARK: - ArticleExtractorDelegate
|
|
|
|
|
|
|
|
|
|
extension MainWindowController: ArticleExtractorDelegate {
|
|
|
|
|
|
|
|
|
|
func articleExtractionDidFail(with: Error) {
|
|
|
|
|
makeToolbarValidate()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func articleExtractionDidComplete(extractedArticle: ExtractedArticle) {
|
2019-09-20 00:25:36 +02:00
|
|
|
|
if let article = oneSelectedArticle, articleExtractor?.state != .cancelled {
|
|
|
|
|
isShowingExtractedArticle = true
|
2019-09-19 01:15:55 +02:00
|
|
|
|
let detailState = DetailState.extracted(article, extractedArticle)
|
|
|
|
|
detailViewController?.setState(detailState, mode: timelineSourceMode)
|
2019-09-20 00:25:36 +02:00
|
|
|
|
makeToolbarValidate()
|
2019-09-19 01:15:55 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-08 09:11:52 +01:00
|
|
|
|
// MARK: - Scripting Access
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
the ScriptingMainWindowController protocol exposes a narrow set of accessors with
|
|
|
|
|
internal visibility which are very similar to some private vars.
|
|
|
|
|
|
|
|
|
|
These would be unnecessary if the similar accessors were marked internal rather than private,
|
|
|
|
|
but for now, we'll keep the stratification of visibility
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
extension MainWindowController : ScriptingMainWindowController {
|
|
|
|
|
|
|
|
|
|
internal var scriptingCurrentArticle: Article? {
|
|
|
|
|
return self.oneSelectedArticle
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal var scriptingSelectedArticles: [Article] {
|
|
|
|
|
return self.selectedArticles ?? []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-25 07:39:57 +01:00
|
|
|
|
// MARK: - NSToolbarDelegate
|
|
|
|
|
|
|
|
|
|
extension NSToolbarItem.Identifier {
|
2020-08-12 04:03:19 +02:00
|
|
|
|
static let sidebarToggle = NSToolbarItem.Identifier("sidebarToggle")
|
2020-08-09 22:36:32 +02:00
|
|
|
|
static let newFeed = NSToolbarItem.Identifier("newFeed")
|
|
|
|
|
static let newFolder = NSToolbarItem.Identifier("newFolder")
|
|
|
|
|
static let refresh = NSToolbarItem.Identifier("refresh")
|
2020-08-09 18:46:53 +02:00
|
|
|
|
static let newSidebarItemMenu = NSToolbarItem.Identifier("newSidebarItemMenu")
|
2020-08-09 22:36:32 +02:00
|
|
|
|
static let timelineTrackingSeparator = NSToolbarItem.Identifier("timelineTrackingSeparator")
|
2020-08-09 17:47:56 +02:00
|
|
|
|
static let search = NSToolbarItem.Identifier("search")
|
2020-08-09 22:36:32 +02:00
|
|
|
|
static let markAllAsRead = NSToolbarItem.Identifier("markAllAsRead")
|
2020-11-19 19:10:42 +01:00
|
|
|
|
static let toggleReadArticlesFilter = NSToolbarItem.Identifier("toggleReadArticlesFilter")
|
2020-08-09 22:36:32 +02:00
|
|
|
|
static let nextUnread = NSToolbarItem.Identifier("nextUnread")
|
|
|
|
|
static let markRead = NSToolbarItem.Identifier("markRead")
|
|
|
|
|
static let markStar = NSToolbarItem.Identifier("markStar")
|
|
|
|
|
static let readerView = NSToolbarItem.Identifier("readerView")
|
|
|
|
|
static let openInBrowser = NSToolbarItem.Identifier("openInBrowser")
|
|
|
|
|
static let share = NSToolbarItem.Identifier("share")
|
2020-08-10 10:58:19 +02:00
|
|
|
|
static let cleanUp = NSToolbarItem.Identifier("cleanUp")
|
2019-02-25 07:39:57 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extension MainWindowController: NSToolbarDelegate {
|
|
|
|
|
|
2020-08-09 13:02:48 +02:00
|
|
|
|
func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
|
2020-08-10 18:43:18 +02:00
|
|
|
|
if #available(macOS 11.0, *) {
|
2020-08-10 10:58:19 +02:00
|
|
|
|
|
|
|
|
|
switch itemIdentifier {
|
|
|
|
|
|
2020-08-12 04:03:19 +02:00
|
|
|
|
case .sidebarToggle:
|
|
|
|
|
let title = NSLocalizedString("Toggle Sidebar", comment: "Toggle Sidebar")
|
|
|
|
|
return buildToolbarButton(.toggleSidebar, title, AppAssets.sidebarToggleImage, "toggleTheSidebar:")
|
|
|
|
|
|
2020-08-10 10:58:19 +02:00
|
|
|
|
case .refresh:
|
|
|
|
|
let title = NSLocalizedString("Refresh", comment: "Refresh")
|
2020-08-10 16:53:06 +02:00
|
|
|
|
return buildToolbarButton(.refresh, title, AppAssets.refreshImage, "refreshAll:")
|
2020-08-10 10:58:19 +02:00
|
|
|
|
|
|
|
|
|
case .newSidebarItemMenu:
|
|
|
|
|
let toolbarItem = NSMenuToolbarItem(itemIdentifier: .newSidebarItemMenu)
|
|
|
|
|
toolbarItem.image = AppAssets.addNewSidebarItemImage
|
|
|
|
|
let description = NSLocalizedString("Add Item", comment: "Add Item")
|
|
|
|
|
toolbarItem.toolTip = description
|
|
|
|
|
toolbarItem.label = description
|
|
|
|
|
toolbarItem.menu = buildNewSidebarItemMenu()
|
|
|
|
|
return toolbarItem
|
|
|
|
|
|
2020-08-10 16:28:45 +02:00
|
|
|
|
case .markAllAsRead:
|
|
|
|
|
let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")
|
2020-08-10 16:53:06 +02:00
|
|
|
|
return buildToolbarButton(.markAllAsRead, title, AppAssets.markAllAsReadImage, "markAllAsRead:")
|
2020-08-10 16:28:45 +02:00
|
|
|
|
|
2020-11-19 19:10:42 +01:00
|
|
|
|
case .toggleReadArticlesFilter:
|
|
|
|
|
let title = NSLocalizedString("Read Articles Filter", comment: "Read Articles Filter")
|
|
|
|
|
return buildToolbarButton(.toggleReadArticlesFilter, title, AppAssets.filterInactive, "toggleReadArticlesFilter:")
|
|
|
|
|
|
2020-08-10 10:58:19 +02:00
|
|
|
|
case .timelineTrackingSeparator:
|
|
|
|
|
return NSTrackingSeparatorToolbarItem(identifier: .timelineTrackingSeparator, splitView: splitViewController!.splitView, dividerIndex: 1)
|
|
|
|
|
|
2020-08-10 16:45:48 +02:00
|
|
|
|
case .markRead:
|
|
|
|
|
let title = NSLocalizedString("Mark Read", comment: "Mark Read")
|
2020-08-10 16:53:06 +02:00
|
|
|
|
return buildToolbarButton(.markRead, title, AppAssets.readClosedImage, "toggleRead:")
|
2020-08-10 16:45:48 +02:00
|
|
|
|
|
|
|
|
|
case .markStar:
|
|
|
|
|
let title = NSLocalizedString("Star", comment: "Star")
|
2020-08-10 16:53:06 +02:00
|
|
|
|
return buildToolbarButton(.markStar, title, AppAssets.starOpenImage, "toggleStarred:")
|
2020-08-10 16:45:48 +02:00
|
|
|
|
|
2020-11-19 18:31:58 +01:00
|
|
|
|
case .nextUnread:
|
|
|
|
|
let title = NSLocalizedString("Next Unread", comment: "Next Unread")
|
|
|
|
|
return buildToolbarButton(.nextUnread, title, AppAssets.nextUnreadImage, "nextUnread:")
|
|
|
|
|
|
2020-08-11 00:34:12 +02:00
|
|
|
|
case .readerView:
|
|
|
|
|
let toolbarItem = RSToolbarItem(itemIdentifier: .readerView)
|
|
|
|
|
toolbarItem.autovalidates = true
|
|
|
|
|
let description = NSLocalizedString("Reader View", comment: "Reader View")
|
|
|
|
|
toolbarItem.toolTip = description
|
|
|
|
|
toolbarItem.label = description
|
|
|
|
|
let button = ArticleExtractorButton()
|
|
|
|
|
button.action = #selector(toggleArticleExtractor(_:))
|
|
|
|
|
toolbarItem.view = button
|
|
|
|
|
return toolbarItem
|
|
|
|
|
|
2020-08-10 16:45:48 +02:00
|
|
|
|
case .share:
|
|
|
|
|
let title = NSLocalizedString("Share", comment: "Share")
|
2020-08-10 16:53:06 +02:00
|
|
|
|
return buildToolbarButton(.share, title, AppAssets.shareImage, "toolbarShowShareMenu:")
|
2020-08-10 16:45:48 +02:00
|
|
|
|
|
2020-11-19 18:31:58 +01:00
|
|
|
|
case .openInBrowser:
|
|
|
|
|
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
|
|
|
|
|
return buildToolbarButton(.openInBrowser, title, AppAssets.openInBrowserImage, "openArticleInBrowser:")
|
|
|
|
|
|
|
|
|
|
case .search:
|
|
|
|
|
let toolbarItem = NSSearchToolbarItem(itemIdentifier: .search)
|
|
|
|
|
let description = NSLocalizedString("Search", comment: "Search")
|
|
|
|
|
toolbarItem.toolTip = description
|
|
|
|
|
toolbarItem.label = description
|
|
|
|
|
return toolbarItem
|
|
|
|
|
|
2020-08-10 16:45:48 +02:00
|
|
|
|
case .cleanUp:
|
|
|
|
|
let title = NSLocalizedString("Clean Up", comment: "Clean Up")
|
2020-08-10 16:53:06 +02:00
|
|
|
|
return buildToolbarButton(.cleanUp, title, AppAssets.cleanUpImage, "cleanUp:")
|
2020-08-10 16:45:48 +02:00
|
|
|
|
|
2020-08-10 10:58:19 +02:00
|
|
|
|
default:
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-09 13:02:48 +02:00
|
|
|
|
}
|
2020-08-09 18:46:53 +02:00
|
|
|
|
|
2020-08-09 13:02:48 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-10 10:58:19 +02:00
|
|
|
|
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
2020-08-10 18:43:18 +02:00
|
|
|
|
if #available(macOS 11.0, *) {
|
2020-08-09 22:36:32 +02:00
|
|
|
|
return [
|
2020-08-12 04:03:19 +02:00
|
|
|
|
.sidebarToggle,
|
2020-08-09 22:36:32 +02:00
|
|
|
|
.refresh,
|
|
|
|
|
.newSidebarItemMenu,
|
|
|
|
|
.sidebarTrackingSeparator,
|
2020-08-10 16:28:45 +02:00
|
|
|
|
.markAllAsRead,
|
2020-11-19 19:10:42 +01:00
|
|
|
|
.toggleReadArticlesFilter,
|
2020-08-09 22:36:32 +02:00
|
|
|
|
.timelineTrackingSeparator,
|
|
|
|
|
.flexibleSpace,
|
2020-08-10 16:45:48 +02:00
|
|
|
|
.nextUnread,
|
|
|
|
|
.markRead,
|
|
|
|
|
.markStar,
|
2020-08-11 00:34:12 +02:00
|
|
|
|
.readerView,
|
2020-08-10 16:45:48 +02:00
|
|
|
|
.openInBrowser,
|
|
|
|
|
.share,
|
2020-11-19 18:29:56 +01:00
|
|
|
|
.search,
|
2020-08-10 16:45:48 +02:00
|
|
|
|
.cleanUp
|
2020-08-10 10:58:19 +02:00
|
|
|
|
]
|
|
|
|
|
} else {
|
|
|
|
|
return [
|
|
|
|
|
.newFeed,
|
|
|
|
|
.newFolder,
|
|
|
|
|
.refresh,
|
|
|
|
|
.flexibleSpace,
|
|
|
|
|
.markAllAsRead,
|
|
|
|
|
.search,
|
|
|
|
|
.flexibleSpace,
|
2020-08-09 22:36:32 +02:00
|
|
|
|
.nextUnread,
|
|
|
|
|
.markStar,
|
2020-08-10 10:58:19 +02:00
|
|
|
|
.markRead,
|
|
|
|
|
.readerView,
|
2020-08-09 22:36:32 +02:00
|
|
|
|
.openInBrowser,
|
2020-08-10 10:58:19 +02:00
|
|
|
|
.share,
|
|
|
|
|
.cleanUp
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
2020-08-10 18:43:18 +02:00
|
|
|
|
if #available(macOS 11.0, *) {
|
2020-08-10 10:58:19 +02:00
|
|
|
|
return [
|
2020-08-12 04:03:19 +02:00
|
|
|
|
.flexibleSpace,
|
2020-08-10 10:58:19 +02:00
|
|
|
|
.refresh,
|
|
|
|
|
.newSidebarItemMenu,
|
|
|
|
|
.sidebarTrackingSeparator,
|
2020-08-10 16:28:45 +02:00
|
|
|
|
.markAllAsRead,
|
2020-11-19 19:10:42 +01:00
|
|
|
|
.toggleReadArticlesFilter,
|
2020-08-10 10:58:19 +02:00
|
|
|
|
.timelineTrackingSeparator,
|
2020-08-10 16:45:48 +02:00
|
|
|
|
.markRead,
|
|
|
|
|
.markStar,
|
2020-11-19 18:29:56 +01:00
|
|
|
|
.nextUnread,
|
2020-08-11 00:34:12 +02:00
|
|
|
|
.readerView,
|
2020-11-19 18:29:56 +01:00
|
|
|
|
.share,
|
2020-08-10 16:45:48 +02:00
|
|
|
|
.openInBrowser,
|
2020-11-19 18:29:56 +01:00
|
|
|
|
.flexibleSpace,
|
|
|
|
|
.search
|
2020-08-09 22:36:32 +02:00
|
|
|
|
]
|
|
|
|
|
} else {
|
|
|
|
|
return [
|
|
|
|
|
.newFeed,
|
|
|
|
|
.newFolder,
|
|
|
|
|
.refresh,
|
|
|
|
|
.flexibleSpace,
|
|
|
|
|
.markAllAsRead,
|
|
|
|
|
.search,
|
|
|
|
|
.flexibleSpace,
|
|
|
|
|
.nextUnread,
|
|
|
|
|
.markStar,
|
|
|
|
|
.markRead,
|
|
|
|
|
.readerView,
|
|
|
|
|
.openInBrowser,
|
|
|
|
|
.share
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-25 07:39:57 +01:00
|
|
|
|
func toolbarWillAddItem(_ notification: Notification) {
|
|
|
|
|
guard let item = notification.userInfo?["item"] as? NSToolbarItem else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-09 17:47:56 +02:00
|
|
|
|
if item.itemIdentifier == .share, let button = item.view as? NSButton {
|
2019-02-25 07:39:57 +01:00
|
|
|
|
// The share button should send its action on mouse down, not mouse up.
|
|
|
|
|
button.sendAction(on: .leftMouseDown)
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-10 18:43:18 +02:00
|
|
|
|
if #available(macOS 11.0, *) {
|
2020-08-09 23:40:44 +02:00
|
|
|
|
if item.itemIdentifier == .search, let searchItem = item as? NSSearchToolbarItem {
|
|
|
|
|
searchItem.searchField.delegate = self
|
|
|
|
|
searchItem.searchField.target = self
|
|
|
|
|
searchItem.searchField.action = #selector(runSearch(_:))
|
|
|
|
|
currentSearchField = searchItem.searchField
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if item.itemIdentifier == .search, let searchField = item.view as? NSSearchField {
|
|
|
|
|
searchField.delegate = self
|
|
|
|
|
searchField.target = self
|
|
|
|
|
searchField.action = #selector(runSearch(_:))
|
|
|
|
|
currentSearchField = searchField
|
|
|
|
|
}
|
2019-02-25 07:39:57 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func toolbarDidRemoveItem(_ notification: Notification) {
|
|
|
|
|
guard let item = notification.userInfo?["item"] as? NSToolbarItem else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-10 18:43:18 +02:00
|
|
|
|
if #available(macOS 11.0, *) {
|
2020-08-09 23:40:44 +02:00
|
|
|
|
if item.itemIdentifier == .search, let searchItem = item as? NSSearchToolbarItem {
|
|
|
|
|
searchItem.searchField.delegate = nil
|
|
|
|
|
searchItem.searchField.target = nil
|
|
|
|
|
searchItem.searchField.action = nil
|
|
|
|
|
currentSearchField = nil
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if item.itemIdentifier == .search, let searchField = item.view as? NSSearchField {
|
|
|
|
|
searchField.delegate = nil
|
|
|
|
|
searchField.target = nil
|
|
|
|
|
searchField.action = nil
|
|
|
|
|
currentSearchField = nil
|
|
|
|
|
}
|
2019-02-25 07:39:57 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-08-09 23:40:44 +02:00
|
|
|
|
|
2019-02-25 07:39:57 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-06 05:03:35 +02:00
|
|
|
|
// MARK: - Private
|
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
|
private extension MainWindowController {
|
2019-02-15 17:16:38 +01:00
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
|
var splitViewController: NSSplitViewController? {
|
2018-02-08 06:42:33 +01:00
|
|
|
|
guard let viewController = contentViewController else {
|
|
|
|
|
return nil
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2018-12-09 21:32:33 +01:00
|
|
|
|
return viewController.children.first as? NSSplitViewController
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-18 22:10:16 +01:00
|
|
|
|
var currentTimelineViewController: TimelineViewController? {
|
|
|
|
|
return timelineContainerViewController?.currentTimelineViewController
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2018-02-08 06:42:33 +01:00
|
|
|
|
|
2019-07-28 07:53:27 +02:00
|
|
|
|
var regularTimelineViewController: TimelineViewController? {
|
|
|
|
|
return timelineContainerViewController?.regularTimelineViewController
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-08 06:42:33 +01:00
|
|
|
|
var sidebarSplitViewItem: NSSplitViewItem? {
|
|
|
|
|
return splitViewController?.splitViewItems[0]
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
|
var detailSplitViewItem: NSSplitViewItem? {
|
2018-02-08 06:42:33 +01:00
|
|
|
|
return splitViewController?.splitViewItems[2]
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var selectedArticles: [Article]? {
|
2019-02-18 22:10:16 +01:00
|
|
|
|
return currentTimelineViewController?.selectedArticles
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2018-02-08 06:42:33 +01:00
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
|
var oneSelectedArticle: Article? {
|
2018-02-08 06:42:33 +01:00
|
|
|
|
if let articles = selectedArticles {
|
|
|
|
|
return articles.count == 1 ? articles[0] : nil
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2018-02-08 06:42:33 +01:00
|
|
|
|
return nil
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2018-02-08 06:42:33 +01:00
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
|
var currentLink: String? {
|
2018-02-08 06:42:33 +01:00
|
|
|
|
return oneSelectedArticle?.preferredLink
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2018-02-08 06:42:33 +01:00
|
|
|
|
|
2020-03-06 00:42:54 +01:00
|
|
|
|
// MARK: - State Restoration
|
|
|
|
|
|
|
|
|
|
func savableState() -> [AnyHashable : Any] {
|
|
|
|
|
var state = [AnyHashable : Any]()
|
2020-05-09 21:14:46 +02:00
|
|
|
|
state[UserInfoKey.windowFullScreenState] = window?.styleMask.contains(.fullScreen) ?? false
|
2020-03-06 00:42:54 +01:00
|
|
|
|
saveSplitViewState(to: &state)
|
|
|
|
|
sidebarViewController?.saveState(to: &state)
|
|
|
|
|
timelineContainerViewController?.saveState(to: &state)
|
|
|
|
|
return state
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func restoreState(from state: [AnyHashable : Any]) {
|
2020-05-09 21:14:46 +02:00
|
|
|
|
if let fullScreen = state[UserInfoKey.windowFullScreenState] as? Bool, fullScreen {
|
|
|
|
|
window?.toggleFullScreen(self)
|
|
|
|
|
}
|
2020-03-06 00:42:54 +01:00
|
|
|
|
restoreSplitViewState(from: state)
|
|
|
|
|
sidebarViewController?.restoreState(from: state)
|
|
|
|
|
timelineContainerViewController?.restoreState(from: state)
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-18 05:58:30 +01:00
|
|
|
|
// MARK: - Command Validation
|
|
|
|
|
|
2020-11-01 23:33:48 +01:00
|
|
|
|
func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
|
2019-02-18 22:10:16 +01:00
|
|
|
|
guard let timelineViewController = currentTimelineViewController, let sidebarViewController = sidebarViewController else {
|
2017-05-27 19:43:27 +02:00
|
|
|
|
return false
|
|
|
|
|
}
|
2019-02-18 22:10:16 +01:00
|
|
|
|
// TODO: handle search mode
|
2020-11-01 23:33:48 +01:00
|
|
|
|
return timelineViewController.canGoToNextUnread(wrappingToTop: wrapping) || sidebarViewController.canGoToNextUnread(wrappingToTop: wrapping)
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func canMarkAllAsRead() -> Bool {
|
|
|
|
|
|
2019-02-18 22:10:16 +01:00
|
|
|
|
return currentTimelineViewController?.canMarkAllAsRead() ?? false
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
2019-05-19 16:10:19 +02:00
|
|
|
|
|
2018-09-13 22:00:33 +02:00
|
|
|
|
func validateToggleRead(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
2017-11-06 06:27:34 +01:00
|
|
|
|
|
2019-02-18 22:10:16 +01:00
|
|
|
|
let validationStatus = currentTimelineViewController?.markReadCommandStatus() ?? .canDoNothing
|
2018-09-13 22:00:33 +02:00
|
|
|
|
let markingRead: Bool
|
|
|
|
|
let result: Bool
|
|
|
|
|
|
|
|
|
|
switch validationStatus {
|
|
|
|
|
case .canMark:
|
|
|
|
|
markingRead = true
|
|
|
|
|
result = true
|
|
|
|
|
case .canUnmark:
|
|
|
|
|
markingRead = false
|
|
|
|
|
result = true
|
|
|
|
|
case .canDoNothing:
|
|
|
|
|
markingRead = true
|
|
|
|
|
result = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let commandName = markingRead ? NSLocalizedString("Mark as Read", comment: "Command") : NSLocalizedString("Mark as Unread", comment: "Command")
|
|
|
|
|
|
|
|
|
|
if let toolbarItem = item as? NSToolbarItem {
|
|
|
|
|
toolbarItem.toolTip = commandName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let menuItem = item as? NSMenuItem {
|
|
|
|
|
menuItem.title = commandName
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-10 18:43:18 +02:00
|
|
|
|
if #available(macOS 11.0, *), let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
|
2020-08-09 13:02:48 +02:00
|
|
|
|
button.image = markingRead ? AppAssets.readClosedImage : AppAssets.readOpenImage
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-13 22:00:33 +02:00
|
|
|
|
return result
|
2017-11-06 06:27:34 +01:00
|
|
|
|
}
|
2017-12-25 21:21:44 +01:00
|
|
|
|
|
2019-09-19 01:15:55 +02:00
|
|
|
|
func validateToggleArticleExtractor(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
2020-07-02 05:17:38 +02:00
|
|
|
|
guard !AppDefaults.shared.isDeveloperBuild else {
|
2020-03-28 22:51:41 +01:00
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-11 00:34:12 +02:00
|
|
|
|
if #available(macOS 11.0, *) {
|
|
|
|
|
|
|
|
|
|
guard let toolbarItem = item as? NSToolbarItem, let toolbarButton = toolbarItem.view as? ArticleExtractorButton else {
|
|
|
|
|
if let menuItem = item as? NSMenuItem {
|
|
|
|
|
menuItem.state = isShowingExtractedArticle ? .on : .off
|
|
|
|
|
}
|
|
|
|
|
return currentLink != nil
|
2019-09-20 00:25:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-11 00:34:12 +02:00
|
|
|
|
guard let state = articleExtractor?.state else {
|
|
|
|
|
toolbarButton.buttonState = .off
|
|
|
|
|
return currentLink != nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch state {
|
|
|
|
|
case .processing:
|
|
|
|
|
toolbarButton.buttonState = .animated
|
|
|
|
|
case .failedToParse:
|
|
|
|
|
toolbarButton.buttonState = .error
|
|
|
|
|
case .ready, .cancelled, .complete:
|
|
|
|
|
toolbarButton.buttonState = isShowingExtractedArticle ? .on : .off
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
guard let toolbarItem = item as? NSToolbarItem, let toolbarButton = toolbarItem.view as? LegacyArticleExtractorButton else {
|
|
|
|
|
if let menuItem = item as? NSMenuItem {
|
|
|
|
|
menuItem.state = isShowingExtractedArticle ? .on : .off
|
|
|
|
|
}
|
|
|
|
|
return currentLink != nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toolbarButton.state = isShowingExtractedArticle ? .on : .off
|
|
|
|
|
|
|
|
|
|
guard let state = articleExtractor?.state else {
|
|
|
|
|
toolbarButton.isError = false
|
|
|
|
|
toolbarButton.isInProgress = false
|
|
|
|
|
toolbarButton.state = .off
|
|
|
|
|
return currentLink != nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch state {
|
|
|
|
|
case .processing:
|
|
|
|
|
toolbarButton.isError = false
|
|
|
|
|
toolbarButton.isInProgress = true
|
|
|
|
|
case .failedToParse:
|
|
|
|
|
toolbarButton.isError = true
|
|
|
|
|
toolbarButton.isInProgress = false
|
|
|
|
|
case .ready, .cancelled, .complete:
|
|
|
|
|
toolbarButton.isError = false
|
|
|
|
|
toolbarButton.isInProgress = false
|
|
|
|
|
}
|
|
|
|
|
|
2019-09-19 01:15:55 +02:00
|
|
|
|
}
|
2019-09-20 00:25:36 +02:00
|
|
|
|
|
|
|
|
|
return true
|
2019-09-19 01:15:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-09 00:15:17 +01:00
|
|
|
|
func canMarkAboveArticlesAsRead() -> Bool {
|
|
|
|
|
return currentTimelineViewController?.canMarkAboveArticlesAsRead() ?? false
|
2017-12-25 21:21:44 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-09 00:15:17 +01:00
|
|
|
|
func canMarkBelowArticlesAsRead() -> Bool {
|
|
|
|
|
return currentTimelineViewController?.canMarkBelowArticlesAsRead() ?? false
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-18 05:58:30 +01:00
|
|
|
|
func canShowShareMenu() -> Bool {
|
|
|
|
|
|
|
|
|
|
guard let selectedArticles = selectedArticles else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return !selectedArticles.isEmpty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateToggleStarred(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
|
|
|
|
|
2019-02-18 22:10:16 +01:00
|
|
|
|
let validationStatus = currentTimelineViewController?.markStarredCommandStatus() ?? .canDoNothing
|
2018-02-18 05:58:30 +01:00
|
|
|
|
let starring: Bool
|
|
|
|
|
let result: Bool
|
|
|
|
|
|
|
|
|
|
switch validationStatus {
|
|
|
|
|
case .canMark:
|
|
|
|
|
starring = true
|
|
|
|
|
result = true
|
|
|
|
|
case .canUnmark:
|
|
|
|
|
starring = false
|
|
|
|
|
result = true
|
|
|
|
|
case .canDoNothing:
|
|
|
|
|
starring = true
|
|
|
|
|
result = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let commandName = starring ? NSLocalizedString("Mark as Starred", comment: "Command") : NSLocalizedString("Mark as Unstarred", comment: "Command")
|
|
|
|
|
|
|
|
|
|
if let toolbarItem = item as? NSToolbarItem {
|
|
|
|
|
toolbarItem.toolTip = commandName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let menuItem = item as? NSMenuItem {
|
|
|
|
|
menuItem.title = commandName
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-10 18:43:18 +02:00
|
|
|
|
if #available(macOS 11.0, *), let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
|
2020-08-09 13:02:48 +02:00
|
|
|
|
button.image = starring ? AppAssets.starOpenImage : AppAssets.starClosedImage
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-18 05:58:30 +01:00
|
|
|
|
return result
|
|
|
|
|
}
|
2019-11-24 11:29:15 +01:00
|
|
|
|
|
2020-03-16 00:02:55 +01:00
|
|
|
|
func validateCleanUp(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
2020-08-18 22:04:30 +02:00
|
|
|
|
return timelineContainerViewController?.isCleanUpAvailable ?? false
|
2020-03-16 00:02:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-24 11:29:15 +01:00
|
|
|
|
func validateToggleReadFeeds(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
|
|
|
|
guard let menuItem = item as? NSMenuItem else { return false }
|
|
|
|
|
|
|
|
|
|
let showCommand = NSLocalizedString("Show Read Feeds", comment: "Command")
|
|
|
|
|
let hideCommand = NSLocalizedString("Hide Read Feeds", comment: "Command")
|
|
|
|
|
menuItem.title = sidebarViewController?.isReadFiltered ?? false ? showCommand : hideCommand
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateToggleReadArticles(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
|
|
|
|
let showCommand = NSLocalizedString("Show Read Articles", comment: "Command")
|
|
|
|
|
let hideCommand = NSLocalizedString("Hide Read Articles", comment: "Command")
|
|
|
|
|
|
2020-11-19 19:10:42 +01:00
|
|
|
|
guard let isReadFiltered = timelineContainerViewController?.isReadFiltered else {
|
|
|
|
|
(item as? NSMenuItem)?.title = hideCommand
|
|
|
|
|
if #available(macOS 11.0, *), let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
|
|
|
|
|
toolbarItem.toolTip = hideCommand
|
|
|
|
|
button.image = AppAssets.filterInactive
|
|
|
|
|
}
|
2019-11-24 11:29:15 +01:00
|
|
|
|
return false
|
|
|
|
|
}
|
2020-11-19 19:10:42 +01:00
|
|
|
|
|
|
|
|
|
if isReadFiltered {
|
|
|
|
|
(item as? NSMenuItem)?.title = showCommand
|
|
|
|
|
if #available(macOS 11.0, *), let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
|
|
|
|
|
toolbarItem.toolTip = showCommand
|
|
|
|
|
button.image = AppAssets.filterActive
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
(item as? NSMenuItem)?.title = hideCommand
|
|
|
|
|
if #available(macOS 11.0, *), let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
|
|
|
|
|
toolbarItem.toolTip = hideCommand
|
|
|
|
|
button.image = AppAssets.filterInactive
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
2019-11-24 11:29:15 +01:00
|
|
|
|
}
|
2018-02-18 05:58:30 +01:00
|
|
|
|
|
|
|
|
|
// MARK: - Misc.
|
|
|
|
|
|
2020-11-01 23:33:48 +01:00
|
|
|
|
func goToNextUnreadInTimeline(wrappingToTop wrapping: Bool) {
|
2018-02-18 05:58:30 +01:00
|
|
|
|
|
2019-02-18 22:10:16 +01:00
|
|
|
|
guard let timelineViewController = currentTimelineViewController else {
|
2018-02-18 05:58:30 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-01 23:33:48 +01:00
|
|
|
|
if timelineViewController.canGoToNextUnread(wrappingToTop: wrapping) {
|
|
|
|
|
timelineViewController.goToNextUnread(wrappingToTop: wrapping)
|
2018-02-18 05:58:30 +01:00
|
|
|
|
makeTimelineViewFirstResponder()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeTimelineViewFirstResponder() {
|
|
|
|
|
|
2019-02-18 22:10:16 +01:00
|
|
|
|
guard let window = window, let timelineViewController = currentTimelineViewController else {
|
2018-02-18 05:58:30 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
window.makeFirstResponderUnlessDescendantIsFirstResponder(timelineViewController.tableView)
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-19 06:53:45 +02:00
|
|
|
|
func updateWindowTitle() {
|
2020-11-19 23:21:51 +01:00
|
|
|
|
guard timelineSourceMode != .search else {
|
|
|
|
|
let localizedLabel = NSLocalizedString("Search: %@", comment: "Search")
|
|
|
|
|
window?.title = NSString.localizedStringWithFormat(localizedLabel as NSString, searchString ?? "") as String
|
|
|
|
|
if #available(macOS 11.0, *) {
|
|
|
|
|
window?.subtitle = ""
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-02 20:00:09 +02:00
|
|
|
|
func setSubtitle(_ count: Int) {
|
|
|
|
|
let localizedLabel = NSLocalizedString("%d unread", comment: "Unread")
|
|
|
|
|
let formattedLabel = NSString.localizedStringWithFormat(localizedLabel as NSString, count)
|
|
|
|
|
if #available(macOS 11.0, *) {
|
|
|
|
|
window?.subtitle = formattedLabel as String
|
|
|
|
|
}
|
2017-10-19 06:53:45 +02:00
|
|
|
|
}
|
2018-09-19 04:57:38 +02:00
|
|
|
|
|
2020-09-02 20:00:09 +02:00
|
|
|
|
guard let selectedObjects = selectedObjectsInSidebar(), selectedObjects.count > 0 else {
|
|
|
|
|
window?.title = appDelegate.appName!
|
|
|
|
|
if #available(macOS 11.0, *) {
|
|
|
|
|
setSubtitle(appDelegate.unreadCount)
|
|
|
|
|
}
|
|
|
|
|
return
|
2017-10-19 06:53:45 +02:00
|
|
|
|
}
|
2018-09-19 04:57:38 +02:00
|
|
|
|
|
2020-09-02 20:00:09 +02:00
|
|
|
|
guard selectedObjects.count == 1 else {
|
|
|
|
|
window?.title = NSLocalizedString("Multiple", comment: "Multiple")
|
2020-08-10 18:43:18 +02:00
|
|
|
|
if #available(macOS 11.0, *) {
|
2020-09-02 20:00:09 +02:00
|
|
|
|
let unreadCount = selectedObjects.reduce(0, { result, selectedObject in
|
|
|
|
|
if let unreadCountProvider = selectedObject as? UnreadCountProvider {
|
|
|
|
|
return result + unreadCountProvider.unreadCount
|
|
|
|
|
} else {
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
setSubtitle(unreadCount)
|
2018-09-19 04:57:38 +02:00
|
|
|
|
}
|
2020-09-02 20:00:09 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let displayNameProvider = currentFeedOrFolder as? DisplayNameProvider {
|
|
|
|
|
window?.title = displayNameProvider.nameForDisplay
|
2020-08-14 21:35:36 +02:00
|
|
|
|
if #available(macOS 11.0, *) {
|
2020-09-02 20:00:09 +02:00
|
|
|
|
if let unreadCountProvider = currentFeedOrFolder as? UnreadCountProvider {
|
|
|
|
|
setSubtitle(unreadCountProvider.unreadCount)
|
|
|
|
|
}
|
2020-08-14 21:35:36 +02:00
|
|
|
|
}
|
2018-09-19 04:57:38 +02:00
|
|
|
|
}
|
2017-10-19 06:53:45 +02:00
|
|
|
|
}
|
2019-09-21 22:03:42 +02:00
|
|
|
|
|
|
|
|
|
func startArticleExtractorForCurrentLink() {
|
|
|
|
|
if let link = currentLink, let extractor = ArticleExtractor(link) {
|
|
|
|
|
extractor.delegate = self
|
|
|
|
|
extractor.process()
|
|
|
|
|
articleExtractor = extractor
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-02-27 07:31:07 +01:00
|
|
|
|
|
2020-03-06 00:42:54 +01:00
|
|
|
|
func saveSplitViewState(to state: inout [AnyHashable : Any]) {
|
2018-02-27 07:31:07 +01:00
|
|
|
|
guard let splitView = splitViewController?.splitView else {
|
|
|
|
|
return
|
2019-01-28 03:00:09 +01:00
|
|
|
|
}
|
2020-03-03 20:26:33 +01:00
|
|
|
|
|
|
|
|
|
let widths = splitView.arrangedSubviews.map{ Int(floor($0.frame.width)) }
|
2020-03-06 00:42:54 +01:00
|
|
|
|
state[MainWindowController.mainWindowWidthsStateKey] = widths
|
2018-02-27 07:31:07 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-06 00:42:54 +01:00
|
|
|
|
func restoreSplitViewState(from state: [AnyHashable : Any]) {
|
2020-03-03 20:26:33 +01:00
|
|
|
|
guard let splitView = splitViewController?.splitView,
|
2020-03-06 00:42:54 +01:00
|
|
|
|
let widths = state[MainWindowController.mainWindowWidthsStateKey] as? [Int],
|
2020-03-03 20:26:33 +01:00
|
|
|
|
widths.count == 3,
|
|
|
|
|
let window = window else {
|
|
|
|
|
return
|
2018-02-27 07:31:07 +01:00
|
|
|
|
}
|
2020-03-03 20:26:33 +01:00
|
|
|
|
|
2018-02-27 07:31:07 +01:00
|
|
|
|
let windowWidth = Int(floor(window.frame.width))
|
|
|
|
|
let dividerThickness: Int = Int(splitView.dividerThickness)
|
|
|
|
|
let sidebarWidth: Int = widths[0]
|
|
|
|
|
let timelineWidth: Int = widths[1]
|
|
|
|
|
|
|
|
|
|
// Make sure the detail view has its mimimum thickness, at least.
|
|
|
|
|
if windowWidth < sidebarWidth + dividerThickness + timelineWidth + dividerThickness + MainWindowController.detailViewMinimumThickness {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
splitView.setPosition(CGFloat(sidebarWidth), ofDividerAt: 0)
|
|
|
|
|
splitView.setPosition(CGFloat(sidebarWidth + dividerThickness + timelineWidth), ofDividerAt: 1)
|
|
|
|
|
}
|
2020-08-09 18:46:53 +02:00
|
|
|
|
|
2020-08-10 16:53:06 +02:00
|
|
|
|
func buildToolbarButton(_ itemIdentifier: NSToolbarItem.Identifier, _ title: String, _ image: NSImage, _ selector: String) -> NSToolbarItem {
|
2020-08-10 10:58:19 +02:00
|
|
|
|
let toolbarItem = RSToolbarItem(itemIdentifier: itemIdentifier)
|
|
|
|
|
toolbarItem.autovalidates = true
|
|
|
|
|
|
|
|
|
|
let button = NSButton()
|
|
|
|
|
button.bezelStyle = .texturedRounded
|
|
|
|
|
button.image = image
|
2020-11-09 20:02:46 +01:00
|
|
|
|
button.imageScaling = .scaleProportionallyDown
|
2020-08-10 10:58:19 +02:00
|
|
|
|
button.action = Selector((selector))
|
|
|
|
|
|
|
|
|
|
toolbarItem.view = button
|
2020-08-10 16:53:06 +02:00
|
|
|
|
toolbarItem.toolTip = title
|
2020-08-10 10:58:19 +02:00
|
|
|
|
toolbarItem.label = title
|
|
|
|
|
return toolbarItem
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-09 18:46:53 +02:00
|
|
|
|
func buildNewSidebarItemMenu() -> NSMenu {
|
|
|
|
|
let menu = NSMenu()
|
|
|
|
|
|
|
|
|
|
let newWebFeedItem = NSMenuItem()
|
|
|
|
|
newWebFeedItem.title = NSLocalizedString("New Web Feed", comment: "New Web Feed")
|
|
|
|
|
newWebFeedItem.action = Selector(("showAddWebFeedWindow:"))
|
|
|
|
|
menu.addItem(newWebFeedItem)
|
|
|
|
|
|
|
|
|
|
let newRedditFeedItem = NSMenuItem()
|
|
|
|
|
newRedditFeedItem.title = NSLocalizedString("New Reddit Feed", comment: "New Reddit Feed")
|
|
|
|
|
newRedditFeedItem.action = Selector(("showAddRedditFeedWindow:"))
|
|
|
|
|
menu.addItem(newRedditFeedItem)
|
|
|
|
|
|
|
|
|
|
let newTwitterFeedItem = NSMenuItem()
|
|
|
|
|
newTwitterFeedItem.title = NSLocalizedString("New Twitter Feed", comment: "New Twitter Feed")
|
|
|
|
|
newTwitterFeedItem.action = Selector(("showAddTwitterFeedWindow:"))
|
|
|
|
|
menu.addItem(newTwitterFeedItem)
|
|
|
|
|
|
|
|
|
|
let newFolderFeedItem = NSMenuItem()
|
|
|
|
|
newFolderFeedItem.title = NSLocalizedString("New Folder", comment: "New Folder")
|
|
|
|
|
newFolderFeedItem.action = Selector(("showAddFolderWindow:"))
|
|
|
|
|
menu.addItem(newFolderFeedItem)
|
|
|
|
|
|
|
|
|
|
return menu
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-27 19:43:27 +02:00
|
|
|
|
}
|
|
|
|
|
|