📜 ⬆️ ⬇️

"Eat me" ... no, not so ... "Fulfill me"!

I periodically have different needs to solve small urgent tasks in Mac OS X. For these purposes, I usually do small programs that "close" the need in private. But sometimes you want the program to be universal, and other people could use it if necessary (for example, “Pellator” ). It happened this time too ...

I love it when the Dock is always displayed on the screen. But when launching the iOS Simulator, it was constantly necessary to turn on automatic hiding so that the simulator fully fit on the screen. There was a task - to automate this process. For a couple of days, I sketched a universal program with which you can set AppleScript for a specific action of any program: “The program is running”, “The program is completed”, “The program is activated”, “The program is deactivated”, etc.


')

I will divide this topic into two parts. One for users who just want to use the program (or get acquainted). The second for novice developers - I will describe the scheme of the program and provide the source code. Despite the seeming simplicity of the program itself, the source code covers many nuances from various topics that can save a significant amount of time in the future.

You can download the program via the link (only for 10.6+) (at 18.51 I updated the program and source codes thanks to a bug report. I fixed a small bug that prevented the assistant from receiving messages after removing the program from the list. Handicraft testing costs ...) .

The program is very easy to use (if you have the skills to work with AppleScript). This is not a separate application, but a panel for System Settings. Double-click on it with the mouse and get a new "Run me" panel in the "System Settings". On the left is a list of programs. And in the right scripts specified for the selected program.

When you click on the "+" button , a list of running programs (with an identifier - Bundle identifier) ​​will open, from which we can select the desired program to add (if the required program is not running, then it must first be started).


All that remains is to set the scripts for the actions that are required. For example, to enable the Dock auto-hide mode:
tell application "System Events" to set the autohide of the dock preferences to true

Or run some program:
tell application "iTunes" to activate

Or execute some console script:
do shell script "…"

The possibilities are virtually unlimited - AppleScript allows you to do a lot.

In fact, the program consists of two parts. Panel system settings. And a special hidden assistant program - “Run me (assistant)”, which is automatically registered in autoload. It is so miserable that it actually does not consume system resources, but it is she who is responsible for the execution of scripts written in the settings panel.

To remove a program, simply right-click (or Ctrl + click / touch) the name of the “Follow me” panel and select “Delete panel”.


FOR BEGINNING DEVELOPERS

Here is a link to the source code of the project.
Here is an option on github .

The program consists of two parts - the settings panel and the assistant program. For each of them I made a separate project (in this case it was more convenient for me). If necessary, two projects can be combined into one with two targets.

Creating a panel for "System Settings" is not much different from creating a regular program. Xcode already has a template for this System Plug-in> Preference Pane. In which a class based on an NSPreferencePane and an interface (xib) file have already been added. Everyone decides for himself how to test this module. For example, you can do everything in the form of a normal program and only at the final stage to transfer everything to the Preference Pane.

It is important to understand that this is not an independent program, but a module for "System Settings". This means that all methods that are tied to the main bundle will not be correctly executed (since the main bundle is the “System Settings”).

We cannot use the following macro:
NSLocalizedString ( NSString * key, NSString * comment )

Need to use:
NSLocalizedStringFromTableInBundle ( NSString * key, NSString * tableName, NSBundle * bundle, NSString * comment )

Or we cannot use the NSImage method:
+ ( id ) imageNamed : ( NSString * ) name

You need to use, for example:
- ( id ) initWithContentsOfFile : ( NSString * ) filename

Etc.

An object of class NSPreferencePane has a method:
- ( NSBundle * ) bundle

His and need to be used.

The project uses two buttons "+" and "-" to edit the list of programs.

Initialization of the delete button is trivial and simple:
removeButton = [ [ NSButton alloc ] initWithFrame : NSMakeRect ( 43 , 19 , 22 , 22 ) ] ;<br/>
[ removeButton setButtonType : NSMomentaryChangeButton ] ;<br/>
[ removeButton setImage : buttonImage ] ;<br/>
[ removeButton setImagePosition : NSImageOnly ] ;<br/>
[ removeButton setBordered : NO ] ;<br/>
[ removeButton setTarget : self ] ;<br/>
[ removeButton setAction : @selector ( removeButtonAction : ) ] ;<br/>
[ [ self mainView ] addSubview : removeButton ] ;

But the add button is more complicated. For it, you must enter two subclasses of NSPopUpButton and NSPopUpButtonCell. Formally, the NSPopUpButton class serves as a drop-down button. But in the nude, it does not suit us. Firstly, it does not allow to set a static image for the button (more precisely, it allows, but the actual size of the button does not correspond to the actual size of the static image, which is unacceptable in our case, since we have two buttons next to each other) - we are going around with the help of the subclass NSPopUpButtonCell. Secondly, NSPopUpButton does not allow to dynamically change the contents of the menu at the moment of its pressing (our menu should be formed exactly at the moment of pressing the button) - we are going around it using the subclass NSPopUpButton.

In order for the button to have the size of one to one with a given picture, create the class PopUpCell:
@interface PopUpCell : NSPopUpButtonCell <br/>
{ <br/>
NSButtonCell * buttonCell;<br/>
} <br/>
<br/>
- ( id ) initWithimage : ( NSImage * ) image;<br/>
<br/>
@end <br/>
<br/>
…<br/>
<br/>
@implementation PopUpCell<br/>
<br/>
- ( id ) initWithimage : ( NSImage * ) image<br/>
{ <br/>
self = [ super initTextCell : @ "" pullsDown : YES ] ;<br/>
<br/>
buttonCell = [ [ NSButtonCell alloc ] initImageCell : image ] ;<br/>
[ buttonCell setButtonType : NSPushOnPushOffButton ] ;<br/>
[ buttonCell setImagePosition : NSImageOnly ] ;<br/>
[ buttonCell setImageDimsWhenDisabled : YES ] ;<br/>
[ buttonCell setBordered : NO ] ;<br/>
<br/>
return self;<br/>
} <br/>
<br/>
...<br/>
<br/>
- ( void ) drawWithFrame : ( NSRect ) cellFrame inView : ( NSView * ) controlView<br/>
{ <br/>
[ buttonCell drawWithFrame : cellFrame inView : controlView ] ;<br/>
} <br/>
<br/>
- ( void ) highlight : ( BOOL ) flag withFrame : ( NSRect ) cellFrame inView : ( NSView * ) controlView<br/>
{ <br/>
[ buttonCell highlight : flag withFrame : cellFrame inView : controlView ] ;<br/>
} <br/>
<br/>
@end

What does all this code mean? It's simple - we create an object of class NSButtonCell and draw it instead of NSPopUpButtonCell. Those. the program draws an NSButtonCell (the size of which can correspond one-on-one with a given picture) instead of NSPopUpButtonCell, but functionally it is NSPopUpButton.

Now the menu ... With the dynamic menu, we proceed as follows - add a delegate to the NSPopUpButton, which will give us the menu at the moment when the left mouse button on the button is pressed.

Create a subclass of NSPopUpButton:
@protocol PopUpDelegate <NSObject><br/>
@optional<br/>
- ( NSMenu * ) menuForPopUp;<br/>
@end <br/>
<br/>
<br/>
@interface PopUpButton : NSPopUpButton <br/>
{ <br/>
id<PopUpDelegate> delegate; <br/>
} <br/>
<br/>
@property ( assign ) id delegate;<br/>
<br/>
@end <br/>

So we declared the delegate protocol, which has only one method:
- ( NSMenu * ) menuForPopUp<br/>

