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