Support for creating and deleting feeds in accounts and folders, and

for creating and deleting folders in accounts
This commit is contained in:
Olof Hellman 2018-03-04 18:43:29 -08:00
parent 71005b56af
commit c48c54b468
12 changed files with 294 additions and 89 deletions

View File

@ -161,6 +161,7 @@
D5558FD32002245C0066386B /* ScriptingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5558FD22002245C0066386B /* ScriptingTests.swift */; };
D5558FD5200225680066386B /* NSAppleEventDescriptor+UserRecordFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5558FD4200225680066386B /* NSAppleEventDescriptor+UserRecordFields.swift */; };
D5558FD9200228D30066386B /* AppleEventUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5558FD7200228B80066386B /* AppleEventUtils.swift */; };
D57BE6E0204CD35F00D11AAC /* NSScriptCommand+Evergreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57BE6DF204CD35F00D11AAC /* NSScriptCommand+Evergreen.swift */; };
D5907CA0200232A1005947E5 /* testGenericScript.applescript in Sources */ = {isa = PBXBuildFile; fileRef = D5907C9D20023249005947E5 /* testGenericScript.applescript */; };
D5907CA1200232A1005947E5 /* testGetURL.applescript in Sources */ = {isa = PBXBuildFile; fileRef = D5558FD1200223F60066386B /* testGetURL.applescript */; };
D5907CA2200232AD005947E5 /* testGenericScript.applescript in CopyFiles */ = {isa = PBXBuildFile; fileRef = D5907C9D20023249005947E5 /* testGenericScript.applescript */; };
@ -174,6 +175,8 @@
D5A2679D201313A200A8D3C0 /* testNameOfAuthors.applescript in CopyFiles */ = {isa = PBXBuildFile; fileRef = D5A2679B201312F900A8D3C0 /* testNameOfAuthors.applescript */; };
D5A267C120131B8300A8D3C0 /* testFeedOPML.applescript in Sources */ = {isa = PBXBuildFile; fileRef = D5A267B220131B8300A8D3C0 /* testFeedOPML.applescript */; };
D5A267C220131BA000A8D3C0 /* testFeedOPML.applescript in CopyFiles */ = {isa = PBXBuildFile; fileRef = D5A267B220131B8300A8D3C0 /* testFeedOPML.applescript */; };
D5D07B18204B42050093F739 /* testIterativeCreateAndDeleteFeed.applescript in Sources */ = {isa = PBXBuildFile; fileRef = D5D07B09204B42050093F739 /* testIterativeCreateAndDeleteFeed.applescript */; };
D5D07B19204B423C0093F739 /* testIterativeCreateAndDeleteFeed.applescript in CopyFiles */ = {isa = PBXBuildFile; fileRef = D5D07B09204B42050093F739 /* testIterativeCreateAndDeleteFeed.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 */; };
@ -502,6 +505,7 @@
dstPath = TestScripts;
dstSubfolderSpec = 7;
files = (
D5D07B19204B423C0093F739 /* testIterativeCreateAndDeleteFeed.applescript in CopyFiles */,
D5E4CCDE20303A66009B4FFC /* testCurrentArticleIsNil.applescript in CopyFiles */,
D5E4CCDF20303A66009B4FFC /* testURLsOfCurrentArticle.applescript in CopyFiles */,
D5E4CCDD20303A59009B4FFC /* establishMainWindowStartingState.applescript in CopyFiles */,
@ -681,6 +685,7 @@
D5558FD22002245C0066386B /* ScriptingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ScriptingTests.swift; path = EvergreenTests/ScriptingTests/ScriptingTests.swift; sourceTree = SOURCE_ROOT; };
D5558FD4200225680066386B /* NSAppleEventDescriptor+UserRecordFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "NSAppleEventDescriptor+UserRecordFields.swift"; path = "AppleEvents/NSAppleEventDescriptor+UserRecordFields.swift"; sourceTree = SOURCE_ROOT; };
D5558FD7200228B80066386B /* AppleEventUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppleEventUtils.swift; sourceTree = "<group>"; };
D57BE6DF204CD35F00D11AAC /* NSScriptCommand+Evergreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScriptCommand+Evergreen.swift"; sourceTree = "<group>"; };
D5907C9D20023249005947E5 /* testGenericScript.applescript */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.applescript; path = testGenericScript.applescript; sourceTree = "<group>"; };
D5907CDC2002F0BE005947E5 /* Evergreen_project_release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Evergreen_project_release.xcconfig; sourceTree = "<group>"; };
D5907CDD2002F0BE005947E5 /* Evergreen_project_debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Evergreen_project_debug.xcconfig; sourceTree = "<group>"; };
@ -693,6 +698,7 @@
D5A2678B20130ECF00A8D3C0 /* Author+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Author+Scriptability.swift"; sourceTree = "<group>"; };
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>"; };
D5D07B09204B42050093F739 /* testIterativeCreateAndDeleteFeed.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = testIterativeCreateAndDeleteFeed.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>"; };
@ -1355,6 +1361,7 @@
D5A2679B201312F900A8D3C0 /* testNameOfAuthors.applescript */,
D5F4EDD720075C1300B9E363 /* testNameOfEveryFolder.applescript */,
D55373A02018797B006D8857 /* testTitleOfArticlesWhose.applescript */,
D5D07B09204B42050093F739 /* testIterativeCreateAndDeleteFeed.applescript */,
D5E4CCD12030260E009B4FFC /* testCurrentArticleIsNil.applescript */,
D5E4CCCF203025FF009B4FFC /* testURLsOfCurrentArticle.applescript */,
D5E4CCB220300024009B4FFC /* uiScriptingTestSetup.applescript */,
@ -1396,6 +1403,7 @@
D5907D7E2004AC00005947E5 /* NSApplication+Scriptability.swift */,
D5907DB12004BB37005947E5 /* ScriptingObjectContainer.swift */,
D5F4EDB4200744A700B9E363 /* ScriptingObject.swift */,
D57BE6DF204CD35F00D11AAC /* NSScriptCommand+Evergreen.swift */,
);
name = Scriptability;
path = Evergreen/Scriptability;
@ -1535,12 +1543,12 @@
};
849C645F1ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = M8L2WTLA8W;
ProvisioningStyle = Manual;
DevelopmentTeam = 6V7D786XTL;
ProvisioningStyle = Automatic;
};
849C64701ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = 9C84TZ7Q6Z;
DevelopmentTeam = 6V7D786XTL;
ProvisioningStyle = Automatic;
TestTargetID = 849C645F1ED37A5D003D8FC0;
};
@ -1925,6 +1933,7 @@
849A97641ED9EB96007D329B /* SidebarOutlineView.swift in Sources */,
D5A2678C20130ECF00A8D3C0 /* Author+Scriptability.swift in Sources */,
84F2D5371FC22FCC00998D64 /* PseudoFeed.swift in Sources */,
D57BE6E0204CD35F00D11AAC /* NSScriptCommand+Evergreen.swift in Sources */,
D553738B20186C20006D8857 /* Article+Scriptability.swift in Sources */,
845EE7C11FC2488C00854A1F /* SmartFeed.swift in Sources */,
84702AA41FA27AC0006B8943 /* MarkStatusCommand.swift in Sources */,
@ -2018,6 +2027,7 @@
D5558FD9200228D30066386B /* AppleEventUtils.swift in Sources */,
D55373AF2018797C006D8857 /* testTitleOfArticlesWhose.applescript in Sources */,
D5907CA1200232A1005947E5 /* testGetURL.applescript in Sources */,
D5D07B18204B42050093F739 /* testIterativeCreateAndDeleteFeed.applescript in Sources */,
D5A267C120131B8300A8D3C0 /* testFeedOPML.applescript in Sources */,
D5A2679C201312F900A8D3C0 /* testNameOfAuthors.applescript in Sources */,
849C64761ED37A5D003D8FC0 /* EvergreenTests.swift in Sources */,

