diff --git a/Evergreen.xcodeproj/project.pbxproj b/Evergreen.xcodeproj/project.pbxproj index 89429b796..a93982a53 100644 --- a/Evergreen.xcodeproj/project.pbxproj +++ b/Evergreen.xcodeproj/project.pbxproj @@ -166,6 +166,8 @@ D5A267C120131B8300A8D3C0 /* testFeedOPML.applescript in Sources */ = {isa = PBXBuildFile; fileRef = D5A267B220131B8300A8D3C0 /* testFeedOPML.applescript */; }; D5A267C220131BA000A8D3C0 /* testFeedOPML.applescript in CopyFiles */ = {isa = PBXBuildFile; fileRef = D5A267B220131B8300A8D3C0 /* testFeedOPML.applescript */; }; D5D1751220020B980047B29D /* Evergreen.sdef in Resources */ = {isa = PBXBuildFile; fileRef = D5D175012002039D0047B29D /* Evergreen.sdef */; }; + D5E4CC54202C1361009B4FFC /* AppDelegate+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E4CC53202C1361009B4FFC /* AppDelegate+Scriptability.swift */; }; + D5E4CC64202C1AC1009B4FFC /* MainWindowController+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E4CC63202C1AC1009B4FFC /* MainWindowController+Scriptability.swift */; }; D5F4EDB5200744A700B9E363 /* ScriptingObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB4200744A700B9E363 /* ScriptingObject.swift */; }; D5F4EDB720074D6500B9E363 /* Feed+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB620074D6500B9E363 /* Feed+Scriptability.swift */; }; D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */; }; @@ -654,6 +656,8 @@ D5A2679B201312F900A8D3C0 /* testNameOfAuthors.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = testNameOfAuthors.applescript; sourceTree = ""; }; D5A267B220131B8300A8D3C0 /* testFeedOPML.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = testFeedOPML.applescript; sourceTree = ""; }; D5D175012002039D0047B29D /* Evergreen.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = Evergreen.sdef; path = ../Resources/Evergreen.sdef; sourceTree = ""; }; + D5E4CC53202C1361009B4FFC /* AppDelegate+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Scriptability.swift"; sourceTree = ""; }; + D5E4CC63202C1AC1009B4FFC /* MainWindowController+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainWindowController+Scriptability.swift"; sourceTree = ""; }; D5F4EDB4200744A700B9E363 /* ScriptingObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptingObject.swift; sourceTree = ""; }; D5F4EDB620074D6500B9E363 /* Feed+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Feed+Scriptability.swift"; sourceTree = ""; }; D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Folder+Scriptability.swift"; sourceTree = ""; }; @@ -1311,10 +1315,12 @@ isa = PBXGroup; children = ( D5907D962004B7EB005947E5 /* Account+Scriptability.swift */, + D5E4CC53202C1361009B4FFC /* AppDelegate+Scriptability.swift */, D553737C20186C1F006D8857 /* Article+Scriptability.swift */, D5A2678B20130ECF00A8D3C0 /* Author+Scriptability.swift */, D5F4EDB620074D6500B9E363 /* Feed+Scriptability.swift */, D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */, + D5E4CC63202C1AC1009B4FFC /* MainWindowController+Scriptability.swift */, D5907D7E2004AC00005947E5 /* NSApplication+Scriptability.swift */, D5907DB12004BB37005947E5 /* ScriptingObjectContainer.swift */, D5F4EDB4200744A700B9E363 /* ScriptingObject.swift */, @@ -1891,6 +1897,7 @@ 849A97771ED9EC04007D329B /* TimelineCellData.swift in Sources */, 84B99C6B1FAE370B00ECDEDB /* FeedListFeed.swift in Sources */, 841ABA6020145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift in Sources */, + D5E4CC54202C1361009B4FFC /* AppDelegate+Scriptability.swift in Sources */, D5F4EDB5200744A700B9E363 /* ScriptingObject.swift in Sources */, D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */, 842611A01FCB72600086A189 /* FeaturedImageDownloader.swift in Sources */, @@ -1902,6 +1909,7 @@ 849A97541ED9EAC0007D329B /* AddFeedWindowController.swift in Sources */, 849A976D1ED9EBC8007D329B /* TimelineTableView.swift in Sources */, 84D52E951FE588BB00D14F5B /* DetailStatusBarView.swift in Sources */, + D5E4CC64202C1AC1009B4FFC /* MainWindowController+Scriptability.swift in Sources */, 84B99C671FAE35E600ECDEDB /* FeedListTreeControllerDelegate.swift in Sources */, 84D5BA20201E8FB6009092BD /* SidebarGearMenuDelegate.swift in Sources */, 84B99C691FAE36B800ECDEDB /* FeedListFolder.swift in Sources */, diff --git a/Evergreen/AppDelegate.swift b/Evergreen/AppDelegate.swift index 9ac432399..c37337275 100644 --- a/Evergreen/AppDelegate.swift +++ b/Evergreen/AppDelegate.swift @@ -526,3 +526,26 @@ private extension AppDelegate { sortByOldestArticleOnTopMenuItem.state = sortByNewestOnTop ? .off : .on } } + +/* + the ScriptingAppDelegate 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 AppDelegate : ScriptingAppDelegate { + + internal var scriptingMainWindowController: ScriptingMainWindowController? { + return mainWindowController + } + + internal var scriptingCurrentArticle: Article? { + return self.scriptingMainWindowController?.scriptingCurrentArticle + } + + internal var scriptingSelectedArticles: [Article] { + return self.scriptingMainWindowController?.scriptingSelectedArticles ?? [] + } +} + diff --git a/Evergreen/MainWindow/MainWindowController.swift b/Evergreen/MainWindow/MainWindowController.swift index e336244b6..5a73960dd 100644 --- a/Evergreen/MainWindow/MainWindowController.swift +++ b/Evergreen/MainWindow/MainWindowController.swift @@ -400,6 +400,28 @@ extension MainWindowController: NSToolbarDelegate { } } + +// 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 ?? [] + } +} + // MARK: - Private private extension MainWindowController { diff --git a/Evergreen/Resources/Evergreen.sdef b/Evergreen/Resources/Evergreen.sdef index fa47941f6..85a88c6db 100644 --- a/Evergreen/Resources/Evergreen.sdef +++ b/Evergreen/Resources/Evergreen.sdef @@ -20,7 +20,13 @@ - + + + + + + + @@ -42,7 +48,7 @@ - + @@ -71,7 +77,7 @@ - + @@ -101,7 +107,7 @@ - + @@ -120,7 +126,7 @@ - + @@ -130,7 +136,7 @@ - + diff --git a/Evergreen/Scriptability/Account+Scriptability.swift b/Evergreen/Scriptability/Account+Scriptability.swift index 5bc8ac248..4530dfea8 100644 --- a/Evergreen/Scriptability/Account+Scriptability.swift +++ b/Evergreen/Scriptability/Account+Scriptability.swift @@ -54,12 +54,29 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta return feeds.map { ScriptableFeed($0, container:self) } as NSArray } + @objc(valueInFeedsWithUniqueID:) + func valueInFeeds(withUniqueID id:String) -> ScriptableFeed? { + let feeds = account.children.compactMap { $0 as? Feed } + guard let feed = feeds.first(where:{$0.feedID == id}) else { return nil } + return ScriptableFeed(feed, container:self) + } + + @objc(folders) var folders:NSArray { let folders = account.children.compactMap { $0 as? Folder } return folders.map { ScriptableFolder($0, container:self) } as NSArray } + @objc(valueInFoldersWithUniqueID:) + func valueInFolders(withUniqueID id:NSNumber) -> ScriptableFolder? { + let folderId = id.intValue + let folders = account.children.compactMap { $0 as? Folder } + guard let folder = folders.first(where:{$0.folderID == folderId}) else { return nil } + return ScriptableFolder(folder, container:self) + } + + // MARK: --- Scriptable properties --- @objc(contents) diff --git a/Evergreen/Scriptability/AppDelegate+Scriptability.swift b/Evergreen/Scriptability/AppDelegate+Scriptability.swift new file mode 100644 index 000000000..b8b8f7c72 --- /dev/null +++ b/Evergreen/Scriptability/AppDelegate+Scriptability.swift @@ -0,0 +1,28 @@ +// +// AppDelegate+Scriptability.swift +// Evergreen +// +// Created by Olof Hellman on 2/7/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +/* + Note: strictly, the AppDelegate doesn't appear as part of the scripting model, + so this file is rather unlike the other Object+Scriptability.swift files. + However, the AppDelegate object is the de facto scripting accessor for some + application elements and properties. For, example, the main window is accessed + via the AppDelegate's MainWindowController, and the main window itself has + selected feeds, selected articles and a current article. This file supplies the glue to access + these scriptable objects, while being completely separate from the core AppDelegate code, +*/ + +import Foundation +import Data + +protocol ScriptingAppDelegate { + var scriptingCurrentArticle: Article? {get} + var scriptingSelectedArticles: [Article] {get} + var scriptingMainWindowController:ScriptingMainWindowController? {get} +} + + diff --git a/Evergreen/Scriptability/Feed+Scriptability.swift b/Evergreen/Scriptability/Feed+Scriptability.swift index d443c3dd2..8a5ddad8b 100644 --- a/Evergreen/Scriptability/Feed+Scriptability.swift +++ b/Evergreen/Scriptability/Feed+Scriptability.swift @@ -85,11 +85,19 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine return self.feed.OPMLString(indentLevel:0) } + // MARK: --- scriptable elements --- + @objc(authors) var authors:NSArray { let feedAuthors = feed.authors ?? [] return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray } + + @objc(valueInAuthorsWithUniqueID:) + func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? { + guard let author = feed.authors?.first(where:{$0.authorID == id}) else { return nil } + return ScriptableAuthor(author, container:self) + } @objc(articles) var articles:NSArray { @@ -100,5 +108,12 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine }) return sortedArticles.map { ScriptableArticle($0, container:self) } as NSArray } + + @objc(valueInArticlesWithUniqueID:) + func valueInArticles(withUniqueID id:String) -> ScriptableArticle? { + let articles = feed.fetchArticles() + guard let article = articles.first(where:{$0.uniqueID == id}) else { return nil } + return ScriptableArticle(article, container:self) + } } diff --git a/Evergreen/Scriptability/MainWindowController+Scriptability.swift b/Evergreen/Scriptability/MainWindowController+Scriptability.swift new file mode 100644 index 000000000..71cdb1cd7 --- /dev/null +++ b/Evergreen/Scriptability/MainWindowController+Scriptability.swift @@ -0,0 +1,16 @@ +// +// MainWindowController+Scriptability.swift +// Evergreen +// +// Created by Olof Hellman on 2/7/18. +// Copyright © 2018 Ranchero Software. All rights reserved. +// + +import Foundation +import Data + +protocol ScriptingMainWindowController { + var scriptingCurrentArticle: Article? { get } + var scriptingSelectedArticles: [Article] { get } +} + diff --git a/Evergreen/Scriptability/NSApplication+Scriptability.swift b/Evergreen/Scriptability/NSApplication+Scriptability.swift index 61903af5f..47a298519 100644 --- a/Evergreen/Scriptability/NSApplication+Scriptability.swift +++ b/Evergreen/Scriptability/NSApplication+Scriptability.swift @@ -20,27 +20,74 @@ extension NSApplication : ScriptingObjectContainer { return "application" } + @objc(currentArticle) + func currentArticle() -> ScriptableArticle? { + var scriptableArticle: ScriptableArticle? + if let currentArticle = appDelegate.scriptingCurrentArticle { + if let feed = currentArticle.feed { + let scriptableFeed = ScriptableFeed(feed, container:self) + scriptableArticle = ScriptableArticle(currentArticle, container:scriptableFeed) + } + } + return scriptableArticle + } + + @objc(selectedArticles) + func selectedArticles() -> NSArray { + let articles = appDelegate.scriptingSelectedArticles + let scriptableArticles:[ScriptableArticle] = articles.compactMap { article in + if let feed = article.feed { + let scriptableFeed = ScriptableFeed(feed, container:self) + return ScriptableArticle(article, container:scriptableFeed) + } else { + return nil + } + } + return scriptableArticles as NSArray + } + + // MARK: --- scriptable elements --- + @objc(accounts) func accounts() -> NSArray { let accounts = AccountManager.shared.accounts return accounts.map { ScriptableAccount($0) } as NSArray } + @objc(valueInAccountsWithUniqueID:) + func valueInAccounts(withUniqueID id:String) -> ScriptableAccount? { + let accounts = AccountManager.shared.accounts + guard let account = accounts.first(where:{$0.accountID == id}) else { return nil } + return ScriptableAccount(account) + } + /* accessing feeds from the application object skips the 'account' containment hierarchy this allows a script like 'articles of feed "The Shape of Everything"' as a shorthand for 'articles of feed "The Shape of Everything" of account "On My Mac"' - */ - @objc(feeds) - func feeds() -> NSArray { + */ + + func allFeeds() -> [Feed] { let accounts = AccountManager.shared.accounts let emptyFeeds:[Feed] = [] - let feeds = accounts.reduce(emptyFeeds) { (result, nthAccount) -> [Feed] in + return accounts.reduce(emptyFeeds) { (result, nthAccount) -> [Feed] in let accountFeeds = nthAccount.children.compactMap { $0 as? Feed } return result + accountFeeds } + } + + @objc(feeds) + func feeds() -> NSArray { + let feeds = self.allFeeds() return feeds.map { ScriptableFeed($0, container:self) } as NSArray } + + @objc(valueInFeedsWithUniqueID:) + func valueInFeeds(withUniqueID id:String) -> ScriptableFeed? { + let feeds = self.allFeeds() + guard let feed = feeds.first(where:{$0.feedID == id}) else { return nil } + return ScriptableFeed(feed, container:self) + } }