NetNewsWire/NetNewsWire/MainWindow/MainWindowController.swift

619 lines
16 KiB
Swift
Raw Normal View History

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.
//
import AppKit
import Articles
import Account
import RSCore
2017-05-27 19:43:27 +02:00
enum TimelineSourceMode {
case regular, search
}
2017-05-27 19:43:27 +02:00
class MainWindowController : NSWindowController, NSUserInterfaceValidations {
@IBOutlet var toolbarDelegate: MainWindowToolbarDelegate?
private var sharingServicePickerDelegate: NSSharingServicePickerDelegate?
2017-05-27 19:43:27 +02:00
2018-12-09 21:32:33 +01:00
private let windowAutosaveName = NSWindow.FrameAutosaveName("MainWindow")
static var didPositionWindowOnFirstRun = false
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
}
return selectedObjects.first
2017-10-19 06:53:45 +02:00
}
private var shareToolbarItem: NSToolbarItem? {
return window?.toolbar?.existingItem(withIdentifier: .Share)
}
private static var detailViewMinimumThickness = 384
private var sidebarViewController: SidebarViewController!
// MARK: - NSWindowController
override func windowDidLoad() {
super.windowDidLoad()
sharingServicePickerDelegate = SharingServicePickerDelegate(self.window)
if !AppDefaults.showTitleOnMainWindow {
window?.titleVisibility = .hidden
}
2017-10-06 03:17:07 +02:00
window?.setFrameUsingName(windowAutosaveName, force: true)
if AppDefaults.isFirstRun && !MainWindowController.didPositionWindowOnFirstRun {
if let window = window {
let point = NSPoint(x: 128, y: 64)
let size = NSSize(width: 1000, height: 700)
let minSize = NSSize(width: 600, height: 600)
window.setPointAndSizeAdjustingForScreen(point: point, size: size, minimumSize: minSize)
MainWindowController.didPositionWindowOnFirstRun = true
}
}
detailSplitViewItem?.minimumThickness = CGFloat(MainWindowController.detailViewMinimumThickness)
restoreSplitViewState()
sidebarViewController = splitViewController?.splitViewItems[0].viewController as? SidebarViewController
sidebarViewController.delegate = self
2017-09-18 01:30:45 +02:00
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillTerminate(_:)), name: NSApplication.willTerminateNotification, object: nil)
2017-05-27 19:43:27 +02:00
NotificationCenter.default.addObserver(self, selector: #selector(refreshProgressDidChange(_:)), name: .AccountRefreshDidBegin, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(refreshProgressDidChange(_:)), name: .AccountRefreshDidFinish, object: nil)
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)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
2017-10-19 06:53:45 +02:00
DispatchQueue.main.async {
self.updateWindowTitle()
2017-05-27 19:43:27 +02:00
}
2017-10-19 06:53:45 +02:00
}
2018-01-21 20:35:50 +01:00
// MARK: - API
func saveState() {
saveSplitViewState()
}
2018-01-21 20:35:50 +01:00
func selectedObjectsInSidebar() -> [AnyObject]? {
return sidebarViewController?.selectedObjects
}
// MARK: - Notifications
2017-09-17 21:54:08 +02:00
@objc func applicationWillTerminate(_ note: Notification) {
saveState()
2017-10-06 03:17:07 +02:00
window?.saveFrame(usingName: windowAutosaveName)
}
2017-05-27 19:43:27 +02:00
2017-09-17 21:54:08 +02:00
@objc func refreshProgressDidChange(_ note: Notification) {
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) {
updateWindowTitleIfNecessary(note.object)
}
@objc func displayNameDidChange(_ note: Notification) {
updateWindowTitleIfNecessary(note.object)
}
2017-10-19 06:53:45 +02:00
private func updateWindowTitleIfNecessary(_ noteObject: Any?) {
if let folder = currentFeedOrFolder as? Folder, let noteObject = noteObject as? Folder {
if folder == noteObject {
updateWindowTitle()
return
}
}
if let feed = currentFeedOrFolder as? Feed, let noteObject = noteObject as? Feed {
if feed == noteObject {
updateWindowTitle()
return
}
2017-10-19 06:53:45 +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
// PsuedoFeed object.
if let currentObject = currentFeedOrFolder, let noteObject = noteObject {
if currentObject === noteObject as AnyObject {
updateWindowTitle()
}
}
2017-10-19 06:53:45 +02:00
}
// MARK: - Toolbar
2017-05-27 19:43:27 +02:00
@objc func makeToolbarValidate() {
2017-05-27 19:43:27 +02:00
window?.toolbar?.validateVisibleItems()
}
// MARK: - NSUserInterfaceValidations
2017-05-27 19:43:27 +02:00
public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
if item.action == #selector(openArticleInBrowser(_:)) {
return currentLink != nil
}
if item.action == #selector(nextUnread(_:)) {
return canGoToNextUnread()
}
if item.action == #selector(markAllAsRead(_:)) {
return canMarkAllAsRead()
}
2017-11-06 06:27:34 +01:00
if item.action == #selector(toggleRead(_:)) {
return validateToggleRead(item)
2017-11-06 06:27:34 +01:00
}
if item.action == #selector(toggleStarred(_:)) {
return validateToggleStarred(item)
}
if item.action == #selector(markOlderArticlesAsRead(_:)) {
return canMarkOlderArticlesAsRead()
}
if item.action == #selector(toolbarShowShareMenu(_:)) {
return canShowShareMenu()
}
if item.action == #selector(toggleSidebar(_:)) {
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
}
2017-05-27 19:43:27 +02:00
return true
}
// MARK: - Actions
@IBAction func scrollOrGoToNextUnread(_ sender: Any?) {
2017-12-20 22:39:31 +01:00
guard let detailViewController = detailViewController else {
return
}
2017-12-20 22:39:31 +01:00
detailViewController.canScrollDown { (canScroll) in
NSCursor.setHiddenUntilMouseMoves(true)
2017-12-20 22:39:31 +01:00
canScroll ? detailViewController.scrollPageDown(sender) : self.nextUnread(sender)
}
}
@IBAction func showAddFolderWindow(_ sender: Any?) {
appDelegate.showAddFolderSheetOnWindow(window!)
}
@IBAction func showAddFeedWindow(_ sender: Any?) {
appDelegate.showAddFeedSheetOnWindow(window!, urlString: nil, name: nil, folder: nil)
}
@IBAction func openArticleInBrowser(_ sender: Any?) {
2017-05-27 19:43:27 +02:00
if let link = currentLink {
Browser.open(link)
2017-05-27 19:43:27 +02:00
}
}
@IBAction func openInBrowser(_ sender: Any?) {
openArticleInBrowser(sender)
}
@IBAction func nextUnread(_ sender: Any?) {
2017-05-27 19:43:27 +02:00
guard let timelineViewController = timelineViewController, let sidebarViewController = sidebarViewController else {
return
}
2018-09-05 06:18:59 +02:00
NSCursor.setHiddenUntilMouseMoves(true)
2017-05-27 19:43:27 +02:00
if timelineViewController.canGoToNextUnread() {
goToNextUnreadInTimeline()
2017-05-27 19:43:27 +02:00
}
else if sidebarViewController.canGoToNextUnread() {
sidebarViewController.goToNextUnread()
if timelineViewController.canGoToNextUnread() {
goToNextUnreadInTimeline()
}
}
}
@IBAction func markAllAsRead(_ sender: Any?) {
2017-05-27 19:43:27 +02:00
timelineViewController?.markAllAsRead()
}
2017-11-06 06:27:34 +01:00
@IBAction func toggleRead(_ sender: Any?) {
2017-11-06 06:27:34 +01:00
timelineViewController?.toggleReadStatusForSelectedArticles()
2017-11-06 06:27:34 +01:00
}
@IBAction func markUnread(_ sender: Any?) {
timelineViewController?.markSelectedArticlesAsUnread(sender)
}
@IBAction func toggleStarred(_ sender: Any?) {
timelineViewController?.toggleStarredStatusForSelectedArticles()
}
@IBAction func markAllAsReadAndGoToNextUnread(_ sender: Any?) {
markAllAsRead(sender)
nextUnread(sender)
}
@IBAction func markUnreadAndGoToNextUnread(_ sender: Any?) {
markUnread(sender)
nextUnread(sender)
}
@IBAction func markReadAndGoToNextUnread(_ sender: Any?) {
markUnread(sender)
nextUnread(sender)
}
@IBAction func toggleSidebar(_ sender: Any?) {
2017-05-27 19:43:27 +02:00
splitViewController!.toggleSidebar(sender)
}
2017-11-17 03:23:07 +01:00
@IBAction func markOlderArticlesAsRead(_ sender: Any?) {
2017-11-17 03:23:07 +01:00
timelineViewController?.markOlderArticlesRead()
2017-11-20 01:28:26 +01:00
}
@IBAction func navigateToTimeline(_ sender: Any?) {
timelineViewController?.focus()
}
@IBAction func navigateToSidebar(_ sender: Any?) {
sidebarViewController?.focus()
}
@IBAction func goToPreviousSubscription(_ sender: Any?) {
sidebarViewController?.outlineView.selectPreviousRow(sender)
}
@IBAction func goToNextSubscription(_ sender: Any?) {
sidebarViewController?.outlineView.selectNextRow(sender)
}
@IBAction func gotoToday(_ sender: Any?) {
sidebarViewController?.gotoToday(sender)
}
@IBAction func gotoAllUnread(_ sender: Any?) {
sidebarViewController?.gotoAllUnread(sender)
}
@IBAction func gotoStarred(_ sender: Any?) {
sidebarViewController?.gotoStarred(sender)
}
@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
}
let sortedArticles = selectedArticles.sortedByDate(.orderedAscending)
let items = sortedArticles.map { ArticlePasteboardWriter(article: $0) }
let sharingServicePicker = NSSharingServicePicker(items: items)
sharingServicePicker.delegate = sharingServicePickerDelegate
sharingServicePicker.show(relativeTo: view.bounds, of: view, preferredEdge: .minY)
}
}
// MARK: - SidebarDelegate
extension MainWindowController: SidebarDelegate {
func selectionDidChange(to selectedObjects: [AnyObject]?) {
// TODO: communicate with TimelineViewController
updateWindowTitle()
NotificationCenter.default.post(name: .InspectableObjectsDidChange, object: nil)
}
}
// 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 ?? []
}
}
2017-10-06 05:03:35 +02:00
// MARK: - Private
2017-05-27 19:43:27 +02:00
private extension MainWindowController {
var splitViewController: NSSplitViewController? {
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
}
var timelineViewController: TimelineViewController? {
return splitViewController?.splitViewItems[1].viewController as? TimelineViewController
2017-05-27 19:43:27 +02:00
}
var sidebarSplitViewItem: NSSplitViewItem? {
return splitViewController?.splitViewItems[0]
}
2017-05-27 19:43:27 +02:00
var detailSplitViewItem: NSSplitViewItem? {
return splitViewController?.splitViewItems[2]
2017-05-27 19:43:27 +02:00
}
var detailViewController: DetailViewController? {
return splitViewController?.splitViewItems[2].viewController as? DetailViewController
2017-05-27 19:43:27 +02:00
}
var selectedArticles: [Article]? {
return timelineViewController?.selectedArticles
2017-05-27 19:43:27 +02:00
}
2017-05-27 19:43:27 +02:00
var oneSelectedArticle: Article? {
if let articles = selectedArticles {
return articles.count == 1 ? articles[0] : nil
2017-05-27 19:43:27 +02:00
}
return nil
2017-05-27 19:43:27 +02:00
}
2017-05-27 19:43:27 +02:00
var currentLink: String? {
return oneSelectedArticle?.preferredLink
2017-05-27 19:43:27 +02:00
}
// MARK: - Command Validation
2017-05-27 19:43:27 +02:00
func canGoToNextUnread() -> Bool {
guard let timelineViewController = timelineViewController, let sidebarViewController = sidebarViewController else {
return false
}
return timelineViewController.canGoToNextUnread() || sidebarViewController.canGoToNextUnread()
}
func canMarkAllAsRead() -> Bool {
return timelineViewController?.canMarkAllAsRead() ?? false
}
2017-10-19 06:53:45 +02:00
func validateToggleRead(_ item: NSValidatedUserInterfaceItem) -> Bool {
2017-11-06 06:27:34 +01:00
let validationStatus = timelineViewController?.markReadCommandStatus() ?? .canDoNothing
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
}
return result
2017-11-06 06:27:34 +01:00
}
func canMarkOlderArticlesAsRead() -> Bool {
return timelineViewController?.canMarkOlderArticlesAsRead() ?? false
}
func canShowShareMenu() -> Bool {
guard let selectedArticles = selectedArticles else {
return false
}
return !selectedArticles.isEmpty
}
func validateToggleStarred(_ item: NSValidatedUserInterfaceItem) -> Bool {
let validationStatus = timelineViewController?.markStarredCommandStatus() ?? .canDoNothing
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 button = toolbarItem.view as? NSButton {
// button.image = NSImage(named: starring ? .star : .unstar)
// }
}
if let menuItem = item as? NSMenuItem {
menuItem.title = commandName
}
return result
}
// MARK: - Misc.
func goToNextUnreadInTimeline() {
guard let timelineViewController = timelineViewController else {
return
}
if timelineViewController.canGoToNextUnread() {
timelineViewController.goToNextUnread()
makeTimelineViewFirstResponder()
}
}
func makeTimelineViewFirstResponder() {
guard let window = window, let timelineViewController = timelineViewController else {
return
}
window.makeFirstResponderUnlessDescendantIsFirstResponder(timelineViewController.tableView)
}
2017-10-19 06:53:45 +02:00
func updateWindowTitle() {
var displayName: String? = nil
var unreadCount: Int? = nil
if let displayNameProvider = currentFeedOrFolder as? DisplayNameProvider {
displayName = displayNameProvider.nameForDisplay
2017-10-19 06:53:45 +02:00
}
if let unreadCountProvider = currentFeedOrFolder as? UnreadCountProvider {
unreadCount = unreadCountProvider.unreadCount
2017-10-19 06:53:45 +02:00
}
if displayName != nil {
if unreadCount ?? 0 > 0 {
window?.title = "\(displayName!) (\(unreadCount!))"
}
else {
window?.title = "\(displayName!)"
}
}
else {
window?.title = appDelegate.appName!
return
}
2017-10-19 06:53:45 +02:00
}
func saveSplitViewState() {
// TODO: Update this for multiple windows.
guard let splitView = splitViewController?.splitView else {
return
}
let widths = splitView.arrangedSubviews.map{ Int(floor($0.frame.width)) }
if AppDefaults.mainWindowWidths != widths {
AppDefaults.mainWindowWidths = widths
}
}
func restoreSplitViewState() {
// TODO: Update this for multiple windows.
guard let splitView = splitViewController?.splitView, let widths = AppDefaults.mainWindowWidths, widths.count == 3, let window = window else {
return
}
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)
}
2017-05-27 19:43:27 +02:00
}