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