PyLan a GTD todo application written in python and PyGTK – part one
Introduction
All right, many of you have probably heard of, or read, David Allen’s book Getting Things Done if not it’s a pretty interesting book about how to organize the things in your life.
As I was reading it I thought that a simple todo list could be created using Python and PyGTK pretty easily, so I decided to create the application and write a tutorial surrounding it. Since the actual application is quite large I have decided to break it up into a series of tutorials, each highlighting a few specific tasks. The full goal of this series is to show everything that is required when creating a python/PyGTK based application, for the GUI setup all the way to distribution.
These tutorials assume that you have a basic knowledge of python and PyGTK. If you have not already done so it is recommended that you take a look at some of my previous PyGTK based tutorials as I will be glossing over much of that which is explained there:
- Creating a GUI using PyGTK and Glade
- Building an Application with PyGTK and Glade
- Extending our PyGTK Application
- Translating your Python/PyGTK application
- Writing a Custom Widget Using PyGTK
- WordPy offline blogging tool
- WordPy 0.2 – Using XML to Save and Load Data
PyLan – part one

So the application that we will be creating will be called PyLan, pronounced: plan. In brief it will be used to create a tree of tasks that can be organized into categories and subcategories.
You can download the full source to this tutorial here.
Part one of this tutorial will introduce you to the idea of the application and create the basic shell for it. It will outline the following:
- Creating the basic GUI in Glade
- Working with a gtk.TreeView
- Working with a gtk.TreeStore
- Saving and loading the data from the tree using cPickle.
- Using a gtk.MenuToolButton
The tutorial is organized into the following sections:
- The GUI
- The Code: Setting it all up
- The Code: Initializing the tree
- The Code: todo items
- The Code: Adding Categories
- The Code: Removing Categories
- The Code: Saving and Loading with cPickle
- The Code: Connecting our menu with the gtk.MenuToolButton
- Conclusion
The GUI
The GUI will be created using Glade, it will be a relatively simple especially in this first part. Since the glade project file is available I won’t go into too much detail about how to create this GUI, however I will outline the major steps that one could follow in case they would like to create their own GUI, or model a GUI after this one.
The main window:
- Create a new Glade Project and save it as pylan a folder named PyLan that you have created.
- Create a window, and call it “mainWindow”, set its title to be PyLan. If you are using Glade3 make sure that you set your window to be visible. Add the GObject destroy signal handler like usual. On the common tab set the width to be 400 and the height to be 300.
- Add a vertical box to the window and give it 4 rows.
- In the first row add a menu, call it “mainMenu”. Make the Quit menu item’s menu handler the same as the Main Windows destroy signal handler. For the rest of the File menu items, give them signal handlers using the following format: “on_file_new”, “on_file_open”, and so on.
- In the second row add a tool bar with two items.
- In the first spot add a GtkMenuToolButton, and set its name to “btnAdd”. Make it a stock gtk-add button, and add a clicked signal handler with the name “on_add_category”.
- In the second spot at a GtkToolButton with the name “btnRemoveCategory”, being a stock gtk-remove button. Set its clicked signal hander to be “on_remove_item”.
- In the fourth row add a Status Bar.
- In the Third row add a Tree View, set its name to be “todoTree”
The category dialog
- Now create a Dialog with an Ok and a Cancel button. Sets it’s name to be “categoryDialog”, and its title to be
- “Category”. Give it a default width and a width of 300. Set its named icon to be: “stock_post-message”.
- Add a GTKHBox to the dialog with two columns.
- In the fist column add a GtkLabel with the label “Name:”. Give it an X Pad of 3 pixels.
- In the second column add a GtkEntry with the name: “enName”
The Menu for the Add button
Since we used a GtkMenuToolButton, we need to create the menu that we are going to associate with that button. To do that we will use a GtkMenu.
- Create a GtkMenu and call it: “addMenu”
- Add one Menu item to the menu with the label “Add _Category”. Set the menu items handler “on_add_category”
After all of that you should be left with something like this:

The Code: Setting it all up
Much of this initial code is taken from previous pyGTK tutorials, so if you are confused about any of this starting code, I suggest you take a look at the previous PyGTK tutorials where it is explained in depth. Create this in a file called pyLan.py in the same directory where you saved your glade project.
#!/usr/bin/env python # PyLan - Python + PyGTK todo list # Copyright (C) 2007 Mark Mruss <selsine @gmail.com> # http://www.learningpython.com # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Library General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # If you find any bugs or have any suggestions email: selsine@gmail.com # URL: http://www.learningpython.com __author__ = "Mark Mruss </selsine><selsine @gmail.com>" __version__ = "0.1" __date__ = "Date: 2007/02/17" __copyright__ = "Copyright (c) 2007 Mark Mruss" __license__ = "GPL" _ = lambda x : x try: import pygtk pygtk.require("2.0") except: pass try: import sys import gtk import gtk.glade import gobject import os import locale import gettext import cPickle import helper import todo except ImportError, e: print "Import error pyLan cannot start:", e sys.exit(1) FILE_EXT = "pylan" APP_NAME = "pyLan" class pyLan(object): """A simple python based GTD todo list""" def __init__(self): #Get the local path self.local_path = os.path.realpath(os.path.dirname(sys.argv[0])) #Translation stuff self.initialize_translation() #Set the Glade file self.gladefile = os.path.join(self.local_path, "pylan.glade") #Get the Main Widget Tree self.wTree = gtk.glade.XML(self.gladefile, "mainWindow") #Connect with yourself self.wTree.signal_autoconnect(self) #init todo file self.todo_file = None #Get the Main Window self.main_window = self.wTree.get_widget("mainWindow") self.set_window_title_from_file(self.todo_file) """" ************************************************************ * Initialize ************************************************************ """ def initialize_translation(self): """This function initializes the possible translations""" # Init the list of languages to support langs = [] #Check the default locale lc, encoding = locale.getdefaultlocale() if (lc): #If we have a default, it's the first in the list langs = [lc] # Now lets get all of the supported languages on the system language = os.environ.get('LANGUAGE', None) if (language): """language comes back something like en_CA:en_US:en_GB:en on linuxy systems, on Win32 it's nothing, so we need to split it up into a list""" langs += language.split(":") """Now add on to the back of the list the translations that we know that we have, our defaults""" langs += ["en_CA", "en_US"] """Now langs is a list of all of the languages that we are going to try to use. First we check the default, then what the system told us, and finally the 'known' list""" gettext.bindtextdomain(APP_NAME, self.local_path) gettext.textdomain(APP_NAME) # Get the language to use self.lang = gettext.translation(APP_NAME, self.local_path , languages=langs, fallback = True) """Install the language, map _() (which we marked our strings to translate with) to self.lang.gettext() which will translate them.""" gettext.install(APP_NAME, self.local_path) """" ******************************************************** * Simple Helpers ******************************************************** """ def set_window_title_from_file(self, todo_file): """Set the windows title, take it from todo_file. @param todo_file - string - The todo file name that we will base the window title off of """ if (todo_file): self.main_window.set_title("PyLan - %s" % (os.path.basename(todo_file))) else: self.main_window.set_title(_("PyLan - Untitled")) """ ************************************************************ * Signal Handlers ************************************************************ """ def on_mainWindow_destroy(self, widget): """Called when the application is going to quit""" gtk.main_quit() def on_add_category(self, widget): """Called when the Add Category button is clicked. Can also be generally used to add a category""" pass def on_remove_item(self, widget): """called then the remove category button is clicked. Can also be generally used to remove a category or item.""" pass def on_file_new(self, widget): """File | New - Start a new project file, blank out the current project and start from scratch""" pass def on_file_open(self, widget): """Function called to open a todo file""" pass def on_file_save(self, widget): """File | Save function - Save the Todo file""" pass def on_file_save_as(self, widget): """File | Save As function - Save the todo file""" pass if __name__ == "__main__": plan = pyLan() gtk.main()
If you run the code you should be greeted with the following:

