PyLan a GTD todo application written in python and PyGTK – part two


PyLan two

This is part two of the PyLan tutorial series, if you want to follow along with the code in detail, and have not done so already, you should read part one of this series.

In this tutorial I will go over the following items:

This tutorial is organized into the following sections:

  1. The GUI
  2. The todo.Task Object
  3. Adding a todo.Task object
  4. Showing the Calendar window
  5. Editing todo.Category and todo.Task items
  6. Pango markup in the gtk.TreeView
  7. Conclusion

Python GTD pyGTK

You can download the full source to this tutorial here.

The GUI

I will be working with the same glade project as last time, except I will be adding a some new dialogs and buttons. I will not go over all of the changes in the GUI since many of them are very simple changes, similar to changes that have been made in previous tutorials.

For example I decided to get rid of the Dropdown menu for adding the Categories and the tasks, instead I decided to create separate buttons to do the two separate tasks. I won’t describe the switch here, since adding toolbar buttons is pretty straightforward. For anyone interested in why I decided to make this switch, the answer is pretty simple, I didn’t like using the drop down menu as much as I thought that I would. Instead I found it confusing and prone to error. As a result I made the switch to the separate buttons.

So here is a non-comprehensive list of the changes that I made to the GUI in case you were interested in seeing how something was done.

The Task dialog

This dialog is used to add or edit a todo.Task object.

  • Create a Dialog with an Ok and a Cancel button. Sets it’s name to be “taskItemDialog”, and its title to be “Task Item”. Give it a width of 300 and a height of 225.
  • Add a Vertical Box to the dialog with 2 rows. In the second row add a Check Button with the label “Completed” and the name “checkCompleted”.
  • In the first row add a table with two columns and four rows. To the following to the table, adding vertical padding of 3 pixels to each space in the second column:
    • Add a label with the label “Title” in [0, 0].
    • Add a Text Entry in [0, 1] and call it enTitle
    • Add a label with the label “Details” in [1, 0], give it three pixel of horizontal padding.
    • Add a Text View in [1, 1] and call it tvDetails. In the scrolled window’s properties set the H Policy to “Never” and the V Policy to “Automatic”. This means that there will never be a horizontal scrollbar, and that there will be a vertical scrollbar only when one is necessary.
    • Add a label in [2, 0] with the Label “Due:”.
    • Add a horizontal box with three columns in [2, 1]. In the first add an entry labelled enDueDate, in the second add a button named btnDue add a clicked signal handler, and in the third add a combobox and give it the name “cmbTime”. Add time entries to the combobox in the form: “”(blank),12:00AM, 12:30AM, 01:00AM…11:30PM. Then right-click on the button and select it in the menu and then select “Remove Button Contents”. Then add an arrow to the button and set its direction to down. The button will be used to display a Calendar so that the people can select the due date.
    • Add a label in 3,0 and set its label to “Priority:
    • Add a Combo Box in 3,1 and set its name to “cmbPriority”. Set its items to “High”, “Medium”, and “Low”.

Python GTD pyGTK

The Calendar Window

This is a popup window that we will us to let the user select the due date for their tasks.

  • Add another window, and call it “calendarWindow”. Set its border width to 1, its title to “”, its type to “popup”, modal to “yes”, resizeable to “no”, and skip taskbar to “yes”.
  • Add a vertical box to the window with two rows.
  • In the first row ad a calendar widget and call it “calendarWidget”
  • In the second row add a button with the name “btnNoDate”. Set the label to be “No Date” and the icon to be gtk-no. Add a clicked signal handler.

Python GTD pyGTK

The todo.Task object

The todo.Task object is used to represent the task item in the same way that the todo.Category object represents a category item. The astute among you will notice that I renamed the category object, I just thought that todo.Category looked so much better then todo.todoCategory.

Since we will be using the datetime and time python modules to represent the due date for tasks we need to import them in the todo file:

import datetime
import time

The todo.Task class is relatively simple, if you’ve read the first tutorial it should be very easy to understand this code:

