ViewMenu now works with new gui layout
[hooke.git] / hooke / ui / gui / __init__.py
1 # Copyright\r
2 \r
3 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.\r
4 """\r
5 \r
6 WX_GOOD=['2.8']\r
7 \r
8 import wxversion\r
9 wxversion.select(WX_GOOD)\r
10 \r
11 import copy\r
12 import os.path\r
13 import platform\r
14 import shutil\r
15 import time\r
16 \r
17 import wx.html\r
18 import wx.aui as aui\r
19 import wx.lib.evtmgr as evtmgr\r
20 \r
21 \r
22 # wxPropertyGrid included in wxPython >= 2.9.1, until then, see\r
23 #   http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download\r
24 # until then, we'll avoid it because of the *nix build problems.\r
25 #import wx.propgrid as wxpg\r
26 \r
27 from matplotlib.ticker import FuncFormatter\r
28 \r
29 from ...command import CommandExit, Exit, Success, Failure, Command, Argument\r
30 from ...config import Setting\r
31 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig\r
32 from ...ui import UserInterface, CommandMessage\r
33 from . import menu as menu\r
34 from . import navbar as navbar\r
35 from . import panel as panel\r
36 from . import prettyformat as prettyformat\r
37 from . import statusbar as statusbar\r
38 \r
39 \r
40 class HookeFrame (wx.Frame):\r
41     """The main Hooke-interface window.\r
42 \r
43     \r
44     """\r
45     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):\r
46         super(HookeFrame, self).__init__(*args, **kwargs)\r
47         self.gui = gui\r
48         self.commands = commands\r
49         self.inqueue = inqueue\r
50         self.outqueue = outqueue\r
51         self._perspectives = {}  # {name: perspective_str}\r
52         self._c = {}\r
53 \r
54         self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))\r
55 \r
56         # setup frame manager\r
57         self._c['manager'] = aui.AuiManager()\r
58         self._c['manager'].SetManagedWindow(self)\r
59 \r
60         # set the gradient and drag styles\r
61         self._c['manager'].GetArtProvider().SetMetric(\r
62             aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)\r
63         self._c['manager'].SetFlags(\r
64             self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)\r
65 \r
66         # Min size for the frame itself isn't completely done.  See\r
67         # the end of FrameManager::Update() for the test code. For\r
68         # now, just hard code a frame minimum size.\r
69         self.SetMinSize(wx.Size(500, 500))\r
70 \r
71         self._setup_panels()\r
72         self._setup_toolbars()\r
73         self._c['manager'].Update()  # commit pending changes\r
74 \r
75         # Create the menubar after the panes so that the default\r
76         # perspective is created with all panes open\r
77         self._c['menu bar'] = menu.HookeMenuBar(\r
78             parent=self,\r
79             callbacks={\r
80                 'close': self._on_close,\r
81                 'about': self._on_about,\r
82                 'view_panel': self._on_panel_visibility,\r
83                 'save_perspective': self._on_save_perspective,\r
84                 'delete_perspective': self._on_delete_perspective,\r
85                 })\r
86         self.SetMenuBar(self._c['menu bar'])\r
87 \r
88         self._c['status bar'] = statusbar.StatusBar(\r
89             parent=self,\r
90             style=wx.ST_SIZEGRIP)\r
91         self.SetStatusBar(self._c['status bar'])\r
92 \r
93         self._update_perspectives()\r
94         self._bind_events()\r
95 \r
96         name = self.gui.config['active perspective']\r
97         return # TODO: cleanup\r
98         menu_item = self.GetPerspectiveMenuItem(name)\r
99         if menu_item is not None:\r
100             self._on_restore_perspective(menu_item)\r
101             #TODO: config setting to remember playlists from last session\r
102         self.playlists = self._c['playlists'].Playlists\r
103         self._displayed_plot = None\r
104         #load default list, if possible\r
105         self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist'))\r
106 \r
107     def _setup_panels(self):\r
108         client_size = self.GetClientSize()\r
109         for label,p,style in [\r
110 #            ('folders', wx.GenericDirCtrl(\r
111 #                    parent=self,\r
112 #                    dir=self.gui.config['folders-workdir'],\r
113 #                    size=(200, 250),\r
114 #                    style=wx.DIRCTRL_SHOW_FILTERS,\r
115 #                    filter=self.gui.config['folders-filters'],\r
116 #                    defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'),  #HACK: config should convert\r
117 #            ('playlists', panel.PANELS['playlist'](\r
118 #                    callbacks={},\r
119 #                    config=self.gui.config,\r
120 #                    parent=self,\r
121 #                    style=wx.WANTS_CHARS|wx.NO_BORDER,\r
122 #                    # WANTS_CHARS so the panel doesn't eat the Return key.\r
123 #                    size=(160, 200)), 'left'),\r
124 #            ('note', panel.note.Note(\r
125 #                    parent=self\r
126 #                    style=wx.WANTS_CHARS|wx.NO_BORDER,\r
127 #                    size=(160, 200)), 'left'),\r
128 #            ('notebook', Notebook(\r
129 #                    parent=self,\r
130 #                    pos=wx.Point(client_size.x, client_size.y),\r
131 #                    size=wx.Size(430, 200),\r
132 #                    style=aui.AUI_NB_DEFAULT_STYLE\r
133 #                    | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),\r
134             ('commands', panel.PANELS['commands'](\r
135                     commands=self.commands,\r
136                     selected=self.gui.config['selected command'],\r
137                     callbacks={\r
138                         'execute': self.execute_command,\r
139                         'select_plugin': self.select_plugin,\r
140                         'select_command': self.select_command,\r
141 #                        'selection_changed': self.panelProperties.select(self, method, command),  #SelectedTreeItem = selected_item,\r
142                         },\r
143                     parent=self,\r
144                     style=wx.WANTS_CHARS|wx.NO_BORDER,\r
145                     # WANTS_CHARS so the panel doesn't eat the Return key.\r
146 #                    size=(160, 200)\r
147                     ), 'center'),\r
148             #('properties', panel.propertyeditor.PropertyEditor(self),'right'),\r
149 #            ('assistant', wx.TextCtrl(\r
150 #                    parent=self,\r
151 #                    pos=wx.Point(0, 0),\r
152 #                    size=wx.Size(150, 90),\r
153 #                    style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),\r
154 #            ('output', wx.TextCtrl(\r
155 #                    parent=self,\r
156 #                    pos=wx.Point(0, 0),\r
157 #                    size=wx.Size(150, 90),\r
158 #                    style=wx.NO_BORDER|wx.TE_MULTILINE), 'bottom'),\r
159 #            ('results', panel.results.Results(self), 'bottom'),\r
160             ]:\r
161             self._add_panel(label, p, style)\r
162         #self._c['assistant'].SetEditable(False)\r
163 \r
164     def _add_panel(self, label, panel, style):\r
165         self._c[label] = panel\r
166         cap_label = label.capitalize()\r
167         info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)\r
168         info.PaneBorder(False).CloseButton(True).MaximizeButton(False)\r
169         if style == 'top':\r
170             info.Top()\r
171         elif style == 'center':\r
172             info.CenterPane()\r
173         elif style == 'left':\r
174             info.Left()\r
175         elif style == 'right':\r
176             info.Right()\r
177         else:\r
178             assert style == 'bottom', style\r
179             info.Bottom()\r
180         self._c['manager'].AddPane(panel, info)\r
181 \r
182     def _setup_toolbars(self):\r
183         self._c['navigation bar'] = navbar.NavBar(\r
184             callbacks={\r
185                 'next': self._next_curve,\r
186                 'previous': self._previous_curve,\r
187                 },\r
188             parent=self,\r
189             style=wx.TB_FLAT | wx.TB_NODIVIDER)\r
190         self._c['manager'].AddPane(\r
191             self._c['navigation bar'],\r
192             aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'\r
193                 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False\r
194                 ).RightDockable(False))\r
195 \r
196     def _bind_events(self):\r
197         # TODO: figure out if we can use the eventManager for menu\r
198         # ranges and events of 'self' without raising an assertion\r
199         # fail error.\r
200         self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)\r
201         self.Bind(wx.EVT_SIZE, self._on_size)\r
202         self.Bind(wx.EVT_CLOSE, self._on_close)\r
203         self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)\r
204         self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)\r
205 \r
206         return # TODO: cleanup\r
207         for value in self._c['menu bar']._c['view']._c.values():\r
208             self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)\r
209 \r
210         self.Bind(wx.EVT_MENU, self._on_save_perspective,\r
211                   self._c['menu bar']._c['perspective']._c['save'])\r
212         self.Bind(wx.EVT_MENU, self._on_delete_perspective,\r
213                   self._c['menu bar']._c['perspective']._c['delete'])\r
214 \r
215         treeCtrl = self._c['folders'].GetTreeCtrl()\r
216         treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)\r
217         \r
218         # TODO: playlist callbacks\r
219         return # TODO: cleanup\r
220         evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)\r
221         #property editor\r
222         self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)\r
223         #results panel\r
224         self.panelResults.results_list.OnCheckItem = self.OnResultsCheck\r
225 \r
226     def _command_by_name(self, name):\r
227         cs = [c for c in self.commands if c.name == name]\r
228         if len(cs) == 0:\r
229             raise KeyError(name)\r
230         elif len(cs) > 1:\r
231             raise Exception('Multiple commands named "%s"' % name)\r
232         return cs[0]\r
233 \r
234     def execute_command(self, _class=None, method=None,\r
235                         command=None, args=None):\r
236         self.inqueue.put(CommandMessage(command, args))\r
237         results = []\r
238         while True:\r
239             msg = self.outqueue.get()\r
240             results.append(msg)\r
241             print type(msg), msg\r
242             if isinstance(msg, Exit):\r
243                 self._on_close()\r
244                 break\r
245             elif isinstance(msg, CommandExit):\r
246                 # TODO: display command complete\r
247                 break\r
248             elif isinstance(msg, ReloadUserInterfaceConfig):\r
249                 self.gui.reload_config(msg.config)\r
250                 continue\r
251             elif isinstance(msg, Request):\r
252                 h = handler.HANDLERS[msg.type]\r
253                 h.run(self, msg)  # TODO: pause for response?\r
254                 continue\r
255         pp = getattr(\r
256             self, '_postprocess_%s' % command.name.replace(' ', '_'), None)\r
257         if pp != None:\r
258             pp(command=command, results=results)\r
259         return results\r
260 \r
261     def _handle_request(self, msg):\r
262         """Repeatedly try to get a response to `msg`.\r
263         """\r
264         if prompt == None:\r
265             raise NotImplementedError('_%s_request_prompt' % msg.type)\r
266         prompt_string = prompt(msg)\r
267         parser = getattr(self, '_%s_request_parser' % msg.type, None)\r
268         if parser == None:\r
269             raise NotImplementedError('_%s_request_parser' % msg.type)\r
270         error = None\r
271         while True:\r
272             if error != None:\r
273                 self.cmd.stdout.write(''.join([\r
274                         error.__class__.__name__, ': ', str(error), '\n']))\r
275             self.cmd.stdout.write(prompt_string)\r
276             value = parser(msg, self.cmd.stdin.readline())\r
277             try:\r
278                 response = msg.response(value)\r
279                 break\r
280             except ValueError, error:\r
281                 continue\r
282         self.inqueue.put(response)\r
283 \r
284 \r
285     def _GetActiveFileIndex(self):\r
286         lib.playlist.Playlist = self.GetActivePlaylist()\r
287         #get the selected item from the tree\r
288         selected_item = self._c['playlists']._c['tree'].GetSelection()\r
289         #test if a playlist or a curve was double-clicked\r
290         if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
291             return -1\r
292         else:\r
293             count = 0\r
294             selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
295             while selected_item.IsOk():\r
296                 count += 1\r
297                 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
298             return count\r
299 \r
300     def _GetPlaylistTab(self, name):\r
301         for index, page in enumerate(self._c['notebook']._tabs._pages):\r
302             if page.caption == name:\r
303                 return index\r
304         return -1\r
305 \r
306     def _restore_perspective(self, name):\r
307         # TODO: cleanup\r
308         self.gui.config['active perspective'] = name  # TODO: push to engine's Hooke\r
309         self._c['manager'].LoadPerspective(self._perspectives[name])\r
310         self._c['manager'].Update()\r
311         for pane in self._c['manager'].GetAllPanes():\r
312             if pane.name in self._c['menu bar']._c['view']._c.keys():\r
313                 pane.Check(pane.window.IsShown())\r
314 \r
315     def _SavePerspectiveToFile(self, name, perspective):\r
316         filename = ''.join([name, '.txt'])\r
317         filename = lh.get_file_path(filename, ['perspective'])\r
318         perspectivesFile = open(filename, 'w')\r
319         perspectivesFile.write(perspective)\r
320         perspectivesFile.close()\r
321 \r
322     def select_plugin(self, _class=None, method=None, plugin=None):\r
323         for option in config[section]:\r
324             properties.append([option, config[section][option]])\r
325 \r
326     def select_command(self, _class, method, command):\r
327         self.select_plugin(plugin=command.plugin)\r
328         plugin = self.GetItemText(selected_item)\r
329         if plugin != 'core':\r
330             doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')\r
331         else:\r
332             doc_string = 'The module "core" contains Hooke core functionality'\r
333         if doc_string is not None:\r
334             self.panelAssistant.ChangeValue(doc_string)\r
335         else:\r
336             self.panelAssistant.ChangeValue('')\r
337         panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)\r
338         self.gui.config['selected command'] = command\r
339 \r
340     def AddPlaylistFromFiles(self, files=[], name='Untitled'):\r
341         if files:\r
342             playlist = lib.playlist.Playlist(self, self.drivers)\r
343             for item in files:\r
344                 playlist.add_curve(item)\r
345         if playlist.count > 0:\r
346             playlist.name = self._GetUniquePlaylistName(name)\r
347             playlist.reset()\r
348             self.AddTayliss(playlist)\r
349 \r
350     def AppendToOutput(self, text):\r
351         self.panelOutput.AppendText(''.join([text, '\n']))\r
352 \r
353     def AppliesPlotmanipulator(self, name):\r
354         '''\r
355         Returns True if the plotmanipulator 'name' is applied, False otherwise\r
356         name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')\r
357         '''\r
358         return self.GetBoolFromConfig('core', 'plotmanipulators', name)\r
359 \r
360     def ApplyPlotmanipulators(self, plot, plot_file):\r
361         '''\r
362         Apply all active plotmanipulators.\r
363         '''\r
364         if plot is not None and plot_file is not None:\r
365             manipulated_plot = copy.deepcopy(plot)\r
366             for plotmanipulator in self.plotmanipulators:\r
367                 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):\r
368                     manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)\r
369             return manipulated_plot\r
370 \r
371     def GetActiveFigure(self):\r
372         playlist_name = self.GetActivePlaylistName()\r
373         figure = self.playlists[playlist_name].figure\r
374         if figure is not None:\r
375             return figure\r
376         return None\r
377 \r
378     def GetActiveFile(self):\r
379         playlist = self.GetActivePlaylist()\r
380         if playlist is not None:\r
381             return playlist.get_active_file()\r
382         return None\r
383 \r
384     def GetActivePlot(self):\r
385         playlist = self.GetActivePlaylist()\r
386         if playlist is not None:\r
387             return playlist.get_active_file().plot\r
388         return None\r
389 \r
390     def GetDisplayedPlot(self):\r
391         plot = copy.deepcopy(self.displayed_plot)\r
392         #plot.curves = []\r
393         #plot.curves = copy.deepcopy(plot.curves)\r
394         return plot\r
395 \r
396     def GetDisplayedPlotCorrected(self):\r
397         plot = copy.deepcopy(self.displayed_plot)\r
398         plot.curves = []\r
399         plot.curves = copy.deepcopy(plot.corrected_curves)\r
400         return plot\r
401 \r
402     def GetDisplayedPlotRaw(self):\r
403         plot = copy.deepcopy(self.displayed_plot)\r
404         plot.curves = []\r
405         plot.curves = copy.deepcopy(plot.raw_curves)\r
406         return plot\r
407 \r
408     def GetDockArt(self):\r
409         return self._c['manager'].GetArtProvider()\r
410 \r
411     def GetPlotmanipulator(self, name):\r
412         '''\r
413         Returns a plot manipulator function from its name\r
414         '''\r
415         for plotmanipulator in self.plotmanipulators:\r
416             if plotmanipulator.name == name:\r
417                 return plotmanipulator\r
418         return None\r
419 \r
420     def GetPerspectiveMenuItem(self, name):\r
421         if self._perspectives.has_key(name):\r
422             perspectives_list = [key for key, value in self._perspectives.iteritems()]\r
423             perspectives_list.sort()\r
424             index = perspectives_list.index(name)\r
425             perspective_Id = ID_FirstPerspective + index\r
426             menu_item = self._c['menu bar'].FindItemById(perspective_Id)\r
427             return menu_item\r
428         else:\r
429             return None\r
430 \r
431     def HasPlotmanipulator(self, name):\r
432         '''\r
433         returns True if the plotmanipulator 'name' is loaded, False otherwise\r
434         '''\r
435         for plotmanipulator in self.plotmanipulators:\r
436             if plotmanipulator.command == name:\r
437                 return True\r
438         return False\r
439 \r
440     def _on_about(self, *args):\r
441         dialog = wx.MessageDialog(\r
442             parent=self,\r
443             message=self.gui._splash_text(),\r
444             caption='About Hooke',\r
445             style=wx.OK|wx.ICON_INFORMATION)\r
446         dialog.ShowModal()\r
447         dialog.Destroy()\r
448 \r
449     def _on_close(self, *args):\r
450         # apply changes\r
451         self.gui.config['main height'] = str(self.GetSize().GetHeight())\r
452         self.gui.config['main left'] = str(self.GetPosition()[0])\r
453         self.gui.config['main top'] = str(self.GetPosition()[1])\r
454         self.gui.config['main width'] = str(self.GetSize().GetWidth())\r
455         # push changes back to Hooke.config?\r
456         self._c['manager'].UnInit()\r
457         del self._c['manager']\r
458         self.Destroy()\r
459 \r
460     def _update_perspectives(self):\r
461         """Add perspectives to menubar and _perspectives.\r
462         """\r
463         self._perspectives = {\r
464             'Default': self._c['manager'].SavePerspective(),\r
465             }\r
466         path = self.gui.config['perspective path']\r
467         if os.path.isdir(path):\r
468             files = sorted(os.listdir(path))\r
469             for fname in files:\r
470                 name, extension = os.path.splitext(fname)\r
471                 if extension != '.txt':\r
472                     continue\r
473                 fpath = os.path.join(path, fpath)\r
474                 if not os.path.isfile(fpath):\r
475                     continue\r
476                 perspective = None\r
477                 with open(fpath, 'rU') as f:\r
478                     perspective = f.readline()\r
479                 if perspective:\r
480                     self._perspectives[name] = perspective\r
481 \r
482         selected_perspective = self.gui.config['active perspective']\r
483         if not self._perspectives.has_key(selected_perspective):\r
484             self.gui.config['active perspective'] = 'Default'  # TODO: push to engine's Hooke\r
485 \r
486         self._update_perspective_menu()\r
487         self._restore_perspective(selected_perspective)\r
488 \r
489     def _update_perspective_menu(self):\r
490         self._c['menu bar']._c['perspective'].update(\r
491             sorted(self._perspectives.keys()),\r
492             self.gui.config['active perspective'],\r
493             self._on_restore_perspective)\r
494 \r
495     def _on_restore_perspective(self, event):\r
496         name = self._c['menu bar'].FindItemById(event.GetId()).GetLabel()\r
497         self._restore_perspective(name)\r
498 \r
499     def _on_save_perspective(self, event):\r
500         def nameExists(name):\r
501             menu_position = self._c['menu bar'].FindMenu('Perspective')\r
502             menu = self._c['menu bar'].GetMenu(menu_position)\r
503             for item in menu.GetMenuItems():\r
504                 if item.GetText() == name:\r
505                     return True\r
506             return False\r
507 \r
508         done = False\r
509         while not done:\r
510             dialog = wx.TextEntryDialog(self, 'Enter a name for the new perspective:', 'Save perspective')\r
511             dialog.SetValue('New perspective')\r
512             if dialog.ShowModal() != wx.ID_OK:\r
513                 return\r
514             else:\r
515                 name = dialog.GetValue()\r
516 \r
517             if nameExists(name):\r
518                 dialogConfirm = wx.MessageDialog(self, 'A file with this name already exists.\n\nDo you want to replace it?', 'Confirm', wx.YES_NO|wx.ICON_QUESTION|wx.CENTER)\r
519                 if dialogConfirm.ShowModal() == wx.ID_YES:\r
520                     done = True\r
521             else:\r
522                 done = True\r
523 \r
524         perspective = self._c['manager'].SavePerspective()\r
525         self._SavePerspectiveToFile(name, perspective)\r
526         self.gui.config['active perspectives'] = name\r
527         self._update_perspective_menu()\r
528 #        if nameExists(name):\r
529 #            #check the corresponding menu item\r
530 #            menu_item = self.GetPerspectiveMenuItem(name)\r
531 #            #replace the perspectiveStr in _pespectives\r
532 #            self._perspectives[name] = perspective\r
533 #        else:\r
534 #            #because we deal with radio items, we need to do some extra work\r
535 #            #delete all menu items from the perspectives menu\r
536 #            for item in self._perspectives_menu.GetMenuItems():\r
537 #                self._perspectives_menu.DeleteItem(item)\r
538 #            #recreate the perspectives menu\r
539 #            self._perspectives_menu.Append(ID_SavePerspective, 'Save Perspective')\r
540 #            self._perspectives_menu.Append(ID_DeletePerspective, 'Delete Perspective')\r
541 #            self._perspectives_menu.AppendSeparator()\r
542 #            #convert the perspectives dictionary into a list\r
543 #            # the list contains:\r
544 #            #[0]: name of the perspective\r
545 #            #[1]: perspective\r
546 #            perspectives_list = [key for key, value in self._perspectives.iteritems()]\r
547 #            perspectives_list.append(name)\r
548 #            perspectives_list.sort()\r
549 #            #add all previous perspectives\r
550 #            for index, item in enumerate(perspectives_list):\r
551 #                menu_item = self._perspectives_menu.AppendRadioItem(ID_FirstPerspective + index, item)\r
552 #                if item == name:\r
553 #                    menu_item.Check()\r
554 #            #add the new perspective to _perspectives\r
555 #            self._perspectives[name] = perspective\r
556 \r
557     def _on_delete_perspective(self, event):\r
558         dialog = panel.selection.Selection(\r
559             options=sorted(os.listdir(self.gui.config['perspective path'])),\r
560             message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",\r
561             button_id=wx.ID_DELETE,\r
562             callbacks={'button': self._on_delete_perspective},\r
563             selection_style='multiple',\r
564             parent=self,\r
565             label='Delete perspective(s)',\r
566             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)\r
567         dialog.CenterOnScreen()\r
568         dialog.ShowModal()\r
569         dialog.Destroy()\r
570         self._update_perspective_menu()\r
571         # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258\r
572         #   http://trac.wxwidgets.org/ticket/3258 \r
573         # ) that makes the radio item indicator in the menu disappear.\r
574         # The code should be fine once this issue is fixed.\r
575 \r
576     def _on_delete_perspective(self, _class, method, options, selected):\r
577         for p in selected:\r
578             self._perspectives.remove(p)\r
579             if p == self.gui.config['active perspective']:\r
580                 self.gui.config['active perspective'] = 'Default'\r
581             path = os.path.join(self.gui.config['perspective path'],\r
582                                 p+'.txt')\r
583             remove(path)\r
584         self._update_perspective_menu()\r
585 \r
586     def _on_dir_ctrl_left_double_click(self, event):\r
587         file_path = self.panelFolders.GetPath()\r
588         if os.path.isfile(file_path):\r
589             if file_path.endswith('.hkp'):\r
590                 self.do_loadlist(file_path)\r
591         event.Skip()\r
592 \r
593     def _on_erase_background(self, event):\r
594         event.Skip()\r
595 \r
596     def _next_curve(self, *args):\r
597         """Call the `next curve` command.\r
598         """\r
599         results = self.execute_command(\r
600             command=self._command_by_name('next curve'))\r
601         if isinstance(results[-1], Success):\r
602             self.execute_command(\r
603                 command=self._command_by_name('get curve'))\r
604 \r
605     def _previous_curve(self, *args):\r
606         """Call the `previous curve` command.\r
607         """\r
608         self.execute_command(\r
609             command=self._command_by_name('previous curve'))\r
610         if isinstance(results[-1], Success):\r
611             self.execute_command(\r
612                 command=self._command_by_name('get curve'))\r
613 \r
614     def _postprocess_get_curve(self, command, results):\r
615         """Update `self` to show the curve.\r
616         """\r
617         if not isinstance(results[-1], Success):\r
618             return  # error executing 'get curve'\r
619         assert len(results) == 2, results\r
620         curve = results[0]\r
621         print curve\r
622 \r
623         selected_item = self._c['playlists']._c['tree'].GetSelection()\r
624         if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
625             #GetFirstChild returns a tuple\r
626             #we only need the first element\r
627             next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]\r
628         else:\r
629             next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)\r
630             if not next_item.IsOk():\r
631                 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)\r
632                 #GetFirstChild returns a tuple\r
633                 #we only need the first element\r
634                 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]\r
635         self._c['playlists']._c['tree'].SelectItem(next_item, True)\r
636         if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
637             playlist = self.GetActivePlaylist()\r
638             if playlist.count > 1:\r
639                 playlist.next()\r
640                 self._c['status bar'].set_playlist(playlist)\r
641                 self.UpdateNote()\r
642                 self.UpdatePlot()\r
643 \r
644     def _on_notebook_page_close(self, event):\r
645         ctrl = event.GetEventObject()\r
646         playlist_name = ctrl.GetPageText(ctrl._curpage)\r
647         self.DeleteFromPlaylists(playlist_name)\r
648 \r
649     def OnPaneClose(self, event):\r
650         event.Skip()\r
651 \r
652     def OnPropGridChanged (self, event):\r
653         prop = event.GetProperty()\r
654         if prop:\r
655             item_section = self.panelProperties.SelectedTreeItem\r
656             item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)\r
657             plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)\r
658             config = self.gui.config[plugin]\r
659             property_section = self._c['commands']._c['tree'].GetItemText(item_section)\r
660             property_key = prop.GetName()\r
661             property_value = prop.GetDisplayedString()\r
662 \r
663             config[property_section][property_key]['value'] = property_value\r
664 \r
665     def OnResultsCheck(self, index, flag):\r
666         results = self.GetActivePlot().results\r
667         if results.has_key(self.results_str):\r
668             results[self.results_str].results[index].visible = flag\r
669             results[self.results_str].update()\r
670             self.UpdatePlot()\r
671 \r
672 \r
673     def _on_size(self, event):\r
674         event.Skip()\r
675 \r
676     def OnUpdateNote(self, event):\r
677         '''\r
678         Saves the note to the active file.\r
679         '''\r
680         active_file = self.GetActiveFile()\r
681         active_file.note = self.panelNote.Editor.GetValue()\r
682 \r
683     def _on_panel_visibility(self, _class, method, panel_name, visible):\r
684         pane = self._c['manager'].GetPane(panel_name)\r
685         print visible\r
686         pane.Show(visible)\r
687         #if we don't do the following, the Folders pane does not resize properly on hide/show\r
688         if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():\r
689             #folders_size = pane.GetSize()\r
690             self.panelFolders.Fit()\r
691         self._c['manager'].Update()\r
692 \r
693     def _clickize(self, xvector, yvector, index):\r
694         '''\r
695         Returns a ClickedPoint() object from an index and vectors of x, y coordinates\r
696         '''\r
697         point = lib.clickedpoint.ClickedPoint()\r
698         point.index = index\r
699         point.absolute_coords = xvector[index], yvector[index]\r
700         point.find_graph_coords(xvector, yvector)\r
701         return point\r
702 \r
703     def _delta(self, message='Click 2 points', block=0):\r
704         '''\r
705         Calculates the difference between two clicked points\r
706         '''\r
707         clicked_points = self._measure_N_points(N=2, message=message, block=block)\r
708 \r
709         plot = self.GetDisplayedPlotCorrected()\r
710         curve = plot.curves[block]\r
711 \r
712         delta = lib.delta.Delta()\r
713         delta.point1.x = clicked_points[0].graph_coords[0]\r
714         delta.point1.y = clicked_points[0].graph_coords[1]\r
715         delta.point2.x = clicked_points[1].graph_coords[0]\r
716         delta.point2.y = clicked_points[1].graph_coords[1]\r
717         delta.units.x = curve.units.x\r
718         delta.units.y = curve.units.y\r
719 \r
720         return delta\r
721 \r
722     def _measure_N_points(self, N, message='', block=0):\r
723         '''\r
724         General helper function for N-points measurements\r
725         By default, measurements are done on the retraction\r
726         '''\r
727         if message:\r
728             dialog = wx.MessageDialog(None, message, 'Info', wx.OK)\r
729             dialog.ShowModal()\r
730 \r
731         figure = self.GetActiveFigure()\r
732 \r
733         xvector = self.displayed_plot.curves[block].x\r
734         yvector = self.displayed_plot.curves[block].y\r
735 \r
736         clicked_points = figure.ginput(N, timeout=-1, show_clicks=True)\r
737 \r
738         points = []\r
739         for clicked_point in clicked_points:\r
740             point = lib.clickedpoint.ClickedPoint()\r
741             point.absolute_coords = clicked_point[0], clicked_point[1]\r
742             point.dest = 0\r
743             #TODO: make this optional?\r
744             #so far, the clicked point is taken, not the corresponding data point\r
745             point.find_graph_coords(xvector, yvector)\r
746             point.is_line_edge = True\r
747             point.is_marker = True\r
748             points.append(point)\r
749         return points\r
750 \r
751     def do_copylog(self):\r
752         '''\r
753         Copies all files in the current playlist that have a note to the destination folder.\r
754         destination: select folder where you want the files to be copied\r
755         use_LVDT_folder: when checked, the files will be copied to a folder called 'LVDT' in the destination folder (for MFP-1D files only)\r
756         '''\r
757         playlist = self.GetActivePlaylist()\r
758         if playlist is not None:\r
759             destination = self.GetStringFromConfig('core', 'copylog', 'destination')\r
760             if not os.path.isdir(destination):\r
761                 os.makedirs(destination)\r
762             for current_file in playlist.files:\r
763                 if current_file.note:\r
764                     shutil.copy(current_file.filename, destination)\r
765                     if current_file.driver.filetype == 'mfp1d':\r
766                         filename = current_file.filename.replace('deflection', 'LVDT', 1)\r
767                         path, name = os.path.split(filename)\r
768                         filename = os.path.join(path, 'lvdt', name)\r
769                         use_LVDT_folder = self.GetBoolFromConfig('core', 'copylog', 'use_LVDT_folder')\r
770                         if use_LVDT_folder:\r
771                             destination = os.path.join(destination, 'LVDT')\r
772                         shutil.copy(filename, destination)\r
773 \r
774     def do_plotmanipulators(self):\r
775         '''\r
776         Please select the plotmanipulators you would like to use\r
777         and define the order in which they will be applied to the data.\r
778 \r
779         Click 'Execute' to apply your changes.\r
780         '''\r
781         self.UpdatePlot()\r
782 \r
783     def do_preferences(self):\r
784         '''\r
785         Please set general preferences for Hooke here.\r
786         hide_curve_extension: hides the extension of the force curve files.\r
787                               not recommended for 'picoforce' files\r
788         '''\r
789         pass\r
790 \r
791     def do_test(self):\r
792         '''\r
793         Use this command for testing purposes. You find do_test in hooke.py.\r
794         '''\r
795         pass\r
796 \r
797     def do_version(self):\r
798         '''\r
799         VERSION\r
800         ------\r
801         Prints the current version and codename, plus library version. Useful for debugging.\r
802         '''\r
803         self.AppendToOutput('Hooke ' + __version__ + ' (' + __codename__ + ')')\r
804         self.AppendToOutput('Released on: ' + __releasedate__)\r
805         self.AppendToOutput('---')\r
806         self.AppendToOutput('Python version: ' + python_version)\r
807         self.AppendToOutput('WxPython version: ' + wx_version)\r
808         self.AppendToOutput('Matplotlib version: ' + mpl_version)\r
809         self.AppendToOutput('SciPy version: ' + scipy_version)\r
810         self.AppendToOutput('NumPy version: ' + numpy_version)\r
811         self.AppendToOutput('ConfigObj version: ' + configobj_version)\r
812         self.AppendToOutput('wxPropertyGrid version: ' + '.'.join([str(PROPGRID_MAJOR), str(PROPGRID_MINOR), str(PROPGRID_RELEASE)]))\r
813         self.AppendToOutput('---')\r
814         self.AppendToOutput('Platform: ' + str(platform.uname()))\r
815         self.AppendToOutput('******************************')\r
816         self.AppendToOutput('Loaded plugins')\r
817         self.AppendToOutput('---')\r
818 \r
819         #sort the plugins into alphabetical order\r
820         plugins_list = [key for key, value in self.plugins.iteritems()]\r
821         plugins_list.sort()\r
822         for plugin in plugins_list:\r
823             self.AppendToOutput(plugin)\r
824 \r
825     def UpdateNote(self):\r
826         #update the note for the active file\r
827         active_file = self.GetActiveFile()\r
828         if active_file is not None:\r
829             self.panelNote.Editor.SetValue(active_file.note)\r
830 \r
831     def UpdatePlaylistsTreeSelection(self):\r
832         playlist = self.GetActivePlaylist()\r
833         if playlist is not None:\r
834             if playlist.index >= 0:\r
835                 self._c['status bar'].set_playlist(playlist)\r
836                 self.UpdateNote()\r
837                 self.UpdatePlot()\r
838 \r
839     def UpdatePlot(self, plot=None):\r
840 \r
841         def add_to_plot(curve, set_scale=True):\r
842             if curve.visible and curve.x and curve.y:\r
843                 #get the index of the subplot to use as destination\r
844                 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1\r
845                 #set all parameters for the plot\r
846                 axes_list[destination].set_title(curve.title)\r
847                 if set_scale:\r
848                     axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)\r
849                     axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)\r
850                     #set the formatting details for the scale\r
851                     formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)\r
852                     formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)\r
853                     axes_list[destination].xaxis.set_major_formatter(formatter_x)\r
854                     axes_list[destination].yaxis.set_major_formatter(formatter_y)\r
855                 if curve.style == 'plot':\r
856                     axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)\r
857                 if curve.style == 'scatter':\r
858                     axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)\r
859                 #add the legend if necessary\r
860                 if curve.legend:\r
861                     axes_list[destination].legend()\r
862 \r
863         if plot is None:\r
864             active_file = self.GetActiveFile()\r
865             if not active_file.driver:\r
866                 #the first time we identify a file, the following need to be set\r
867                 active_file.identify(self.drivers)\r
868                 for curve in active_file.plot.curves:\r
869                     curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')\r
870                     curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')\r
871                     curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')\r
872                     curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')\r
873                     curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')\r
874             if active_file.driver is None:\r
875                 self.AppendToOutput('Invalid file: ' + active_file.filename)\r
876                 return\r
877             self.displayed_plot = copy.deepcopy(active_file.plot)\r
878             #add raw curves to plot\r
879             self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)\r
880             #apply all active plotmanipulators\r
881             self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)\r
882             #add corrected curves to plot\r
883             self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)\r
884         else:\r
885             active_file = None\r
886             self.displayed_plot = copy.deepcopy(plot)\r
887 \r
888         figure = self.GetActiveFigure()\r
889         figure.clear()\r
890 \r
891         #use '0' instead of e.g. '0.00' for scales\r
892         use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')\r
893         #optionally remove the extension from the title of the plot\r
894         hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')\r
895         if hide_curve_extension:\r
896             title = lh.remove_extension(self.displayed_plot.title)\r
897         else:\r
898             title = self.displayed_plot.title\r
899         figure.suptitle(title, fontsize=14)\r
900         #create the list of all axes necessary (rows and columns)\r
901         axes_list =[]\r
902         number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])\r
903         number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])\r
904         for index in range(number_of_rows * number_of_columns):\r
905             axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))\r
906 \r
907         #add all curves to the corresponding plots\r
908         for curve in self.displayed_plot.curves:\r
909             add_to_plot(curve)\r
910 \r
911         #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'\r
912         figure.subplots_adjust(hspace=0.3)\r
913 \r
914         #display results\r
915         self.panelResults.ClearResults()\r
916         if self.displayed_plot.results.has_key(self.results_str):\r
917             for curve in self.displayed_plot.results[self.results_str].results:\r
918                 add_to_plot(curve, set_scale=False)\r
919             self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])\r
920         else:\r
921             self.panelResults.ClearResults()\r
922         #refresh the plot\r
923         figure.canvas.draw()\r
924 \r
925     def _on_curve_select(self, playlist, curve):\r
926         #create the plot tab and add playlist to the dictionary\r
927         plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))\r
928         notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)\r
929         #tab_index = self._c['notebook'].GetSelection()\r
930         playlist.figure = plotPanel.get_figure()\r
931         self.playlists[playlist.name] = playlist\r
932         #self.playlists[playlist.name] = [playlist, figure]\r
933         self._c['status bar'].set_playlist(playlist)\r
934         self.UpdateNote()\r
935         self.UpdatePlot()\r
936 \r
937 \r
938     def _on_playlist_left_doubleclick(self):\r
939         index = self._c['notebook'].GetSelection()\r
940         current_playlist = self._c['notebook'].GetPageText(index)\r
941         if current_playlist != playlist_name:\r
942             index = self._GetPlaylistTab(playlist_name)\r
943             self._c['notebook'].SetSelection(index)\r
944         self._c['status bar'].set_playlist(playlist)\r
945         self.UpdateNote()\r
946         self.UpdatePlot()\r
947 \r
948     def _on_playlist_delete(self, playlist):\r
949         notebook = self.Parent.plotNotebook\r
950         index = self.Parent._GetPlaylistTab(playlist.name)\r
951         notebook.SetSelection(index)\r
952         notebook.DeletePage(notebook.GetSelection())\r
953         self.Parent.DeleteFromPlaylists(playlist_name)\r
954         \r
955 \r
956 class HookeApp (wx.App):\r
957     """A :class:`wx.App` wrapper around :class:`HookeFrame`.\r
958 \r
959     Tosses up a splash screen and then loads :class:`HookeFrame` in\r
960     its own window.\r
961     """\r
962     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):\r
963         self.gui = gui\r
964         self.commands = commands\r
965         self.inqueue = inqueue\r
966         self.outqueue = outqueue\r
967         super(HookeApp, self).__init__(*args, **kwargs)\r
968 \r
969     def OnInit(self):\r
970         self.SetAppName('Hooke')\r
971         self.SetVendorName('')\r
972         self._setup_splash_screen()\r
973 \r
974         height = int(self.gui.config['main height']) # HACK: config should convert\r
975         width = int(self.gui.config['main width'])\r
976         top = int(self.gui.config['main top'])\r
977         left = int(self.gui.config['main left'])\r
978 \r
979         # Sometimes, the ini file gets confused and sets 'left' and\r
980         # 'top' to large negative numbers.  Here we catch and fix\r
981         # this.  Keep small negative numbers, the user might want\r
982         # those.\r
983         if left < -width:\r
984             left = 0\r
985         if top < -height:\r
986             top = 0\r
987 \r
988         self._c = {\r
989             'frame': HookeFrame(\r
990                 self.gui, self.commands, self.inqueue, self.outqueue,\r
991                 parent=None, title='Hooke',\r
992                 pos=(left, top), size=(width, height),\r
993                 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),\r
994             }\r
995         self._c['frame'].Show(True)\r
996         self.SetTopWindow(self._c['frame'])\r
997         return True\r
998 \r
999     def _setup_splash_screen(self):\r
1000         if self.gui.config['show splash screen']:\r
1001             path = self.gui.config['splash screen image']\r
1002             if os.path.isfile(path):\r
1003                 duration = int(self.gui.config['splash screen duration'])  # HACK: config should decode types\r
1004                 wx.SplashScreen(\r
1005                     bitmap=wx.Image(path).ConvertToBitmap(),\r
1006                     splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,\r
1007                     milliseconds=duration,\r
1008                     parent=None)\r
1009                 wx.Yield()\r
1010                 # For some reason splashDuration and sleep do not\r
1011                 # correspond to each other at least not on Windows.\r
1012                 # Maybe it's because duration is in milliseconds and\r
1013                 # sleep in seconds.  Thus we need to increase the\r
1014                 # sleep time a bit. A factor of 1.2 seems to work.\r
1015                 sleepFactor = 1.2\r
1016                 time.sleep(sleepFactor * duration / 1000)\r
1017 \r
1018 \r
1019 class GUI (UserInterface):\r
1020     """wxWindows graphical user interface.\r
1021     """\r
1022     def __init__(self):\r
1023         super(GUI, self).__init__(name='gui')\r
1024 \r
1025     def default_settings(self):\r
1026         """Return a list of :class:`hooke.config.Setting`\s for any\r
1027         configurable UI settings.\r
1028 \r
1029         The suggested section setting is::\r
1030 \r
1031             Setting(section=self.setting_section, help=self.__doc__)\r
1032         """\r
1033         return [\r
1034             Setting(section=self.setting_section, help=self.__doc__),\r
1035             Setting(section=self.setting_section, option='icon image',\r
1036                     value=os.path.join('doc', 'img', 'microscope.ico'),\r
1037                     help='Path to the hooke icon image.'),\r
1038             Setting(section=self.setting_section, option='show splash screen',\r
1039                     value=True,\r
1040                     help='Enable/disable the splash screen'),\r
1041             Setting(section=self.setting_section, option='splash screen image',\r
1042                     value=os.path.join('doc', 'img', 'hooke.jpg'),\r
1043                     help='Path to the Hooke splash screen image.'),\r
1044             Setting(section=self.setting_section, option='splash screen duration',\r
1045                     value=1000,\r
1046                     help='Duration of the splash screen in milliseconds.'),\r
1047             Setting(section=self.setting_section, option='perspective path',\r
1048                     value=os.path.join('resources', 'gui', 'perspective'),\r
1049                     help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.\r
1050             Setting(section=self.setting_section, option='hide extensions',\r
1051                     value=False,\r
1052                     help='Hide file extensions when displaying names.'),\r
1053             Setting(section=self.setting_section, option='folders-workdir',\r
1054                     value='.',\r
1055                     help='This should probably go...'),\r
1056             Setting(section=self.setting_section, option='folders-filters',\r
1057                     value='.',\r
1058                     help='This should probably go...'),\r
1059             Setting(section=self.setting_section, option='active perspective',\r
1060                     value='Default',\r
1061                     help='Name of active perspective file (or "Default").'),\r
1062             Setting(section=self.setting_section, option='folders-filter-index',\r
1063                     value='0',\r
1064                     help='This should probably go...'),\r
1065             Setting(section=self.setting_section, option='main height',\r
1066                     value=500,\r
1067                     help='Height of main window in pixels.'),\r
1068             Setting(section=self.setting_section, option='main width',\r
1069                     value=500,\r
1070                     help='Width of main window in pixels.'),\r
1071             Setting(section=self.setting_section, option='main top',\r
1072                     value=0,\r
1073                     help='Pixels from screen top to top of main window.'),\r
1074             Setting(section=self.setting_section, option='main left',\r
1075                     value=0,\r
1076                     help='Pixels from screen left to left of main window.'),            \r
1077             Setting(section=self.setting_section, option='selected command',\r
1078                     value='load playlist',\r
1079                     help='Name of the initially selected command.'),\r
1080             ]\r
1081 \r
1082     def _app(self, commands, ui_to_command_queue, command_to_ui_queue):\r
1083         redirect = True\r
1084         if __debug__:\r
1085             redirect=False\r
1086         app = HookeApp(gui=self,\r
1087                        commands=commands,\r
1088                        inqueue=ui_to_command_queue,\r
1089                        outqueue=command_to_ui_queue,\r
1090                        redirect=redirect)\r
1091         return app\r
1092 \r
1093     def run(self, commands, ui_to_command_queue, command_to_ui_queue):\r
1094         app = self._app(commands, ui_to_command_queue, command_to_ui_queue)\r
1095         app.MainLoop()\r