Tuesday, June 1, 2010

Having Users Makes Your Code So Much Better


I mentioned in a previous post that I was finding PyTask to be pretty cool. Of course, one of the cool things, for me, was that it used Quickly Widgets. As I mentioned, Quickly Widgets lacked some key features, like a DateColumn.

Having a user means that I know at least one way that someone it trying to use the code. I went ahead and implemented a DateColumn in PyTask, and my next step will be to add DateColumn to quickly widgets, so Ryan doesn't have to maintain the code in PyTask. First I need to kind of make room for this in the grid_filter module. I have a good idea of how to do it, so just a SMOP at this point.

There were other more subtle things that I ran into as well. For example, it turns out that I didn't handle the case of deleting rows in a CouchGrid, or even removing rows from a DictionaryGrid if that grid was filtered. The later case was "just" a bug. So I worked around this in PyTask code so that PyTask could ship while waiting for me to fix Quickly Widgets.

Since I have intimate knowledge about how the PyGtk was assembled, I was able to write this code for PyTask

    def remove_row(self, widget, data=None):
"""Removes the currently selected row from the couchgrid."""
# work around to actually delete records from desktopcouch
# in maverick, using delete=true in remove_selected_rows will have
# the same effect
database = CouchDatabase("pytask")
for r in self.grid.selected_rows:
database.delete_record(r["__desktopcouch_id"])

if type(self.grid.get_model()) is gtk.ListStore:
self.grid.remove_selected_rows()

else:

# The following code works around:
# https://bugs.edge.launchpad.net/quidgets/+bug/587568
# get the selected rows, and return if nothing is selected
model, rows = self.grid.get_selection().get_selected_rows()

if len(rows) == 0:
return

# store the last selected row to reselect after removal
next_to_select = rows[-1][0] + 1 - len(rows)

# loop through and remove
iters = [model.get_model().get_iter(path) for path in rows]
store_iters = []
for i in iters:
# convert the iter to a useful iter
store_iters.append(model.get_model().convert_iter_to_child_iter(i))

for store_iter in store_iters:
# remove the row from the store
self.filt.store.remove(store_iter)

# select a row for the user, nicer that way
rows_remaining = len(model)

# don't try to select anything if there are no rows left
if rows_remaining < 1:
return

# select the next row down, unless it's out of range
# in which case just select the last row
if next_to_select < rows_remaining:
self.grid.get_selection().select_path(next_to_select)
else:
self.grid.get_selection().select_path(rows_remaining - 1)
Essentially, it deletes the selected rows from desktop couch, and then goes on to figure if the grid is filtered, and if so, figures out where in the unfiltered model the rows are, and removes them from there. It also tries to select a row for the user after removing.

So I moved the code to delete records from desktop couch into CouchGrid.remove_selected_rows and all the "remove properly even if filtered" goo into DictionaryGrid.remove_selected_rows. The result is that when the next version of Quickly Widgets lands, Ryan will be able to simplify the function down to this:
    def remove_row(self, widget, data=None):
"""Removes the currently selected row from the couchgrid."""
self.grid.remove_selected_rows(delete=True)
A bit more sensible.

Another area where the DictionaryGrid lacked functionality was related to column titles. It was easy to write a little code to change the column titles:

        for c in self.grid.get_columns():
if c.get_title() == "name":
c.set_title(_("Name"))
elif c.get_title() == "priority":
c.set_title(_("Priority"))
elif c.get_title() == "due":
c.set_title(_("Due"))
elif c.get_title() == "project":
c.set_title(_("Project"))
if c.get_title() == "complete?":
c.set_title(_("Completed"))
Except, when doing this, it meant that the column titles in the GridFilter didn't match. :/ This is because the GridFilter got the names of the columns from the keys instead of the title.

Again, knowing the structure of the PyGtk intimately, I was able to work around this by modifying rows in the filter as each is created:
   def __new_filter_row(self, widget, data=None):
"""
new_filter row - hack to allow naming of columns
in a grid filter.

This code works around:
https://bugs.edge.launchpad.net/quidgets/+bug/587558

"""

row = self.filt.rows[len(self.filt.rows)-1]
row.connect("add_row_requested",self.__new_filter_row)
model = row.column_combo.get_model()

for i, k in enumerate(model):
itr = model.get_iter(i)
title = model.get_value(itr,0)
if title == "name":
model.set_value(itr,0,_("Name"))
elif title == "priority":
model.set_value(itr,0,_("Priority"))
elif title == "due":
model.set_value(itr,0,_("Due"))
elif title == "project":
model.set_value(itr,0,_("Project"))
if title == "complete?":
model.set_value(itr,0,_("Completed"))
I fixed this a bit easier in Quickly Widgets.

All of this managing the title stuff was really obtuse, and it seemed that setting column titles might be generally useful. So I added two things to DictionaryGrid to make this easier. First, I made a property that returns a dictionary of columns indexed by the column key, so you can easily get ahold of a column you want. Here's some code from one of the Quickly Widget tests:
        grid.columns["key1_1"].set_title("KEY")
That's a bit easier than for c in grid.get_columns(), etc...

I also made a convenience function that Ryan can use. Here's the code from the tests:
        titles = {"key1_1":"KEY1","key1_2":"KEY2","key1_3":"KEY3"}
grid.set_column_titles(titles)
So this should make it really trivial to manage the titles of columns separate from the keys in the dictionaries. Of course, if you don't want to care about column titles, that's fine too. They still work just by using the keys.

Anyway, thanks to Ryan for letting me use PyTask to improve Quickly Widgets!

1 comment: