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