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