source: sasview/guiframe/gui_manager.py @ f4e3df3

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 f4e3df3 was a0d56d5, checked in by Mathieu Doucet <doucetm@…>, 16 years ago

Added "check for update" call

  • Property mode set to 100644
File size: 28.3 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
39
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        # Check for update
227        self._check_update(None)
228       
229        # Register the close event so it calls our own method
230        wx.EVT_CLOSE(self, self._onClose)
231        # Register to status events
232        self.Bind(EVT_STATUS, self._on_status_event)
233        #TODO: The following does not belong in gui_manager, it belong in the application code.
234        self.Bind(EVT_SLICER_PARS_UPDATE, self._onEVT_SLICER_PANEL)
235       
236       
237    def _onEVT_SLICER_PANEL(self, event):
238        """
239            receive and event telling to update a panel with a name starting with
240            event.panel_name. this method update slicer panel for a given interactor.
241            @param event: contains type of slicer , paramaters for updating the panel
242            and panel_name to find the slicer 's panel concerned.
243        """
244        for item in self.panels:
245            if self.panels[item].window_caption.startswith(event.panel_name): 
246                self.panels[item].set_slicer(event.type, event.params)
247                self._mgr.Update()
248                break
249       
250    def build_gui(self):
251        # Set up the layout
252        self._setup_layout()
253       
254        # Set up the menu
255        self._setup_menus()
256       
257        self.Fit()
258       
259        #self._check_update(None)
260             
261    def _setup_layout(self):
262        """
263            Set up the layout
264        """
265        # Status bar
266        from statusbar import MyStatusBar
267        self.sb = MyStatusBar(self,wx.ID_ANY)
268        self.SetStatusBar(self.sb)
269
270        # Add panel
271        self._mgr = wx.aui.AuiManager(self)
272       
273        # Load panels
274        self._load_panels()
275       
276        self._mgr.Update()
277
278    def add_perspective(self, plugin):
279        """
280            Add a perspective if it doesn't already
281            exist.
282        """
283        is_loaded = False
284        for item in self.plugins:
285             if plugin.__class__==item.__class__:
286                 print "Plugin %s already loaded" % plugin.__class__.__name__
287                 is_loaded = True
288                 
289        if not is_loaded:
290            self.plugins.append(plugin)
291     
292    def _find_plugins(self, dir="perspectives"):
293        """
294            Find available perspective plug-ins
295            @param dir: directory in which to look for plug-ins
296            @return: list of plug-ins
297        """
298        import imp
299        #print "Looking for plug-ins in %s" % dir
300        # List of plug-in objects
301       
302        #path_exe = os.getcwd()
303        #path_plugs = os.path.join(path_exe, dir)
304        f = open("load.log",'w') 
305        f.write(os.getcwd()+'\n\n')
306        #f.write(str(os.listdir(dir))+'\n')
307       
308       
309        plugins = []
310        # Go through files in panels directory
311        try:
312            list = os.listdir(dir)
313            for item in list:
314                toks = os.path.splitext(os.path.basename(item))
315                name = None
316                if not toks[0] == '__init__':
317                   
318                    if toks[1]=='.py' or toks[1]=='':
319                        name = toks[0]
320               
321                    path = [os.path.abspath(dir)]
322                    file = None
323                    try:
324                        if toks[1]=='':
325                            f.write("trying to import \n")
326                            mod_path = '.'.join([dir, name])
327                            f.write("mod_path= %s\n" % mod_path)
328                            module = __import__(mod_path, globals(), locals(), [name])
329                            f.write(str(module)+'\n')
330                        else:
331                            (file, path, info) = imp.find_module(name, path)
332                            module = imp.load_module( name, file, item, info )
333                        if hasattr(module, "PLUGIN_ID"):
334                            try:
335                                plugins.append(module.Plugin())
336                                print "Found plug-in: %s" % module.PLUGIN_ID
337                            except:
338                                config.printEVT("Error accessing PluginPanel in %s\n  %s" % (name, sys.exc_value))
339                       
340                    except:
341                        print sys.exc_value
342                        f.write(str(sys.exc_value)+'\n')
343                    finally:
344                        if not file==None:
345                            file.close()
346        except:
347            # Should raise and catch at a higher level and display error on status bar
348            pass   
349        f.write(str(plugins)+'\n')
350        f.close()
351        return plugins
352   
353       
354     
355    def _load_panels(self):
356        """
357            Load all panels in the panels directory
358        """
359       
360        # Look for plug-in panels
361        panels = []       
362        for item in self.plugins:
363            if hasattr(item, "get_panels"):
364                ps = item.get_panels(self)
365                panels.extend(ps)
366
367        # Show a default panel with some help information
368        # It also sets the size of the application windows
369        self.panels["default"] = self.defaultPanel
370       
371        self._mgr.AddPane(self.defaultPanel, wx.aui.AuiPaneInfo().
372                              Name("default").
373                              CenterPane().
374                              # This is where we set the size of the application window
375                              BestSize(wx.Size(self._window_width, self._window_height)).
376                              MinSize(wx.Size(self._window_width, self._window_height)).
377                              Show())
378     
379
380        # Add the panels to the AUI manager
381        for panel_class in panels:
382            p = panel_class
383            id = wx.NewId()
384           
385            # Check whether we need to put this panel
386            # in the center pane
387            if hasattr(p, "CENTER_PANE"):
388                if p.CENTER_PANE:
389                    self.panels[str(id)] = p
390                    self._mgr.AddPane(p, wx.aui.AuiPaneInfo().
391                                          Name(p.window_name).Caption(p.window_caption).
392                                          CenterPane().
393                                          BestSize(wx.Size(600,600)).
394                                          MinSize(wx.Size(400,400)).
395                                          Hide())
396               
397            else:
398                self.panels[str(id)] = p
399                self._mgr.AddPane(p, wx.aui.AuiPaneInfo().
400                                  Name(p.window_name).Caption(p.window_caption).
401                                  Right().
402                                  Dock().
403                                  TopDockable().
404                                  BottomDockable().
405                                  LeftDockable().
406                                  RightDockable().
407                                  MinimizeButton().
408                                  Hide().
409                                  BestSize(wx.Size(600,600)).
410                                  MinSize(wx.Size(500,500)))
411                               
412
413               
414       
415    def get_context_menu(self, graph=None):
416        """
417            Get the context menu items made available
418            by the different plug-ins.
419            This function is used by the plotting module
420        """
421        menu_list = []
422        for item in self.plugins:
423            if hasattr(item, "get_context_menu"):
424                menu_list.extend(item.get_context_menu(graph))
425           
426        return menu_list
427       
428    def popup_panel(self, p):
429        """
430            Add a panel object to the AUI manager
431            @param p: panel object to add to the AUI manager
432            @return: ID of the event associated with the new panel [int]
433        """
434       
435        ID = wx.NewId()
436        self.panels[str(ID)] = p
437       
438        count = 0
439        for item in self.panels:
440            if self.panels[item].window_name.startswith(p.window_name): 
441                count += 1
442       
443        windowname = p.window_name
444        caption = p.window_caption
445       
446        if count>0:
447            windowname += str(count+1)
448            caption += (' '+str(count))
449         
450        p.window_name = windowname
451        p.window_caption = caption
452           
453        self._mgr.AddPane(p, wx.aui.AuiPaneInfo().
454                          Name(windowname).Caption(caption).
455                          Floatable().
456                          #Float().
457                          Right().
458                          Dock().
459                          TopDockable().
460                          BottomDockable().
461                          LeftDockable().
462                          RightDockable().
463                          MinimizeButton().
464                          #Hide().
465                          #Show().
466                          BestSize(wx.Size(600,600)).
467                          MinSize(wx.Size(500,500)))
468                          #BestSize(wx.Size(400,400)).
469                          #MinSize(wx.Size(350,350)))
470        pane = self._mgr.GetPane(windowname)
471        self._mgr.MaximizePane(pane)
472        self._mgr.RestoreMaximizedPane()
473       
474       
475        # Register for showing/hiding the panel
476       
477        wx.EVT_MENU(self, ID, self._on_view)
478       
479        self._mgr.Update()
480        return ID
481       
482    def _setup_menus(self):
483        """
484            Set up the application menus
485        """
486        # Menu
487        menubar = wx.MenuBar()
488       
489        # File menu
490        filemenu = wx.Menu()
491       
492        id = wx.NewId()
493        filemenu.Append(id, '&Open', 'Open a file')
494        wx.EVT_MENU(self, id, self._on_open)
495   
496        id = wx.NewId()
497        filemenu.Append(id,'&Quit', 'Exit') 
498        wx.EVT_MENU(self, id, self.Close)
499       
500        # Add sub menus
501        menubar.Append(filemenu,  '&File')
502       
503        # Plot menu
504        # Attach a menu item for each panel in our
505        # panel list that also appears in a plug-in.
506        # TODO: clean this up. We should just identify
507        # plug-in panels and add them all.
508       
509        # Only add the panel menu if there is more than one panel
510        n_panels = 0
511        for plug in self.plugins:
512            pers = plug.get_perspective()
513            if len(pers)>0:
514                n_panels += 1
515       
516        if n_panels>1:
517            viewmenu = wx.Menu()
518            for plug in self.plugins:
519                plugmenu = wx.Menu()
520                pers = plug.get_perspective()
521                if len(pers)>0:
522                    for item in self.panels:
523                        if item == 'default':
524                            continue
525                        panel = self.panels[item]
526                        if panel.window_name in pers:
527                            plugmenu.Append(int(item), panel.window_caption, "Show %s window" % panel.window_caption)
528                           
529                           
530                           
531                            wx.EVT_MENU(self, int(item), self._on_view)
532                   
533                    viewmenu.AppendMenu(wx.NewId(), plug.sub_menu, plugmenu, plug.sub_menu)
534               
535            menubar.Append(viewmenu, '&Panel')
536
537        # Perspective
538        # Attach a menu item for each defined perspective.
539        # Only add the perspective menu if there are more than one perspectves
540        n_perspectives = 0
541        for plug in self.plugins:
542            if len(plug.get_perspective()) > 0:
543                n_perspectives += 1
544       
545        if n_perspectives>1:
546            p_menu = wx.Menu()
547            for plug in self.plugins:
548                if len(plug.get_perspective()) > 0:
549                    id = wx.NewId()
550                    p_menu.Append(id, plug.sub_menu, "Switch to %s perspective" % plug.sub_menu)
551                    wx.EVT_MENU(self, id, plug.on_perspective)
552            menubar.Append(p_menu,   '&Perspective')
553 
554        # Help menu
555        helpmenu = wx.Menu()
556
557        # Look for help item in plug-ins
558        for item in self.plugins:
559            if hasattr(item, "help"):
560                id = wx.NewId()
561                helpmenu.Append(id,'&%s help' % item.sub_menu, '')
562                wx.EVT_MENU(self, id, item.help)
563       
564        if config._do_aboutbox:
565            id = wx.NewId()
566            helpmenu.Append(id,'&About', 'Software information')
567            wx.EVT_MENU(self, id, self._onAbout)
568        id = wx.NewId()
569        helpmenu.Append(id,'&Check for update', 'Check for the latest version of %s' % config.__appname__)
570        wx.EVT_MENU(self, id, self._check_update)
571       
572       
573       
574       
575        # Look for plug-in menus
576        # Add available plug-in sub-menus.
577        for item in self.plugins:
578            if hasattr(item, "populate_menu"):
579                for (self.next_id, menu, name) in item.populate_menu(self.next_id, self):
580                    menubar.Append(menu, name)
581                   
582
583        menubar.Append(helpmenu, '&Help')
584         
585        self.SetMenuBar(menubar)
586       
587       
588       
589    def _on_status_event(self, evt):
590        """
591            Display status message
592        """
593        self.sb.clear_gauge( msg="")
594        mythread=None
595        mytype= None
596        if hasattr(evt, "curr_thread"):
597            mythread= evt.curr_thread
598        if hasattr(evt, "type"):
599            mytype= evt.type
600        self.sb.set_status( type=mytype,msg=str(evt.status),thread=mythread)
601       
602
603       
604    def _on_view(self, evt):
605        """
606            A panel was selected to be shown. If it's not already
607            shown, display it.
608            @param evt: menu event
609        """
610        self.show_panel(evt.GetId())
611
612    def show_panel(self, uid):
613        """
614            Shows the panel with the given id
615            @param uid: unique ID number of the panel to show
616        """
617        ID = str(uid)
618        config.printEVT("show_panel: %s" % ID)
619        if ID in self.panels.keys():
620            if not self._mgr.GetPane(self.panels[ID].window_name).IsShown():
621                self._mgr.GetPane(self.panels[ID].window_name).Show()
622                # Hide default panel
623                self._mgr.GetPane(self.panels["default"].window_name).Hide()
624           
625               
626            self._mgr.Update()
627   
628    def _on_open(self, event):
629   
630        from data_loader import plot_data
631        path = self.choose_file()
632        if path ==None:
633            return
634        if path and os.path.isfile(path):
635            plot_data(self, path)
636           
637       
638       
639    def _onClose(self, event):
640        import sys
641        wx.Exit()
642        sys.exit()
643                   
644    def Close(self, event=None):
645        """
646            Quit the application
647        """
648        import sys
649        wx.Frame.Close(self)
650        wx.Exit()
651        sys.exit()
652
653 
654    def _check_update(self, event=None): 
655        """
656            Check with the deployment server whether a new version
657            of the application is available
658        """
659        import urllib
660        try: 
661            h = urllib.urlopen(config.__update_URL__)
662            lines = h.readlines()
663            line = ''
664            if len(lines)>0:
665                line = lines[0]
666               
667                toks = line.lstrip().rstrip().split('.')
668                toks_current = config.__version__.split('.')
669                update_available = False
670                for i in range(len(toks)):
671                    if len(toks[i].strip())>0:
672                        if int(toks[i].strip())>int(toks_current[i]):
673                            update_available = True
674                if update_available:
675                    #print "Version %s is available" % line.rstrip().lstrip()
676                    self.SetStatusText("Version %s is available! See the Help menu to download it." % line.rstrip().lstrip())
677                    if event != None:
678                        import webbrowser
679                        webbrowser.open(config.__download_page__)
680                else:
681                    if event != None:
682                        self.SetStatusText("You have the latest version of %s" % config.__appname__)
683        except:
684            if event != None:
685                self.SetStatusText("You have the latest version of %s" % config.__appname__)
686           
687           
688    def _onAbout(self, evt):
689        """
690            Pop up the about dialog
691            @param evt: menu event
692        """
693        if config._do_aboutbox:
694            import aboutbox 
695            dialog = aboutbox.DialogAbout(None, -1, "")
696            dialog.ShowModal()
697           
698    def set_manager(self, manager):
699        """
700            Sets the application manager for this frame
701            @param manager: frame manager
702        """
703        self.app_manager = manager
704       
705    def post_init(self):
706        """
707            This initialization method is called after the GUI
708            has been created and all plug-ins loaded. It calls
709            the post_init() method of each plug-in (if it exists)
710            so that final initialization can be done.
711        """
712        for item in self.plugins:
713            if hasattr(item, "post_init"):
714                item.post_init()
715       
716    def set_perspective(self, panels):
717        """
718            Sets the perspective of the GUI.
719            Opens all the panels in the list, and closes
720            all the others.
721           
722            @param panels: list of panels
723        """
724        for item in self.panels:
725            # Check whether this is a sticky panel
726            if hasattr(self.panels[item], "ALWAYS_ON"):
727                if self.panels[item].ALWAYS_ON:
728                    continue 
729           
730            if self.panels[item].window_name in panels:
731                if not self._mgr.GetPane(self.panels[item].window_name).IsShown():
732                    self._mgr.GetPane(self.panels[item].window_name).Show()
733            else:
734                if self._mgr.GetPane(self.panels[item].window_name).IsShown():
735                    self._mgr.GetPane(self.panels[item].window_name).Hide()
736                 
737        self._mgr.Update()
738       
739    def choose_file(self):
740        """
741            Functionality that belongs elsewhere
742            Should add a hook to specify the preferred file type/extension.
743        """
744        #TODO: clean this up
745        from data_loader import choose_data_file
746        path = choose_data_file(self, self._default_save_location)
747        if not path==None:
748            try:
749                self._default_save_location = os.path.dirname(path)
750            except:
751                pass
752        return path
753   
754    def load_ascii_1D(self, path):
755        from data_loader import load_ascii_1D
756        return load_ascii_1D(path)
757                 
758class DefaultPanel(wx.Panel):
759    """
760        Defines the API for a panels to work with
761        the GUI manager
762    """
763    ## Internal nickname for the window, used by the AUI manager
764    window_name = "default"
765    ## Name to appear on the window title bar
766    window_caption = "Welcome panel"
767    ## Flag to tell the AUI manager to put this panel in the center pane
768    CENTER_PANE = True
769
770 
771# Toy application to test this Frame
772class ViewApp(wx.App):
773    def OnInit(self):
774        #from gui_manager import ViewerFrame
775        self.frame = ViewerFrame(None, -1, config.__appname__)   
776        self.frame.Show(True)
777
778        if hasattr(self.frame, 'special'):
779            print "Special?", self.frame.special.__class__.__name__
780            self.frame.special.SetCurrent()
781        self.SetTopWindow(self.frame)
782        return True
783   
784    def set_manager(self, manager):
785        """
786            Sets a reference to the application manager
787            of the GUI manager (Frame)
788        """
789        self.frame.set_manager(manager)
790       
791    def build_gui(self):
792        """
793            Build the GUI
794        """
795        self.frame.build_gui()
796        self.frame.post_init()
797       
798    def add_perspective(self, perspective):
799        """
800            Manually add a perspective to the application GUI
801        """
802        self.frame.add_perspective(perspective)
803       
804
805if __name__ == "__main__": 
806    app = ViewApp(0)
807    app.MainLoop()             
Note: See TracBrowser for help on using the repository browser.