Implementation:
@implementation PopUpButton<br/>
<br/>
@synthesize delegate;<br/>
<br/>
- ( void ) mouseDown : ( NSEvent * ) event<br/>
{ <br/>
if ( [ delegate respondsToSelector : @selector ( menuForPopUp ) ] ) <br/>
{ <br/>
[ self setMenu : [ delegate menuForPopUp ] ] ;<br/>
} <br/>
<br/>
[ super mouseDown : event ] ;<br/>
} <br/>
<br/>
@end <br/>


When you click the mouse, before the event starts being processed by an object of the NSPopUpButton class, we set the delegate menu for this object.

Now you can add our "+" button to our panel:
addButton = [ [ PopUpButton alloc ] initWithFrame : NSMakeRect ( 20 , 19 , 23 , 22 ) pullsDown : YES ] ;<br/>
addButton.delegate = self;<br/>
[ addButton setCell : [ [ [ PopUpCell alloc ] initWithimage : buttonImage ] autorelease ] ] ;<br/>
[ addButton setMenu : [ self menuForPopUp ] ] ;<br/>
[ [ self mainView ] addSubview : addButton ] ;

Note the “addButton.delegate = self”. We have designated our main class as a delegate to addButton. To do this, we additionally implement the method:
- ( NSMenu * ) menuForPopUp<br/>
{ <br/>
...<br/>
}

in our main class.

How to get a list of running programs for the menu?

An object of class NSWorkspace has a method:
- ( NSArray * ) runningApplications

which will give us an array of all running programs (with full information on them: name, identifier, icon, etc.).
NSArray * apps = [ [ NSWorkspace sharedWorkspace ] runningApplications ] ;

From this apps object, the menu for the add button will be formed (see the source code).

Our NSTableView object displays cells with a picture, a caption, and a subtitle. There is no standard cell of this type, it needs to be done independently.

We declare the class AppCell:
@interface AppCell : NSTextFieldCell <br/>
{ <br/>
NSImage * image;<br/>
NSString * title;<br/>
NSString * subtitle;<br/>
} <br/>
...<br/>
@end <br/>

The most important thing is to reassign the cell's drawing method:
- ( void ) drawInteriorWithFrame : ( NSRect ) inCellFrame inView : ( NSView * ) inView<br/>
{ <br/>
// image <br/>
// title <br/>
// subtitle <br/>
…<br/>
} <br/>

The cell class is ready, it only remains to add it to our NSTableView object. There are two ways.

one). If the number of objects in the table is small, then you can use the delagate method (which is our main class NSPreferencePane) to the NSTableView:
- ( NSCell * ) tableView : ( NSTableView * ) tableView dataCellForTableColumn : ( NSTableColumn * ) tableColumn row : ( NSInteger ) row<br/>
{ <br/>
AppCell * cell = [ [ [ AppCell alloc ] init ] autorelease ] ;<br/>
[ cell setEditable : NO ] ;<br/>
cell.title = [ [ tableDataSource objectAtIndex : row ] objectForKey : @ "name" ] ;<br/>
cell.subtitle = [ [ tableDataSource objectAtIndex : row ] objectForKey : @ "ID" ] ;<br/>
cell.image = [ [ tableDataSource objectAtIndex : row ] objectForKey : @ "icon" ] ;<br/>
<br/>
return cell;<br/>
}

2). If there are many objects, then you can define a universal cell for the entire column:
NSTableColumn * column = [ [ appTable tableColumns ] objectAtIndex : 0 ] ;
[ column setDataCell : [ [ [ AppCell alloc ] init ] autorelease ] ] ;

and set the required values ​​in the delegate method to the NSTableView:
- ( void ) tableView : ( NSTableView * ) aTableView willDisplayCell : ( id ) aCell forTableColumn : ( NSTableColumn * ) aTableColumn row : ( NSInteger ) rowIndex<br/>
{ <br/>
AppCell * cell = ( AppCell * ) aCell;<br/>
[ cell setEditable : NO ] ;<br/>
cell.title = [ [ tableDataSource objectAtIndex : row ] objectForKey : @ "name" ] ;<br/>
cell.subtitle = [ [ tableDataSource objectAtIndex : row ] objectForKey : @ "ID" ] ;<br/>
cell.image = [ [ tableDataSource objectAtIndex : row ] objectForKey : @ "icon" ] ;<br/>
}