If you are familiar with my other tutorials none of this code should be very surprising to you. The only exception is perhaps the way that we connect our functions with the signal handlers:
self.wTree.signal_autoconnect(self)
So instead of creating a dict with our functions and then passing it to the signal_autoconnect() function we simply pass our class to the function, and then any of our functions that match the signal handlers in the widget tree will be connected. A very slick way to handle this if you ask me.
The Code: Initializing the tree
The next thing that we have to do is initialize our tree, i.e. set up the columns, create the model for the gtk.TreeView, and so on. I struggled with how to set up the columns for a while since I knew that I wanted them to be dynamic and for the application to function properly no matter what order the columns were in.
As a result I decided to use a simple class and a list of those objects to define what columns are available and what is in each column. I’m still not sure if this is the best way to handle it, but it meets most of my requirements and was relatively simple to implement.
The first thing that we need to do is create some “constants” near the top of our code right underneath out APP_NAME define:
"""Column IDs""" COL_OBJECT = 0 COL_OBJECT_TYPE = 1 COL_TITLE = 2 COL_DETAILS = 3 COL_DUE = 4 COL_PRIORITY = 5 COL_COMPLETED = 6
These are the IDs of the columns that we will have in our list. For this tutorial most of these will remain unused, but as we progress more and more will be used. The IDs are used to let us know what each column is. So COL_OBJECT will be the column that holds the actual python object (as we did in the PyWine tutorial). COL_OBJECT_TYPE will hold the objects type, and so on.
Then we need a simple class that will contain this information:
class todoColumn(object): """This is a class that represents a column in the todo tree. It is simply a helper class that makes it easier to inialize the tree.""" def __init__(self, ID, type, name, pos, visible=False, cellrenderer = None): """ @param ID - int - The Columns ID @param type - int - A gobject.TYPE_ for the gtk.TreeStore @param name - string - The name of the column @param pos - int - The index of the column. @param visible - boolean - Is the column visible or not? @param cellrenderer - gtk.CellRenderer - a constructor function for the column """ self.ID = ID self.type = type self.name = name self.pos = pos self.visible = visible self.cellrenderer = cellrenderer self.colour = 0 def __str__(self): return "<todocolumn object: ID = %s type = %s name = %s pos = %d visible = %s cellrenderer = %s>" % (self.ID, self.type, self.name, self.pos, self.visible, self.cellrenderer)
This class should be created before the PyLan class. Then at the beginning of the PyLan class we will create a hidden member at the beginnig of the __init__() function:
"""The colum list, at one point this could be saved and loaded from a file.""" self.__tree_columns = [ todoColumn(COL_OBJECT, gobject.TYPE_PYOBJECT, "object", 0) , todoColumn(COL_OBJECT_TYPE, gobject.TYPE_INT, "object_type", 1) , todoColumn(COL_TITLE, gobject.TYPE_STRING, _("Title"), 2, True, gtk.CellRendererText()) , todoColumn(COL_DETAILS, gobject.TYPE_STRING, _("Details"), 3, True, gtk.CellRendererText()) , todoColumn(COL_DUE, gobject.TYPE_STRING, _("Due"), 4, True, gtk.CellRendererText()) , todoColumn(COL_PRIORITY, gobject.TYPE_STRING, _("Priority"), 5, True, gtk.CellRendererText()) , todoColumn(COL_COMPLETED, gobject.TYPE_STRING, _("Completed"), 6, True, gtk.CellRendererText()) ]
So what we are doing is creating a list that defines the columns that will be added to our Tree. As you can see when we create each todoColumn instance we specify everything that is needed to create that column in the tree.
Next we’ll add a function that we will use to initialize our tree. This function will be called from our __init__() function:
#Connect with yourself self.wTree.signal_autoconnect(self) #Initialize the todo Tree self.initialize_todo_tree()
def initialize_todo_tree(self): """Called when we want to initialize the tree. """ tree_type_list = [] #For creating the TreeStore self.__column_dict = {} #For easy access later on #Get the treeView from the widget Tree self.todoTreeView = self.wTree.get_widget("todoTree") #Make it so that the colours of each row can alternate self.todoTreeView.set_rules_hint(True) # Loop through the columns and initialize the Tree for item_column in self.__tree_columns: #Add the column to the column dict self.__column_dict[item_column.ID] = item_column #Save the type for gtk.TreeStore creation tree_type_list.append(item_column.type) #is it visible? if (item_column.visible): #Create the Column column = gtk.TreeViewColumn(item_column.name , item_column.cellrenderer , text=item_column.pos) column.set_resizable(True) column.set_sort_column_id(item_column.pos) self.todoTreeView.append_column(column) #Create the gtk.TreeStore Model to use with the todoTree self.todoTree = gtk.TreeStore(*tree_type_list) #Attache the model to the treeView self.todoTreeView.set_model(self.todoTree)
So what we do in this function is first get our gtk.TreeView widget from our widget tree. Then we loop through the column list that we created, and add the column’s type to the tree_type_list, which will be used to create our gtk.TreeStore. If the todoColum is visible we need to create a gtk.TreeViewColum and then add it to the gtk.TreeView.
After we have looped through the column list we create the gtk.TreeStore (our model for the gtk.TreeView) based on all the types in our column list. Then we set that model as the model for our gtk.TreeView.
This may seem a bit confusing and if you have any problems understanding take a look at the Building an Application with PyGTK and Glade tutorial where the relationship between gtk.TreeViews, gtk.TreeViewColumns, gtk.CellRenderers (which we define in our todoColumn), and gtk.TreeModels is described. Basically the model (the gtk.TreeStore in this case) is the data. The View (gtk.TreeView, gtk.TreeViewColumn, gtk.CellRenderer) is what is used to display the data.
This means that you could have multiple views for the same data. We’re not doing that in this application so far, but the idea is quite powerful. We could, for example, use it to create a “simple” and “advanced” view for our data, or other “filtered” views.
Now if we run the code we get the following:

