| Author: | Steven Zatz |
|---|---|
| Contact: | slzatz@hotmail.com |
| Date: | 2003/12/24 |
| Status: | This is a "work in progress" |
| Revision: | 1.02 |
| Copyright: | Application and documentation use the Python license which is compatible with the GPL. |
This is experimental documentation of a program called ListManager, written in Python and wxPython using Leo to create both the application code and the associated reST documentation.
ListManager is an application that allows a group of people working on a joint project to maintain a common list of todos and related items that have owners, due dates and associated notes. The application uses mysql as its database for group use and also uses sqlite for locally resident databases for personal lists. It works in conjunction with Outlook to allow email messages to be sent to ListManager for inclusion in lists and uses Outlook to mail messages to users.
code:
comments:
Nothing unusual in what follows: we start with the module imports, setting some global constants including Menu Ids and read the ListManager.ini file.
code:
comments:
code:
comments:
The following two global constants are needed to create emails through Outlook via COM:
olMailItem = 0x0 olFlagMarked = 0x2
For some reason, it seemed easier to just include them explicitly rather than worrying about generating all the Outlook constants in order to use early binding. I supppose if MSFT changes the api, that would be a problem.
code:
comments:
Application uses the ConfigParser module ito parse the ini file. Unfortunately, ConfigParser doesn't work exactly like I think it should although it has been improved in 2.3. My main issue is in the handling of default options. The default options specified through the constructor show up in every section. For example, if you use the items(section) method then in addition to returning a list of tuples with whatever option/value pairs exist in the section, the list will include all the default option/value pairs, which does not make a whole lot of sense to me. At the least, there should be a 'nodefaults' argument whose default was False but which could be set to True. The following methods should have this option:
In any event, because a nodefaults option does not exist, I create the ConfigParser object twice -- once with default options and once without them.
The application will work fine if there is no ini file. In an effort to save some typing but not be too obscure, many of the options are read such that they default to the correct value either through explicit defaults in the constructor or statements that evaluate to None or False.
QUICK_LIST = cp.has_option('User','quicklist') and cp.get('User','quicklist') or None
OUTLOOK = cp.has_option('Mail','outlook') and cp.getboolean('Mail,'outlook')
code:
comments:
ListManager is the main class in the application and is a sublass of wxFrame, which is typical for a wxPython application. From a GUI standpoint, the main child window of the ListManager object is a wxNoteBook object that holds one wxListCtrl per notebook page and one wxListBox. The wxListCtrls display item information (e.g., name of the item, owners of the item, etc.) for a particular List and the wxListBoxes displays a list of owners that is used to filter the items displayed by the wxListCtrl object.
Each wxListCtrl object has its own set of events that it is hooked to (see CreateNewNotebookPage`<< ListControl Events >>`_.
comments:
code:
comments:
The ListManager __init__ method is pretty straightforward. The __init__ arguments are the ones that need to be passed to wxFrame __init__ method. The wxFrame class has the following form:
wxFrame(parent, id, title, pos=wxDefaultPosition, size=wxDefaultSize, style=wxDEFAULT_FRAME_STYLE, name="frame")
The default style (wxDEFAULT_FRAME_STYLE) includes wxMINIMIZE_BOX, wxMAXIMIZE_BOX, wxRESIZE_BORDER, wxSYSTEM_MENU, wxCAPTION (the latter is the text that appears in the title bar).
SetIcon is a method of wxFrame that sets the icon in the upper left of the title bar of the frame. The wxIcon class has the following form:
wxIcon(filename, type, desiredWidth=-1, desiredHeight=-1)
CreateStatusBar is a method of wxFrame. The wxPython form is:
CreateStatusBar(number=1, style=0, id=-1)
CreateStatusBar needs to be called before << Load Recent Files >>.
The various sections of __init__ are explained in their corresponding section:
<< ListManager Attributes >> << Menu Setup >> << Toolbar Setup >> << Menu/Toolbar Events >> << Create Controls>> << Layout Stuff >> << Other Events >> << GUI Instance Objects >> << Create Socket >> << Load Recent Files >>
code:
comments:
The class Item is just an empty class being used as a convenience to hold item attributes:
class Item:
pass
The purpose of the class is just to create an object that can have various attributes as follows:
| item.id | GUID |
| item.name | string that describes the item |
| item.priority | integer ranging from 1 (high) to 3 (low) |
| item.owners | list of the form ["Zatz, Steve", "Hoffman, Steve"] |
| item.note | string that provides additional info on item |
| item.timestamp | timestamp indicating when an item was last modified |
| item.duedate | default is None; mx.DateTime date |
| item.createdate | mx.DateTime.now() mx.DateTime timestamp |
| item.finisheddate | efaut is None; mx.DateTime date |
The wxPython constructor for a wxListBox is:
wxListBox(parent, id, pos=wxDefaultPosition, size=wxDefaultSize, choices=[], style=0)
The following lines set the default printer data:
self.printdata = wxPrintData() self.printdata.SetPaperId(wxPAPER_LETTER) self.printdata.SetOrientation(wxPORTRAIT)
The wxPython class wxPrintData holds a variety of information related to printers and printer device contexts. This class is used to create a wxPrinterDC and a wxPostScriptDC. It is also used as a data member of wxPrintDialogData and wxPageSetupDialogData, as part of the mechanism for transferring data between the print dialogs and the application.
dictionary that contains the information concerning whether any of several elements have been changed. Chose a dictionary more to test the idea that I could create a simple method that would update the dictionary and here is an example:
EVT_TEXT(self, self.name.GetId(), lambda e: self.modified.update({'name':1}))
So this lambda function means that if an EVT_TEXT event occurs then update the dictionary by adding the key to the dictionary (the value is not used and arbitrarily set to 1). The wxPython form for the macro EVT_TEXT is:
EVT_TEXT(window, id, func)
A wxEVT_COMMAND_TEXT_UPDATED event is generated when the text in a wxTextCtrl changes and that is what EVT_TEXT catches. Note that this event will always be sent when the text control’s content changes - whether this is due to user input or comes programmatically (for example, if SetValue() is called)
list that holds the dictionaries that describe the notes that are edited by the external text editor:
[
{
'table': 'mine',
'host': 'wxLMDB:sqlite',
'path': 'C:\\DOCUME~1\\STEVEN~1\\LOCALS~1\\Temp\\Journal Scan schedule.txt',
'id': '1AB34FB9-9EE6-4AFC-8AF0-FFCA50103BF3',
'time': 1070850894
},
{
'table': 'factoids',
'host': 'wxLMDB:sqlite',
'path': 'C:\\DOCUME~1\\STEVEN~1\\LOCALS~1\\Temp\\How many cme programs are sponsored- - 91%.txt',
'id': '9CAC4D18-DE1C-4535-B9A5-4CDB1AD3F304',
'time': 1070850908
}
]
The method that uses self.editor is << Check if Edited File has Changed >>.
There is a wxPanel in the AddListControl method so each wxListCtrl has a different panel as parent.
There is a nb_sizer = wxNotebookSizer(nb) class but doesn't seem to make any difference.
The wxPython wxFont constructor is:
wxFont(pointSize, family, style, weight, underline=False, faceName="", wencoding=wxFONTENCODING_DEFAULT)
The following lines construct the Find Dialog and the Dialog that catches errors and shows expressions for debugging:
self.FindDialog = FindDialog(self, "Find...", "") self.EvalDialog = EvalDialog(self, "Evaluate...", "")
code:
comments:

![]() |
Creates a new List | self.OnNewList |
![]() |
Open an existing List | self.OnOpenList |
![]() |
lambda e: self.OnPrint(e,showprtdlg=False)) | |
![]() |
Print Preview | lambda e: self.OnPrint(e, prev=True)) |
![]() |
Page Setup | self.OnPageSetup |
![]() |
New Item | self.OnNewItem |
![]() |
Refresh | self.OnRefresh |
![]() |
Edit Note | self.OnEditNote |
![]() |
Find | self.OnFind |
![]() |
Cut | lambda e: self.OnCopyItems(e, cut=True)) |
![]() |
Copy | self.OnCopyItems |
![]() |
Paste | self.OnPasteItems |
![]() |
Toggle Finished | self.OnToggleFinished |
![]() |
Delete Item | self.OnDeleteItems |
![]() |
Set Item Due Date | self.OnDueDate |
![]() |
Set Item Owners | self.OnEditOwner |
![]() |
Mail Item | self.OnMailItem |
code:
comments:
The wxFrame has two wxPanels: upper_panel will contain the notebook. The bottom_panel will contain the various item textctrls including name, owners and note.
code:
comments:
The EVT_TEXT event macros indicate whether a particular textctrl has changed.
EVT_CLOSE(self, self.OnWindowExit) is used to record settings and cleanup on exiting
EVT_IDLE(self, self.OnIdle) --> Idle events used for checking text files and transfers from Outlook
There are also a number of events related to the individual ListCtrls that are placed on Notebook pages << ListControl Events >>.
code:
comments:
The parent of the wxPanel object upper_panel is the ListManager, which is a subclass of wxFrame. The parent of wxNotebook object nb is upper_panel. Since the only child of upper_panel is the nb it wasn't obvious to me that a sizer was needed but apparently without it the wxListCtrl that will be a child of the wxPanel of nb won't size right if we don't do it this was.
Frame ---> upper_panel ---> notebook ---> panel (for each page) ---> listctrl | | } one on each page ---> lower_panel ---> variety of textctrls ---> listbox
code:
comments:
No comments yet.
code:
comments:
No comment
code:
comments:
pathlist = [f[1] for f in cp.items('Files')]
This uses the new in 2.3 ConfigParser method items. This will not work unless ConfigParser has been constructed without any defaults and so a ConfigParser object is created twice.
The only remotely subtle thing here is that we don't want to execute the EVT_NOTEBOOK_PAGE_CHANGED statement while we're doing the initial loading of files since it does unnecessary processing. The statement is executed before the last file is loaded.
code:
comments:
Need to figure out exactly what this timer is doing.
comments:
No comment.
code:
comments:
This method grabs the owners from many of the tables to create a list of possile owners for each item. The alternative is actually to create a separate owner table but it seemed to make sense to just construct the owners on the fly from the various List databases. This is done in a thread so no matter how long it takes to construct the owners it doesn't slow the appearance of the GUI. The result of this method is the contruction of the instance variable self._list.
The most interesting thing here is creating a custom event (without needing to create an event macro) to signal that this thread is done:
evt = wxPyEvent() evt_id = wxNewEventType() evt.SetEventType(evt_id) self.Connect(-1, -1, evt_id, self.createownerdialog) wxPostEvent(self, evt)
The code above is adapted from the more general wxCallAfter, which I could have used but just wanted to explicitly show the steps involved in creating a custom event, associating it with a callback and posting it.
The general point is that if you want to notify the main GUI thread of something going on in a non-GUI thread, posting events is an easy way to do it whether you use the code above, the more complete wxCallAfter (see below) from which it was derived or actually create your own custom event macro (farther below).
The code for wxCallAfter is:
def wxCallAfter(callable, *args, **kw):
"""
Call the specified function after the current and pending event
handlers have been completed. This is also good for making GUI
method calls from non-GUI threads.
"""
app = wxGetApp()
assert app, 'No wxApp created yet'
global _wxCallAfterId
if _wxCallAfterId is None:
_wxCallAfterId = wxNewEventType()
app.Connect(-1, -1, _wxCallAfterId,
lambda event: event.callable(*event.args, **event.kw) )
evt = wxPyEvent()
evt.SetEventType(_wxCallAfterId)
evt.callable = callable
evt.args = args
evt.kw = kw
wxPostEvent(app, evt)
Unless you want multiple handlers to be able to respond to a custom event (by using evt.Skip()) or just want custom event macros that are like native event macros there doesn't seem to be much need to create full-blown custom events. If you do need to, here is how it is done:
wxEVT_THREAD_DONE = wxNewEventType()
def EVT_THREAD_DONE(win, func):
win.Connect(-1, -1, wxEVT_THREAD_DONE, func)
class ThreadDoneEvent(wxPyEvent):
def __init__(self):
wxPyEvent.__init__(self)
self.SetEventType(wxEVT_THREAD_DONE)
When you want to post the custom event, you do the following:
evt = ThreadDoneEvent() wxPostEvent(win, evt)
code:
comments:
When the thread is done that creates the owner list it posts an event whose callback is this method. This method uses self._list that was generated by the createownerlist method. As an alternative, we could probably pass the list as an attribute of the event that is generated in the thread.
comments:
The main method here is the one that constructs a new Notebook page by creating a new ListCtrl and new OwnerListBox and populating them. The second method does what is needed when an existing notebook page is selected.
code:
comments:
This method creates the ListCtrl and ListBox that appears on every notebook page.
Each list has a properties dictionary associated with it.
| 'owner' | The owner that is filtering the display or '*ALL' |
| 'LCdate' | Date that is displayed by the ListCtrl; values: 'duedate', 'createdate', 'timestamp', 'finisheddate' |
| 'sort' | Value is a dictionary of the form {'attribute':'priority','direction':0} |
| 'showfinished' | Values: -1 show them all; 0 show none; integer show for that many days |
| 'table' | The table that holds the List |
| 'host' | The form for this is 'nycpsszatzsql:mysql' or 'wxLMDB:sqlite' |
code:
comments:
The list is sorted by the ListBox control.
mysql doesn't like '%s' while sqlite is fine with '%s' for table names.
If you don't do OLBox.Clear() then you get a blank line in the list that must be from initiating it with "".
Relying on the fact that '*All' should be first alphabetically, which is dumb so should change it.
code:
comments:
It would seem that the two mouse events: EVT_LEFT_DOWN(LCtrl, self.OnLeftDown) and EVT_LEFT_DCLICK(LCtrl, self.OnLeftDown) could be owned by the ListManager object and not each ListCtrl object, but when I tried this, the mouse events were not detected. I did not investigate this for long so maybe I was just screwing things up or perhaps a wxFrame cannot detect a mousedown event (does that make sense?). In any event (no pun intended), it is certainly not a big deal to create these mousedown events for each ListCtrl.
Each ListBox object has one event associated with it that occurs, not surprisingly, when a name in the control is selected. The callback, self.OnFilterOwners, causes the ListCtrl to display only the items of the selected owner.
code:
comments:
Call-back for the EVT_NOTEBOOK_PAGE_CHANGED(self,nb.GetId(),self.OnPageChange) event.
self.modified is the dictionary that indicates which textctrls have data that has changed.
Note
Not sure whether event.Skip() is needed or not.code:
comments:
code:
comments:
code:
code:
code:
code:
code:
code:
code:
code:
code:
code:
code:
code:
code: