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