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