# DragApp

- `Download example`

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"],
    )