Spring load NSOutlineView leaf rows - swift

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

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

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

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?

IGListSectionController's didUpdate and cellForItem always re-called, even though isEqual == true

Trying to implement the IGListKit library, I'm running into the issue that my cells are updated unnecessarily. I'm using a singleton adapter.dataSource with one section per row in the table.
Minimum example:
import IGListKit
class ContentItem: ListDiffable {
weak var item: Content?
weak var section: ContentSectionController?
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
return true
}
init(item: Content?) {
self.item = item
}
}
class ContentSectionController: ListSectionController {
weak var object: ContentItem?
override func didUpdate(to object: Any) {
self.object = object as? ContentItem
self.object?.section = self
// should only be called on updates
}
override func sizeForItem(at index: Int) -> CGSize {
guard let content = object?.item else {
return CGSize(width: 0, height: 0)
}
// calculate height
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
let cell = collectionContext!.dequeueReusableCellFromStoryboard(withIdentifier: "ContentCell", for: self, at: index)
(cell as? ContentCell)?.item = object // didSet will update cell
return cell
}
override init() {
super.init()
self.workingRangeDelegate = self
}
}
extension ContentSectionController: ListWorkingRangeDelegate {
func listAdapter(_ listAdapter: ListAdapter, sectionControllerWillEnterWorkingRange sectionController: ListSectionController) {
// prepare
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerDidExitWorkingRange sectionController: ListSectionController) {
return
}
}
class ContentDataSource: NSObject {
static let sharedInstance = ContentDataSource()
var items: [ContentItem] {
return Content.displayItems.map { ContentItem(item: $0) }
}
}
extension ContentDataSource: ListAdapterDataSource {
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return items
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
return ContentSectionController()
}
func emptyView(for listAdapter: ListAdapter) -> UIView? {
return nil
}
}
/// VC ///
class ContentViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
let updater = ListAdapterUpdater()
adapter = ListAdapter(updater: updater, viewController: self, workingRangeSize: 2)
adapter.collectionView = collectionView
adapter.dataSource = ContentDataSource.sharedInstance
}
var adapter: ListAdapter!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
adapter.performUpdates(animated: true)
}
// ...
}
On every view appear I call adapter.performUpdates(animated: true), which should never update the cells since isEqual is overridden with true. Nonetheless, all cells' didUpdate is triggered, calling cellForItem again too.
IGListKit requires both diffIdentifier and isEqual to be implemented with the IGListDiffable protocol in order to compare the identity/equality of two objects. (You're missing the diff identifier in your model).
My understanding is that under the hood, ListKit checks to see if the two diff identifiers of the objects are equal, if they are THEN it moves on to comparing them with isEqual.
Resources:
IGListKit Best Practices
IGListDiffable Protocol Reference

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.