View File

@ -7,6 +7,11 @@
<suite name="Standard Suite" code="core" description="Subset of the Standard Suite.">
<access-group identifier="com.ranchero.Evergreen" access="rw"/>
<command name="delete" code="coredelo" description="Delete an object.">
<cocoa class="Evergreen.EvergreenDeleteCommand"/>
<direct-parameter type="specifier" description="The object(s) to delete."/>
</command>
<command name="exists" code="coredoex" description="Verify that an object exists.">
<cocoa class="Evergreen.EvergreenExistsCommand"/>
<direct-parameter type="any" requires-access="r" description="The object(s) to check."/>
@ -15,7 +20,6 @@
<command name="make" code="corecrel" description="Create a new object.">
<cocoa class="Evergreen.EvergreenCreateElementCommand"/>
<access-group identifier="*"/>
<parameter name="new" code="kocl" type="type" description="The class of the new object.">
<cocoa key="ObjectClass"/>
</parameter>
@ -158,6 +162,9 @@
<property name="opml representation" code="OPML" type="text" access="r" description="OPML representation for the folder">
<cocoa key="opmlRepresentation"/>
</property>
<element type="feed">
<cocoa key="feeds"/>
</element>
</class>
<class name="article" code="Arcl" plural="articles" description="An article in a feed">