The data (file with a list of programs and scripts and icons) are saved in the appropriate folder Application Support (user library). You can (and should) get the path to this folder as follows:
NSArray * paths = NSSearchPathForDirectoriesInDomains ( NSApplicationSupportDirectory, NSUserDomainMask, YES ) ; <br/>
NSString * appSupportPath = [ paths objectAtIndex : 0 ] ;<br/>
NSString * prefDir = [ appSupportPath stringByAppendingPathComponent : @ "info.yuriev.OnAppBehaviour" ] ;

Icons we will store in PNG format. There are several ways to get PNG data from NSImage, I will give one of them (the most universal), which is used in the program. For the NSImage class, we will introduce a new method. Here is what its implementation looks like:
@implementation NSImage ( PNGExport ) <br/>
<br/>
- ( NSData * ) PNGData<br/>
{ <br/>
[ self lockFocus ] ;<br/>
NSBitmapImageRep * rep = [ [ NSBitmapImageRep alloc ] initWithFocusedViewRect : NSMakeRect ( 0 , 0 , [ self size ] .width, [ self size ] .height ) ] ;<br/>
[ self unlockFocus ] ;<br/>
<br/>
NSData * PNGData = [ rep representationUsingType : NSPNGFileType properties : nil ] ;<br/>
[ rep release ] ;<br/>
<br/>
return PNGData;<br/>
} <br/>
<br/>
@end

Saving data occurs automatically when a cell changes to a table. Or 3 seconds after the user has changed a script. To do this, we will use the delegate method to NSTextView and deferred execution:
- ( void ) textDidChange : ( NSNotification * ) aNotification<br/>
{ <br/>
[ NSObject cancelPreviousPerformRequestsWithTarget : self ] ;<br/>
[ self performSelector : @selector ( saveCurrentData ) withObject : nil afterDelay : 3.0 ] ;<br/>
<br/>
saved = NO ;<br/>
}

If the user has changed any data, then after 3 seconds automatic saving will occur. If the user continues to edit data during this period, the previous save request is canceled and a new one is created. Such a mechanism is convenient to use, for example, when searching (so that the search will work only some time after the end of editing).

After saving, we notify the helper that the data has been changed:
NSString * observedObject = @ "info.yuriev.OnAppBehaviourHelper" ;<br/>
NSDistributedNotificationCenter * center = [ NSDistributedNotificationCenter defaultCenter ] ;<br/>
[ center postNotificationName : @ "OABReloadScripts" object : observedObject userInfo : nil deliverImmediately : YES ] ;

Our program can launch an assistant on its own (if it is not running) and is able to add it to autoload. Launching is very simple (our assistant is inside the bundle):
[ [ NSWorkspace sharedWorkspace ] launchApplicationAtURL : [ NSURL fileURLWithPath : helperPath ] options : NSWorkspaceLaunchDefault configuration : nil error : NULL ] ;

The management of startup items (Login Items) is the responsibility of the LaunchServices framework. It is written in C. We will make a convenient Objective-C wrapper for our tasks. Let's declare the LoginItems class:
@interface LoginItems : NSObject <br/>
{ <br/>
<br/>
} <br/>
<br/>
+ ( void ) addApplication : ( NSString * ) path;<br/>
+ ( void ) removeApplication : ( NSString * ) path;<br/>
+ ( BOOL ) findApplication : ( NSString * ) path;<br/>
<br/>
@end

These are class methods. They are not bound to any object, they can be simply called [LoginItems addApplication: path].

