Wilcox Development Solutions Blog

Dragging and Dropping with NSTableViews and PyObjC

October 20, 2004

While working on a project for one of our clients, we realized that we needed to create a Cocoa application that uses a NSTableView to display a list of files and the order in which our application should process them.

In this entry I hope to guide you through some of the oddities I encountered while working on this application, while also providing an example program demonstrating table views and drag and drop using PyObjC.

Our application is written in PyObjC. I’ll go through my code and highlight the main points, and I’ll explain why I’m doing things the way I am. At the top of the code we need to tell Python what modules we need to import.

Imports

from Foundation import * from PyObjCTools import NibClassBuilder from AppKit import *

Foundation and AppKit provide us all the basic classes we will need for our application. NibClassBuilder is used as the base class for our delegate. The next line will tell NibClassBuilder to extract our .nib file. NibClassBuilder.extractClasses("MainMenu")

Next we have our class declaration.

Controller Class Declaration

class reorderRowsAppDelegate(NibClassBuilder.AutoBaseClass):

Class will be included and named automatically by XCode when you create a Cocoa-Python application project.

Preparation Functions

init() and awakeFromNib() will help to prepare the necessary objects for use in our application.

def init(self): self.abcList = ["a", "b", "c", "d", "e"] NSLog("init called") return self

In this example we have an NSTableView that has two columns - the position column and the letter column. The identifiers for these columns are set in Interface Builder by selecting the column in the NSTableView and bringing up the Attributes section in the Info palette. We need to create a list in our init() function to use when we populate the NSTableView later on.

Then we simply log the fact that init() was called. Finally we need to return self. Please note that you wouldn’t normally have to do this in Python, but in Cocoa, this is the expected behavior.

Also note that any outlets you have defined in your .nib file are not available for use at this point in the program. They will not be able for use until awakeFromNib() is called.

Note that all class methods take self as their first parameter (even those methods that, in Objective-C, would not get a parameters passed to it.) This is expected by Python programmers, and part of the basis of Python’s OOP system.

Our next function will handle what happens when the objects on our .nib file are “awoken”.

def awakeFromNib(self): NSLog("awakeFromNib called") self.tableView.registerForDraggedTypes_( [NSTabularTextPboardType, NSStringPboardType] )

Here we log the fact that this method got called. Then we need to tell our NSTableView object to register itself to be able to drag certain NSPasteBoard types. The NSPasteBoard types will be used in the dragging and reordering of our rows. Here we are registering our NSTableView object for NSTabularTextPboardType and NSStringPboardType.

We need to call registerForDraggedTypes_() here. If the NSPastedBoard is not registered for a certain type, you will not be able to implement dragging and dropping later on.

Our next function will simply log a message when the application finishes launching.

def applicationDidFinishLaunching_(self, aNotification): NSLog( "Application did finish launching." )

This function will also be automatically included by XCode when you create a Cocoa-Python application.

NSTableView Implementation

The next two functions must be implemented in your class in order for your NSTableView object to work correctly.

def numberOfRowsInTableView_(self, sender): return (len(self.abcList))

This function will get called at various times by our NSTableView object. Its sole purpose is to let the NSTableView object know how many rows it should contain. Here we are telling it that we want to have as many rows as there are entries in abcList. Note that since we created abcList during init(), it will always exist at this point. If you do not have abcList already created, then this function will not work correctly. Also note that abcList is a member variable. This guarantees that abcList will exist throughout the duration of the application run.

def tableView_objectValueForTableColumn_row_(self, sender, tableColumn, row): if ( len(self.abcList) > 0 ): if tableColumn.identifier() == u"pos": return row + 1 else: return (self.abcList[row])

This function will be called every time the tableView needs to update the values for each cell. First we check to make sure that the length of self.abcList is greater than 0. This will ensure that we are putting good data into our NSTableView object. If the length is greater than 0, we check to see what column the NSTableView object is asking about. If it is asking about the column labeled “pos”, then we simply return the row number plus 1. We need to add one because the NSTableView object counts its rows from 0, but we display them starting from 1. If we are in the “letter” column, we return the letter at position row in self.abcList.

