DragApp

This example shows one possible way of implementing Drag and Drop for tableviews using bindings and core data. Our purpose is to provide a simple UI for adding members from a pool of all people into a club. The focus of this example is the NSObject subclass named DragSupportDataSource. All of the table views in the application UI are bound to an array controller but have their data source set to a single DragSupportDataSource.

NSTableView drag and drop methods are called on the table view’s datasource. Using infoForBinding API, the DragSupportDataSource can find out which arraycontroller the table view in the drag operation is bound to. Once the destination array controller is found, it’s simple to perform the correct operations.

The data source methods implemented by the DragSupportDataSource return None/0 so that the normal bindings machinery will populate the table view with data. This may seem like a waste, but is a simple way of letting the DragSupportDataSource do the work of registering the table views for dragging. See DragSupportDataSource.py for more information.

Things to keep in mind:

  • The drag and drop implementation assumes all controllers are working with the same NSManagedObjectContext

  • Most of the code in the DragSupportDataSource is for error checking and un/packing objects

Sources

DragAppAppDelegate.py

import Cocoa
import CoreData
import objc


class DragAppAppDelegate(Cocoa.NSObject):
    clubWindow = objc.IBOutlet()
    peopleWindow = objc.IBOutlet()

    _managedObjectModel = objc.ivar()
    _managedObjectContext = objc.ivar()
    _things = objc.ivar()

    def managedObjectModel(self):
        if self._managedObjectModel is None:
            allBundles = Cocoa.NSMutableSet.alloc().init()
            allBundles.addObject_(Cocoa.NSBundle.mainBundle())
            allBundles.addObjectsFromArray_(Cocoa.NSBundle.allFrameworks())

            self._managedObjectModel = (
                CoreData.NSManagedObjectModel.mergedModelFromBundles_(
                    allBundles.allObjects()
                )
            )

        return self._managedObjectModel

    # Change this path/code to point to your App's data store.
    def applicationSupportFolder(self):
        paths = Cocoa.NSSearchPathForDirectoriesInDomains(
            Cocoa.NSApplicationSupportDirectory, Cocoa.NSUserDomainMask, True
        )

        if len(paths) == 0:
            Cocoa.NSRunAlertPanel(
                "Alert", "Can't find application support folder", "Quit", None, None
            )
            Cocoa.NSApplication.sharedApplication().terminate_(self)
        else:
            applicationSupportFolder = paths[0].stringByAppendingPathComponent_(
                "DragApp"
            )

        return applicationSupportFolder

    def managedObjectContext(self):
        if self._managedObjectContext is None:
            fileManager = Cocoa.NSFileManager.defaultManager()
            applicationSupportFolder = self.applicationSupportFolder()

            if not fileManager.fileExistsAtPath_isDirectory_(
                applicationSupportFolder, None
            )[0]:
                fileManager.createDirectoryAtPath_attributes_(
                    applicationSupportFolder, None
                )

            url = Cocoa.NSURL.fileURLWithPath_(
                applicationSupportFolder.stringByAppendingPathComponent_("DragApp.xml")
            )

            coordinator = CoreData.NSPersistentStoreCoordinator.alloc().initWithManagedObjectModel_(  # noqa: B950
                self.managedObjectModel()
            )
            (
                result,
                error,
            ) = coordinator.addPersistentStoreWithType_configuration_URL_options_error_(
                CoreData.NSXMLStoreType, None, url, None, None
            )
            if result:
                self._managedObjectContext = (
                    CoreData.NSManagedObjectContext.alloc().init()
                )
                self._managedObjectContext.setPersistentStoreCoordinator_(coordinator)
            else:
                Cocoa.NSApplication.sharedApplication().presentError_(error)

        return self._managedObjectContext

    def windowWillReturnUndoManager_(self, window):
        return self.managedObjectContext().undoManager()

    @objc.IBAction
    def saveAction_(self, sender):
        res, error = self.managedObjectContext().save_(None)
        if not res:
            Cocoa.NSApplication.sharedApplication().presentError_(error)

    def applicationShouldTerminate_(self, sender):
        context = self.managedObjectContext()

        reply = Cocoa.NSTerminateNow

        if context is not None:
            if context.commitEditing():
                res, error = context.save_(None)
                if not res:
                    # This default error handling implementation should be
                    # changed to make sure the error presented includes
                    # application specific error recovery. For now, simply
                    # display 2 panels.
                    errorResult = Cocoa.NSApplication.sharedApplication().presentError_(
                        error
                    )

                    if errorResult:  # Then the error was handled
                        reply = Cocoa.NSTerminateCancel
                    else:
                        # Error handling wasn't implemented. Fall back to
                        # displaying a "quit anyway" panel.
                        alertReturn = Cocoa.NSRunAlertPanel(
                            None,
                            "Could not save changes while quitting. Quit anyway?",
                            "Quit anyway",
                            "Cancel",
                            None,
                        )
                        if alertReturn == Cocoa.NSAlertAlternateReturn:
                            reply = Cocoa.NSTerminateCancel

            else:
                reply = Cocoa.NSTerminateCancel

        return reply

DragSupportDataSource.py

"""
Abstract: Custom that handles Drag and Drop for table views by acting as a datasource.
"""
import Cocoa
import objc
from objc import super  # noqa: A004


