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:
- Showing a popup window
- Working with a gtk.Calendar widget
- Working with gtk.ComboBox widgets
- Working with Pango markup in a gtk.TreeView
- Working with gtk.CheckButton widgets
- Working with the DateTime python module.
This tutorial is organized into the following sections:
- The GUI
- The todo.Task Object
- Adding a todo.Task object
- Showing the Calendar window
- Editing todo.Category and todo.Task items
- Pango markup in the gtk.TreeView
You can download the full source to this tutorial here.
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”.
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.
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.
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)
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]): #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.
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.
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))
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.
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.
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.
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.