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