class DragSupportDataSource(Cocoa.NSObject):
    # all the table views for which self is the datasource
    registeredTableViews = objc.ivar()

    def init(self):
        self = super().init()
        if self is None:
            return None

        self.registeredTableViews = Cocoa.NSMutableSet.alloc().init()
        return self

    # ******** table view data source necessities *********

    # We use this method as a way of registering for drag types for all
    # the table views that will depend on us to implement D&D. Instead of
    # setting up innumerable outlets, simply depend on the fact that every
    # table view will ask its datasource for number of rows.
    def numberOfRowsInTableView_(self, aTableView):
        # this is potentially slow if there are lots of table views
        if not self.registeredTableViews.containsObject_(aTableView):
            aTableView.registerForDraggedTypes_([Cocoa.NSStringPboardType])
            # Cache the table views that have "registered" with us.
            self.registeredTableViews.addObject_(aTableView)

        # return 0 so the table view will fall back to getting data from
        # its binding
        return 0

    def tableView_objectValueForTableColumn_row_(self, aView, aColumn, rowIdx):
        # return None so the table view will fall back to getting data from
        # its binding
        return None

    # put the managedobject's ID on the pasteboard as an URL
    def tableView_writeRowsWithIndexes_toPasteboard_(self, tv, rowIndexes, pboard):
        success = False

        infoForBinding = tv.infoForBinding_(Cocoa.NSContentBinding)
        if infoForBinding is not None:
            arrayController = infoForBinding.objectForKey_(Cocoa.NSObservedObjectKey)
            objects = arrayController.arrangedObjects().objectsAtIndexes_(rowIndexes)

            objectIDs = Cocoa.NSMutableArray.array()
            for i in range(objects.count()):
                item = objects[i]
                objectID = item.objectID()
                representedURL = objectID.URIRepresentation()
                objectIDs.append(representedURL)

            pboard.declareTypes_owner_([Cocoa.NSStringPboardType], None)
            pboard.addTypes_owner_([Cocoa.NSStringPboardType], None)
            success = pboard.setString_forType_(
                objectIDs.componentsJoinedByString_(", "), Cocoa.NSStringPboardType
            )

        return success

    # *************** actual drag and drop work *****************
    def tableView_validateDrop_proposedRow_proposedDropOperation_(
        self, tableView, info, row, operation
    ):
        # Avoid drag&drop on self. This might be interersting to enable in
        # light of ordered relationships
        if info.draggingSource() is not tableView:
            return Cocoa.NSDragOperationCopy
        else:
            return Cocoa.NSDragOperationNone

    def tableView_acceptDrop_row_dropOperation_(self, tableView, info, row, operation):
        success = False
        urlStrings = info.draggingPasteboard().stringForType_(Cocoa.NSStringPboardType)

        # get to the arraycontroller feeding the destination table view
        destinationContentBindingInfo = tableView.infoForBinding_(
            Cocoa.NSContentBinding
        )
        if destinationContentBindingInfo is not None:
            destinationArrayController = destinationContentBindingInfo.objectForKey_(
                Cocoa.NSObservedObjectKey
            )
            sourceArrayController = None

            # check for the arraycontroller feeding the source table view
            contentSetBindingInfo = destinationArrayController.infoForBinding_(
                Cocoa.NSContentSetBinding
            )
            if contentSetBindingInfo is not None:
                sourceArrayController = contentSetBindingInfo.objectForKey_(
                    Cocoa.NSObservedObjectKey
                )

            # there should be exactly one item selected in the source controller, otherwise
            # the destination controller won't be able to manipulate the relationship when
            # we do addObject:
            if (sourceArrayController is not None) and (
                sourceArrayController.selectedObjects().count() == 1
            ):
                context = destinationArrayController.managedObjectContext()
                destinationControllerEntity = Cocoa.NSEntityDescription.entityForName_inManagedObjectContext_(  # noqa: B950
                    destinationArrayController.entityName(), context
                )

                items = urlStrings.split(", ")
                itemsToAdd = []

                for i in range(len(items)):
                    urlString = items[i]

                    # take the URL and get the managed object - assume
                    # all controllers using the same context
                    url = Cocoa.NSURL.URLWithString_(urlString)
                    objectID = context.persistentStoreCoordinator().managedObjectIDForURIRepresentation_(  # noqa: B950
                        url
                    )
                    if objectID is not None:
                        value = context.objectRegisteredForID_(objectID)

                        # make sure objects match the entity expected by
                        # the destination controller, and not already there
                        if (
                            value is not None
                            and (value.entity() is destinationControllerEntity)
                            and not (
                                destinationArrayController.arrangedObjects().containsObject_(
                                    value
                                )
                            )
                        ):
                            itemsToAdd.append(value)

                if len(itemsToAdd) > 0:
                    destinationArrayController.addObjects_(itemsToAdd)
                    success = True

        return success

main.py

import DragAppAppDelegate  # noqa: F401
import DragSupportDataSource  # noqa: F401
from PyObjCTools import AppHelper

if __name__ == "__main__":
    AppHelper.runEventLoop()

setup.py

"""
Script for building the example.

Usage:
    python3 setup.py py2app
"""
from setuptools import setup

setup(
    name="DragApp",
    app=["main.py"],
    data_files=["English.lproj"],
    options={"py2app": {"datamodels": ["DragApp_DataModel.xcdatamodel"]}},
    setup_requires=["py2app", "pyobjc-framework-Cocoa", "pyobjc-framework-CoreData"],
)