Canonical Voices

What The Raving Rick talks about

Posts tagged with 'quidgets'

Rick Spencer

I started working on a chapter for the Ubuntu Developers' Manual. The chapter will be on how to use media in your apps. That chapter will cover:

  • Playing a system sound
  • Showing an picture
  • Playing a sound file
  • Playing a video
  • Playing from a web cam
  • Composing media
I created an app for demonstrating some of these things in that chapter. After I wrote the app, I realized that it shows a lot of different parts of app writing for Ubuntu:
  • Using Quickly to get it all started
  • Using Glade to get the UI laid out
  • Using quickly.prompts.choose_directory() to prompt the user
  • Using os.walk for iterating through a directory
  • Using a dictionary
  • Using DictionaryGrid to display a list
  • Using MediaPlayerBox to play videos or Sounds
  • Using GooCanvas to compose a singe image out of images and text
  • Using some PyGtk trickery to push some UI around
A pretty decent amount of overlap with the chapter, but not a subset or superset. So I am writing a more full tutorial to post here, and then I can pull out the media specific parts for the chapter later. Certain things will change as we progress with Natty, so I will make edits to this posting as those occur. So without Further Ado ...

Simple Player Tutorial
Introduction
In this tutorial you will build a simple media player. It will introduce how to start projects, edit UI, and write the code necessary to play videos and songs in Ubuntu.
The app works by letting the user choose a directory. Simple Player then puts all the media into a list. The user can choose media to play from that list.

This tutorial uses Quickly, which is an easy and fun way to manage application creation, editing, packaging, and distribution using simple commands from the terminal. Don't worry if you are used to using an IDE for writing applications, Quickly is super easy to use.

Requirements
This tutorial is for Ubuntu Natty Narwhal (11.04). There are some key differences between 10.10 and 11.04 versions of Quickly and other tools that will make it hard to do the tutorial if you are not on Natty. So, probably best to make sure you are running 11.04.

You also need Quickly. To install Quickly:

$sudo apt-get install quickly python-quickly.widgets

This tutorial also uses a yet to be merged branch of Quickly Widgets. In a few weeks, you can just install quickly-widgets, but for now, you'll need to get the branch:

$bzr branch lp:~rick-rickspencer3/quidgets/natty-trunk

Note that these are alpha versions, so there may be bugs.

Caution About Copy and Pasting Code

In Python, white space is very significant, especially in terms of indentions. In HTML, white space is not. As a result, Blog postings frequently mangle Python code, no matter how carefully a blogger might format it. So while you're following along, be careful about copying and pasting out of here.

If you're going to copy and paste, you might want to use the code for the tutorial project in launchpad, from this:
Link to Code File in the Launchpad Project

You can also look at the tutorial in text format this:
Link to this tutorial in text for in Launchpad

Creating the Application
You get started by creating a Quickly project using the ubuntu-application template. Run this command in the terminal:
$quickly create ubuntu-application simple-player

This will create and run your application for you.

Notice that the application knows it is called Simple Player, and the menus and everything work.

To edit and run the application, you need to use the terminal from within the simple-player directory that was created. So, change into that directory for running commands:

$cd simple-player

Edit the User Interface
We'll start by the User Interface with the Glade UI editor. We'll be adding a lot of things to the UI from code, so we can't build it all in Glade. But we can do some key things. We can:
  • Layout the HPaned that separates the list from the media playing area
  • Set up the toolbar
Get Started
To run Glade with a Quickly project, you have to use this command from within your project's directory:
$quickly design

If you just try to run Glade directly, it won't work with your project.
Now that Glade is open, we'll start out by deleting some of the stuff that Quickly put in there automatically. Delete items by selecting them and hitting the delete key. So, delete:
  • label1
  • image1
  • label2
This will leave you with a nice blank slate for your app:
Now, we want to make sure the window doesn't open too small when the app runs. Scroll to the top of the TreeView in the upper right of Glade, and select simple_player_window. Then in the editor below, click the common tab, and set the Width Request and Height Request.
There's also a small bug in the quickly-application template, but it's easy to fix. Select statusbar1, then on the packing tab, set "Pack type" to "End".

Save your changes or they won't show up when you try running the app! Then see how your changes worked by using the command:
$quickly run

A nice blank window, ready for us to party on!
Adding in Your Widgets
The main part of the user interface is going to have an area that divides between the list of media and the media when it is playing. There is widget for that called HPaned (Horizontal Paned). Find HPaned on the toolbox on the left, and click on it to active paint mode. Then click into the second open space in the main part of the window. This will put the HPaned in the window for you.

Make sure the HPaned starts out with an appropriate division of space. Do this by going to the General tab, and setting an appropriate number of pixels in Position property.
The user should be able to scroll through the list, so click on ScrolledWindow in the toolbar, and then click in the left hand part of the HPaned to place it in there.

Now add a toolbar. Find the toolbar icon in the toolbox, click on it and click in the top space open space. This will cause that space to collapse, because the toolbar is empty by default.
To add the open button click the edit button (looks like pencil) in Glade's toolbar. This will bring up the toolbar editing dialog. Switch to the Hierarchy tab, and click "Add". This will add a default toolbar button.

To turn this default button into an open button, first, rename the button to openbutton (this will make it easier to refer to in code). Then under Edit Image set Stock Id to "Open". That's all you need to do to make an open button in Glade.

Due to a bug in the current version of Glade, you might need to rename your tool bar button again. When you close the editor, look in the treeview. If the button is still called "toolbutton1", then select it, and use the general tab to change the Name property to "openbutton". Then save again.

Now if you use $quickly run again, you'll see that your toolbar button is there.

Coding the Media List
Making the Open Button Work
The open button will have an important job. It will respond to a click from the user, offer a directory chooser, and then build a list of media in that directory. So, it's time write some code.

You can use:
$quickly edit &

This will open your code Gedit, the default text and code editor for Ubuntu.

Switch to the file called "simple-player". This is the file for your main window, and the file that gets run when users run your app from Ubuntu.
First let's make sure that the open button is hooked up to the code. Create a function to handle the signal that looks like this (and don't forget about proper space indenting in Python!):

def openbutton_clicked_event(self, widget, data=None):
print "OPEN"


Put this function under "finish_initializing", but above "on_preferences_changed". Save the code, run the app, and when you click the button, you should see "OPEN" printed out to the terminal.

