r8 - 08 Nov 2004 - 10:16:46 - DuckySherwoodYou are here: OSAF >  Projects Web  >  ChandlerHome > DeveloperDocumentation > MrMenusTutorial

Building a Viewer Parcel

Chandler Tutorial, Part 1: Building MrMenus

In this tutorial, we'll look at how to create basic elements of a simple viewer parcel named MrMenus?. MrMenus? isn't representative of a useful application, but he does allow us to demonstrate key concepts using a brief amount of code--specifically, we'll look at how to create a parcel, then give it some simple viewer UI, a new menu, and custom items in an existing Chandler menu. We'll also show how data persists while the parcel isn't running, and how user events set the persistent data model and update the view or user interface.

Viewer Parcel Basics

In Chandler, a parcel is a generalized plug-in module. This could be a spelling checker, interactive agent, data schema, email client, or any other chunk of code that extends Chandler. The type of parcel we're going to build here is a viewer parcel--an area of the user-interface where users view Information items. These are any user-centric data, such as emails, documents, addresses, or anything else users work with directly. We'll work more with items in later tutorials, but for now, we'll demonstrate some key concepts using interface data.

  1. Get familiar with Python and WxWindows

    Chandler parcels are written in wxPython (Python + wxWindows), which you should be familiar with to follow this tutorial. Python, because of its interpretive nature, is easier to work with than static languages like C++ or Java, and it's powerful enough that you can still do everything you want. The wxWindows cross-platform toolkit allows writing the majority of your user interface code only once then deploying it on Windows, Macintosh, and Linux. It also does a good job of using each platform's native widgets. This is important to us, as we've noticed that most, if not all best-of-breed applications have interfaces that fit the native platform.

  2. Use a good debugger!

    Here's a link to information on some of my favorites:

    http://wiki.osafoundation.org/bin/view/Chandler/DebuggingChandler

  3. Download and Run MrMenus?

    Download the source code and sample files we'll be referring to in this tutorial from http://wiki.osafoundation.org/bin/view/Chandler/GettingChandlerSource#cvs. When you fire up MrMenus?, you'll notice:

    A radio button array with options named Lunch and Dinner.

    A MrMenus? parcel menu containing two corresponding menu items, one of which has a check mark next to it, according to the selection in the radio button array.

    Two custom menu items in Chandler's Edit menu: one whose label toggles between two different names, and one that's enabled or disabled--all based on the current radio selection.

Set up the directory structure

