Fill out prop_from_arguments so PropertyPanel displays command args.
[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 is included in wxPython >= 2.9.1, 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 .panel.propertyeditor2 import prop_from_argument, prop_from_setting\r
40 from . import prettyformat as prettyformat\r
41 from . import statusbar as statusbar\r
42 \r
43 \r
44 class HookeFrame (wx.Frame):\r
45     """The main Hooke-interface window.    \r
46     """\r
47     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):\r
48         super(HookeFrame, self).__init__(*args, **kwargs)\r
49         self.gui = gui\r
50         self.commands = commands\r
51         self.inqueue = inqueue\r
52         self.outqueue = outqueue\r
53         self._perspectives = {}  # {name: perspective_str}\r
54         self._c = {}\r
55 \r
56         self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))\r
57 \r
58         # setup frame manager\r
59         self._c['manager'] = aui.AuiManager()\r
60         self._c['manager'].SetManagedWindow(self)\r
61 \r
62         # set the gradient and drag styles\r
63         self._c['manager'].GetArtProvider().SetMetric(\r
64             aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)\r
65         self._c['manager'].SetFlags(\r
66             self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)\r
67 \r
68         # Min size for the frame itself isn't completely done.  See\r
69         # the end of FrameManager::Update() for the test code. For\r
70         # now, just hard code a frame minimum size.\r
71         self.SetMinSize(wx.Size(500, 500))\r
72 \r
73         self._setup_panels()\r
74         self._setup_toolbars()\r
75         self._c['manager'].Update()  # commit pending changes\r
76 \r
77         # Create the menubar after the panes so that the default\r
78         # perspective is created with all panes open\r
79         self._c['menu bar'] = menu.HookeMenuBar(\r
80             parent=self,\r
81             callbacks={\r
82                 'close': self._on_close,\r
83                 'about': self._on_about,\r
84                 'view_panel': self._on_panel_visibility,\r
85                 'save_perspective': self._on_save_perspective,\r
86                 'delete_perspective': self._on_delete_perspective,\r
87                 'select_perspective': self._on_select_perspective,\r
88                 })\r
89         self.SetMenuBar(self._c['menu bar'])\r
90 \r
91         self._c['status bar'] = statusbar.StatusBar(\r
92             parent=self,\r
93             style=wx.ST_SIZEGRIP)\r
94         self.SetStatusBar(self._c['status bar'])\r
95 \r
96         self._setup_perspectives()\r
97         self._bind_events()\r
98 \r
99         name = self.gui.config['active perspective']\r
100         return # TODO: cleanup\r
101         self.playlists = self._c['playlists'].Playlists\r
102         self._displayed_plot = None\r
103         #load default list, if possible\r
104         self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist'))\r
105 \r
106 \r
107     # GUI maintenance\r
108 \r
109     def _setup_panels(self):\r
110         client_size = self.GetClientSize()\r
111         for label,p,style in [\r
112 #            ('folders', wx.GenericDirCtrl(\r
113 #                    parent=self,\r
114 #                    dir=self.gui.config['folders-workdir'],\r
115 #                    size=(200, 250),\r
116 #                    style=wx.DIRCTRL_SHOW_FILTERS,\r
117 #                    filter=self.gui.config['folders-filters'],\r
118 #                    defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'),  #HACK: config should convert\r
119 #            ('playlists', panel.PANELS['playlist'](\r
120 #                    callbacks={},\r
121 #                    config=self.gui.config,\r
122 #                    parent=self,\r
123 #                    style=wx.WANTS_CHARS|wx.NO_BORDER,\r
124 #                    # WANTS_CHARS so the panel doesn't eat the Return key.\r
125 #                    size=(160, 200)), 'left'),\r
126 #            ('note', panel.note.Note(\r
127 #                    parent=self\r
128 #                    style=wx.WANTS_CHARS|wx.NO_BORDER,\r
129 #                    size=(160, 200)), 'left'),\r
130 #            ('notebook', Notebook(\r
131 #                    parent=self,\r
132 #                    pos=wx.Point(client_size.x, client_size.y),\r
133 #                    size=wx.Size(430, 200),\r
134 #                    style=aui.AUI_NB_DEFAULT_STYLE\r
135 #                    | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),\r
136             ('commands', panel.PANELS['commands'](\r
137                     commands=self.commands,\r
138                     selected=self.gui.config['selected command'],\r
139                     callbacks={\r
140                         'execute': self.execute_command,\r
141                         'select_plugin': self.select_plugin,\r
142                         'select_command': self.select_command,\r
143 #                        'selection_changed': self.panelProperties.select(self, method, command),  #SelectedTreeItem = selected_item,\r
144                         },\r
145                     parent=self,\r
146                     style=wx.WANTS_CHARS|wx.NO_BORDER,\r
147                     # WANTS_CHARS so the panel doesn't eat the Return key.\r
148 #                    size=(160, 200)\r
149                     ), 'center'),\r
150             ('property', panel.PANELS['propertyeditor2'](\r
151                     callbacks={},\r
152                     parent=self,\r
153                     style=wx.WANTS_CHARS,\r
154                     # WANTS_CHARS so the panel doesn't eat the Return key.\r
155                     ), 'center'),\r
156 #            ('assistant', wx.TextCtrl(\r
157 #                    parent=self,\r
158 #                    pos=wx.Point(0, 0),\r
159 #                    size=wx.Size(150, 90),\r
160 #                    style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),\r
161             ('output', panel.PANELS['output'](\r
162                     parent=self,\r
163                     pos=wx.Point(0, 0),\r
164                     size=wx.Size(150, 90),\r
165                     style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),\r
166              'bottom'),\r
167 #            ('results', panel.results.Results(self), 'bottom'),\r
168             ]:\r
169             self._add_panel(label, p, style)\r
170         #self._c['assistant'].SetEditable(False)\r
171 \r
172     def _add_panel(self, label, panel, style):\r
173         self._c[label] = panel\r
174         cap_label = label.capitalize()\r
175         info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)\r
176         info.PaneBorder(False).CloseButton(True).MaximizeButton(False)\r
177         if style == 'top':\r
178             info.Top()\r
179         elif style == 'center':\r
180             info.CenterPane()\r
181         elif style == 'left':\r
182             info.Left()\r
183         elif style == 'right':\r
184             info.Right()\r
185         else:\r
186             assert style == 'bottom', style\r
187             info.Bottom()\r
188         self._c['manager'].AddPane(panel, info)\r
189 \r
190     def _setup_toolbars(self):\r
191         self._c['navigation bar'] = navbar.NavBar(\r
192             callbacks={\r
193                 'next': self._next_curve,\r
194                 'previous': self._previous_curve,\r
195                 },\r
196             parent=self,\r
197             style=wx.TB_FLAT | wx.TB_NODIVIDER)\r
198         self._c['manager'].AddPane(\r
199             self._c['navigation bar'],\r
200             aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'\r
201                 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False\r
202                 ).RightDockable(False))\r
203 \r
204     def _bind_events(self):\r
205         # TODO: figure out if we can use the eventManager for menu\r
206         # ranges and events of 'self' without raising an assertion\r
207         # fail error.\r
208         self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)\r
209         self.Bind(wx.EVT_SIZE, self._on_size)\r
210         self.Bind(wx.EVT_CLOSE, self._on_close)\r
211         self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)\r
212         self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)\r
213 \r
214         return # TODO: cleanup\r
215         for value in self._c['menu bar']._c['view']._c.values():\r
216             self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)\r
217 \r
218         self.Bind(wx.EVT_MENU, self._on_save_perspective,\r
219                   self._c['menu bar']._c['perspective']._c['save'])\r
220         self.Bind(wx.EVT_MENU, self._on_delete_perspective,\r
221                   self._c['menu bar']._c['perspective']._c['delete'])\r
222 \r
223         treeCtrl = self._c['folders'].GetTreeCtrl()\r
224         treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)\r
225         \r
226         # TODO: playlist callbacks\r
227         return # TODO: cleanup\r
228         evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)\r
229         #property editor\r
230         self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)\r
231         #results panel\r
232         self.panelResults.results_list.OnCheckItem = self.OnResultsCheck\r
233 \r
234     def _on_about(self, *args):\r
235         dialog = wx.MessageDialog(\r
236             parent=self,\r
237             message=self.gui._splash_text(),\r
238             caption='About Hooke',\r
239             style=wx.OK|wx.ICON_INFORMATION)\r
240         dialog.ShowModal()\r
241         dialog.Destroy()\r
242 \r
243     def _on_close(self, *args):\r
244         # apply changes\r
245         self.gui.config['main height'] = str(self.GetSize().GetHeight())\r
246         self.gui.config['main left'] = str(self.GetPosition()[0])\r
247         self.gui.config['main top'] = str(self.GetPosition()[1])\r
248         self.gui.config['main width'] = str(self.GetSize().GetWidth())\r
249         # push changes back to Hooke.config?\r
250         self._c['manager'].UnInit()\r
251         del self._c['manager']\r
252         self.Destroy()\r
253 \r
254 \r
255 \r
256     # Command handling\r
257 \r
258     def _command_by_name(self, name):\r
259         cs = [c for c in self.commands if c.name == name]\r
260         if len(cs) == 0:\r
261             raise KeyError(name)\r
262         elif len(cs) > 1:\r
263             raise Exception('Multiple commands named "%s"' % name)\r
264         return cs[0]\r
265 \r
266     def execute_command(self, _class=None, method=None,\r
267                         command=None, args=None):\r
268         self.inqueue.put(CommandMessage(command, args))\r
269         results = []\r
270         while True:\r
271             msg = self.outqueue.get()\r
272             results.append(msg)\r
273             if isinstance(msg, Exit):\r
274                 self._on_close()\r
275                 break\r
276             elif isinstance(msg, CommandExit):\r
277                 # TODO: display command complete\r
278                 break\r
279             elif isinstance(msg, ReloadUserInterfaceConfig):\r
280                 self.gui.reload_config(msg.config)\r
281                 continue\r
282             elif isinstance(msg, Request):\r
283                 h = handler.HANDLERS[msg.type]\r
284                 h.run(self, msg)  # TODO: pause for response?\r
285                 continue\r
286         pp = getattr(\r
287             self, '_postprocess_%s' % command.name.replace(' ', '_'),\r
288             self._postprocess_text)\r
289         pp(command=command, results=results)\r
290         return results\r
291 \r
292     def _handle_request(self, msg):\r
293         """Repeatedly try to get a response to `msg`.\r
294         """\r
295         if prompt == None:\r
296             raise NotImplementedError('_%s_request_prompt' % msg.type)\r
297         prompt_string = prompt(msg)\r
298         parser = getattr(self, '_%s_request_parser' % msg.type, None)\r
299         if parser == None:\r
300             raise NotImplementedError('_%s_request_parser' % msg.type)\r
301         error = None\r
302         while True:\r
303             if error != None:\r
304                 self.cmd.stdout.write(''.join([\r
305                         error.__class__.__name__, ': ', str(error), '\n']))\r
306             self.cmd.stdout.write(prompt_string)\r
307             value = parser(msg, self.cmd.stdin.readline())\r
308             try:\r
309                 response = msg.response(value)\r
310                 break\r
311             except ValueError, error:\r
312                 continue\r
313         self.inqueue.put(response)\r
314 \r
315 \r
316 \r
317     # Command-specific postprocessing\r
318 \r
319     def _postprocess_text(self, command, results):\r
320         """Print the string representation of the results to the Results window.\r
321 \r
322         This is similar to :class:`~hooke.ui.commandline.DoCommand`'s\r
323         approach, except that :class:`~hooke.ui.commandline.DoCommand`\r
324         doesn't print some internally handled messages\r
325         (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).\r
326         """\r
327         for result in results:\r
328             if isinstance(result, CommandExit):\r
329                 self._c['output'].write(result.__class__.__name__+'\n')\r
330             self._c['output'].write(str(result).rstrip()+'\n')\r
331 \r
332     def _postprocess_get_curve(self, command, results):\r
333         """Update `self` to show the curve.\r
334         """\r
335         if not isinstance(results[-1], Success):\r
336             return  # error executing 'get curve'\r
337         assert len(results) == 2, results\r
338         curve = results[0]\r
339         print curve\r
340 \r
341         selected_item = self._c['playlists']._c['tree'].GetSelection()\r
342         if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
343             #GetFirstChild returns a tuple\r
344             #we only need the first element\r
345             next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]\r
346         else:\r
347             next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)\r
348             if not next_item.IsOk():\r
349                 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)\r
350                 #GetFirstChild returns a tuple\r
351                 #we only need the first element\r
352                 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]\r
353         self._c['playlists']._c['tree'].SelectItem(next_item, True)\r
354         if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
355             playlist = self.GetActivePlaylist()\r
356             if playlist.count > 1:\r
357                 playlist.next()\r
358                 self._c['status bar'].set_playlist(playlist)\r
359                 self.UpdateNote()\r
360                 self.UpdatePlot()\r
361 \r
362 \r
363 \r
364     # TODO: cruft\r
365 \r
366     def _GetActiveFileIndex(self):\r
367         lib.playlist.Playlist = self.GetActivePlaylist()\r
368         #get the selected item from the tree\r
369         selected_item = self._c['playlists']._c['tree'].GetSelection()\r
370         #test if a playlist or a curve was double-clicked\r
371         if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):\r
372             return -1\r
373         else:\r
374             count = 0\r
375             selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
376             while selected_item.IsOk():\r
377                 count += 1\r
378                 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)\r
379             return count\r
380 \r
381     def _GetPlaylistTab(self, name):\r
382         for index, page in enumerate(self._c['notebook']._tabs._pages):\r
383             if page.caption == name:\r
384                 return index\r
385         return -1\r
386 \r
387     def select_plugin(self, _class=None, method=None, plugin=None):\r
388         pass\r
389 \r
390     def AddPlaylistFromFiles(self, files=[], name='Untitled'):\r
391         if files:\r
392             playlist = lib.playlist.Playlist(self, self.drivers)\r
393             for item in files:\r
394                 playlist.add_curve(item)\r
395         if playlist.count > 0:\r
396             playlist.name = self._GetUniquePlaylistName(name)\r
397             playlist.reset()\r
398             self.AddTayliss(playlist)\r
399 \r
400     def AppliesPlotmanipulator(self, name):\r
401         '''\r
402         Returns True if the plotmanipulator 'name' is applied, False otherwise\r
403         name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')\r
404         '''\r
405         return self.GetBoolFromConfig('core', 'plotmanipulators', name)\r
406 \r
407     def ApplyPlotmanipulators(self, plot, plot_file):\r
408         '''\r
409         Apply all active plotmanipulators.\r
410         '''\r
411         if plot is not None and plot_file is not None:\r
412             manipulated_plot = copy.deepcopy(plot)\r
413             for plotmanipulator in self.plotmanipulators:\r
414                 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):\r
415                     manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)\r
416             return manipulated_plot\r
417 \r
418     def GetActiveFigure(self):\r
419         playlist_name = self.GetActivePlaylistName()\r
420         figure = self.playlists[playlist_name].figure\r
421         if figure is not None:\r
422             return figure\r
423         return None\r
424 \r
425     def GetActiveFile(self):\r
426         playlist = self.GetActivePlaylist()\r
427         if playlist is not None:\r
428             return playlist.get_active_file()\r
429         return None\r
430 \r
431     def GetActivePlot(self):\r
432         playlist = self.GetActivePlaylist()\r
433         if playlist is not None:\r
434             return playlist.get_active_file().plot\r
435         return None\r
436 \r
437     def GetDisplayedPlot(self):\r
438         plot = copy.deepcopy(self.displayed_plot)\r
439         #plot.curves = []\r
440         #plot.curves = copy.deepcopy(plot.curves)\r
441         return plot\r
442 \r
443     def GetDisplayedPlotCorrected(self):\r
444         plot = copy.deepcopy(self.displayed_plot)\r
445         plot.curves = []\r
446         plot.curves = copy.deepcopy(plot.corrected_curves)\r
447         return plot\r
448 \r
449     def GetDisplayedPlotRaw(self):\r
450         plot = copy.deepcopy(self.displayed_plot)\r
451         plot.curves = []\r
452         plot.curves = copy.deepcopy(plot.raw_curves)\r
453         return plot\r
454 \r
455     def GetDockArt(self):\r
456         return self._c['manager'].GetArtProvider()\r
457 \r
458     def GetPlotmanipulator(self, name):\r
459         '''\r
460         Returns a plot manipulator function from its name\r
461         '''\r
462         for plotmanipulator in self.plotmanipulators:\r
463             if plotmanipulator.name == name:\r
464                 return plotmanipulator\r
465         return None\r
466 \r
467     def HasPlotmanipulator(self, name):\r
468         '''\r
469         returns True if the plotmanipulator 'name' is loaded, False otherwise\r
470         '''\r
471         for plotmanipulator in self.plotmanipulators:\r
472             if plotmanipulator.command == name:\r
473                 return True\r
474         return False\r
475 \r
476 \r
477     def _on_dir_ctrl_left_double_click(self, event):\r
478         file_path = self.panelFolders.GetPath()\r
479         if os.path.isfile(file_path):\r
480             if file_path.endswith('.hkp'):\r
481                 self.do_loadlist(file_path)\r
482         event.Skip()\r
483 \r
484     def _on_erase_background(self, event):\r
485         event.Skip()\r
486 \r
487     def _on_notebook_page_close(self, event):\r
488         ctrl = event.GetEventObject()\r
489         playlist_name = ctrl.GetPageText(ctrl._curpage)\r
490         self.DeleteFromPlaylists(playlist_name)\r
491 \r
492     def OnPaneClose(self, event):\r
493         event.Skip()\r
494 \r
495     def OnPropGridChanged (self, event):\r
496         prop = event.GetProperty()\r
497         if prop:\r
498             item_section = self.panelProperties.SelectedTreeItem\r
499             item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)\r
500             plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)\r
501             config = self.gui.config[plugin]\r
502             property_section = self._c['commands']._c['tree'].GetItemText(item_section)\r
503             property_key = prop.GetName()\r
504             property_value = prop.GetDisplayedString()\r
505 \r
506             config[property_section][property_key]['value'] = property_value\r
507 \r
508     def OnResultsCheck(self, index, flag):\r
509         results = self.GetActivePlot().results\r
510         if results.has_key(self.results_str):\r
511             results[self.results_str].results[index].visible = flag\r
512             results[self.results_str].update()\r
513             self.UpdatePlot()\r
514 \r
515 \r
516     def _on_size(self, event):\r
517         event.Skip()\r
518 \r
519     def OnUpdateNote(self, event):\r
520         '''\r
521         Saves the note to the active file.\r
522         '''\r
523         active_file = self.GetActiveFile()\r
524         active_file.note = self.panelNote.Editor.GetValue()\r
525 \r
526     def UpdateNote(self):\r
527         #update the note for the active file\r
528         active_file = self.GetActiveFile()\r
529         if active_file is not None:\r
530             self.panelNote.Editor.SetValue(active_file.note)\r
531 \r
532     def UpdatePlaylistsTreeSelection(self):\r
533         playlist = self.GetActivePlaylist()\r
534         if playlist is not None:\r
535             if playlist.index >= 0:\r
536                 self._c['status bar'].set_playlist(playlist)\r
537                 self.UpdateNote()\r
538                 self.UpdatePlot()\r
539 \r
540     def UpdatePlot(self, plot=None):\r
541 \r
542         def add_to_plot(curve, set_scale=True):\r
543             if curve.visible and curve.x and curve.y:\r
544                 #get the index of the subplot to use as destination\r
545                 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1\r
546                 #set all parameters for the plot\r
547                 axes_list[destination].set_title(curve.title)\r
548                 if set_scale:\r
549                     axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)\r
550                     axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)\r
551                     #set the formatting details for the scale\r
552                     formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)\r
553                     formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)\r
554                     axes_list[destination].xaxis.set_major_formatter(formatter_x)\r
555                     axes_list[destination].yaxis.set_major_formatter(formatter_y)\r
556                 if curve.style == 'plot':\r
557                     axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)\r
558                 if curve.style == 'scatter':\r
559                     axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)\r
560                 #add the legend if necessary\r
561                 if curve.legend:\r
562                     axes_list[destination].legend()\r
563 \r
564         if plot is None:\r
565             active_file = self.GetActiveFile()\r
566             if not active_file.driver:\r
567                 #the first time we identify a file, the following need to be set\r
568                 active_file.identify(self.drivers)\r
569                 for curve in active_file.plot.curves:\r
570                     curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')\r
571                     curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')\r
572                     curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')\r
573                     curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')\r
574                     curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')\r
575             if active_file.driver is None:\r
576                 self.AppendToOutput('Invalid file: ' + active_file.filename)\r
577                 return\r
578             self.displayed_plot = copy.deepcopy(active_file.plot)\r
579             #add raw curves to plot\r
580             self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)\r
581             #apply all active plotmanipulators\r
582             self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)\r
583             #add corrected curves to plot\r
584             self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)\r
585         else:\r
586             active_file = None\r
587             self.displayed_plot = copy.deepcopy(plot)\r
588 \r
589         figure = self.GetActiveFigure()\r
590         figure.clear()\r
591 \r
592         #use '0' instead of e.g. '0.00' for scales\r
593         use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')\r
594         #optionally remove the extension from the title of the plot\r
595         hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')\r
596         if hide_curve_extension:\r
597             title = lh.remove_extension(self.displayed_plot.title)\r
598         else:\r
599             title = self.displayed_plot.title\r
600         figure.suptitle(title, fontsize=14)\r
601         #create the list of all axes necessary (rows and columns)\r
602         axes_list =[]\r
603         number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])\r
604         number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])\r
605         for index in range(number_of_rows * number_of_columns):\r
606             axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))\r
607 \r
608         #add all curves to the corresponding plots\r
609         for curve in self.displayed_plot.curves:\r
610             add_to_plot(curve)\r
611 \r
612         #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'\r
613         figure.subplots_adjust(hspace=0.3)\r
614 \r
615         #display results\r
616         self.panelResults.ClearResults()\r
617         if self.displayed_plot.results.has_key(self.results_str):\r
618             for curve in self.displayed_plot.results[self.results_str].results:\r
619                 add_to_plot(curve, set_scale=False)\r
620             self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])\r
621         else:\r
622             self.panelResults.ClearResults()\r
623         #refresh the plot\r
624         figure.canvas.draw()\r
625 \r
626     def _on_curve_select(self, playlist, curve):\r
627         #create the plot tab and add playlist to the dictionary\r
628         plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))\r
629         notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)\r
630         #tab_index = self._c['notebook'].GetSelection()\r
631         playlist.figure = plotPanel.get_figure()\r
632         self.playlists[playlist.name] = playlist\r
633         #self.playlists[playlist.name] = [playlist, figure]\r
634         self._c['status bar'].set_playlist(playlist)\r
635         self.UpdateNote()\r
636         self.UpdatePlot()\r
637 \r
638 \r
639     def _on_playlist_left_doubleclick(self):\r
640         index = self._c['notebook'].GetSelection()\r
641         current_playlist = self._c['notebook'].GetPageText(index)\r
642         if current_playlist != playlist_name:\r
643             index = self._GetPlaylistTab(playlist_name)\r
644             self._c['notebook'].SetSelection(index)\r
645         self._c['status bar'].set_playlist(playlist)\r
646         self.UpdateNote()\r
647         self.UpdatePlot()\r
648 \r
649     def _on_playlist_delete(self, playlist):\r
650         notebook = self.Parent.plotNotebook\r
651         index = self.Parent._GetPlaylistTab(playlist.name)\r
652         notebook.SetSelection(index)\r
653         notebook.DeletePage(notebook.GetSelection())\r
654         self.Parent.DeleteFromPlaylists(playlist_name)\r
655 \r
656 \r
657 \r
658     # Command panel interface\r
659 \r
660     def select_command(self, _class, method, command):\r
661         #self.select_plugin(plugin=command.plugin)\r
662         if 'assistant' in self._c:\r
663             self._c['assitant'].ChangeValue(command.help)\r
664         self._c['property'].clear()\r
665         for argument in command.arguments:\r
666             if argument.name == 'help':\r
667                 continue\r
668             self._c['property'].append_property(prop_from_argument(\r
669                     argument, curves=[], playlists=[]))  # TODO: lookup playlists/curves\r
670         self.gui.config['selected command'] = command  # TODO: push to engine\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