Difficulty with Drag and Drop - swift

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)")
}
}
}

Related

MacOS-Drag String onto NSStatusItem

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

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)")
}
}

Dropping into NSBox subclass doesn't work

I have created NSBox subclass to implement drag-and-drop. I followed this tutorial. Code looks like this:
import Cocoa
class EditCard: NSBox, NSDraggingSource, NSPasteboardItemDataProvider, NSPasteboardWriting {
var value: String = "0"
let supportedTypes: [NSPasteboard.PasteboardType] = [.string]
func highlight() {
self.borderColor = NSColor.controlAccentColor
self.borderWidth = 2.0
}
func unhighlight() {
self.borderColor = NSColor.clear
self.borderWidth = 0.0
}
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
let canReadPasteboardObjects = sender.draggingPasteboard.canReadObject(forClasses: [NSString.self], options: nil)
if canReadPasteboardObjects {
highlight()
return .copy
}
return NSDragOperation()
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
sender.draggingDestinationWindow?.orderFrontRegardless()
guard let pasteboardObjects = sender.draggingPasteboard.readObjects(forClasses: [NSString.self], options: nil), pasteboardObjects.count > 0 else { return false }
pasteboardObjects.forEach { (object) in
//Add both views to group
}
sender.draggingDestinationWindow?.orderFrontRegardless()
return true
}
override func draggingEnded(_ sender: NSDraggingInfo) {
unhighlight()
}
override func draggingExited(_ sender: NSDraggingInfo?) {
unhighlight()
}
override func concludeDragOperation(_ sender: NSDraggingInfo?) {
highlight()
}
override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
return true
}
func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
return .copy
}
func pasteboard(_ pasteboard: NSPasteboard?, item: NSPasteboardItem, provideDataForType type: NSPasteboard.PasteboardType) {
switch type {
case .string:
guard let indexData = value.data(using: .utf8) else { return }
item.setData(indexData, forType: type)
default: break
}
}
func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
var types = [NSPasteboard.PasteboardType]()
types.append(.string)
return types
}
func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
return nil
}
override func mouseDragged(with event: NSEvent) {
let pasteboardItem = NSPasteboardItem()
pasteboardItem.setDataProvider(self, forTypes: [.string])
let pdfData = self.dataWithPDF(inside: self.bounds)
let imageFromPDF = NSImage(data: pdfData)
let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
draggingItem.setDraggingFrame(self.bounds, contents: imageFromPDF)
beginDraggingSession(with: [draggingItem], event: event, source: self)
}
override func awakeFromNib() {
self.wantsLayer = true
self.registerForDraggedTypes(supportedTypes)
}
}
I want to have this view to work both ways. Dragging this view works, but other views of the same subclass don't highlight when putting dragged view over it. When I try to print() on any function that accepts dragging (draggingEntered(), performDragOperation(), etc) nothing prints. Can somebody help me?

Spring load NSOutlineView leaf rows

I've subclassed NSOutlineView and implemented NSSpringLoadingDestination, but springLoadingActivated() is only ever called on non-leaf rows, even though I've implemented springLoadingEntered() and springLoadingUpdated() to indiscriminately return [.enabled]. I assume NSOutlineView's built-in drag & drop support is interfering with my attempts. Is there a workaround for this?
Feels a little hacky, but it seems I can get away with this by spring loading the rows, but also overriding all of the NSDraggingDestination methods to pass the word on to the parent NSOutlineView:
class SpringLoadedOutlineRow: NSTableCellView, NSSpringLoadingDestination {
var outlineView: NSOutlineView! {
didSet {
registerForDraggedTypes(outlineView.registeredDraggedTypes)
}
}
//Pass through `NSDraggingDestination` methods so we don't interfere with `NSOutlineView`
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
return outlineView.draggingEntered(sender)
}
override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
return outlineView.prepareForDragOperation(sender)
}
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
return outlineView.performDragOperation(sender)
}
override func concludeDragOperation(_ sender: NSDraggingInfo?) {
outlineView.concludeDragOperation(sender)
}
override func draggingExited(_ sender: NSDraggingInfo?) {
outlineView.draggingExited(sender)
}
//Only pass through if we're expandable & not yet expanded (to get `NSOutlineView`'s default disclosure spring-loading); otherwise it will eat our leaf spring-loading
override func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation {
guard let representedItem = outlineView.item(atRow: outlineView.row(for: self)) else { return outlineView.draggingUpdated(sender) }
if outlineView.isExpandable(representedItem) && !outlineView.isItemExpanded(representedItem) {
return outlineView.draggingUpdated(sender)
} else {
return []
}
}
//Actually spring-load!
func springLoadingActivated(_ activated: Bool, draggingInfo: NSDraggingInfo) {
//Whatever you want
}
func springLoadingHighlightChanged(_ draggingInfo: NSDraggingInfo) {
}
func springLoadingEntered(_ draggingInfo: NSDraggingInfo) -> NSSpringLoadingOptions {
return [.enabled]
}
func springLoadingUpdated(_ draggingInfo: NSDraggingInfo) -> NSSpringLoadingOptions {
return [.enabled]
}
}

