This commit is contained in:
Brent Simmons 2018-02-09 21:33:38 -08:00
commit 5fdb50b255
12 changed files with 252 additions and 11 deletions

View File

@ -103,6 +103,7 @@
84A1500520048DDF0046AD9A /* SendToMarsEditCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */; };
84A37CB5201ECD610087C5AF /* RenameWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A37CB4201ECD610087C5AF /* RenameWindowController.swift */; };
84A37CBB201ECE590087C5AF /* RenameSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84A37CB9201ECE590087C5AF /* RenameSheet.xib */; };
84AAF2BF202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */; };
84B06FAE1ED37DBD00F0B54B /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FA91ED37DAD00F0B54B /* RSCore.framework */; };
84B06FAF1ED37DBD00F0B54B /* RSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06FA91ED37DAD00F0B54B /* RSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
84B06FB21ED37DBD00F0B54B /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84B06F9D1ED37DA000F0B54B /* RSDatabase.framework */; };
@ -166,6 +167,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 */; };
@ -600,6 +603,7 @@
84A37CBA201ECE590087C5AF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Evergreen/Base.lproj/RenameSheet.xib; sourceTree = SOURCE_ROOT; };
84A6B6931FB8D43C006754AC /* DinosaursWindow.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DinosaursWindow.xib; sourceTree = "<group>"; };
84A6B6951FB8DBD2006754AC /* DinosaursWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DinosaursWindowController.swift; sourceTree = "<group>"; };
84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineContextualMenuDelegate.swift; sourceTree = "<group>"; };
84B06F961ED37DA000F0B54B /* RSDatabase.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSDatabase.xcodeproj; path = Frameworks/RSDatabase/RSDatabase.xcodeproj; sourceTree = "<group>"; };
84B06FA21ED37DAC00F0B54B /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = Frameworks/RSCore/RSCore.xcodeproj; sourceTree = "<group>"; };
84B06FB61ED37E8B00F0B54B /* RSWeb.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSWeb.xcodeproj; path = Frameworks/RSWeb/RSWeb.xcodeproj; sourceTree = "<group>"; };
@ -654,6 +658,8 @@
D5A2679B201312F900A8D3C0 /* testNameOfAuthors.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = testNameOfAuthors.applescript; sourceTree = "<group>"; };
D5A267B220131B8300A8D3C0 /* testFeedOPML.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = testFeedOPML.applescript; sourceTree = "<group>"; };
D5D175012002039D0047B29D /* Evergreen.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = Evergreen.sdef; path = ../Resources/Evergreen.sdef; sourceTree = "<group>"; };
D5E4CC53202C1361009B4FFC /* AppDelegate+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Scriptability.swift"; sourceTree = "<group>"; };
D5E4CC63202C1AC1009B4FFC /* MainWindowController+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainWindowController+Scriptability.swift"; sourceTree = "<group>"; };
D5F4EDB4200744A700B9E363 /* ScriptingObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptingObject.swift; sourceTree = "<group>"; };
D5F4EDB620074D6500B9E363 /* Feed+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Feed+Scriptability.swift"; sourceTree = "<group>"; };
D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Folder+Scriptability.swift"; sourceTree = "<group>"; };
@ -940,6 +946,7 @@
844B5B6C1FEA282400C7C76A /* Keyboard */,
84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */,
8414AD241FCF5A1E00955102 /* TimelineHeaderView.swift */,
84AAF2BE202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift */,
849A976F1ED9EC04007D329B /* Cell */,
);
path = Timeline;
@ -1311,10 +1318,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 */,
@ -1877,6 +1886,7 @@
848F6AE51FC29CFB002D422E /* FaviconDownloader.swift in Sources */,
849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */,
849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */,
84AAF2BF202CF684004A0BC4 /* TimelineContextualMenuDelegate.swift in Sources */,
849A97831ED9EC63007D329B /* SidebarStatusBarView.swift in Sources */,
84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */,
841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */,
@ -1891,6 +1901,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 +1913,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 */,

View File

@ -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 ?? []
}
}

View File

