From eaa99db5c7b65a5f1a9e231cfb7c237949e4ed3d Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 2 Mar 2020 17:46:31 -0800 Subject: [PATCH] Initial support for multiple windows and state preservation. --- Mac/AppDelegate.swift | 151 +++++++++++------- Mac/Base.lproj/Main.storyboard | 7 + Mac/MainWindow/MainWindowController.swift | 42 +++-- .../Sidebar/SidebarViewController.swift | 16 +- 4 files changed, 144 insertions(+), 72 deletions(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 9b3137d4d..b7c916272 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -29,6 +29,10 @@ var appDelegate: AppDelegate! class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate { + private struct WindowRestorationIdentifiers { + static let mainWindow = "mainWindow" + } + var userNotificationManager: UserNotificationManager! var faviconDownloader: FaviconDownloader! var imageDownloader: ImageDownloader! @@ -65,8 +69,22 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } + private var mainWindowController: MainWindowController? { + var bestController: MainWindowController? + for candidateController in mainWindowControllers { + if let bestWindow = bestController?.window, let candidateWindow = candidateController.window { + if bestWindow.orderedIndex > candidateWindow.orderedIndex { + bestController = candidateController + } + } else { + bestController = candidateController + } + } + return bestController + } + + private var mainWindowControllers = [MainWindowController]() private var preferencesWindowController: NSWindowController? - private var mainWindowController: MainWindowController? private var addFeedController: AddFeedController? private var addFolderWindowController: AddFolderWindowController? private var importOPMLController: ImportOPMLWindowController? @@ -126,6 +144,32 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, func applicationWillFinishLaunching(_ notification: Notification) { installAppleEventHandlers() + + CacheCleaner.purgeIfNecessary() + + // Try to establish a cache in the Caches folder, but if it fails for some reason fall back to a temporary dir + let cacheFolder: String + if let userCacheFolder = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path { + cacheFolder = userCacheFolder + } + else { + let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String) + cacheFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier) + } + + let faviconsFolder = (cacheFolder as NSString).appendingPathComponent("Favicons") + let faviconsFolderURL = URL(fileURLWithPath: faviconsFolder) + try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil) + faviconDownloader = FaviconDownloader(folder: faviconsFolder) + + let imagesFolder = (cacheFolder as NSString).appendingPathComponent("Images") + let imagesFolderURL = URL(fileURLWithPath: imagesFolder) + try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil) + imageDownloader = ImageDownloader(folder: imagesFolder) + + authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader) + webFeedIconDownloader = WebFeedIconDownloader(imageDownloader: imageDownloader, folder: cacheFolder) + } func applicationDidFinishLaunching(_ note: Notification) { @@ -162,34 +206,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } } - CacheCleaner.purgeIfNecessary() - - // Try to establish a cache in the Caches folder, but if it fails for some reason fall back to a temporary dir - let cacheFolder: String - if let userCacheFolder = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path { - cacheFolder = userCacheFolder - } - else { - let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String) - cacheFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier) - } - - let faviconsFolder = (cacheFolder as NSString).appendingPathComponent("Favicons") - let faviconsFolderURL = URL(fileURLWithPath: faviconsFolder) - try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil) - faviconDownloader = FaviconDownloader(folder: faviconsFolder) - - let imagesFolder = (cacheFolder as NSString).appendingPathComponent("Images") - let imagesFolderURL = URL(fileURLWithPath: imagesFolder) - try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil) - imageDownloader = ImageDownloader(folder: imagesFolder) - - authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader) - webFeedIconDownloader = WebFeedIconDownloader(imageDownloader: imageDownloader, folder: cacheFolder) - updateSortMenuItems() updateGroupByFeedMenuItem() - createAndShowMainWindow() + createAndShowMainWindowIfNecessary() + if isFirstRun { mainWindowController?.window?.center() } @@ -332,19 +352,35 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } // MARK: Main Window - func windowControllerWithName(_ storyboardName: String) -> NSWindowController { + + func createMainWindowController() -> MainWindowController { + let controller = windowControllerWithName("MainWindow") as! MainWindowController + mainWindowControllers.append(controller) + return controller + } + func windowControllerWithName(_ storyboardName: String) -> NSWindowController { let storyboard = NSStoryboard(name: NSStoryboard.Name(storyboardName), bundle: nil) return storyboard.instantiateInitialController()! as! NSWindowController } - func createAndShowMainWindow() { - - if mainWindowController == nil { - mainWindowController = createReaderWindow() + @discardableResult + func createAndShowMainWindow() -> NSWindow? { + let controller = createMainWindowController() + controller.showWindow(self) + + if let window = controller.window { + window.restorationClass = Self.self + window.identifier = NSUserInterfaceItemIdentifier(rawValue: WindowRestorationIdentifiers.mainWindow) } + + return controller.window + } - mainWindowController!.showWindow(self) + func createAndShowMainWindowIfNecessary() { + if mainWindowController == nil { + createAndShowMainWindow() + } } // MARK: NSUserInterfaceValidations @@ -388,8 +424,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, // MARK: Add Feed func addFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) { - - createAndShowMainWindow() + createAndShowMainWindowIfNecessary() + if mainWindowController!.isDisplayingSheet { return } @@ -405,7 +441,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, // MARK: - Actions @IBAction func showPreferences(_ sender: Any?) { - if preferencesWindowController == nil { preferencesWindowController = windowControllerWithName("Preferences") } @@ -413,30 +448,30 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, preferencesWindowController!.showWindow(self) } - @IBAction func showMainWindow(_ sender: Any?) { - + @IBAction func newMainWindow(_ sender: Any?) { createAndShowMainWindow() } - @IBAction func refreshAll(_ sender: Any?) { + @IBAction func showMainWindow(_ sender: Any?) { + createAndShowMainWindowIfNecessary() + } + @IBAction func refreshAll(_ sender: Any?) { AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present) } @IBAction func showAddFeedWindow(_ sender: Any?) { - addFeed(nil) } @IBAction func showAddFolderWindow(_ sender: Any?) { - - createAndShowMainWindow() + createAndShowMainWindowIfNecessary() showAddFolderSheetOnWindow(mainWindowController!.window!) } @IBAction func showKeyboardShortcutsWindow(_ sender: Any?) { - if keyboardShortcutsWindowController == nil { + keyboardShortcutsWindowController = WebViewWindowController(title: NSLocalizedString("Keyboard Shortcuts", comment: "window title")) let htmlFile = Bundle(for: type(of: self)).path(forResource: "KeyboardShortcuts", ofType: "html")! keyboardShortcutsWindowController?.displayContents(of: htmlFile) @@ -447,6 +482,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, let minSize = NSSize(width: 400, height: 400) window.setPointAndSizeAdjustingForScreen(point: point, size: size, minimumSize: minSize) } + } keyboardShortcutsWindowController!.showWindow(self) @@ -467,7 +503,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } @IBAction func importOPMLFromFile(_ sender: Any?) { - createAndShowMainWindow() + createAndShowMainWindowIfNecessary() if mainWindowController!.isDisplayingSheet { return } @@ -477,7 +513,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } @IBAction func importNNW3FromFile(_ sender: Any?) { - createAndShowMainWindow() + createAndShowMainWindowIfNecessary() if mainWindowController!.isDisplayingSheet { return } @@ -485,7 +521,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } @IBAction func exportOPML(_ sender: Any?) { - createAndShowMainWindow() + createAndShowMainWindowIfNecessary() if mainWindowController!.isDisplayingSheet { return } @@ -565,19 +601,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, @IBAction func gotoToday(_ sender: Any?) { - createAndShowMainWindow() + createAndShowMainWindowIfNecessary() mainWindowController!.gotoToday(sender) } @IBAction func gotoAllUnread(_ sender: Any?) { - createAndShowMainWindow() + createAndShowMainWindowIfNecessary() mainWindowController!.gotoAllUnread(sender) } @IBAction func gotoStarred(_ sender: Any?) { - createAndShowMainWindow() + createAndShowMainWindowIfNecessary() mainWindowController!.gotoStarred(sender) } @@ -633,11 +669,6 @@ private extension AppDelegate { syncTimer?.fireOldTimer() } - func createReaderWindow() -> MainWindowController { - - return windowControllerWithName("MainWindow") as! MainWindowController - } - func objectsForInspector() -> [Any]? { guard let window = NSApplication.shared.mainWindow, let windowController = window.windowController as? MainWindowController else { @@ -686,3 +717,15 @@ extension AppDelegate : ScriptingAppDelegate { return self.scriptingMainWindowController?.scriptingSelectedArticles ?? [] } } + +extension AppDelegate: NSWindowRestoration { + + @objc static func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier, state: NSCoder, completionHandler: @escaping (NSWindow?, Error?) -> Void) { + var mainWindow: NSWindow? = nil + if identifier.rawValue == WindowRestorationIdentifiers.mainWindow { + mainWindow = appDelegate.createAndShowMainWindow() + } + completionHandler(mainWindow, nil) + } + +} diff --git a/Mac/Base.lproj/Main.storyboard b/Mac/Base.lproj/Main.storyboard index 19fbdd9f3..5774c150e 100644 --- a/Mac/Base.lproj/Main.storyboard +++ b/Mac/Base.lproj/Main.storyboard @@ -79,6 +79,13 @@ + + + + + + + diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index 6c70ec92c..25cf69e4e 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -131,20 +131,6 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { // MARK: - Notifications -// func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) { -// -// saveSplitViewState(to: state) -// } -// -// func window(_ window: NSWindow, didDecodeRestorableState state: NSCoder) { -// -// restoreSplitViewState(from: state) -// -// // Make sure the timeline view is first responder if possible, to start out viewing -// // whatever preserved selection might have been restored -// makeTimelineViewFirstResponder() -// } - @objc func applicationWillTerminate(_ note: Notification) { saveState() window?.saveFrame(usingName: windowAutosaveName) @@ -460,9 +446,32 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { // MARK: NSWindowDelegate extension MainWindowController: NSWindowDelegate { + + func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) { + + if let sidebarReadFiltered = sidebarViewController?.isReadFiltered { + state.encode(sidebarReadFiltered, forKey: UserInfoKey.readFeedsFilterState) + } + +// saveSplitViewState(to: state) + } + + func window(_ window: NSWindow, didDecodeRestorableState state: NSCoder) { + + let sidebarReadFiltered = state.decodeBool(forKey: UserInfoKey.readFeedsFilterState) + sidebarViewController?.isReadFiltered = sidebarReadFiltered + +// restoreSplitViewState(from: state) +// +// // Make sure the timeline view is first responder if possible, to start out viewing +// // whatever preserved selection might have been restored +// makeTimelineViewFirstResponder() + } + func windowWillClose(_ notification: Notification) { detailViewController?.stopMediaPlayback() } + } // MARK: - SidebarDelegate @@ -489,6 +498,11 @@ extension MainWindowController: SidebarDelegate { } return timelineViewController.unreadCount } + + func sidebarInvalidateRestorableState(_: SidebarViewController) { + invalidateRestorableState() + } + } // MARK: - TimelineContainerViewControllerDelegate diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift index a951fbd11..dd0a212cf 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift @@ -15,6 +15,7 @@ import RSCore protocol SidebarDelegate: class { func sidebarSelectionDidChange(_: SidebarViewController, selectedObjects: [AnyObject]?) func unreadCount(for: AnyObject) -> Int + func sidebarInvalidateRestorableState(_: SidebarViewController) } @objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSOutlineViewDataSource, NSMenuDelegate, UndoableCommandRunner { @@ -31,8 +32,16 @@ protocol SidebarDelegate: class { lazy var dataSource: SidebarOutlineDataSource = { return SidebarOutlineDataSource(treeController: treeController) }() + var isReadFiltered: Bool { - return treeControllerDelegate.isReadFiltered + get { + return treeControllerDelegate.isReadFiltered + } + set { + treeControllerDelegate.isReadFiltered = newValue + delegate?.sidebarInvalidateRestorableState(self) + rebuildTreeAndRestoreSelection() + } } var undoableCommands = [UndoableCommand]() @@ -355,11 +364,10 @@ protocol SidebarDelegate: class { func toggleReadFilter() { if treeControllerDelegate.isReadFiltered { - treeControllerDelegate.isReadFiltered = false + isReadFiltered = false } else { - treeControllerDelegate.isReadFiltered = true + isReadFiltered = true } - rebuildTreeAndRestoreSelection() } }