SMAppService register() - How to detect launch at startup/login vs regular launch? - macos

We use the new MacOS Ventura SMAppService functionality to offer a "Launch at Login" feature to our users. We do this in a very straightforward way:
SMAppService.mainApp.register()
We'd like to do some specific processing if we are launched at startup/login that we wouldn't do if just launched regularly. Specifically our users would prefer not to see any windows/UI if launched at startup as a menu bar app.
Is there any way to detect that our App has been launched at startup/login vs a regular user initiated launch?
Perhaps a command line argument or is there a special parent process we could look for? There doesn't seem to be a way to pass command line arguments and we don't know of any special parent process we could look for.

To answer my own question, it turns out this is possible. Inspired by this older answer which worked with the non Ventura API/paradigm.
Inside your AppDelegate:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSAppleEventDescriptor* event = NSAppleEventManager.sharedAppleEventManager.currentAppleEvent;
BOOL launchedAsLoginItem = (event.eventID == kAEOpenApplication &&
[event paramDescriptorForKeyword:keyAEPropData].enumCodeValue == keyAELaunchedAsLogInItem);
...
}
and Swift:
let event = NSAppleEventManager.shared().currentAppleEvent
let launchedAsLogInItem =
event?.eventID == kAEOpenApplication &&
event?.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem

Related

Change OSX keyboard layout("input source") programmatically via terminal or AppleScript?

