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