Drag & Drop Implementation

The next three functions are necessary to implement dragging and dropping in our NSTableView object. If any of the three are not present in your code, dragging and dropping (and row reordering) will not function for you NSTableView.

`def tableView_writeRows_toPasteboard_(self, sender, rows, pasteBoard): pasteBoard.declareTypes_owner_(NSArray.arrayWithObjects_(NSTabularTextPboardType, None), self.tableView) outputStr = ""

for each in rows: outputStr = “%s\t%s” % (each, self.abcList[each]) result = pasteBoard.setString_forType_(outputStr, NSTabularTextPboardType) return result`

This function will be called as soon as our dragging begins. Its purpose it to take the data we have selected and write it to an NSPasteBoard object, so that it can be retrieved at the end of the dragging.

First we need to tell the pasteBoard to declare what sort of types it can use. We also specify the pasteBoard’s owner, which in this case is our NSTableView object. Once we have declared the appropriate types, we loops through the rows parameter and create a tab delimited string containing the order number and appropriate letter for each row. Once we have this string, we tell the pasteBoard to set this string value for the specified type and store the result. If the string was set correctly, the result will be 1. If the string was not able to be set, the result will be 0. Since we created a tab delimited string, we use the NSTabularTextPboardType type. This will allow our NSPasteBoard object to handle tab delimited strings. Notice that this type was registered in our awakeFromNib()function. Once we get the result, we return it.

Once the NSTableView object has written the selected data to the NSPasteBoard object, it needs to validate the drop operation. This will tell our NSTableView object whether or not it is alright to move the data to the desired position. To make sure the drop operation gets validated (and not cancelled by the application for some reason), Cocoa calls tableView_validateDrop_proposedRow_proposedDropOperation during a drag operation.

def tableView_validateDrop_proposedRow_proposedDropOperation_(self, tableView, info, row, dropOperation): return NSDragOperationCopy

For our example we simply tell this function to return the defined constant NSDragOperationCopy. This lets our NSTableView object know that it is alright to perform the drag operation. It also lets it know what type of operation should be performed. In this case the operation we want to perform is a copy.

The last function that must be implemented by our class will allow the NSTableView object to accept the drop that was just validated. Please note that you are required to implement this function if your application uses dragging and dropping.

`

def tableView_acceptDrop_row_dropOperation_(self, tableView, info, row, dropOperation): pboard = info.draggingPasteboard() myString = pboard.stringForType_(NSTabularTextPboardType) myList = myString.split(“\t”) pos = myList[0] toInsert = myList[1] popPos = int(pos) self.abcList.pop(popPos) if popPos > row: self.abcList.insert(row, toInsert) else: self.abcList.insert(row - 1, toInsert) self.tableView.reloadData() return True`

Again, it is important to note that this function is required for dragging and dropping to function correctly.

First we need to grab the NSPasteBoard object that was sent in with the info parameter, and we store that into a variable. Next, we grab the data (in string form) from that NSPasteBoard object, and we store that as well. Since the data was stored earlier as a tab delimited string, we need to split the string into its appropriate pieces. This will give us a list containing two objects. The first is the row that the data came from, and the second object is the letter contained at that row. Next we need to make sure that row number is actually an integer. Once we have the row number as an integer, we tell abcList to pop() that position. This will keep us from having duplicated data in abcList. Next we need to check whether or not the row number we retrieved from our list is greater than the row number passed in as a parameter to the function. If it is greater, we simply insert the row number and the letter data into abcList. If it is not greater, we insert the row minus 1 and the letter data into abcList. This greater than check keeps us from trying to insert data into a row number that doesn’t exist (such as row -1). We then need to tell the NSTableView object to reload its data to make sure the changes we have made actually appear correctly. Then we simply return True.

This is all you need to implement simple dragging and dropping in a Cocoa-Python application using PyObjC.


Written by Ryan Wilcox Chief Developer, Wilcox Development Solutions... and other things