Full implementation of these methods can be viewed in source codes. Here's how, for example, the method of adding is implemented:
+ ( void ) addApplication : ( NSString * ) path<br/>
{ <br/>
LSSharedFileListRef loginItemsRef = LSSharedFileListCreate ( NULL , kLSSharedFileListSessionLoginItems, NULL ) ;<br/>
<br/>
if ( loginItemsRef ) <br/>
{ <br/>
LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL ( loginItemsRef, kLSSharedFileListItemLast, NULL , NULL , ( CFURLRef ) [ NSURL fileURLWithPath : path ] , NULL , NULL ) ;<br/>
if ( itemRef ) CFRelease ( itemRef ) ;<br/>
CFRelease ( loginItemsRef ) ; <br/>
} <br/>
}

That's basically our entire panel.

Now the assistant ... he is very simple. Because the helper should not be visible to the user; in the program description file, set the LSBackOnly key to YES. This means that the program will not be displayed in the dock, in the Force Quit window, it will not display the menu, etc.

The most important part of initializing the helper is receiving messages from the panel that the settings are changed (to reload them), and receiving NSWorkspace messages about the state of the programs:
NSNotificationCenter * notificationCenter = [ [ NSWorkspace sharedWorkspace ] notificationCenter ] ;<br/>
<br/>
[ notificationCenter addObserver : self selector : @selector ( didLaunchApplication : ) name : NSWorkspaceDidLaunchApplicationNotification object : nil ] ;<br/>
[ notificationCenter addObserver : self selector : @selector ( didTerminateApplication : ) name : NSWorkspaceDidTerminateApplicationNotification object : nil ] ;<br/>
[ notificationCenter addObserver : self selector : @selector ( didHideApplication : ) name : NSWorkspaceDidHideApplicationNotification object : nil ] ;<br/>
[ notificationCenter addObserver : self selector : @selector ( didUnhideApplication : ) name : NSWorkspaceDidUnhideApplicationNotification object : nil ] ;<br/>
[ notificationCenter addObserver : self selector : @selector ( didActivateApplication : ) name : NSWorkspaceDidActivateApplicationNotification object : nil ] ;<br/>
[ notificationCenter addObserver : self selector : @selector ( didDeactivateApplication : ) name : NSWorkspaceDidDeactivateApplicationNotification object : nil ] ;<br/>
<br/>
NSString * observedObject = @ "info.yuriev.OnAppBehaviourHelper" ;<br/>
NSDistributedNotificationCenter * dNotificationCenter = [ NSDistributedNotificationCenter defaultCenter ] ;<br/>
<br/>
[ dNotificationCenter addObserver : self selector : @selector ( loadPreferences : ) name : @ "OABReloadScripts" object : observedObject ] ;

And the main method that AppleScript performs, if the program and its action matches the settings:
- ( void ) preformScriptOnApp : ( NSRunningApplication * ) app forKey : ( NSString * ) key<br/>
{ <br/>
if ( ( ! app ) || ( ! [ app bundleIdentifier ] ) ) return ;<br/>
<br/>
NSString * bundleID = [ app bundleIdentifier ] ;<br/>
<br/>
for ( int i = 0 ; i < [ preferences count ] ; i ++ ) <br/>
{ <br/>
if ( [ [ [ preferences objectAtIndex : i ] objectForKey : @ "ID" ] isEqualToString : bundleID ] ) <br/>
{ <br/>
NSString * script = [ [ preferences objectAtIndex : i ] objectForKey : key ] ;<br/>
<br/>
if ( script && ( [ script length ] > 0 ) ) <br/>
{ <br/>
NSAppleScript * AScript = [ [ NSAppleScript alloc ] initWithSource : script ] ;<br/>
[ AScript executeAndReturnError : NULL ] ;<br/>
[ AScript release ] ;<br/>
} <br/>
<br/>
break ;<br/>
} <br/>
} <br/>
<br/>
}

Here is such a small project, and there is a lot of interesting inside. I hope that this material will be useful to someone. I have already received my favor - the Dock automatically disappears when I activate the iOS Simulator :).

Source: https://habr.com/ru/post/117248/


All Articles