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.<