How to handle with a default URL scheme - macos

I want to build URI (or URL scheme) support in my app.
I do a LSSetDefaultHandlerForURLScheme() in my + (void)initialize and I setted the specific URL schemes also in my info.plist. So I have URL schemes without Apple Script or Apple Events.
When I call myScheme: in my favorite browser the system activates my app.
The problem is, how to handle the schemes when they are called. Or better said: How can I define what my app should do, when myScheme: is called.
Is there a special method that I have to implement or do I have to register one somewhere?

As you are mentioning AppleScript, I suppose you are working on Mac OS X.
A simple way to register and use a custom URL scheme is to define the scheme in your .plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>URLHandlerTestApp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>urlHandlerTestApp</string>
</array>
</dict>
</array>
To register the scheme, put this in your AppDelegate's initialization:
[[NSAppleEventManager sharedAppleEventManager]
setEventHandler:self
andSelector:#selector(handleURLEvent:withReplyEvent:)
forEventClass:kInternetEventClass
andEventID:kAEGetURL];
Whenever your application gets activated via URL scheme, the defined selector gets called.
A stub for the event-handling method, that shows how to get the URL string:
- (void)handleURLEvent:(NSAppleEventDescriptor*)event
withReplyEvent:(NSAppleEventDescriptor*)replyEvent
{
NSString* url = [[event paramDescriptorForKeyword:keyDirectObject]
stringValue];
NSLog(#"%#", url);
}
Apple's documentation: Installing a Get URL Handler
Update
I just noticed a problem for sandboxed apps that install the event handler in applicationDidFinishLaunching:. With enabled sandboxing, the handler method doesn't get called when the app is launched by clicking a URL that uses the custom scheme.
By installing the handler a bit earlier, in applicationWillFinishLaunching:, the method gets called as expected:
- (void)applicationWillFinishLaunching:(NSNotification *)aNotification
{
[[NSAppleEventManager sharedAppleEventManager]
setEventHandler:self
andSelector:#selector(handleURLEvent:withReplyEvent:)
forEventClass:kInternetEventClass
andEventID:kAEGetURL];
}
- (void)handleURLEvent:(NSAppleEventDescriptor*)event
withReplyEvent:(NSAppleEventDescriptor*)replyEvent
{
NSString* url = [[event paramDescriptorForKeyword:keyDirectObject]
stringValue];
NSLog(#"%#", url);
}
On the iPhone, the easiest way to handle URL-scheme activation is, to implement UIApplicationDelegate's application:handleOpenURL: - Documentation

All credits should go to weichsel and kch
I'm just adding swift(2.2/3.0) code for your convenience
func applicationWillFinishLaunching(_ notification: Notification) {
NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(self.handleGetURL(event:reply:)), forEventClass: UInt32(kInternetEventClass), andEventID: UInt32(kAEGetURL) )
}
#objc func handleGetURL(event: NSAppleEventDescriptor, reply:NSAppleEventDescriptor) {
if let urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue {
print("got urlString \(urlString)")
}
}

The problem is, how to handle the schemes when they are called.
That's where the Apple Events come in. When Launch Services wants your app to open a URL, it sends your app a kInternetEventClass/kAEGetURL event.
The Cocoa Scripting Guide uses this very task as an example of installing an event handler.

I'm just adding slightly different Swift 4/5 version of the code:
func applicationWillFinishLaunching(_ notification: Notification) {
NSAppleEventManager
.shared()
.setEventHandler(
self,
andSelector: #selector(handleURL(event:reply:)),
forEventClass: AEEventClass(kInternetEventClass),
andEventID: AEEventID(kAEGetURL)
)
}
#objc func handleURL(event: NSAppleEventDescriptor, reply: NSAppleEventDescriptor) {
if let path = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue?.removingPercentEncoding {
NSLog("Opened URL: \(path)")
}
}

You can define the “get URL” command in a scripting terminology SDEF and implement the corresponding method. For example, Terminal’s SDEF contains the following command definition for handling URLs
<command name="get URL" code="GURLGURL" description="Open a command an ssh, telnet, or x-man-page URL." hidden="yes">
<direct-parameter type="text" description="The URL to open." />
</command>
and declares that the application responds to it:
<class name="application" code="capp" description="The application's top-level scripting object.">
<cocoa class="TTApplication"/>
<responds-to command="get URL">
<cocoa method="handleGetURLScriptCommand:" />
</responds-to>
</class>
The TTApplication class (a subclass of NSApplication) defines the method:
- (void)handleGetURLScriptCommand:(NSScriptCommand *)command { … }

Related

Links in iframe are not triggering decidePolicyForNewWindowAction

