Cocoa - Programmatically adding an application to all spaces - cocoa

is there a way to add an application to all spaces programmatically? I'd like my application to be on all spaces by default.

The methods you need are in NSWindow.
For Lion use:
- (void)setCollectionBehavior:(NSWindowCollectionBehavior)behavior
For pre-Lion override the following to return YES:
- (BOOL)canBeVisibleOnAllSpaces

This piece of code works for me (at least on 10.6.8 in a little project I recently worked on):
-(void)windowDidLoad {
// Make the window visible on all Spaces
if([[self window] respondsToSelector: #selector(setCollectionBehavior:)]) {
[[self window] setCollectionBehavior: NSWindowCollectionBehaviorCanJoinAllSpaces];
}
else if([[self window] respondsToSelector: #selector(canBeVisibleOnAllSpaces)]) {
[[self window] canBeVisibleOnAllSpaces]; // AVAILABLE_MAC_OS_X_VERSION_10_5_AND_LATER_BUT_DEPRECATED
}
}
I put this code in a (custom subclass of a) WindowController for the main app window.

Ok. Just setting the workspaces-app-bindings programmatically didn't work. I tried:
1) Verified no entries were in System Preferences->Spaces
2) defaults write com.apple.dock workspaces-app-bindings -dict-add com.apple.mail 65544
3) killall Dock (also needed to kill System Preferences )
4) Opened System Preferences->Spaces to verify the Mail app entry
appeared and was set to Every Space
5) Launched Mail, but it was still stuck to Space 1
6) Only when I went back into System Preferences->Spaces and changed the
Mail app *from* Every Space and then *back* to Every Space did the Mail
app stick to every space
So clearly system preferences is doing something extra to activate the setting. Does anyone know what this could be? Thanks!
Update: So I was able to get this working by using the applescript api instead of the user defaults api. The following post tells how to append an entry using applescript. Then just kill the dock.
Applescript; opening an app in Space number N

Use the defaults-command that ships with OS X, like so:
defaults write com.apple.dock workspaces-app-bindings -dict-add com.apple.mail 65544
By issuing the above command, you set the application identified by “com.apple.mail” to appear on every space. 65544 is a magic value saying “every space”. If the key-value pair (identifier + settings) exists it will be overwritten.
Note that you have to reload the Dock (killall Dock) and somehow execute these commands from within your application. From within objective-c you can use the following snippet to quit the Dock:
NSRunningApplication *dock = [NSRunningApplicationrunningApplicationWithBundleIdentifier:#"com.apple.dock"];
[dock terminate];
From within AppleScript use the following:
quit application "Dock"

Your app delegate should look like this...
#import "alwaysOnTopAppDelegate.h"
#implementation alwaysOnTopAppDelegate
#synthesize window;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[window setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];
}
#end

Related

Close and launch with different command line parameters/arguments

