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