Intro
quickly-widgets is a personal project of mine
the goal of quickly widgets is to make PyGtk programming more easy and fun
PyGtk is a fantastic UI tool kit
It is very powerful, functional, and flexible
However, at the moment, it lacks some higher order abstractions that developers have come to expect
The result being that developers have to write reams of code using multiple classes and such for things that are more simple in other frameworks
for example, popping dialogs, putting data into a grid and such
so I'm hoping quickly-widgets can fill in some of these use cases
please note that quickly-widgets is very early in development and experimental
this means that it is going to be buggy and incomplete
but it also means that if you want to, you can have a lot of influence on how it develops
so bug reports and branches, more than welcome
so what is Quidgets?
"Quidgets" is the original name of quickly-widgets
the project is still called Quidgets
so you can get the code:
$bzr branch lp:quidgets
quickly-widgets is currently in universe for Lucid
it is not packaged for Karmic
though if someone wanted to do that, I would not complain too much
currently, quickly widgets has two modules
quickly.prompts
and quickly.widgets
quickly.widgets has some general widgets
but also some widgets for displaying data, basically extending TreeView and making it simpler to use
I'll discuss each of these three areas in a bit more depth
But first, Questions?
Usage Model
Before we look at the modules, a word on the mental model of using quickly-widgets
There are 3 "levels" to consuming quickly-widgets
Most of the time, you should be fine with the first level, which is the "Use" level
At this level, you just make a one-liner that uses the API and does something useful
like, get as string from the user: quickly.prompts.string("My Title","Give me a string","default val")
or maybe you want do display the data in a dictionary: dg = DictionaryGrid(my_list_of_dicts) dg.show()
sometimes you want to tweak something a bit. In these cases you go to the configure level.
When you are doing "configure" you might end up a using the underlying PyGtk API a bit
For example, a dictionary grid is really just a Treeview
So if you want to configure the title for a column, you might go
dg.get_columns()[0].set_title("New Title")
some of the configuration might be done in the quickly.widgets library though
like if you want to display certain keys from your dictionary, you can use a list of keys to display
keys = ["key1","key2'] dg = DictionaryGrid(my_dict,keys=keys)
let's say you get back a feature request from a user or customer, and you need to do something that is not suppported by quickly-widgets through using or configuring
well, quickly-widgets strives to consume PyGtk in such a way that you can extend classes as needed to accomplish your goals
for example, if you want a grid that displays data from a certain SQL database somewhere, you can inherit from DictionaryGrid and add the SQL functionality.
This is exactly what CouchGrid does, but for generally desktopcouchy things
So, before I discuss using the prompts a bit, any questions?
Prompts
Prompts are basically dialogs that you can use to interact with the user without writing too much code
rather than creating a PyGtk dialog or similar yourself, and then populating it, for certain scenarios a prompt in quickly-widgets can take care of it for you
the prompts module is right under quickly, so you can get it like this: from quickly import prompts
or just quickly.prompts if you want
there are three kinds of prompts
the first kind just displays info the the user
these take a couple of strings from you and display a message box to the user with the appropriate icon
so, you can go
prompts.error("Title for the Error Dialog","Error text")
there is also prompts.warning and prompts.info
they works the same as error
The next kind of prompt collects some info from the user
the kind of prompts built in of this kind are: string, date, integer, decimal, and price
they all take care of displaying an appropriate widget for the user input for you
like a textbox for a string, a calendar for a date, and a spinner appropriately formatted for the other two
they let you set a title, a message, a default value, and in the case of the numeric ones some other defaults if you want
the functions all return a gtk.RESPONSE and a value
make sure you check the response because if the user cancelled, you want to do the right thing
so to get a string, go: response, val = quickly.prompts.string() if response == gtk.RESPONSE_OK:
- #do something with val
where val is a string
you can get a date in the same way
when dealing with date values are tuples in the form of integers for (year,month,day)
where month is zero indexed (Jaunary is 0, December is 11)
and you use tuples for the default value in teh same way
so: response, date = quickly.widgets.date("Title String","Enter your birthday",(1968,03,22))
so that's *April* because the months are zero indexed
if you want to get an integer, you can set min_value, and max_value as well
if you look at the doc string, you can see other options as well
decimal and price are also rich in options
price is just decimal, but with 2 decimal places set
however, they all work basically the same way, response, val = function()
if you need to configure the prompt a bit more, for each function, there is a symetrical class that you can use
so for string(), if you want to own the dialog for some reason, you can use StringPrompt
sp = StringPrompt(title, text, default_string)
then you can party on sp if you want
there is a similar dialog for date, integer, decimal, and price
each has a "get_value()" function to extract the value
The last set of prompts are FileChooserDialogs
they work the same way
response, path = quickly.prompts.choose_directory(title)
there is choose_directory, save_image_file, and open_image_file
there are classes for these as well
DirectoryChooserDialog, OpenImageDialog, and SaveImageDialog
these lack get_value, because they are just subclasses of FileChooserDialog
which already has a get_filename() function
before I talk about the gtk.Widgets, any questions?
Widgets
for widgets, there are a few miscelaneous ones, and then the grid related ones
I'll start with the miscelaneous ones
the most miscelaneous one is
from quickly.widgets.camera_button import CameraButton
CameraButton wraps up the PyGame web cam API and serves up pixbufs
I don't like how this is implemented at the moment, and am planning to pull it into it's own package for a while, until I can make it work well
however, it's kinda fun when it works
there's also a subclass of button called PressAndHold button
this guy fires a signal every 250ms
so this allows you to do something while the user is holding down a button
I use this in photobomb to manage rotation of times for example
first, create the button, then connect to the signal
button = PressAndHoldButton() button.connect("tick",action)
then write a function that does something every time the signal fires def action(self, widget=None, data=None):
- #do something every 250ms
the last misc. quickly.widget is
from quickly.widgets.asynch_task_progressbox import AsynchTaskProgressBox
in general, you should avoid threads in your python app
you should use things like gobject.timeout_add() or gobject.idle_add()
however, for times when you absolutely must use a thread AsynchTaskProgressBox can make it easier for you
this derives from gtk.ProgressBox, so it supplies some throbbing UI for you if you need it
the basic idea is to write a function that you want to run on a thread
I usually call this a task
create a dictionary of parameters if you want to pass some params to the function
create the AsynchTaskProgressBox
if you want to know when the task is done, connect to the complete event
here's some photobomb code for example
params = {"directory":directory} pb = AsynchTaskProgressBox(self.load_task, params, False) pb.connect("complete",self.load_task_complete)
for your task, you can simulate a killable thread (in python you can't really kill a thread, just wait for it to end)
you do this by checking a special key that is added to params called "kill"
if it's set to True, you can stop working
here's a bit of code from photobomb to demonstrate
def load_task(self, params):
- pictures_dir = params["directory"] images = [] files = os.listdir(pictures_dir) for f in files:
- try:
- if params["kill"]:
- return None
- if params["kill"]:
- try:
- pictures_dir = params["directory"] images = [] files = os.listdir(pictures_dir) for f in files:
So the last bit about quickly-widgets is about Grids and Filters and stuff
before I do that, any questions so far?
Grids
lots of the code in quickly-widgets is about presenting data to users in a TreeView
in PyGtk, gtk.TreeView handles both displaying grid data and data in trees
quickly-widgets focuses on the Grid
the essentially scenario is that you have some tabular data to display
gtk.TreeView can let you display this in any manner you like
so long as you:
1. Create a TreeView 2. Cerate a treeview model store of some kind 3. create columns 4. create cellrenders for each column 5. popluate the treeview model
from quickly.widgets.dictionary_grid import DictionaryGrid tries to handle this for you in essentially, one line of code
the basic interaction is that you create a DictionaryGrid by handing it a list of dictionary, and it makes a TreeView for you
you just show it and add it to your form
Here's a bit from the dictionary grid test program
first, a list of dictionaries dicts = [{"key?": True, "price":0.00,"tags" : "aaa bbb ccc","_foo":"bar","bing count":20},
- {"ID": 11, "key?": False, "price":2.00,"tags" : "bbb ccc ddd","_foo":"bar"}, {"key?": True, "price":33.00,"tags" : "ccc ddd eee","_foo":"bar","bing count":15}, {"ID": 3, "tags" : "ddd eee fff","_foo":"bar"}, {"ID": 4, "price":5.00,"_foo":"bar"}]
then, create the DictionaryGrid: grid = DictionaryGrid(dicts)
if you want the user to be able to edit the grid, set it to editable: grid.editable = True
note that setting some of the properties, including editable will cause the grid to reset itself
you can add to your grid by appending rows grid.append_row(my_dict)
by default, the grid will display a column for every key it encounters UNLESS that key starts with "" (two underscores), in which case it will be ignored
if you want to control the keys displayed, you can do this when you create it: grid = DictionaryGrid(dicts,list_of_keys)
or you can set the keys later grid.keys = list_of_keys
this causes the grid to reset, btw
Columns will sort properly most of the time
this is accomplished by having each column be a certain type there is the default StingColumn, but there is also IntegerColumn, CurrencyColumn, TagsColumn, and CheckColumn
DictionaryGrid tries to guess the right column type to use based on the name of the key
So if you choose your key names correctly, you can get a lot of good functionality for free
for example, and key that ends with "?" DictionaryGrid will assume it is a boolean, and use CheckBoxes to show the values
and provide the appropriate sortign for you
he key "price" is assumed to be currency
anything ending in count will be an IntegerColumn, same with a key called "id"
a key called "tags" will be a tags column
tags columns are mostly important for filtering, which I will discuss in a moment
you can control the columntypes manually if you want, by passing in "Type Hints"
here's some code from the tests:
- keys = ["id","price","bool?","foo"]
hints = {"id":StringColumn, "price":IntegerColumn,
"bool?":CurrencyColumn,"foo":CheckColumn}
grid = DictionaryGrid(dicts, keys, hints)
in this case hints are overriding the defaults
so I should talk about filtering next, but first, questions?
so one cool thing about gtk.TreeView is the filtering capabilities
Filtering even pretty long lists works really fast
from quickly.widgets.grid_filter import GridFilter makes a filter UI for you
this will let the user add filters to filter columns
if you've seen the bughugger UI, then you have seen GridFilter and DictionaryGrid in action
you just need to hand it a Grid to filter
it will pick up filter types from the GridColumns in the Grid
So for example columns with strings will get a filter with "contains substring" while a column with numbers will get ">,<.=", etc...
all this just by creating it:
filt = GridFilter(grid) filt.show()
if you want to override the default filter types, you can pass in hints
this works pretty much like in the grid
you pass in a dictionary that matches up keys to filter types
well, actually to filter combos, but that's a bit complex
there are StringFitlerCombo, TagsFilterCombo, CheckFilterCombo etc...
there is also blank filter combo that you can use to easily make your own filters
for example, in bughugger I created a custom filter to filter status and importance like this:
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}
each call to append adds a string and a function call
Finally, there is CouchGrid
I feel this has been talked about a lot
In Lucid, a CouchGrid is a DictionaryGrid
so you get all the sorting and filtering of DictionaryGrid, plus persistance in desktopcouch
you need to define a database and a record type
if you want, you can pass in a dictionary with some starter data as well
here is some code from the GridFilter test app
- database_name = "couch_widget_test" record_type = "couch_grid_filter_test" hints = {}
grid = CouchGrid(database_name, record_type=record_type, dictionaries=dicts) grid.show()
filt = GridFilter(grid,hints) filt.show()
Questions?