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         pass\r
386 \r
387     def AddPlaylistFromFiles(self, files=[], name='Untitled'):\r
388         if files:\r
389             playlist = lib.playlist.Playlist(self, self.drivers)\r
390             for item in files:\r
391                 playlist.add_curve(item)\r
392         if playlist.count > 0:\r
393             playlist.name = self._GetUniquePlaylistName(name)\r
394             playlist.reset()\r
395             self.AddTayliss(playlist)\r
396 \r
397     def AppliesPlotmanipulator(self, name):\r
398         '''\r
399         Returns True if the plotmanipulator 'name' is applied, False otherwise\r
400         name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')\r
401         '''\r
402         return self.GetBoolFromConfig('core', 'plotmanipulators', name)\r
403 \r
404     def ApplyPlotmanipulators(self, plot, plot_file):\r
405         '''\r
406         Apply all active plotmanipulators.\r
407         '''\r
408         if plot is not None and plot_file is not None:\r
409             manipulated_plot = copy.deepcopy(plot)\r
410             for plotmanipulator in self.plotmanipulators:\r
411                 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):\r
412                     manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)\r
413             return manipulated_plot\r
414 \r
415     def GetActiveFigure(self):\r
416         playlist_name = self.GetActivePlaylistName()\r
417         figure = self.playlists[playlist_name].figure\r
418         if figure is not None:\r
419             return figure\r
420         return None\r
421 \r
422     def GetActiveFile(self):\r
423         playlist = self.GetActivePlaylist()\r
424         if playlist is not None:\r
425             return playlist.get_active_file()\r
426         return None\r
427 \r
428     def GetActivePlot(self):\r
429         playlist = self.GetActivePlaylist()\r
430         if playlist is not None:\r
431             return playlist.get_active_file().plot\r
432         return None\r
433 \r
434     def GetDisplayedPlot(self):\r
435         plot = copy.deepcopy(self.displayed_plot)\r
436         #plot.curves = []\r
437         #plot.curves = copy.deepcopy(plot.curves)\r
438         return plot\r
439 \r
440     def GetDisplayedPlotCorrected(self):\r
441         plot = copy.deepcopy(self.displayed_plot)\r
442         plot.curves = []\r
443         plot.curves = copy.deepcopy(plot.corrected_curves)\r
444         return plot\r
445 \r
446     def GetDisplayedPlotRaw(self):\r
447         plot = copy.deepcopy(self.displayed_plot)\r
448         plot.curves = []\r
449         plot.curves = copy.deepcopy(plot.raw_curves)\r
450         return plot\r
451 \r
452     def GetDockArt(self):\r
453         return self._c['manager'].GetArtProvider()\r
454 \r
455     def GetPlotmanipulator(self, name):\r
456         '''\r
457         Returns a plot manipulator function from its name\r
458         '''\r
459         for plotmanipulator in self.plotmanipulators:\r
460             if plotmanipulator.name == name:\r
461                 return plotmanipulator\r
462         return None\r
463 \r
464     def HasPlotmanipulator(self, name):\r
465         '''\r
466         returns True if the plotmanipulator 'name' is loaded, False otherwise\r
467         '''\r
468         for plotmanipulator in self.plotmanipulators:\r
469             if plotmanipulator.command == name:\r
470                 return True\r
471         return False\r
472 \r
473 \r
474     def _on_dir_ctrl_left_double_click(self, event):\r
475         file_path = self.panelFolders.GetPath()\r
476         if os.path.isfile(file_path):\r
477             if file_path.endswith('.hkp'):\r
478                 self.do_loadlist(file_path)\r
479         event.Skip()\r
480 \r
481     def _on_erase_background(self, event):\r
482         event.Skip()\r
483 \r
484     def _on_notebook_page_close(self, event):\r
485         ctrl = event.GetEventObject()\r
486         playlist_name = ctrl.GetPageText(ctrl._curpage)\r
487         self.DeleteFromPlaylists(playlist_name)\r
488 \r
489     def OnPaneClose(self, event):\r
490         event.Skip()\r
491 \r
492     def OnPropGridChanged (self, event):\r
493         prop = event.GetProperty()\r
494         if prop:\r
495             item_section = self.panelProperties.SelectedTreeItem\r
496             item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)\r
497             plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)\r
498             config = self.gui.config[plugin]\r
499             property_section = self._c['commands']._c['tree'].GetItemText(item_section)\r
500             property_key = prop.GetName()\r
501             property_value = prop.GetDisplayedString()\r
502 \r
503             config[property_section][property_key]['value'] = property_value\r
504 \r
505     def OnResultsCheck(self, index, flag):\r
506         results = self.GetActivePlot().results\r
507         if results.has_key(self.results_str):\r
508             results[self.results_str].results[index].visible = flag\r
509             results[self.results_str].update()\r
510             self.UpdatePlot()\r
511 \r
512 \r
513     def _on_size(self, event):\r
514         event.Skip()\r
515 \r
516     def OnUpdateNote(self, event):\r
517         '''\r
518         Saves the note to the active file.\r
519         '''\r
520         active_file = self.GetActiveFile()\r
521         active_file.note = self.panelNote.Editor.GetValue()\r
522 \r
523     def UpdateNote(self):\r
524         #update the note for the active file\r
525         active_file = self.GetActiveFile()\r
526         if active_file is not None:\r
527             self.panelNote.Editor.SetValue(active_file.note)\r
528 \r
529     def UpdatePlaylistsTreeSelection(self):\r
530         playlist = self.GetActivePlaylist()\r
531         if playlist is not None:\r
532             if playlist.index >= 0:\r
533                 self._c['status bar'].set_playlist(playlist)\r
534                 self.UpdateNote()\r
535                 self.UpdatePlot()\r
536 \r
537     def UpdatePlot(self, plot=None):\r
538 \r
539         def add_to_plot(curve, set_scale=True):\r
540             if curve.visible and curve.x and curve.y:\r
541                 #get the index of the subplot to use as destination\r
542                 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1\r
543                 #set all parameters for the plot\r
544                 axes_list[destination].set_title(curve.title)\r
545                 if set_scale:\r
546                     axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)\r
547                     axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)\r
548                     #set the formatting details for the scale\r
549                     formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)\r
550                     formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)\r
551                     axes_list[destination].xaxis.set_major_formatter(formatter_x)\r
552                     axes_list[destination].yaxis.set_major_formatter(formatter_y)\r
553                 if curve.style == 'plot':\r
554                     axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)\r
555                 if curve.style == 'scatter':\r
556                     axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)\r
557                 #add the legend if necessary\r
558                 if curve.legend:\r
559                     axes_list[destination].legend()\r
560 \r
561         if plot is None:\r
562             active_file = self.GetActiveFile()\r
563             if not active_file.driver:\r
564                 #the first time we identify a file, the following need to be set\r
565                 active_file.identify(self.drivers)\r
566                 for curve in active_file.plot.curves:\r
567                     curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')\r
568                     curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')\r
569                     curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')\r
570                     curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')\r
571                     curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')\r
572             if active_file.driver is None:\r
573                 self.AppendToOutput('Invalid file: ' + active_file.filename)\r
574                 return\r
575             self.displayed_plot = copy.deepcopy(active_file.plot)\r
576             #add raw curves to plot\r
577             self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)\r
578             #apply all active plotmanipulators\r
579             self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)\r
580             #add corrected curves to plot\r
581             self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)\r
582         else:\r
583             active_file = None\r
584             self.displayed_plot = copy.deepcopy(plot)\r
585 \r
586         figure = self.GetActiveFigure()\r
587         figure.clear()\r
588 \r
589         #use '0' instead of e.g. '0.00' for scales\r
590         use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')\r
591         #optionally remove the extension from the title of the plot\r
592         hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')\r
593         if hide_curve_extension:\r
594             title = lh.remove_extension(self.displayed_plot.title)\r
595         else:\r
596             title = self.displayed_plot.title\r
597         figure.suptitle(title, fontsize=14)\r
598         #create the list of all axes necessary (rows and columns)\r
599         axes_list =[]\r
600         number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])\r
601         number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])\r
602         for index in range(number_of_rows * number_of_columns):\r
603             axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))\r
604 \r
605         #add all curves to the corresponding plots\r
606         for curve in self.displayed_plot.curves:\r
607             add_to_plot(curve)\r
608 \r
609         #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'\r
610         figure.subplots_adjust(hspace=0.3)\r
611 \r
612         #display results\r
613         self.panelResults.ClearResults()\r
614         if self.displayed_plot.results.has_key(self.results_str):\r
615             for curve in self.displayed_plot.results[self.results_str].results:\r
616                 add_to_plot(curve, set_scale=False)\r
617             self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])\r
618         else:\r
619             self.panelResults.ClearResults()\r
620         #refresh the plot\r
621         figure.canvas.draw()\r
622 \r
623     def _on_curve_select(self, playlist, curve):\r
624         #create the plot tab and add playlist to the dictionary\r
625         plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))\r
626         notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)\r
627         #tab_index = self._c['notebook'].GetSelection()\r
628         playlist.figure = plotPanel.get_figure()\r
629         self.playlists[playlist.name] = playlist\r
630         #self.playlists[playlist.name] = [playlist, figure]\r
631         self._c['status bar'].set_playlist(playlist)\r
632         self.UpdateNote()\r
633         self.UpdatePlot()\r
634 \r
635 \r
636     def _on_playlist_left_doubleclick(self):\r
637         index = self._c['notebook'].GetSelection()\r
638         current_playlist = self._c['notebook'].GetPageText(index)\r
639         if current_playlist != playlist_name:\r
640             index = self._GetPlaylistTab(playlist_name)\r
641             self._c['notebook'].SetSelection(index)\r
642         self._c['status bar'].set_playlist(playlist)\r
643         self.UpdateNote()\r
644         self.UpdatePlot()\r
645 \r
646     def _on_playlist_delete(self, playlist):\r
647         notebook = self.Parent.plotNotebook\r
648         index = self.Parent._GetPlaylistTab(playlist.name)\r
649         notebook.SetSelection(index)\r
650         notebook.DeletePage(notebook.GetSelection())\r
651         self.Parent.DeleteFromPlaylists(playlist_name)\r
652 \r
653 \r
654 \r
655     # Command panel interface\r
656 \r
657     def select_command(self, _class, method, command):\r
658         self.select_plugin(plugin=command.plugin)\r
659         plugin = self.GetItemText(selected_item)\r
660         if plugin != 'core':\r
661             doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')\r
662         else:\r
663             doc_string = 'The module "core" contains Hooke core functionality'\r
664         if doc_string is not None:\r
665             self.panelAssistant.ChangeValue(doc_string)\r
666         else:\r
667             self.panelAssistant.ChangeValue('')\r
668         panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)\r
669         self.gui.config['selected command'] = command\r
670 \r
671 \r
672 \r
673     # Navbar interface\r
674 \r
675     def _next_curve(self, *args):\r
676         """Call the `next curve` command.\r
677         """\r
678         results = self.execute_command(\r
679             command=self._command_by_name('next curve'))\r
680         if isinstance(results[-1], Success):\r
681             self.execute_command(\r
682                 command=self._command_by_name('get curve'))\r
683 \r
684     def _previous_curve(self, *args):\r
685         """Call the `previous curve` command.\r
686         """\r
687         self.execute_command(\r
688             command=self._command_by_name('previous curve'))\r
689         if isinstance(results[-1], Success):\r
690             self.execute_command(\r
691                 command=self._command_by_name('get curve'))\r
692 \r
693 \r
694 \r
695     # Panel display handling\r
696 \r
697     def _on_panel_visibility(self, _class, method, panel_name, visible):\r
698         pane = self._c['manager'].GetPane(panel_name)\r
699         print visible\r
700         pane.Show(visible)\r
701         #if we don't do the following, the Folders pane does not resize properly on hide/show\r
702         if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():\r
703             #folders_size = pane.GetSize()\r
704             self.panelFolders.Fit()\r
705         self._c['manager'].Update()\r
706 \r
707     def _setup_perspectives(self):\r
708         """Add perspectives to menubar and _perspectives.\r
709         """\r
710         self._perspectives = {\r
711             'Default': self._c['manager'].SavePerspective(),\r
712             }\r
713         path = self.gui.config['perspective path']\r
714         if os.path.isdir(path):\r
715             files = sorted(os.listdir(path))\r
716             for fname in files:\r
717                 name, extension = os.path.splitext(fname)\r
718                 if extension != self.gui.config['perspective extension']:\r
719                     continue\r
720                 fpath = os.path.join(path, fname)\r
721                 if not os.path.isfile(fpath):\r
722                     continue\r
723                 perspective = None\r
724                 with open(fpath, 'rU') as f:\r
725                     perspective = f.readline()\r
726                 if perspective:\r
727                     self._perspectives[name] = perspective\r
728 \r
729         selected_perspective = self.gui.config['active perspective']\r
730         if not self._perspectives.has_key(selected_perspective):\r
731             self.gui.config['active perspective'] = 'Default'  # TODO: push to engine's Hooke\r
732 \r
733         self._restore_perspective(selected_perspective)\r
734         self._update_perspective_menu()\r
735 \r
736     def _update_perspective_menu(self):\r
737         self._c['menu bar']._c['perspective'].update(\r
738             sorted(self._perspectives.keys()),\r
739             self.gui.config['active perspective'])\r
740 \r
741     def _save_perspective(self, perspective, perspective_dir, name,\r
742                           extension=None):\r
743         path = os.path.join(perspective_dir, name)\r
744         if extension != None:\r
745             path += extension\r
746         if not os.path.isdir(perspective_dir):\r
747             os.makedirs(perspective_dir)\r
748         with open(path, 'w') as f:\r
749             f.write(perspective)\r
750         self._perspectives[name] = perspective\r
751         self._restore_perspective(name)\r
752         self._update_perspective_menu()\r
753 \r
754     def _delete_perspectives(self, perspective_dir, names,\r
755                              extension=None):\r
756         print 'pop', names\r
757         for name in names:\r
758             path = os.path.join(perspective_dir, name)\r
759             if extension != None:\r
760                 path += extension\r
761             os.remove(path)\r
762             del(self._perspectives[name])\r
763         self._update_perspective_menu()\r
764         if self.gui.config['active perspective'] in names:\r
765             self._restore_perspective('Default')\r
766         # TODO: does this bug still apply?\r
767         # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258\r
768         #   http://trac.wxwidgets.org/ticket/3258 \r
769         # ) that makes the radio item indicator in the menu disappear.\r
770         # The code should be fine once this issue is fixed.\r
771 \r
772     def _restore_perspective(self, name):\r
773         if name != self.gui.config['active perspective']:\r
774             print 'restoring perspective:', name\r
775             self.gui.config['active perspective'] = name  # TODO: push to engine's Hooke\r
776             self._c['manager'].LoadPerspective(self._perspectives[name])\r
777             self._c['manager'].Update()\r
778             for pane in self._c['manager'].GetAllPanes():\r
779                 if pane.name in self._c['menu bar']._c['view']._c.keys():\r
780                     pane.Check(pane.window.IsShown())\r
781 \r
782     def _on_save_perspective(self, *args):\r
783         perspective = self._c['manager'].SavePerspective()\r
784         name = self.gui.config['active perspective']\r
785         if name == 'Default':\r
786             name = 'New perspective'\r
787         name = select_save_file(\r
788             directory=self.gui.config['perspective path'],\r
789             name=name,\r
790             extension=self.gui.config['perspective extension'],\r
791             parent=self,\r
792             message='Enter a name for the new perspective:',\r
793             caption='Save perspective')\r
794         if name == None:\r
795             return\r
796         self._save_perspective(\r
797             perspective, self.gui.config['perspective path'], name=name,\r
798             extension=self.gui.config['perspective extension'])\r
799 \r
800     def _on_delete_perspective(self, *args, **kwargs):\r
801         options = sorted([p for p in self._perspectives.keys()\r
802                           if p != 'Default'])\r
803         dialog = SelectionDialog(\r
804             options=options,\r
805             message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",\r
806             button_id=wx.ID_DELETE,\r
807             selection_style='multiple',\r
808             parent=self,\r
809             title='Delete perspective(s)',\r
810             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)\r
811         dialog.CenterOnScreen()\r
812         dialog.ShowModal()\r
813         names = [options[i] for i in dialog.selected]\r
814         dialog.Destroy()\r
815         self._delete_perspectives(\r
816             self.gui.config['perspective path'], names=names,\r
817             extension=self.gui.config['perspective extension'])\r
818 \r
819     def _on_select_perspective(self, _class, method, name):\r
820         self._restore_perspective(name)\r
821 \r
822 \r
823 \r
824 class HookeApp (wx.App):\r
825     """A :class:`wx.App` wrapper around :class:`HookeFrame`.\r
826 \r
827     Tosses up a splash screen and then loads :class:`HookeFrame` in\r
828     its own window.\r
829     """\r
830     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):\r
831         self.gui = gui\r
832         self.commands = commands\r
833         self.inqueue = inqueue\r
834         self.outqueue = outqueue\r
835         super(HookeApp, self).__init__(*args, **kwargs)\r
836 \r
837     def OnInit(self):\r
838         self.SetAppName('Hooke')\r
839         self.SetVendorName('')\r
840         self._setup_splash_screen()\r
841 \r
842         height = int(self.gui.config['main height']) # HACK: config should convert\r
843         width = int(self.gui.config['main width'])\r
844         top = int(self.gui.config['main top'])\r
845         left = int(self.gui.config['main left'])\r
846 \r
847         # Sometimes, the ini file gets confused and sets 'left' and\r
848         # 'top' to large negative numbers.  Here we catch and fix\r
849         # this.  Keep small negative numbers, the user might want\r
850         # those.\r
851         if left < -width:\r
852             left = 0\r
853         if top < -height:\r
854             top = 0\r
855 \r
856         self._c = {\r
857             'frame': HookeFrame(\r
858                 self.gui, self.commands, self.inqueue, self.outqueue,\r
859                 parent=None, title='Hooke',\r
860                 pos=(left, top), size=(width, height),\r
861                 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),\r
862             }\r
863         self._c['frame'].Show(True)\r
864         self.SetTopWindow(self._c['frame'])\r
865         return True\r
866 \r
867     def _setup_splash_screen(self):\r
868         if self.gui.config['show splash screen'] == 'True': # HACK: config should decode\r
869             print 'splash', self.gui.config['show splash screen']\r
870             path = self.gui.config['splash screen image']\r
871             if os.path.isfile(path):\r
872                 duration = int(self.gui.config['splash screen duration'])  # HACK: config should decode types\r
873                 wx.SplashScreen(\r
874                     bitmap=wx.Image(path).ConvertToBitmap(),\r
875                     splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,\r
876                     milliseconds=duration,\r
877                     parent=None)\r
878                 wx.Yield()\r
879                 # For some reason splashDuration and sleep do not\r
880                 # correspond to each other at least not on Windows.\r
881                 # Maybe it's because duration is in milliseconds and\r
882                 # sleep in seconds.  Thus we need to increase the\r
883                 # sleep time a bit. A factor of 1.2 seems to work.\r
884                 sleepFactor = 1.2\r
885                 time.sleep(sleepFactor * duration / 1000)\r
886 \r
887 \r
888 class GUI (UserInterface):\r
889     """wxWindows graphical user interface.\r
890     """\r
891     def __init__(self):\r
892         super(GUI, self).__init__(name='gui')\r
893 \r
894     def default_settings(self):\r
895         """Return a list of :class:`hooke.config.Setting`\s for any\r
896         configurable UI settings.\r
897 \r
898         The suggested section setting is::\r
899 \r
900             Setting(section=self.setting_section, help=self.__doc__)\r
901         """\r
902         return [\r
903             Setting(section=self.setting_section, help=self.__doc__),\r
904             Setting(section=self.setting_section, option='icon image',\r
905                     value=os.path.join('doc', 'img', 'microscope.ico'),\r
906                     help='Path to the hooke icon image.'),\r
907             Setting(section=self.setting_section, option='show splash screen',\r
908                     value=True,\r
909                     help='Enable/disable the splash screen'),\r
910             Setting(section=self.setting_section, option='splash screen image',\r
911                     value=os.path.join('doc', 'img', 'hooke.jpg'),\r
912                     help='Path to the Hooke splash screen image.'),\r
913             Setting(section=self.setting_section, option='splash screen duration',\r
914                     value=1000,\r
915                     help='Duration of the splash screen in milliseconds.'),\r
916             Setting(section=self.setting_section, option='perspective path',\r
917                     value=os.path.join('resources', 'gui', 'perspective'),\r
918                     help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.\r
919             Setting(section=self.setting_section, option='perspective extension',\r
920                     value='.txt',\r
921                     help='Extension for perspective files.'),\r
922             Setting(section=self.setting_section, option='hide extensions',\r
923                     value=False,\r
924                     help='Hide file extensions when displaying names.'),\r
925             Setting(section=self.setting_section, option='folders-workdir',\r
926                     value='.',\r
927                     help='This should probably go...'),\r
928             Setting(section=self.setting_section, option='folders-filters',\r
929                     value='.',\r
930                     help='This should probably go...'),\r
931             Setting(section=self.setting_section, option='active perspective',\r
932                     value='Default',\r
933                     help='Name of active perspective file (or "Default").'),\r
934             Setting(section=self.setting_section, option='folders-filter-index',\r
935                     value='0',\r
936                     help='This should probably go...'),\r
937             Setting(section=self.setting_section, option='main height',\r
938                     value=500,\r
939                     help='Height of main window in pixels.'),\r
940             Setting(section=self.setting_section, option='main width',\r
941                     value=500,\r
942                     help='Width of main window in pixels.'),\r
943             Setting(section=self.setting_section, option='main top',\r
944                     value=0,\r
945                     help='Pixels from screen top to top of main window.'),\r
946             Setting(section=self.setting_section, option='main left',\r
947                     value=0,\r
948                     help='Pixels from screen left to left of main window.'),            \r
949             Setting(section=self.setting_section, option='selected command',\r
950                     value='load playlist',\r
951                     help='Name of the initially selected command.'),\r
952             ]\r
953 \r
954     def _app(self, commands, ui_to_command_queue, command_to_ui_queue):\r
955         redirect = True\r
956         if __debug__:\r
957             redirect=False\r
958         app = HookeApp(gui=self,\r
959                        commands=commands,\r
960                        inqueue=ui_to_command_queue,\r
961                        outqueue=command_to_ui_queue,\r
962                        redirect=redirect)\r
963         return app\r
964 \r
965     def run(self, commands, ui_to_command_queue, command_to_ui_queue):\r
966         app = self._app(commands, ui_to_command_queue, command_to_ui_queue)\r
967         app.MainLoop()\r