@ -592,6 +592,7 @@
<outlet property="dataSource" destination="36G-bQ-b96" id="OpB-zC-ItJ"/>
<outlet property="delegate" destination="36G-bQ-b96" id="s1m-42-GQ4"/>
<outlet property="keyboardDelegate" destination="ZOV-xh-WJE" id="HiG-Bz-vD0"/>
<outlet property="menu" destination="gb5-z4-YPr" id="pey-0u-ogu"/>
</connections>
</tableView>
</subviews>
@ -623,15 +624,33 @@
</userDefinedRuntimeAttributes>
</view>
<connections>
<outlet property="contextualMenuDelegate" destination="iD1-KK-gFc" id="b0j-aW-e4B"/>
<outlet property="tableView" destination="DRs-j8-R9a" id="2AG-SP-7n2"/>
</connections>
</viewController>
<customObject id="Ebq-4s-EwK" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
<menu id="gb5-z4-YPr">
<items>
<menuItem title="Item 1" id="Ikx-w7-cua">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Item 2" id="QX3-hL-Dqh">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Item 3" id="IsQ-j7-Njb">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
</items>
<connections>
<outlet property="delegate" destination="iD1-KK-gFc" id="q4j-wE-tnf"/>
</connections>
</menu>
<customObject id="ZOV-xh-WJE" customClass="TimelineKeyboardDelegate" customModule="Evergreen" customModuleProvider="target">
<connections>
<outlet property="timelineViewController" destination="36G-bQ-b96" id="rED-2Z-kh6"/>
</connections>
</customObject>
<customObject id="iD1-KK-gFc" customClass="TimelineContextualMenuDelegate" customModule="Evergreen" customModuleProvider="target"/>
</objects>
<point key="canvasLocation" x="62" y="394"/>
</scene>

View File

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

View File

@ -0,0 +1,35 @@
//
// TimelineContextualMenuDelegate.swift
// Evergreen
//
// Created by Brent Simmons on 2/8/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import AppKit
@objc final class TimelineContextualMenuDelegate: NSObject, NSMenuDelegate {
@IBOutlet weak var timelineViewController: TimelineViewController?
public func menuNeedsUpdate(_ menu: NSMenu) {
// guard let timelineViewController = timelineViewController else {
// return
// }
// menu.removeAllItems()
// guard let contextualMenu = sidebarViewController.contextualMenuForClickedRows() else {
// return
// }
//
// let items = contextualMenu.items
// contextualMenu.removeAllItems()
// for menuItem in items {
// menu.addItem(menuItem)
// }
}
}

View File

@ -15,7 +15,8 @@ import Account
class TimelineViewController: NSViewController, UndoableCommandRunner {
@IBOutlet var tableView: TimelineTableView!
@IBOutlet var contextualMenuDelegate: TimelineContextualMenuDelegate?
var selectedArticles: [Article] {
get {
return Array(articles.articlesForIndexes(tableView.selectedRowIndexes))

View File

@ -20,7 +20,13 @@
<!-- the name of the Cocoa class where we have provided the
various accessor methods for our application class. -->
<cocoa class="NSApplication"/>
<property name="current article" code="CurA" type="article" access="r" description="The article, if any, being currently displayed">
<cocoa key="currentArticle"/>
</property>
<property name="selected articles" code="SelA" access="r" description="All articles in the main window that are currently selected, if any.">
<type type="article" list="yes"/>
<cocoa key="selectedArticles"/>
</property>
<element type="account">
<cocoa key="accounts"/>
</element>
@ -42,7 +48,7 @@
<property name="name" code="pnam" type="text" access="r" description="The name of the account">
<cocoa key="name"/>
</property>
<property name="id" code="id " type="text" access="r" description="The unique id of the account">
<property name="id" code="ID " type="text" access="r" description="The unique id of the account">
<cocoa key="uniqueId"/>
</property>
<property name="type" code="ATyp" type="account type" access="r" description="The type of the account">
@ -71,7 +77,7 @@
<property name="name" code="pnam" type="text" access="r" description="The name of the feed">
<cocoa key="name"/>
</property>
<property name="id" code="id " type="text" access="r" description="The unique id of the account">
<property name="id" code="ID " type="text" access="r" description="The unique id of the account">
<cocoa key="uniqueId"/>
</property>
<property name="url" code="URL " type="text" access="r" description="The type of the account">
@ -101,7 +107,7 @@
<property name="name" code="pnam" type="text" access="r" description="The name of the author">
<cocoa key="name"/>
</property>
<property name="id" code="id " type="text" access="r" description="The unique id of the author">
<property name="id" code="ID " type="text" access="r" description="The unique id of the author">
<cocoa key="uniqueId"/>
</property>
<property name="url" code="URL " type="text" access="r" description="url for the author">
@ -120,7 +126,7 @@
<property name="name" code="pnam" type="text" access="r" description="The name of the account">
<cocoa key="name"/>
</property>
<property name="id" code="id " type="integer" access="r" description="The unique id of the account">
<property name="id" code="ID " type="integer" access="r" description="The unique id of the account">
<cocoa key="uniqueId"/>
</property>
<property name="opml representation" code="OPML" type="text" access="r" description="OPML representation for the folder">
@ -130,7 +136,7 @@
<class name="article" code="Arcl" plural="articles" description="An article in a feed">
<cocoa class="ScriptableArticle"/>
<property name="id" code="id " type="text" access="r" description="The unique id of the article as set by the feed">
<property name="id" code="ID " type="text" access="r" description="The unique id of the article as set by the feed">
<cocoa key="uniqueId"/>
</property>
<property name="title" code="titl" type="text" access="r" description="The article title">

View File

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

View File

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

View File

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

View File

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

View File

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