class Task(todoBase):
	"""This is a task in the todoTree.  It represents
	something that needs to be done.
	"""

	#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)

	#Due date property
	def __get_due_date(self):
		return self.__m_due_date
	def __set_due_date(self, due_date):
		self.__m_due_date = due_date
	due_date = property(__get_due_date, __set_due_date)

	#Due time property
	def __get_due_time(self):
		return self.__m_due_time
	def __set_due_time(self, due_time):
		self.__m_due_time = due_time
	due_time = property(__get_due_time, __set_due_time)

	#priority property
	def __get_priority(self):
		return self.__m_priority
	def __set_priority(self, priority):
		self.__m_priority = priority
	priority = property(__get_priority, __set_priority)

	def __init__(self, name):

		#init variables
		self.__m_name = ""
		self.__m_due_date = None
		self.__m_due_time = None
		self.__m_priority = ""

		self.completed = False
		self.details = ""

		#set properties
		self.name = name
		#init base
		todoBase.__init__(self, TASK)

		"""Format strings so that the date and time
		formats returns can easily be changed."""
		self.date_format = "%Y-%m-%d"
		self.time_format = "%I:%M%p"

	def __str__(self):
		return _("todo .Task object: name = %s") % (self.name)

	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)
			elif (item_column.ID == pyLan.COL_DETAILS):
				lst_return.append(self.details)
			elif (item_column.ID == pyLan.COL_DUE):
				lst_return.append(self.get_datetime_string())
			elif (item_column.ID == pyLan.COL_PRIORITY):
				lst_return.append(self.priority)
			elif (item_column.ID == pyLan.COL_COMPLETED):
				if (self.completed):
					lst_return.append(_("True"))
				else:
					lst_return.append(_("False"))
			else:
				helper.show_error_dlg(_("Error unknown column ID: %d" % item_column.ID))
				lst_return.append("")

		return lst_return

	def get_date_string(self):
		"""This function is used to get the Date string. It will
		be returned in the format of self.date_format.
		@returns - string - The current Due date or "" if nothing
		has been set yet.
		"""

		date_string = ""
		if (self.due_date):
			date_string = self.due_date.strftime(self.date_format)
		return date_string

	def get_time_string(self):
		"""This function is used to get the time string. It will
		be returned in the format of self.time_format.
		@returns - string - The current Due time or "" if nothing
		has been set yet.
		"""

		date_string = ""
		if (self.due_time):
			date_string = self.due_time.strftime(self.time_format)
		return date_string

	def get_datetime_string(self):
		"""Used to get the date and the time in their
		specified formats.
		@returns - string - the date and the time.
		"""

		date_string = self.get_date_string()
		time_string = self.get_time_string()
		if (len(time_string)>0):
			date_string = "%s %s" % (date_string, time_string)

		return date_string

One thing that you may notice is how we use the datetime and time modules. You should notice that we have two member variables:

self.date_format = "%Y-%m-%d"
self.time_format = "%I:%M%p"

These variable store the date and time format that we will use to display the date and time in the tree. We will pass the format strings to thestrftime function (from the python docs):

date, datetime, and time objects all support a strftime(format) method, to create a string representing the time under the control of an explicit format string. Broadly speaking, d.strftime(fmt) acts like the time module’s time.strftime(fmt, d.timetuple()) although not all objects support a timetuple() method.

We define them as members of the class so that they can be changed easily and even pickled with the object itself.

I won’t go into too much other detail about this class since it’s pretty much just a data holder for now, and is very similar to the todo.Category object explained in tutorial one except that it has a few more data members.

Adding a todo.Task object

For this we are going to have to use the task dialog that we created in the GUI stage. We are going to encapsulate the handling of displaying the dialog in a class. It made sense to me to do this since this dialog is far more complicated then many that we have used before.

The class is called TaskDialog and I created it in the todo.py file:

class TaskDialog(object):
	"""This is a class that is used to show a taskDialog.  It
	can be used to create or edit a todo.Task.  To create one
	simply initialize the class and do not pass a totoTask.  If you
	want to edit an object initialize with the object that
	you want to edit."""

	def __init__(self, glade_file, task = None):
		"""Initialize the task dialog.
		@param glade_file - string - the glade file for this
		dialog.
		@param task - todo.Task - None to create a new
		todo.Task object, or the object that you wish to
		edit.
		"""
		self.glade_file = glade_file
		self.task = task

		#Get the widget tree
		self.wTree = gtk.glade.XML(self.glade_file, "taskDialog")
		#Connect with yourself
		self.wTree.signal_autoconnect(self)
		self.dialog = self.wTree.get_widget("taskDialog")
		#get the widgets from the dlg
		self.en_title = self.wTree.get_widget("enTitle")
		self.tv_details = self.wTree.get_widget("tvDetails")
		self.en_due = self.wTree.get_widget("enDue")
		self.btn_due = self.wTree.get_widget("btnDue")
		self.cmb_Time = self.wTree.get_widget("cmbTime")
		self.cmb_priority = self.wTree.get_widget("cmbPriority")
		self.check_completed = self.wTree.get_widget("checkCompleted")
		#select the items in the combo boxes
		self.cmb_Time.set_active(0)
		self.cmb_priority.set_active(2)

		wTreeDue = gtk.glade.XML(self.glade_file, "calendarWindow")
		self.calendar_window = wTreeDue.get_widget("calendarWindow")
		wTreeDue.signal_autoconnect(self)

		self.update_dialog_from_object()

