Initial support for multiple windows and state preservation.

This commit is contained in:
Maurice Parker 2020-03-02 17:46:31 -08:00
parent 5755dd0844
commit eaa99db5c7
4 changed files with 144 additions and 72 deletions

View File

@ -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)
}
}

View File

@ -79,6 +79,13 @@
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
<menuItem title="New Window" keyEquivalent="n" id="pGg-Gc-PU2">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="newMainWindow:" target="Ady-hI-5gd" id="esB-AD-W2m"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="MFJ-WG-3fx"/>
<menuItem title="Refresh" keyEquivalent="r" id="Veh-SV-KWy">
<connections>
<action selector="refreshAll:" target="Ady-hI-5gd" id="gIc-Zj-9tB"/>

View File

@ -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

View File

@ -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()
}
}