It’s the same as it was before, except now we have columns in our gtk.TreeView.
The Code: todo items
Now that we have our tree defined, we need to create the objects that will store the data. The data will be divided into two types (for now) a category and an task.
A Category is simply a container for other categories or items. An task is an actual task that you need to do.
So a category could be “Work” and in that category you might add another category called “Project X” and in “Project X” you may add the task “Implement feature y.” For this tutorial we will not be working with tasks, we will simply be working with categories.
We will create a new file in the same directory where we created our pyLan.py file. The file will be named todo.py. We will start off be defining the todoBase class which we will use as the base class for both the Category and Task classes:
#!/usr/bin/env python import pyLan import helper CATEGORY = 0 TASK= 1 class todoBase(object): """This is the base class for the todoCategory and the todoTask. """ #Type property def __get_type(self): return self.__m_type type = property(__get_type) def __init__(self, type): """The only way to set the type is through the initialization""" self.__m_type = type def get_column_list(self, todoColumnList): """This function is used to get the display list for the todoTree. The todoCOlumnList controls the order that the list will be returned in. Used as the second param in the gtk.TreeStore.append function. @param todoColumnList - list - A list of todoColumn items. Their type member should use used to determine the order of the returned list. @returns list - A list for the todoTree. """ pass def add_to_tree(self, tree, parent, todoColumnList): """This function is used to add an item to a gtk.TreeStore, usually when loading. All children will be added as well @param tree - gtk.TreeStore - The tree store that we wil be adding to. @param parent gtk.TreeIter - The parent of this item. @param todoColumnList - list - A list of todoColumn items. Their type member should use used to determine the order of the returned list. """ tree.append(parent, self.get_column_list(todoColumnList))
So the todoBase is simply a class with a type property (either CATEGORY, or TASK) that can only be set in the constructor and two functions get_column_list() and add_to_tree().
get_column_list() is used to create the second parameter for the gtk.TreeStore.append() function “a tuple or list containing ordered column values to be set in the new row.”
add_to_tree() is used to add the todo item to the actual tree. For the category class that we are going to create this is used to ensure that any children (i.e. categories or tasks) will also be added to the tree at the same time.
The todoCategory class is defined as follows:
class todoCategory(todoBase): """This is a category in the todoTree. It can have child categories or child tasks.""" #Name property def __get_name(self): return self.__m_name def __set_name(self, name): self.__m_name = name name = property(__get_name, __set_name) def __init__(self, name): #init variables self.__m_name = "" self.name = name #children self.__m_children = [] #init base todoBase.__init__(self, CATEGORY) def __str__(self): return _("<todocategory object: name = %s num_children = %d>") % (self.name, len(self.__m_children)) def add_child(self, todo_object): """Add a child to the category. @param todo_object - Either a todoCategory or a todoTask. This will be a child of the category. """ self.__m_children.append(todo_object) def remove_child(self, todo_object): """Removes a child from the category. @param todo_object - Either a todoCategory or a todoTask. This will be a removed. """ try: self.__m_children.remove(todo_object) except ValueError, e: helper.show_error_dlg(_("Error removing child %s = %s") % (todo_object, e)) def get_column_list(self, todoColumnList): """This function is used to get the display list for the todoTree. The todoCOlumnList controls the order that the list will be returned in. @param todoColumnList - list - A list of todoColumn items. Their type member should use used to determine the order of the returned list. @returns list - A list for the todoTree. """ lst_return = [] # Loop through the columns and create the return list for item_column in todoColumnList: if (item_column.ID == pyLan.COL_OBJECT): lst_return.append(self) elif (item_column.ID == pyLan.COL_OBJECT_TYPE): lst_return.append(self.type) elif (item_column.ID == pyLan.COL_TITLE): lst_return.append(self.name) else: lst_return.append("") return lst_return def add_to_tree(self, tree, parent, todoColumnList): """This function is used to add an item to a gtk.TreeStore, usually when loading. All children will be added as well @param tree - gtk.TreeStore - The tree store that we will be adding to. @param parent gtk.TreeIter - The parent of this item. @param todoColumnList - list - A list of todoColumn items. Their type member should use used to determine the order of the returned list. """ insert_iter = tree.append(parent, self.get_column_list(todoColumnList)) if (insert_iter): for child in self.__m_children: child.add_to_tree(tree, insert_iter, todoColumnList) else: helper.show_error_dlg(_("Error loading category: %s" % self))
You can see that it sets its type by initializing the base class and passing it the CATEGORY constant. It also has some additional members, the name property and the private member __m_children. Name is the name of the category and __m_children is a list of todoBase objects that are the children of this category.
It implements two functions add_child() and remove_child() that are pretty self explanatory, and it overrides the get_column_list() and add_to_tree() functions from the todoBase.
You can see that in the get_column_list() function that the todoCategory only specifies three columns, for the rest is passes empty strings. It should be pretty obvious what the function is doing.
add_to_tree() is also overridden. What it does is add itself to the tree, if that succeeds it then loops through all of its children and then adds them to the tree as well. If that fails it calls the show_error_dlg() function from the helper module that has not been explained yet. All you need to know is that show_error_dlg() pops up a simple error dialog.
The Code: Adding Categories
Now that we have our category classes defined we need to work on adding them to the actual tree. To do that we will want to use our categoryDialog. We will show that to the user and let them set the name of the category. We will create a function called show_category_dialog() to do this for us:
def show_category_dialog(self, name): """This function shows the category dialog. @param name - string - The name to display in category dialog by default. @returns - If OK is pressed the name typed on the dialog. If Cancel is pressed None is returned. """ #init to cancel name_return = None #load the dialog from the glade file wTree = gtk.glade.XML(self.gladefile, "categoryDialog") #Get the actual dialog widget dlg = wTree.get_widget("categoryDialog") #Get all of the Entry Widgets and set their text enName = wTree.get_widget("enName") enName.set_text(name) #run the dialog and store the response result = dlg.run() if (result==gtk.RESPONSE_OK): #get the value of the entry fields name_return = enName.get_text() #we are done with the dialog, destroy it dlg.destroy() #return the result return name_return
This function simply shows the dialog and returns the name that the person typed or None if they cancelled.
We will call this function from the on_add_category() function in the PyLan class. The on_add_category() function is called when you want to add a category to the Tree. It is called from either the addButton or from a menu item on the menu that the addButton can display. Here is the function:
def on_add_category(self, widget): """Called when the Add Category button is clicked. Can also be generally used to add a category""" name = self.show_category_dialog("") if (name): # Get the selection category iter model, selection_iter, todo_cat = self.get_category_selected() #Create the new Category category = todo.todoCategory(name) #Append to the tree category.add_to_tree(self.todoTree , selection_iter , self.__tree_columns) if (todo_cat): """something was selected so add the new category to the old category""" todo_cat.add_child(category) else: """Nothing selected so add to the current project data""" self.__categories.append(category)
The function is relatively simple as it relies on many helper functions. The first thing that it does is show the category dialog. If a valid name is returned then we continue adding the category.
We call the get_category_selected() function which is a helper function that is used to get the currently selected category, the gtk.TreeIter representing the selection, and the model associated with our gtk.TreeView. The function wraps the gtk.TreeView.get_selection() and the gtk.TreeSelection.get_selected() functions ensuring that todoCategory is selected. This means that if a todoTask is selected then its parent category will be returned.
We then create the category based on the name and add it to the tree. If there is a parent category (returned by get_category_selected()) then we add it to its children, otherwise we add it to a new private member self.__categories. self.__categories is basically a list of all of the top level todoCategories, it is our representation of the data displayed in the tree. It is created in the __init__ function:
#init todo file
self.todo_file = None
#Get the Main Window
self.main_window = self.wTree.get_widget("mainWindow")
self.set_window_title_from_file(self.todo_file)
#No categories
self.__categories = []The get_category_selected() looks like this:
def get_category_selected(self): """This function is a wrapper for the gtk.TreeView.get_selection() function and the gtk.TreeSelection.get_selected() function. It returns the same as gtk.TreeSelection.get_selected(), but ensures that it is a category that is selected. So if a todoTask is selected then it's parent category will be returned. @returns A 3-tuple containing a reference to the gtk.TreeModel and a gtk.TreeIter pointing to the currently selected node. Just like gtk.TreeSelection.get_selected but with the todoObject being returned as well. """ #First get the object column todo_ob = None tcolumn, pos = self.find_todoColumn(COL_OBJECT) if (not tcolumn): return None,None #Get the current selection in the gtk.TreeView selection = self.todoTreeView.get_selection() # Get the selection iter model, selection_iter = selection.get_selected() if (selection_iter): #Something is selected so get the object todo_ob = model.get_value(selection_iter, tcolumn.pos) if ((todo_ob) and (todo_ob.type != todo.CATEGORY)): #Alright we need the parent, this is not a category selection_iter = model.iter_parent(selection_iter) todo_ob = model.get_value(selection_iter, tcolumn.pos) return model, selection_iter, todo_ob
It relies on another helper function find_todoColumn which basically loops through the self.__tree_columns list looking for the todoColumn with the matching ID. If is found it is returned, along with the position in the list that it was returned at. This is partly done because I am unsure whether the order in the list will matter after initialization or whether the objects in the list should keep track of their position. If it is the objects, then they could be moved into a dictionary later on (you’ll notice that one is already created) for faster access.
We use find_todoColum() so that we can get the column representing the Object. (We use the Object instead of the type since we may need to get the actual object later. If we just wanted the type we could have used COL_OBJECT_TYPE)
Then what we do is we get the current gtk.TreeSelection and then get what (if anything) is actually selected within it. If something is selected (selection_iter is not None) then we check to see if the selected item is a category or not. If it is not a category we get the parent of the selecton iter, which we know is a category since tasks cannot have children.
Then we simply return all of the values: the current model, the gtk.TreeIter representing the selection category (may be None) and the todoCategory object (may also be none).
find_todoColumn() should be self explanatory:
def find_todoColumn(self, column_ID): """This function is used to search the __tree_columns list to find a specific todoColumn. @param column_ID - The ID of the column that we are looking for. @returns todoColumn, int - The column found and the position in the list. If todoColumn is None then the column was not found. """ count = 0 columnReturn = None for item_column in self.__tree_columns: if (item_column.ID == column_ID): columnReturn = item_column break if (columnReturn == None): #Something is really wrong we did not match helper.show_error_dlg(_("Error column data appears corrupted")) #Return the results return columnReturn, count
After all that code, we are finally able to add things to our tree!

