Get currently selected text in active application in Cocoa - macos

I have a status-menu app that can be started using a system wide shortcut. When the app gets active, it would be great if I could somehow get the text that is selected in the currently running application.
So for example I type something in my text-editor, select the text, hit my global shortcut, my app comes up and I would now love to know the selected text from the text-editor.
What I have so far is the following (adopted code from How to get global screen coordinates of currently selected text via Accessibility APIs.)
AXUIElementRef systemWideElement = AXUIElementCreateSystemWide();
AXUIElementRef focussedElement = NULL;
AXError error = AXUIElementCopyAttributeValue(systemWideElement, kAXFocusedUIElementAttribute, (CFTypeRef *)&focussedElement);
if (error != kAXErrorSuccess) {
NSLog(#"Could not get focussed element");
} else {
AXValueRef selectedTextValue = NULL;
AXError getSelectedTextError = AXUIElementCopyAttributeValue(focussedElement, kAXSelectedTextAttribute, (CFTypeRef *)&selectedTextValue);
if (getSelectedTextError == kAXErrorSuccess) {
selectedText = (__bridge NSString *)(selectedTextValue);
NSLog(#"%#", selectedText);
} else {
NSLog(#"Could not get selected text");
}
}
if (focussedElement != NULL) CFRelease(focussedElement);
CFRelease(systemWideElement);
The problem here is that it does not work with apps like Safari and Mail...
Thanks

This is actually very easy, kAXSelectedTextAttribute is your friend.
extension AXUIElement {
var selectedText: String? {
rawValue(for: kAXSelectedTextAttribute) as? String
}
func rawValue(for attribute: String) -> AnyObject? {
var rawValue: AnyObject?
let error = AXUIElementCopyAttributeValue(self, attribute as CFString, &rawValue)
return error == .success ? rawValue : nil
}
}

This is not technically a solution to your exact question because the user would have to trigger this from the Services menu rather than it simply happening when they trigger your menu bar app.
You could use a System Service. You create a service for your app that sends the currently selected text to your menu bar app via a Pasteboard.

Related

Xamarin.IOS UILocalNotification.UserInfo is null after closing application

Local notification works as it should when application is opened. But there are cases when local notification remains in notification center after closing application. And then clicking on notification starts application and notification data should be passed in options in AppDelegate.FinishedLaunching method. Options contains key UIApplicationLaunchOptionsLocalNotificationKey which has value of type UIKit.UILocalNotification. This value contains UserInfo property which should be filled with notification data. But this UserInfo is null.
Another problem is when local notification remains in notification center and application is restarted. Clicking on notification causes application to start again and it stops immediately.
Have you had such problem? Is it problem with Xamarin? How to handle such scenarios?
Creating notification:
public void DisplayNotification(MessageInfo info)
{
var notificationCenter = UNUserNotificationCenter.Current;
var content = new UNMutableNotificationContent();
content.Title = info.Title;
content.Body = info.Body;
content.UserInfo = IosStaticMethods.CreateNsDictFromMessageInfo(info);
UNNotificationTrigger trigger;
trigger = UNTimeIntervalNotificationTrigger.CreateTrigger(0.1, false);
var id = (++_LastNotificationId).ToString();
var request = UNNotificationRequest.FromIdentifier(id, content, trigger);
notificationCenter.Delegate = new UserNotificationCenterDelegate();
notificationCenter.AddNotificationRequest(request, (error) =>
{
//handle error
});
}
internal class UserNotificationCenterDelegate : UNUserNotificationCenterDelegate
{
public override void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
completionHandler(UNNotificationPresentationOptions.Alert);
}
public override void DidReceiveNotificationResponse(UNUserNotificationCenter center, UNNotificationResponse response, Action completionHandler)
{
//do something
}
}
Handling notification in AppDelegate.FinishedLaunching
if (options != null)
{
var notification = options["UIApplicationLaunchOptionsLocalNotificationKey"] as UIKit.UILocalNotification;
var userInfo = notification.UserInfo;//the userInfo is null
}
It seems NSDictionary should not contain NSNull values. After removing NSNull values everything is OK even if NSDictionary documentation https://developer.apple.com/documentation/foundation/nsdictionary says NSNull is allowed.

Unable to get RootViewController in Xamarin iOS

I created app, which Authenticate using Azure AD
In Android it is working fine, but in iOS, it need RootViewController for load the page. But UIApplication.SharedApplication.KeyWindow is null. So I am not able to get UIApplication.SharedApplication.KeyWindow.RootViewController
Bellow is the code:
var authResult = await authContext.AcquireTokenAsync(
graphResourceUri,
ApplicationID,
new Uri(returnUri),
new PlatformParameters(UIApplication.SharedApplication.KeyWindow.RootViewController)
);
Any other way from which I can get RootViewController
This looks stupid but works.
UIWindow window = UIApplication.SharedApplication.KeyWindow;
UIViewController presentedVC = window.RootViewController;
while (presentedVC.PresentedViewController != null)
{
presentedVC = presentedVC.PresentedViewController;
}
I tried this code also, but it is not working.
I got the root cause of this issue. Issue was, when I am going to access the RootViewController then there should be at least one page Initialized, but it is not Initialized so I am unable to get RootViewController
so I gave daily to Initialized the page then I got the RootViewController
Access RootViewController after your window is actually created.
Do this after base.FinishedLauching, like this:
var result = base.FinishedLaunching(app, options);
var platformParameters = UIApplication.SharedApplication.KeyWindow.RootViewController;
App.AuthenticationClient.PlatformParameters = new PlatformParameters(platformParameters);
return result;
Depending on the type of window you have loaded getting the RootViewController can be problematic. This version has been the most stable one I've tried thus far and avoids tail recursive loops.
public UIViewController GetRootController(){
var root = UIApplication.SharedApplication.KeyWindow.RootViewController;
while (true)
{
switch (root)
{
case UINavigationController navigationController:
root = navigationController.VisibleViewController;
continue;
case UITabBarController uiTabBarController:
root = uiTabBarController.SelectedViewController;
continue;
}
if (root.PresentedViewController == null) return root;
root = root.PresentedViewController;
}
}

Present a sheet from a menu item

I have a Cocoa app that calls a sheet from the main window. This works fine from an NSButton on the main window. However, when I call from a MainMenu item, the sheet shows as a separate window. Is this expected behavior, or just expected from me :) I have studied this question ..
I call the sheet with this code:
-(IBAction) showSettingsSheet:(id)sender {
NSLog(#"%s", __FUNCTION__);
settingsSheetController = [[SettingsSheetController alloc] initWithWindowNibName:#"SettingsSheet"];
[settingsSheetController beginSheetModalForWindow:self.window completionHandler:^(NSUInteger returnCode) {
if (returnCode == kSettingsSheetReturnedOk) {
NSLog(#"Settings Returned ok");
} else if (returnCode == kSettingsSheetReturnedCancel) {
NSLog(#"Settings Returned cancel");
} else {
//self.categoryDisplayString = #"Oops!";
NSLog(#" Ooops");
}
}];
}
UPDATE
This is how the Attributes Inspector is set:
Try like this, if you want to display sheet in a mainmenu then try the below steps:-
Uncheck the visible at launch option inside attribute inspector of window which you want to display as a sheet attached the screenshot as well

Getting the position of my application's dock icon using Cocoa's Accessibility API

How can I get the position of my application's dock icon using the Accessibility API?
Found it! Using this forum post as reference, I was able to shape the given sample code to what I needed:
- (NSArray *)subelementsFromElement:(AXUIElementRef)element forAttribute:(NSString *)attribute
{
NSArray *subElements = nil;
CFIndex count = 0;
AXError result;
result = AXUIElementGetAttributeValueCount(element, (CFStringRef)attribute, &count);
if (result != kAXErrorSuccess) return nil;
result = AXUIElementCopyAttributeValues(element, (CFStringRef)attribute, 0, count, (CFArrayRef *)&subElements);
if (result != kAXErrorSuccess) return nil;
return [subElements autorelease];
}
- (AXUIElementRef)appDockIconByName:(NSString *)appName
{
AXUIElementRef appElement = NULL;
appElement = AXUIElementCreateApplication([[[NSRunningApplication runningApplicationsWithBundleIdentifier:#"com.apple.dock"] lastObject] processIdentifier]);
if (appElement != NULL)
{
AXUIElementRef firstChild = (__bridge AXUIElementRef)[[self subelementsFromElement:appElement forAttribute:#"AXChildren"] objectAtIndex:0];
NSArray *children = [self subelementsFromElement:firstChild forAttribute:#"AXChildren"];
NSEnumerator *e = [children objectEnumerator];
AXUIElementRef axElement;
while (axElement = (__bridge AXUIElementRef)[e nextObject])
{
CFTypeRef value;
id titleValue;
AXError result = AXUIElementCopyAttributeValue(axElement, kAXTitleAttribute, &value);
if (result == kAXErrorSuccess)
{
if (AXValueGetType(value) != kAXValueIllegalType)
titleValue = [NSValue valueWithPointer:value];
else
titleValue = (__bridge id)value; // assume toll-free bridging
if ([titleValue isEqual:appName]) {
return axElement;
}
}
}
}
return nil;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
AXUIElementRef dockIcon = [self appDockIconByName:#"MYAPPNAME"];
if (dockIcon) {
CFTypeRef value;
CGPoint iconPosition;
AXError result = AXUIElementCopyAttributeValue(dockIcon, kAXPositionAttribute, &value);
if (result == kAXErrorSuccess)
{
if (AXValueGetValue(value, kAXValueCGPointType, &iconPosition)) {
NSLog(#"position: (%f, %f)", iconPosition.x, iconPosition.y);
}
}
}
}
As for Mac OS El Capitan, looks like you aren't supposed to get the position of the icon using Accessibility API. The matter is that the icon isn't located in accessibility objects hierarchy of the app—it can be found in the hierarchy of the system Dock application. A sandboxed app isn't supposed to access the accessibility objects of other apps.
The code in approved answer doesn't yield any warnings of the sandboxd daemon in the console, looks like it doesn't violate any rules. It creates the top-level accessibility object with the function AXUIElementCreateApplication. The documentation states, that it:
Creates and returns the top-level accessibility object for the
application with the specified process ID.
Unfortunately, this top-level object is not the ancestor of the Dock icon.
I've tried to run the code, and it calculates the position of the first app's main menu item (which has the same title as the app itself). The comparison takes place in this line:
if ([titleValue isEqual:appName]) {
So the output was always the same for my app:
position: (45.000000, 0.000000)
An attempt to access the other app's accessibility object yielded a warning in console. I guess another way to calculate the position of the icon has to be found.

Register as Login Item with Cocoa?

Google gave me: http://developer.apple.com/samplecode/LoginItemsAE/index.html
And I figured there must be a better way than using AppleScript Events.
So I downloaded the Growl sources. They use the exact sources from that Apple developer article.
Is there a better way?
(I refer to Login Items in Accounts in System Preferences, ie. making my program start when the user Logs in, programmatically)
There's an API that's new in Leopard called LSSharedFileList. One of the things it lets you do is view and edit the Login Items list (called Session Login Items in that API).
BTW, I'm the lead developer of Growl. We haven't switched away from AE yet because we still require Tiger, but I'm thinking of dropping that for 1.2 (haven't talked it over with the other developers yet). When we do drop Tiger, we'll drop LoginItemsAE as well, and switch to the Shared File List API.
EDIT from the year 2012: Since 2009, when I originally wrote this answer, Growl has switched to LSSharedFileList and I've left the project.
I stumbled across Ben Clark-Robinson's LaunchAtLoginController. A very elegant solution to a very common problem.
This works on xcode 5.
- (BOOL)isLaunchAtStartup {
// See if the app is currently in LoginItems.
LSSharedFileListItemRef itemRef = [self itemRefInLoginItems];
// Store away that boolean.
BOOL isInList = itemRef != nil;
// Release the reference if it exists.
if (itemRef != nil) CFRelease(itemRef);
return isInList;
}
- (void)toggleLaunchAtStartup {
// Toggle the state.
BOOL shouldBeToggled = ![self isLaunchAtStartup];
// Get the LoginItems list.
LSSharedFileListRef loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
if (loginItemsRef == nil) return;
if (shouldBeToggled) {
// Add the app to the LoginItems list.
CFURLRef appUrl = (__bridge CFURLRef)[NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsRef, kLSSharedFileListItemLast, NULL, NULL, appUrl, NULL, NULL);
if (itemRef) CFRelease(itemRef);
}
else {
// Remove the app from the LoginItems list.
LSSharedFileListItemRef itemRef = [self itemRefInLoginItems];
LSSharedFileListItemRemove(loginItemsRef,itemRef);
if (itemRef != nil) CFRelease(itemRef);
}
CFRelease(loginItemsRef);
}
- (LSSharedFileListItemRef)itemRefInLoginItems {
LSSharedFileListItemRef res = nil;
// Get the app's URL.
NSURL *bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
// Get the LoginItems list.
LSSharedFileListRef loginItemsRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
if (loginItemsRef == nil) return nil;
// Iterate over the LoginItems.
NSArray *loginItems = (__bridge NSArray *)LSSharedFileListCopySnapshot(loginItemsRef, nil);
for (id item in loginItems) {
LSSharedFileListItemRef itemRef = (__bridge LSSharedFileListItemRef)(item);
CFURLRef itemURLRef;
if (LSSharedFileListItemResolve(itemRef, 0, &itemURLRef, NULL) == noErr) {
// Again, use toll-free bridging.
NSURL *itemURL = (__bridge NSURL *)itemURLRef;
if ([itemURL isEqual:bundleURL]) {
res = itemRef;
break;
}
}
}
// Retain the LoginItem reference.
if (res != nil) CFRetain(res);
CFRelease(loginItemsRef);
CFRelease((__bridge CFTypeRef)(loginItems));
return res;
}
I do this in an app I'm writing:
Check out UKLoginItemRegistry for an easy way to do this pragmatically. Afaik, there is no way in Tiger to do this without Apple Events; in Leopard there's a better way, but if you use UKLoginItemRegistry it really is no problem. Here's the complete code for implementing an "Open at Logon" menu item
+ (bool)isAppSetToRunAtLogon {
int ret = [UKLoginItemRegistry indexForLoginItemWithPath:[[NSBundle mainBundle] bundlePath]];
NSLog(#"login item index = %i", ret);
return (ret >= 0);
}
- (IBAction)toggleOpenAtLogon:(id)sender {
if ([PopupController isAppSetToRunAtLogon]) {
[UKLoginItemRegistry removeLoginItemWithPath:[[NSBundle mainBundle] bundlePath]];
} else {
[UKLoginItemRegistry addLoginItemWithPath:[[NSBundle mainBundle] bundlePath] hideIt: NO];
}
}
I've refactored some of the answers here to provide a category on NSApplication that provides a launchAtLogin property.
https://gist.github.com/joerick/73670eba228c177bceb3
SMLoginItemSetEnabled is another modern option, see Modern Login Items article by Cory Bohon where he explains that you have to create a helper application whose sole purpose is to launch the main application. There's also a full step by step explanation in SMLoginItemSetEnabled - Start at Login with App Sandboxed on Stack Overflow.
Check here an open source example: https://github.com/invariant/rhpnotifier (LoginItem.m, LoginItem.h)

Resources