The directory structure for your viewer parcel must contain:

  • A subdirectory structure for your parcels

  • A Python
    __init__.py
    file at each level of the hierarchy

  • A Python module (ParcelName.py file)

  • An XRC resources file (ParcelName.xrc file), whose name matches that of the ParcelName.py file.

    Take Home Point: Throughout this example, ParcelName is a placeholder for text representing your parcel (in our case, MrMenus?), and in many cases, this text must be the same. For starters, the prefixes (file names minus extension) of your parcel's ParcelName.py and ParcelName.xrc files must be the same text.

  1. Create your parcel's directory

    A parcel is a Python package, which in Chandler lives in the parcels/ directory, the path to which is shown below. Within that, put your parcels in their own subdirectory. You can make as deep a directory structure as you want.

       osaf/chandler/Chandler/parcels/YourParcels/ParcelName/
    

    All our parcels are in an OSAF folder, so our path looks like this:

       osaf/chandler/Chandler/parcels/OSAF/MrMenus/
    

    Take Home Point: Putting your parcel's directory (ParcelName) in a subdirectory separate from everyone else's (YourParcels) avoids conflict between parcels with the same name, say, if you're building a Calendar parcel in addition to ours.

  2. Add a Python "init" file to each level of the hierarchy

    Every directory under parcels/ has to contain a

    __init__.py
    file. That's how Python recognizes Python packages. In our case, there's one in parcels/, OSAF/, MrMenus/. Note that at higher levels of the hierarchy (parcels/ and OSAF/, this file doesn't have to have anything in it.

       parcels/
          __init__.py
          OSAF/
             __init__.py
    
             MrMenus/
                __init__.py
    

  3. Define your parcel class

    Now it's time to define your parcel class, and tell Chandler that it's in your Python module. You do this in the

    __init__.py
    file at the level of your actual parcel (in our case, the MrMenus/ directory). This "init" file gets executed when the parcel is loaded and must contain at least the following:

        parcelClass = "ParcelName.ParcelClass"
    

    where ParcelName matches the filename-minus-extension of the Python module file (ParcelName.py) and ParcelClass is the name of the parcel class you're creating, which you'll be defining in ParcelName.py. In our case, the definition is: parcelClass="MrMenus.MrMenus" where the first MrMenus identifies our Python module (MrMenus.py) and the second MrMenus identifies the class we're about to create in that module.

    Take Home Point:ParcelName in the parcelClass definition must match ParcelName in your ParcelName.py file name.

Build the Python Module

Viewer parcels follow an MVC (Model, Viewer, Controller) architecture, which, not surprisingly, consists of a:

  • Model--an object that contains data, which in Chandler persists even when the parcel isn't running

  • Viewer--an object that displays the model, which in Chandler is not persistent

  • Controller--code that ties all of these things together

Here, we'll show you MrMenus?' model and viewer objects (he's simple enough that he doesn't have any separate controller code). For simplicity, the model and viewer objects both reside in the MrMenus.py Python module.

While reading this section, look at the code discussed in parcels/OSAF/MrMenus/MrMenus.py.

  1. Create the Python module file

    This is the ParcelName.py file. As we said before, the stem, ParcelName must be the same text as ParcelName in the "init" file's parcel class definition. In our case, the module name is MrMenus.py. It goes in the same directory as the "init" file with the class definition.

  2. Include relevant imports

    As with any Python project, you have to import the relevant pieces for your project. For this example, you need wxPython, the ViewerParcel?, the application object, and a splash screen:

    from wxPython.wx import *
    from application.ViewerParcel import *
    from application.Application import app
    from application.SplashScreen import SplashScreen
    

  3. Create the Model object (MrMenus?)

    The model object, MrMenus?, is a subclass of the ViewerParcel? class. We override the superclass's Python __init__ to initialize data that should persist across runs of the application. Most parcels need to remember some user interface state so the user's world is the same when they return to it as it was when they left (we think best of breed applications should work this way). Our example includes a radio button array, whose selection will be remembered across runs of the application. For now, we initialize the selection to zero. Notice that before initializing the data, we must call the parcel superclass's __init__.

    class MrMenus(ViewerParcel):
    
      def __init__(self):
      ViewerParcel.__init__(self)
      self.radioSelection = 0
    

    Note that this value gets set here only once--the very first time the parcel is loaded. After that, the last user setting will persist and be loaded automatically each time the parcel is run.

    Take Home Point: Before doing anything else, you must call your superclass's __init__

  4. Create the ViewerObject? (wxMrMenus)

    This object is the wxWindows viewer counterpart to the model object and contains the user interface that displays the model. We suggest naming the object wxParcelName (notice the wxWindows convention of using the prefix wx in the object name):

            class wxMrMenus(wxViewerParcel):
    

  5. Initialize the Viewer

    Notice that general initialization for the viewer goes inside OnInit, which is called afterthe Viewer Parcel's __init__. This is where we associate menu items and radio buttons with the XRC resources file, and define user events.

    def OnInit(self):
        """
        General initialization goes here, e.g. wiring up menus, etc.
        """
        EVT_RADIOBOX(self, XRCID ('RadioBox'),self.OnRadioBox)
        etc...
    

    Take Home Point: In general, the viewer has to be initialized in OnInit, after the Viewer Parcel's __init__ gets called. This is because wxPython objects aren't completely wired up in __init__.

  6. Override Activate to synchronize UI to the model

    Unlike the model, which persists, the viewer object isn't created until it's displayed, at which point you must synchronize it to the value of the data in the model. You do this by overriding the superclass's Activate method. The system calls Activate each time the user goes to a viewer parcel, but just before the parcel is displayed, so you can synchronize the UI to the model. In our example, Activate sets MrMenus?' parcel menu and viewer UI. Notice again that you must call the superclass's Activate as the first thing you do in your override:

      def Activate(self):
    
        wxViewerParcel.Activate(self)
        self.SynchronizeParcelMenu()
        self.SynchronizeParcelViewer()
    

    Take Home Point: The system calls Activate each time the user goes to a viewer parcel. You can override Activate to synchronize UI to the model before anything gets displayed in the viewer.

  7. Using events to change UI

    The remaining methods in MrMenus.py define how events change the model and update the user interface. Within the viewer, you can access the model as self.model. The method below, for example, which is called when the user makes a radio button selection, gets the new selection, sets the model (self.model.radioSelection), and updates the current selection in the parcel menu:

      def OnRadioBox(self, event):
    
        self.model.radioSelection = self.radioBox.GetSelection()
        self.SynchronizeParcelMenu()
    

    By the way, we're assuming that you're familiar with wxPython events. If you're not, check out the wxPython documentation before continuing this tutorial.

    Take Home Point: Use self.model to access data in the model.

Create Viewer Parcel Resources

Now let's look at the wxWindows XRC file that defines the UI elements named in our Python code.

For the code discussed in this section, see parcels/OSAF/MrMenus/MrMenus.xrc.

  1. Create your XRC file

    You can create and edit the XRC file directly in a text editor. Or, use one of the many available tools that let you lay out user interface graphically and export to XRC. In either case, name the file ParcelName.xrc, where ParcelName is the same as ParcelName in both your parcel class definition (parcelClass="ParcelName.ParcelClass") and your Python module's file name (ParcelName.py). In our case, it's MrMenus.xrc. Put this file in the same directory as the Python module.

  2. Define the ViewerObject (wxMrMenus)

    Now you need to define the UI elements for the viewer to display, as a panel in the XRC file. Specify the class to associate with the panel by using the subclass tag. This subclass definition must include the path, starting under the parcel's directory, to the wxViewerObject in MrMenus.py. In the path below, OSAF.MrMenus is the directory path, the second MrMenus is the MrMenus.py file, and wxMrMenus is the wxViewerObject:

    <object class="wxPanel" subclass="OSAF.MrMenus.MrMenus.wxMrMenus" name="MrMenus">

    You can now go ahead and define the contents of the viewer--in our case, a radio button array with the options "Lunch" and "Dinner," beneath a label "Please Choose a Menu".

    Take Home Point: The subclass definition of your Viewer object in ParcelName.xrc must include the path to the wxViewerObject in ParcelName.py.

  3. Add a parcel menu

    Every parcel can have its own wxMenu added to Chandler's regular menu structure. You create this menu using name="ViewerParcelMenu". We've labeled our parcel menu "MrMenus," and created two menu items, "Lunch" and "Dinner":

    <object class="wxMenu" name="ViewerParcelMenu">
         <label>MrMenus</label>
         <object class="wxMenuItem" name="ParcelMenu0">
             <label>Lunch</label>
             <radio>1</radio>
         </object>
         <object class="wxMenuItem" name="ParcelMenu1">
             <label>Dinner</label>
             <radio>1</radio>
         </object>
    </object>
    

    Notice that for the Python module to define events and methods that act on UI elements created here, code in both places must refer to these same elements. You do this in the XRC file using name="UI_element", where UI_element (e.g., Parcel1Menu0) is also named in any Python code acting on that element. To see how this works, go back to MrMenus.py and check out how EVT_RADIOBOX calls onRadioBox to update the model and change which of the menu items created here (either ParcelMenu0 or ParcelMenu1) becomes the current selection.

    Take Home Points: name="ViewerParcelMenu" creates your parcel menu. You use other name="UI_element" values throughout the XRC file to refer to the viewer's UI elements in ParcelName.py.

  4. Insert menu items in an existing Chandler menu

    Now we'll insert two new menu items in Chandler's Edit menu--one before Cut (along with a separator), and the other at the end of the menu. Initially, these each have the same label but show two different ways to update menu state.

    First, create a menu with the same label as the existing menu. You can indicate where to insert new items using the insertbefore and insertafter XRC tags. Insert separator lines using class="separator". The only trick here is that you can't refer to a separator when inserting, but you can insert one before or after items with labels, so we insert our menu item first, followed by the separator.

    <object class="wxMenu" name="EditMenu">
       <label>Edit</label>
       <object class="wxMenuItem" name="EditMenu0">
           <label></label>
           <insertBefore>Cut</insertBefore>
       </object>
       <object class="separator">
           <insertBefore>Cut</insertBefore>
       </object>
    

    If you don't indicate where to insert an item, it automatically goes at the end of the menu, as is the case with our second menu item.

        <object class="wxMenuItem" name="EditMenu1">
            <label>Set to Dinner</label>
        </object>
    

    Remember again that you use the names of menu items here to refer to UI elements in your ParcelName.py code.

    Take Home Point: You can't refer to a separator when inserting menu items, but you can insert one before or after an item with a label.

    Did you notice that the label for EditMenu0 is initially empty? That's because we set its label in the Python code, using UPDATE_UI events, which is what we'll look at next.

  5. Update custom Edit menu items

    As you can see in the wxMrMenus class code, choosing a menu item generates an EVT_MENU event that can be associated with methods in the Python code. In the case of our custom Edit menu items, EVT_MENU events in MrMenus.py get handled by the OnEditMenu0 or OnEditMenu1 method to set the model and update the parcel menu and viewer.

    To update our Edit menu items themselves, however, we take a different approach that comes in handy when you have a lot of menu item states that become hard to keep track of. Instead of having widgets themselves set menu labels, you can wait until the menu is opened to respond to an UPDATE_UI event, as demonstrated in MrMenus.py:

    EVT_UPDATE_UI(self, XRCID('EditMenu0'), self.OnEditMenu0UpdateUI)
    EVT_UPDATE_UI(self, XRCID('EditMenu1'), self.OnEditMenu1UpdateUI)
    

    The first of these associates the update event with the OnEditMenu0UpdateUI method, which gets the current state from the model, then uses event.SetText to set EditMenu0's label to either "Set to Dinner" or "Set to Lunch", overriding the blank label defined in the XRC file.

      def OnEditMenu0UpdateUI(self, event):
    
        if self.model.radioSelection == 0: 
          event.SetText(_('Set to Dinner'))
        elif self.model.radioSelection == 1:
          event.SetText(_('Set to Lunch'))
    

    The second EVT_UPDATE_UI associates the update event with the OnEditMenu1UpdateUI method, which enables or disables EditMenu1 .

      def OnEditMenu1UpdateUI(self,event):
        event.Enable(self.model.radioSelection == 0)
    

    Take Home Point: Keeping menu logic in one place, rather than peppering the rest of your code with it, reduces the risk that you'll forget to think about updating menus when you're writing the backend and not thinking about menus at all.

What Else?

Persistence, and Three Lifespans of an Object

In Chandler, an object can have one of three different life spans, depending on the type of object.

Forever Persistent objects, such as the model object, are created only once. They then persist forever, even when the application isn't running.

Run to Quit Python objects are created when the application is started, and destroyed when the user quits. They stay around as long as the application is running, even when the user leaves the parcel and comes back.

View to View wxViewerParcel objects are created when the user displays the parcel, then completely destroyed when the viewer leaves it -- even if only to briefly look at another parcel. This prevents parcels that aren't being displayed from consuming too much memory, which is the same reason we don't load a parcel until it's needed.

Sometimes, however, you want to keep viewer data around between views of the parcel, say if your parcel opens files that you want to stay around when users leave the parcel and be there when they return. Such data can't persist in the model because Python can't pickle it, in this case because it contains pointers to memory, so you can't store opened files in the model.

To store such non-persistent data with the viewer without having it destroyed between sessions, you can override OnInitData and place it in a dictionary accessed as self.data in your code. OnInitData is called the very first time your parcel is initialized. When the user leaves the parcel, the dictionary is saved, and then the viewer is destroyed. When the user returns, self.data gets restored before the viewer is displayed.

Contributors

(wikified by DuckySherwood)


Excellent reading!

Some thoughts:

About: from wxPython.wx import *

I had the impression Robin and others are working hard to remove this "namespace pollution" from wxPython?

About: Naming the Viewer class in every parcel with the wx prefix.

Doesn't this have the potential of creating name collisions with wxPython names, especially given the import method above?

About: "Such data can't persist in the model because Python can't pickle it..."

Does this mean that adding any object that can't be pickled to the Model object, will make it fail? If so, maybe some guidance of what you can't add may be useful. I've read about nested functions and classes, as well as new style class instances using __slots__, but without __getstate__ and __setstate__.

-- NilsR - 31 Jul 2003

Edit | WYSIWYG | Attach | Printable | Raw View | Backlinks: Web, All Webs | History: r8 < r7 < r6 < r5 < r4 | More topic actions
 
Open Source Applications Foundation
Except where otherwise noted, this site and its content are licensed by OSAF under an Creative Commons License, Attribution Only 3.0.
See list of page contributors for attributions.