MacOS-Drag String onto NSStatusItem - swift

I am trying to detect when a string is getting dragged onto the NSStatusItem. Here is the implementation of my code in AppDelegate:
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let button = statusItem.button {
button.image = NSImage(named:NSImage.Name("StatusBarButtonImage"))
button.action = #selector(menubarAction(_:))
button.sendAction(on: [.leftMouseUp, .rightMouseUp])
button.window?.registerForDraggedTypes([.string ])
button.window?.delegate = self
}
}
And here is my NSWindow delegate calls:
extension AppDelegate:NSWindowDelegate{
func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation{
NSApplication.shared.activate(ignoringOtherApps: true )
return .copy
}
func performDragOperation(_ sender: NSDraggingInfo) -> Bool{
NSApplication.shared.activate(ignoringOtherApps: true )
return true
}
}
However these delegates do not get called when a string is dragged onto the NSStatusItem. I know that drag has been detected as:
applicationDidBecomeActive(_ notification: Notification)
gets called. Any suggestions why my delegate is not getting called.
Thanks
Reza

draggingEntered and performDragOperation are methods of protocol NSDraggingDestination. Swift doesn't call a protocol method if the class doesn't adopt the protocol. AppDelegate must adopt NSDraggingDestination.
extension AppDelegate: NSWindowDelegate {
}
extension AppDelegate: NSDraggingDestination {
func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation{
NSApplication.shared.activate(ignoringOtherApps: true )
return .copy
}
func performDragOperation(_ sender: NSDraggingInfo) -> Bool{
NSApplication.shared.activate(ignoringOtherApps: true )
return true
}
}

Related

Function called for all NSWindow

When I call function, it's called for all window opened and not just for the selected window.
If the function is called by #IBAction It's applied for the selected window. Otherwhise, it's applied for all windows.
How can i call the function just for the current selected window ?
Here is an preview:
This is the minimal reproductible code:
// AppDelegate.swift
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
#objc func openMyWindow()
{
let storyboard:NSStoryboard = NSStoryboard(name: "Main", bundle: nil)
guard let controller:NSWindowController = storyboard.instantiateController(withIdentifier: "WindowMain") as? NSWindowController else { return }
controller.showWindow(self)
}
#objc func test()
{
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "TEST"), object: nil, userInfo: nil)
}
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
let dockMenu = NSMenu()
dockMenu.addItem(withTitle: "New window", action: #selector(openMyWindow), keyEquivalent: "")
dockMenu.addItem(withTitle: "test", action: #selector(test), keyEquivalent: "")
return dockMenu
}
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
// ViewController.swift
import Cocoa
class ViewController: NSViewController {
#objc func Test(){
TextView.string = "It's applied for ALL views -> it's NOT ok"
}
#IBAction func button(_ sender: Any) {
TextView.string = "It's applied just for this view -> it's ok"
}
#IBOutlet var TextView: NSTextView!
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(Test), name: NSNotification.Name(rawValue: "TEST"), object: nil)
}
override var representedObject: Any? {
didSet {
}
}
}
Notification with object nil (no object) are hard to distinguish when it is not even evaluated which of the windows invoked the post.
In other words, make use of the object: parameter when you post the Notification.
Otherwise all registered observers in multiple windows will act on one and the same Notification.
So what object could be used to know who was sending?
The window object itself of course.
Your WindowController has a window it belongs to as well, just compare its address to the posted Notifications object and act when they are the same.
Or compare against the front most windows address, which usually is the window the user expects to act on commands given.
If the target of the menu item isn't set then the action message is sent to the first responder. In your view the text view is the first responder but it doesn't handle the test message and sends it to the next responder. The view controller is in the responder chain and will receive the test message.
Set the selector of the menu item to the action of the view controller and the view controller of the front window will receive it. No notifications required.
// AppDelegate.swift
import Cocoa
#main
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
#objc func openMyWindow() {
let storyboard:NSStoryboard = NSStoryboard(name: "Main", bundle: nil)
guard let controller:NSWindowController = storyboard.instantiateController(withIdentifier: "WindowMain") as? NSWindowController else { return }
controller.showWindow(self)
}
func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
let dockMenu = NSMenu()
dockMenu.addItem(withTitle: "New window", action: #selector(openMyWindow), keyEquivalent: "")
dockMenu.addItem(withTitle: "test", action: #selector(ViewController.test), keyEquivalent: "")
return dockMenu
}
}
// ViewController.swift
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#objc func test() {
TextView.string = "It's applied for this view -> it's now ok"
}
#IBAction func button(_ sender: Any) {
TextView.string = "It's applied just for this view -> it's ok"
}
#IBOutlet var TextView: NSTextView!
}

Subclassed NSView to notify ViewController of action

I have subclassed NSView to receive a dropped folder in order to get its URL.
I've been getting the URL to my ViewController class by accessing a property set in my custom NSView class.
import Cocoa
class DropView: NSView {
var droppedURL : URL!
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}
public required init?(coder: NSCoder) {
super .init(coder: coder)
registerForDraggedTypes([NSPasteboard.PasteboardType.fileURL])
}
public override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
return NSDragOperation.copy
}
public override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
NSDragOperation.copy
}
public override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
let pboard = sender.draggingPasteboard
let urlString = pboard.string(forType: NSPasteboard.PasteboardType.fileURL)
let folderURL = URL(string: urlString!)
print(folderURL)
droppedURL = folderURL
return true
}
}
How can I let my ViewController know when a folder has been dropped onto my NSView and that a URL has been successfully captured? Is there a way other than posting a notification?
Usually you'd use delegates or closures for this. I prefer closures because they're so clean, but it's up to you.
First, define your closure in DropView:
class DropView: NSView {
var droppedURL : URL!
var droppedSuccessfully: ((URL) -> Void)? /// here!
Then, call it just like how you'd call a function. Make sure to pass in your folderURL too.
public override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
let pboard = sender.draggingPasteboard
let urlString = pboard.string(forType: NSPasteboard.PasteboardType.fileURL)
let folderURL = URL(string: urlString!)
print(folderURL)
droppedURL = folderURL
droppedSuccessfully?(folderURL) /// here!
return true
}
Finally, assign the closure back in your ViewController.
override func viewDidLoad() {
super.viewDidLoad()
...
/// prevent retain cycle
yourDropView.droppedSuccessfully = { [weak self] url in
print("URL received: \(url)")
}
}

Difficulty with Drag and Drop

I'm having difficulty understanding how to implement simple drag and drop in a macOS application. What I want to do is make a TextField that can accept a directory or file that is dropped onto it and capture the URL to that directory or file.
With the code shown below, my print("dragging entered") line does fire when I drag an object onto the TextField control, however when I release it the performDragOperation doesn't fire.
Can anyone please help me understand simple drag and drop?
Thanks
import Cocoa
class DropTextField: NSTextField {
var dragTypes : [NSPasteboard.PasteboardType] = [.fileURL, .URL]
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}
override func awakeFromNib() {
self.registerForDraggedTypes(dragTypes)
}
public override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
return true
}
public override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
print("dragging engtered")
return NSDragOperation.copy
}
public override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
print("drag operation")
return true
}
}
Assign this custom class to the NSTextField object on the storyboard and create an outlet in the controller:
public class DragTextField: NSTextField {
public var completionHandler: (URL) -> Void = { fileURL in Swift.print(fileURL) }
public override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { return NSDragOperation.copy }
public override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation { return NSDragOperation.copy }
public override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
let pboard = sender.draggingPasteboard
if let fileURLFromClipboard = pboard.string(forType: NSPasteboard.PasteboardType.fileURL) {
let sourceFileURL = URL(string: fileURLFromClipboard)!
print(sourceFileURL)
completionHandler(sourceFileURL)
}
return true
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
stringValue = "Drop File Here";
isEditable = false
wantsLayer = true;
layer?.backgroundColor = NSColor.blue.cgColor
// Register for file name drag
registerForDraggedTypes([NSPasteboard.PasteboardType.fileURL])
}
}
Usage:
class ViewController: NSViewController {
#IBOutlet weak var dropView: DragTextField!
override func viewDidLoad() {
super.viewDidLoad()
dropView.completionHandler = {
print("File Path: \($0)")
}
}
}

Checking for key down on menu bar application

I am writing a MacOS Menu Bar Application which uses a popover. I have relied on a number of tutorials to get things going.
Very briefly, the code looks something like this:
class AppDelegate: NSObject, NSApplicationDelegate {
var popover=NSPopover()
var statusBarItem: NSStatusItem!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Popover & Content View
let contentView = ContentView()
self.popover.contentViewController = NSHostingController(rootView: contentView)
// Menu
self.statusBarItem = NSStatusBar.system.statusItem(withLength: 18)
if let statusBarButton = self.statusBarItem.button {
statusBarButton.title = "☰"
statusBarButton.action = #selector(togglePopover(_:))
}
}
#objc func togglePopover(_ sender: AnyObject?) {
let statusBarButton=self.statusBarItem.button!
func show(_ sender: AnyObject) {
self.popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
}
func hide(_ sender: AnyObject) {
popover.performClose(sender)
}
self.popover.isShown ? hide(sender as AnyObject) : show(sender as AnyObject)
}
}
How can I check whether the option key is down when the menu button is clicked?
Ask the current event whether the modifier flags contain option
func isOptionkeyPressed() -> Bool
{
return NSApp.currentEvent?.modifierFlags.contains(.option) == true
}

Why is windowDidMove() not being called? [duplicate]

I'm trying to know when a window closes, I implemented this code:
class ViewController: NSViewController, NSWindowDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let window: NSWindow? = view.window
window?.delegate = self
}
func windowWillClose(_ aNotification: Notification) {
print("windowWillClose")
}
}
Unfortunately nothing happens, what could I made wrong?
Documents: https://developer.apple.com/documentation/appkit/nswindow/1419400-willclosenotification
PS
I already read this question without to find a solution: Handle close event of the window in Swift
The problem there is that the window property will always return nil inside viewDidLoadMethod. You need to set the delegate inside viewWillAppear method:
class ViewController: NSViewController, NSWindowDelegate {
override func viewWillAppear() {
super.viewWillAppear()
view.window?.delegate = self
}
func windowWillClose(_ aNotification: Notification) {
print("windowWillClose")
}
}