Fixed curve/playlist choice argument handling in panel.propertyeditor2
[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=None):\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=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=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=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 \r
700             results = self.execute_command(\r
701                 command=self._command_by_name('playlists'))\r
702             if not isinstance(results[-1], Success):\r
703                 self._postprocess_text(command, results=results)\r
704                 playlists = []\r
705             else:\r
706                 playlists = results[0]\r
707 \r
708             results = self.execute_command(\r
709                 command=self._command_by_name('playlist curves'))\r
710             if not isinstance(results[-1], Success):\r
711                 self._postprocess_text(command, results=results)\r
712                 curves = []\r
713             else:\r
714                 curves = results[0]\r
715 \r
716             p = prop_from_argument(\r
717                 argument, curves=curves, playlists=playlists)\r
718             if p == None:\r
719                 continue  # property intentionally not handled (yet)\r
720             self._c['property'].append_property(p)\r
721 \r
722         self.gui.config['selected command'] = command  # TODO: push to engine\r
723 \r
724 \r
725 \r
726     # Playlist panel interface\r
727 \r
728     def _on_user_delete_playlist(self, _class, method, playlist):\r
729         pass\r
730 \r
731     def _on_delete_playlist(self, _class, method, playlist):\r
732         if hasattr(playlist, 'path') and playlist.path != None:\r
733             os.remove(playlist.path)\r
734 \r
735     def _on_user_delete_curve(self, _class, method, playlist, curve):\r
736         pass\r
737 \r
738     def _on_delete_curve(self, _class, method, playlist, curve):\r
739         os.remove(curve.path)\r
740 \r
741     def _on_set_selected_playlist(self, _class, method, playlist):\r
742         """TODO: playlists plugin with `jump to playlist`.\r
743         """\r
744         pass\r
745 \r
746     def _on_set_selected_curve(self, _class, method, playlist, curve):\r
747         """Call the `jump to curve` command.\r
748 \r
749         TODO: playlists plugin.\r
750         """\r
751         # TODO: jump to playlist, get playlist\r
752         index = playlist.index(curve)\r
753         results = self.execute_command(\r
754             command=self._command_by_name('jump to curve'),\r
755             args={'index':index})\r
756         if not isinstance(results[-1], Success):\r
757             return\r
758         #results = self.execute_command(\r
759         #    command=self._command_by_name('get playlist'))\r
760         #if not isinstance(results[-1], Success):\r
761         #    return\r
762         self.execute_command(\r
763             command=self._command_by_name('get curve'))\r
764 \r
765 \r
766 \r
767     # Navbar interface\r
768 \r
769     def _next_curve(self, *args):\r
770         """Call the `next curve` command.\r
771         """\r
772         results = self.execute_command(\r
773             command=self._command_by_name('next curve'))\r
774         if isinstance(results[-1], Success):\r
775             self.execute_command(\r
776                 command=self._command_by_name('get curve'))\r
777 \r
778     def _previous_curve(self, *args):\r
779         """Call the `previous curve` command.\r
780         """\r
781         results = self.execute_command(\r
782             command=self._command_by_name('previous curve'))\r
783         if isinstance(results[-1], Success):\r
784             self.execute_command(\r
785                 command=self._command_by_name('get curve'))\r
786 \r
787 \r
788 \r
789     # Panel display handling\r
790 \r
791     def _on_panel_visibility(self, _class, method, panel_name, visible):\r
792         pane = self._c['manager'].GetPane(panel_name)\r
793         print visible\r
794         pane.Show(visible)\r
795         #if we don't do the following, the Folders pane does not resize properly on hide/show\r
796         if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():\r
797             #folders_size = pane.GetSize()\r
798             self.panelFolders.Fit()\r
799         self._c['manager'].Update()\r
800 \r
801     def _setup_perspectives(self):\r
802         """Add perspectives to menubar and _perspectives.\r
803         """\r
804         self._perspectives = {\r
805             'Default': self._c['manager'].SavePerspective(),\r
806             }\r
807         path = self.gui.config['perspective path']\r
808         if os.path.isdir(path):\r
809             files = sorted(os.listdir(path))\r
810             for fname in files:\r
811                 name, extension = os.path.splitext(fname)\r
812                 if extension != self.gui.config['perspective extension']:\r
813                     continue\r
814                 fpath = os.path.join(path, fname)\r
815                 if not os.path.isfile(fpath):\r
816                     continue\r
817                 perspective = None\r
818                 with open(fpath, 'rU') as f:\r
819                     perspective = f.readline()\r
820                 if perspective:\r
821                     self._perspectives[name] = perspective\r
822 \r
823         selected_perspective = self.gui.config['active perspective']\r
824         if not self._perspectives.has_key(selected_perspective):\r
825             self.gui.config['active perspective'] = 'Default'  # TODO: push to engine's Hooke\r
826 \r
827         self._restore_perspective(selected_perspective)\r
828         self._update_perspective_menu()\r
829 \r
830     def _update_perspective_menu(self):\r
831         self._c['menu bar']._c['perspective'].update(\r
832             sorted(self._perspectives.keys()),\r
833             self.gui.config['active perspective'])\r
834 \r
835     def _save_perspective(self, perspective, perspective_dir, name,\r
836                           extension=None):\r
837         path = os.path.join(perspective_dir, name)\r
838         if extension != None:\r
839             path += extension\r
840         if not os.path.isdir(perspective_dir):\r
841             os.makedirs(perspective_dir)\r
842         with open(path, 'w') as f:\r
843             f.write(perspective)\r
844         self._perspectives[name] = perspective\r
845         self._restore_perspective(name)\r
846         self._update_perspective_menu()\r
847 \r
848     def _delete_perspectives(self, perspective_dir, names,\r
849                              extension=None):\r
850         print 'pop', names\r
851         for name in names:\r
852             path = os.path.join(perspective_dir, name)\r
853             if extension != None:\r
854                 path += extension\r
855             os.remove(path)\r
856             del(self._perspectives[name])\r
857         self._update_perspective_menu()\r
858         if self.gui.config['active perspective'] in names:\r
859             self._restore_perspective('Default')\r
860         # TODO: does this bug still apply?\r
861         # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258\r
862         #   http://trac.wxwidgets.org/ticket/3258 \r
863         # ) that makes the radio item indicator in the menu disappear.\r
864         # The code should be fine once this issue is fixed.\r
865 \r
866     def _restore_perspective(self, name):\r
867         if name != self.gui.config['active perspective']:\r
868             print 'restoring perspective:', name\r
869             self.gui.config['active perspective'] = name  # TODO: push to engine's Hooke\r
870             self._c['manager'].LoadPerspective(self._perspectives[name])\r
871             self._c['manager'].Update()\r
872             for pane in self._c['manager'].GetAllPanes():\r
873                 if pane.name in self._c['menu bar']._c['view']._c.keys():\r
874                     pane.Check(pane.window.IsShown())\r
875 \r
876     def _on_save_perspective(self, *args):\r
877         perspective = self._c['manager'].SavePerspective()\r
878         name = self.gui.config['active perspective']\r
879         if name == 'Default':\r
880             name = 'New perspective'\r
881         name = select_save_file(\r
882             directory=self.gui.config['perspective path'],\r
883             name=name,\r
884             extension=self.gui.config['perspective extension'],\r
885             parent=self,\r
886             message='Enter a name for the new perspective:',\r
887             caption='Save perspective')\r
888         if name == None:\r
889             return\r
890         self._save_perspective(\r
891             perspective, self.gui.config['perspective path'], name=name,\r
892             extension=self.gui.config['perspective extension'])\r
893 \r
894     def _on_delete_perspective(self, *args, **kwargs):\r
895         options = sorted([p for p in self._perspectives.keys()\r
896                           if p != 'Default'])\r
897         dialog = SelectionDialog(\r
898             options=options,\r
899             message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",\r
900             button_id=wx.ID_DELETE,\r
901             selection_style='multiple',\r
902             parent=self,\r
903             title='Delete perspective(s)',\r
904             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)\r
905         dialog.CenterOnScreen()\r
906         dialog.ShowModal()\r
907         names = [options[i] for i in dialog.selected]\r
908         dialog.Destroy()\r
909         self._delete_perspectives(\r
910             self.gui.config['perspective path'], names=names,\r
911             extension=self.gui.config['perspective extension'])\r
912 \r
913     def _on_select_perspective(self, _class, method, name):\r
914         self._restore_perspective(name)\r
915 \r
916 \r
917 \r
918 class HookeApp (wx.App):\r
919     """A :class:`wx.App` wrapper around :class:`HookeFrame`.\r
920 \r
921     Tosses up a splash screen and then loads :class:`HookeFrame` in\r
922     its own window.\r
923     """\r
924     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):\r
925         self.gui = gui\r
926         self.commands = commands\r
927         self.inqueue = inqueue\r
928         self.outqueue = outqueue\r
929         super(HookeApp, self).__init__(*args, **kwargs)\r
930 \r
931     def OnInit(self):\r
932         self.SetAppName('Hooke')\r
933         self.SetVendorName('')\r
934         self._setup_splash_screen()\r
935 \r
936         height = int(self.gui.config['main height']) # HACK: config should convert\r
937         width = int(self.gui.config['main width'])\r
938         top = int(self.gui.config['main top'])\r
939         left = int(self.gui.config['main left'])\r
940 \r
941         # Sometimes, the ini file gets confused and sets 'left' and\r
942         # 'top' to large negative numbers.  Here we catch and fix\r
943         # this.  Keep small negative numbers, the user might want\r
944         # those.\r
945         if left < -width:\r
946             left = 0\r
947         if top < -height:\r
948             top = 0\r
949 \r
950         self._c = {\r
951             'frame': HookeFrame(\r
952                 self.gui, self.commands, self.inqueue, self.outqueue,\r
953                 parent=None, title='Hooke',\r
954                 pos=(left, top), size=(width, height),\r
955                 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),\r
956             }\r
957         self._c['frame'].Show(True)\r
958         self.SetTopWindow(self._c['frame'])\r
959         return True\r
960 \r
961     def _setup_splash_screen(self):\r
962         if self.gui.config['show splash screen'] == 'True': # HACK: config should decode\r
963             print 'splash', self.gui.config['show splash screen']\r
964             path = self.gui.config['splash screen image']\r
965             if os.path.isfile(path):\r
966                 duration = int(self.gui.config['splash screen duration'])  # HACK: config should decode types\r
967                 wx.SplashScreen(\r
968                     bitmap=wx.Image(path).ConvertToBitmap(),\r
969                     splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,\r
970                     milliseconds=duration,\r
971                     parent=None)\r
972                 wx.Yield()\r
973                 # For some reason splashDuration and sleep do not\r
974                 # correspond to each other at least not on Windows.\r
975                 # Maybe it's because duration is in milliseconds and\r
976                 # sleep in seconds.  Thus we need to increase the\r
977                 # sleep time a bit. A factor of 1.2 seems to work.\r
978                 sleepFactor = 1.2\r
979                 time.sleep(sleepFactor * duration / 1000)\r
980 \r
981 \r
982 class GUI (UserInterface):\r
983     """wxWindows graphical user interface.\r
984     """\r
985     def __init__(self):\r
986         super(GUI, self).__init__(name='gui')\r
987 \r
988     def default_settings(self):\r
989         """Return a list of :class:`hooke.config.Setting`\s for any\r
990         configurable UI settings.\r
991 \r
992         The suggested section setting is::\r
993 \r
994             Setting(section=self.setting_section, help=self.__doc__)\r
995         """\r
996         return [\r
997             Setting(section=self.setting_section, help=self.__doc__),\r
998             Setting(section=self.setting_section, option='icon image',\r
999                     value=os.path.join('doc', 'img', 'microscope.ico'),\r
1000                     help='Path to the hooke icon image.'),\r
1001             Setting(section=self.setting_section, option='show splash screen',\r
1002                     value=True,\r
1003                     help='Enable/disable the splash screen'),\r
1004             Setting(section=self.setting_section, option='splash screen image',\r
1005                     value=os.path.join('doc', 'img', 'hooke.jpg'),\r
1006                     help='Path to the Hooke splash screen image.'),\r
1007             Setting(section=self.setting_section, option='splash screen duration',\r
1008                     value=1000,\r
1009                     help='Duration of the splash screen in milliseconds.'),\r
1010             Setting(section=self.setting_section, option='perspective path',\r
1011                     value=os.path.join('resources', 'gui', 'perspective'),\r
1012                     help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.\r
1013             Setting(section=self.setting_section, option='perspective extension',\r
1014                     value='.txt',\r
1015                     help='Extension for perspective files.'),\r
1016             Setting(section=self.setting_section, option='hide extensions',\r
1017                     value=False,\r
1018                     help='Hide file extensions when displaying names.'),\r
1019             Setting(section=self.setting_section, option='folders-workdir',\r
1020                     value='.',\r
1021                     help='This should probably go...'),\r
1022             Setting(section=self.setting_section, option='folders-filters',\r
1023                     value='.',\r
1024                     help='This should probably go...'),\r
1025             Setting(section=self.setting_section, option='active perspective',\r
1026                     value='Default',\r
1027                     help='Name of active perspective file (or "Default").'),\r
1028             Setting(section=self.setting_section, option='folders-filter-index',\r
1029                     value='0',\r
1030                     help='This should probably go...'),\r
1031             Setting(section=self.setting_section, option='main height',\r
1032                     value=450,\r
1033                     help='Height of main window in pixels.'),\r
1034             Setting(section=self.setting_section, option='main width',\r
1035                     value=800,\r
1036                     help='Width of main window in pixels.'),\r
1037             Setting(section=self.setting_section, option='main top',\r
1038                     value=0,\r
1039                     help='Pixels from screen top to top of main window.'),\r
1040             Setting(section=self.setting_section, option='main left',\r
1041                     value=0,\r
1042                     help='Pixels from screen left to left of main window.'),            \r
1043             Setting(section=self.setting_section, option='selected command',\r
1044                     value='load playlist',\r
1045                     help='Name of the initially selected command.'),\r
1046             ]\r
1047 \r
1048     def _app(self, commands, ui_to_command_queue, command_to_ui_queue):\r
1049         redirect = True\r
1050         if __debug__:\r
1051             redirect=False\r
1052         app = HookeApp(gui=self,\r
1053                        commands=commands,\r
1054                        inqueue=ui_to_command_queue,\r
1055                        outqueue=command_to_ui_queue,\r
1056                        redirect=redirect)\r
1057         return app\r
1058 \r
1059     def run(self, commands, ui_to_command_queue, command_to_ui_queue):\r
1060         app = self._app(commands, ui_to_command_queue, command_to_ui_queue)\r
1061         app.MainLoop()\r