I have a iframe of youtube player in my webview macos application, and most of the links (the <a> element) inside the iframe are not triggering the decidePolicyForNewWindowAction delegate.
The only working <a> element is the channel link, others like video title, the youtube icon are all silent, and I can't tell the differences between these <a>s.
So why are some links cannot trigger decidePolicyForNewWindowAction?
Documentation of the delegate: https://developer.apple.com/documentation/webkit/webpolicydelegate/1536381-webview?language=objc
Documentation of the iframe youtube player: https://developers.google.com/youtube/iframe_api_reference
For someone maybe interested:
Turns out it's not a iframe problem, it's because some <a> are not directly changing the url, it calls some javascript to do the job for it. This was inspected from the callstack.
In such case, the first thing is that one more delegate needs to be hooked up, the createWebViewWithRequest. This delegate returns nil by default. So change it like this:
- (WebView*)webView:(WebView *)sender createWebViewWithRequest:(NSURLRequest *)request
{
return sender;
}
Not doing much but let the following codes continue.
The second thing is that the javascript url request goes to decidePolicyForNavigationAction instead. So still not firing decidePolicyForNewWindowAction but at least there's a chance to know it.
In my case I add some condition inside decidePolicyForNavigationAction to distinguish my request and iframe's request, like this:
- (void) webView: (WebView*) sender decidePolicyForNavigationAction: (NSDictionary*) actionInformation
request: (NSURLRequest*) request
frame: (WebFrame*) frame
decisionListener: (id <WebPolicyDecisionListener>) listener
{
NSString* mainDocumentURL = [[request mainDocumentURL] absoluteString];
NSString* fromYT = #"https://www.youtube.com";
if ([mainDocumentURL hasPrefix:fromYT]) {
[[NSWorkspace sharedWorkspace] openURL:url]; // fires Safari
[listener ignore];
return;
}
// normal flow here, you could open it inside the webview or something
}

How do I get the selected text from a WKWebView from objective-C

