Notification of active document change on OS X? - macos

I'm using NSWorkspace's NSWorkspaceDidActivateApplicationNotification to detect when the active application changes. I get NSRunningApplication from the userInfo key of the notification.
I need to get a notification when the active document changes. I can get the active document by using the accessibility framework's NSAccessibilityDocumentAttribute key through AXUIElementCopyAttributeValue().
I need a more accurate way of detecting when the document changes other than polling. Some applications use multiple windows, while others use a single window with multiple tabs. With tabbed applications the window returns the currently viewed document.
I don't have to use the accessibility framework. AppleScript (scripting bridge) seems to also be able to get a window's document, but the accessibility framework seems to work with more applications.
I only care about the active document, of the active window, of the active application. What currently has focus on the system.
I've been testing with applications like Sublime Text 2, and Xcode. Sublime returns the currently selected tab, where Xcode returns the active project.

I was actually trying to achieve exactly the same thing and I think I've found a solution for it.
What I did was using CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID);
That will give you a list of all the active windows, including windows that you probably don't care about.
I only care about windows that have kCGWindowLayer = 0; so I filtered the windows that are on layer 0.
Here's how I did it:
CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID);
NSMutableArray *data = [(__bridge NSArray *) windowList mutableCopy];
NSMutableArray *filteredData = [[NSMutableArray alloc] initWithCapacity:10];
for (NSMutableDictionary *theDict in data) {
id layer = [theDict objectForKey:(id)kCGWindowLayer];
if ([layer intValue] == 0) {
[filteredData addObject:theDict];
}
}
NSLog(#"window: %#", filteredData);
This might be the most elegant solution, so if anyone else has a better idea, please share. Also you should have a look at Apple's demo app Son of Grab.

Related

Register for global file drag events in Cocoa

I'm trying to be notified when a OS X user is dragging any file in OS X, not only in my app.
My current approach was using addGlobalMonitorForEventsMatchingMask:handler: on NSEvent, as follows:
[NSEvent addGlobalMonitorForEventsMatchingMask:NSLeftMouseDraggedMask handler:^(NSEvent* event) {
NSPasteboard* pb = [NSPasteboard pasteboardWithName:NSDragPboard];
NSLog(#"%#", [pb propertyListForType:NSFilenamesPboardType]);
}];
This works partially - the handler is being called when I start dragging a file from my desktop or Finder, however it also is being called when I perform every other operation that contains a left-mouse-drag, e.g. moving a window. The issue is that the NSDragPboard still seems to contain the latest dragged file URL e.g. when I let off the file and start moving a window, which makes it hard to distinguish between these operations.
TL;DR - I am interested in file drag operations system-wide. I do not need any information about the dragged file itself, just the information that a file drag operation has been started or stopped. I would appreciate any hint to a possible solution for this question.
After having talked to Apple DTS, this is most likely a bug. I have filed rdar://25892115 for this issue. There currently seems to be no way to solve my original question with the given API.
To solve my problem, I am now using the Accessibility API to figure out if the item below the cursor is a file (kAXFilenameAttribute is not NULL).
NSPasteboard* pb = [NSPasteboard pasteboardWithName:NSDragPboard];
NSArray* filenames = [pb propertyListForType:NSFilenamesPboardType];
NSInteger changeCount = pb.changeCount;
//when moving a window. the changeCount is not changed, use it to distinguish
if (filenames.count > 0 && self.lastChangeCount != changeCount){
self.lastChangeCount = changeCount;
//your code here
}

Getting a notification when other app's window move / resize

I'm creating a screen scraping application. What I'm trying now is to fit a rectangle around the corners of the active windows of a given application (by PID).
I managed that by getting a reference to all the active windows in the workspace and using CGWindowListCopyWindowInfo to match the owner PID. Now I have an array of the active windows, but after showing the rectangle that incorporates all the windows as above stated, I didn't find a way to register to get notifications when those windows are moved and / or resized in order to resize and fit my drawn rectangle.
The following is a sample of the code I used to get the NSWindow's frames of the selected application.
pid_t pid = [selectedApplication processIdentifier];
CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionAll,kCGNullWindowID);
for (NSMutableDictionary* entry in (NSArray*)windowList) {
pid_t ownerPID = [[entry objectForKey:(id)kCGWindowOwnerPID] integerValue];
if (pid == ownerPID) {
// setting the bounds for each window of the application on match
CGRectMakeWithDictionaryRepresentation((CFDictionaryRef)[entry objectForKey:(id)kCGWindowBounds], &bounds);
.....
Is there any way in which I can register for updates from those NSwindows given a specific PID of an application I don't own ?
Currently this is a limitation of the APIs from Apple. Didn't find any other way to register for update NSNotifications from other applications NSWindows. Maybe this is treated as a security measure.

How do you display multiple files in a gui field on Mac OSX

Using a Windows file dialog if multiple files are selected they are double quote limited and separated by a space as follows:
"C:\MusicMatched2\Gold Greatest Hits" "C:\MusicMatched2\The Trials of Van Occupanther"
But is this what OSX does as well, I can't seem to find an example with applications on my mac, but I need to do the correct way to display multiple filenames in a simple textfield of a Gui application on a mac.
You're not supposed to present file paths to the user on the Mac. First of all, the user's view of the file hierarchy is not the same as the low-level view represented by paths. The Finder and the standard file dialogs maintain a consistent illusion of a file system hierarchy which doesn't actually exist. For example, each volume is a separate root in the Finder, but volumes other than the boot volume appear under /Volumes in the Unix file system. The Finder has a Computer view which has no representation in the Unix file system. Often, the Finder will represent the user's home folder as a independent root, even though the user can also usually get there from (boot drive) > Users > (user's account name).
There are other subtleties. For example, the names of various folders can be localized for display. However, they are never localized at the Unix path level. So, there will always be a folder whose path is /Applications, but it would be displayed as "Programme" on a German system.
Your app should not pierce this veil and expose users to the Unix-style paths of files.
There are APIs for obtaining the "display name" of a file or the array of display components to display (which is not the same as obtaining the display name of each element of the path). You could concatenate the display components with some separator string to make a text string for a path, but there's not really a standard separator.
The more correct approach is to use a "path control" (NSPathControl). This is a GUI widget that displays a file's location in the standard manner, with the standard separator, etc.
If you need to display multiple file locations, you might consider doing what the Finder does when you do a search. It shows the matches in a list and as you select them it shows the location of each in a path control below the list. You could also show a table with the display names in one column and a path control showing the location in another column.
A typical use of NSOpenPanel looks like this
-(void)showFileSelector {
NSOpenPanel *openpanel = [NSOpenPanel openPanel];
[openpanel setDelegate:self];
[openpanel setCanCreateDirectories:NO];
[openpanel setCanChooseFiles:YES];
[openpanel setAllowsMultipleSelection:YES];
[openpanel setCanChooseDirectories:NO];
[openpanel beginSheetModalForWindow:self.window
completionHandler:^(NSInteger result) {
if (result == NSOKButton) {
[self doSomethingWithURLS:openpanel.URLs];
}
}];
}
so in your case the most important switch is [openpanel setAllowsMultipleSelection:YES];
and you are going to want to make your object which presents the panel conform to NSOpenSavePanelDelegate (or some other object) then call
[openpanel setDelegate:self];
which has the following method available
- (void)panelSelectionDidChange:(id)sender where the sender is the panel
- (void)panelSelectionDidChange:(NSOpenPanel *)sender {
NSArray *urlsSelected = sender.URLs;
NSMutableArray *final = [#[] mutableCopy];
for (NSURL *url in urlsSelected) {
[final addObject:[NSString stringWithFormat:#"\"%#\"",[url path]]];
}
[self.labelToPresentIn setStringValue:[final componentsJoinedByString:#","];
}
So each time the selection changes your label will change.
Thats a explicit answer to how to do what you are asking. Where you choose to present the label with that info is up to you. NSOpenPanel has a property accessoryView which allows you to glue an arbitrary view onto the panel. Like this…
So for a panel with multiple selection enabled.
As the other commenters mention don't try and make your OS X app look and act like Windows. Windows is Windows and OS X is OS X. Different OS's , Different UX.
What you are asking would look a bit weird in OS X. But maybe you have a better idea of what you want.
Try and observe how other OS X apps use the save and open panels.

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;
}

Can an NSStatusItem be shrunk to fit?

I have a variable length NSStatusItem which I'd like to stay visible whenever possible, even if that means showing only some of the content, but when my item is wide enough to run into an application's menu bar, it is hidden entirely. Is there a way to tell when this happens so that I can shrink the view to fit available space?
I've experimented with a custom view, overriding all the viewWill* methods, the frame setters, and the display methods, and periodically checking whether the containing window has moved or become hidden. I can't find any way to tell when my item is too long.
This depends on whether your status item application can detect the number of menu items in the OS X menu bar. A quick search through apple documentation shows that there are no public APIs provided by Apple for the purpose of doing this. To my knowledge, there are no private ones available too.
So I would recommend instead that you make your status item small by default and expanded when clicked by the user.
Edit:
Actually look at the discussion here: a really clever way to detect if your status item is being hidden. So once you have detected that it is being hidden, you can shrink it so that it reappears.
Here's a complete working example based on the discussion that hollow7 referenced:
self.statusItem.title = #"Message that will be truncated as necessary.";
while (self.statusItem.title.length > 0) {
CFArrayRef windowList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenAboveWindow, (CGWindowID)self.statusItemWindow.windowNumber);
if (CFArrayGetCount(windowList) > 1) {
CFRelease(windowList);
self.statusItem.title = [self.statusItem.title substringToIndex:self.statusItem.title.length - 1];
} else {
CFRelease(windowList);
break;
}
}
A tricky part that remains is getting the NSStatusItem window. So far, I've found two methods for obtaining it.
1 - There's a private method called _window. You can utilize it like this:
self.statusItemWindow = [self.statusItem performSelector:#selector(_window)];
2 - This is a bit more complicated but I think this is more likely to pass Apple's static analysis for private method usage in the Mac App Store:
Set the target and action of the NSStatusItem to a method you control, like this:
self.statusItem.target = self;
self.statusItem.action = #selector(itemClicked:);
Then access the window in the invoked method:
- (void)itemClicked:(id)sender {
self.statusItemWindow = [[NSApp currentEvent] window];
}

Resources