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