source: sasview/guiframe/gui_manager.py @ 14226f4

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

Handle over application window size. Allow plot clearing.

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