Thursday, December 31, 2009

Quidgets update - Do you want to use verbs? Yes/No


Some folks pointed out yesterday that Yes/No dialogs aren't terribly HIG compliant. So I tweaked the API a bit to allow the usage of verbs instead of stock Yes/No buttons if desired.
   response = yes_no(title,message, "_Add Verbs","_Don't add Verbs")
if response == gtk.RESPONSE_YES:
print "they said yes"

If you choose not to use the last two arguments for the Yes/No button labels, the dialog will simply default to the Stock buttons.


About Quidgets
Quickly + Widgets = Quidgets
There is a Launchpad Project for Quidgets
The most up to date changes are in the Quidgets Trunk Branch

Wednesday, December 30, 2009

More Quidget One-Liners: Yes/No, Warnings, Info, Errors





Last night I spent some time working on bug #500911, adding some more useful functions to the quidgets.prompts namespace.

quidgets.prompts.yes_no() works like this:
  response = quidgets.prompts.yes_no("I need some info","Do you want to give me a yes or no?")
if response == gtk.RESPONSE_YES:
print "they said yes"
elif response == gtk.RESPONSE_NO:
print "they said no"
else:
print "they dismissed the dialog"
This is a little different than the other functions in the namespace in that in that the responses are gtk.RESPONSE_YES or gtk.RESPONSE_NO, rather than gtk.RESPONSE_OK, or gtk.RESPONSE_CANCEL. Of course, there is no "value", as you are just looking for a yes or no from the user.

Warning, Error, and Info are similar, but you don't even check the return value, as you are not seeking any feedback. You just supply a title and some text that you are trying to tell the user.

So:
quidgets.prompts.warning("Title goes here","Warning")
quidgets.prompts.error("Title goes here","Error")
quidgets.prompts.info("Title goes here","Info")
I hope that folks find this useful, and that it saves writing lots of lines of code. I've pushed the branch, and should get it into my PPA in the next few days.

About Quidgets
Quickly + Widgets = Quidgets
There is a Launchpad Project for Quidgets
The most up to date changes are in the Quidgets Trunk Branch

Monday, December 28, 2009

Ubuntu as an Internet Client



The Bughugger project is indicative of an essential bias of mine, that most tasks are more enjoyable and can be done more easily from a local "thick client" than from a web-based thin client.

In general, what I think people like about web based clients are that they:
  1. Are easy to access, requiring minimal installation steps and such
  2. Can be installed with no risk, and do not require "un-install" steps if usage is abandoned
  3. Are free as in bear, at least initially
Ubuntu actually does all right by these criteria, so perhaps to some degree the conflation of "Internet" and "Web" is driven by the complexities of installing third party apps on Windows, and all of the problems this can cause one's computer as different versions of shared libraries and such are copied in.

So today I decided to move one of my key web-based activities, reading and writing blogs, into Ubuntu itself. Note that I am already a heavy Gwibber user and I rarely use a browser to access micro-blogging features.

Installing
My experience with the Software center shows how Ubuntu does fairly well with points 1-3 above. There is lots of blog related software to try, and it's all free in every meaning of the word. It was really easy to install software:


Reading Blogs
Reading my feeds from Ubuntu using LifeArea was easy to set up, and is working perfectly for me. Also, the UI can fit into my netbook rather well. Yeah for LifeArea!
Writing Blogs
I am writing this posting form my browser, as I haven't found a posting tool that I like yet.

Blog Entry Poster made it very easy to connect to my blog. I also like the overall simplicity of the design. However, the editor itself lacked the formatting features that I use heavily, so I'm not quite able to adopt it.
BloGtk has the editor options I need, but it doesn't quite fit into my netbook screen size. This doesn't really matter as I couldn't figure out how to get it to connect to my blog anyway.
Ultimately, I suppose that I would love it if LifeArea had the Gwibber-like ability to post. There are still other posting tools to try in any case, so I may find one yet. I'm installing Drivel atm.

Saturday, December 26, 2009

Quidgets DictionaryGrid Real Life Use Case

Bughugger is designed to make it easier for teams to manage Ubuntu related bugs by creating an extensible and fast desktop client for managing large lists of bugs.

Recall that I started working on Quidgets out of my annoyances with PyGtk programming for the Bughugger project. Specifically, the code required for the grid of bugs was extensive, repetitive, and hard to modify. I decided that I would never right TreeView code again, well, at least not directly.

This means that Bughugger is the ideal use case for me to understand the quidgets.widgets API. Here is the code that creates the above grid:

  def __setup_grid(self, bug_tasks):
"""set_up_treeview: sets up the treeview for the BugPan.

arguments:
bug_tasks -- a dictionary to set up for the Pane
"""
sw_filter = BlankFilterCombo()
sw_filter.append("=",lambda x,y: convert_to_num(x) == float(y) )
sw_filter.append("<",lambda x,y: convert_to_num(x) < float(y) )
sw_filter.append(">",lambda x,y: convert_to_num(x) > float(y) )
sw_filter.append("<=",lambda x,y: convert_to_num(x) <= float(y) )
sw_filter.append(">=",lambda x,y: convert_to_num(x) >= float(y) )
sw_filter2 = BlankFilterCombo()
sw_filter2.append("=",lambda x,y: convert_to_num(x) == float(y) )
sw_filter2.append("<",lambda x,y: convert_to_num(x) < float(y) )
sw_filter2.append(">",lambda x,y: convert_to_num(x) > float(y) )
sw_filter2.append("<=",lambda x,y: convert_to_num(x) <= float(y) )
sw_filter2.append(">=",lambda x,y: convert_to_num(x) >= float(y) )
filter_hints = {"status":sw_filter,"importance":sw_filter2}
type_hints = {"gravity":IntegerColumn,"effected users":IntegerColumn}
if self.keys is not None:
self.grid = DictionaryGrid(bug_tasks,self.keys,type_hints)
else:
self.grid = DictionaryGrid(bug_tasks,[],type_hints)
grid_filt = GridFilter(self.grid,filter_hints)
grid_filt.show()
self.grid.show()
#add the gridview to the top of the pane
self.pack_start(grid_filt, False)
#wire the treeview to the signal handlers
self.grid.connect("row-activated",self.__row_clicked)
self.grid.connect("button_press_event", self.__treeview_clicked)
self.grid.connect("cursor_changed", self.selection_changed)
self.grid.connect("move-cursor", self.__cursor_moved)
self.grid.connect("select-all", self.selection_all)
self.grid.connect("select-cursor-row", self.selection_changed)
self.grid.connect("unselect-all", self.selection_none)
self.grid.connect("toggle-cursor-row", self.selection_changed)
#create a scrolled window for the treeview
grid_scroller = gtk.ScrolledWindow()
grid_scroller.add_with_viewport(self.grid)
grid_scroller.show()
#add the scrolled window to the bottom of the pane
self.pack_start(grid_scroller, True, True)
If this looks like a lot of code, believe me, it's not. The code required to create a TreeView with the related sorting, filtering, and different types of columns would be probably 1o times the number of lines here, and would by much harder to change.