I have a WKWebView.
When the user right-clicks on it, I can customize a contextual menu in my objective-c method. I'd like to add a menu item only if the user has selected some text in the WKWebView. And of course I'll need to retrieve the selected text later on to process it.
How can I retrieve the selection from a WKWebView from objective-c, make sure it is only text and get that text ?
Thanks
Here is how I managed to do that. Not a perfect solution, but good enough.
General explanation
It seems that anything that happens inside the WKWebView must be managed in JavaScript. And Apple provides a framework for exchanging information between the JavaScript world and the Objective-C (or Swift) world. This framework is based on some messages being sent from the JavaScript world and caught in the Objective-C (or Swift) world via a message handler that can be installed in the WKWebView.
First step - Install the message handler
In the Objective-C (or Swift) world, define an object that will be responsible for receiving the messages from the JavaScript world. I used my view controller for that. The code below installs the view controller as a "user content controller" that will receive events named "newSelectionDetected" that can be sent from JavaScript
- (void)viewDidLoad
{
[super viewDidLoad];
// Add self as scriptMessageHandler of the webView
WKUserContentController *controller = self.webView.configuration.userContentController ;
[controller addScriptMessageHandler:self
name:#"newSelectionDetected"] ;
... the rest will come further down...
Second step - Install a JavaScript in the view
This JavaScript will detect selection change, and send the new selection through a message named "newSelectionDetected"
- (void) viewDidLoad
{
...See first part up there...
NSURL *scriptURL = .. URL to file DetectSelection.js...
NSString *scriptString = [NSString stringWithContentsOfURL:scriptURL
encoding:NSUTF8StringEncoding
error:NULL] ;
WKUserScript *script = [[WKUserScript alloc] initWithSource:scriptString
injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
forMainFrameOnly:YES] ;
[controller addUserScript:script] ;
}
and the JavaScript:
function getSelectionAndSendMessage()
{
var txt = document.getSelection().toString() ;
window.webkit.messageHandlers.newSelectionDetected.postMessage(txt) ;
}
document.onmouseup = getSelectionAndSendMessage ;
document.onkeyup = getSelectionAndSendMessage ;
document.oncontextmenu = getSelectionAndSendMessage ;
Third step - receive and treat the event
Now, every time we have a mouse up or a key up in the WKWebView, the selection (possibly empty) will be caught and send to the Objective-C world through the message.
We just need a handler in the view controller to handle that message
- (void) userContentController:(WKUserContentController*)userContentController
didReceiveScriptMessage:(WKScriptMessage*)message
{
// A new selected text has been received
if ([message.body isKindOfClass:[NSString class]])
{
...Do whatever you want with message.body which is an NSString...
}
}
I made a class which inherits from WKWebView, and has a NSString property 'selectedText'. So what I do in this handler, is to store the received NSString in this property.
Fourth step - update the contextual menu
In my daughter class of WKWebView, I just override the willOpenMenu:WithEvent: method to add a menu item if selectedText is not empty.
- (void) willOpenMenu:(NSMenu*)menu withEvent:(NSEvent*)event
{
if ([self.selectedText length]>0)
{
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:#"That works !!!"
action:#selector(myAction:)
keyEquivalent:#""] ;
item.target = self ;
[menu addItem:item] ;
}
}
- (IBAction) myAction:(id)sender
{
NSLog(#"tadaaaa !!!") ;
}
Now why isn't that ideal? Well, if your web page already sets onmouseup or onkeyup, I override that.
But as I said, good enough for me.
Edit: I added the document.oncontextmenu line in the JavaScript, that solved the strange selection behavior I sometimes had.
Swift 5 translation
webView.configuration.userContentController.add(self, name: "newSelectionDetected")
let scriptString = """
function getSelectionAndSendMessage()
{
var txt = document.getSelection().toString() ;
window.webkit.messageHandlers.newSelectionDetected.postMessage(txt);
}
document.onmouseup = getSelectionAndSendMessage;
document.onkeyup = getSelectionAndSendMessage;
document.oncontextmenu = getSelectionAndSendMessage;
"""
let script = WKUserScript(source: scriptString, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(script)
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// Use message.body here
}
One only needs to evaluate simple js script
NSString *script = #"window.getSelection().toString()";
using evaluateJavaScript method
[wkWebView evaluateJavaScript:script completionHandler:^(NSString *selectedString, NSError *error) {
}];
The Swift version
let script = "window.getSelection().toString()"
wkWebView.evaluateJavaScript(script) { selectedString, error in
}

How to close window (NSWindowController) by hitting the ESC key?

Issue
I would like the user being able to close a window by hitting the ESC key but I can't get it to work in this specific case, hitting ESC triggers an error sound (the "no you can't do that" macOS bloop) and nothing happens.
Context
I'm making a subclass of NSWindowController which itself creates an instance of a subclass of NSViewController and sets it in a view. Both controllers have their own xib file.
NSWindowController:
final class MyWindowController: NSWindowController, NSWindowDelegate {
#IBOutlet weak var targetView: MainView!
let myVC: MyViewController!
var params: SomeParams!
override func windowDidLoad() {
super.windowDidLoad()
myVC = MyViewController(someParams: params)
myVC.view.setFrameSize(targetView.frame.size)
myVC.view.setBoundsSize(targetView.bounds.size)
targetView.addSubview(myVC.view)
}
override var windowNibName: String! {
return "MyWindowController"
}
convenience init(someParams params: SomeType) {
self.init(window: nil)
self.params = params
}
}
NSViewController:
final class MyViewController: NSViewController {
convenience init(someParams params: SomeType) {
// do stuff with the params
}
override func viewDidLoad() {
super.viewDidLoad()
// configure stuff for the window
}
}
What I've tried
I suppose that my issue is that the MyWindowController NSWindow is the .initialFirstResponder when I would want the content of the targetView (an NSTableView) to be the first responder - this way I could use keyDown, I guess, and send the close command to the window from there. This doesn't seem optimal, though.
I've tried forcing the view controller views into being the first responder by using window?.makeFirstResponder(theView) in the windowDidLoad of MyWindowController but nothing ever changes.
I've also tried adding this to MyWindowController:
override func cancelOperation(_ sender: Any?) {
print("yeah, let's close!")
}
But this only works if the user clicks first on the background of the window then hits ESC, and it still emits the error sound anyway. Which is actually what made me think that the issue was about the first responder being on the window.
Question
How would you achieve that? Of course, I know that the user can already close the window with CMD+W, but I'd really like to sort out this issue nonetheless.
Note that the code example is in Swift but I can also accept explanations using Objective-C.
The documentation of cancelOperation explains how cancelOperation should work:
This method is bound to the Escape and Command-. (period) keys. The key window first searches the view hierarchy for a view whose key equivalent is Escape or Command-., whichever was entered. If none of these views handles the key equivalent, the window sends a default action message of cancelOperation: to the first responder and from there the message travels up the responder chain.
If no responder in the responder chain implements cancelOperation:, the key window searches the view hierarchy for a view whose key equivalent is Escape (note that this may be redundant if the original key equivalent was Escape). If no such responder is found, then a cancel: action message is sent to the first responder in the responder chain that implements it.
NSResponder declares but does not implement this method.
NSWindow implements cancelOperation: and the next responder, the window controller, isn't checked for an implementation of cancelOperation:. The cancel: message does arrive at the window controller. Implementing
- (void)cancel:(id)sender
{
NSLog(#"cancel");
}
will work. The cancel: message isn't inherited from a superclass so autocompletion doesn't suggest it.
This worked for me in Xcode 10 and Swift 4.2:
#objc func cancel(_ sender: Any?) {
close()
}
I tried it before but without the #objc part and it didn't work. So don't omit it.
When I needed such behavior I implemented it by overriding keyDown: of the NSWindow object.
I.e. something like the following:
- (void)keyDown:(NSEvent *)theEvent
{
int k = [theEvent keyCode];
if (k == kVK_Escape)
{
[self close];
return;
}
[super keyDown:theEvent];
}

Drag messages from Mail onto Dock using Swift

I am trying to get my application to accept a mail message that was dropped onto my application's dock icon directly from Mail.
I have followed this link Dropping Files onto Dock Icon in Cocoa and tried to convert in into Swift and the latest version of Xcode but with no joy.
This is my AppDelegate.Swift file:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate
{
func application(sender: NSApplication, openFile filename: String) -> Bool
{
println(filename)
return true
}
func application(sender: NSApplication, openFiles filenames: [String])
{
println(filenames)
}
}
I have set the document types for my project:
When I drag the mail document from Mail into the dock, then the dock highlights as if it wants to accept it but nothing triggers the openFiles method.
Incidentally if I drag the mail file out of Mail and into the Finder, and then drag it onto the dock icon it works fine.
And Mail drop only seems to work in El Capitan. I can see that mail can now be dropped into TextWrangler; this did not work under Yosemite.
As a bonus I'm offering an additional 50 bounty to anyone who can help me sort this out.
You can extract the mail item's URL by registering your app as a service by adding the following to your app's info.plist:
<key>NSServices</key>
<array>
<dict>
<key>NSMessage</key>
<string>itemsDroppedOnDock</string>
<key>NSSendTypes</key>
<array>
<string>public.data</string>
</array>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>Open Mail</string>
</dict>
</dict>
</array>
Then your Swift app delegate would look like this:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(aNotification: NSNotification) {
NSApp.servicesProvider = self
}
#objc func itemsDroppedOnDock(pboard: NSPasteboard, userData: NSString, error: UnsafeMutablePointer<NSString>) {
// help from https://stackoverflow.com/questions/14765063/get-dropped-mail-message-from-apple-mail-in-cocoa
print("dropped types: \(pboard.types)")
if let types = pboard.types {
for type in types {
print(" - type: \(type) string: \(pboard.stringForType(type))")
}
}
}
}
When you drop a mail message on your app's dock, the output will be something like:
dropped types: Optional(["public.url", "CorePasteboardFlavorType 0x75726C20", "dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu", "Apple URL pasteboard type"])
- type: public.url string: Optional("message:%3C2004768713.4671#tracking.epriority.com%3E")
- type: CorePasteboardFlavorType 0x75726C20 string: Optional("message:%3C2004768713.4671#tracking.epriority.com%3E")
- type: dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu string: Optional("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<array>\n\t<string>message:%3C2004768713.4671#tracking.epriority.com%3E</string>\n\t<string></string>\n</array>\n</plist>\n")
- type: Apple URL pasteboard type string: Optional("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<array>\n\t<string>message:%3C2004768713.4671#tracking.epriority.com%3E</string>\n\t<string></string>\n</array>\n</plist>\n")
Unfortunately, you probably need to figure out how to convert the mail URL "message:%3C2004768713.4671#tracking.epriority.com%3E" into the actual underlying mail file, but it's a start.
Alternatively, if you are willing to accept a drop in your app's window rather than on the dock, you should be able to just use NSDraggingInfo.namesOfPromisedFilesDroppedAtDestination, which is how I expect the Finder is able to copy the mail message when you drop one on a Finder window (note that the Finder does not respond to mail messages being dropped in its dock icon, only when they are dropped on a window).
Edit:
See Dropping promised files on to application icon in Dock on how to get promised file.

Replying to an Apple Event in Cocoa

I'm registering an Apple Event handler using NSAppleEventManager:
[[NSAppleEventManager sharedAppleEventManager]
setEventHandler:self andSelector:#selector(handleGetURLEvent:withReplyEvent:)
forEventClass:kInternetEventClass andEventID:kAEGetURL];
My handler method will, of course, receive the event and a reply event:
- (void) handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
//Open this URL; reply if we can't
}
So, if I need to reply with an error, indicating that I failed in some way to open this URL, how should I use the replyEvent to do that?
I have translated the following from the old-style C procedural API described in Apple's legacy document "Apple Events Programming Guide" to Cocoa:
if ([replyEvent descriptorType] != typeNull)
{
[replyEvent setParamDescriptor:[NSAppleEventDescriptor descriptorWithInt32:someStatusCode] forKeyword:keyErrorNumber];
[replyEvent setParamDescriptor:[NSAppleEventDescriptor descriptorWithString:someErrorString] forKeyword:keyErrorString];
}
See "Returning Error Information" in the "Apple Events Programming Guide" (in the Legacy library).

Resources