hooke.ui.gui was getting complicated, so I stripped it down for a moment.
[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                 })\r
83         self.SetMenuBar(self._c['menu bar'])\r
84 \r
85         self._c['status bar'] = statusbar.StatusBar(\r
86             parent=self,\r
87             style=wx.ST_SIZEGRIP)\r
88         self.SetStatusBar(self._c['status bar'])\r
89 \r
90         self._update_perspectives()\r
91         self._bind_events()\r
92 \r
93         name = self.gui.config['active perspective']\r
94         return # TODO: cleanup\r
95         menu_item = self.GetPerspectiveMenuItem(name)\r
96         if menu_item is not None:\r
97             self._on_restore_perspective(menu_item)\r
98             #TODO: config setting to remember playlists from last session\r
99         self.playlists = self._c['playlists'].Playlists\r
100         self._displayed_plot = None\r
101         #load default list, if possible\r
102         self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist'))\r
103 \r
104     def _setup_panels(self):\r
105         client_size = self.GetClientSize()\r
106         for label,p,style in [\r
107 #            ('folders', wx.GenericDirCtrl(\r
108 #                    parent=self,\r
109 #                    dir=self.gui.config['folders-workdir'],\r
110 #                    size=(200, 250),\r
111 #                    style=wx.DIRCTRL_SHOW_FILTERS,\r
112 #                    filter=self.gui.config['folders-filters'],\r
113 #                    defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'),  #HACK: config should convert\r
114 #            ('playlists', panel.PANELS['playlist'](\r
115 #                    callbacks={},\r
116 #                    config=self.gui.config,\r
117 #                    parent=self,\r
118 #                    style=wx.WANTS_CHARS|wx.NO_BORDER,\r
119 #                    # WANTS_CHARS so the panel doesn't eat the Return key.\r
120 #                    size=(160, 200)), 'left'),\r
121 #            ('note', panel.note.Note(\r
122 #                    parent=self\r
123 #                    style=wx.WANTS_CHARS|wx.NO_BORDER,\r
124 #                    size=(160, 200)), 'left'),\r
125 #            ('notebook', Notebook(\r
126 #                    parent=self,\r
127 #                    pos=wx.Point(client_size.x, client_size.y),\r
128 #                    size=wx.Size(430, 200),\r
129 #                    style=aui.AUI_NB_DEFAULT_STYLE\r
130 #                    | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),\r
131             ('commands', panel.PANELS['commands'](\r
132                     commands=self.commands,\r
133                     selected=self.gui.config['selected command'],\r
134                     callbacks={\r
135                         'execute': self.execute_command,\r
136                         'select_plugin': self.select_plugin,\r
137                         'select_command': self.select_command,\r
138 #                        'selection_changed': self.panelProperties.select(self, method, command),  #SelectedTreeItem = selected_item,\r
139                         },\r
140                     parent=self,\r
141                     style=wx.WANTS_CHARS|wx.NO_BORDER,\r
142                     # WANTS_CHARS so the panel doesn't eat the Return key.\r
143 #                    size=(160, 200)\r
144                     ), 'center'),\r
145             #('properties', panel.propertyeditor.PropertyEditor(self),'right'),\r
146 #            ('assistant', wx.TextCtrl(\r
147 #                    parent=self,\r
148 #                    pos=wx.Point(0, 0),\r
149 #                    size=wx.Size(150, 90),\r
150 #                    style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),\r
151 #            ('output', wx.TextCtrl(\r
152 #                    parent=self,\r
153 #                    pos=wx.Point(0, 0),\r
154 #                    size=wx.Size(150, 90),\r
155 #                    style=wx.NO_BORDER|wx.TE_MULTILINE), 'bottom'),\r
156 #            ('results', panel.results.Results(self), 'bottom'),\r
157             ]:\r
158             self._add_panel(label, p, style)\r
159         #self._c['assistant'].SetEditable(False)\r
160 \r
161     def _add_panel(self, label, panel, style):\r
162         self._c[label] = panel\r
163         cap_label = label.capitalize()\r
164         info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)\r
165         info.PaneBorder(False).CloseButton(True).MaximizeButton(False)\r
166         if style == 'top':\r
167             info.Top()\r
168         elif style == 'center':\r
169             info.CenterPane()\r
170         elif style == 'left':\r
171             info.Left()\r
172         elif style == 'right':\r
173             info.Right()\r
174         else:\r
175             assert style == 'bottom', style\r
176             info.Bottom()\r
177         self._c['manager'].AddPane(panel, info)\r
178 \r
179     def _setup_toolbars(self):\r
180         self._c['navigation bar'] = navbar.NavBar(\r
181             callbacks={\r
182                 'next': self._next_curve,\r
183                 'previous': self._previous_curve,\r
184                 },\r
185             parent=self,\r
186             style=wx.TB_FLAT | wx.TB_NODIVIDER)\r
187         self._c['manager'].AddPane(\r
188             self._c['navigation bar'],\r
189             aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'\r
190                 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False\r
191                 ).RightDockable(False))\r
192 \r
193     def _bind_events(self):\r
194         # TODO: figure out if we can use the eventManager for menu\r
195         # ranges and events of 'self' without raising an assertion\r
196         # fail error.\r
197         self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)\r
198         self.Bind(wx.EVT_SIZE, self._on_size)\r
199         self.Bind(wx.EVT_CLOSE, self._on_close)\r
200         self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)\r
201         self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)\r
202 \r
203         return # TODO: cleanup\r
204         for value in self._c['menu bar']._c['view']._c.values():\r
205             self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)\r
206 \r
207         self.Bind(wx.EVT_MENU, self._on_save_perspective,\r
208                   self._c['menu bar']._c['perspective']._c['save'])\r
209         self.Bind(wx.EVT_MENU, self._on_delete_perspective,\r
210                   self._c['menu bar']._c['perspective']._c['delete'])\r
211 \r
212         treeCtrl = self._c['folders'].GetTreeCtrl()\r
213         treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)\r
214         \r
215         # TODO: playlist callbacks\r
216         return # TODO: cleanup\r
217         evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)\r
218         #property editor\r
219         self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)\r
220         #results panel\r
221         self.panelResults.results_list.OnCheckItem = self.OnResultsCheck\r
222 \r
223     def _command_by_name(self, name):\r
224         cs = [c for c in self.commands if c.name == name]\r
225         if len(cs) == 0:\r
226             raise KeyError(name)\r
227         elif len(cs) > 1:\r
228             raise Exception('Multiple commands named "%s"' % name)\r
229         return cs[0]\r
230 \r
231     def execute_command(self, _class=None, method=None,\r
232                         command=None, args=None):\r
233         self.inqueue.put(CommandMessage(command, args))\r
234         results = []\r
235         while True:\r
236             msg = self.outqueue.get()\r
237             results.append(msg)\r
238             print type(msg), msg\r
239             if isinstance(msg, Exit):\r
240                 self._on_close()\r
241                 break\r
242             elif isinstance(msg, CommandExit):\r
243                 # TODO: display command complete\r
244                 break\r
245             elif isinstance(msg, ReloadUserInterfaceConfig):\r
246                 self.gui.reload_config(msg.config)\r
247                 continue\r
248             elif isinstance(msg, Request):\r
249                 h = handler.HANDLERS[msg.type]\r
250                 h.run(self, msg)  # TODO: pause for response?\r
251                 continue\r
252         pp = getattr(\r
253             self, '_postprocess_%s' % command.name.replace(' ', '_'), None)\r
254         if pp != None:\r
255             pp(command=command, results=results)\r
256         return results\r
257 \r
258     def _handle_request(self, msg):\r
259         """Repeatedly try to get a response to `msg`.\r
260         """\r
261         if prompt == None:\r
262             raise NotImplementedError('_%s_request_prompt' % msg.type)\r
263         prompt_string = prompt(msg)\r
264         parser = getattr(self, '_%s_request_parser' % msg.type, None)\r
265         if parser == None:\r
266             raise NotImplementedError('_%s_request_parser' % msg.type)\r
267         error = None\r
268         while True:\r
269             if error != None:\r
270                 self.cmd.stdout.write(''.join([\r
271                         error.__class__.__name__, ': ', str(error), '\n']))\r
272             self.cmd.stdout.write(prompt_string)\r
273             value = parser(msg, self.cmd.stdin.readline())\r
274             try:\r
275                 response = msg.response(value)\r
276                 break\r
277             except ValueError, error:\r
278                 continue\r
279         self.inqueue.put(response)\r
280 \r
281 \r
282     def _GetActiveFileIndex(self):\r
283         lib.playlist.Playlist = self.GetActivePlaylist()\r
284         #get the selected item from the tree\r
285         selected_item = self._c['playlists']._c['tree'].GetSelection()\r
286         #test if a playlist or a curve was double-clicked\r
287         if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
288             return -1\r
289         else:\r
290             count = 0\r
291             selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
292             while selected_item.IsOk():\r
293                 count += 1\r
294                 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
295             return count\r
296 \r
297     def _GetPlaylistTab(self, name):\r
298         for index, page in enumerate(self._c['notebook']._tabs._pages):\r
299             if page.caption == name:\r
300                 return index\r
301         return -1\r
302 \r
303     def _restore_perspective(self, name):\r
304         # TODO: cleanup\r
305         self.gui.config['active perspective'] = name  # TODO: push to engine's Hooke\r
306         self._c['manager'].LoadPerspective(self._perspectives[name])\r
307         self._c['manager'].Update()\r
308         for pane in self._c['manager'].GetAllPanes():\r
309             if pane.name in self._c['menu bar']._c['view']._c.keys():\r
310                 pane.Check(pane.window.IsShown())\r
311 \r
312     def _SavePerspectiveToFile(self, name, perspective):\r
313         filename = ''.join([name, '.txt'])\r
314         filename = lh.get_file_path(filename, ['perspective'])\r
315         perspectivesFile = open(filename, 'w')\r
316         perspectivesFile.write(perspective)\r
317         perspectivesFile.close()\r
318 \r
319     def select_plugin(self, _class=None, method=None, plugin=None):\r
320         for option in config[section]:\r
321             properties.append([option, config[section][option]])\r
322 \r
323     def select_command(self, _class, method, command):\r
324         self.select_plugin(plugin=command.plugin)\r
325         plugin = self.GetItemText(selected_item)\r
326         if plugin != 'core':\r
327             doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')\r
328         else:\r
329             doc_string = 'The module "core" contains Hooke core functionality'\r
330         if doc_string is not None:\r
331             self.panelAssistant.ChangeValue(doc_string)\r
332         else:\r
333             self.panelAssistant.ChangeValue('')\r
334         panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)\r
335         self.gui.config['selected command'] = command\r
336 \r
337     def AddPlaylistFromFiles(self, files=[], name='Untitled'):\r
338         if files:\r
339             playlist = lib.playlist.Playlist(self, self.drivers)\r
340             for item in files:\r
341                 playlist.add_curve(item)\r
342         if playlist.count > 0:\r
343             playlist.name = self._GetUniquePlaylistName(name)\r
344             playlist.reset()\r
345             self.AddTayliss(playlist)\r
346 \r
347     def AppendToOutput(self, text):\r
348         self.panelOutput.AppendText(''.join([text, '\n']))\r
349 \r
350     def AppliesPlotmanipulator(self, name):\r
351         '''\r
352         Returns True if the plotmanipulator 'name' is applied, False otherwise\r
353         name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')\r
354         '''\r
355         return self.GetBoolFromConfig('core', 'plotmanipulators', name)\r
356 \r
357     def ApplyPlotmanipulators(self, plot, plot_file):\r
358         '''\r
359         Apply all active plotmanipulators.\r
360         '''\r
361         if plot is not None and plot_file is not None:\r
362             manipulated_plot = copy.deepcopy(plot)\r
363             for plotmanipulator in self.plotmanipulators:\r
364                 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):\r
365                     manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)\r
366             return manipulated_plot\r
367 \r
368     def GetActiveFigure(self):\r
369         playlist_name = self.GetActivePlaylistName()\r
370         figure = self.playlists[playlist_name].figure\r
371         if figure is not None:\r
372             return figure\r
373         return None\r
374 \r
375     def GetActiveFile(self):\r
376         playlist = self.GetActivePlaylist()\r
377         if playlist is not None:\r
378             return playlist.get_active_file()\r
379         return None\r
380 \r
381     def GetActivePlot(self):\r
382         playlist = self.GetActivePlaylist()\r
383         if playlist is not None:\r
384             return playlist.get_active_file().plot\r
385         return None\r
386 \r
387     def GetDisplayedPlot(self):\r
388         plot = copy.deepcopy(self.displayed_plot)\r
389         #plot.curves = []\r
390         #plot.curves = copy.deepcopy(plot.curves)\r
391         return plot\r
392 \r
393     def GetDisplayedPlotCorrected(self):\r
394         plot = copy.deepcopy(self.displayed_plot)\r
395         plot.curves = []\r
396         plot.curves = copy.deepcopy(plot.corrected_curves)\r
397         return plot\r
398 \r
399     def GetDisplayedPlotRaw(self):\r
400         plot = copy.deepcopy(self.displayed_plot)\r
401         plot.curves = []\r
402         plot.curves = copy.deepcopy(plot.raw_curves)\r
403         return plot\r
404 \r
405     def GetDockArt(self):\r
406         return self._c['manager'].GetArtProvider()\r
407 \r
408     def GetPlotmanipulator(self, name):\r
409         '''\r
410         Returns a plot manipulator function from its name\r
411         '''\r
412         for plotmanipulator in self.plotmanipulators:\r
413             if plotmanipulator.name == name:\r
414                 return plotmanipulator\r
415         return None\r
416 \r
417     def GetPerspectiveMenuItem(self, name):\r
418         if self._perspectives.has_key(name):\r
419             perspectives_list = [key for key, value in self._perspectives.iteritems()]\r
420             perspectives_list.sort()\r
421             index = perspectives_list.index(name)\r
422             perspective_Id = ID_FirstPerspective + index\r
423             menu_item = self._c['menu bar'].FindItemById(perspective_Id)\r
424             return menu_item\r
425         else:\r
426             return None\r
427 \r
428     def HasPlotmanipulator(self, name):\r
429         '''\r
430         returns True if the plotmanipulator 'name' is loaded, False otherwise\r
431         '''\r
432         for plotmanipulator in self.plotmanipulators:\r
433             if plotmanipulator.command == name:\r
434                 return True\r
435         return False\r
436 \r
437     def _on_about(self, *args):\r
438         dialog = wx.MessageDialog(\r
439             parent=self,\r
440             message=self.gui._splash_text(),\r
441             caption='About Hooke',\r
442             style=wx.OK|wx.ICON_INFORMATION)\r
443         dialog.ShowModal()\r
444         dialog.Destroy()\r
445 \r
446     def _on_close(self, *args):\r
447         # apply changes\r
448         self.gui.config['main height'] = str(self.GetSize().GetHeight())\r
449         self.gui.config['main left'] = str(self.GetPosition()[0])\r
450         self.gui.config['main top'] = str(self.GetPosition()[1])\r
451         self.gui.config['main width'] = str(self.GetSize().GetWidth())\r
452         # push changes back to Hooke.config?\r
453         self._c['manager'].UnInit()\r
454         del self._c['manager']\r
455         self.Destroy()\r
456 \r
457     def _update_perspectives(self):\r
458         """Add perspectives to menubar and _perspectives.\r
459         """\r
460         self._perspectives = {\r
461             'Default': self._c['manager'].SavePerspective(),\r
462             }\r
463         path = self.gui.config['perspective path']\r
464         if os.path.isdir(path):\r
465             files = sorted(os.listdir(path))\r
466             for fname in files:\r
467                 name, extension = os.path.splitext(fname)\r
468                 if extension != '.txt':\r
469                     continue\r
470                 fpath = os.path.join(path, fpath)\r
471                 if not os.path.isfile(fpath):\r
472                     continue\r
473                 perspective = None\r
474                 with open(fpath, 'rU') as f:\r
475                     perspective = f.readline()\r
476                 if perspective:\r
477                     self._perspectives[name] = perspective\r
478 \r
479         selected_perspective = self.gui.config['active perspective']\r
480         if not self._perspectives.has_key(selected_perspective):\r
481             self.gui.config['active perspective'] = 'Default'  # TODO: push to engine's Hooke\r
482 \r
483         self._update_perspective_menu()\r
484         self._restore_perspective(selected_perspective)\r
485 \r
486     def _update_perspective_menu(self):\r
487         self._c['menu bar']._c['perspective'].update(\r
488             sorted(self._perspectives.keys()),\r
489             self.gui.config['active perspective'],\r
490             self._on_restore_perspective)\r
491 \r
492     def _on_restore_perspective(self, event):\r
493         name = self._c['menu bar'].FindItemById(event.GetId()).GetLabel()\r
494         self._restore_perspective(name)\r
495 \r
496     def _on_save_perspective(self, event):\r
497         def nameExists(name):\r
498             menu_position = self._c['menu bar'].FindMenu('Perspective')\r
499             menu = self._c['menu bar'].GetMenu(menu_position)\r
500             for item in menu.GetMenuItems():\r
501                 if item.GetText() == name:\r
502                     return True\r
503             return False\r
504 \r
505         done = False\r
506         while not done:\r
507             dialog = wx.TextEntryDialog(self, 'Enter a name for the new perspective:', 'Save perspective')\r
508             dialog.SetValue('New perspective')\r
509             if dialog.ShowModal() != wx.ID_OK:\r
510                 return\r
511             else:\r
512                 name = dialog.GetValue()\r
513 \r
514             if nameExists(name):\r
515                 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
516                 if dialogConfirm.ShowModal() == wx.ID_YES:\r
517                     done = True\r
518             else:\r
519                 done = True\r
520 \r
521         perspective = self._c['manager'].SavePerspective()\r
522         self._SavePerspectiveToFile(name, perspective)\r
523         self.gui.config['active perspectives'] = name\r
524         self._update_perspective_menu()\r
525 #        if nameExists(name):\r
526 #            #check the corresponding menu item\r
527 #            menu_item = self.GetPerspectiveMenuItem(name)\r
528 #            #replace the perspectiveStr in _pespectives\r
529 #            self._perspectives[name] = perspective\r
530 #        else:\r
531 #            #because we deal with radio items, we need to do some extra work\r
532 #            #delete all menu items from the perspectives menu\r
533 #            for item in self._perspectives_menu.GetMenuItems():\r
534 #                self._perspectives_menu.DeleteItem(item)\r
535 #            #recreate the perspectives menu\r
536 #            self._perspectives_menu.Append(ID_SavePerspective, 'Save Perspective')\r
537 #            self._perspectives_menu.Append(ID_DeletePerspective, 'Delete Perspective')\r
538 #            self._perspectives_menu.AppendSeparator()\r
539 #            #convert the perspectives dictionary into a list\r
540 #            # the list contains:\r
541 #            #[0]: name of the perspective\r
542 #            #[1]: perspective\r
543 #            perspectives_list = [key for key, value in self._perspectives.iteritems()]\r
544 #            perspectives_list.append(name)\r
545 #            perspectives_list.sort()\r
546 #            #add all previous perspectives\r
547 #            for index, item in enumerate(perspectives_list):\r
548 #                menu_item = self._perspectives_menu.AppendRadioItem(ID_FirstPerspective + index, item)\r
549 #                if item == name:\r
550 #                    menu_item.Check()\r
551 #            #add the new perspective to _perspectives\r
552 #            self._perspectives[name] = perspective\r
553 \r
554     def _on_delete_perspective(self, event):\r
555         dialog = panel.selection.Selection(\r
556             options=sorted(os.listdir(self.gui.config['perspective path'])),\r
557             message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",\r
558             button_id=wx.ID_DELETE,\r
559             callbacks={'button': self._on_delete_perspective},\r
560             selection_style='multiple',\r
561             parent=self,\r
562             label='Delete perspective(s)',\r
563             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)\r
564         dialog.CenterOnScreen()\r
565         dialog.ShowModal()\r
566         dialog.Destroy()\r
567         self._update_perspective_menu()\r
568         # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258\r
569         #   http://trac.wxwidgets.org/ticket/3258 \r
570         # ) that makes the radio item indicator in the menu disappear.\r
571         # The code should be fine once this issue is fixed.\r
572 \r
573     def _on_delete_perspective(self, _class, method, options, selected):\r
574         for p in selected:\r
575             self._perspectives.remove(p)\r
576             if p == self.gui.config['active perspective']:\r
577                 self.gui.config['active perspective'] = 'Default'\r
578             path = os.path.join(self.gui.config['perspective path'],\r
579                                 p+'.txt')\r
580             remove(path)\r
581         self._update_perspective_menu()\r
582 \r
583     def _on_dir_ctrl_left_double_click(self, event):\r
584         file_path = self.panelFolders.GetPath()\r
585         if os.path.isfile(file_path):\r
586             if file_path.endswith('.hkp'):\r
587                 self.do_loadlist(file_path)\r
588         event.Skip()\r
589 \r
590     def _on_erase_background(self, event):\r
591         event.Skip()\r
592 \r
593     def _next_curve(self, *args):\r
594         """Call the `next curve` command.\r
595         """\r
596         results = self.execute_command(\r
597             command=self._command_by_name('next curve'))\r
598         if isinstance(results[-1], Success):\r
599             self.execute_command(\r
600                 command=self._command_by_name('get curve'))\r
601 \r
602     def _previous_curve(self, *args):\r
603         """Call the `previous curve` command.\r
604         """\r
605         self.execute_command(\r
606             command=self._command_by_name('previous curve'))\r
607         if isinstance(results[-1], Success):\r
608             self.execute_command(\r
609                 command=self._command_by_name('get curve'))\r
610 \r
611     def _postprocess_get_curve(self, command, results):\r
612         """Update `self` to show the curve.\r
613         """\r
614         if not isinstance(results[-1], Success):\r
615             return  # error executing 'get curve'\r
616         assert len(results) == 2, results\r
617         curve = results[0]\r
618         print curve\r
619 \r
620         selected_item = self._c['playlists']._c['tree'].GetSelection()\r
621         if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
622             #GetFirstChild returns a tuple\r
623             #we only need the first element\r
624             next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]\r
625         else:\r
626             next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)\r
627             if not next_item.IsOk():\r
628                 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)\r
629                 #GetFirstChild returns a tuple\r
630                 #we only need the first element\r
631                 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]\r
632         self._c['playlists']._c['tree'].SelectItem(next_item, True)\r
633         if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
634             playlist = self.GetActivePlaylist()\r
635             if playlist.count > 1:\r
636                 playlist.next()\r
637                 self._c['status bar'].set_playlist(playlist)\r
638                 self.UpdateNote()\r
639                 self.UpdatePlot()\r
640 \r
641     def _on_notebook_page_close(self, event):\r
642         ctrl = event.GetEventObject()\r
643         playlist_name = ctrl.GetPageText(ctrl._curpage)\r
644         self.DeleteFromPlaylists(playlist_name)\r
645 \r
646     def OnPaneClose(self, event):\r
647         event.Skip()\r
648 \r
649     def OnPropGridChanged (self, event):\r
650         prop = event.GetProperty()\r
651         if prop:\r
652             item_section = self.panelProperties.SelectedTreeItem\r
653             item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)\r
654             plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)\r
655             config = self.gui.config[plugin]\r
656             property_section = self._c['commands']._c['tree'].GetItemText(item_section)\r
657             property_key = prop.GetName()\r
658             property_value = prop.GetDisplayedString()\r
659 \r
660             config[property_section][property_key]['value'] = property_value\r
661 \r
662     def OnResultsCheck(self, index, flag):\r
663         results = self.GetActivePlot().results\r
664         if results.has_key(self.results_str):\r
665             results[self.results_str].results[index].visible = flag\r
666             results[self.results_str].update()\r
667             self.UpdatePlot()\r
668 \r
669 \r
670     def _on_size(self, event):\r
671         event.Skip()\r
672 \r
673     def OnUpdateNote(self, event):\r
674         '''\r
675         Saves the note to the active file.\r
676         '''\r
677         active_file = self.GetActiveFile()\r
678         active_file.note = self.panelNote.Editor.GetValue()\r
679 \r
680     def _on_view(self, event):\r
681         menu_id = event.GetId()\r
682         menu_item = self._c['menu bar'].FindItemById(menu_id)\r
683         menu_label = menu_item.GetLabel()\r
684 \r
685         pane = self._c['manager'].GetPane(menu_label)\r
686         pane.Show(not pane.IsShown())\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