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