None the less, looking at the code, there are a few things that I don't like about the API at the moment:
  1. You have to connect to the myriad events related to cursor and selection changes in the DictionaryGrid. I should probably create a single "selection_changed" event, that should capture about half of the events above.
  2. Filter hints are instances of FilterCombo objects, but type hints are types. This is inconsistent and confusing. I should probably make type hints work like filter hints.
  3. You still have to manually place the menu based on the button press event. I should probably create a right click menu property on the grid that you can simply add to the grid. I could also potentially create a set of default right click functions, like "copy", and then have some simple methods to append new commands to it.
  4. There aren't enough conventions for setting types for columns. I should be able to use naming instead of the type hints. Perhaps I shall add "ends with 'count'" as a convention, and then can change "effected users" to "effected users count", and maybe something similar that would grab gravity as well, like "gravity total". Key naming seems more easy and fun than providing type hints and would reduce the number of objects required to customize the grid.
  5. Custom filters use the types stored for displaying in the grid rather than the backing values in the dictionaries. For instance, if you were making a custom filter for a CheckedColumn, rather than receiving a True or False, you would receive, a -1,0, or 1. This seems confusing to me and requires familiarity with the implementation details of DictionaryGrid. Perhaps I will replace the display values with the real values, or perhaps I may provide the display values along with the stored values.
  6. There are a few quidgets.prompts that I would like to add as well, but that's separate from the DictionaryGrid and related functionality. I'm thinking prompts.alert, prompts.info, and prompts.task. Stay tuned.
In any case, the functionality is complete enough for bughugger, so I'm pushing this code, and will get Quidgets into Universe when I get back from vacation. Then we can get Bughugger into Universe as well.

About Quidgets
Quickly + Widgets = Quidgets
There is a Launchpad Project for Quidgets
The most up to date changes are in the Quidgets Trunk Branch
You can install Quidgets from the my PPA

Friday, December 25, 2009

Take that TreeView ... I win *ding ding ding*


The above grid was created with this code:

   dicts = [{"key?": True, "price":0.00,"tags" : "aaa bbb ccc","_foo":"bar"},
{"ID": 11, "key?": False, "price":2.00,"tags" : "bbb ccc ddd","_foo":"bar"},
{"key?": True, "price":33.00,"tags" : "ccc ddd eee","_foo":"bar"},
{"ID": 3, "tags" : "ddd eee fff","_foo":"bar"},
{"ID": 4, "price":5.00,"_foo":"bar"}]

grid = DictionaryGrid(dicts, ["ID","tags","price","key?"])

A couple of days ago I was rightfully miffed with the TreeView, as it has proven again and again to be flexible, but HIGHLY inconvenient to learn much less use. Specifically, the underlying data store for a TreeView could only accept values of the defined type, and missing values were automatically set to defaults. This meant there was no way to display partially complete data sets. But if the proper underlying value was not used, columns did not sort correctly for the specified type.

I have therefore spent a few hours over the last couple of days keeping two related backing stores for the dictionary grid. The display store keeps data proper for displaying to a user, particularly important being an empty string for string and number fields. The "real" store is the dictionary stored with each row that sets types one would expect, such as integers for counting columns, floats for currency, booleans for checks, etc...

For a sense of the effort involved in making TreeViews work the way you want, this work involved:
  1. Responding to clicks on the column header and determining whether the user toggled to ascending or descending sort.
  2. Setting the sort icon on and turn off the icon in the last sorted column.
  3. Tracking sorted columns in the DictionaryGrid to turn off the sorted icon when needed.
  4. Writing a sort_ascending and sort_descending function for each type of column.
  5. Creating format functions for two of the column classes to present currency and checkboxes in the correct format.
  6. Writing mappings from "display" values to "real" values and back.
I have evolved the DictionaryGrid over the last couple of weeks so that I could offer developers a much more fun and easy way to display data to users. The DictionaryGrid now supports features like:

DictionaryGrid Basic Features
  1. Infers column types from the names of the columns.
  2. Using spinners or checkboxes for non-string column types.
  3. Ability to handle dictionaries with data in "wrong" types (for example a Currency column with try to translate a literal string "33.00" to the number 33.00.
  4. Column sorting.
  5. Editing of inputted values.
  6. Easy access to underlying data stores
  7. Easy access to selected rows
  8. Easy access to removing selected rows
  9. Easy to add a "filtering" user interface
I am trying to avoid "advanced" features, but there are some extra supported featuers:
  1. Ability to explicitly set a column type
  2. Ability to explicitly set a filter for a column
  3. Ability to implement custom columns (currently requires creating a class with the necessary interface)
  4. Ability to implement custom filters through sub-classing or by adding filters to an existing filter type).
My next steps are to ensure that the GridFilter functionality is working in a robust manner. Then I will port bughugger over to the latest quidgets code. At that point, I will consider Quidgets "ready" for an initial release.


About Quidgets
Quickly + Widgets = Quidgets
There is a Launchpad Project for Quidgets
The most up to date changes are in the Quidgets Trunk Branch
You can install Quidgets from the my PPA

Wednesday, December 23, 2009

Curse you TreeView, why must you break my heart so?

I've continued to make good progress on DictionaryGrid, and feel that I am close to having a fun and easy replacement for TreeView programming. However, once again I was stunned by the incredible viscosity of the TreeView API, not to mention the unexpected behaviors.

Curse you TreeView API!!

What's the difference between to the two highlighted zeros in the ID column above? The first one was passed into the DictionaryGrid by the test program, but the second was a default value added by, I suppose, the ListStore as a default value. I think this happened because I changed the type of that column to gobject.TYPE_INT to support proper sorting. However, I can't pass in an empty string or None, as ListStore will not accept a value that is not an integer. If I append an empty row, missing numbers get initialized as zero.

Now, there is a big difference between a value in a grid being zero because it's set to zero, and being zero because that's the default. So these columns have to support empty cells.

I believe I can achieve this by making all of the numeric columns of type gobject.TYPE_STRING, but then overriding the sorting method to be numerical (hopefully without adding some kind of hideously slow bubble sort or something ;) ).

Yesterday I was doing a lot of detecting types to specialize behaviors. This is a classic sign of needing to do a refactor into subclasses. So I spent yesterday subclassing TreeViewColumn to support Integers, Strings, Currency, and Booleans the way I wanted. Having these subclasses already in place, I think I can add the numeric sorting in a relatively straight forward manner.

The good news is that the code to create a dictionary view still has not changed, yet Integer, Currency, String, and Boolean columns are all created automagically, and the consuming code has not changed. Here is the code to create the DictionaryGrid pictured above:
   dicts = [{"key?": True, "price":0.00,"tags" : "aaa bbb ccc","_foo":"bar"},
{"ID": 1, "key?": False, "price":2.00,"tags" : "bbb ccc ddd","_foo":"bar"},
{"key?": True, "price":3.00,"tags" : "ccc ddd eee","_foo":"bar"},
{"ID": 3, "key?": False, "price":4.00,"tags" : "ddd eee fff","_foo":"bar"},
{"ID": 4, "key?": True, "price":5.00,"tags" : "eee fff ggg","_foo":"bar"}]

grid = DictionaryGrid(dicts, ["ID","tags","price","key?"])
grid.editable = False

Sunday, December 20, 2009

Bending Gtk.TreeView to my Will

Today I made some progress in bending the Gtk.Treeview to my will, and making a proper easy and fun Quidget, called DictionaryGrid. Notice that key? is a column of checkboxes, and price is a column of spinner controls formatted with two decimal digits. This is done automatically by the DictionaryGrid.

The code for creating this grid is easy and fun, as the behavior is controlled simply through choosing the right name for keys:
    dicts = [{"ID": 0, "key?": True, "price":0.00,"tags" : "aaa bbb ccc","_foo":"bar"},
{"ID": 1, "key?": False, "price":2.00,"tags" : "bbb ccc ddd","_foo":"bar"},
{"ID": 2, "key?": True, "price":3.00,"tags" : "ccc ddd eee","_foo":"bar"},
{"ID": 3, "key?": False, "price":4.00,"tags" : "ddd eee fff","_foo":"bar"},
{"ID": 4, "key?": True, "price":5.00,"tags" : "eee fff ggg","_foo":"bar"}]
#create and show a test window
win = gtk.Window(gtk.WINDOW_TOPLEVEL)
win.set_title("DictionaryGrid Test Window")
win.connect("destroy",gtk.main_quit)
win.show()

#create a top level container
vbox = gtk.VBox(False, False)
vbox.show()
win.add(vbox)

#create a test widget with test database values
grid = DictionaryGrid(dicts)



My accomplishments today were:
  • Started implementing the "conventions" system. A key of "id", will be automatically set to an integer column, for proper sorting.
  • A key of "price" will automatically be formatted as currency, and will also be presented in a spinner control. I will change the id columns to use the spinner as well, so that all numerical columns will automatically use spinners.
Due to specifying types, the persistence of any edits are broken at the moment. My next step will be to ensure proper conversion to the necessary types for the ListStore. This may have the strange effect of changing the type in the dictionary that backs the store. I'm not sure what the right thing to do there is. I like that if you hand a the DictionaryGrid a string that represents the number, the DictionaryGrid does the right thing. However, handing back a different type seems unexpected. Perhaps the DictionaryGrid should remember if the original type was a string, and if so, convert back to that before it stores the value back into the dictionary.


About Quidgets
Quickly + Widgets = Quidgets
There is a Launchpad Project for Quidgets
The most up to date changes are in the Quidgets Trunk Branch
You can install Quidgets from the my PPA

Friday, December 18, 2009

Quidget Custom Filters Simplified


Last night I created a custom filter for bughugger. I knew when I created GridFilter that I would be able to easily create new filters, and when I added "filter_hints" I knew it would then be easy to add new custom filters for a grid.

But when I wrote the custom filter:
class StartsWithNumberFilterCombo(gtk.ComboBox):
def __init__(self):

combo_store = gtk.ListStore(gobject.TYPE_STRING,gobject.TYPE_PYOBJECT)
combo_store.append(["=",lambda x,y: convert_to_num(x) == float(y) ])
combo_store.append(["<",lambda x,y: convert_to_num(x) < float(y) ])
combo_store.append([">",lambda x,y: convert_to_num(x) > float(y) ])
combo_store.append(["<=",lambda x,y: convert_to_num(x) <= float(y) ])
combo_store.append([">=",lambda x,y: convert_to_num(x) >= float(y) ])
gtk.ComboBox.__init__( self, combo_store)
cell = gtk.CellRendererText()
self.pack_start(cell, True)
self.add_attribute(cell, 'text', 0)

def convert_to_num(string):
return float(string.split(" ")[0])


I didn't like that I had to use my knowledge of the implementation details. In other words, it seemed easy to me because I know what a ListStore is, and I knew that a ComboBox uses a ListStore and that in this particular case the second field of the ListStore stores a reference to a function, while the first field stores a string to display in the combo.

So today I reworked the filters so that developers could easily write new filters without having to have knowledge of the implementation details. I create a new base class called "BlankFilterCombo" that has no associated filters by default. There is a single public member exposed, "append". Append takes text to display, and a filter to use for a filter.

So, now I could rewrite the custom filter code without sub-classing, and without understanding anything about how the filters are implemented:
  sw_filter = BlankFilterCombo()
sw_filter.append("=",lambda x,y: convert_to_num(x) == float(y) )
sw_filter.append("<",lambda x,y: convert_to_num(x) < float(y) )
sw_filter.append(">",lambda x,y: convert_to_num(x) > float(y) )
sw_filter.append("<=",lambda x,y: convert_to_num(x) <= float(y) )
sw_filter.append(">=",lambda x,y: convert_to_num(x) >= float(y) )


So now the code just appends the pairs of text and functions.

While I was at it, I did change the existing filters to sub-class BlankFilterCombo, but this is not visible to other developers.

About Quidgets
Quickly + Widgets = Quidgets
There is a Launchpad Project for Quidgets
The most up to date changes are in the Quidgets Trunk Branch
You can install Quidgets from the my PPA

quidgets.prompts ... gtk.Dialogs now one-liners!

As I mentioned in a previous post, I became a tad enraged at PyGtk when I was providing some simple "get a value from a user code" to one of my bughugger collaborators. My response was to write a bit of code to provide a fun and easy way to get values from users, meet the quidgets.prompts namespace.

The essential patter is quite simple. All of the prompts showed above follow the same pattern. To use a prompt, simply:
  1. Call the right function, such as quidgets.prompts.string()
  2. Check the response from the function call to see if the user said "Ok" or not.
  3. If the response was ok, use the returned value.

The code to get a string looks like this:
reponse, val = quidgets.prompts.string()
if reponse == gtk.RESPONSE_OK:
print val
All of the prompts can work just like this, without any arguments in the function. Right now there is:
  1. quidgets.prompts.string()
  2. quidgets.prompts.date()
  3. quidgets.prompts.integer()
  4. quidgets.prompts.price()
  5. quidgets.prompts.decimal()
Typically, though, you'll want to provide users with a bit of context. Each prompt function also supports an argument for a window title, some prompt text for the user, and a default value.

So to create a string prompt, you are more likely to do something like this:
reponse, val = quidgets.prompts.string("A Title","Some prompt text", "some default text")
if reponse == gtk.RESPONSE_OK:
print val


Note that the default value needs to be a type expected by the prompt. quidgets.prompts.date() expects the default value to be a three tuple in the form of year, month (zero indexed!), and day. So it might look like this:
reponse, val = quidgets.prompts.date("Change Birthday","Set your birthday", (1995,06,11))
if reponse == gtk.RESPONSE_OK:
print val


Of course the numeric prompts (integer, price, decimal) all expect numbers for default values.

Speaking of numeric prompts, as you can see, they use spinners to collect input from users. By default the spinners allow negative numbers. If you don't want to allow negative numbers, the numeric prompts have extended the arguments a bit so that you can set the minimum value to zero:
min_val = 0
default_val = 0
reponse, val = quidgets.prompts.price("Enter Price","Set price in dollars between zero and 100:",default_val,min_val)
if reponse == gtk.RESPONSE_OK:
print val


Note that the "lower" button is disabled because the value is at the specified minimum.

You can set the max value and how much you want the spinner to increment with each button click ("step") as well:
min_val = 0
default_val = 50
step = 1
max_val = 100
reponse, val = quidgets.prompts.price("Enter Price","Set price in dollars between zero and 100:",default_val,min_val,step,max_val)
if reponse == gtk.RESPONSE_OK:
print val

These guys seem ready to go. Next up, I'll be adding prompts to manage asynchronous activity. So that you can create a prompt to display while a long running computation is occurring without freezing the UI. Stay tuned.

About Quidgets
Quickly + Widgets = Quidgets
There is a Launchpad Project for Quidgets
The most up to date changes are in the Quidgets Trunk Branch
You can install Quidgets from the my PPA

Thursday, December 17, 2009

Quidgets in Action: Custom Filters

I started the Quidgets project out of my frustrations with PyGtk while I was working on this bughugger tool, which a few of us in Ubuntu have been working on to help us streamline our bug triaging on the Ubuntu Desktop a bit.

We use a DictionaryGrid to display, sort, and filter bug data. In order to support correct sorting of status and importance, Brian Murray came up with a numbering scheme. However, what does status of "2" mean? We needed some text in there to also tell us what the numbers meant. But you can see the problem. Since the status and importance columns have to be typed to strings to support the extra text, the GridFilter's filter for numbers couldn't be used, but the GridFilter's string filter doesn't support < (less than), = (equals), > (greater than), etc..., which would obviously be useful.

Fortunately, GridFilter takes an optional argument in the constructor called "filter_hints" so you can tell the GridFilter what filter to use for a specified key. However, as noted above, the number filter won't work, and the string filter won't work. But it's easy to build a custom filter.

A filter actually derives from a combo box. It associates text to select in the combo box with a function that compares what is in a cell of a DictionaryGrid with text that user inputs, x and y.

Using lambda functions, I can write some quite terse code to provide the functions:
class StartsWithNumberFilterCombo(gtk.ComboBox):
def __init__(self):

combo_store = gtk.ListStore(gobject.TYPE_STRING,gobject.TYPE_PYOBJECT)
combo_store.append(["=",lambda x,y: convert_to_num(x) == float(y) ])
combo_store.append(["<",lambda x,y: convert_to_num(x) < float(y) ])
combo_store.append([">",lambda x,y: convert_to_num(x) > float(y) ])
combo_store.append(["<=",lambda x,y: convert_to_num(x) <= float(y) ])
combo_store.append([">=",lambda x,y: convert_to_num(x) >= float(y) ])
gtk.ComboBox.__init__( self, combo_store)
cell = gtk.CellRendererText()
self.pack_start(cell, True)
self.add_attribute(cell, 'text', 0)

def convert_to_num(string):
return float(string.split(" ")[0])
Note that if I need to do more complex transformations and/or comparisons, I could have used actually functions and passed the names of those functions in instead of using lambda functions.

Anyway, then I simply set up the filter hint when I create the GridFilter:
filter_hints = {"gravity":NumericFilterCombo(),"effected users":NumericFilterCombo(),
"status":StartsWithNumberFilterCombo(),"importance":StartsWithNumberFilterCombo()}
grid_filt = GridFilter(self.grid,filter_hints)
Presto-chango, I have a proper and custom filter for my DictionaryGrid, importance and status can be filtered numerically:

I think I shall simplify the creation of a custom filter somewhat by creating a FilterCombo class that you can derive from or that you can use stock and just stuff pairs of strings and functions into. Still, as is, this dramatically reduced the amount of time it took me to set up filters for the DictionaryGrid compared to if I used a stock Gtk TreeView.


About Quidgets
Quickly + Widgets = Quidgets
There is a Launchpad Project for Quidgets
The most up to date changes are in the Quidgets Trunk Branch
You can install Quidgets from the my PPA

Wednesday, December 16, 2009

First Application Indicator on My Lucid Desktop


I did an dist-upgrade today, and saw that the first Application Indicator was installed and working.

This is the Rhythmbox indicator, and it is a nice consistent menu that gives me left click access to the key music player functions. It works like the typical Rhythmbox Notification Icon, but works consistently and predictably.

I still don't know what key combinations gets me to activate the Application Indicator Area, but I did notice that I can arrow key between Rhythmbox indicator and the Messaging Indicator, which is a major win for me, as I tend to stick with the keyboard if I can avoid using a mouse. This is also important for accessibility, as there are a large range of things that can make it difficult for a person to use a mouse.

Conventions for DictionaryGrid and Quidgets (or more on why I hate TreeViews)

We ran into a rather sorry limitation with the current implementation of the DictionaryGrid Quidget. Well, really, I blame the TreeView API, and it's complexity and rigidness.

It turns out that if a TreeView is treating a column as a string, and those strings are numbers, than the ordering can get a little screwy if you sort columns. Basically, the ordering of strings is not numeric, so the different number of characters can through it off. For example, in this test app, you can see that the column for key2 is in an odd order:
Clearly 55 is not the least of these numbers.

As a result, I worked a bit with the DictionaryGrid code, and realized that I can in fact set the proper type for column at run time, and don't have to treat them all as string. (Thanks to Robert Ancell ;) )

Configuration
So the updated code for DictionaryGrid can set the proper column type in two ways, by convention of by configuration. The supported way to configure is to pass in "type_hints" when you create the Grid. This is a dictionary of keys to gobject types. So the following code will make thekey2 column properly sortable:
 dicts = [{"ID": 0, "key2": 55, "tags": "aaa bbb ccc","_foo":"bar"},
{"ID": 1, "key2": 6, "tags": "bbb ccc ddd","_foo":"bar"},
{"ID": 2, "key2": 7, "tags": "ccc ddd eee","_foo":"bar"},
{"ID": 3, "key2": 8, "tags": "ddd eee fff","_foo":"bar"},
{"ID": 4, "key2": 9, "tags": "eee fff ggg","_foo":"bar"}]

keys = ["ID","key2","tags"]
type_hints = {"key2":gobject.TYPE_INT}
grid = DictionaryGrid(dicts, keys,type_hints)

These changes are in trunk, but as there are no tests yet, they have not been pushed to my PPA.

Note that with typing of columns, it's no longer a simple matter of converting everything to a string, so I will need to add some code to check the column's type and then convert the values to the appropriate to type to ensure that the code is suitably robust.

Convention
Now, it's nice that you can configure in this manner, but I am a big fan of the Ruby on Rails principle of Conventions over Configuration. So, I've already added one convention. Any column who's key is "id" (or "Id", "ID", "iD") will automatically be set to a column with type Integer.

I want to add a nice rich list of conventions to make using a DictionaryGrid super easy and fun. For example:
  • ends in " count" = Integer type
  • ends in "?" = Boolean type (column will have check boxes)
  • is "cost" or "price" = Float type, and value is properly formatted
  • ends in "%" = Float type
  • etc...
Everything else will default to String. In addition to typing the column properly, these same conventions will cause the proper GridFilter type to be used by default.

The use of these conventions should reduce the need for the use of type_hints, though won't be intended to eliminate them. The configuration options should be available for developers who want or need that option. Though by default, if a type_hint is provided for a column, it would be good if the related filter type were picked up as well.

About Quidgets
Quickly + Widgets = Quidgets
There is a Launchpad Project for Quidgets
The most up to date changes are in the Quidgets Trunk Branch
You can install Quidgets from the my PPA

Tuesday, December 15, 2009

UDS Wrap Up

[Note that I originally posted this on 11/16, I'm reposting with a title]

At the closing plenary for UDS, each of the track leads went over some of the important decisions that were taken over the course of the week. Here is a copy of my barely cleaned up notes with some extra comments ... These will make it onto the Ubuntu wiki in a cleaned up form, and then into work items over the next couple of weeks so we can start tracking ...

xorg
bryce led the xorg decision making process over the mailing list to accommodate the remote participatin of may stake holder. The version they decided on ...
xorg 1.7
-intel 2.10
mesa 7.7
bryce and tselliot will also work on a more robust install process for proprietary video drivers based on a system pioneered by mandriva.

Audio
TheMuso put it best, "Bug fixing and hardware enablement from here on out ..."

Gnome
Of course this planning was led by seb128, but we were really fortunate to have some our faves from upstream at UDS as well ...
  • Update platform libs to glib and gtk current versions (platform)
  • but not update all applications (stick with current evolution), depending on whether they have intrusive structural changes
  • potentially keyring depending on how shared gnome/kde keyring work goes
Social from the Start
Desktop team planning was led by kenvandine, but segphault was in attendance as well, so lots of great collaboration with Gwibber upstream:
  • store messages in desktopcouch
  • move account to desktopcouch
  • add a simple microblog function call on the backend
  • create a reusable UI widget for microblogging (in pygtk)
  • create a accounts configuration screen (kenvandine)
GeoClue
We were hoping to turn on GeoClue in Lucid, but after Ryan did some testing, it looks like we won't be able to take this into an LTS release :( Maybe Lucid +1?

Desktop in the Cloud
Coordinating with the server team, we will host daily builds of the Lucid desktop somewhere "in the cloud", maybe on an internal Canonical cloud to start, but also maybe on aws.

GDM
robert_ancell will be rotating off to the OEM team during Lucid, giving him only 20% time to work on the desktop. During that time he will:
  • patch GDM to allow preselecting a user to support fast user switching
  • Maybe refactor Greeter/GDM split
  • Maybe add back some options that went missing when GDM uupgraded (any one who feels strongly this is a must do, please feel free to work on this feature).

Apps installed by default
This was a fun session. Also some interesting responses from the internets.
  • remove GIMP
  • leaner games selection
  • add video editor [if ready] (pitivi)
Others
  • desktop start up time
  • improve compiz visual defaults (djsiegal)
  • UNE Desktop sessions
  • Battery life improvements
  • OOo Version = 3.2.1

Monday, December 14, 2009

Here's why I need Quidgets


A friend of mine asked me today to help him with a little bit of code. Basically, he needed to get a number for a user from a Python script. While he is a good coder, he doesn't know PyGtk all that well.

So here's the code that I wrote for him, it grabs a string as a tag from the user:

        dialog = gtk.Dialog("Bug Tag",None,gtk.DIALOG_MODAL,(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
content_area = dialog.get_content_area()
hbox = gtk.HBox(2, 5)
label = gtk.Label("Tag:")
entry = gtk.Entry()
hbox.pack_start(label,False, False)
hbox.pack_end(entry, False, True)
content_area.pack_start(hbox)
hbox.show()
label.show()
entry.show()

result = dialog.run()
dialog.destroy()

if result == gtk.RESPONSE_ACCEPT:
tag = entry.get_text()
else:
return


What? I have to refer to six separate objects just to show a simple dialog? The code should be more like:

       
result, input = whatever_I_name_this_function("Title")
dialog.destroy()

if result == gtk.RESPONSE_ACCEPT:
tag = input
else:
return


I don't think this will take me all that long to right, though making the dialog return a tuple may be a tad harder than I expect, I'll see what I can do. Maybe I'll make a simple object to create instead of a single function call ...

Friday, December 11, 2009

Quidgets now in my PPA




If you want to try out Quidgets, please find them in my PPA.

Note that I only have a Lucid ppa atm, but it should work fine on Karmic.

What are Quidgets? Quidgets = (Quickly + Widgets), see my previous post.



Cheers, Rick

Wednesday, December 9, 2009

More Quidgets ... No More TreeViews



TreeViews (Not Easy and Fun)
If you have data that you want to display in a grid format in PyGtk is code intensive and inflexible. In Cognitive Dimensions speak we would say that the Work Step Units are too small, and the resulting code has high viscosity. It's also quite hard to learn.

In order to take a dictionary of data and display it to a user in a grid in PyGtk, you use a combination of three object:
  • TreeView
  • ListModel
  • TreeViewColumn

Here's some real code that I wrote while back for using the FogBugz XML API:
def __init__(self,session,data):
gtk.VBox.__init__( self, False, 3 )
child = gtk.ScrolledWindow()
#start time, end time, duration, case, title, category
store = gtk.ListStore(gobject.TYPE_STRING,gobject.TYPE_STRING,gobject.TYPE_STRING, gobject.TYPE_INT,gobject.TYPE_STRING,gobject.TYPE_STRING)

total_time = None
for interval in data:
b = pyFogbugz.getbugs(session, [interval["case"]])[0]
store.append([interval["start"],interval["end"],interval["duration"],interval["case"],b["title"],b["category"]])
#see if this is the earliest date

#accumulate total time
if total_time == None:
total_time = interval["duration"]
else:
total_time += interval["duration"]

view = gtk.TreeView(store)

stcol = gtk.TreeViewColumn("Start", gtk.CellRendererText(), text=0)
stcol.set_sort_column_id(0)
view.append_column(stcol)

endcol = gtk.TreeViewColumn("End", gtk.CellRendererText(), text=1)
endcol.set_sort_column_id(1)
view.append_column(endcol)

durcol = gtk.TreeViewColumn("Duration", gtk.CellRendererText(), text=2)
durcol.set_sort_column_id(2)
view.append_column(durcol)

casecol = gtk.TreeViewColumn("Case", gtk.CellRendererText(), text=3)
casecol.set_sort_column_id(3)
view.append_column(casecol)

titlecol = gtk.TreeViewColumn("Case Title", gtk.CellRendererText(), text=4)
titlecol.set_sort_column_id(4)
titlecol.set_resizable(True)
view.append_column(titlecol)

catcol = gtk.TreeViewColumn("Case Category", gtk.CellRendererText(), text=5)
catcol.set_sort_column_id(5)
catcol.set_resizable(True)
view.append_column(catcol)

view.show()


Then in order to do anything with the TreeView, you need to manipulate:
  • The ListModel
  • Iters
  • Paths

So to remove all the selected rows, you do something like this:
#get the selected rows, and return if nothing is selected
model, rows = self.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_iter(path) for path in rows]
for i in iters:
self.get_model().remove(i)

#select a row for the user, nicer that way
rows_remaining = len(self.get_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.get_selection().select_path(next_to_select)
else:
self.get_selection().select_path(rows_remaining - 1)


Enter DictionaryGrid (Easy and Fun)
Last week I found that the TreeView code that I wrote for bughugger was not flexible enough to incorporate some new features. In Karmic I wrote a TreeView wrapper for Desktop Couch called CouchGrid, so I had recent experience in how I could do this better, and I determined that this would be the last time that I write this code. Thus, the quidget DictionaryGrid was born. To use it, simply hand the DictionaryGrid a list of dictionaries to display and it will do the work for you.

Here is what the test code for DictionaryGrid looks like:
    dicts = [{"ID": 0, "key2": 5, "tags": "aaa bbb ccc","_foo":"bar"},
{"ID": 1, "key2": 6, "tags": "bbb ccc ddd","_foo":"bar"},
{"ID": 2, "key2": 7, "tags": "ccc ddd eee","_foo":"bar"},
{"ID": 3, "key2": 8, "tags": "ddd eee fff","_foo":"bar"},
{"ID": 4, "key2": 9, "tags": "eee fff ggg","_foo":"bar"}]

grid = DictionaryGrid(dicts)
grid.show()


This produces this:


Seems to make more sense right? That is Easy and Fun! You just bind the TreeView to the dictionary. There are a couple of things to make DictionaryGrid a bit more flexible:

  1. If you use keys that start with underscores, they are hidden by default, thus the key "_foo" is not displayed
  2. You can determine which keys to display and in what order by passing in a list of keys, so this:
    dicts = [{"ID": 0, "key2": 5, "tags": "aaa bbb ccc","_foo":"bar"},
{"ID": 1, "key2": 6, "tags": "bbb ccc ddd","_foo":"bar"},
{"ID": 2, "key2": 7, "tags": "ccc ddd eee","_foo":"bar"},
{"ID": 3, "key2": 8, "tags": "ddd eee fff","_foo":"bar"},
{"ID": 4, "key2": 9, "tags": "eee fff ggg","_foo":"bar"}]

keys = ["ID","tags"]
grid = DictionaryGrid(dicts, keys)

grid.show()


produces this:

You can use underscores and or the keys property to store data along with the dictionary that you don't want to display, and then retrieve the complete dictionary for the selected rows. If you've set the DictionaryGrid to be editable, the user can only edit the columns that are displayed, but all of the data that you passed along in the dictionaries is persisted. This is useful if you are persisting the data in a database or similar, and need to track something like the row id but don't want to expose it to the user. The code simply looks like this:
for row in self.grid.selected_rows:
url = "http://bugs.launchpad.net/bugs/" + row["_id"]
webbrowser.open(url)

Finally, DictionaryGrid is just a subclass of gtk.TreeView, so you don't have to sacrifice any of the power and flexibility of TreeView.

GridFilter
One of the coolest things about TreeViews is that they support lightening fast filtering. But building filters for them is onerous. In fact, so onerous, I don't even want to discuss it in this posting, if you are interested, you can peruse the documentation.

However, I have created a Quidget that makes it very easy to create a filter. To cut to the chase, this code:
    dicts = [{"ID": 0, "key2": 5, "tags": "aaa bbb ccc"},
{"ID": 1, "key2": 6, "tags": "bbb ccc ddd"},
{"ID": 2, "key2": 7, "tags": "ccc ddd eee"},
{"ID": 3, "key2": 8, "tags": "ddd eee fff"},
{"ID": 4, "key2": 9, "tags": "eee fff ggg"}]
grid = DictionaryGrid(dicts)
grid.show()

filt = GridFilter(grid)
filt.show()


produces something the user can use the filters like this:

Note that:
1. There are built in filters for strings, numbers, and tags
2. a column with a key of "Id" (case insensitive) will default to using a number filter, a column with a key of "tags" (case insentitive) will default to using a tag filter, other columns will default to a string filter
3. You can create custom filters, but I don't have a base class yet to make this easy, but the code is simple if you look at GridFilter.py. In fact, the tags filter was originally written by Loic Miner.

You can override the default filter by passing in a filter hint, like this:
    dicts = [{"ID": 0, "key2": 5, "tags": "aaa bbb ccc"},
{"ID": 1, "key2": 6, "tags": "bbb ccc ddd"},
{"ID": 2, "key2": 7, "tags": "ccc ddd eee"},
{"ID": 3, "key2": 8, "tags": "ddd eee fff"},
{"ID": 4, "key2": 9, "tags": "eee fff ggg"}]
hints = {"key2":NumericFilterCombo()}
grid = DictionaryGrid(dicts)
grid.show()

filt = GridFilter(grid,hints)
filt.show()

Which makes the key2 column use a number filter:


I've already started to pull these Quidgets into a branch of bughugger. I hope that other developers find them useful, and easy and fun.

And So It Begins


It appears that someone has successfully tricked a few Ubuntu users into install malware on their computers, see here, and here.



Note that this wasn't a security exploit in the sense that a coding bug was exploited. So far as I can tell, it was a bit of social engineering, where someone tricked someone into running a deb to get a cool screen saver, but the deb really only installed a bit of malware. I suppose this is a bit like a trojan, only the screen saver itself wasn't installed.

What this means is:
  1. Linux on the desktop has reached the phase where it is interesting for people to attack your system.
  2. The community may now be infiltrated by untrustworthy people, so the 99.999% of the good people will have to be a bit on the look out for the bad guys.
  3. Users will need to be on gaurd, and question the source of software or other goodies that they are installing a bit more.
Of course, users of Linux on the server have been aware of security threats for quite some time.

This situation is a big of a bummer, as I think as a community we've have the luxury of widespread trust, and being able to sample goodies from around the web. I suppose we'll need to do more to make it easy for good people to get their software into trusted repositories and, more to the point, put effort into ensuring those repositories are trustworthy!

In the meantime, remember, don't grant install privileges to random .debs. You wouldn't give the keys to your house to a stranger, right?

Tuesday, December 8, 2009

Quidgets: Fun and Easy Widgets



Quidgets (Quickly + Widgets) is a new project that I started be a container for all of the Python modules that I use over and over again in different forms throughout the projects that I work on. Essentially, I got tired of copying the modules into different projects, and I just couldn't find a proper upstream.

I plan to get Quidgets packaged and then into Universe so that I can simply set dependencies on Quidgets in projects like Bughugger. Also, if it's a unified project, I can accept other people's modules and have better collaboration on my modules.

One key thing about Quidgets, is that I want to keep all of the modules in it to follow the spirit of Quickly. Programming with Quickly modules should make Python programming, especially pygtk programming, more Easy and Fun. Since I am the only target user right now, it's easy to tell if my user base thinks it is easy and fun to program with Quickly modules.

Easy and Fun Threading
The first module that I added to Quidgets is my AsynchTaskProgressBox that I wrote over a year ago. This thing needs a better name. But in any case, since I wrote it, I haven't written any Ptyhon threading code, because this widget takes care of it for me.

I use AsynchTaskProgressBox to keep the UI from freezing while a program is doing long running task, like fetching something from the web. In order to write a threaded function I:
  1. write a function that I want to run on a thread
  2. instantiate an AsyncTaskProgressBox widget and hand the function to it, along with any parameters the function might need
  3. call "start()"

Trust me, this is waaaay easier than writing Python thread code yourself. Note also that I've created a way that you can sort of cancel a thread while it is running, and you get a free "Cancel" button, which you can hide if you want.

Here is a code sample from Bughugger:

from quidgets.widgets.asynch_task_progressbox import AsynchTaskProgressBox

def bugs_from_url(self, widget, data):
#the name of the search to use for the document tab and the url
#are passed in as data to this handler
name, url = data

#contruct a dictionary for passign this data into the AsynchTaskProgressBox
params = {"url":url, "name":name}

#create the progress box. __get_bugs_from_url is a function that retrieves
#bugs from a web server. It will get the params dictionary when it is run.
get_bugs_progressbox = AsynchTaskProgressBox(self.__get_bugs_from_url, params)

#add the progress box to the window. "progress_box" is the name of
#an HBox that I want to contain the AysnchTaskProgressBox
self.builder.get_object("progress_box").pack_end(get_bugs_progressbox, False, False)

#when the task is complete, I want it to call __get_bugs_complete
#whatever is returned from __get_bugs_from_url will be passed to
#__get_bugs_complete along with a reference to the AsynchProgressBox
#which is handy if you want to remove or hide it
get_bugs_progressbox.connect("complete",self.__get_bugs_complete)

#I want it to display, but this is optional
get_bugs_progressbox.show()

#Finally call start to kick of the thread
#the string is displayed in the progress box as it runs
get_bugs_progressbox.start("Retrieving team assigned bugs from Launchpad")


Here it is working:

So what is going on is that the ProgressBox is throbbing while the bugs are getting dowlnoaded from the server. The window UI does not block while that work is going on in the background. Again, there was NO THREAD code to write.

There's also sample code a the end of the code file in the trunk branch:


DictionaryGrid and the related GridFilter are also in there, will post on those later ....

Monday, December 7, 2009

Ubuntu Community Membership

Thanks to the Americas Regional Ubuntu board for considering me for Ubuntu Membership and for accepting me!

I've been working on Ubuntu as the Engineering Manager for the Desktop Team for a bit over a year now, but I was waiting until I felt that I had made some contributions that didn't necessarily rely on that position before I applied for membership.

Now I can start using my @ubuntu email address!

Saturday, November 21, 2009

Lucid Lynx Development Process

robbiew and I covered how we plan to schedule and track development during the Lucid Lynx cycle. Here is a video of our plenary session at UDS where we explain it:
(our part starts at 13m52s)


Slides are here.
The plan boils down to two things:

  1. We will use burndown charts to track progress over the course of the release.

  2. We will be building a bit more agility into our schedule by working in three separate milestones. These are simply overlaid on top of the normal Ubuntu schedule.



The milestones look like this:





















GoalMilestoneDate
Initial Integration and Risky WorkAlpha 22010-01-14
DevelopmentAlpha 32010-02-25
Clean Up and CompletionBeta 22010-04-08


The Lucid Lynx Release Schedule is not effected by this plan,

The desktop team kept a burndown chart for Karmic, and it worked quite well.

Saturday, October 17, 2009

Quickly Demo Parts 2 and 3

Thanks to Jorge Castro, I now have hi def embeds for the Quickly intro videos.

As promised, here is part 2, which covers adding a toolbar in Glade and responding to the signal, as well as changing the columns in the CouchGrid:


And here is part 3, which shows how to make a debian package with Quickly:


Join us in #quickly on freenode if you have any questions whatsoever! Thanks.

Monday, October 12, 2009

Quickly Demo Part 1

Quickly is a new feature in the Karmic Koala that is designed to make it easy and fun to write, package, and share programs.

Today I made a quick video to give developers or people interested in development on Ubuntu a sense of what programming with Quickly is like. It's an 18 minute video total, so I'm splitting it into three parts. Since the video has a lot of typing, you'll probably want to run it in full screen so you can see all the good stuff.

Here's is Part 1, which covers getting an application started with Quickly:


Part 2 will show tweaking the application a bit, and part 3 shows packaging the application.

Parts 2 and 3 are now available here


The video is just supposed to be kind of a teaser, to get you interested in installing and trying Quickly? To really get started with Quickly, first, you should be running Karmic (Ubuntu version 9.10 which will be released in it's final form soon.

Then, open a terminal, and install quickly and fire up the tutorial:
$sudo apt-get install quickly
$quickly tutorial ubuntu-project

[edit]Added embed for full def video[/edit]

Thursday, October 8, 2009

Karmic as the Collaborators' Desktop


Here's a screen shot to pimp a few things:
1. This is Jono Bacon and I working together. We are video conferencing with Empathty.
2. I am sharing my screen so Jono can look at what I am working on.
3. What I am working on is bughugger, a tool that I am starting up that is a desktop-based front end to Launchpad for managing large sets of bugs.
4. And I am writing it with Quickly, a developer tool set is new for Karmic.

Karmic is rock solid for me right now. I think this is going to be the single best desktop I have ever run.

[edit] Jono has a mirror image to this post.