source: sasview/guiframe/gui_manager.py @ 914a499

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalccostrafo411magnetic_scattrelease-4.1.1release-4.1.2release-4.2.2release_4.0.1ticket-1009ticket-1094-headlessticket-1242-2d-resolutionticket-1243ticket-1249ticket885unittest-saveload
Last change on this file since 914a499 was 3e852ef5, checked in by Jae Cho <jhjcho@…>, 16 years ago

recovered the neutral ball ico

  • Property mode set to 100644
File size: 28.1 KB
Line 
1"""
2This software was developed by the University of Tennessee as part of the
3Distributed Data Analysis of Neutron Scattering Experiments (DANSE)
4project funded by the US National Science Foundation.
5
6See the license text in license.txt
7
8copyright 2008, University of Tennessee
9
10How-to build an application using guiframe:
11
12 1- Write a main application script along the lines of dummyapp.py
13 2- Write a config script along the lines of config.py, and name it local_config.py
14 3- Write your plug-ins and place them in a directory called "perspectives".
15     - Look at local_perspectives/plotting for an example of a plug-in.
16     - A plug-in should define a class called Plugin. See abstract class below.
17
18"""
19import wx
20import wx.aui
21import os, sys
22try:
23    # Try to find a local config
24    import imp
25    path = os.getcwd()
26    if(os.path.isfile("%s/%s.py" % (path, 'local_config'))) or \
27      (os.path.isfile("%s/%s.pyc" % (path, 'local_config'))):
28            fObj, path, descr = imp.find_module('local_config', [path])
29            config = imp.load_module('local_config', fObj, path, descr) 
30    else:
31        # Try simply importing local_config
32        import local_config as config
33except:
34    # Didn't find local config, load the default
35    import config
36   
37from sans.guicomm.events import EVT_STATUS
38from sans.guicomm.events import EVT_NEW_PLOT,EVT_SLICER_PARS_UPDATE
39import history
40import warnings
41warnings.simplefilter("ignore")
42
43import logging
44
45class Plugin:
46    """
47        This class defines the interface for a Plugin class
48        that can be used by the gui_manager.
49       
50        Plug-ins should be placed in a sub-directory called "perspectives".
51        For example, a plug-in called Foo should be place in "perspectives/Foo".
52        That directory contains at least two files:
53            perspectives/Foo/__init.py contains two lines:
54           
55                PLUGIN_ID = "Foo plug-in 1.0"
56                from Foo import *
57               
58            perspectives/Foo/Foo.py contains the definition of the Plugin
59            class for the Foo plug-in. The interface of that Plugin class
60            should follow the interface of the class you are looking at.
61    """
62   
63    def __init__(self):
64        """
65            Abstract class for gui_manager Plugins.
66        """
67        ## Plug-in name. It will appear on the application menu.
68        self.sub_menu = "Plugin"       
69       
70        ## Reference to the parent window. Filled by get_panels() below.
71        self.parent = None
72       
73        ## List of panels that you would like to open in AUI windows
74        #  for your plug-in. This defines your plug-in "perspective"
75        self.perspective = []
76       
77        raise RuntimeError, "gui_manager.Plugin is an abstract class"
78       
79    def populate_menu(self, id, parent):
80        """
81            Create and return the list of application menu
82            items for the plug-in.
83           
84            @param id: deprecated. Un-used.
85            @param parent: parent window
86            @return: plug-in menu
87        """
88        import wx
89        # Create a menu
90        plug_menu = wx.Menu()
91
92        # Always get event IDs from wx
93        id = wx.NewId()
94       
95        # Fill your menu
96        plug_menu.Append(id, '&Do something')
97        wx.EVT_MENU(owner, id, self._on_do_something)
98   
99        # Returns the menu and a name for it.
100        return [(id, plug_menu, "name of the application menu")]
101   
102   
103    def get_panels(self, parent):
104        """
105            Create and return the list of wx.Panels for your plug-in.
106            Define the plug-in perspective.
107           
108            Panels should inherit from DefaultPanel defined below,
109            or should present the same interface. They must define
110            "window_caption" and "window_name".
111           
112            @param parent: parent window
113            @return: list of panels
114        """
115        ## Save a reference to the parent
116        self.parent = parent
117       
118        # Define a panel
119        mypanel = DefaultPanel(self.parent, -1)
120       
121        # If needed, add its name to the perspective list
122        self.perspective.append(self.control_panel.window_name)
123
124        # Return the list of panels
125        return [mypanel]
126   
127    def get_context_menu(self, graph=None):
128        """
129            This method is optional.
130       
131            When the context menu of a plot is rendered, the
132            get_context_menu method will be called to give you a
133            chance to add a menu item to the context menu.
134           
135            A ref to a Graph object is passed so that you can
136            investigate the plot content and decide whether you
137            need to add items to the context menu. 
138           
139            This method returns a list of menu items.
140            Each item is itself a list defining the text to
141            appear in the menu, a tool-tip help text, and a
142            call-back method.
143           
144            @param graph: the Graph object to which we attach the context menu
145            @return: a list of menu items with call-back function
146        """
147        return [["Menu text", 
148                 "Tool-tip help text", 
149                 self._on_context_do_something]]     
150   
151    def get_perspective(self):
152        """
153            Get the list of panel names for this perspective
154        """
155        return self.perspective
156   
157    def on_perspective(self, event):
158        """
159            Call back function for the perspective menu item.
160            We notify the parent window that the perspective
161            has changed.
162            @param event: menu event
163        """
164        self.parent.set_perspective(self.perspective)
165   
166    def post_init(self):
167        """
168            Post initialization call back to close the loose ends
169        """
170        pass
171
172
173class ViewerFrame(wx.Frame):
174    """
175        Main application frame
176    """
177    def __init__(self, parent, id, title, window_height=700, window_width=900):
178    #def __init__(self, parent, id, title, window_height=800, window_width=800):
179        """
180            Initialize the Frame object
181        """
182        from local_perspectives.plotting import plotting
183        #wx.Frame.__init__(self, parent, id, title, wx.DefaultPosition, size=(800, 700))
184        wx.Frame.__init__(self, parent, id, title, wx.DefaultPosition, size=(900, 600))
185       
186        # Preferred window size
187        self._window_height = window_height
188        self._window_width  = window_width
189       
190        # Logging info
191        logging.basicConfig(level=logging.DEBUG,
192                    format='%(asctime)s %(levelname)s %(message)s',
193                    filename='sans_app.log',
194                    filemode='w')       
195       
196        path = os.path.dirname(__file__)
197        ico_file = os.path.join(path,'images/ball.ico')
198        if os.path.isfile(ico_file):
199            self.SetIcon(wx.Icon(ico_file, wx.BITMAP_TYPE_ICO))
200        else:
201            ico_file = os.path.join(os.getcwd(),'images/ball.ico')
202            if os.path.isfile(ico_file):
203                self.SetIcon(wx.Icon(ico_file, wx.BITMAP_TYPE_ICO))
204       
205        ## Application manager
206        self.app_manager = None
207       
208        ## Find plug-ins
209        # Modify this so that we can specify the directory to look into
210        self.plugins = self._find_plugins()
211        self.plugins.append(plotting.Plugin())
212
213        ## List of panels
214        self.panels = {}
215
216        ## Next available ID for wx gui events
217        #TODO:  No longer used - remove all calls to this
218        self.next_id = 20000
219
220        # Default locations
221        self._default_save_location = os.getcwd()       
222
223        ## Default welcome panel
224        self.defaultPanel    = DefaultPanel(self, -1, style=wx.RAISED_BORDER)
225       
226       
227        # Register the close event so it calls our own method
228        wx.EVT_CLOSE(self, self._onClose)
229        # Register to status events
230        self.Bind(EVT_STATUS, self._on_status_event)
231        self.Bind(EVT_SLICER_PARS_UPDATE, self._onEVT_SLICER_PANEL)
232       
233       
234    def _onEVT_SLICER_PANEL(self, event):
235        """
236            receive and event telling to update a panel with a name starting with
237            event.panel_name. this method update slicer panel for a given interactor.
238            @param event: contains type of slicer , paramaters for updating the panel
239            and panel_name to find the slicer 's panel concerned.
240        """
241        for item in self.panels:
242            if self.panels[item].window_caption.startswith(event.panel_name): 
243                self.panels[item].set_slicer(event.type, event.params)
244                self._mgr.Update()
245                break
246       
247    def build_gui(self):
248        # Set up the layout
249        self._setup_layout()
250       
251        # Set up the menu
252        self._setup_menus()
253       
254        self.Fit()
255       
256        #self._check_update(None)
257             
258    def _setup_layout(self):
259        """
260            Set up the layout
261        """
262        # Status bar
263        from statusbar import MyStatusBar
264        self.sb = MyStatusBar(self,wx.ID_ANY)
265        self.SetStatusBar(self.sb)
266
267        # Add panel
268        self._mgr = wx.aui.AuiManager(self)
269       
270        # Load panels
271        self._load_panels()
272       
273        self._mgr.Update()
274
275    def add_perspective(self, plugin):
276        """
277            Add a perspective if it doesn't already
278            exist.
279        """
280        is_loaded = False
281        for item in self.plugins:
282             if plugin.__class__==item.__class__:
283                 print "Plugin %s already loaded" % plugin.__class__.__name__
284                 is_loaded = True
285                 
286        if not is_loaded:
287            self.plugins.append(plugin)
288     
289    def _find_plugins(self, dir="perspectives"):
290        """
291            Find available perspective plug-ins
292            @param dir: directory in which to look for plug-ins
293            @return: list of plug-ins
294        """
295        import imp
296        #print "Looking for plug-ins in %s" % dir
297        # List of plug-in objects
298       
299        #path_exe = os.getcwd()
300        #path_plugs = os.path.join(path_exe, dir)
301        f = open("load.log",'w') 
302        f.write(os.getcwd()+'\n\n')
303        #f.write(str(os.listdir(dir))+'\n')
304       
305       
306        plugins = []
307        # Go through files in panels directory
308        try:
309            list = os.listdir(dir)
310            for item in list:
311                toks = os.path.splitext(os.path.basename(item))
312                name = None
313                if not toks[0] == '__init__':
314                   
315                    if toks[1]=='.py' or toks[1]=='':
316                        name = toks[0]
317               
318                    path = [os.path.abspath(dir)]
319                    file = None
320                    try:
321                        if toks[1]=='':
322                            f.write("trying to import \n")
323                            mod_path = '.'.join([dir, name])
324                            f.write("mod_path= %s\n" % mod_path)
325                            module = __import__(mod_path, globals(), locals(), [name])
326                            f.write(str(module)+'\n')
327                        else:
328                            (file, path, info) = imp.find_module(name, path)
329                            module = imp.load_module( name, file, item, info )
330                        if hasattr(module, "PLUGIN_ID"):
331                            try:
332                                plugins.append(module.Plugin())
333                                print "Found plug-in: %s" % module.PLUGIN_ID
334                            except:
335                                config.printEVT("Error accessing PluginPanel in %s\n  %s" % (name, sys.exc_value))
336                       
337                    except:
338                        print sys.exc_value
339                        f.write(str(sys.exc_value)+'\n')
340                    finally:
341                        if not file==None:
342                            file.close()
343        except:
344            # Should raise and catch at a higher level and display error on status bar
345            pass   
346        f.write(str(plugins)+'\n')
347        f.close()
348        return plugins
349   
350       
351     
352    def _load_panels(self):
353        """
354            Load all panels in the panels directory
355        """
356       
357        # Look for plug-in panels
358        panels = []       
359        for item in self.plugins:
360            if hasattr(item, "get_panels"):
361                ps = item.get_panels(self)
362                panels.extend(ps)
363
364        # Show a default panel with some help information
365        # It also sets the size of the application windows
366        self.panels["default"] = self.defaultPanel
367       
368        self._mgr.AddPane(self.defaultPanel, wx.aui.AuiPaneInfo().
369                              Name("default").
370                              CenterPane().
371                              # This is where we set the size of the application window
372                              BestSize(wx.Size(self._window_width, self._window_height)).
373                              MinSize(wx.Size(self._window_width, self._window_height)).
374                              Show())
375     
376
377        # Add the panels to the AUI manager
378        for panel_class in panels:
379            p = panel_class
380            id = wx.NewId()
381           
382            # Check whether we need to put this panel
383            # in the center pane
384            if hasattr(p, "CENTER_PANE"):
385                if p.CENTER_PANE:
386                    self.panels[str(id)] = p
387                    self._mgr.AddPane(p, wx.aui.AuiPaneInfo().
388                                          Name(p.window_name).Caption(p.window_caption).
389                                          CenterPane().
390                                          BestSize(wx.Size(600,600)).
391                                          MinSize(wx.Size(400,400)).
392                                          Hide())
393               
394            else:
395                self.panels[str(id)] = p
396                self._mgr.AddPane(p, wx.aui.AuiPaneInfo().
397                                  Name(p.window_name).Caption(p.window_caption).
398                                  Right().
399                                  Dock().
400                                  TopDockable().
401                                  BottomDockable().
402                                  LeftDockable().
403                                  RightDockable().
404                                  MinimizeButton().
405                                  Hide().
406                                  BestSize(wx.Size(600,600)).
407                                  MinSize(wx.Size(500,500)))
408                               
409
410               
411       
412    def get_context_menu(self, graph=None):
413        """
414            Get the context menu items made available
415            by the different plug-ins.
416            This function is used by the plotting module
417        """
418        menu_list = []
419        for item in self.plugins:
420            if hasattr(item, "get_context_menu"):
421                menu_list.extend(item.get_context_menu(graph))
422           
423        return menu_list
424       
425    def popup_panel(self, p):
426        """
427            Add a panel object to the AUI manager
428            @param p: panel object to add to the AUI manager
429            @return: ID of the event associated with the new panel [int]
430        """
431       
432        ID = wx.NewId()
433        self.panels[str(ID)] = p
434       
435        count = 0
436        for item in self.panels:
437            if self.panels[item].window_name.startswith(p.window_name): 
438                count += 1
439       
440        windowname = p.window_name
441        caption = p.window_caption
442       
443        if count>0:
444            windowname += str(count+1)
445            caption += (' '+str(count))
446         
447        p.window_name = windowname
448        p.window_caption = caption
449           
450        self._mgr.AddPane(p, wx.aui.AuiPaneInfo().
451                          Name(windowname).Caption(caption).
452                          Floatable().
453                          #Float().
454                          Right().
455                          Dock().
456                          TopDockable().
457                          BottomDockable().
458                          LeftDockable().
459                          RightDockable().
460                          MinimizeButton().
461                          #Hide().
462                          #Show().
463                          BestSize(wx.Size(600,600)).
464                          MinSize(wx.Size(500,500)))
465                          #BestSize(wx.Size(400,400)).
466                          #MinSize(wx.Size(350,350)))
467        pane = self._mgr.GetPane(windowname)
468        self._mgr.MaximizePane(pane)
469        self._mgr.RestoreMaximizedPane()
470       
471       
472        # Register for showing/hiding the panel
473       
474        wx.EVT_MENU(self, ID, self._on_view)
475       
476        self._mgr.Update()
477        return ID
478       
479    def _setup_menus(self):
480        """
481            Set up the application menus
482        """
483        # Menu
484        menubar = wx.MenuBar()
485       
486        # File menu
487        filemenu = wx.Menu()
488       
489        id = wx.NewId()
490        filemenu.Append(id, '&Open', 'Open a file')
491        wx.EVT_MENU(self, id, self._on_open)
492   
493        id = wx.NewId()
494        filemenu.Append(id,'&Quit', 'Exit') 
495        wx.EVT_MENU(self, id, self.Close)
496       
497        # Add sub menus
498        menubar.Append(filemenu,  '&File')
499       
500        # Plot menu
501        # Attach a menu item for each panel in our
502        # panel list that also appears in a plug-in.
503        # TODO: clean this up. We should just identify
504        # plug-in panels and add them all.
505       
506        # Only add the panel menu if there is more than one panel
507        n_panels = 0
508        for plug in self.plugins:
509            pers = plug.get_perspective()
510            if len(pers)>0:
511                n_panels += 1
512       
513        if n_panels>1:
514            viewmenu = wx.Menu()
515            for plug in self.plugins:
516                plugmenu = wx.Menu()
517                pers = plug.get_perspective()
518                if len(pers)>0:
519                    for item in self.panels:
520                        if item == 'default':
521                            continue
522                        panel = self.panels[item]
523                        if panel.window_name in pers:
524                            plugmenu.Append(int(item), panel.window_caption, "Show %s window" % panel.window_caption)
525                           
526                           
527                           
528                            wx.EVT_MENU(self, int(item), self._on_view)
529                   
530                    viewmenu.AppendMenu(wx.NewId(), plug.sub_menu, plugmenu, plug.sub_menu)
531               
532            menubar.Append(viewmenu, '&Panel')
533
534        # Perspective
535        # Attach a menu item for each defined perspective.
536        # Only add the perspective menu if there are more than one perspectves
537        n_perspectives = 0
538        for plug in self.plugins:
539            if len(plug.get_perspective()) > 0:
540                n_perspectives += 1
541       
542        if n_perspectives>1:
543            p_menu = wx.Menu()
544            for plug in self.plugins:
545                if len(plug.get_perspective()) > 0:
546                    id = wx.NewId()
547                    p_menu.Append(id, plug.sub_menu, "Switch to %s perspective" % plug.sub_menu)
548                    wx.EVT_MENU(self, id, plug.on_perspective)
549            menubar.Append(p_menu,   '&Perspective')
550 
551        # Help menu
552        helpmenu = wx.Menu()
553
554        # Look for help item in plug-ins
555        for item in self.plugins:
556            if hasattr(item, "help"):
557                id = wx.NewId()
558                helpmenu.Append(id,'&%s help' % item.sub_menu, '')
559                wx.EVT_MENU(self, id, item.help)
560       
561        if config._do_aboutbox:
562            id = wx.NewId()
563            helpmenu.Append(id,'&About', 'Software information')
564            wx.EVT_MENU(self, id, self._onAbout)
565        id = wx.NewId()
566        helpmenu.Append(id,'&Check for update', 'Check for the latest version of %s' % config.__appname__)
567        wx.EVT_MENU(self, id, self._check_update)
568       
569       
570       
571       
572        # Look for plug-in menus
573        # Add available plug-in sub-menus.
574        for item in self.plugins:
575            if hasattr(item, "populate_menu"):
576                for (self.next_id, menu, name) in item.populate_menu(self.next_id, self):
577                    menubar.Append(menu, name)
578                   
579
580        menubar.Append(helpmenu, '&Help')
581         
582        self.SetMenuBar(menubar)
583       
584       
585       
586    def _on_status_event(self, evt):
587        """
588            Display status message
589        """
590        self.sb.clear_gauge( msg="")
591        mythread=None
592        mytype= None
593        if hasattr(evt, "curr_thread"):
594            mythread= evt.curr_thread
595        if hasattr(evt, "type"):
596            mytype= evt.type
597        self.sb.set_status( type=mytype,msg=str(evt.status),thread=mythread)
598       
599
600       
601    def _on_view(self, evt):
602        """
603            A panel was selected to be shown. If it's not already
604            shown, display it.
605            @param evt: menu event
606        """
607        self.show_panel(evt.GetId())
608
609    def show_panel(self, uid):
610        """
611            Shows the panel with the given id
612            @param uid: unique ID number of the panel to show
613        """
614        ID = str(uid)
615        config.printEVT("show_panel: %s" % ID)
616        if ID in self.panels.keys():
617            if not self._mgr.GetPane(self.panels[ID].window_name).IsShown():
618                self._mgr.GetPane(self.panels[ID].window_name).Show()
619                # Hide default panel
620                self._mgr.GetPane(self.panels["default"].window_name).Hide()
621           
622               
623            self._mgr.Update()
624   
625    def _on_open(self, event):
626   
627        from data_loader import plot_data
628        path = self.choose_file()
629        if path ==None:
630            return
631        if path and os.path.isfile(path):
632            plot_data(self, path)
633           
634       
635       
636    def _onClose(self, event):
637        import sys
638        wx.Exit()
639        sys.exit()
640                   
641    def Close(self, event=None):
642        """
643            Quit the application
644        """
645        import sys
646        wx.Frame.Close(self)
647        wx.Exit()
648        sys.exit()
649
650 
651    def _check_update(self, event=None): 
652        """
653            Check with the deployment server whether a new version
654            of the application is available
655        """
656        import urllib
657        try: 
658            h = urllib.urlopen(config.__update_URL__)
659            lines = h.readlines()
660            line = ''
661            if len(lines)>0:
662                line = lines[0]
663               
664                toks = line.lstrip().rstrip().split('.')
665                toks_current = config.__version__.split('.')
666                update_available = False
667                for i in range(len(toks)):
668                    if len(toks[i].strip())>0:
669                        if int(toks[i].strip())>int(toks_current[i]):
670                            update_available = True
671                if update_available:
672                    #print "Version %s is available" % line.rstrip().lstrip()
673                    self.SetStatusText("Version %s is available! See the Help menu to download it." % line.rstrip().lstrip())
674                    if event != None:
675                        import webbrowser
676                        webbrowser.open(config.__download_page__)
677                else:
678                    if event != None:
679                        self.SetStatusText("You have the latest version of %s" % config.__appname__)
680        except:
681            if event != None:
682                self.SetStatusText("You have the latest version of %s" % config.__appname__)
683           
684           
685    def _onAbout(self, evt):
686        """
687            Pop up the about dialog
688            @param evt: menu event
689        """
690        if config._do_aboutbox:
691            import aboutbox 
692            dialog = aboutbox.DialogAbout(None, -1, "")
693            dialog.ShowModal()
694           
695    def set_manager(self, manager):
696        """
697            Sets the application manager for this frame
698            @param manager: frame manager
699        """
700        self.app_manager = manager
701       
702    def post_init(self):
703        """
704            This initialization method is called after the GUI
705            has been created and all plug-ins loaded. It calls
706            the post_init() method of each plug-in (if it exists)
707            so that final initialization can be done.
708        """
709        for item in self.plugins:
710            if hasattr(item, "post_init"):
711                item.post_init()
712       
713    def set_perspective(self, panels):
714        """
715            Sets the perspective of the GUI.
716            Opens all the panels in the list, and closes
717            all the others.
718           
719            @param panels: list of panels
720        """
721        for item in self.panels:
722            # Check whether this is a sticky panel
723            if hasattr(self.panels[item], "ALWAYS_ON"):
724                if self.panels[item].ALWAYS_ON:
725                    continue 
726           
727            if self.panels[item].window_name in panels:
728                if not self._mgr.GetPane(self.panels[item].window_name).IsShown():
729                    self._mgr.GetPane(self.panels[item].window_name).Show()
730            else:
731                if self._mgr.GetPane(self.panels[item].window_name).IsShown():
732                    self._mgr.GetPane(self.panels[item].window_name).Hide()
733                 
734        self._mgr.Update()
735       
736    def choose_file(self):
737        """
738            Functionality that belongs elsewhere
739            Should add a hook to specify the preferred file type/extension.
740        """
741        #TODO: clean this up
742        from data_loader import choose_data_file
743        path = choose_data_file(self, self._default_save_location)
744        if not path==None:
745            try:
746                self._default_save_location = os.path.dirname(path)
747            except:
748                pass
749        return path
750   
751    def load_ascii_1D(self, path):
752        from data_loader import load_ascii_1D
753        return load_ascii_1D(path)
754                 
755class DefaultPanel(wx.Panel):
756    """
757        Defines the API for a panels to work with
758        the GUI manager
759    """
760    ## Internal nickname for the window, used by the AUI manager
761    window_name = "default"
762    ## Name to appear on the window title bar
763    window_caption = "Welcome panel"
764    ## Flag to tell the AUI manager to put this panel in the center pane
765    CENTER_PANE = True
766
767 
768# Toy application to test this Frame
769class ViewApp(wx.App):
770    def OnInit(self):
771        #from gui_manager import ViewerFrame
772        self.frame = ViewerFrame(None, -1, config.__appname__)   
773        self.frame.Show(True)
774
775        if hasattr(self.frame, 'special'):
776            print "Special?", self.frame.special.__class__.__name__
777            self.frame.special.SetCurrent()
778        self.SetTopWindow(self.frame)
779        return True
780   
781    def set_manager(self, manager):
782        """
783            Sets a reference to the application manager
784            of the GUI manager (Frame)
785        """
786        self.frame.set_manager(manager)
787       
788    def build_gui(self):
789        """
790            Build the GUI
791        """
792        self.frame.build_gui()
793        self.frame.post_init()
794       
795    def add_perspective(self, perspective):
796        """
797            Manually add a perspective to the application GUI
798        """
799        self.frame.add_perspective(perspective)
800       
801
802if __name__ == "__main__": 
803    app = ViewApp(0)
804    app.MainLoop()             
Note: See TracBrowser for help on using the repository browser.