As you can see what happens here is the TaskDialog is initialized with the path to the glade file that contains the taskDialog, and an optional todo.Task object. If the todo.Task is not specified (or None) when creating the TaskDialog then the TaskDialog instance is in “add” mode, which means that it will create a todo.Task object with the settings specified on the dialog if the user presses the Ok button.

If an instance of a todo.Task object is used when constructing the TaskDialog then the dialog is in “edit” mode, and it will modify the object that was passed if the user presses the Ok button.

So not much happens in the __init__ function except we load the dialog and the calendar window from the glade file (the calendar window must be set not to be initially visible in the glade file! Otherwise it will be shown when it is loaded.) and we auto connect the signals from the dialog and the window with the TaskDialog class.

Then we get access to all of the important widgets on the task dialog. You’ll also notice that we set the active item in both the time and priority gtk.ComboBox’s using the set_active() function.

We do this so that the first item is selected in the time gtk.ComboBox (the blank) and we select the third item in the priority gtk.ComboBox so that the low priority item is selected.

At the end of the function we call the update_dialog_from_object() function, this function is used to update what is displayed on the dialog based on the contents of the todo.Task. There is a sister function named save_data_to_object() that is used to update the todo.Task item with the contents of the dialog:

def update_dialog_from_object(self):
	"""Used to update the settings on the dialog"""

	if (self.task):
		#Title
		self.en_title.set_text(self.task.name)
		#Details
		self.set_details(self.task.details)
		#Due Date
		self.en_due.set_text(self.task.get_date_string())
		#Due Time
		found_iter = self.find_text_in_combo(self.cmb_Time
						, self.task.get_time_string())
		if (found_iter):
			self.cmb_Time.set_active_iter(found_iter)
		#priority
		found_iter = self.find_text_in_combo(self.cmb_priority
									, self.task.priority)
		if (found_iter):
			self.cmb_priority.set_active_iter(found_iter)
		#completed
		self.check_completed.set_active(self.task.completed)


def save_data_to_object(self):
	"""This function is used to read the data from the dialog
	and then store it in the todo.Task object.
	"""
	if (self.task == None):
		self.task = Task(self.en_title.get_text())
	else:
		self.task.name = self.en_title.get_text()

	self.task.details = self.get_details()

	self.task.set_due_date_from_string(self.en_due.get_text())
	self.task.set_due_time_from_string(self.cmb_Time.get_active_text())
	self.task.priority = self.cmb_priority.get_active_text()
	self.task.completed = self.check_completed.get_active()

Over all these two functions are pretty straightforward, they simply marshal the date between the dialog and the todo.Task. You’ll see that there are some helper functions that are used in both functions in order to make saving and loading the data from the object to the dialog easier. For example to set the text in the gtk.TextView we use the set_details() function, and then to get the text we use the get_details() function:

def get_details(self):
	"""This function gets the details from the TextView
	@returns string - The text in the gtk.TextView
	"""
	txtBuffer = self.tv_details.get_buffer()
	return txtBuffer.get_text(*txtBuffer.get_bounds())

def set_details(self, details):
	"""This function sets the text in the defails gtk.TextView
	@param details - string - The text that will be
	put into the gtk.TextView.
	"""
	txtBuffer = self.tv_details.get_buffer()
	txtBuffer.set_text(details)

These two function are very simple, we get the gkt.TextBuffer associated with the gtk.TextView and then we use the get_text() and set_text() functions to get and set the text.

The next helper that we use is the find_text_in_combo() function:

