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