// // TimelineContainerViewController.swift // NetNewsWire // // Created by Brent Simmons on 2/14/19. // Copyright © 2019 Ranchero Software. All rights reserved. // import AppKit import Account import Articles protocol TimelineContainerViewControllerDelegate: AnyObject { @MainActor func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) @MainActor func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed) @MainActor func timelineInvalidatedRestorationState(_: TimelineContainerViewController) } final class TimelineContainerViewController: NSViewController { @IBOutlet weak var viewOptionsPopUpButton: NSPopUpButton! @IBOutlet weak var newestToOldestMenuItem: NSMenuItem! @IBOutlet weak var oldestToNewestMenuItem: NSMenuItem! @IBOutlet weak var groupByFeedMenuItem: NSMenuItem! @IBOutlet weak var readFilteredButton: NSButton! @IBOutlet var containerView: TimelineContainerView! var currentTimelineViewController: TimelineViewController? { didSet { let view = currentTimelineViewController?.view if containerView.contentView === view { return } containerView.contentView = view view?.window?.recalculateKeyViewLoop() } } weak var delegate: TimelineContainerViewControllerDelegate? var isReadFiltered: Bool? { guard let currentTimelineViewController = currentTimelineViewController, mode(for: currentTimelineViewController) == .regular else { return nil } return regularTimelineViewController.isReadFiltered } var isCleanUpAvailable: Bool { guard let currentTimelineViewController = currentTimelineViewController, mode(for: currentTimelineViewController) == .regular else { return false } return regularTimelineViewController.isCleanUpAvailable } lazy var regularTimelineViewController = { return TimelineViewController(delegate: self) }() private lazy var searchTimelineViewController: TimelineViewController = { let viewController = TimelineViewController(delegate: self) viewController.showsSearchResults = true return viewController }() override func viewDidLoad() { super.viewDidLoad() setRepresentedObjects(nil, mode: .regular) showTimeline(for: .regular) makeMenuItemTitleLarger(newestToOldestMenuItem) makeMenuItemTitleLarger(oldestToNewestMenuItem) makeMenuItemTitleLarger(groupByFeedMenuItem) updateViewOptionsPopUpButton() NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) } // MARK: - Notifications @objc func userDefaultsDidChange(_ note: Notification) { updateViewOptionsPopUpButton() } // MARK: - API func setRepresentedObjects(_ objects: [AnyObject]?, mode: TimelineSourceMode) { timelineViewController(for: mode).representedObjects = objects updateReadFilterButton() } func showTimeline(for mode: TimelineSourceMode) { currentTimelineViewController = timelineViewController(for: mode) } func regularTimelineViewControllerHasRepresentedObjects(_ representedObjects: [AnyObject]?) -> Bool { // Use this to find out if the regular timeline view already has the specified representedObjects. // This is used in determining whether a search should end. // The sidebar may think that the selection has changed, and therefore search should end — // but it could be that the regular timeline already has these representedObjects, // and therefore the selection hasn’t actually changed, // and therefore search shouldn’t end. // https://github.com/brentsimmons/NetNewsWire/issues/791 if representedObjects == nil && regularTimelineViewController.representedObjects == nil { return true } guard let currentObjects = regularTimelineViewController.representedObjects, let representedObjects = representedObjects else { return false } if currentObjects.count != representedObjects.count { return false } for object in representedObjects { guard let _ = currentObjects.firstIndex(where: { $0 === object } ) else { return false } } return true } func cleanUp() { regularTimelineViewController.cleanUp() } func toggleReadFilter() { regularTimelineViewController.toggleReadFilter() updateReadFilterButton() } // MARK: State Restoration func saveState(to state: inout [AnyHashable : Any]) { regularTimelineViewController.saveState(to: &state) } func restoreState(from state: [AnyHashable : Any]) { regularTimelineViewController.restoreState(from: state) updateReadFilterButton() } } extension TimelineContainerViewController: TimelineDelegate { func timelineSelectionDidChange(_ timelineViewController: TimelineViewController, selectedArticles: [Article]?) { delegate?.timelineSelectionDidChange(self, articles: selectedArticles, mode: mode(for: timelineViewController)) } func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed) { delegate?.timelineRequestedFeedSelection(self, feed: feed) } func timelineInvalidatedRestorationState(_: TimelineViewController) { delegate?.timelineInvalidatedRestorationState(self) } } private extension TimelineContainerViewController { func makeMenuItemTitleLarger(_ menuItem: NSMenuItem) { menuItem.attributedTitle = NSAttributedString(string: menuItem.title, attributes: [NSAttributedString.Key.font: NSFont.controlContentFont(ofSize: NSFont.systemFontSize)]) } func timelineViewController(for mode: TimelineSourceMode) -> TimelineViewController { switch mode { case .regular: return regularTimelineViewController case .search: return searchTimelineViewController } } func mode(for timelineViewController: TimelineViewController) -> TimelineSourceMode { if timelineViewController === regularTimelineViewController { return .regular } else if timelineViewController === searchTimelineViewController { return .search } assertionFailure("Expected timelineViewController to match either regular or search timelineViewController, but it doesn’t.") return .regular // Should never get here. } func updateViewOptionsPopUpButton() { if AppDefaults.shared.timelineSortDirection == .orderedAscending { newestToOldestMenuItem.state = .off oldestToNewestMenuItem.state = .on viewOptionsPopUpButton.setTitle(oldestToNewestMenuItem.title) } else { newestToOldestMenuItem.state = .on oldestToNewestMenuItem.state = .off viewOptionsPopUpButton.setTitle(newestToOldestMenuItem.title) } if AppDefaults.shared.timelineGroupByFeed == true { groupByFeedMenuItem.state = .on } else { groupByFeedMenuItem.state = .off } } func updateReadFilterButton() { guard currentTimelineViewController == regularTimelineViewController else { readFilteredButton.isHidden = true return } guard let isReadFiltered = regularTimelineViewController.isReadFiltered else { readFilteredButton.isHidden = true return } readFilteredButton.isHidden = false if isReadFiltered { readFilteredButton.image = AppAsset.filterActive } else { readFilteredButton.image = AppAsset.filterInactive } } }