def find_text_in_combo(self, combobox, text):
	"""This is a helper function use to find text in
	a gtk.ComboBox.
	@param conbobox gtk.ComboBox - This should contain
	text.
	@param text - string - the text that we are looking
	to find.
	@returns - gtk.TreeIter - The iter at the found
	position or None if nothing was found.
	"""
	found_iter = None #The Iter where text is found
	#Get the gtk.TreeModel associated with the combo
	combo_model = combobox.get_model()
	if (combo_model):
		#Get the first iter in the model
		search_iter = combo_model.get_iter_first()
		"""Now loop through the model checking for
		matches until one is found.  Or until
		we have ran out of iters."""
		while ((found_iter == None)
			and (search_iter)):
			if (text == combo_model[search_iter][0]):
				#Found!
				found_iter = search_iter
			else:
				search_iter = combo_model.iter_next(search_iter)
	return found_iter

This function takes a gtk.ComboBox and a string as parameters and searches the gtk.TreeModel associated with the gtk.ComboBox for the text. If the text is found then the gtk.TreeIter associated with that item is returned, otherwise None is returned. The function is pretty simple, but I was slightly disappointed that there was no built in way to do this…perhaps I missed something or perhaps this will be addressed in a future release.

Then once we have the gtk.TreeIter we can use the set_active_iter() to set that item as the current item in the gtk.ComboBox.

To get the date and time we use the todo.Task.get_time_string() and todo.Task.get_date_string() functions that we have already touched on. The setting of the due date and due time however is slightly more complicated and I have not gone over it yet. What we need to do is generate datetime.date and datetime.time instances from strings.

The following two functions in the todo.Task are used to accomplish this:

def set_due_date_from_string(self, date_string):
	"""This function is used to set the due_date based
	on a string. We will try to parse the string.  We will try
	to parse the following formats: YYYY-MM-DD
	, MM-DD-YYYY, or DD-MM-YYYY
	@param date_string - string - The string that we will
	turn into our datetime.date object.
	@returns boolean - success or failure"""
	success = False
	#Switch chars
	date_string = date_string.replace("/","-")
	date_string = date_string.replace("\\","-")
	date_string = date_string.replace(" ","")
	#All the possible formats
	lst_formats = [
		"%Y-%m-%d"
		, "%m-%d-%Y"
		, "%d-%m-%Y"]
	#Now loop through the formats and try to find a match
	date = None
	for format in lst_formats:
		try:
			date = datetime.datetime(*time.strptime(date_string
						, format)[0:3])
			#We found a matching format
			self.due_date = date
			#Save the format
			self.date_format = format
			break
		except:
			#failed
			pass
	return success

def set_due_time_from_string(self, time_string):
	"""This function is used to set the due_time based
	on a string. We will try to parse the string.  We will try
	to parse some formats.
	@param time_string - string - A string representing the
	time.
	@returns - boolean success or failer
	"""
	success = False
	#strip whitespace
	time_string = time_string.replace(" ","")
	#All the possible formats
	lst_formats = [
		"%I:%M%p"
		, "%H:%M%p"]
	#Now loop through the formats and try to find a match
	due_time = None
	for format in lst_formats:
		try:
			time_struct = time.strptime(time_string
						, format)
			due_time = datetime.time(time_struct.tm_hour
						, time_struct.tm_min)
			#Save the time
			self.due_time = due_time
			#Save the format that was used
			self.time_format = format
			success = True
			break
		except:
			#failed
			pass
	return success

The functions are slightly difficult to understand I will take a little bit of time to explain how they work. The general idea of the functions is to loop through a list of possible formats and try to create either a datetime.date or datetime.time instance using the current format.

If the creation throws an exception then it has failed, if it does not then it has succeeded and we need to save the format that we used (it becomes the format that we display things in, so if you type in the date in a certain way it will be displayed in that way,) and save the actual instance.

We use the time modules strptime function to create a struct_time, which we then use to create the date or time instance (from the python documentation):

Parse a string representing a time according to a format. The return value is a struct_time as returned by gmtime() or localtime(). The format parameter uses the same directives as those used by strftime(); it defaults to “%a %b %d %H:%M:%S %Y” which matches the formatting returned by ctime(). If string cannot be parsed according to format, ValueError is raised. If the string to be parsed has excess data after parsing, ValueError is raised. The default values used to fill in any missing data when more accurate values cannot be inferred are (1900, 1, 1, 0, 0, 0, 0, 1, -1) .

Support for the %Z directive is based on the values contained in tzname and whether daylight is true. Because of this, it is platform-specific except for recognizing UTC and GMT which are always known (and are considered to be non-daylight savings timezones).

struct_time is simply a sequence of 9 integers that we can then use to create our date or time instances. The only real difference between the two is that instead of always being able to use the first three numbers in the struct_time sequence when creating the date instance we need to reference the specific time members when creating the time object.

So the code is a bit complicated, but when you look the overview of what it’s doing it’s actually pretty simple.

The next thing we have to do is actually show do the dialog:

def run(self):
	"""Show the dialog"""

	break_out = False
	while (not break_out):
		result = self.dialog.run()
		if (result==gtk.RESPONSE_OK):
			"""Save the date to the object becuase the use pressed
			ok."""
			self.save_data_to_object()
			break_out = True
			#Validate here eventually
		else:
			break_out = True

	self.dialog.hide()

	return result;

Another simple function (I seem to say that a lot don’t I?) we’re just showing the dialog, and if the users clicks on the OK button we save the data to our todo.Task object and then we leave. You might wonder why this is done in a loop, the reason is so that in the future we will be able to validate what the user has entered.

So when if the user clicks on the OK button and the validation fails, then we would not set break_out to True, and the dialog would be shown again. This is just an easy way to set up the dialog for future validation.

The last thing that we need to do is actually use the dialog! We do this in the on_add_task() function in our main PyLan class:

def on_add_task(self, widget):
	"""Called when we want to add a task."""

	model, selection_iter, todo_cat = self.get_category_selected()
	#Make sure that a category is selected
	if ((model) and (selection_iter) and (todo_cat)):
		task_dialog = todo.TaskDialog(self.gladefile)
		if (task_dialog.run() == gtk.RESPONSE_OK):
			#Append to the tree
			task_dialog.task.add_to_tree(model
				, selection_iter
				, self.__tree_columns)
			#Add to the Category
			todo_cat.add_child(task_dialog.task)

Pretty simple stuff here again (I’m like a broken record!) we get the selected category, if all of the variables come through then we create the todo.TaskDialog in “add” mode and then we run it. If the response is gtk.RESPONSE_OK we add the task to the tree, and then add the task to the category.

If you are wondering about the last two steps there, then you should check out the first tutorial where those helpers are explained.

Showing the Calendar window

This next step took me a long time to figure out even though the actual code is very short. This step is getting the calendar window, that we created in our glade project, to display when the user clicks on the Due Date “dropdown” button.

Python GTD pyGTK

So here is the code:

def on_btnDue_clicked(self, widget):
	"""Called when the due button is clicked."""
	rect = widget.get_allocation()
	x, y = widget.window.get_origin()
	cal_width, cal_height = self.calendar_window.get_size()

	self.calendar_window.move((x + rect.x - cal_width + rect.width)
		, (y + rect.y + rect.height))

	self.calendar_window.show()

	"""Because some window managers ignore move before
	you show a window."""
	self.calendar_window.move((x + rect.x - cal_width + rect.width)
		, (y + rect.y + rect.height))

So the first thing we do is use the gtk.Widget.get_allocation() function to get the gtk.gdk.Rectangle representing the size of the btnDue button.

Then we get the X and Y screen coordinates of the gtk.gdk.Window (the window where the widget is actually drawn) using the get_origin() function. This is in root window coordinates instead of being relative to the parent window. So this is actually the top left corner of the TaskDialog.

Then we get the height and width of the calendar window, and finally move it into position so that the top right corner of the calendar window lines up with the bottom right corner of the btnDue widget.

So, we take the top left corner of the dialog, and then add onto it the x,y coordinates of the btnDue (which are relative to the top left cornet of the TaskDialog) and then we add to it the height and the width of the btnDue so that we get the X and Y of the bottom right corder of the btnDue. Then we subtract the width of the calendar window, so that the right side of the calendar window lines up with the right side of the btnDue.

If this seems a bit confusing try playing with the numbers a little bit, commenting out parts of the equation, and then see where the calendar window shows up. After playing with it a little bit it should be pretty straight forward.

So what happens when then calendar window is displayed? Well it will stay visible until the user double-clicks on a date, or clicks on the “No Date” button.

So we need to add two signal handlers:

def on_btnNoDate_clicked(self, widget):
	"""Called when the now date widget is
	clicked.  This means that they do not want
	a due date."""
	#Hide the calendar window
	self.calendar_window.hide()
	self.en_due.set_text("")

def on_calendarWidget_day_selected_double_click(self, widget):
	"""Called when the used has double-clicked on a date
	in the calendar widget"""
	#Hide the calendar window
	self.calendar_window.hide()

	year, month, day = widget.get_date()
	month +=1 #since it's 0 based
	self.en_due.set_text("%d-%02d-%02d" % (year, month, day))

Pretty simple stuff here, if the “No Date” button is clicked then we blank out the due date edit field, and if a date is double clicked on, we get the date from the gtk.Calendar using the get_date() function. Then we set the date in the edit fields based on those values.

For now we use a standard format, but in the future we could easily work with the date_format stored in the todo.Task object.

Editing todo.Category and todo.Task items

The next step is letting the user edit a todo.Category or todo.Task item that they have already added to the list. Fortunately the way that we have shown both the Category and Task dialogs makes this step very easy.

The first thing that we need to do is add an “edit” event handler in the PyLan class. This function can be triggered from a menu item, or from the edit button in the main toolbar:

def on_edit_object(self, widget):
	"""Called when we want to edit the selected item"""

	# Get the selected object
	model, selection_iter, todo_ob = self.get_selected_object()
	if ((selection_iter) and (model) and (todo_ob)):
		"""All right something and we have all the needed data"""
		self.edit_object(todo_ob, model, selection_iter)

Pretty simple, we call a helper function get_selected_object() which does exactly that, gets the selected object, the gtk.TreeIter associated with it, and the gtk.TreeModel that it is displayed in. Then we call edit object with those parameters.

get_selected_object() is a lot like the get_selected_category() function:

def get_selected_object(self):
	"""Just a helper function that will give you the
	selected object, whether it is a todoItem or
	a todoTask.
	@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."""

	todo_ob = None
	tcolumn, pos = self.find_todoColumn(COL_OBJECT)
	if (not tcolumn):
		return None,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 and model):
		#Something is selected so get the object
		todo_ob = model.get_value(selection_iter, tcolumn.pos)
	return model, selection_iter, todo_ob

The edit_object() function is equally as easy:

def edit_object(self, todo_object, model, object_iter):
	"""Helper function used to edit an object in the tree.
	@param todo_object - todoBase - The object that we
	are editing.
	@param model - gtk.TreeModel - The model for the tree.
	@param object_iter - gtk.TreeIter representing the
	todo_object's position in the tree.
	"""
	#Just make sure that they are all correct
	if ((todo_object) and (model) and (object_iter)):
		if (todo_object.type == todo.CATEGORY):
			#Edit category
			name = self.show_category_dialog(todo_object.name)
			if (name):
				todo_object.name = name
				todo_object.set_tree_values(model
					, object_iter
					, self.__tree_columns)
		else:
			#Edit task
			task_dialog = todo.TaskDialog(self.gladefile, todo_object)
			if (task_dialog.run() == gtk.RESPONSE_OK):
				todo_object.set_tree_values(model
					, object_iter
					, self.__tree_columns)

So what we do is ensure that the parameters passed in are valid, then we check the type of the object, and then show the correct dialog. If the user presses the Ok button then we update the data that is stored in the model by calling a new function set_tree_values():

def set_tree_values(self, tree_model, iter, todoColumnList):
	"""This is used to set the values in a tree.
	For whatever reason you cannot use the same list that you
	use to append items into the tree?  I don't know why.
	@param tree - gtk.TreeStore - The tree store that we will be
	setting the values in
	@param iter - gtk.TreeIter - The item in the tree
	@param todoColumnList - list - A list of todoColumn items.
	Their type member should use used to determine the order
	of the returned list."""
	lst_values = self.get_column_list(todoColumnList)
	count = 0
	for value in lst_values:
		tree_model.set_value(iter, count, value)
		count += 1

This function is defined in the todo.todoBase class, which is the base class for both the todo.Task and todo.Caegory. It can be overwritten by either if they want to perform some special processing, but by default it does everything that we need. It gets the list of values to display in the tree, and then loops through that list setting each value in the model.

So that’s basically editing items, not too difficult. The only other feature that I added was allowing you to edit an item when you double-click on in the gtk.TreeView. I was slightly surprised to find out that there was no “double-click” signal for the gtk.TreeView instead one has to handle the row-activated signal:

The “row-activated” signal is emitted when the row_activated() method is called or the user double clicks a treeview row. “row-activated” is also emitted when a non-editable row is selected and one of the keys: Space, Shift+Space, Return or Enter is pressed.

So we can handle the “row-activated” signal like this:

def on_todoTree_row_activated(self, tree_view, path, tree_column):
	"""This is called when a row is "activated" in the tree
	view.  It happens when the user double clicks on an item
	in the tree.  We will use it to edit the item.
	@param tree_view gtk.TreeView - The Tree
	@param path - string - The path string
	@param tree_column - gtk.TreeViewColumn - The column that
	was clicked on.
	"""
	#Get the column of the object
	tcolumn, pos = self.find_todoColumn(COL_OBJECT)
	model = tree_view.get_model()
	if ((tcolumn) and (model)):
		#Now get the selection iter from the path
		selection_iter = model.get_iter(path)
		if (selection_iter):
			#Now that we have the selection let's get the object
			todo_ob = model.get_value(selection_iter, tcolumn.pos)
			#Now lets edit the object
			self.edit_object(todo_ob, model, selection_iter)

First we get the model and the object column. Then we use the path that was passed to the signal handler to get the gtk.TreeIter that matches it. Then we get the todo object and we edit it.

Pretty simple, and now instead of having to find the edit button in the toolbar you can simple double-click on an item in order to edit it.

Pango markup in the gtk.TreeView

Since you currently cannot tell the difference between a category and a task I decided to make the category titles bold so that you can quickly tell the difference.

This is actually quite easy to do once you know what you need to do. It took me a little while to find information on how to do this, but once I was able to find the information implementing it was pretty simple.

The first thing that we need to do is update the todoColumn object to have a new member variable: markup. This lets us know that this column will display markup instead of simple text.

So now the create_column() function in the todoColumn class, which was added to handle the creating of columns, looks like this:

def create_column(self):

	column = None
	if (self.visible):
		if (self.markup):
			column = gtk.TreeViewColumn(self.name
					, self.cellrenderer
					, text = self.pos)
		else:
			column = gtk.TreeViewColumn(self.name
					, self.cellrenderer
					, markup = self.pos)

	return column

So if the member variable markup is set to True, then instead of getting the text from the specified position in the model, the column gets the markup from that position.

Then when the todo.Category populates the tree with it’s values in the get_coloumn_list() it returns the title with markup:

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("<b>%s</b>" % self.name)
		else:
			lst_return.append("")
	return lst_return

So pretty simple, we wrap the title in the bold tags and then it displays as bold.

Python GTD pyGTK

Conclusion

You can download the full source to this tutorial here.

So that’s it for this one, not as long or drawn out as the last one, but I think some interesting ground is covered. Again this isn’t necessarily to teach you how to do everything that I’m doing, but to provide examples of how one might do things.

If you’re ever wanting to program something that you have never done before, I always think it’s nice to see examples of how other people do it. Even if you don’t do it in the exact same manner, you will at least have an idea of how to approach the task. Something that I really couldn’t find for a few features. For example the drop-down gtk.Calendar, which, come to think of it, would make a really nice custom widget if anyone was thinking of creating one.

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.

selsine

del.icio.us del.icio.us

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

  1. Jesus
    Says:

    I will read it when i have time :P

    Thanks you a lot ;)

  2. Carlan Calazans
    Says:

    Im doing almost the same project except that im using a ORM solution with sqlite and sqlAlchemy. Its amazing, real fast and organized, make sense. Back to this, its the second time that i google for something and come to here :D

    Congrats for all the tutorials (im reading yet)!
    Thx!

  3. selsine

    selsine
    Says:

    Jesus – Thanks for the kind words again!

    Carlan – Thanks! I’m pleased that what I have written can be of use to you. I would love to take a look at your project! If your project has a website please post the link here I would really like to see what direction you are taking your project in!

  4. Carlan Calazans
    Says:

    Hi selsine! The code you can find it here ( http://code.google.com/p/carlan/ ). As the description said, its not ready for public yet, but maybe someone like me would like to look at the code ;)

  5. selsine

    selsine
    Says:

    Carlan, I’ve browsed to code a little bit but I haven’t had the chance to try it out yet, but what I’ve seen looks good.

    I was actually going to go the SQLite route with this tutorial but decided against it at the last minute so I am interesting in seeing what you come up with. I will be sure to check back on your code from time to time.

    If you near a significant release or need any beta testers drop me a line and I will be sure to write a post and try to get the link out there.

  6. redgun
    Says:

    Hi there :) Nice stuff on your site. I’m studying python and I have opened a blog also where I put python stuff. My first post is about notifications with python and dbus. I think that pylan could be extended using notifications on events.

    If someone is interested could read my post here:
    http://openposts.blogspot.com/2007/04/python-class-for-notifications-via-dbus.html

  7. mrben
    Says:

    Hi – this isn’t really specific to this tutorial, but I was wondering if you could help with something that has been bothering me for a while with Python/Gtk.

    I want to be able to open a window fullscreen on the screen of my choosing in a dual-screen setup. I can do a fullscreen window (window.set_fullscreen()) no problems, and I can find out how many monitors are plugged in (window.get_screen().get_n_monitors()), but I can’t seem to find a way to select which screen the window opens on or fullscreens to….

    One option I tried was to move the window to the very extreme of the display, and then fullscreen it. Certainly under Windows this still fullscreens on the “primary” display, which, in my case, is the left hand monitor. I’ll be trying it under Linux over the weekend.

    Is this something that gtk is able to do? Or should I be looking at gnome bindings, given that gnome handles the window manager? Are there equivalent bindings in Windows, or does cross-platform go out the window if I use gnome specific stuff? (Not a huge problem, but cross-platform would be nice)

    Hope you can help – thanks for taking the time to read, at least :)

    Ben

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

  9. Software Development Guide
    Says:

    Software Development Guide

    I couldn’t understand some parts of this article, but it sounds interesting

  10. srot
    Says:

    why when the calendar pop ups the window doesn’t react (for example when I try to close it) ? can it be fixed ?

  11. selsine

    selsine
    Says:

    Hi srot,

    For the time being you have to close the calendar first before being able to work with the dialog behind it.

  12. wheaties.box
    Says:

    Hey, thanks for this excellent tutorial!! I am currently working on a much more basic todo application with PyGTK. I’m doing it in my free time (mostly when I decide I don’t have to pay attention in class) to help myself learn both Python and PyGTK better. This tutorial has been helping me understand what seem to be Python “best practices.” So thanks for that.

    I started browsing your code this morning, and I think I found a minor problem. In pyLan.py in find_todoColumn, you are returning a todoColumn and its position in the list. The stuff with the todoColumn seems to be fine, but you declare count = 0 and simply return it. There is nothing to manipulate the value of count as you iterate through the list.

    Like I said, it’s a minor problem, but it might be a stumbling block for those of us who are newer to programming :)

    Thanks again for the tutorial!

  13. selsine

    selsine
    Says:

    Hi wheaties.box,

    Good catch! You are right there most certainly is an error in that function. Thanks I’ll have to update that. You’re probably the only person who has looked that closely at the code besides myself.

    My suggestion for your todo application is: start small. I tried to add the kitchen sink into my program and it ended up being a bit too much. I’m thinking of starting over in fact, with something a lot simpler.

  14. tasos
    Says:

    Hello,
    i see that you used some pixmaps from files in some menubar buttons (category, task) and that these are loaded from your ‘pixmaps’ folder. (perhaps to avoid using gnome support?)

    I can’t seem to be able to do the same..it only works for me if the pixmaps are in the same folder with the glade file..
    Perhaps the .gladep file has something to do with it? How did u generated this file?

    Thanx in advance

  15. Anusha
    Says:

    hi selsine,
    i am finding your tutorials as a easy path to learn glade and pygtk.The tutorials are both intresting and easy to understand.but one trouble is that i am not able to find all tutorials at one place .so it is difficult to find them too.can u send me all these tutorials(or send me the url where all links are present) so that it will be easy for me to complete my application
    Thanks for your tutorials.
    Anusha

  16. Ploum
    Says:

    Hi,

    Thanks a lot for all your tutorials, they are a really great help.

    In this particular case, I found one interesting point : you can move the window beneath the calendar widget which result in an “strange” situation with a calendar floating around. The DateEdit widget doesn’t have this problem because it is closed automatically if you click outside.

    I tried to resolve this in your example to have the same behaviour.
    1) Firstly, you have to grab the mouse pointer in the calendar widget. I did this with gtk.gdk.pointer grab without understanding the parameters because gtk.widget.grab_add doesn’t do anything (or I didn’t understood at all)
    2) Second : Closing the widget when the user clic outside. I still have no idea on how I can do that. any help would be appreciated so I can send you the patch ;-)

    Thanks again for your wonderful blog !

  17. Lawrence
    Says:

    Great tutorial…

    Let me ask a question, how to connect up 3 different dialog window with python?

    what i meant is let say i have 3 dialog windows, after the first window, i clicked next, then the second window will appear.

    thanks.

  18. sten
    Says:

    weak GTKmm?

Leave a Reply

 

Popular Posts