I am running a .app and I need to "restart" it so to speak. Basically I need to tell it to close, then after closing, it should launch the path i tell (which is to itself) with some command line arguments. Is this possible with cocoa? Im getting stuck at the part where my app is closing then closed, then I cant get it back.
My code is in js-ctypes, but here is the objc pseudo code:
default_center = [NSDistributedNotificationCenter defaultCenter];
shared_workspace = [NSWorkspace sharedWorkspace];
notification_center = [[NSWorkspace sharedWorkspace] notificationCenter];
[notification_center addObserver:selector:name:object: ***, ***, NSWorkspaceDidLaunchApplicationNotification, NIL]
And in my observr when it responds with completion of quit it has code to launch. But as my app is closed it never gets to the observer responder.
Here
Thanks
You didn't mention any reason that you cannot launch a second instance of your app from the first instance, rather than the chicken & egg approach of trying to restart after you've quit... I have this code in my AppWillTerminate function where I have a situation like yours:
[[NSWorkspace sharedWorkspace] launchApplicationAtURL:appUrl options:NSWorkspaceLaunchNewInstance configuration:nil error:&error];
( To get the AppWillTerminate to be called in the first place, I had to disableSuddenTermination before calling [app quit] )
There's also some flag in the app's plist file like "allow multiple instance" or something.
Also, KNOW THIS: if your app is sandboxed, this will not work UNLESS it is code signed with an AppleStore given id, or with a Developer ID Application id. Also, it won't work on X.7 no matter what, when sandboxed.
METHOD TWO,
is to create a "Helper App". Your KillerApp goes through the Quit process, and right before it dies, it launches "HelperApp", which is a tiny command line tool which waits for KillerApp to really die, then relaunches it.
In XCode, the code for the HelperApp, a "Command line tool", is like this:
#import <Cocoa/Cocoa.h>
int main( int argc , char *argv[] ) {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
pid_t parentPID = atoi(argv[2]);
ProcessSerialNumber psn;
while ( GetProcessForPID(parentPID, &psn) != procNotFound )
sleep(1);
NSString* appPath = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
BOOL success = [[NSWorkspace sharedWorkspace] openFile:[appPath stringByExpandingTildeInPath]];
if ( ! success )
NSLog(#"Error: could not relaunch application at %#", appPath);
[pool drain];
return (success) ? 0 : 1;
}
As you can see, you call the HelperApp with a couple parameters from your KillerApp... And in the case where you don't need to be sandboxed, that's about it.
If you DO need sandboxing, then it gets more complicated, of course. You need to create a "privileged helper tool", and thank goodness there is sample code for it.
"SMJobBless" is the Apple sample code project which outlines how to do this, but it's kind of weird-- it doesn't actually DO anything. Thankfully, somebody took that project and created "SMJobBlessXPC" from it, which really does finish the job, ( and when you get it working, your KillerApp can actually communicate with your HelperApp ). The downside is that you need to exactly maintain the plist files of the two apps in terms of code signing.

Cocoa - go to foreground/background programmatically

I have an application with LSUIElement set to 1. It has a built-in editor, so I want the application to appear in Cmd+Tab cycle when the editor is open.
-(void)stepIntoForeground
{
if (NSAppKitVersionNumber < NSAppKitVersionNumber10_7) return;
if (counter == 0) {
ProcessSerialNumber psn = {0, kCurrentProcess};
OSStatus osstatus = TransformProcessType(&psn, kProcessTransformToForegroundApplication);
if (osstatus == 0) {
++counter;
} else {
//...
}
}
}
-(void)stepIntoBackground
{
if (NSAppKitVersionNumber < NSAppKitVersionNumber10_7) return;
if (counter == 0) return;
if (counter == 1) {
ProcessSerialNumber psn = {0, kCurrentProcess};
OSStatus osstatus = TransformProcessType(&psn, kProcessTransformToUIElementApplication);
if (osstatus == 0) {
--counter;
} else {
//..
}
}
}
The problems are:
there's also a Dock icon (not a big deal);
there's also Menu, that is not a big deal too, but they appear not always.
Is there any way to disable menu at all or to make it appear always in foreground? Thanks in advance.
This is how we do it.
(Works 10.7+)
DO NOT USE LSBackgroundOnly NOR LSUIElement in the app plist
Add and init your menu and NSStatusBar menu
After app initialized but not yet shown any window take a place where you might want to show the first window if any. We use applicationDidFinishLaunching.
If you do not want to show any window yet after app initialized use
[NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
on 10.9 you can use at last the otherwise much correct
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
If you should open any window after app init finished than simply show the main window
Maintain your list of windows
If last window closed, call
[NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
on 10.9 you can use at last the otherwise much correct
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
When your first window shown next time, call
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp activateIgnoringOtherApps:YES];
[[self window] makeKeyAndOrderFront:nil];
This should do the trick, if at least one app window is visible you will have menu, dock icon with state signaled, and cmd+tab element with your app, if last app window closed only your NSStatusBar element stays.
Known issues:
The first step is important because without that if a system modal dialog suspends your startup (f.e. your app is downloaded from the net and become quarantined a confirmation dialog might appear at first startup depending on your security settings) your menubar might not be owned by your app after your first app window shown.
Workaround: Starting as normal app (step 1.) would solve this problem, but will cause another small one, your app icon might appear for a moment in the dock at startup even if you would like to startup without any window shown. (but we can deal with this, not owning the menubar was a bigger problem for us, so we chose this instead)
Changing between NSApplicationActivationPolicyRegular and NSApplicationActivationPolicyAccessory (or NSApplicationActivationPolicyProhibited on OSes bellow 10.9) will kill your tooltip of status bar menu element, the tooltip will be shown initially but will not ever after the second call of NSApplicationActivationPolicyAccessory -> NSApplicationActivationPolicyProhibited
Workaround: We could not find a working workaround for this and reported to Apple as a bug.
Changing from NSApplicationActivationPolicyRegular to NSApplicationActivationPolicyAccessory has other problems on some OS versions like there might be no more mouse events in visible app windows sometimes
Workaround: switch first to NSApplicationActivationPolicyProhibited (take care this leads to unwanted app messages, like NSApplicationWillResignActiveNotification, NSWindowDidResignMainNotification, etc. !)
Changing from NSApplicationActivationPolicyAccessory to NSApplicationActivationPolicyRegular is bogus as on some OS versions
the app main menu is frozen till the first app front status change
the app activated after this policy not always get placed front in the application order
Workaround: switch first to NSApplicationActivationPolicyProhibited, take care the final switch to the desired NSApplicationActivationPolicyRegular should be made delayed, use f.e. dispatch_async or similar
With swift 4, in applicationDidfinishLaunching(_:Notification)
NSApplication.shared.setActivationPolicy(.regular)
did the trick for me, but I was only trying to get keyboard focus to my programmatically created window. Thanks.
You can set App "Application is agent (UIElement)" to YES in your plist file.
EDIT:
I think there are some hacks to do this.
But it's really not the way it's meant to be.
Cmd+tab is for getting an application to foreground, but if you don't have a menu bar, it doesn't look like foreground to the user.
I'd rather make a menu bar to access the app.

addGlobalMonitorForEventsMatchingMask doesn't capture global keypresses

I have a status bar app. I'm using this code to capture very user's key press in the system:
_keybordEventMonitor =
[NSEvent addGlobalMonitorForEventsMatchingMask:(NSKeyDownMask) handler:^(NSEvent *incomingEvent)
{
[self inputKeyboardEventHandler: incomingEvent];
}];
It captures everything fine, but doesn't capture global system hotkeys like cmd + space or cmd+shift+3.
Accessibility APIs are enabled. Any ideas?
PS: I tried using CGEventTap and it's kind of works, but had it's own problems and since I'm a cocoa noob, I prefer to keep things simple for now.

Change the wallpaper on all desktops in OS X 10.7 Lion?

I would like to change the wallpaper of all desktops (formerly "spaces") on a screen. As of OS X 10.6 there is a category to NSWorkspace which allows the setting of the wallpaper, however, when I use this function only the wallpaper of the current desktop gets changed and all the other desktops remain unchanged.
I then looked at the desktop preferences plist and wrote a class that modifies it to reflect the changes I want (basically set a new image file path). After the new file was saved I sent the com.apple.desktop "BackgroundChanged" notification - Google if you don't know what I am talking about, this was how people changed wallpapers in pre 10.6 days. At first this didn't produce any result, so instead of "nil" as userInfo dictionary I sent the exact same userInfo dictionary along as Apple does when you change the wallpaper in your settings (subscribe to the notification in an app and change the wallpaper in the settings app and you will see what it looks like). Luck helped me here, when I sent the notification this way for some reason the Dock crashed and when it reloaded, it loaded the settings from the preferences file thus displaying my changes.
This works on 10.7.1, however, I would a) rather not have the bad user experience of the dock crashing and reloading, and b) use a path that is more or less guaranteed to work in future releases as well. Exploiting a bug doesn't seem like a stable path.
Any other ideas on how to change the wallpaper of all desktops? I am also unsure whether the current behaviour of the NSWorkspace wallpaper category is intended or a bug, however, judging from the behaviour of the wallpaper preferences pane it seems that the former is the case.
There is no api for setting the same wallpaper to all screens or all spaces, NSWorkspace setDesktopImageURL it is implemented as such that it only sets the wallpaper for the current space on the current screen, this is how System Preferences does it too.
Besides the volatile method of manually modifying the ~/Library/Preferences/com.apple.desktop.plist (format could change) and using notifications to reload it (crashes you experienced) what you can do is set the wallpaper to spaces as the user switches to it , e.g. look for NSWorkspaceActiveSpaceDidChangeNotification (if your application is not always running you could tell the user to switch to all spaces he wants the wallpaper to apply to) , arguably these methods are not ideal but at least they are not volatile.
-(void)setWallpaper
{
NSWorkspace *sws = [NSWorkspace sharedWorkspace];
NSURL *image = [NSURL fileURLWithPath:#"/Library/Desktop Pictures/Andromeda Galaxy.jpg"];
NSError *err = nil;
for (NSScreen *screen in [NSScreen screens]) {
NSDictionary *opt = [sws desktopImageOptionsForScreen:screen];
[sws setDesktopImageURL:image forScreen:screen options:opt error:&err];
if (err) {
NSLog(#"%#",[err localizedDescription]);
}else{
NSNumber *scr = [[screen deviceDescription] objectForKey:#"NSScreenNumber"];
NSLog(#"Set %# for space %i on screen %#",[image path],[self spaceNumber],scr);
}
}
}
-(int)spaceNumber
{
CFArrayRef windowsInSpace = CGWindowListCopyWindowInfo(kCGWindowListOptionAll | kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
for (NSMutableDictionary *thisWindow in (NSArray *)windowsInSpace) {
if ([thisWindow objectForKey:(id)kCGWindowWorkspace]){
return [[thisWindow objectForKey:(id)kCGWindowWorkspace] intValue];
}
}
return -1;
}

Catch system event as Cmd-Tab or Spotlight in a Cocoa App

In a Cocoa App, I'm trying to find a way to catch system events like the app switcher usually launched with Cmd-Tab or spotlight, usually launched by Cmd-Space. I'm looking for either a way to catch the key event or any another way that would tell me that one of those event is about to happen, and ideally cancel it.
Apple Screen Sharing remote desktop app does it, so it should be possible. It catches those events and send them to the connected remote computer.
Here is what I already tried :
Catching events with the sendEvent method in NSApplication. I see all events like the Cmd keydown, the Tab keydown, but when both are pressed, I see nothing.
Registering a Carbon Hot key listener. I can register anything like Cmd+Q, but again, when I register Cmd+Tab, it does not respond.
Any other ideas ?
See Event Taps.
Found it!
In my WindowViewController.m file
#import <Carbon/Carbon.h>
void *oldHotKeyMode;
- (void)windowDidBecomeKey:(NSNotification *)notification{
oldHotKeyMode = PushSymbolicHotKeyMode(kHIHotKeyModeAllDisabled);
}
- (void)windowDidResignKey:(NSNotification *)notification{
PopSymbolicHotKeyMode(oldHotKeyMode);
}
This is pretty magic ! and it passes the new Apple sandboxing requirement for the Mac App Store !
I will describe you how to catch cmd+tab. But please note that will work only in fullscreen mode. I beleive there is no way to this in windowed mode. Code is pretty simple. It is a minor fix of SDL mac code - update for handling cmd+tab in fullscreen mode.
NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask
untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES ];
if ( event == nil ) {
break;
}
if (([event type] == NSKeyDown) &&
([event modifierFlags] & NSCommandKeyMask)
&&([[event characters] characterAtIndex:0] == '\t')
{
do something here
}

Resources