It may seem complicated or like a lot of work, but in reality it’s simply a large process broken up into smaller reusable bits that make more sense when looked at as a whole.
The Code: Removing Categories
Now that we can add categories let’s add some code so that we can remove them. We will do this in the on_remove_item() function which is triggered by the btnRemoveCategory button’s clicked signal:
def on_remove_item(self, widget): """called then the remove category button is clicked. Can also be generally used to remove a category or item.""" #Get the column of the object tcolumn, pos = self.find_todoColumn(COL_OBJECT) #Get the current selection in the gtk.TreeView selection = self.todoTreeView.get_selection() # Get the selection iter model, selection_iter = selection.get_selected() if ((selection_iter) and (tcolumn)): """All right we have something to remove and we have the column that represents the todo object, first let's get the object from the selection_iter""" todo_ob = model.get_value(selection_iter, tcolumn.pos) if (todo_ob): """Allright everything worked, time to remove. Does this item have a parent""" todo_parent, iter = self.get_parent_category( selection_iter, tcolumn.pos) #Remove from the tree model.remove(selection_iter) #if there is a parent, remove from the parent if (todo_parent): todo_parent.remove_child(todo_ob) else: """There is no parent so remove from base list""" self.__categories.remove(todo_ob)
If you understood how we added the columns, how we remove them should be pretty straight forward. First we get the object todoColumn and the gtk.TreeSelection. Then if they are both valid we get the object that is selected. If nothing is selected then we can return because there is nothing to remove.
If something is selected then we call the helper get_parent_category() which simply gets us the item’s parent category. We then remove the selected item from the tree, but since the object is still stored within our data tree (self.__categories) we need to remove it from there.
If the item has a parent (todo_parent) we remove the item from the parent by calling remove_child(). If the item did not have a parent it means that it is a top level category and needs to be removed directly from self.__categories.
Here is get_parent_category():
def get_parent_category(self, selection_iter, object_pos = None): """Get the parent category of the selection_item @param selection_iter - gtk.TreeIter - A iter whose parent you want to get. @param object_pos - number - The index of the object column. If None this will be calculated. @returns - todoCategory, gtk.TreeIter - The parent category of the iter on success, or None on failure. The parent gtk.TreeIter """ if (object_pos == None): tcolumn, pos = self.find_todoColumn(COL_OBJECT) if (tcolumn): object_pos = tcolumn.pos else: #Column Data Error return None, None todo_parent = None selection_parent = None #Get the Model model = self.todoTreeView.get_model() #Get the current selection in the gtk.TreeView selection = self.todoTreeView.get_selection() if ((model) and (selection)): selection_parent = model.iter_parent(selection_iter) if (selection_parent): todo_parent = model.get_value(selection_parent, object_pos) return todo_parent, selection_parent
The Code: Saving and Loading with cPickle
Now that we can add and remove categories, the next thing that we want to do is be able to save and load our data to and from files. This code is very similar to the method used in my Extending our PyGTK Application so I will gloss over the repeated code and only explain the new stuff.
You probably remember the helper module mention above and used a little bit in some of the previously mentioned code. It’s simply a helper module that I have started to use so that I don’t always have to recode the same functions. The file’s name is “helper.py” and it looks like this:
#!/usr/bin/env python try: import pygtk pygtk.require("2.0") except: pass try: import os import gtk except ImportError, e: print "Import error in helper:", e sys.exit(1) def file_browse(dialog_action, filters, file_extension="", file_name=""): """This function is used to browse for a file. It can be either a save or open dialog depending on what dialog_action is. The path to the file will be returned if the user selects one, however a blank string will be returned if they cancel or do not select one. dialog_action - The open or save mode for the dialog either gtk.FILE_CHOOSER_ACTION_OPEN, gtk.FILE_CHOOSER_ACTION_SAVE @param filters - list - list of gtk.FileFilter() objects that will be added to the dialog. @param file_extension - string - The file extension that will be added to the filename when saving @param file_name - Default name when doing a save @returns - File Name, or None on cancel. """ if (dialog_action==gtk.FILE_CHOOSER_ACTION_OPEN): dialog_buttons = (gtk.STOCK_CANCEL , gtk.RESPONSE_CANCEL , gtk.STOCK_OPEN , gtk.RESPONSE_OK) dlg_title = _("Open File") else: dialog_buttons = (gtk.STOCK_CANCEL , gtk.RESPONSE_CANCEL , gtk.STOCK_SAVE , gtk.RESPONSE_OK) dlg_title = _("Save File") file_dialog = gtk.FileChooserDialog(title=dlg_title , action=dialog_action , buttons=dialog_buttons) """set the filename if we are saving""" if (dialog_action==gtk.FILE_CHOOSER_ACTION_SAVE): file_dialog.set_current_name(file_name) #Add filters for filter in filters: file_dialog.add_filter(filter) if (dialog_action==gtk.FILE_CHOOSER_ACTION_OPEN): """Create and add the 'all files' filter""" filter = gtk.FileFilter() filter.set_name(_("All files")) filter.add_pattern("*") file_dialog.add_filter(filter) """Init the return value""" result = None if file_dialog.run() == gtk.RESPONSE_OK: result = file_dialog.get_filename() if (dialog_action==gtk.FILE_CHOOSER_ACTION_SAVE): result, extension = os.path.splitext(result) result = result + "." + file_extension file_dialog.destroy() return result def show_error_dlg(error_string): """This Function is used to show an error dialog when an error occurs. error_string - The error string that will be displayed on the dialog. """ error_dlg = gtk.MessageDialog(type=gtk.MESSAGE_ERROR , message_format=error_string , buttons=gtk.BUTTONS_OK) error_dlg.run() error_dlg.destroy()
These are both old functions (changed slightly) so I won’t go over them.
The same is true for the File menu signal handers:
def on_file_new(self, widget): """File | New - Start a new project file, blank out the currnet project and start from scratch""" self.__categories = [] self.todo_file = None self.set_window_title_from_file(self.todo_file) self.reload_from_data() def on_file_open(self, widget): """Function called to open a todo file""" todo_file = helper.file_browse(gtk.FILE_CHOOSER_ACTION_OPEN , self.get_browse_filter_list() , FILE_EXT) #If we have a file if (todo_file): if (self.load_from_file(todo_file)): """Allright it all worked! save the current file and set the title.""" self.todo_file = todo_file self.set_window_title_from_file(self.todo_file) def on_file_save(self, widget): """File | Save function - Save the Todo file""" # Let the user browse for the save location and name if (self.todo_file == None): self.todo_file = helper.file_browse(gtk.FILE_CHOOSER_ACTION_SAVE , self.get_browse_filter_list() , FILE_EXT) #If we have a todo file if (self.todo_file): if (self.save_to_file(self.todo_file)): #Allright it all worked! Set the Title self.set_window_title_from_file(self.todo_file) def on_file_save_as(self, widget): """File | Save As function - Save the todo file""" todo_file = _("Untitled") if (self.todo_file != None): todo_file = os.path.basename(self.todo_file) todo_file = helper.file_browse(gtk.FILE_CHOOSER_ACTION_SAVE , self.get_browse_filter_list() , FILE_EXT) #If we have a xml_file if (todo_file): if (self.save_to_file(todo_file)): """Allright it all worked! save the current file and set the title.""" self.todo_file = todo_file self.set_window_title_from_file(self.todo_file)
They are all taken almost verbatim from previous tutorials so I won’t explaining them in detail. The do rely on one helper function: get_browse_filter_list() that simply creates a list of gtk.FileFilters to use when browsing:
def get_browse_filter_list(self): """Used to get the list of gtk.FileFilter objects to use when browsing for a file. @returns - list - List of gtk.FileFilter objects """ filter = gtk.FileFilter() filter.set_name(_("Todo file")) filter.add_pattern("*." + FILE_EXT) return [filter]
The actual work of saving and loading the data happens in the save_to_file(), load_from_file(), and reload_from_data() functions. We’ll go over the save_to_file() function first:
def save_to_file(self, filename): """Save the current todoproject to a filename @param filename - string - the file name to save the file too. @returns boolean - success or failure """ try: todo_file = open(filename, "w") cPickle.dump(self.__categories, todo_file, cPickle.HIGHEST_PROTOCOL) todo_file.close() return True except cPickle.PicklingError, e: helper.show_error_dlg(_("Error saving file: %s\r\n%s") % (filename, e)) return False except: helper.show_error_dlg(_("Error saving file: %s" % filename)) return False
Besides the exception handling, the actual code in this function is very simple:
todo_file = open(filename, "w") cPickle.dump(self.__categories, todo_file, cPickle.HIGHEST_PROTOCOL) todo_file.close() return True
First we open the file for writing, then we use cPickle to dump our __categories tree into the file, and then we close the file. That’s it.
Loading the data is equally as simple:
def load_from_file(self, filename): """Try to a todo project from a specific file. @param filename - string - the file name to load from @returns boolean - success or failure """ try: todo_file = open(filename, "rb") self.__categories = cPickle.load(todo_file) todo_file.close() self.reload_from_data() return True except cPickle.UnpicklingError, e: helper.show_error_dlg(_("Error opening file: %s\r\n%s") % (filename, e)) return False except: helper.show_error_dlg(_("Error opening file: %s" % filename)) return False
First we open the file for reading and in binary mode (as instructed by the Pickle documentation) and then we load the file into our self.__categories tree. After that we close the files and reload the tree from self.__categories using the reload_from_data() function.
def reload_from_data(self): """Called when we want to reset everything based on internal data. Probably called when a file has been loaded.""" self.todoTree.clear() if (self.__categories): for todo_ob in self.__categories: todo_ob.add_to_tree(self.todoTree, None, self.__tree_columns) else: self.__categories = []
reload_from_data simply clears the gtk.TreeStore using the gtk.TreeStore.clear() function. Then it loops through all of the toplevel categories in self.__categories and adds each category to the tree by calling the add_to_tree() function. If self.__categories is None for some reason (perhaps a bad file?) we simply reset it to a blank list.
If you remember the add_to_tree() function you’ll remember that it will also add all children of the category (and their children recursively) to the tree.
The Code: Connecting our menu with the gtk.MenuToolButton
Well we’re almost done here, all we have to do is connect our gtk.MenuToolButton with the menu that we created way back in the GUI section. Up until now you may have noticed that the arrow beside the Add button has been greyed out or disabled, this is because we have not attached a menu to the button yet using the gtk.MenuToolButton.set_menu() function. We will connect the menu to the button in a function called initialize_menus() that will be called in the __init__ function:
#Initialize the todo Tree self.initialize_todo_tree() #initialize menus self.initialize_menus()
def initialize_menus(self): """Called to initialize the menus""" add_button = self.wTree.get_widget("btnAdd") #load the menu from the glade file wTree = gtk.glade.XML(self.gladefile, "addMenu") #connect the menu with the signals wTree.signal_autoconnect(self) #Get the menu dialog widget menu = wTree.get_widget("addMenu") if ((add_button) and (menu)): add_button.set_menu(menu)
The first thing that we do is get the our gtk.MenuToolButton “btnAdd” from the widget tree. Then we get the widget tree that represents the “addMenu”. We then auto connect that widget tree with ourselves (this connects the “Add Category” menu item with our on_add_category() function. Then we get the actual gtk.Menu from the menu’s widget tree.
If all of the widgets have been returned properly we then connect the two using the gtk.MenuToolButton.set_menu() function.
Now when you run the application you will be able to use the menu to add categories!

Conclusion
You can download the full source to this tutorial here.
Whew! That was a lot of code and a lot of text to get through. Hopefully you were able to understand and follow along with all of it, and hopefully the next parts won’t be as long! The only reason that this part was so long was that it had so much ground to cover and because so much of it had been covered before. Future tutorials should cover smaller and more discrete tasks.
You will also notice that none of the translation files are included with the source, this was simply left out for now since there will be more text added in the future. There are also some extra dialogs in the glade project that will be used in future tutorials, if you want you could start implementing them.
Of course you will probably never find yourself creating an application that is identical to this pyLan application, but the idea is that you may find yourself creating an application that is similar or that requires similar elements. In those situations hopefully you will be able to re-use or re-factor that code found in this, and future, tutorials.
As always if you find any errors or have any questions feel free to leave a comment. If however you have a general question about python or PyGTK I would appreciate it if you ask it in the LearningPython forums so that a possible solution to someone elses problem will not get buried in an unrelated area.
Time for a some food!



February 18th, 2007 at 3:54 am
Thank you very much for that article!
It’s really nice to see how to make an application from scratch, and all the little tricks you may use during the dev.
And thank you for the time you spend screenshoting, explaining and detailing some stuff you usually wouldn’t!

selsine Says:February 19th, 2007 at 10:07 am
Hi Pierre,
Thanks for the kind words. That’s exactly what I wanted to achieve with this tutorial. I know that sometimes when I’m looking to do something that isn’t documented very well (or at all) all I’m looking for is an example of how to do things.
I’m not saying that the methods that I chose are the best or the only ways to do things, I’m just trying to give people an example of how one might accomplish these tasks.
Thanks.
February 21st, 2007 at 12:41 pm
Hey man, i just want to thank you for your tutorial series, keep on!
February 26th, 2007 at 4:36 pm
Excelent!! But a little dificult the code i think
Thanks for the tutorials
March 2nd, 2007 at 5:15 am
Let me ask you: what gtk theme do you use? I like it very much.

selsine Says:March 3rd, 2007 at 1:54 pm
Sascha – Thanks! You are very welcome!
Jesus – If you have any difficulty understanding any of the code feel free to ask questions about it here or on the forums. I’m more then happy to explain any of it.
pm – I use ClearLooks for the Control and Borders and then Tango for the icons. Hope that helps!
May 16th, 2007 at 11:07 pm
This is my favourite Python Articles
July 4th, 2007 at 8:39 pm
[...] If you want to follow along with the code in detail, and have not done so already, you should read part one and part two of this series. [...]
August 28th, 2007 at 3:01 pm
Hi, I tried to run the code but I get a name error about the gettext prefix “_”
Any idea?
September 23rd, 2007 at 1:18 pm
[...] If you want to follow along with the code in detail, and have not done so already, you should read part one, part two and part three of this series. [...]
December 11th, 2007 at 9:48 am
Nice Tutorials. I’ve searched for many things in using google but I didn’t find a reasonable answer. I finally found those things in this tutorial. Thanks and good luck.
December 13th, 2007 at 11:29 am
Hello, I am reading bits and pieces of GTD and I’ve started my own todo like application, although I initially attempted it in ruby: http://alcopop.org/code/todo/. However I’m considering some radical changes and I’ve grown bored of ruby so I am planning to rewrite it in python. I expect my tool to differ quite a lot from what I’ve seen of yours, however I am looking forward to reading your articles and trying it out tonight.
January 4th, 2008 at 8:26 pm
Very nice tutorial, I to submitted a article to the Python Magazine, but mine was focused on a security Library to connect to a Nessus VA server. Anyways, I just wanted to let you know that these articles and the one in the mag are fantastic.
I do have a few questions though. I find in a lot of my projects I use a progress bar for well , keeping track of the progress of tasks that are currently running such as a file download or what not. I was thinking about extending the widget to help with the complexity of actually doing the bar updates.
For Example:
current_percent = current_count/100 * total_size
final = percent * .1/10 #lets convert it to fit in the progressbar format
gtk.ProgressBar.set_Fraction(final)
I was just wondering if you had any thoughts on this and maybe some pointers pointing me in the right directions.
Again, I would like to say thank you on the great articles.
Dan

selsine Says:January 17th, 2008 at 11:32 am
Hi Jon,
Thanks for the kind words, you GTD tool looks really cool! Keep up the good work.

selsine Says:January 17th, 2008 at 11:36 am
Hi Dan,
Thanks! I’m glad you are enjoying these tutorials and PythonMagazine!
As far as the progress control is concerned I don’t really have that much to tell you since I have not done that much work on it. It goes make me feel like perhaps I need to take a closer look at it though.
If you do end up looking into it let me know, I’d love to learn more about it.
January 18th, 2008 at 5:47 pm
Thanks a million for this fantastic article. However, I wondering if you could tell me how to run this sample application on Windows platform.
I have Python 2.4 installed on my computer and I was wondering if you could tell me how to install pyGtk on my computer. I really like your excellent tutorial and so impressed that I’ve set myself the task to learn to program in Python.

selsine Says:February 21st, 2008 at 8:55 pm
Hi Dominkik,
Take a look at the PyGTK FAQ it has a good topic on how to install PyGTK on Windows. I’ve done it a few times, you shouldn’t have any problems.
May 4th, 2008 at 6:22 pm
You’re tutorials were a wonderful intro for me. I’m a code by profession, but I haven’t used python before, so I wanted a quick intro to get me started. See my website link for the app I built, if you’re interested.
Thanks for this and the other incredibly helpful tutorials you’ve written up.
August 5th, 2008 at 10:52 pm
Great article, i shall have to add this to my mental evidence list as to why you should learn a programming language. Its a tool, and with tools you build things you want, repair the things that don’t work, even reinvent things with your own little twist and this is prime example of that.
You are never without with a programming language, the universe and your fingertips only restrained by your imagination. If you want something, need something you can make it and if it doesn’t exist invent it.
Why spend ÂÅ20 on a program you can make yourself? or am i just cheap =P
October 3rd, 2008 at 3:05 pm
Great work! I really like the contents of your tutorial. You are covering alot of stuff ive failed to get right in the past.
Unfortunately i am stuck right at the beginning. I dont seem to have eather the tool bar or the GtkMenuToolButton in my menu. Im using glade-3. Am i jus tmissing it or is it really no there. Seems like im the only one having trouple with this.
Cheers!