View File

@ -9,6 +9,7 @@
import AppKit
import Account
import Data
import RSCore
@objc(ScriptableAccount)
class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
@ -46,6 +47,18 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
return self.classDescription as! NSScriptClassDescription
}
func deleteElement(_ element:ScriptingObject) {
if let scriptableFolder = element as? ScriptableFolder {
BatchUpdate.shared.perform {
account.deleteFolder(scriptableFolder.folder)
}
} else if let scriptableFeed = element as? ScriptableFeed {
BatchUpdate.shared.perform {
account.deleteFeed(scriptableFeed.feed)
}
}
}
@objc(isLocationRequiredToCreateForKey:)
func isLocationRequiredToCreate(forKey key:String) -> Bool {
return false;
@ -66,6 +79,13 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
return ScriptableFeed(feed, container:self)
}
@objc(valueInFeedsWithName:)
func valueInFeeds(withName name:String) -> ScriptableFeed? {
let feeds = account.children.compactMap { $0 as? Feed }
guard let feed = feeds.first(where:{$0.name == name}) else { return nil }
return ScriptableFeed(feed, container:self)
}
@objc(folders)
var folders:NSArray {
let folders = account.children.compactMap { $0 as? Folder }

View File

@ -61,6 +61,71 @@ class EvergreenCreateElementCommand : NSCreateCommand {
let classDescription = self.createClassDescription
if (classDescription.className == "feed") {
return ScriptableFeed.handleCreateElement(command:self)
} else if (classDescription.className == "folder") {
return ScriptableFolder.handleCreateElement(command:self)
}
return nil
}
}
/*
NSDeleteCommand is kind of an oddball AppleScript command in that the command dispatch
goes to the container of the object(s) to be deleted, and the container needs to
figure out what to delete. In the code below, 'receivers' is the container object(s)
and keySpecifier is the thing to delete, relative to the container(s). Because there
is ambiguity about whether specifiers are lists or single objects, the code switches
based on which it is.
*/
class EvergreenDeleteCommand : NSDeleteCommand {
/*
delete(objectToDelete:, from container:)
At this point in handling the command, we know what the container is.
Here the code unravels the case of objectToDelete being a list or a single object,
ultimately calling container.deleteElement(element) for each element to delete
*/
func delete(objectToDelete:Any, from container:ScriptingObjectContainer) {
if let objectList = objectToDelete as? [Any] {
for nthObject in objectList {
self.delete(objectToDelete:nthObject, from:container)
}
} else if let element = objectToDelete as? ScriptingObject {
container.deleteElement(element)
}
}
/*
delete(specifier:, from container:)
At this point in handling the command, the container could be a list or a single object,
and what to delete is still an unresolved NSScriptObjectSpecifier.
Here the code unravels the case of container being a list or a single object. Once the
container(s) is known, it is possible to resolve the keySpecifier based on that container.
After resolving, we call delete(objectToDelete:, from container:) with the container and
the resolved objects
*/
func delete(specifier:NSScriptObjectSpecifier, from container:Any) {
if let containerList = container as? [Any] {
for nthObject in containerList {
self.delete(specifier:specifier, from:nthObject)
}
} else if let container = container as? ScriptingObjectContainer {
if let resolvedObjects = specifier.objectsByEvaluating(withContainers:container) {
self.delete(objectToDelete:resolvedObjects, from:container)
}
}
}
/*
performDefaultImplementation()
This is where handling the delete event starts. receiversSpecifier should be the container(s) of
the item to be deleted. keySpecifier is the thing in that container(s) to be deleted
The first step is to resolve the receiversSpecifier and then call delete(specifier:, from container:)
*/
override func performDefaultImplementation() -> Any? {
if let receiversSpecifier = self.receiversSpecifier {
if let receiverObjects = receiversSpecifier.objectsByEvaluatingSpecifier {
self.delete(specifier:self.keySpecifier, from:receiverObjects)
}
}
return nil
}
@ -81,8 +146,6 @@ class EvergreenExistsCommand : NSExistsCommand {
override func performDefaultImplementation() -> Any? {
guard let result = super.performDefaultImplementation() else { return NSNumber(booleanLiteral:false) }
return result
// return NSNumber(booleanLiteral:true)
// scriptingContainer.handleDoObjectsExist(command:self)
}
}

View File

@ -47,6 +47,10 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
var scriptingClassDescription: NSScriptClassDescription {
return self.classDescription as! NSScriptClassDescription
}
func deleteElement(_ element:ScriptingObject) {
print ("delete event not handled")
}
// MARK: --- Scriptable properties ---

View File

@ -54,8 +54,10 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
return self.classDescription as! NSScriptClassDescription
}
func deleteElement(_ element:ScriptingObject) {
}
// MARK: --- Create Element Handlers ---
// MARK: --- handle NSCreateCommand ---
class func parsedFeedForURL(_ urlString:String, _ completionHandler: @escaping (_ parsedFeed: ParsedFeed?) -> Void) {
guard let url = URL(string: urlString) else {
@ -79,105 +81,51 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
return url
}
class func accountAndFolderForNewFeed(appleEvent:NSAppleEventDescriptor?) -> (Account, Folder?) {
var account = AccountManager.shared.localAccount
var folder:Folder? = nil
if let appleEvent = appleEvent {
var descriptorToConsider:NSAppleEventDescriptor?
if let insertionLocationDescriptor = appleEvent.paramDescriptor(forKeyword:keyAEInsertHere) {
print("insertionLocation : \(insertionLocationDescriptor)")
// insertion location can be a typeObjectSpecifier, e.g. 'in account "Acct"'
// or a typeInsertionLocation, e.g. 'at end of folder "
if (insertionLocationDescriptor.descriptorType == "insl".FourCharCode()) {
descriptorToConsider = insertionLocationDescriptor.forKeyword("kobj".FourCharCode())
} else if ( insertionLocationDescriptor.descriptorType == "obj ".FourCharCode()) {
descriptorToConsider = insertionLocationDescriptor
}
} else if let subjectDescriptor = appleEvent.attributeDescriptor(forKeyword:"subj".FourCharCode()) {
descriptorToConsider = subjectDescriptor
}
if let descriptorToConsider = descriptorToConsider {
guard let newContainerSpecifier = NSScriptObjectSpecifier(descriptor:descriptorToConsider) else {return (account, folder)}
let newContainer = newContainerSpecifier.objectsByEvaluatingSpecifier
if let scriptableAccount = newContainer as? ScriptableAccount {
account = scriptableAccount.account
} else if let scriptableFolder = newContainer as? ScriptableFolder {
if let folderAccount = scriptableFolder.folder.account {
folder = scriptableFolder.folder
account = folderAccount
}
}
}
print("found account : \(account)")
print("found folder : \(folder)")
class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableFeed {
let scriptableAccount = ScriptableAccount(account)
if let folder = folder {
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)
return ScriptableFeed(feed, container:scriptableFolder)
} else {
return ScriptableFeed(feed, container:scriptableAccount)
}
return (account, folder)
}
class func handleCreateElement(command:NSCreateCommand) -> Any? {
let appleEventManager = NSAppleEventManager.shared()
if let receivers = command.receiversSpecifier {
print("receivers : \(receivers)")
}
if let evaluatedReceivers = command.evaluatedReceivers {
print("evaluatedReceivers : \(evaluatedReceivers)")
}
if let evaluatedArguments = command.evaluatedArguments {
print("evaluatedArguments : \(evaluatedArguments)")
}
if let directObject = command.directParameter {
print("directObject : \(directObject)")
}
if let appleEvent = command.appleEvent { // keyDirectObject
print("appleEvent : \(appleEvent)")
if let subjectDescriptor = appleEvent.attributeDescriptor(forKeyword:"subj".FourCharCode()) {
print("subjectDescriptor : \(subjectDescriptor)")
let subjectObjectSpecifier = NSScriptObjectSpecifier(descriptor:subjectDescriptor)
let subjects = subjectObjectSpecifier?.objectsByEvaluatingSpecifier
print("resolvedSubjects : \(subjects)")
}
}
let commandDescription = command.commandDescription
print("commandDescription : \(commandDescription)")
let (account, folder) = self.accountAndFolderForNewFeed(appleEvent:command.appleEvent)
let scriptableAccount = ScriptableAccount(account)
guard command.isCreateCommand(forClass:"Feed") else { return nil }
guard let arguments = command.arguments else {return nil}
guard let newObjectClass = arguments["ObjectClass"] as? Int else {return nil}
guard (newObjectClass.FourCharCode() == "Feed".FourCharCode()) else {return nil}
let titleFromArgs = command.property(forKey:"name") as? String
let (account, folder) = command.accountAndFolderForNewChild()
guard let url = self.urlForNewFeed(arguments:arguments) else {return nil}
if let existingFeed = account.existingFeed(withURL:url) {
return ScriptableFeed(existingFeed, container:scriptableAccount)
return self.scriptableFeed(existingFeed, account:account, folder:folder)
}
// at this point, we have to download the feed and parse it.
// RS Parser does the callback for the download on the main thread
// at this point, we need to download the feed and parse it.
// RS Parser does the callback for the download on the main thread (which it probably shouldn't?)
// because we can't wait here (on the main thread, maybe) for the callback, we have to return from this function
// generally, the means handling the appleEvent is over, but to prevent the apple event from returning
// we call suspendExecution here. When we get the callback, we can resume execution
// Generally, returning from an AppleEvent handler function means that handling the appleEvent is over,
// but we don't yet have the result of the event yet, so we prevent the AppleEvent from returning by calling
// suspendExecution(). When we get the callback, we can supply the event result and call resumeExecution()
command.suspendExecution()
self.parsedFeedForURL(url, { (parsedFeedOptional) in
if let parsedFeed = parsedFeedOptional {
let titleFromFeed = parsedFeed.title
let titleFromArgs = arguments["name"] as? String
guard let feed = account.createFeed(with: titleFromFeed, editedName: titleFromArgs, url: url) else {
command.resumeExecution(withResult:nil)
return
}
account.update(feed, with:parsedFeed, {})
// add the feed, puttin git in a folder if needed
if account.addFeed(feed, to: folder) {
// add the feed, putting it in a folder if needed
if account.addFeed(feed, to:folder) {
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
}
let resolvedKeyDictionary = command.resolvedKeyDictionary
print("resolvedKeyDictionary : \(resolvedKeyDictionary)")
let scriptableFeed = ScriptableFeed(feed, container:ScriptableAccount(account))
let scriptableFeed = self.scriptableFeed(feed, account:account, folder:folder)
command.resumeExecution(withResult:scriptableFeed.objectSpecifier)
} else {
command.resumeExecution(withResult:nil)
@ -185,12 +133,9 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
})
return nil
}
// MARK: --- Scriptable properties ---
@objc(url)
var url:String {
return self.feed.url

View File

@ -8,9 +8,11 @@
import Foundation
import Account
import Data
import RSCore
@objc(ScriptableFolder)
class ScriptableFolder: NSObject, UniqueIdScriptingObject {
class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let folder:Folder
let container:ScriptingObjectContainer
@ -41,6 +43,58 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject {
return folder.folderID
}
// MARK: --- ScriptingObjectContainer protocol ---
var scriptingClassDescription: NSScriptClassDescription {
return self.classDescription as! NSScriptClassDescription
}
func deleteElement(_ element:ScriptingObject) {
if let scriptableFolder = element as? ScriptableFolder {
BatchUpdate.shared.perform {
folder.deleteFolder(scriptableFolder.folder)
}
} else if let scriptableFeed = element as? ScriptableFeed {
BatchUpdate.shared.perform {
folder.deleteFeed(scriptableFeed.feed)
}
}
}
// MARK: --- handle NSCreateCommand ---
/*
handle an AppleScript like
make new folder in account X with properties {name:"new folder name"}
or
tell account X to make new folder at end with properties {name:"new folder name"}
*/
class func handleCreateElement(command:NSCreateCommand) -> Any? {
guard command.isCreateCommand(forClass:"fold") else { return nil }
let name = command.property(forKey:"name") as? String ?? ""
// some combination of the tell target and the location specifier ("in" or "at")
// identifies where the new folder should be created
let (account, folder) = command.accountAndFolderForNewChild()
guard folder == nil else {
print("support for folders within folders is NYI");
return nil
}
let scriptableAccount = ScriptableAccount(account)
if let newFolder = account.ensureFolder(with:name) {
let scriptableFolder = ScriptableFolder(newFolder, container:scriptableAccount)
return(scriptableFolder.objectSpecifier)
}
return nil
}
// MARK: --- Scriptable elements ---
@objc(feeds)
var feeds:NSArray {
let feeds = folder.children.compactMap { $0 as? Feed }
return feeds.map { ScriptableFeed($0, container:self) } as NSArray
}
// MARK: --- Scriptable properties ---
@objc(uniqueId)

View File

@ -11,11 +11,17 @@ import Account
import Data
extension NSApplication : ScriptingObjectContainer {
// MARK: --- ScriptingObjectContainer protocol ---
var scriptingClassDescription: NSScriptClassDescription {
return NSApplication.shared.classDescription as! NSScriptClassDescription
}
func deleteElement(_ element:ScriptingObject) {
print ("delete event not handled")
}
var scriptingKey: String {
return "application"
}

View File

@ -0,0 +1,63 @@
//
// NSScriptCommand+Evergreen.swift
// Evergreen
//
// Created by Olof Hellman on 3/4/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import Foundation
import Account
extension NSScriptCommand {
func property(forKey key:String) -> Any? {
if let evaluatedArguments = self.evaluatedArguments {
if let props = evaluatedArguments["KeyDictionary"] as? [String: Any] {
return props[key]
}
}
return nil
}
func isCreateCommand(forClass whatClass:String) -> Bool {
guard let arguments = self.arguments else {return false}
guard let newObjectClass = arguments["ObjectClass"] as? Int else {return false}
guard (newObjectClass.FourCharCode() == whatClass.FourCharCode()) else {return false}
return true
}
func accountAndFolderForNewChild() -> (Account, Folder?) {
let appleEvent = self.appleEvent
var account = AccountManager.shared.localAccount
var folder:Folder? = nil
if let appleEvent = appleEvent {
var descriptorToConsider:NSAppleEventDescriptor?
if let insertionLocationDescriptor = appleEvent.paramDescriptor(forKeyword:keyAEInsertHere) {
print("insertionLocation : \(insertionLocationDescriptor)")
// insertion location can be a typeObjectSpecifier, e.g. 'in account "Acct"'
// or a typeInsertionLocation, e.g. 'at end of folder "
if (insertionLocationDescriptor.descriptorType == "insl".FourCharCode()) {
descriptorToConsider = insertionLocationDescriptor.forKeyword("kobj".FourCharCode())
} else if ( insertionLocationDescriptor.descriptorType == "obj ".FourCharCode()) {
descriptorToConsider = insertionLocationDescriptor
}
} else if let subjectDescriptor = appleEvent.attributeDescriptor(forKeyword:"subj".FourCharCode()) {
descriptorToConsider = subjectDescriptor
}
if let descriptorToConsider = descriptorToConsider {
guard let newContainerSpecifier = NSScriptObjectSpecifier(descriptor:descriptorToConsider) else {return (account, folder)}
let newContainer = newContainerSpecifier.objectsByEvaluatingSpecifier
if let scriptableAccount = newContainer as? ScriptableAccount {
account = scriptableAccount.account
} else if let scriptableFolder = newContainer as? ScriptableFolder {
if let folderAccount = scriptableFolder.folder.account {
folder = scriptableFolder.folder
account = folderAccount
}
}
}
}
return (account, folder)
}
}

View File

@ -7,9 +7,11 @@
//
import AppKit
import Account
protocol ScriptingObjectContainer: ScriptingObject {
var scriptingClassDescription:NSScriptClassDescription { get }
func deleteElement(_ element:ScriptingObject)
}
extension ScriptingObjectContainer {

View File

@ -56,6 +56,10 @@ class ScriptingTests: AppleScriptXCTestCase {
_ = doIndividualScript(filename: "testTitleOfArticlesWhose")
}
func testIterativeCreateAndDeleteScript() {
_ = doIndividualScriptWithExpectation(filename: "testIterativeCreateAndDeleteFeed")
}
func doIndividualScriptWithExpectation(filename:String) {
let queue = DispatchQueue(label:"testQueue")
let scriptExpectation = self.expectation(description: filename+"expectation")

View File

@ -0,0 +1,27 @@
try
tell application "Evergreen"
tell account id "OnMyMac"
repeat 3 times
set newFeed to make new feed with data "https://boingboing.net/feed"
delete newFeed
end repeat
set newFolder to make new folder with properties {name:"XCTest folder"}
repeat 3 times
set newFeed to make new feed in newFolder with data "https://boingboing.net/feed"
delete newFeed
end repeat
delete newFolder
end tell
end tell
set test_result to true
set script_result to "Success"
on error message
return {test_result:false, script_result:message}
end try
return {test_result:test_result, script_result:script_result}