I am currently switching input sources by running a GUI AppleScript through Alfred, and the GUI script can sometime take up to 1s to complete the change. It gets quite annoying at times.
I have come across Determine OS X keyboard layout (“input source”) in the terminal/a script. And I want to know since we can find out the current input source if there's a way to change input source programatically? I'd tried overwriting the com.apple.HIToolbox.plist but it does not change the input.
(I do realise there's mapping shortcut to input sources available in the system preference, however I prefer mapping keywords with Alfred)
You can do it using the Text Input Services API:
NSArray* sources = CFBridgingRelease(TISCreateInputSourceList((__bridge CFDictionaryRef)#{ (__bridge NSString*)kTISPropertyInputSourceID : #"com.apple.keylayout.French" }, FALSE));
TISInputSourceRef source = (__bridge TISInputSourceRef)sources[0];
OSStatus status = TISSelectInputSource(source);
if (status != noErr)
/* handle error */;
The dictionary in the first line can use other properties for other criteria for picking an input source.
There's also NSTextInputContext. It has a selectedKeyboardInputSource which can be set to an input source ID to select a different input source. The issue there is that you need an instance of NSTextInputContext to work with and one of those exists only when you have a key window with a text view as its first responder.
#Ken Thomases' solution is probably the most robust - but it requires creation of a command-line utility.
A non-GUI-scripting shell scripting / AppleScripting solution is unfortunately not an option: while it is possible to update the *.plist file that reflects the currently selected input source (keyboard layout) - ~/Library/Preferences/com.apple.HIToolbox.plist - the system will ignore the change.
However, the following GUI-scripting solution (based on this), while still involving visible action, is robust and reasonably fast on my machine (around 0.2 seconds):
(If you just wanted to cycle through installed layouts, using a keyboard shortcut defined in System Preferences is probably your best bet; the advantage of this solution is that you can target a specific layout.)
Note the prerequisites mentioned in the comments.
# Example call
my switchToInputSource("Spanish")
# Switches to the specified input source (keyboard layout) using GUI scripting.
# Prerequisites:
# - The application running this script must be granted assisistive access.
# - Showing the Input menu in the menu bar must be turned on
# (System Preferences > Keyboard > Input Sources > Show Input menu in menu bar).
# Parameters:
# name ... input source name, as displayed when you open the Input menu from
# the menu bar; e.g.: "U.S."
# Example:
# my switchToInputSource("Spanish")
on switchToInputSource(name)
tell application "System Events" to tell process "SystemUIServer"
tell (menu bar item 1 of menu bar 1 whose description is "text input")
# !! Sadly, we must *visibly* select (open) the text-input menu-bar extra in order to
# !! populate its menu with the available input sources.
select
tell menu 1
# !! Curiously, using just `name` instead of `(get name)` didn't work: 'Access not allowed'.
click (first menu item whose title = (get name))
end tell
end tell
end tell
end switchToInputSource
Solution using Xcode Command Line Tools
For those, who would like to build #Ken Thomases' solution but without installing Xcode (which is several GiB and is totally useless to spend so much space on unless used seriously) it is possible to build it using the Xcode Command Line Tools.
There are several tutorials on the internet about how to install Xcode Command Line Tools. The point here is only that it takes fraction of the space compared to full-blown Xcode.
Once you have it installed, these are the steps:
Create a file called whatever.m
In whatever.m put the following:
#include <Carbon/Carbon.h>
int main (int argc, const char * argv[]) {
NSArray* sources = CFBridgingRelease(TISCreateInputSourceList((__bridge CFDictionaryRef)#{ (__bridge NSString*)kTISPropertyInputSourceID : #"com.apple.keylayout.French" }, FALSE));
TISInputSourceRef source = (__bridge TISInputSourceRef)sources[0];
OSStatus status = TISSelectInputSource(source);
if (status != noErr)
return -1;
return 0;
}
Replace French with your desired layout.
Save the file
Open terminal in the same folder as whatever.m is
Run this command:
clang -framework Carbon whatever.m -o whatever
Your application is created as whatever in the same folder and can be executed as:
.\whatever
Additionally
I've never created any Objective-C programs, so this may be suboptimal, but I wanted an executable that can take the keyboard layout as a command line parameter. For anyone interested, here's the solution I came up with:
In step 2 use this code:
#import <Foundation/Foundation.h>
#include <Carbon/Carbon.h>
int main (int argc, const char * argv[]) {
NSArray *arguments = [[NSProcessInfo processInfo] arguments];
NSArray* sources = CFBridgingRelease(TISCreateInputSourceList((__bridge CFDictionaryRef)#{ (__bridge NSString*)kTISPropertyInputSourceID : [#"com.apple.keylayout." stringByAppendingString:arguments[1]] }, FALSE));
TISInputSourceRef source = (__bridge TISInputSourceRef)sources[0];
OSStatus status = TISSelectInputSource(source);
if (status != noErr)
return -1;
return 0;
}
In step 6. run this command:
clang -framework Carbon -framework Foundation whatever.m -o whatever
You can now switch to any layout from the command line, e.g.:
./whatever British
Note: it only allows to switch to layouts already configured on your system!
Another option is to use Swift. It can be used in a script-like fashion (no compilation).
Install Xcode Command Line Tools
Create a script from the code below
Run the script using swift script_file_name
Code:
import Carbon
let command = ProcessInfo.processInfo.arguments.dropFirst().last ?? ""
let filter = command == "list" ? nil : [kTISPropertyInputSourceID: command]
guard let cfSources = TISCreateInputSourceList(filter as CFDictionary?, false),
let sources = cfSources.takeRetainedValue() as? [TISInputSource] else {
print("Use \"list\" as an argument to list all enabled input sources.")
exit(-1)
}
if filter == nil { // Print all sources
print("Change input source by passing one of these names as an argument:")
sources.forEach {
let cfID = TISGetInputSourceProperty($0, kTISPropertyInputSourceID)!
print(Unmanaged<CFString>.fromOpaque(cfID).takeUnretainedValue() as String)
}
} else if let firstSource = sources.first { // Select this source
exit(TISSelectInputSource(firstSource))
}
This elaborates on answers by Ken Thomases and sbnc.eu.
On AppleScript you must only take cmd + "space" (or something other, what you use for change keyboard source).
And all what you need:
key code 49 using command down
49 - code of 'space' button in ASCII for AppleScript.
P.S.: don't forget get access for you AppleScript utility in System Preferences.
tell application "System Events"
key code 49 using control down
end tell
Changes layout via keypress

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.

Cocoa - Programmatically adding an application to all spaces

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

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