Applescript command in document class - objective-c

I am trying to create an applescript command in my document class. I know I am doing something wrong, but I am not sure how to do this.
From what I understand (which may be incorrect), when I create a new command
I need to specify a new class for that command. But from that new class, lets call it
ScriptResetCommand, how do I access the document object from the performDefaultImplementation method? The applescript call is something like
tell document 1 of application "DocScript" to simple reset
Here is my current code:
ScriptResetCommand.m file:
#implementation ScriptResetCommand
- (id)performDefaultImplementation {
// Somehow I need to access the correct document class and
// perform my reset.
NSLog(#"ScriptResetCommand performDefaultImplementation");
return #"Reset Stuff";
}
ScriptResetCommand.h file:
#interface ScriptResetCommand : NSScriptCommand
- (id)performDefaultImplementation;
.sdef file:
<command name="simple reset" code="jDsgSrst" description="run a simple reset">
<cocoa class="ScriptResetCommand"/>
<result type="text" description="returns the result"/>
</command>
So with this code, I can successfully call the performDefaultImplementation method in the ScriptResetCommand class, but how do I then access the Document object which has the desired reset command in it?
Thanks in advance.

You should be able to access the document object using the method -[NSScriptCommand evaluatedReceivers]. Try something like this:
- (id)performDefaultImplementation {
NSDocument *document = [self.evaluatedReceivers firstObject];
if (![document isKindOfClass:[NSDocument class]]) {
// I'm just guessing how this error should be handled; untested (for example, you'll want to make sure your app returns an error if you run "reset stuff" without specifying a document)
[self setScriptErrorExpectedTypeDescriptor:[NSAppleEventDescriptor descriptorWithTypeCode:cDocument]];
return nil;
}
NSLog(#"Got document: %#", document);
return #"Reset Stuff";
}

I'm not sure what
tell document 1 of application "DocScript" to simple reset
would do, but to process it, break it down into a verb (simple reset) and a target (document 1 of application "DocScript"). When the command is executed, a reference to the target will be provided in [command directParameter], from which you can get an objectSpecifier for your document.
Here's how I implemented text selection in my scriptable text editor, Ted. The verb (command) is "select" and the target is "paragraph 5 of document 1".
tell application "Ted"
select paragraph 5 of document 1
end tell
SDEF: The command is defined in the Text Suite objects:
<suite name="Text Suite" code="????">
...
<class name="paragraph" code="cpar" inherits="item" >
...
<responds-to command="select">
<cocoa method="handleSelectCommand:"/>
</responds-to>
</class>
...
</suite>
The "select" command actually is included for EVERY class in the text suite that can be selected: e.g. paragraph, line, word, character.
Implementation:
Since all the various scriptable text objects are represented as NSTextStorage classes, I just use a category on NSTextStorage to implement the command.
#implementation NSTextStorage (Scriptability)
- (void) handleSelectCommand:(NSScriptCommand *)command
{
NSScriptObjectSpecifier *directParameter =[command directParameter];
NSScriptObjectSpecifier *container = [directParameter containerSpecifier];
TedDocument *document = [self extractDocument:container];
NSString *contents = document.textView.string;
NSRange selRange;
[self getRangeForSelectCommand:directParameter contents:contents range:&selRange];
[document.textView setSelectedRange:selRange];
}
#end
A reference to the document is provided by the directParameter, but it is part of a containment hierarchy, in this case 'paragraph 5 of document 1 of application "Ted"', so we need a method to walk that hierarchy to extract the document from it. Here is that function:
// Navigate the containment hierarch using recursion, if necessary.
- (TedDocument *) extractDocument:(NSScriptObjectSpecifier *)container
{
if (container == nil)
{
NSLog(#"%#", #"ERROR: extractDocument received nil container reference");
return nil;
}
FourCharCode containerClass = [[container keyClassDescription] appleEventCode];
if (containerClass != 'docu')
{
return [self extractDocument:[container containerSpecifier]];
}
return [container objectsByEvaluatingSpecifier];
}

Related

NSFileWrapper, lazy loading and saving

I have an NSDocument based application that uses filewrappers to save and load its data.
The document can have all kinds of resources, so I don't want to load everything into memory. I might be doing something fundamentally wrong, but as soon as I change one (inner) file and then save, I can't read any file that hasn't been loaded into memory.
I have separated the relevant code into a separate project to reproduce this behaviour, and I get the same results. The basic flow is this:
I load an existing document from disk. The main fileWrapper is a directory filewrapper (I'll call that main) containing two other filewrappers (sub1 and sub2). The two inner filewrappers are not loaded at this point.
When the user wants to edit sub1, it is loaded from disk.
The user saves the document
If the user wants to edit the other file (sub2), it cannot load. The error that appears:
-[NSFileWrapper regularFileContents] tried to read the file wrapper's contents lazily but an error occurred: The file couldn’t be opened because it doesn’t exist.
Here is the relevant code in my project:
This code might be easier to read in this gist:
https://gist.github.com/bob-codingdutchmen/6869871
#define FileName01 #"testfile1.txt"
#define FileName02 #"testfile2.txt"
/**
* Only called when initializing a NEW document
*/
-(id)initWithType:(NSString *)typeName error:(NSError *__autoreleasing *)outError {
self = [self init];
if (self) {
self.myWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:nil];
NSLog(#"Initializing new document...");
NSString *testString1 = #"Lorem ipsum first sub file";
NSString *testString2 = #"This is the second sub file with completely unrelated contents";
NSFileWrapper *w1 = [[NSFileWrapper alloc] initRegularFileWithContents:[testString1 dataUsingEncoding:NSUTF8StringEncoding]];
NSFileWrapper *w2 = [[NSFileWrapper alloc] initRegularFileWithContents:[testString2 dataUsingEncoding:NSUTF8StringEncoding]];
w1.preferredFilename = FileName01;
w2.preferredFilename = FileName02;
[self.myWrapper addFileWrapper:w1];
[self.myWrapper addFileWrapper:w2];
}
return self;
}
-(NSFileWrapper *)fileWrapperOfType:(NSString *)typeName error:(NSError *__autoreleasing *)outError {
// This obviously wouldn't happen here normally, but it illustrates
// how the contents of the first file would be replaced
NSFileWrapper *w1 = [self.myWrapper.fileWrappers objectForKey:FileName01];
[self.myWrapper removeFileWrapper:w1];
NSFileWrapper *new1 = [[NSFileWrapper alloc] initRegularFileWithContents:[#"New file contents" dataUsingEncoding:NSUTF8StringEncoding]];
new1.preferredFilename = FileName01;
[self.myWrapper addFileWrapper:new1];
return self.myWrapper;
}
-(BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError {
self.myWrapper = fileWrapper;
return YES;
}
- (IBAction)button1Pressed:(id)sender {
// Read from file1 and show result in field1
NSFileWrapper *w1 = [[self.myWrapper fileWrappers] objectForKey:FileName01];
NSString *string1 = [[NSString alloc] initWithData:w1.regularFileContents encoding:NSUTF8StringEncoding];
[self.field1 setStringValue:string1];
}
- (IBAction)button2Pressed:(id)sender {
// Read from file2 and show result in field2
NSFileWrapper *w2 = [[self.myWrapper fileWrappers] objectForKey:FileName02];
NSString *string2 = [[NSString alloc] initWithData:w2.regularFileContents encoding:NSUTF8StringEncoding];
[self.field2 setStringValue:string2];
}
The bottom two methods are only for updating the UI so I can see what happens.
To change the contents of a file, I remove the existing fileWrapper and add a new one. This is the only way I've found to change the contents of a file, and the way I've seen it done in other SO answers.
When a document is loaded from disk, I keep the fileWrapper around so I can use it (called myWrapper in the code above)
The Apple docs say that NSFileWrapper supports lazy loading and incremental saving, so I'm assuming that my code has some fundamental flaw that I can't see.
An NSFileWrapper is essentially a wrapper around a unix file node. If the file is moved the wrapper stays valid.
The problem yo seem to have is that creating a new file wrapper during saving is a new folder. And the system deletes your previous wrapper including sub2.
To achieve what you want you need to change to incremental saving, i.e. Only saving changed parts in place. See "save in place" in NSDocument.
In your -fileWrapperOfType:error: method, try building a new file wrapper that has new contents for the changed members and references the old file wrappers for the unchanged members.
Following the documentation to addFileWrapper: you add a child (subdirectory) to it, means
directory/
addfileWrapper:fileName1
directory/fileName1/
addfileWrapper:fileName2
directory/fileName1/fileName2.
That file doesn't exist.
You have to use
addRegularFileWithContents:preferredFilename:
instead.

Document based OSX app - Limit number of open documents to one

I'm trying to figure out how to limit my NSDocument based application to one open document at a time. It is quickly becoming a mess.
Has anyone been able to do this in a straightforward & reliable way?
////EDIT////
I would like to be able to prompt the user to save an existing open document and close it before creating/opening a new document.
////EDIT 2
I'm now trying to just return an error with an appropriate message if any documents are opening -- however, the error message is not displaying my NSLocalizedKeyDescription. This is in my NSDocumentController subclass.
-(id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError{
if([self.documents count]){
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithObject:#"Only one document can be open at a time. Please close your document." forKey:NSLocalizedDescriptionKey];
*outError = [NSError errorWithDomain:#"Error" code:192 userInfo:dict];
return nil;
}
return [super openUntitledDocumentAndDisplay:displayDocument error:outError];
}
It won't be an easy solution, since it's a pretty complex class, but I would suggest that you subclass NSDocumentController and register your own which disables opening beyond a certain number of documents. This will allow you to prevent things like opening files by dropping them on the application's icon in the dock or opening in the finder, both of which bypass the Open menu item.
You will still need to override the GUI/menu activation code to prevent Open... from being available when you have a document open already, but that's just to make sure you don't confuse the user.
Your document controller needs to be created before any other document controllers, but that's easy to do by placing a DocumentController instance in your MainMenu.xib and making sure the class is set to your subclass. (This will cause it to call -sharedDocumentController, which will create an instance of yours.)
In your document controller, then, you will need to override:
- makeDocumentForURL:withContentsOfURL:ofType:error:
- makeUntitledDocumentOfType:error:
- makeDocumentWithContentsOfURL:ofType:error:
to check and see if a document is already open and return nil, setting the error pointer to a newly created error that shows an appropriate message (NSLocalizedDescriptionKey).
That should take care of cases of drag-and-drop, applescript,etc.
EDIT
As for your additional request of the close/save prompt on an opening event, that's a nastier problem. You could:
Save off the information (basically the arguments for the make requests)
Send the -closeAllDocumentsWithDelegate:didCloseAllSelector:contextInfo: with self as a delegate and a newly-created routine as the selector
When you receive the selector, then either clear out the saved arguments, or re-execute the commands with the arguments you saved.
Note that step 2 and 3 might need to be done on delay with performSelector
I haven't tried this myself (the rest I've done before), but it seems like it should work.
Here's the solution I ended up with. All of this is in a NSDocumentController subclass.
- (NSInteger)runModalOpenPanel:(NSOpenPanel *)openPanel forTypes:(NSArray *)extensions{
[openPanel setAllowsMultipleSelection:NO];
return [super runModalOpenPanel:openPanel forTypes:extensions];
}
-(NSUInteger)maximumRecentDocumentCount{
return 0;
}
-(void)newDocument:(id)sender{
if ([self.documents count]) {
[super closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(newDocument:didCloseAll:contextInfo:) contextInfo:(void*)sender];
}
else{
[super newDocument:sender];
}
}
- (void)newDocument:(NSDocumentController *)docController didCloseAll: (BOOL)didCloseAll contextInfo:(void *)contextInfo{
if([self.documents count])return;
else [super newDocument:(__bridge id)contextInfo];
}
-(void)openDocument:(id)sender{
if ([self.documents count]) {
[super closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openDocument:didCloseAll:contextInfo:) contextInfo:(void*)sender];
}
else{
[super openDocument:sender];
}
}
- (void)openDocument:(NSDocumentController *)docController didCloseAll: (BOOL)didCloseAll contextInfo:(void *)contextInfo{
if([self.documents count])return;
else [super openDocument:(__bridge id)contextInfo];
}
Also, I unfortunately needed to remove the "Open Recent" option from the Main Menu. I haven't figured out how to get around that situation.

NSFilePresenter methods never get called

I'm trying to write a simple (toy) program that uses the NSFilePresenter and NSFileCoordinator methods to watch a file for changes.
The program consists of a text view that loads a (hardcoded) text file and a button that will save the file with any changes. The idea is that I have two instances running and saving in one instance will cause the other instance to reload the changed file.
Loading and saving the file works fine but the NSFilePresenter methods are never called. It is all based around a class called FileManager which implements the NSFilePresenter protocol. The code is as follows:
Interface:
#interface FileManager : NSObject <NSFilePresenter>
#property (unsafe_unretained) IBOutlet NSTextView *textView;
- (void) saveFile;
- (void) reloadFile;
#end
Implementation:
#implementation FileManager
{
NSOperationQueue* queue;
NSURL* fileURL;
}
- (id) init {
self = [super init];
if (self) {
self->queue = [NSOperationQueue new];
self->fileURL = [NSURL URLWithString:#"/Users/Jonathan/file.txt"];
[NSFileCoordinator addFilePresenter:self];
}
return self;
}
- (NSURL*) presentedItemURL {
NSLog(#"presentedItemURL");
return self->fileURL;
}
- (NSOperationQueue*) presentedItemOperationQueue {
NSLog(#"presentedItemOperationQueue");
return self->queue;
}
- (void) saveFile {
NSFileCoordinator* coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self];
NSError* error;
[coordinator coordinateWritingItemAtURL:self->fileURL options:NSFileCoordinatorWritingForMerging error:&error byAccessor:^(NSURL* url) {
NSString* content = [self.textView string];
[content writeToFile:[url path] atomically:YES encoding:NSUTF8StringEncoding error:NULL];
}];
}
- (void) reloadFile {
NSFileManager* fileManager = [NSFileManager defaultManager];
NSFileCoordinator* coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:self];
NSError* error;
__block NSData* content;
[coordinator coordinateReadingItemAtURL:self->fileURL options:0 error:&error byAccessor:^(NSURL* url) {
if ([fileManager fileExistsAtPath:[url path]]) {
content = [fileManager contentsAtPath:[url path]];
}
}];
dispatch_async(dispatch_get_main_queue(), ^{
[self.textView setString:[[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding]];
});
}
// After this I implement *every* method in the NSFilePresenter protocol. Each one
// simply logs its method name (so I can see it has been called) and calls reloadFile
// (not the correct implementation for all of them I know, but good enough for now).
#end
Note, reloadFile is called in applicationDidFinishLaunching and saveFile gets called every time the save button is click (via the app delegate).
The only NSFilePresenter method that ever gets called (going by the logs) is presentedItemURL (which gets called four times when the program starts and loads the file and three times whenever save is clicked. Clicking save in a second instance has no noticeable effect on the first instance.
Can anyone tell me what I'm doing wrong here?
I was struggling with this exact issue for quite a while. For me, the only method that would be called was -presentedSubitemDidChangeAtURL: (I was monitoring a directory rather than a file). I opened a technical support issue with Apple, and their response was that this is a bug, and the only thing we can do right now is to do everything through -presentedSubitemDidChangeAtURL: if you're monitoring a directory. Not sure what can be done when monitoring a file.
I would encourage anyone encountering this issue to file a bug (https://bugreport.apple.com) to encourage Apple to get this problem fixed as soon as possible.
(I realize that this is an old question, but... :) )
First of all, I notice you don't have [NSFileCoordinator removeFilePresenter:self]; anywhere (it should be in dealloc).
Secondly, you wrote:
// After this I implement *every* method in the NSFilePresenter protocol. Each one
// simply logs its method name (so I can see it has been called) and calls reloadFile
// (not the correct implementation for all of them I know, but good enough for now).
You're right: it's the incorrect implementation! And you're wrong: it's not good enough, because it's essential for methods like accommodatePresentedItemDeletionWithCompletionHandler: which take a completion block as a parameter, that you actually call this completion block whenever you implement them, e.g.
- (void) savePresentedItemChangesWithCompletionHandler:(void (^)(NSError * _Nullable))completionHandler
{
// implement your save routine here, but only if you need to!
if ( dataHasChanged ) [self save]; // <-- meta code
//
NSError * err = nil; // <-- = no error, in this simple implementation
completionHandler(err); // <-- essential!
}
I don't know whether this is the reason your protocol methods are not being called, but it's certainly a place to start. Well, assuming you haven't already worked out what was wrong in the past three years! :-)

Cocoa Mac Application Title says: "untitled"

I have created a document based Mac OSX application, and when I'm editing in Interface Builder, the title is correct (I filled out that portion of the inspector) but once the program runs, the application title is 'Untitled'. How can I change it? In my IB Doc Window, I have instances of Files Owner, First Responder, NSApplication, and NSWindow. There is no view controller, is that the issue? I'm new to Cocoa..
One solution is to override -displayName in your NSDocument subclass:
- (NSString *)displayName {
if (![self fileURL])
return #"Some custom untitled string";
return [super displayName];
}
You can also check out NSWindowController's -windowTitleForDocumentDisplayName: if you're using custom window controllers.
you have created a document based Cocoa application. For new documents, Cocoa sets the proposed name of the document to 'Untitled'.
That's because you checked Create Document-Based Application when you created this project:
You can remove it from info.plist by clicking the - button next to Document types:
Type in your own title in Storyboard and check the window to "is Inital Controller". After you run your project again, it will be OK.
Do you mean the application menu title? That is changed to match the name of the application at runtime. The simplest way to change it would be to change the Product Name build setting on your target in Xcode.
- (NSString *)displayName
{
NSMutableString *displayName = [NSMutableString stringWithString:[super displayName]];
if ([self fileURL] == nil) {
NSString *firstCharacter = [[displayName substringToIndex:1] lowercaseString];
[displayName deleteCharactersInRange:NSMakeRange(0, 1)];
[displayName insertString:firstCharacter atIndex:0];
}
return [NSString stringWithString:displayName];
}

How do I add Applescript support to my Cocoa application?

I am new to the world of Cocoa programming, and I want to add Applescript support to my application. The examples at Apple's website seem out of date.
How do I add Applescript support to my Cocoa application?
If you want to send AppleScript from your application and need a sandboxed app, you need to create a temporary entitlement
You need to add those two keys in your info.plist
<key>NSAppleScriptEnabled</key>
<true/>
<key>OSAScriptingDefinition</key>
<string>MyAppName.sdef</string>
...of course you have to change "MyAppName" to your app's name
Create a .sdef file and add it to your project.
The further course now greatly depends on the needs of your application, there are:
Class Elements (create an object from AppleScript)
Command Elements (override NSScriptCommand and execute "verb-like" commands)
Enumeration Elements
Record-Type Elements
Value-Type Elements (KVC)
Cocoa Elements
-
Go here to find a detailed description and many details on their implementation: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ScriptableCocoaApplications/SApps_script_cmds/SAppsScriptCmds.html
I found working with Class and KVC Elements very complicated, as I just wanted to execute a single command, nothing fancy. So in order to help others, here's an example of how to create a new simple command with one argument. In this example it'll "lookup" one string like this:
tell application "MyAppName"
lookup "some string"
end tell
The .sdef file for this command looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="MyAppName">
<suite name="MyAppName Suite" code="MApN" description="MyAppName Scripts">
<command name="lookup" code="lkpstrng" description="Look up a string, searches for an entry">
<cocoa class="MyLookupCommand"/>
<direct-parameter description="The string to lookup">
<type type="text"/>
</direct-parameter>
</command>
</suite>
</dictionary>
Create a subclass of NSScriptCommand and name it MyLookupCommand
The MyLookupCommand.h
#import <Foundation/Foundation.h>
#interface MyLookupCommand : NSScriptCommand
#end
The MyLookupCommand.m
#import "MyLookupCommand.h"
#implementation MyLookupCommand
-(id)performDefaultImplementation {
// get the arguments
NSDictionary *args = [self evaluatedArguments];
NSString *stringToSearch = #"";
if(args.count) {
stringToSearch = [args valueForKey:#""]; // get the direct argument
} else {
// raise error
[self setScriptErrorNumber:-50];
[self setScriptErrorString:#"Parameter Error: A Parameter is expected for the verb 'lookup' (You have to specify _what_ you want to lookup!)."];
}
// Implement your code logic (in this example, I'm just posting an internal notification)
[[NSNotificationCenter defaultCenter] postNotificationName:#"AppShouldLookupStringNotification" object:stringToSearch];
return nil;
}
#end
That's basically it. The secret to this is to subclass NSScriptCommand and override performDefaultImplementation. I hope this helps someone to get it faster...
Modern versions of Cocoa can directly interpret the scripting definition (.sdef) property list, so all you need to do for basic AppleScript support is to create the sdef per the docs, add it to your "copy bundle resources" phase and declare AppleScript support in your Info.plist. To access objects other than NSApp, you define object specifiers, so each object knows its position in the scripting world's hierarchy. That gets you kvc manipulation of object properties, and the ability to use object methods as simple script commands.
A simple example to get you started,
place a script (named dialog) into the documents folder then you can run it from Xcode
NSArray *arrayPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docDirectory = [arrayPaths objectAtIndex:0];
NSString *filePath = [docDirectory stringByAppendingString:#"/dialog.scpt"];
NSAppleScript *scriptObject = [[NSAppleScript alloc] initWithContentsOfURL:[NSURL fileURLWithPath:filePath] error:nil];
[scriptObject executeAndReturnError:nil];
The nice thing about keeping the script external is the ability to edit it outside of Xcode.
I would recommend adding the error checking if you did start editing as the applescript may not compile
maybe check with
if(scriptObject.isCompiled){