Sunday, June 20, 2010

New Quickly Widget: Text Editor

Here's an 8 minute demo showing how to use the new TextEditor Quickly Widget. This removes all the pain and suffering of adding text editing functionality to your app. No more gtk.TextBuffer, no more gtk.TextIter. Just a series of simple function calls, and you're ready to go. Of course, TextEditor is a gtk.TextView, so if you need to access any of the power and flexibility of the underlying Gtk library, it's right there.



TextEditor is now available in quidgets trunk.

Sorry for the loudness of the video. I made it in a coffee shop, and moved my mic closer to compensate for background noise. It didn't work out too well.

New Quickly App: Daily Journal



Quickly has started to unlock productivity for me in unexpected ways. I've mentioned about writing my own development tools, like bughugger, and slipcover. Last week I extended this to writing my own productivity apps. In this case, I wrote an app to replace my Tomboy usage to focus specifically on the functions in Tomboy that I actually used. The above video shows that app, "Daily Journal". It's available in my PPA.

In my next posting, I'll show how I used quickly.widgets.text_editor to create Daily Journal.

Monday, June 14, 2010

Go Here to Learn to Program from MIT

I run into folks who want to get started programming, but they "don't know a language". If you are in this camp, I highly recommend the online course from MIT. It's designed for people with no prior programming experience, and it's Python!

After the first few lessons, you'll know enough Python to start a Quickly app!.

Sunday, June 6, 2010

New Quickly Widget - async downloads with two lines of code




The Ubuntu Developer's Manual team was discussing the instructional app that we should use for the manual. During this discussion, it became apparent that there wasn't a way to download from a URL that was both easy and also asynch. Instead of choosing between simple and good, Stuart made a Quickly Widget that provides a way to fetch from a URL that is both simple *and* good.

fetcher = UrlFetchProgressBox("http://identi.ca/api/statusnet/groups/timeline/8.rss")
fetcher.connect("downloaded",self.create_grid_from_feed)
So, two lines! Just say what url you want to download, and tell it the function to call when it's done.

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!