How did this work? Your Quickly project used the auto-signals feature to connect the button to the event. To use auto-sginals, simple follow this pattern when you create a signal handlder:

def widgetname_eventname_event(self, widget, data=None):

Sometimes a signal handler will require a different signature, but (self, widget, data=None) is the most common.

Getting the Directory from the User
We'll use a convenience function built into Quickly Widgets to get the directory info from the user. First, go to the import section of the simple-player file, and around line 11 add an import statement:


from quickly import prompts

Then add to your openbutton_clicked_event function the code to prompt the user so it looks like this:

def openbutton_clicked_event(self, widget, data=None):
#let the user choose a path with the directory chooser
response, path = prompts.choose_directory()

#make certain the user said ok before working
if response == gtk.RESPONSE_OK:
#iterate through root directory
for root, dirs, files in os.walk(path):
#iterate through each file
for f in files:
#make a full path to the file
print os.path.join(root,f)

Now when you run the app you can select a directory, and it will print a full path to each file encountered. Nice start, but what the function needs to do is build a list of files that are media files and display those to the user.

Defining Media Files
This app will use a simple system of looking at file extensions to determine if files are media files. Start by specifying what file types are supporting. Add this in finish_initializing to create 2 lists of supported media:

self.supported_video_formats = [".ogv",".avi"]
self.supported_audio_formats = [".ogg",".mp3"]

GStreamer supports a lot of media types so ,of course, you can add more supported types, but this is fine to start with.

Now change the openbutton handler to only look for these file types:

def openbutton_clicked_event(self, widget, data=None):
#let the user choose a path with the directory chooser
response, path = prompts.choose_directory()

#make certain the user said ok before working
if response == gtk.RESPONSE_OK:
#make one list of support formats
formats = self.supported_video_formats + self.supported_audio_formats
#iterate through root directory
for root, dirs, files in os.walk(path):
#iterate through each file
for f in files:
#check if the file is a supported formats
for format in formats:
if f.lower().endswith(format):
#make a full path to the file
print os.path.join(root,f)

This will now only print out files of supported formats.

Build a List of Media Files
Simple Player will create a list of dictionaries. Each dictionary will have all the information that is needed to display and play the file. Simple Player will need to know the File name to display to the user, a URI to the file so that the file can be played, and the type of media. So, we'll create a list and add a dictionary to each support type to it.

def openbutton_clicked_event(self, widget, data=None):
#let the user choose a path with the directory chooser
response, path = prompts.choose_directory()

#make certain the user said ok before working
if response == gtk.RESPONSE_OK:
#make one list of support formats
formats = self.supported_video_formats + self.supported_audio_formats

#make a list of the supported media files
media_files = []
#iterate through root directory
for root, dirs, files in os.walk(path):
#iterate through each file
for f in files:
#check if the file is a supported formats
for format in formats:
if f.lower().endswith(format):
#create a URI in a format gstreamer likes
file_uri = "file://" + os.path.join(root,f)

#add a dictionary to the list of media files
media_files.append({"File":f,"uri":file_uri, "format":format})
print media_files

Display the List to the User
A DictionaryGrid is the easiest way to display the files, and to allow the user to click on them. So import DicationaryGrid at line 12, like this:

from quickly.widgets.dictionary_grid import DictionaryGrid
Starting in Natty, every window has a ui collection. You can use it to access all of the widgets that you have defined in Glade by their names. So, creating the list of media files, you can remove any old grids in the scrolled window like this:

for c in self.ui.scrolledwindow1.get_children():
self.ui.scrolledwindow1.remove(c)
Then create a new DictionaryGrid. We only want one column, to the view the files, so we'll set up the grid like this:

#create the grid with list of dictionaries
#only show the File column
media_grid = DictionaryGrid(media_files, keys=["File"])

#show the grid, and add it to the scrolled window
media_grid.show()
self.ui.scrolledwindow1.add(media_grid)

So now the whole function looks like this:

def openbutton_clicked_event(self, widget, data=None):
#let the user choose a path with the directory chooser
response, path = prompts.choose_directory()

#make certain the user said ok before working
if response == gtk.RESPONSE_OK:
#make one list of support formats
formats = self.supported_video_formats + self.supported_audio_formats

#make a list of the supported media files
media_files = []
#iterate through root directory
for root, dirs, files in os.walk(path):
#iterate through each file
for f in files:
#check if the file is a supported formats
for format in formats:
if f.lower().endswith(format):
#create a URI in a format gstreamer likes
file_uri = "file://" + os.path.join(root,f)

#add a dictionary to the list of media files
media_files.append({"File":f,"uri":file_uri, "format":format})

#remove any children in scrolled window
for c in self.ui.scrolledwindow1.get_children():
self.ui.scrolledwindow1.remove(c)

#create the grid with list of dictionaries
#only show the File column
media_grid = DictionaryGrid(media_files, keys=["File"])

#show the grid, and add it to the scrolled window
media_grid.show()
self.ui.scrolledwindow1.add(media_grid)

Now the list is displayed when the user picks the directory.

Playing the Media
Adding the MediaPlayer
So now that we have the list of media for the users to interact with, we will use MediaPlayerBox to actually play the media. MediaPlayerBox is not yet integrated into Glade, so we'll have to write code to add it in. As usually, start with an import:

from quickly.widgets.media_player_box import MediaPlayerBox

Then, we'll create and show a MediaPlayerBox in the finish_initializing function. By default, a MediaPlayerBox does not show it's own controls, so pass in True to set the "controls_visible" property to True. You can also do things like this:


player.controls_visible = False
player.controls_visible = True

to control the visibility of the controls.

Since we'll be accessing it a lot, we'll create as a member variable in the SimplePlayerWindow class. Then to put it in the right hand part of the HPaned, we use the add2 function (add1() would put it in the left hand part).

self.player = MediaPlayerBox(True)
self.player.show()
self.ui.hpaned1.add2(self.player)


Connecting to the DictionaryGrid Signals
Now we need to connect the dictionary_grid's "selection_changed" event, and play the selected media. So back in the openbutton_clicked_event function, after creating the grid, we can connect to this signal. We'll play a file when selection changes, so we'll connect to a play_file function (which we haven't created yet). This goes at the end of the function:

#hook up to the selection_changed event
media_grid.connect("selection_changed", self.play_file)
Now create that play_file function, it should look like this:

def play_file(self, widget, selected_rows, data=None):
print selected_rows[-1]["uri"]

