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