DragApp
=======

* :download:`Download example <PyObjCExample-DragApp.zip>`

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


.. rst-class:: tabber

Sources
-------

.. rst-class:: tabbertab

DragAppAppDelegate.py
.....................

.. sourcecode:: python

    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

.. rst-class:: tabbertab

DragSupportDataSource.py
........................

.. sourcecode:: python

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

.. rst-class:: tabbertab

main.py
.......

.. sourcecode:: python

    import DragAppAppDelegate  # noqa: F401
    import DragSupportDataSource  # noqa: F401
    from PyObjCTools import AppHelper
    
    if __name__ == "__main__":
        AppHelper.runEventLoop()

.. rst-class:: tabbertab

setup.py
........

.. sourcecode:: python

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