Notice that the signature for the function is a little different than normal. When the DictionaryGrid fires this signal, it also passes the dictionaries for each row that is now selected. This greatly simplifies things, as typcially you just want to work with the data in the selected rows. If you need to know more about the DictionaryGrid, it passes itself in as the "widget" argument, so you can just work with that.

All the function does now is get the last item in the list of selected rows (in Python, you can use -1 as an index to get the last item in a list. Then it prints the URI for that row that we stored in the dictionary back in openbutton_clicked_event.
Setting the URI and calling play()
Now that we have the URI to play, it's a simple matter to play it. We simply set the uri property of our MediaPlayerBox, and then tell it to stop playing any file it may be playing, and then to play the selected file:

def play_file(self, widget, selected_rows, data=None):
self.player.stop()
self.player.uri = selected_rows[-1]["uri"]
self.player.play()

Now users can click on Videos and movies, and they will play. Since we decided to show the MediaPlayerBox's controls when we created it, we don't need to do any work to enable pausing or stopping. However, if you were creating your own controls, you could use player.pause() and player.stop() to use those functions.


Connecting to the "end-of-file" Signal
When a media files ends, users will expect the next file played automatically. It's easy to find out when a media file ends using the MediaPlayerBox's "end-of-file" signal. Back in finish_initializing, after creating the MediaPlayerBox, connect to that signal:

self.player.connect("end-of-file",self.play_next_file)

Changing the Selection of the DictionaryGrid
Create the play_next_file function in order to respond when a file is done playing:

def play_next_file(self, widget, file_uri):
print file_uri

The file_uri argument is the URI for the file that just finished, so that's not much use in this case. There is no particularly easy way to select the next row in a DictionaryGrid. But every widget in Quickly Widgets is a subclass of another PyGtk class. Therefore, you always have access to full power of PyGtk. A DictionaryGrid is a TreeView, so you can write code to select the next item in a TreeView:

def play_next_file(self, widget, file_uri):
#get a reference to the current grid
grid = self.ui.scrolledwindow1.get_children()[0]

#get a gtk selection object from that grid
selection = grid.get_selection()

#get the selected row, and just return if none are selected
model, rows = selection.get_selected_rows()
if len(rows) == 0:
return

#calculate the next row to be selected by finding
#the last selected row in the list of selected rows
#and incrementing by 1
next_to_select = rows[-1][0] + 1

#if this is not the last row in the last
#unselect all rows, select the next row, and call the
#play_file handle, passing in the now selected row
if next_to_select < len(grid.rows):
selection.unselect_all()
selection.select_path(next_to_select)
self.play_file(self,grid.selected_rows)

Making an Audio File Screen
Notice that when playing a song instead of a video, the media player is blank, or a black box, depending on whether a video has been player before.
It would be nicer to show the user some kind of visualization when a song is playing. The easiest thing to do would be to create a gtk.Image object, and swap it when for the MediaPlayerBox when an audio file is playing. However, there are more powerful tools at our disposal that we can use to create a bit richer of a user experience.

This section will use a GooCanvas to show you how to compose images and text together. A GooCanvas is a very flexible surface on which you can compose and animate all kinds of 2D experiences for users. This tutorial will just scratch the surface, by combining 2 images and some text together. We'll show the Ubuntu logo image that is already built into your project, but a musical note on top of that for some style, and then put the current song playing as some text.

Create a Goo Canvas
Naturally, you need to import the goocanvas module:

import goocanvas

Then, in the finish_initializing function, create and show a goocanvas.Canvas:

self.goocanvas = goocanvas.Canvas()
self.goocanvas.show()

The goocanvas will only be added to the window when there is an audio playing file, so don't pack it into the window yet. But let's an image to the goocanvas so we can make sure that we have the system working.

Add Pictures to the GooCanvas
Add an image to the goocanvas by creating a goocanvas.Image object. First, we'll need to create a gtk.Pixbuf object. You can think of a gtk.Pixbuf as an image stored in memory, but it has a lot of functions to make them easier to work with than just having raw image data. We want to use the file called "background.png". In a quickly project, media files like images and sounds should always go into the data/media directory so that when users install your programs, the files will go to the correct place. There is a helper function called get_media_file built inot quickly projects to get a URI for any media file in the media directory. You should always use this function to get a path to media files, as this function will work even when your program is installed and the files are put into different places on the user's computer. get_media_file returns a URI, but a pixbuf expects a normal path. It's easy to fix this stripping out the beginning of the URI. Since it was created for you, can could also change the way get_media_player works, or create a new function, but this works too:

logo_file = helpers.get_media_file("background.png")
logo_file = logo_file.replace("file:///","")
logo_pb = gtk.gdk.pixbuf_new_from_file(logo_file)


You don't actually pass the goocanvas.Image into the goocanvas.Canvas, rather you tell the goocanvas.Image that it's parent is the rootA_items of the goocanvas. You can also set other properties when you create it, such as the x and y coordinates, and of course the pixbuf to use:

root_item=self.goocanvas.get_root_item()
goocanvas.Image(parent=root_item, pixbuf=logo_pb,x=20,y=20)


Show the GooCanvas When a Song is Playing
So now we want to take the MediaPlayerBox out of the HPaned when a song is playing and show the goocanvas, and also visa versa. We can easily extract the format of the file because we included it in the dictionary for the row when we created the DictionaryGrid in the openbutton_clicked_event function:

format = selected_rows[0]["format"]

We can also get a reference to the visual that is currently in use:

current_visual = self.ui.hpaned1.get_child2()

Knowing those two things, we can then figure out whether to put in the goocanvas.Canvas or the MediaPlayerBox. So the whole function will look like this:

def play_file(self, widget, selected_rows, data=None):
self.player.stop()
format = selected_rows[0]["format"]
current_visual = self.ui.hpaned1.get_child2()

#check if the format of the current file is audio
if format in self.supported_audio_formats:
#if it is audio, see if the current visual is
#the goocanvas, if it's not, do a swapperoo
if current_visual is not self.goocanvas:
self.ui.hpaned1.remove(current_visual)
self.ui.hpaned1.add2(self.goocanvas)
else:
#do the same thing for the player
if current_visual is not self.player:
self.ui.hpaned1.remove(current_visual)
self.ui.hpaned1.add2(self.player)

#go ahead and play the file
self.player.uri = selected_rows[-1]["uri"]
self.player.play()



Add another Image to Canvas
We can add the note image to the goocanvas.Canvas in the same way we added the background image. However, this time we'll play with the scale a bit:


note_file = helpers.get_media_file("note.png")
note_file = note_file.replace("file:///","")
note_pb = gtk.gdk.pixbuf_new_from_file(note_file)
note = goocanvas.Image(parent=root_item, pixbuf=note_pb,x=175,y=255)
note.scale(.75,.6)

Remember for this to work, you have to put a note.png file in the data/media directory for your project. If your image is a different size, you'll need to tweak the x, y, and scale as well.

(BTW, thanks to Daniel Fore for making the artwork used here. If you haven't had the pleasure of working Dan, he is a really great guy, as well as a talented artist and designer. He's also the leader of the #elementary project.)

A goocanvas.Image is a goocanvas.Item. There are different kinds of Items and many of interesting visual things you can do with them. There are items like shapes and paths. You can change things like their scale, rotation, and opacity. You can even animate them!
Add Text to the goocanvas.Canvas
One kind of goocanvas.Item is goocanvas.Text. You create it like a goocanvas.Image. We won't use any text when we create it, because that will be set later when we are playing a song. Since the goocanvas.Text will be accessed from the play_file function, it should be a member variable for the window. So after adding the note image in the finish_initializing function, you can go ahead and add the text.

self.song_text = goocanvas.Text(parent=root_item,text="", x=5, y=5)
self.song_text.set_property("font","Ubuntu")
self.song_text.scale(2,2)

Update the Text
The text property of the goocanvas.Text object should then be set when an audio file is played. Add a line of code to do this in the play_file function, after you've determined the file is an audio file:

self.song_text.set_property("text",selected_rows[0]["File"])

Now when an audio file is playing the title shows.

Moving the Media Player Controls
You've probably noticed a pretty bad bug, when an audio file is playing the user can't access the controls for the media player. Even if that were not the case, are 2 toolbars, one for the controls, and one that only has the openbutton. Also, the controls are shifted over because of the DictionaryGrid, so the time labels are not visible by default.

Fortunately, PyGtk let's you move widgets around really easily. So, it's possible to write a little code that:
  1. Creates the openbutton in code instead of glade
  2. Takes the toolbar for the MediaPlayer controls out of the MediaPlayer
  3. Inserts the openbutton into the controls exactly where we want it
  4. Adds the controls back into the window
To start, go back to Glade, and delete the toolbar you added before. Replace it with an HBox. When prompted, set Number of Items to 1. It should be named hbox1 by default. After adding the HBox choose the packing tab, and set Expand to "No". Otherwise, the HBox will take up all the room it can, making the toolbar huge when you add it back in.
Then, back in finish_initializing, after creating the MediaPlayerBox, remove the controls:

self.player = MediaPlayerBox(True)
self.player.remove(self.player.controls)

Then, create a new openbutton:

open_button = gtk.ToolButton()

We still want the open button to be a stock button. For gtk.ToolButtons, use the set_stock_id function to set the right stock item.

open_button.set_stock_id(gtk.STOCK_OPEN)

Then show the button, and connect it to the existing signal handler.

open_button.show()
open_button.connect("clicked",self.openbutton_clicked_event)

The MediaPlayerBox's controls are a gtk.Toolbar object. So, insert the open_button into the controls using the gtk.Toobar classes insert command. Pass in a zero to tell the gtk.Toolbar to put open_button first. Then you can show the controls, and pack them into the window:

self.player.controls.insert(open_button, 0)
self.ui.hbox1.pack_start(self.player.controls, True)

Now users can use the controls even when audio is playing!
Conclusion
This tutorial demonstrated how to use Quickly, Quickly Widgets, and PyGtk to build a functional and dynamic media player UI, and how to use a goocanvas.Canvas to add interesting visual effects to your program.

The next tutorial will show 2 different ways of implementing play lists, using text files, using pickling, or using desktopcouch for storing files.

API Reference
PyGtk
Quickly Widgets
Reference documentation for Quickly Widgets isn't currently hosted anywhere. However, the code is thoroughly documented, so until the docs are hosted, you can use pydocs to view them locally. To do this, first start pydocs on a local port, such as:
$pydocs -p 1234

Then you can browse the pydocs by opening your web browser and going to http:localhost:1234. Search for quickly, then browse the widgets and prompts libraries.

Since MediaPlayerBox is not installed yet, you can look at the doc comments in the code for the modules in natty-branch/quickly/widgets/media_player_box.py.
GooCanvas
GStreamer
MediaPlayerBox uses a GStreamer playbin to deliver media playing functionality. GStreamer si super powerful, so if you want to do more with it, you can read the docs.

Read more
Rick Spencer

I've created a new quickly-widget to make it dead simple to add a video or sound file to a Quickly application. All you need to do is create a MediaPlayerBox, add it to your app, set the uri property to tell it what file to play, and call "play()". 5 lines to playing a video:

        self.player = MediaPlayerBox(True)
self.player.show()
self.ui.vbox1.pack_start(self.player)
self.player.uri = file_to_play
self.player.play()
By passing in True when I created the MediaPlayerBox, that told it to display controls. You can also control whether controls are displayed by setting the controls_visible property:
        self.player.controls_visible = True

It's got other functions that you would expect:
        self.player.pause()
self.player.stop()

And other useful properties too. For example, you can get the duration of the current media file, and you can get and set the current position. So you could seek to halfway through the media file like this:
        dur = self.player.duration
self.player.position = dur/2

MediaPlayerBox is really just a thin wrapper around the gstreamer's playbin, so you still have all the power of gstreamer if you end up needing to go there. You can just use the playbin property and go to town if it comes to it.

But MediaPlayerBox is focused on simplicity, just getting a video or song playing n your app esasily.

MediaPlayerBox is currently in a branch, so you can grab it and try it out.

Read more
Rick Spencer

This is Photobomb


The last couple of days I made some good solid progress on Photobomb:

  1. Refactored items on the goocanvas into PhotobombItems to make changes easier, and code easier to maintain.
  2. Fixed the Gwibber tab to pull images from the Gwibber sqlite database.
  3. Removed Python threads from throughout the application while keeping the UI from freezing during long running actions (thanks gobject.idle_add!).
  4. Added a "download-error" event to UrlFetchProgressBox and handled download errors better.
There are still a few things I want to get to, but I am making steady progress.

Anyway, I was talking to some folks about Photobomb, and they kept referring to it as an "Image Editor". To me, and image editor is used to open an image, modify the image, and save the image. Photobomb is decidedly not that! Photobomb integrates with your social desktop, allowing you to mashup images from your devices, the web, your feeds, and your web cam. Photobomb also lets you share those mashups into your feeds. So, it's a social app.

Here's a 5 minute video I made to try to help explain Photobomb ...

Read more
Rick Spencer


I'm on holiday for the next week, yeah! I've started filling some of my free time by resurrecting Photobomb (again). There have been some technical improvements to the APIs I've been using, and also, I've learned some ways to do a few things better. I'm hoping that by spending a few hours a day, I can pretty much complete Photobomb by the end of this week, and then work on getting it into Universe for Natty.

Things I accomplished so far:

  1. I fixed the WebcamBox quidget so that it doesn't hang if you try to tell it play when it hasn't been realized. The effect of this is that I can put the webcam tab on the end of the tabs, much nicer.
  2. Then I went on to complete how the UI is organized. I want Photobomb to work really well on netbooks running Unity in Natty. Since I started Photobomb on Lucid, apps have gotten slightly less horizontal space but more vertical space. I moved the toolbar from the right, and added it as a tab on the left. Even when making the tabs wider, this reclaimed lots of vertical space.
  3. I added delete and duplicate! So now you can delete or duplicate the selected item. I was going to add cut, copy, and paste, but I think delete and duplicate works better for this app.
  4. When I started Photobomb, I simply created GooCanvas items like goocanvas.Imate, goocanvas.Path, and goocanvas.Text. Each of these types of items interact with different parts of the toolbar slightly differently, so there were lots of places in the code where I had to use code like "if type(self.selected_item) == goocanvas.Path:". Whenever I find myself type checking, I know that subclassing is in my future. Also, goocanvas.Items only let you write to their opacity properties, so for the increase and decrease opacity functions, I've been tracking opacity externally, in a dictionary, rather than as a property on the items. Refactoring was clearly in order before I continued to add features, so I created photobomb_item.py, and added PhotobombImage, PhotobombPath, and PhotobombText, then implemented common properties on each. It was only after doing this that it was reasonable code factoring to create the duplicate function. I'm not quite done this part, though.
Here's a quick video showing the new layout and the duplicate function in action:


There's still a lot on my Todo list before I'll consider Photobomb ready for general availability.
  1. Complete the refactoring into PhotobombItems. This will drag me into my first experience with multiple inheritance in Python. I think that it will be simple for this particular application. Every PhotobombItem will derive from PhotobombItem to pick up common properties and functions, but also from the appropriate goocanvas.Item (Image, Path, or Text for now). I'll probably do this one next, as I will use that to fix the opacity controls.
  2. I want to move common editing commands into their own toolbar which is always available. The new found vertical space from Unity provides this luxury.
  3. I use Python threading code in directory tab, and also the web tab. Python Thread code is notoriously difficult, and indeed, there are a number of hangs or situations where Photobomb doesn't quite quit all the way due to threads running and colliding. I will replace threads with a combination of UrlFetchProgressbox, and gobject.timeout_add. In fact, I am considering creating quidgets to handle common asynchronous activities that I have mistakenly used Threads for in the past. Depending on how it goes, I may create DirectoryScannerProgressBox, DictionaryScannerProgressBox, XMLScannerProgressBox, and JSONScannerProgressBox. I would intend for these to work in very similar ways, but make it super simple to perform long running tasks without blocking the UI or resorting the Threads.
  4. I will change the Gwibber page to use libgwibber, and also the poster button to use libgwibber.
  5. I'll make the webcam tab present the webcam image on a button instead of using a separate button. This will be more consistent with the other tabs.
  6. I want to add an undo/redo stack (which should be lots easier after I'm done with the PhotobombItem refactoring).
  7. For the toolbar tab, I didn't really do any code refactoring, I just grabbed the table of buttons, removed them from the main window, and packed them into the tab. I should really do a proper job of making the toolbox into a proper widget that rips signals. In this way, the UI will become much easier to modify in the future, and generally the photobomb code will become reusable.
  8. Since Photobomb has no preferences, I may as well remove the preferences code. All it does it start up desktopcouch and then not use it.
  9. Finally, the global menu means I can add in a menu bar without sacrificing any vertical space. This will have a few benefits. It will mean users can access the toolbar functions without changing to the toolbar tab, it will be way easier for me to add key commands for the functions, and I can add certain functions only to the menu. For example, the export and microblog commands may be better hanging out on the file menu, rather than be part of the toolbar.
If you want to play with latest Photobomb, get it from trunk.

Read more
Rick Spencer

Anything New in Quickly Widgets for Maverick?

Going into Maverick, I had envisioned myself spending a lot of free time on Quickly Widgets throughout the cycle. Sadly, though, due to various factors, I did not make the kind of progress I was hoping to. Then a couple of days ago didrocks pinged me to ask if I was going to update Quickly Widgets for Maverick, because if I was, now is the time. So, today I went through all my changes over the last few months, to make sure tests were passing, and that they were generally working well.

Well, it was a good exercise, because I came to find that I had actually made some decent progress! Here's a quick overview of what is new and improved in Quickly Widgets for Maverick (or will be when it actually gets into Maverick next week). Note that most of the changes were put into place for specific users, so I know that each of these improvements will be useful to at least one person :)

Enhanced DictionaryGrid
In Lucid, when you created a dictionary grid, the keys were used for the titles of the columns. I think this is good, because it's easy and fun. You can get going fast, and a fair amount of the time, this functionality will be more than sufficient. However, sometimes that column title needs to be changed or maybe different than the keys. This was doable, of course, because a DictionaryGrid is just a TreeView, and you can set the titles on a TreeView. You could loop through the columns, and set the titles as desired. Too much code, so I added a helper function. You can create a dictionary of keys to titles, and set them. So an app like PyTask would do this if it wanted different keys and titles:

        titles = {"name":_("Name"),"priority":_("Priority"),
"due":_("Due"),"project":_("Project"),
"complete?":_("Completed")}
self.grid.set_column_titles(titles)

You don't have to do all of the titles, you can just pass in the keys that you want you to change titles for. I also made it a bit easier to access a column. You can just index into the columns by the key. So in the tests, for example, you can see where I used it like this:
        grid.columns["key1_1"].set_title("KEY")

Nice one liner. This is just accessing the gtk.TreeViewColumn, so you can whatever you need to with the column, this just makes it easy to get to the columns you need.

As I previously blogged, there is now a datecolumn with built in editing (thanks to the pygtk faq):


And there is a grid filter to go with it:

So now, any column with a key that ends in "date" will default to be a date column.

DictionaryGrid also has an enriched set of arguments for the cell-edited signal. In addition to the row index and key that were previously reported, it now tells you the new value and also provides the dictionary for the row as well. Makes it easier to decide what to do when a cell is edited without having to probe into the grid.

Enhancemed CouchGrid
The most salient enhancement to CouchGrid, is that it's easy to tell the grid to delete data form desktopcouch now. You do this by passing delete=True to the remove_selected_rows function. So apps like PyTask don't have to do all these crazy contortions to remove data from the underlying store, PyTask just does this:
    def remove_row(self, widget, data=None):
"""Removes the currently selected row from the couchgrid."""

self.grid.remove_selected_rows(delete=True)

I like this because it lets the programmer keep the grid as their mental model of how to interact with the underlying store, and keeping desktopcouch an incidental piece of plumbing.

Enhanced GridFilters
Personally, the ability to apply a filter UI to a grid with just a few lines of code is one of the things about quidgets of which I am the most proud. I still don't know how I managed to pull it off, but I did. I like it because it turns a big chore into something easy and fun. If nothing else, it makes a sweet demo.

So I did make some good progress with this in Maverick. The key thing was that I refactored what a FilterRow contains. The FilterRows were hard coded to use a combo box and TextEntry. This became limiting with data types other than strings. So, I did some open heart surgery on the Filter Code, and after a successful refactoring, can now drop in any set of widgets I would like for a filter row. In addition to a date filter, there's an IntegerFilter now:


New Widgets
Thanks to sil, it's now really trivial to download content from the Internet. Sil wrote the code, and I did a vblog about this a while back. In case you missed that:
This widget works by reusing the common signal handling patterns in PyGtk. From the test app:
    def start_download(self, btn):
prog = UrlFetchProgressBox("http://www.ubuntu.com/desktop/get-ubuntu/download")
prog.connect("downloaded", self.downloaded)
self.vbox.pack_start(prog, expand=False)
prog.show()

def downloaded(self, widget, content):
print "downloaded %s bytes of content" % len(content)

There is also a TextEditor now. This removes all the bookkeeping code that you have to write to keep a TextView, TextBuffer, Markers, etc... all straight.

For example, to tell the TextEditor to highlight a word, just add it to the text editor's list of words to highlight:
        self.editor.add_highlight("some")


Really! That's all you need to highlight the word "some" in a text editor.
It's also got undo and redo built in! So instead of mucking with this yourself, you can hook up an undo command with a one-liner:
        undo_button = gtk.Button("Undo")
undo_button.connect("clicked",self.editor.undo)

If you haven't tried making an undo, redo stack yourself, trust me, this is much easier.

I recently blogged about the new WebCamBox as well. I won't rehash that here.

House Keeping
Finally, I went through this morning to ensure that all of the quidgets were using gettext properly (so bring on the translations if you got 'em), and I've also been adding and improving tests as I've been along.

Anyway
I guess I should write a nice comprehensive tutorial or similar. But in the meantime, there are lots of videos and such on my blog. So, if you want to write an app for Ubuntu, consider using Quickly and Quickly Widgets, because it's getting more fun and more easy with each release!


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

Read more
Rick Spencer

I'm finally settling back into the groove of some of my side projects. I guess I'm handling the new position a bit better as time goes on, and feel that I can spend some free time working on somethings that I want to do, not just things that I feel that I should do.

So, these side projects I do for fun, and they are the most fun when they combine together in sweet ways. During the dead of winter, I spent a bunch of weeks working on Photobomb . On of the features that I added was that you could add an image directly from your web cam. To do this, I used the PyGame Web Cam API, essentially because I saw the API, and knew that I would be able to use it relatively easily, which in fact turned out to be the case.

It also turned out that not everyone had PyGame already installed on their systems. As a result installing Photobomb meant a 25+ Megabyte download, most of which was PyGame. So I was advised to use GStreamer instead. I got started on this conversion back in April, by creating a simple web cam display application using gstreamer. I ran into a series of roadblocks, one such roadblock was removed by Chris Halse Rogers of desktop team fame, who knew why it kept crashing (basically, I was trying to access the xid of a widget that wasn't yet realized).

But I soon had a pipeline together that could display the web cam, but I could not figure out how to modify it so that it could save out a picture whilst still displaying the web cam output. I finally hopped into #gstreamer to see if someone could give me a pointer. Well, it turns out that someone already wrote a pipeline called caemerabin that does everything I need for the web cam, and more.

Well, it turned out that the documentation was out of sync with the current API. This isn't too surprising, as camerabin is still in gstreamer0.01-plugins-bad, and the API is actually improved by the changes. But I was struggling to understand camerabin, so I went back to #gstreamer. Often in IRC, someone will volunteer to spend some time helping you out with a problem. thiagoss (who I think might be this guy) really helped me out. I'm not sure, but I think he may actually be a primary author of camerabin. Anyway, he set me straight on a couple of things, namely:

1. use $gst-inspect camerabin to see what properties and methods the GStreamer elements really support (if they are out of sync with the docs).
2. use GST_DEBUG=2 to run your gstreamer apps, as this puts more warnings in your output.

Well, between these 2 tips, I was quickly able to realize that my WebCamBox widget would not much more than some Gtk/Gstreamer app boiler plate, with a wrapper around camerabin.

So, for example the "take picture" function just creates a time stamp, then tells the camerabin instance to emit a "capture-start" signal.
      stamp = str(datetime.datetime.now())
extension = ".png"
directory = os.environ["HOME"] + _("/Pictures/")
self.filename = directory + self.filename_prefix + stamp + extension
self.camerabin.set_property("filename", self.filename)
self.camerabin.emit("capture-start")
return self.filename
Then in on_message, I capture the message that it is done, and fire a signal:
       t = message.type
if t == gst.MESSAGE_ELEMENT:
if message.structure.get_name() == "image-captured":
#work around to keep the camera working after lots
#of pictures are taken
self.camerabin.set_state(gst.STATE_NULL)
self.camerabin.set_state(gst.STATE_PLAYING)

self.emit("image-captured", self.filename)
Play, Pause, and Start were trivially easy to implement:
        self.camerabin.set_state(gst.STATE_PLAYING)
self.camerabin.set_state(gst.STATE_PAUSED)
self.camerabin.set_state(gst.STATE_NULL)
Like I say, there is also some boiler plate to instantiate the camera and associate it with a gtk.DrawingArea. It took me a lot of iterations to get it working, as you can see from all of these pictures of me working on it ...

The net result is that it's now pretty easy to create an app with a web cam in it. Here's all the code for the WebCamBox test app.
if __name__ == "__main__":
"""creates a test WebCamBox"""
import quickly.prompts

#create and show a test window
win = gtk.Window(gtk.WINDOW_TOPLEVEL)
win.set_title("WebCam Test Window")
win.connect("destroy",gtk.main_quit)
win.show()

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

mb = WebCamBox()
mb.video_frame_rate = 30
vbox.add(mb)
mb.show()
mb.play()

mb.connect("image-captured", __image_captured)
play_butt = gtk.Button("Play")
pause_butt = gtk.Button("Pause")
stop_butt = gtk.Button("Stop")
pic_butt = gtk.Button("Picture")

play_butt.connect("clicked", lambda x:mb.play())
play_butt.show()
mb.pack_end(play_butt, False)

pause_butt.connect("clicked", lambda x:mb.pause())
pause_butt.show()
mb.pack_end(pause_butt, False)

stop_butt.connect("clicked", lambda x:mb.stop())
stop_butt.show()
mb.pack_end(stop_butt, False)

pic_butt.connect("clicked", lambda x:mb.take_picture())
pic_butt.show()
mb.pack_end(pic_butt, False)

gtk.main()
Almost all of it is standard code for creating widgets. I love that doing functions like play, pause, stop, and take a picture can be handled in lambdas. So much easier!

So my last step was to drop it into Photobomb. All I had to do was modify the CameraPage class that I had already set up for the PyGame based version.
import gtk
from quickly.widgets.web_cam_box import WebCamBox
from ImageListPage import ImageListPage

class CameraPage(ImageListPage):
def __init__(self):
gtk.VBox.__init__(self,False, 0)
self.__camera = WebCamBox()
self.__camera.connect("image-captured",self.image_captured)
self.__camera.show()
self.__camera.set_size_request(128, 128)
self.pack_start(self.__camera, False, False)

button = gtk.Button("Take Picture")
button.show()
button.connect("clicked", lambda x:self.__camera.take_picture())
self.pack_start(button, False, False)


def image_captured(self, widget, path):
self.emit("clicked",path)

def on_selected(self):
self.__camera.play()

def on_deselected(self):
self.__camera.stop()

Well, almost all I had to do. I discovered a bug where if the WebCamBox is not actually visible, it locks up Photobomb if you try to show it. For now, I've worked around this by putting the CameraPage as the first and open page, so it just works. However, I suspect the bug is due to the xid not being available when camerabin tries to display on it. I think with a little thought, I can block this condition, and perhaps not let the camera play if it's not ready yet.

Anyway, I ended up with a few lines of wrapper code, around my wrapper code, and it all works thanks to the efforts of the folks working on camerabin!

camerabin has a whole lot of functionality that I haven't wrapped yet. It takes video, including audio! Also, it looks like you can change encoders, and drop in filters and such into the pipeline. To handle this for now, WebCamBox exposes a "camerabin" public property, so if you are using the widget, you won't run into a wall.

Read more
Rick Spencer

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.

Read more
Rick Spencer


On Saturday I received an email from a developer name Ryan. He was using Quickly and Quickly Widgets to create a task list application. CouchGrid seemed to suit, but due to the fact that there are no documents for it, he naturally had some questions about how to proceed. His project is called PyTask, and so far, I like it. It's a very simple set up, and just works in terms of data persistence and syncing across desktops.

Looking at the App, it was clear that there were a few more features that DictionaryGrid needs to really rock, though:
  1. It needs a DateColumn to handle the "due" column. Users would want to set this with a gtk.Calendar widget.
  2. It needs a ComboColumn to handle the "priority" column. Users would want to pick from a list of valid predefined priority values rather than free input text. This will be interesting to create a nice API for. I suppose application developers will want to pass in just a list of strings to the column. I think this is doable, but may take some refactoring as currently there is no intuitive way to get a hold of a column and set a property on it.
  3. Both of these will require new GridFilter functionality. In fact, I have been waiting for a reason to refactor this part of Quickly Widgets, as the GridFilterRows are hard coded to use specific widgets, and this should be flexible.

Anyway, as it is, I am using PyTask, I hope Ryan get's it into a PPA soon. I like the simplicity. Ryan and I are currently collaborating on creating the new functionality in DictionaryGrid that PyTask needs. Open Source FTW!

Read more
Rick Spencer


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!

Read more
Rick Spencer




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.

Read more
Rick Spencer

You probably saw that didrocks released another update for Quickly. It's chock full of bug fixes and tweaks based on the feedback from the last release.

About a week ago I made some updated intro videos to show Quickly in action, and highlight some key changes between Quickly in Karmic and Quickly in Lucid. They are all available in HD, so you switch to that when they start playing, it makes them waaay easier to read.

Part 1: Create a project and use Glade to edit the UI
Here you see that you use "quickly create ubuntu-application" instead of "quickly create ubuntu-project" to create an app. You also use "quickly design" instead of "quickly glade" to design the UI.


Part 2: Using CouchGrid
One of the key differences here is that CouchGrid is now in the quickly.widgets module instead of the desktopcouch module. The CouchGrid moved into quickly.widgets because it now extends the DictionaryGrid class. This brings a lot benefits:
  1. Automatic column type inference
  2. Ability to set column types so you get the correct renderers
  3. Correct sorting (for instance 11 > 9 in IntegerColumn, but "11" < "9" in string columns
And of course you get all the goodness of automatic desktopcouch persistence.


Part 3: Using GridFilter
GridFilter is a new class that provides automatic filtering UI for a DictionaryGrid or descendant such as CouchGrid.

Read more
Rick Spencer

I removed the ViewDesignerDialog, and changed it to a ViewDesignerBox. Basically, a widget that is now embedded into Slip Cover. I also wrote some code to extract the views that currently exist in a database, and offer those under "Views" so you can click on them and see the views map function, and the views reduce function if it has one.

Besides editing views and trying out your edits, this lets you look at the databases you have and see how they work. In the example above, you can see how the views for your contacts are set up. Handy if you want a similar feature in one of your databases.



This screenshot shows two things. First, it shows the view I constructed to get the record type for each record in a database, and also whether the record has been deleted. It's a bit "meta", but the purpose of this is visible in the Record Types list. I use the view to list out the record types, and how many records are deleted and how many active.

Still a few missing features:
  1. The "Create View" button doesn't work yet (though shouldn't be too very hard to make)
  2. "Save View" also doesn't work yet.
  3. I want to add options for executing your view, such as Group and such.
  4. I want to add a bit more data about each database, and whether you can sync it or not. I'm thinking about doing this on the server info tab, maybe an editable DictionaryGrid?
Also, in terms of quickly-widgets, I hate writing the same notebook code for tabbed interfaces over and over again. We should have a quidget that makes it easy and fun, and handles close buttons on the tabs, has a free document menu, and has alt # navigation to documents.

Read more
Rick Spencer

Didrocks released quickly 0.4 yesterday. What a great contribution from didrocks! I suspect he his work will help tons of people have a really fun time writing Ubuntu apps. Read about the release in his detailed blog post.




In the meantime, I made a cheesy video last night, showing some of the changes in quickly, and how the new CouchGrid and GridFilter work.

[Note that it takes blip.tv a bit of time to render out a high def video like this, so if the video is not yet working, you can check back later.]

[D'oh .... stupid blip.tv bailed on encoding my video. I'll try again with smaller files. Stay tuned]

Read more
Rick Spencer

So as planned, today I added the feature for creating databases. This was one of those sweet features where it ended up on the tip of previous coding, so it took literally a matter of minutes to implement. Just get a name from the users, create the database, then load it in the UI,

    def new_database(self, widget, data=None):
title = _("New database")
msg = _("Specify a name for the new database")
response, val = prompts.string(title,msg)
if response == gtk.RESPONSE_OK:
CouchDatabase(val, create=True)
self.load_db(self,val)

So now I have an end to end tool for designing my desktopcouch databases.

Use File->New and make a name for my database:
Get an empty screen for the database.
Use the "+" to create a new record type.
Use Add keys to add keys for record type.
Then add rows and fill in the data.

Read more
Rick Spencer

Because I added "Add Key" yesterday, today I wanted to be symmetrical, so I added "delete keys". This turned out to be much more work. First, I ended up creating another quickly.prompt, and then I had to dive into the internals of CouchGrid, and also do some hair raising mucking with desktop couch.

quickly.prompts.checklist()
When the developer clicks the "delete keys" button, I wanted to present them with a list of keys they could choose from. And then have all of those get deleted. This seemed like the kind of thing that I'd want to use in other programs, so I decided to solve this problem generically by adding it to quickly.prompts.

I accomplished this by deriving from quickly.prompts.Prompt, and also creating a helper function. CheckListPrompt works as you would expect for prompt. You set it up by passing in some configuration info, including a dictionary of strings as keys, which will be labels for the checkboxes, and a bool value to determine if the box is checked by default.

You get back a response and val. The val is a dictionary of keys again, with bools for whether the checkboxes are active or not.

So to use the CheckListBox, I just pass in a dictionary of the keys for the CouchGrid, and then see if any were selecct:

       val = {}
for k in self.grid.keys:
val[k] = False
response, val = checklist(title, message, val)
if response == gtk.RESPONSE_OK:
#do stuff

Hair Raising Munging
Since "do stuff" is pretty destructive, I use a quickly.prompts.yes_no to confirm that the users wants to blow away all the data and screw up their database. Assuming they do want to delete the keys and values in the desktopcouch database, it turns out to be *not* easy to do the deletion without reading way into CouchGrid. The issue here is the couchdb reserves anything staring with a "_" for itself. But DictionaryGrid uses "__" as a convention to determine that a key should be hidden in the grid by default. So as a result of this CouchGrid munges _id and _rev and record_type before it reads to and from the database.

The second troublesome part was dealing with desktopcouch. It turns out that you can't just delete a key from a record. You have a delete the whole record and then create a new record without that key. so as a result the code deletes and recreates each and every row.

I really think this code belongs inside CouchGrid:
    def delete_keys_from_store(self, model, path, iter, keys_to_delete):
for k in keys_to_delete:
d = model.get_value(iter,len(self.grid.keys))
if k in d:
del(d[k])
if '__desktopcouch_id' in d:
keys = d.keys()
for k in keys:
if k.startswith("__desktopcouch"):
dc_key = k.split("__desktopcouch")[1]
d[dc_key] = d[k]
del(d[k])
if k == "__record_type":
d["record_type"] = d["__record_type"]
del(d["__record_type"])
self.database.delete_record(d['_id'])
del(d["_rev"])
del(d["_id"])
self.database.put_record(Record(d))

Who would ever be able to figure out to do all this?

Refresh
So after this the refresh function was trivial. Just tell the CouchGrid to reset, and then recreate the grid:
    def refresh(self, widget, data=None):
self.grid._refresh_treeview()
self.remove(self.filt)
self.filt = GridFilter(self.grid)
self.pack_start(self.filt, False, False)
self.reorder_child(self.filt,1)
self.filt.show()

desktopcouch Editor
So now with adding a removing records and keys, along with freshing, I have a functional desktopcouch editor. This tool has already proved a bit useful in getting a peak into certain database. However, I can't actually create new record types yet. Maybe tomorrow?

Read more
Rick Spencer

I started out wanting to add "refresh". However, I just wiped my netbook and set up Lucid UNE Beta 2 on it, so only had one database in my local desktopcouch and it didn't have any records. I didn't know how I'd test the feature. I haven't set up my Ubuntu One yet on this computer, because I've been waiting for gwibber to get fixed (it is!) so that I could test out the Social From the Start Experience.

This is a long winded way of saying that I decided to implement another feature first to make it easier for me to test refresh out, editing an existing database. The first step of this was to be able to add columns. So I added an "add column" button.

I used quickly.prompts to get the string from the user. To actually add the column required a bit of intimate knowledge of the internals of DictionaryGrid. It occurred to me that folks might want to be able to add a key/column to a Grid themselves, so I logged a bug to remind me to make this easier.

Since Fagan asked to see the code when I posts :) -

    def add_column(self, widget, data=None):
response, val = prompts.string("New Column","Please select a name for the new column")
if response == gtk.RESPONSE_OK:
self.grid.keys.append(val)
self.grid._refresh_treeview()

Read more