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:

PyLan – part one

Python GTD pyGTK

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:

The tutorial is organized into the following sections:

  1. The GUI
  2. The Code: Setting it all up
  3. The Code: Initializing the tree
  4. The Code: todo items
  5. The Code: Adding Categories
  6. The Code: Removing Categories
  7. The Code: Saving and Loading with cPickle
  8. The Code: Connecting our menu with the gtk.MenuToolButton
  9. 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:

  1. Create a new Glade Project and save it as pylan a folder named PyLan that you have created.
  2. 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.
  3. Add a vertical box to the window and give it 4 rows.
  4. 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.
  5. In the second row add a tool bar with two items.
  6. 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”.
  7. 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”.
  8. In the fourth row add a Status Bar.
  9. In the Third row add a Tree View, set its name to be “todoTree”

The category dialog

  1. Now create a Dialog with an Ok and a Cancel button. Sets it’s name to be “categoryDialog”, and its title to be
  2. “Category”. Give it a default width and a width of 300. Set its named icon to be: “stock_post-message”.
  3. Add a GTKHBox to the dialog with two columns.
  4. In the fist column add a GtkLabel with the label “Name:”. Give it an X Pad of 3 pixels.
  5. 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.

  1. Create a GtkMenu and call it: “addMenu”
  2. 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:

Python GTD pyGTK

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:

Python GTD pyGTK

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:

Python GTD pyGTK

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!

Python GTD pyGTK

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!

Python GTD pyGTK

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!

selsine

del.icio.us del.icio.us

20 Responses to “PyLan a GTD todo application written in python and PyGTK – part one”

  1. Pierre
    Says:

    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!

  2. selsine

    selsine
    Says:

    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.

  3. Sascha Peilicke
    Says:

    Hey man, i just want to thank you for your tutorial series, keep on!

  4. Jesus
    Says:

    Excelent!! But a little dificult the code i think :P

    Thanks for the tutorials :)

  5. pm
    Says:

    Let me ask you: what gtk theme do you use? I like it very much.

  6. selsine

    selsine
    Says:

    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!

  7. PyArticles
    Says:

    This is my favourite Python Articles

  8. learning python » Blog Archive » PyLan a GTD todo application written in python and PyGTK - part three
    Says:

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

  9. Andreas
    Says:

    Hi, I tried to run the code but I get a name error about the gettext prefix “_”
    Any idea?

  10. learning python » Blog Archive » PyLan a GTD todo application written in python and PyGTK - part four context menus!
    Says:

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

  11. Maksud
    Says:

    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.

  12. Jon
    Says:

    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.

  13. Dan
    Says:

    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

  14. selsine

    selsine
    Says:

    Hi Jon,

    Thanks for the kind words, you GTD tool looks really cool! Keep up the good work.

  15. selsine

    selsine
    Says:

    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.

  16. Dominik
    Says:

    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.

  17. selsine

    selsine
    Says:

    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.

  18. brandon
    Says:

    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.

  19. William
    Says:

    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

  20. Sebastian
    Says:

    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!

Leave a Reply

 

Popular Posts