NSNotifier on Mac OSX - objective-c

I'm building a mac daemon, from scratch.
Here's a simplified version of the code.
#import <stdio.h>
#import <stdlib.h>
#import <Foundation/Foundation.h>
#include "notifier.h"
int new_notification();
notifier *not;
int main () {
#autoreleasepool {
not = [[[notifier alloc] init] autorelease];
pid_t pid;
pid = fork();
if(pid > 0) {
printf("my child id is %d\n", pid);
exit(0);
}
while(1) {
int n = new_notification();
if(n > 0) {
NSUserNotification *notification = [[NSUserNotification alloc] init];
notification.title = #"Hello, World!";
notification.informativeText = #"A notification";
notification.soundName = NSUserNotificationDefaultSoundName;
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
printf("new notification : count = %d !!\n", n);
}
sleep(1);
}
}
return 0;
}
int new_notification() {
return [not get_notifications];
}
I don't see the notification on my window though, I think I have to make my application a "key" application, if so, how do it do that? I can see the output on my terminal though, and on checking if
(NSClassFromString(#"NSUserNotificationCenter")==nil)
I get FALSE

Extended comments that I hope answer your issue...
If your app is active, the notification is unlikely to be displayed. But you'd be able to find it in the notification center. (Notifications draw a user's attention to an app that they're not already looking at.)
I'd suggest trying [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory] to make it clear your app isn't in the foreground but I'm not totally certain whether [NSApplication sharedApplication] aka NSApp is something you'd use with a daemon.
Part of your issue may actually be that without invoking [NSApplication sharedApplication] and telling it to run you never become an app, so you can't use the notification center... or become an accessory. But I'm not certain of that either.

Related

macOS: Detect all application launches including background apps?

Newbie here. I'm trying to create a small listener for application launches, and I already have this:
// almon.m
#import <Cocoa/Cocoa.h>
#import <stdio.h>
#include <signal.h>
#interface almon: NSObject {}
-(id) init;
-(void) launchedApp: (NSNotification*) notification;
#end
#implementation almon
-(id) init {
NSNotificationCenter * notify
= [[NSWorkspace sharedWorkspace] notificationCenter];
[notify addObserver: self
selector: #selector(launchedApp:)
name: #"NSWorkspaceWillLaunchApplicationNotification"
object: nil
];
fprintf(stderr,"Listening...\n");
[[NSRunLoop currentRunLoop] run];
fprintf(stderr,"Stopping...\n");
return self;
}
-(void) launchedApp: (NSNotification*) notification {
NSDictionary *userInfo = [notification userInfo]; // read full application launch info
NSString* AppPID = [userInfo objectForKey:#"NSApplicationProcessIdentifier"]; // parse for AppPID
int killPID = [AppPID intValue]; // define integer from NSString
kill((killPID), SIGSTOP); // interrupt app launch
NSString* AppPath = [userInfo objectForKey:#"NSApplicationPath"]; // read application path
NSString* AppBundleID = [userInfo objectForKey:#"NSApplicationBundleIdentifier"]; // read BundleID
NSString* AppName = [userInfo objectForKey:#"NSApplicationName"]; // read AppName
NSLog(#":::%#:::%#:::%#:::%#", AppPID, AppPath, AppBundleID, AppName);
}
#end
int main( int argc, char ** argv) {
[[almon alloc] init];
return 0;
}
// build: gcc -Wall almon.m -o almon -lobjc -framework Cocoa
// run: ./almon
Note: when I build it, it will run fine, but if you do it with Xcode 10 on High Sierra, you will get ld warnings, which you can ignore, however.
My question: Is there a way to also detect a launch of a background application, e.g. a menu bar application like Viscosity etc.? Apple says that
the system does not post
[NSWorkspaceWillLaunchApplicationNotification] for background apps or
for apps that have the LSUIElement key in their Info.plist file.
If you want to know when all apps (including background apps) are
launched or terminated, use key-value observing to monitor the value
returned by the runningApplications method.
Here: https://developer.apple.com/documentation/appkit/nsworkspacewilllaunchapplicationnotification?language=objc
I would at least try to add support for background apps etc. to the listener, but I don't know how to go about it. Any ideas?
As the document suggests, you use Key-Value Observing to observe the runningApplications property of the shared workspace object:
static const void *kMyKVOContext = (void*)&kMyKVOContext;
[[NSWorkspace sharedWorkspace] addObserver:self
forKeyPath:#"runningApplications"
options:NSKeyValueObservingOptionNew // maybe | NSKeyValueObservingOptionInitial
context:kMyKVOContext];
Then, you would implement the observation method (using Xcode's ready-made snippet):
- (void) observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
{
if (context != kMyKVOContext)
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
return;
}
if ([keyPath isEqualToString:#"runningApplications"])
{
<#code to be executed when runningApplications has changed#>
}
}

Substitute keystroke

I am trying to intercept a keystroke, and substitute it with a different character. I have been able to intercept the key being pressed, as well as perform some extra operations. Now I need to hold the key being pressed if it matches one of the ones I am watching, and insert a different character. Here is the code I have right now:
#import "AppDelegate.h"
#import <AppKit/AppKit.h>
#import <CoreGraphics/CoreGraphics.h>
#include <ApplicationServices/ApplicationServices.h>
#interface AppDelegate ()
#property (weak) IBOutlet NSWindow *window;
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
//Keys that are being watched to be switched out
NSArray *keysToWatch = [[NSArray alloc] initWithObjects:#"c",#".", nil];
// register for keys throughout the device...
[NSEvent addGlobalMonitorForEventsMatchingMask:NSKeyDownMask
handler:^(NSEvent *event){
//Get characters
NSString *chars = [[event characters] lowercaseString];
//Get the actual character being pressed
unichar character = [chars characterAtIndex:0];
//Transform it to a string
NSString *aString = [NSString stringWithCharacters:&character length:1];
//If it is in the list, start looking if Keynote is active
if ([keysToWatch containsObject:[NSString stringWithString:aString]]) {
//DEBUG: Print a message
NSLog(#"Key being watched has been pressed");
//Get a list of all running apps
for (NSRunningApplication *currApp in [[NSWorkspace sharedWorkspace] runningApplications]) {
//Get current active app
if ([currApp isActive]) {
//Check if it is Keynote, if yes perform remap
if ([[currApp localizedName] isEqualToString:#"Keynote"]){
//DEBUG: Print a small message
NSLog(#"Current app is Keynote");
if (character=='.') {
NSLog(#"Pressed a dot");
//I want to post a different character here
PostKeyWithModifiers((CGKeyCode)11, FALSE);
}
else if ([aString isEqualToString:#"c"]) {
NSLog(#"Pressed c");
}
}
else if ([[currApp localizedName] isEqualToString:#"Microsoft PowerPoint"]){
}
}
}
}
}
];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
- (BOOL)acceptsFirstResponder {
return YES;
}
void PostKeyWithModifiers(CGKeyCode key, CGEventFlags modifiers)
{
CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateCombinedSessionState);
CGEventRef keyDown = CGEventCreateKeyboardEvent(source, key, TRUE);
CGEventSetFlags(keyDown, modifiers);
CGEventRef keyUp = CGEventCreateKeyboardEvent(source, key, FALSE);
CGEventPost(kCGAnnotatedSessionEventTap, keyDown);
CGEventPost(kCGAnnotatedSessionEventTap, keyUp);
CFRelease(keyUp);
CFRelease(keyDown);
CFRelease(source);
}
#end
My problem is that I am not able to stop the original keystroke. Please keep in mind that I am completely new at Obj C, so let me know if there is anything that I can do better. Thanks!
From the docs for +[NSEvent addGlobalMonitorForEventsMatchingMask:handler:]:
Events are delivered asynchronously to your app and you can only
observe the event; you cannot modify or otherwise prevent the event
from being delivered to its original target application.
You would have to use a Quartz Event Tap for that.
So, after a LOT of digging around, I found a way to do this using Quartz Event Taps here. Thanks to #Ken Thomases for pointing me in the correct direction. I then combined my code with the one explained in this article and voilĂ , it works.

NSDocument saveDocumentWithDelegate deadlocked during App termination

NSDocument continues to be a software maintenance nightmare.
Anyone else having a problem where they want certain blocking dialogs to be handled SYNCHRONOUSLY?
BEGIN EDIT: I may have found a solution that allows me to wait synchronously
Can anyone verify that this would be an "Apple approved" solution?
static BOOL sWaitingForDidSaveModally = NO;
BOOL gWaitingForDidSaveCallback = NO; // NSDocument dialog calls didSave: when done
...
gWaitingForDidSaveCallback = true;
[toDocument saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:nil];
if ( gWaitingForDidSaveCallback )
{
// first, dispatch any other potential alerts synchronously
while ( gWaitingForDidSaveCallback && [NSApp modalWindow] )
[NSApp runModalForWindow: [NSApp modalWindow]];
if ( gWaitingForDidSaveCallback )
{
sWaitingForDidSaveModally = YES;
[NSApp runModalForWindow: [NSApp mbWindow]]; // mbWindow is our big (singleton) window
sWaitingForDidSaveModally = NO;
}
}
...
- (void)document:(NSDocument *)doc didSave:(BOOL)didSave contextInfo:(void *)contextInfo
{
[self recordLastSaveURL];
gWaitingForDidSaveCallback = NO;
if ( sWaitingForDidSaveModally )
[NSApp stopModal];
}
END EDIT
I have to support Snow Leopard/Lion/ML
App termination is an ugly process.
When the user decides to quit, and the document has changes that need saving, I call this:
gWaitingForDidSaveCallback = true;
[toDocument saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:nil];
I really really really want this call to be synchronous, but in latest Lion, this hangs my app:
while ( gWaitingForDidSaveCallback )
{
// didSave: callback clears sWaitingForDidSaveCallback
// do my own synchronous wait for now
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceReferenceDate:0.05]];
}
My best guess for the hang is that the mouseDown: of a window close button
is confusing the NSDocument.
So now, I have to return, and pepper my apps main loop with unmaintainable state machine logic to prevent user from executing various dangerous hotkeys.
Ok, so I grin and bear it, and run into yet another roadblock!
In previous OS versions/SDKs, [NSApp modalWindow] would return a window when it
was in this state. Now it doesn't! Grrrrr...
NSDocument has no API to test when it is in this state!
So, now there is no mechanism to globally check this state!
I have to add yet another state variable to my state machine.
Anyone have a cleaner solution for this problem that works in all OS versions and all present (and future) SDKs?
The better way is to save unsaved documents in chain. It is very easy:
// Catch application terminate event
-(NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
NSDocumentController *dc = [NSDocumentController sharedDocumentController];
for (NSInteger i = 0; i < [[dc documents] count]; i++)
{
Document *doc = [[dc documents] objectAtIndex:i];
if ([doc isDocumentEdited])
{
// Save first unsaved document
[doc saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:(__bridge void *)([NSNumber numberWithInteger:i + 1])]; // Next document
return NSTerminateLater; // Wait until last document in chain will be saved
}
}
return NSTerminateNow; // All documents are saved or there are no open documents. Terminate.
}
...
// Document saving finished
-(void)document:(NSDocument *)doc didSave:(BOOL)didSave contextInfo:(void *)contextInfo
{
if (didSave) // Save button pressed
{
NSDocumentController *dc = [NSDocumentController sharedDocumentController];
NSInteger nextIndex = [(__bridge NSNumber *)contextInfo integerValue];
for (NSInteger i = nextIndex; i < [[dc documents] count]; i++)
{
Document *doc = [[dc documents] objectAtIndex:nextIndex];
if ([doc isDocumentEdited])
{
// Save next unsaved document
[doc saveDocumentWithDelegate:self
didSaveSelector:#selector(document:didSave:contextInfo:)
contextInfo:(__bridge void *)([NSNumber numberWithInteger:nextIndex + 1])]; // Next document
return;
}
}
[NSApp replyToApplicationShouldTerminate:YES]; // All documents saved. Terminate.
}
else [NSApp replyToApplicationShouldTerminate:NO]; // Saving canceled. Terminate canceled.
}
Maybe this answer is too late to be useful but... In one of my apps I implemented -(IBAction)terminate:(id)sender in my NSApplication derived class which would conditionally call [super terminate] to actually close the application only if all open documents were cleanly saved. I may have found some of this in the Apple docs or other examples.
The terminate override will go through each document and either close it (because it's saved), or call the document's canCloseDocumentWithDelegate method in the NSDocument derived class passing 'self' and 'terminate' as the didSaveSelector. Since the terminate method falls through and does nothing except make the document present an NSAlert, the alert in the document class will callback and re-run the terminate routine if the user clicks YES or NO. If all documents are clean, the app will terminate since [super terminate] will get called. If any more dirty documents exist, the process repeats.
For example:
#interface MyApplication : NSApplication
#end
#implementation MyApplication
- (IBAction)terminate:(id)sender
{
//Loop through and find any unsaved document to warn the user about.
//Close any saved documents along the way.
NSDocument *docWarn = NULL;
NSArray *documents = [[NSDocumentController sharedDocumentController] documents];
for(int i = 0; i < [documents count]; i++)
{
NSDocument *doc = [documents objectAtIndex:i];
if([doc isDocumentEdited])
{
if(docWarn == NULL || [[doc windowForSheet] isKeyWindow])
docWarn = doc;
}
else
{
//close any document that doesn't need saving. this will
//also close anything that was dirty that the user answered
//NO to on the previous call to this routine which triggered
//a save prompt.
[doc close];
}
}
if(docWarn != NULL)
{
[[docWarn windowForSheet] orderFront:self];
[[docWarn windowForSheet] becomeFirstResponder];
[docWarn canCloseDocumentWithDelegate:self shouldCloseSelector:#selector(terminate:) contextInfo:NULL];
}
else
{
[super terminate:sender];
}
}
#end
Later in the document derived class:
typedef struct {
void * delegate;
SEL shouldCloseSelector;
void *contextInfo;
} CanCloseAlertContext;
#interface MyDocument : NSDocument
#end
#implementation MyDocument
- (void)canCloseDocumentWithDelegate:(id)inDelegate shouldCloseSelector:(SEL)inShouldCloseSelector contextInfo:(void *)inContextInfo
{
// This method may or may not have to actually present the alert sheet.
if (![self isDocumentEdited])
{
// There's nothing to do. Tell the delegate to continue with the close.
if (inShouldCloseSelector)
{
void (*callback)(id, SEL, NSDocument *, BOOL, void *) = (void (*)(id, SEL, NSDocument *, BOOL, void *))objc_msgSend;
(callback)(inDelegate, inShouldCloseSelector, self, YES, inContextInfo);
}
}
else
{
NSWindow *documentWindow = [self windowForSheet];
// Create a record of the context in which the panel is being
// shown, so we can finish up when it's dismissed.
CanCloseAlertContext *closeAlertContext = malloc(sizeof(CanCloseAlertContext));
closeAlertContext->delegate = (__bridge void *)inDelegate;
closeAlertContext->shouldCloseSelector = inShouldCloseSelector;
closeAlertContext->contextInfo = inContextInfo;
// Present a "save changes?" alert as a document-modal sheet.
[documentWindow makeKeyAndOrderFront:nil];
NSBeginAlertSheet(#"Would you like to save your changes?", #"Yes", #"Cancel", #"No", documentWindow, self,
#selector(canCloseAlertSheet:didEndAndReturn:withContextInfo:), NULL, closeAlertContext, #"%");
}
}
- (void)canCloseAlertSheet:(NSWindow *)inAlertSheet didEndAndReturn:(int)inReturnCode withContextInfo:(void *)inContextInfo
{
CanCloseAlertContext *canCloseAlertContext = inContextInfo;
void (*callback)(id, SEL, NSDocument *, BOOL, void* ) = (void (*)(id, SEL, NSDocument *, BOOL, void* ))objc_msgSend;
if (inAlertSheet) [inAlertSheet orderOut:self];
// The user's dismissed our "save changes?" alert sheet. What happens next depends on how the dismissal was done.
if (inReturnCode==NSAlertAlternateReturn)
{
//Cancel - do nothing.
}
else if (inReturnCode==NSAlertDefaultReturn)
{
//Yes - save the current document
[self saveDocumentWithDelegate:(__bridge id)canCloseAlertContext->delegate
didSaveSelector:canCloseAlertContext->shouldCloseSelector contextInfo:canCloseAlertContext->contextInfo];
}
else
{
// No - just clear the dirty flag and post a message to
// re-call the shouldCloseSelector. This should be
// the app:terminate routine.
[self clearDirtyFlag];
if (canCloseAlertContext->shouldCloseSelector)
{
(callback)((__bridge id)canCloseAlertContext->delegate,
canCloseAlertContext->shouldCloseSelector, self, YES, canCloseAlertContext->contextInfo);
}
}
// Free up the memory that was allocated in -canCloseDocumentWithDelegate:shouldCloseSelector:contextInfo:.
free(canCloseAlertContext);
}
#end
And that should do it - No loops... no waiting...

Cocoa app with only NSSavePanel

I'm trying to create a Cocoa application that displays SavePanel, and after user choose file, it prints it on stdout. I'm total beginer with Objective-C and Cocao. Problem is that it doesn't take keyboard input, it is only posible to choose file with mouse.
This is the code:
#import <Cocoa/Cocoa.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
NSSavePanel *sPanel = [NSSavePanel savePanel];
int result = [sPanel runModal];
if (result == NSOKButton) {
NSString * filename = [sPanel filename];
char * fileStr = [filename UTF8String];
printf("%s\n", fileStr);
}
return 0;
}
The AppKit/Cocoa classes require an NSApplication object to be initialized in order to handle user input (among other things). Adding this line to the top of your main function should do the trick:
int main(int argc, char *argv[])
{
[NSApplication sharedApplication]; // ** Add this **
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSSavePanel *sPanel = [NSSavePanel savePanel];
int result = [sPanel runModal];
if (result == NSOKButton) {
NSString * filename = [sPanel filename];
const char * fileStr = [filename UTF8String];
printf("%s\n", fileStr);
}
[pool drain];
return 0;
}
More information about this can be found in the documentation for NSApplication, particularly these points:
Every application must have exactly one instance of NSApplication (or
a subclass of NSApplication). Your program’s main() function should
create this instance by invoking the sharedApplication class method.
NSApplication performs the important task of receiving events from the
window server and distributing them to the proper NSResponder objects.
NSApp translates an event into an NSEvent object, then forwards the
NSEvent object to the affected NSWindow object.
Along the lines of bbum and danielpunkass's comments below, this isn't the way you'd really write a Cocoa application, and while it does make your immediate issue go away, it's not a complete or completely correct solution. To expand on Daniel's comment, and to get you started easily, create a new Cocoa application project. Open up the application delegate class (created for you), and put your code in the -applicationDidFinishLaunching: method. As implied by its name, that method is called after the application has finished launching, and everything is setup such that you can use the AppKit classes normally. As you gain more experience, you'll better understand the typical Cocoa application architecture and can move on to creating user interfaces, etc.

Programmatically put a Mac into sleep

I can't find any instructions how to put a Mac programmatically into sleep mode (in Objective-C). I'm sure it should be only one line, but could you give me a hint?
#include <stdio.h>
#include <CoreServices/CoreServices.h>
#include <Carbon/Carbon.h>
SendAppleEventToSystemProcess(kAESleep);
OSStatus SendAppleEventToSystemProcess(AEEventID EventToSend)
{
AEAddressDesc targetDesc;
static const ProcessSerialNumber kPSNOfSystemProcess = { 0, kSystemProcess };
AppleEvent eventReply = {typeNull, NULL};
AppleEvent appleEventToSend = {typeNull, NULL};
OSStatus error = noErr;
error = AECreateDesc(typeProcessSerialNumber, &kPSNOfSystemProcess,
sizeof(kPSNOfSystemProcess), &targetDesc);
if (error != noErr)
{
return(error);
}
error = AECreateAppleEvent(kCoreEventClass, EventToSend, &targetDesc,
kAutoGenerateReturnID, kAnyTransactionID, &appleEventToSend);
AEDisposeDesc(&targetDesc);
if (error != noErr)
{
return(error);
}
error = AESend(&appleEventToSend, &eventReply, kAENoReply,
kAENormalPriority, kAEDefaultTimeout, NULL, NULL);
AEDisposeDesc(&appleEventToSend);
if (error != noErr)
{
return(error);
}
AEDisposeDesc(&eventReply);
return(error);
}
More detail on https://developer.apple.com/library/content/qa/qa1134/_index.html
You can also use scripting bridge. Draft code is
SystemEventsApplication *systemEvents = [SBApplication applicationWithBundleIdentifier:#"com.apple.systemevents"];
[systemEvents sleep];
Tom is correct. The AE methods fail if the display is sleeping. pmset sleepnow works 100%.
NSTask *pmsetTask = [[NSTask alloc] init];
pmsetTask.launchPath = #"/usr/bin/pmset";
pmsetTask.arguments = #[#"sleepnow"];
[pmsetTask launch];
You can use AppleScript
NSAppleScript *script = [[NSAppleScript alloc] initWithSource:#"tell application \"System Events\" to sleep"];
NSDictionary *errorInfo;
[script executeAndReturnError:&errorInfo];
[script release];
I found that running pmset sleepnow worked during a screensaver, while the first two answers did not.
Just in case someone is curious how pmset sleepnow actually works - it uses IOPMSleepSystem API from the Power Management section of the IOKit framework. You can check this via examining the pmset.c source code (link from macOS 10.13.3).
So instead of calling pmset you can request sleep via the following snippet:
#include <IOKit/pwr_mgt/IOPMLib.h>
void SleepNow()
{
io_connect_t fb = IOPMFindPowerManagement(MACH_PORT_NULL);
if (fb != MACH_PORT_NULL)
{
IOPMSleepSystem(fb);
IOServiceClose(fb);
}
}
Don't be scared by the caller must be root or the console user remark in the documentation since it appears to be working for any standard logged in user.
By following the source code, it looks like it calls into IOUserClient::clientHasPrivilege with kIOClientPrivilegeLocalUser which ends up checking if the caller is present in the IOConsoleUsers array in the root IORegistry entry, and apparently currently logged in user is always present there.