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