Drag messages from Mail onto Dock using Swift - macos

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.

Related

Mac Catalyst cannot capture .command key modifier with pressesBegan override

I'm developing a remote desktop control application for iOS (and MacOS through Mac Catalyst) that must be able to capture all keyboard input on the device including the Cmd key (equivalent to Super / Start key on non Mac keyboards) when the app is in the foreground in order to send them to the remote desktop.
I have not yet tried to see if an iOS device with an external keyboard sees the .command key modifier, but when I enabled Mac Catalyst support and installed the app on my Mac and added the following methods to AppDelegate:
override func pressesBegan(_ presses: Set<UIPress>,
with event: UIPressesEvent?) {
super.pressesBegan(presses, with: event)
print(presses.first?.key, presses.first?.key?.modifierFlags)
}
override func pressesEnded(_ presses: Set<UIPress>,
with event: UIPressesEvent?) {
super.pressesEnded(presses, with: event)
print(presses.first?.key, presses.first?.key?.modifierFlags)
}
override func pressesCancelled(_ presses: Set<UIPress>,
with event: UIPressesEvent?) {
super.pressesCancelled(presses, with: event)
print(presses.first?.key, presses.first?.key?.modifierFlags)
}
I was able to capture pretty much any key combination I try except when the Cmd/Start/Super key is also in the key-combination. When the Cmd key is in the key combination or pressed alone, there is absolutely nothing sent to the app. The event appears to be reserved and consumed by Mac OS X completely.
For completeness to this post, I'd like to add that I tried removing all the menus from the app as well just in case the menu was to blame for consuming the Cmd key events, but nothing changed:
override func buildMenu(with builder: UIMenuBuilder) {
if builder.system == .main {
builder.remove(menu: .edit)
builder.remove(menu: .format)
builder.remove(menu: .help)
builder.remove(menu: .file)
builder.remove(menu: .window)
builder.remove(menu: .view)
let dummyCommand = UICommand(title: "Dummy",
action: #selector(dummy),
discoverabilityTitle: "dummy")
let mainMenu = UIMenu(title: "Dummy", image: nil, identifier: UIMenu.Identifier("dummy"), options: .displayInline, children: [dummyCommand])
builder.replace(menu: .application, with: mainMenu)
}
}
I've also tried putting the app into fullscreen mode to no avail.
Any other suggestions on how I can capture the .command modifier?
Next I'm going to try capturing input through the AppKit bundle, but that's not ideal.
Thank you very much!
Hopefully you have an answer already, but in case not:
You will want to inspect the press with code like below
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
var didHandleEvent : Bool = true
print("presses began \(presses.count)")
for press in presses {
guard let key = press.key else { continue }
if key.modifierFlags.contains(.command) {
commandDown = true
print("commandDown")
}
...
You should also only call super.pressesBegan if you want the system to handle the keypress in addition to your code. In your case, I suspect that you don't want the system to do this, so track what presses you have handled, and only if not handled would you call super.
You will also likely want to set your view controller to .becomeFirstResponder()
(assuming you are using a UIVIewController )

How to get state restoration to restore all windows/tabs on macOS?

I have a non-document based macOS AppKit app. It has one window instantiated automatically by the storyboard. I have sub-classed NSWindowController and added a override func newWindowForTab(_ sender: Any?) to enable the + button on the tab-bar. My main view controller lets the user rename the tab title and the window title is set to the same. This is kind of like how Xcode tab renaming works.
Additionally I have sub-classed NSWindow and added a restorableStateKeyPaths to ensure tab and window titles are automatically restored on app restart.
This all works great.
But only for the first tab. The main window is loaded and it has the tab and window titles set automatically.
The other tabs (windows) are not restored.
Any hints on what I miss to make all tabs restored?
My NSWindowController:
class MyWindowController: NSWindowController {
var subview: MyWindowController?
#IBAction override func newWindowForTab(_ sender: Any?) {
let story = self.storyboard
let windowVC = story?.instantiateInitialController() as! Self
window?.addTabbedWindow(windowVC.window!, ordered: .above)
subview = windowVC
windowVC.window?.orderFront(self.window)
windowVC.window?.makeKey()
}
}
My NSWindow:
class MyWindow: NSWindow {
override class var restorableStateKeyPaths: [String] {
return [ "self.tab.title", "self.title" ]
}
}
First you need to make sure that state restoration is enabled for your user, you can do this by going to Preferences->General and unchecking "Close windows when quitting an app".
Then you should use a restoration class in order to restore all open windows.
Basically if an NSWindow doesn't have a restoration class it won't be preserved across launches, that includes your storyboard loaded window. In this case what is happening is Cocoa is ignoring all window preservation because you haven't defined a restoration class for any of your windows so it resorts to its default behavior which is loading the initial storyboard controller.
Implementing restoration class is easy, just create a restoration class that inherits from NSObject and conforms to NSWindowRestoration, then implement its only required type method restoreWindow(identifier:state:completionHandler) like so:
class MyAppWindowRestoration: NSObject, NSWindowRestoration {
static func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier,
state: NSCoder,
completionHandler: #escaping (NSWindow?, Error?) -> Void) {
// 1.- Retrieve and show the window
// Retrieve a new instance of the only window
let window = (NSStoryboard.main?.instantiateInitialController() as? NSWindowController)?.window
// Call the completion handler with the window and no errors
completionHandler(window, nil)
}
}
Then just assign this class as the window restoration class on every window you want restored, you can do this everywhere after the window has loaded:
window.restorationClass = MyAppWindowRestoration.self
Unfortunately Apple's documentation on state restoration completely sucks so if you have any more questions let me know ;)