Swift App runs but no buttons appear

I wrote a Swift app but only the window appears when it runs. I can't see any buttons.
Here is my code... I've tried removing the .white attribute thinking maybe it was hidden behind a layer. Nothing.
//
// ViewController.swift
// BraviaRemote
//
// Created by Ed Gilroy on 7/2/17.
// Copyright © 2017 Edward Williams. All rights reserved.
//
import Cocoa
import Alamofire
class ViewController: NSViewController, NSTextFieldDelegate {
#IBAction func MenuButton(_ sender: NSButtonCell) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAABgAw==")
}
#IBAction func ReturnButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAgAAAJcAAAAjAw==")
}
#IBAction func InfoButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAAA6Aw==")
}
#IBAction func GuideButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAgAAAKQAAABbAw==")
}
#IBAction func SelectButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAABlAw==")
}
#IBAction func ChnUpButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAAAQAw==")
}
#IBAction func ChnDownButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAAARAw==")
}
#IBAction func VolUpButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAAASAw==")
}
#IBAction func VolDownButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAAATAw==")
}
#IBAction func LeftButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAAA0Aw==")
}
#IBAction func RightButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAAAzAw==")
}
#IBAction func UpButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAAB0Aw==")
}
#IBAction func DownButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAAB1Aw==")
}
#IBAction func OnOffButton(_ sender: NSSegmentedControl){
}
#IBOutlet weak var IPField: NSTextField!
var IPAddress: String? {
didSet {
if IPField != nil { IPAddress = "http://\(IPAddress!)/sony/IRCC?" }
else {IPAddress = "http://192.168.2.7/sony/IRCC?"}
if let ip = IPAddress { print (ip) } //Unwraps optional
}
}
override func controlTextDidChange(_ obj: Notification) {
if let txtField = obj.object as? NSTextField {
if txtField.tag == 0 {
//Validation (for later)
IPAddress = txtField.stringValue
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
func viewDidLoad() {
super.viewDidLoad()
}
}
override func viewDidAppear() {
// Window Properties, including solid colour, lack of resize, movable by background.
view.window?.titlebarAppearsTransparent = true
view.window?.backgroundColor = NSColor.white
view.window?.styleMask.remove(.resizable)
view.window?.isMovableByWindowBackground = true
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
struct SOAPEncoding: ParameterEncoding {
let service: String
let action: String
let IRCCC: String
func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
var urlRequest = try urlRequest.asURLRequest()
guard parameters != nil else { return urlRequest }
if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
urlRequest.setValue("text/xml", forHTTPHeaderField: "Content-Type")
}
let soapBody = "<?xml version=\"1.0\" encoding=\"utf-8\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body><u:\(action) xmlns:u=\"\(service)\"><IRCCCode>\(IRCCC)</IRCCCode></u:X_SendIRCC></s:Body></s:Envelope>"
urlRequest.httpBody = soapBody.data(using: String.Encoding.utf8)
return urlRequest
}
}
func triggerRemoteControl(irccc: String) {
Alamofire.request(IPAddress!,
method: .post,
parameters: ["parameter" : "value"],
encoding: SOAPEncoding(service: "urn:schemas-sony-com:service:IRCC:1",
action: "X_SendIRCC", IRCCC: irccc)).responseString { response in
print(response)
}
}
}
Three errors:
First, you are overriding viewDidLoad() and defining another viewDidLoad() inside of it.
Your code:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
func viewDidLoad() {
super.viewDidLoad()
}
}
Should just look like this:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
Second, you are overriding viewDidAppear but never calling super.
Your code:
override func viewDidAppear() {
// Window Properties, including solid colour, lack of resize, movable by background.
view.window?.titlebarAppearsTransparent = true
view.window?.backgroundColor = NSColor.white
view.window?.styleMask.remove(.resizable)
view.window?.isMovableByWindowBackground = true
}
Should look like this:
override func viewDidAppear() {
super.viewDidAppear()
// Window Properties, including solid colour, lack of resize, movable by background.
view.window?.titlebarAppearsTransparent = true
view.window?.backgroundColor = NSColor.white
view.window?.styleMask.remove(.resizable)
view.window?.isMovableByWindowBackground = true
}
Third, you are overriding the IPAdress didSet and then setting it again. This will cause an infinite loop. You are also comparing a textField to nil, which it will never be, because it's a NSTextField!, instead of checking whether it's empty or not. I can't really make sense of what you're trying to achieve here but you should rip all this overriding nonsense out until you can clearly formulate your intention.