Hide NSWindow New tab button

In macOS 10.12 there is a new tab bar that is added to NSWindows for NSDocument apps. You can prevent the toolbar from appearing (see How do I disable the Show Tab Bar menu option in Sierra apps?). But how to remove the "+" button for adding new Windows?
According to the AppKit release notes, returning false for responding newWindowForTab(_:) action message in a NSDocumentController subclass disables "+" button in the tab bar.
override func responds(to aSelector: Selector!) -> Bool {
if #available(OSX 10.12, *) {
if aSelector == #selector(NSResponder.newWindowForTab(_:)) {
return false
}
}
return super.responds(to: aSelector)
}
See "New Button" section in the AppKit Release Notes for macOS 10.12.
Depends of your Application functionality you may subclass NSDocumentController and return empty array for documentClassNames property.
class MyDocumentController: NSDocumentController {
override var documentClassNames: [String] {
return [] // This will disable "+" plus button in NSWindow tab bar.
}
}
Here is a documentation of the documentClassNames property:
documentClassNames
An array of strings representing the custom document classes supported by this app.
The items in the array are NSString objects, each of which represents the name of a document subclasses supported by the app. The document class names are derived from the app’s Info.plist. You can override this property and use it to return the names of document classes that are dynamically loaded from plugins.
Source
And here is explanation how documentClassNames property affects NSWindow tab bar plus button appearance:
New Button
The plus button will be shown if newWindowForTab: is implemented in the responder chain. NSDocumentController informally implements newWindowForTab:, but only returns YES from respondsToSelector: for this selector if the self.documentClassNames.count > 0 and if the app has a default new document type. In other words, it only responds to it if NSDocument has at least one registered document class name which can be edited.
Source
Just set ‘Tabbing Mode’ to Disallowed in Interface Builder for your NSWindow.
Change this
#IBAction override func newWindowForTab(_ sender: Any?) {}
into this
#IBAction func myButton(_ sender: Any?) {}
This will hide the plus button. The tabbing still works

How to get resolution change event in swift?

I try to make an app, and now i shoud make some changes when screen resolution will change, but i coudn't find how to intercept this event.
Do you have any ideea how can i take that event?
The NSApplicationDidChangeScreenParametersNotification is posted when the configuration of the displays attached to the computer is changed, so
you can register for that notification, e.g. with
NSNotificationCenter.defaultCenter().addObserverForName(NSApplicationDidChangeScreenParametersNotification,
object: NSApplication.sharedApplication(),
queue: NSOperationQueue.mainQueue()) {
notification -> Void in
println("screen parameters changed")
}
Note that there can be various reasons why this notification is
fired, e.g. a change in the dock size (as observed in Cocoa Dock fires NSApplicationDidChangeScreenParametersNotification), so you have to
"remember" the old resolution and compare it with the new resolution.
Swift 4:
The didChangeScreenParametersNotification is posted when the configuration of the displays attached to the computer is changed.
Inside the func applicationDidFinishLaunching() in AppDelegate class or func viewDidLoad() in ViewController class, insert the following code:
NotificationCenter.default.addObserver(forName: NSApplication.didChangeScreenParametersNotification,
object: NSApplication.shared,
queue: OperationQueue.main) {
notification -> Void in
print("screen parameters changed")}
I personally, used it to center the position of my application when switching between the Mac and the external screen.
Here is the updated Swift 3 code:
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSApplicationDidChangeScreenParameters,
object: NSApplication.shared(),
queue: OperationQueue.main) {
notification -> Void in
print("screen parameters changed")
}
Code for Swift 5+
NotificationCenter.default.addObserver(
forName: NSNotification.Name(rawValue: "NSApplicationDidChangeScreenParametersNotification"),
object: NSApplication.shared,
queue: .main) { notification in
self.adjustUIIfNeeded()
}

How to handle with a default URL scheme

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 { … }

Resources