Update the curve in ui.gui after a vclamp plugin command.
[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     def _postprocess_zero_block_surface_contact_point(\r
423         self, command, args={}, results=[]):\r
424         """Update the curve, since the available columns may have changed.\r
425         """\r
426         if isinstance(results[-1], Success):\r
427             self.execute_command(\r
428                 command=self._command_by_name('get curve'))\r
429  \r
430     def _postprocess_add_block_force_array(\r
431         self, command, args={}, results=[]):\r
432         """Update the curve, since the available columns may have changed.\r
433         """\r
434         if isinstance(results[-1], Success):\r
435             self.execute_command(\r
436                 command=self._command_by_name('get curve'))\r
437 \r
438 \r
439 \r
440     # TODO: cruft\r
441 \r
442     def _GetActiveFileIndex(self):\r
443         lib.playlist.Playlist = self.GetActivePlaylist()\r
444         #get the selected item from the tree\r
445         selected_item = self._c['playlist']._c['tree'].GetSelection()\r
446         #test if a playlist or a curve was double-clicked\r
447         if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):\r
448             return -1\r
449         else:\r
450             count = 0\r
451             selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)\r
452             while selected_item.IsOk():\r
453                 count += 1\r
454                 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)\r
455             return count\r
456 \r
457     def _GetPlaylistTab(self, name):\r
458         for index, page in enumerate(self._c['notebook']._tabs._pages):\r
459             if page.caption == name:\r
460                 return index\r
461         return -1\r
462 \r
463     def select_plugin(self, _class=None, method=None, plugin=None):\r
464         pass\r
465 \r
466     def AddPlaylistFromFiles(self, files=[], name='Untitled'):\r
467         if files:\r
468             playlist = lib.playlist.Playlist(self, self.drivers)\r
469             for item in files:\r
470                 playlist.add_curve(item)\r
471         if playlist.count > 0:\r
472             playlist.name = self._GetUniquePlaylistName(name)\r
473             playlist.reset()\r
474             self.AddTayliss(playlist)\r
475 \r
476     def AppliesPlotmanipulator(self, name):\r
477         '''\r
478         Returns True if the plotmanipulator 'name' is applied, False otherwise\r
479         name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')\r
480         '''\r
481         return self.GetBoolFromConfig('core', 'plotmanipulators', name)\r
482 \r
483     def ApplyPlotmanipulators(self, plot, plot_file):\r
484         '''\r
485         Apply all active plotmanipulators.\r
486         '''\r
487         if plot is not None and plot_file is not None:\r
488             manipulated_plot = copy.deepcopy(plot)\r
489             for plotmanipulator in self.plotmanipulators:\r
490                 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):\r
491                     manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)\r
492             return manipulated_plot\r
493 \r
494     def GetActiveFigure(self):\r
495         playlist_name = self.GetActivePlaylistName()\r
496         figure = self.playlists[playlist_name].figure\r
497         if figure is not None:\r
498             return figure\r
499         return None\r
500 \r
501     def GetActiveFile(self):\r
502         playlist = self.GetActivePlaylist()\r
503         if playlist is not None:\r
504             return playlist.get_active_file()\r
505         return None\r
506 \r
507     def GetActivePlot(self):\r
508         playlist = self.GetActivePlaylist()\r
509         if playlist is not None:\r
510             return playlist.get_active_file().plot\r
511         return None\r
512 \r
513     def GetDisplayedPlot(self):\r
514         plot = copy.deepcopy(self.displayed_plot)\r
515         #plot.curves = []\r
516         #plot.curves = copy.deepcopy(plot.curves)\r
517         return plot\r
518 \r
519     def GetDisplayedPlotCorrected(self):\r
520         plot = copy.deepcopy(self.displayed_plot)\r
521         plot.curves = []\r
522         plot.curves = copy.deepcopy(plot.corrected_curves)\r
523         return plot\r
524 \r
525     def GetDisplayedPlotRaw(self):\r
526         plot = copy.deepcopy(self.displayed_plot)\r
527         plot.curves = []\r
528         plot.curves = copy.deepcopy(plot.raw_curves)\r
529         return plot\r
530 \r
531     def GetDockArt(self):\r
532         return self._c['manager'].GetArtProvider()\r
533 \r
534     def GetPlotmanipulator(self, name):\r
535         '''\r
536         Returns a plot manipulator function from its name\r
537         '''\r
538         for plotmanipulator in self.plotmanipulators:\r
539             if plotmanipulator.name == name:\r
540                 return plotmanipulator\r
541         return None\r
542 \r
543     def HasPlotmanipulator(self, name):\r
544         '''\r
545         returns True if the plotmanipulator 'name' is loaded, False otherwise\r
546         '''\r
547         for plotmanipulator in self.plotmanipulators:\r
548             if plotmanipulator.command == name:\r
549                 return True\r
550         return False\r
551 \r
552 \r
553     def _on_dir_ctrl_left_double_click(self, event):\r
554         file_path = self.panelFolders.GetPath()\r
555         if os.path.isfile(file_path):\r
556             if file_path.endswith('.hkp'):\r
557                 self.do_loadlist(file_path)\r
558         event.Skip()\r
559 \r
560     def _on_erase_background(self, event):\r
561         event.Skip()\r
562 \r
563     def _on_notebook_page_close(self, event):\r
564         ctrl = event.GetEventObject()\r
565         playlist_name = ctrl.GetPageText(ctrl._curpage)\r
566         self.DeleteFromPlaylists(playlist_name)\r
567 \r
568     def OnPaneClose(self, event):\r
569         event.Skip()\r
570 \r
571     def OnPropGridChanged (self, event):\r
572         prop = event.GetProperty()\r
573         if prop:\r
574             item_section = self.panelProperties.SelectedTreeItem\r
575             item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)\r
576             plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)\r
577             config = self.gui.config[plugin]\r
578             property_section = self._c['commands']._c['tree'].GetItemText(item_section)\r
579             property_key = prop.GetName()\r
580             property_value = prop.GetDisplayedString()\r
581 \r
582             config[property_section][property_key]['value'] = property_value\r
583 \r
584     def OnResultsCheck(self, index, flag):\r
585         results = self.GetActivePlot().results\r
586         if results.has_key(self.results_str):\r
587             results[self.results_str].results[index].visible = flag\r
588             results[self.results_str].update()\r
589             self.UpdatePlot()\r
590 \r
591 \r
592     def _on_size(self, event):\r
593         event.Skip()\r
594 \r
595     def OnUpdateNote(self, event):\r
596         '''\r
597         Saves the note to the active file.\r
598         '''\r
599         active_file = self.GetActiveFile()\r
600         active_file.note = self.panelNote.Editor.GetValue()\r
601 \r
602     def UpdateNote(self):\r
603         #update the note for the active file\r
604         active_file = self.GetActiveFile()\r
605         if active_file is not None:\r
606             self.panelNote.Editor.SetValue(active_file.note)\r
607 \r
608     def UpdatePlaylistsTreeSelection(self):\r
609         playlist = self.GetActivePlaylist()\r
610         if playlist is not None:\r
611             if playlist.index >= 0:\r
612                 self._c['status bar'].set_playlist(playlist)\r
613                 self.UpdateNote()\r
614                 self.UpdatePlot()\r
615 \r
616     def _on_curve_select(self, playlist, curve):\r
617         #create the plot tab and add playlist to the dictionary\r
618         plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))\r
619         notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)\r
620         #tab_index = self._c['notebook'].GetSelection()\r
621         playlist.figure = plotPanel.get_figure()\r
622         self.playlists[playlist.name] = playlist\r
623         #self.playlists[playlist.name] = [playlist, figure]\r
624         self._c['status bar'].set_playlist(playlist)\r
625         self.UpdateNote()\r
626         self.UpdatePlot()\r
627 \r
628 \r
629     def _on_playlist_left_doubleclick(self):\r
630         index = self._c['notebook'].GetSelection()\r
631         current_playlist = self._c['notebook'].GetPageText(index)\r
632         if current_playlist != playlist_name:\r
633             index = self._GetPlaylistTab(playlist_name)\r
634             self._c['notebook'].SetSelection(index)\r
635         self._c['status bar'].set_playlist(playlist)\r
636         self.UpdateNote()\r
637         self.UpdatePlot()\r
638 \r
639     def _on_playlist_delete(self, playlist):\r
640         notebook = self.Parent.plotNotebook\r
641         index = self.Parent._GetPlaylistTab(playlist.name)\r
642         notebook.SetSelection(index)\r
643         notebook.DeletePage(notebook.GetSelection())\r
644         self.Parent.DeleteFromPlaylists(playlist_name)\r
645 \r
646 \r
647 \r
648     # Command panel interface\r
649 \r
650     def select_command(self, _class, method, command):\r
651         #self.select_plugin(plugin=command.plugin)\r
652         if 'assistant' in self._c:\r
653             self._c['assitant'].ChangeValue(command.help)\r
654         self._c['property editor'].clear()\r
655         for argument in command.arguments:\r
656             if argument.name == 'help':\r
657                 continue\r
658 \r
659             results = self.execute_command(\r
660                 command=self._command_by_name('playlists'))\r
661             if not isinstance(results[-1], Success):\r
662                 self._postprocess_text(command, results=results)\r
663                 playlists = []\r
664             else:\r
665                 playlists = results[0]\r
666 \r
667             results = self.execute_command(\r
668                 command=self._command_by_name('playlist curves'))\r
669             if not isinstance(results[-1], Success):\r
670                 self._postprocess_text(command, results=results)\r
671                 curves = []\r
672             else:\r
673                 curves = results[0]\r
674 \r
675             p = prop_from_argument(\r
676                 argument, curves=curves, playlists=playlists)\r
677             if p == None:\r
678                 continue  # property intentionally not handled (yet)\r
679             self._c['property editor'].append_property(p)\r
680 \r
681         self.gui.config['selected command'] = command  # TODO: push to engine\r
682 \r
683 \r
684 \r
685     # Playlist panel interface\r
686 \r
687     def _on_user_delete_playlist(self, _class, method, playlist):\r
688         pass\r
689 \r
690     def _on_delete_playlist(self, _class, method, playlist):\r
691         if hasattr(playlist, 'path') and playlist.path != None:\r
692             os.remove(playlist.path)\r
693 \r
694     def _on_user_delete_curve(self, _class, method, playlist, curve):\r
695         pass\r
696 \r
697     def _on_delete_curve(self, _class, method, playlist, curve):\r
698         os.remove(curve.path)\r
699 \r
700     def _on_set_selected_playlist(self, _class, method, playlist):\r
701         """TODO: playlists plugin with `jump to playlist`.\r
702         """\r
703         pass\r
704 \r
705     def _on_set_selected_curve(self, _class, method, playlist, curve):\r
706         """Call the `jump to curve` command.\r
707 \r
708         TODO: playlists plugin.\r
709         """\r
710         # TODO: jump to playlist, get playlist\r
711         index = playlist.index(curve)\r
712         results = self.execute_command(\r
713             command=self._command_by_name('jump to curve'),\r
714             args={'index':index})\r
715         if not isinstance(results[-1], Success):\r
716             return\r
717         #results = self.execute_command(\r
718         #    command=self._command_by_name('get playlist'))\r
719         #if not isinstance(results[-1], Success):\r
720         #    return\r
721         self.execute_command(\r
722             command=self._command_by_name('get curve'))\r
723 \r
724 \r
725 \r
726     # Navbar interface\r
727 \r
728     def _next_curve(self, *args):\r
729         """Call the `next curve` command.\r
730         """\r
731         results = self.execute_command(\r
732             command=self._command_by_name('next curve'))\r
733         if isinstance(results[-1], Success):\r
734             self.execute_command(\r
735                 command=self._command_by_name('get curve'))\r
736 \r
737     def _previous_curve(self, *args):\r
738         """Call the `previous curve` command.\r
739         """\r
740         results = self.execute_command(\r
741             command=self._command_by_name('previous curve'))\r
742         if isinstance(results[-1], Success):\r
743             self.execute_command(\r
744                 command=self._command_by_name('get curve'))\r
745 \r
746 \r
747 \r
748     # Panel display handling\r
749 \r
750     def _on_panel_visibility(self, _class, method, panel_name, visible):\r
751         pane = self._c['manager'].GetPane(panel_name)\r
752         pane.Show(visible)\r
753         #if we don't do the following, the Folders pane does not resize properly on hide/show\r
754         if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():\r
755             #folders_size = pane.GetSize()\r
756             self.panelFolders.Fit()\r
757         self._c['manager'].Update()\r
758 \r
759     def _setup_perspectives(self):\r
760         """Add perspectives to menubar and _perspectives.\r
761         """\r
762         self._perspectives = {\r
763             'Default': self._c['manager'].SavePerspective(),\r
764             }\r
765         path = self.gui.config['perspective path']\r
766         if os.path.isdir(path):\r
767             files = sorted(os.listdir(path))\r
768             for fname in files:\r
769                 name, extension = os.path.splitext(fname)\r
770                 if extension != self.gui.config['perspective extension']:\r
771                     continue\r
772                 fpath = os.path.join(path, fname)\r
773                 if not os.path.isfile(fpath):\r
774                     continue\r
775                 perspective = None\r
776                 with open(fpath, 'rU') as f:\r
777                     perspective = f.readline()\r
778                 if perspective:\r
779                     self._perspectives[name] = perspective\r
780 \r
781         selected_perspective = self.gui.config['active perspective']\r
782         if not self._perspectives.has_key(selected_perspective):\r
783             self.gui.config['active perspective'] = 'Default'  # TODO: push to engine's Hooke\r
784 \r
785         self._restore_perspective(selected_perspective, force=True)\r
786         self._update_perspective_menu()\r
787 \r
788     def _update_perspective_menu(self):\r
789         self._c['menu bar']._c['perspective'].update(\r
790             sorted(self._perspectives.keys()),\r
791             self.gui.config['active perspective'])\r
792 \r
793     def _save_perspective(self, perspective, perspective_dir, name,\r
794                           extension=None):\r
795         path = os.path.join(perspective_dir, name)\r
796         if extension != None:\r
797             path += extension\r
798         if not os.path.isdir(perspective_dir):\r
799             os.makedirs(perspective_dir)\r
800         with open(path, 'w') as f:\r
801             f.write(perspective)\r
802         self._perspectives[name] = perspective\r
803         self._restore_perspective(name)\r
804         self._update_perspective_menu()\r
805 \r
806     def _delete_perspectives(self, perspective_dir, names,\r
807                              extension=None):\r
808         self.log.debug('remove perspectives %s from %s'\r
809                        % (names, perspective_dir))\r
810         for name in names:\r
811             path = os.path.join(perspective_dir, name)\r
812             if extension != None:\r
813                 path += extension\r
814             os.remove(path)\r
815             del(self._perspectives[name])\r
816         self._update_perspective_menu()\r
817         if self.gui.config['active perspective'] in names:\r
818             self._restore_perspective('Default')\r
819         # TODO: does this bug still apply?\r
820         # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258\r
821         #   http://trac.wxwidgets.org/ticket/3258 \r
822         # ) that makes the radio item indicator in the menu disappear.\r
823         # The code should be fine once this issue is fixed.\r
824 \r
825     def _restore_perspective(self, name, force=False):\r
826         if name != self.gui.config['active perspective'] or force == True:\r
827             self.log.debug('restore perspective %s' % name)\r
828             self.gui.config['active perspective'] = name  # TODO: push to engine's Hooke\r
829             self._c['manager'].LoadPerspective(self._perspectives[name])\r
830             self._c['manager'].Update()\r
831             for pane in self._c['manager'].GetAllPanes():\r
832                 view = self._c['menu bar']._c['view']\r
833                 if pane.name in view._c.keys():\r
834                     view._c[pane.name].Check(pane.window.IsShown())\r
835 \r
836     def _on_save_perspective(self, *args):\r
837         perspective = self._c['manager'].SavePerspective()\r
838         name = self.gui.config['active perspective']\r
839         if name == 'Default':\r
840             name = 'New perspective'\r
841         name = select_save_file(\r
842             directory=self.gui.config['perspective path'],\r
843             name=name,\r
844             extension=self.gui.config['perspective extension'],\r
845             parent=self,\r
846             message='Enter a name for the new perspective:',\r
847             caption='Save perspective')\r
848         if name == None:\r
849             return\r
850         self._save_perspective(\r
851             perspective, self.gui.config['perspective path'], name=name,\r
852             extension=self.gui.config['perspective extension'])\r
853 \r
854     def _on_delete_perspective(self, *args, **kwargs):\r
855         options = sorted([p for p in self._perspectives.keys()\r
856                           if p != 'Default'])\r
857         dialog = SelectionDialog(\r
858             options=options,\r
859             message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",\r
860             button_id=wx.ID_DELETE,\r
861             selection_style='multiple',\r
862             parent=self,\r
863             title='Delete perspective(s)',\r
864             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)\r
865         dialog.CenterOnScreen()\r
866         dialog.ShowModal()\r
867         names = [options[i] for i in dialog.selected]\r
868         dialog.Destroy()\r
869         self._delete_perspectives(\r
870             self.gui.config['perspective path'], names=names,\r
871             extension=self.gui.config['perspective extension'])\r
872 \r
873     def _on_select_perspective(self, _class, method, name):\r
874         self._restore_perspective(name)\r
875 \r
876 \r
877 \r
878 class HookeApp (wx.App):\r
879     """A :class:`wx.App` wrapper around :class:`HookeFrame`.\r
880 \r
881     Tosses up a splash screen and then loads :class:`HookeFrame` in\r
882     its own window.\r
883     """\r
884     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):\r
885         self.gui = gui\r
886         self.commands = commands\r
887         self.inqueue = inqueue\r
888         self.outqueue = outqueue\r
889         super(HookeApp, self).__init__(*args, **kwargs)\r
890 \r
891     def OnInit(self):\r
892         self.SetAppName('Hooke')\r
893         self.SetVendorName('')\r
894         self._setup_splash_screen()\r
895 \r
896         height = int(self.gui.config['main height']) # HACK: config should convert\r
897         width = int(self.gui.config['main width'])\r
898         top = int(self.gui.config['main top'])\r
899         left = int(self.gui.config['main left'])\r
900 \r
901         # Sometimes, the ini file gets confused and sets 'left' and\r
902         # 'top' to large negative numbers.  Here we catch and fix\r
903         # this.  Keep small negative numbers, the user might want\r
904         # those.\r
905         if left < -width:\r
906             left = 0\r
907         if top < -height:\r
908             top = 0\r
909 \r
910         self._c = {\r
911             'frame': HookeFrame(\r
912                 self.gui, self.commands, self.inqueue, self.outqueue,\r
913                 parent=None, title='Hooke',\r
914                 pos=(left, top), size=(width, height),\r
915                 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),\r
916             }\r
917         self._c['frame'].Show(True)\r
918         self.SetTopWindow(self._c['frame'])\r
919         return True\r
920 \r
921     def _setup_splash_screen(self):\r
922         if self.gui.config['show splash screen'] == 'True': # HACK: config should decode\r
923             path = self.gui.config['splash screen image']\r
924             if os.path.isfile(path):\r
925                 duration = int(self.gui.config['splash screen duration'])  # HACK: config should decode types\r
926                 wx.SplashScreen(\r
927                     bitmap=wx.Image(path).ConvertToBitmap(),\r
928                     splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,\r
929                     milliseconds=duration,\r
930                     parent=None)\r
931                 wx.Yield()\r
932                 # For some reason splashDuration and sleep do not\r
933                 # correspond to each other at least not on Windows.\r
934                 # Maybe it's because duration is in milliseconds and\r
935                 # sleep in seconds.  Thus we need to increase the\r
936                 # sleep time a bit. A factor of 1.2 seems to work.\r
937                 sleepFactor = 1.2\r
938                 time.sleep(sleepFactor * duration / 1000)\r
939 \r
940 \r
941 class GUI (UserInterface):\r
942     """wxWindows graphical user interface.\r
943     """\r
944     def __init__(self):\r
945         super(GUI, self).__init__(name='gui')\r
946 \r
947     def default_settings(self):\r
948         """Return a list of :class:`hooke.config.Setting`\s for any\r
949         configurable UI settings.\r
950 \r
951         The suggested section setting is::\r
952 \r
953             Setting(section=self.setting_section, help=self.__doc__)\r
954         """\r
955         return [\r
956             Setting(section=self.setting_section, help=self.__doc__),\r
957             Setting(section=self.setting_section, option='icon image',\r
958                     value=os.path.join('doc', 'img', 'microscope.ico'),\r
959                     help='Path to the hooke icon image.'),\r
960             Setting(section=self.setting_section, option='show splash screen',\r
961                     value=True,\r
962                     help='Enable/disable the splash screen'),\r
963             Setting(section=self.setting_section, option='splash screen image',\r
964                     value=os.path.join('doc', 'img', 'hooke.jpg'),\r
965                     help='Path to the Hooke splash screen image.'),\r
966             Setting(section=self.setting_section, option='splash screen duration',\r
967                     value=1000,\r
968                     help='Duration of the splash screen in milliseconds.'),\r
969             Setting(section=self.setting_section, option='perspective path',\r
970                     value=os.path.join('resources', 'gui', 'perspective'),\r
971                     help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.\r
972             Setting(section=self.setting_section, option='perspective extension',\r
973                     value='.txt',\r
974                     help='Extension for perspective files.'),\r
975             Setting(section=self.setting_section, option='hide extensions',\r
976                     value=False,\r
977                     help='Hide file extensions when displaying names.'),\r
978             Setting(section=self.setting_section, option='plot legend',\r
979                     value=True,\r
980                     help='Enable/disable the plot legend.'),\r
981             Setting(section=self.setting_section, option='plot SI format',\r
982                     value='True',\r
983                     help='Enable/disable SI plot axes numbering.'),\r
984             Setting(section=self.setting_section, option='plot decimals',\r
985                     value=2,\r
986                     help='Number of decimal places to show if "plot SI format" is enabled.'),\r
987             Setting(section=self.setting_section, option='folders-workdir',\r
988                     value='.',\r
989                     help='This should probably go...'),\r
990             Setting(section=self.setting_section, option='folders-filters',\r
991                     value='.',\r
992                     help='This should probably go...'),\r
993             Setting(section=self.setting_section, option='active perspective',\r
994                     value='Default',\r
995                     help='Name of active perspective file (or "Default").'),\r
996             Setting(section=self.setting_section, option='folders-filter-index',\r
997                     value='0',\r
998                     help='This should probably go...'),\r
999             Setting(section=self.setting_section, option='main height',\r
1000                     value=450,\r
1001                     help='Height of main window in pixels.'),\r
1002             Setting(section=self.setting_section, option='main width',\r
1003                     value=800,\r
1004                     help='Width of main window in pixels.'),\r
1005             Setting(section=self.setting_section, option='main top',\r
1006                     value=0,\r
1007                     help='Pixels from screen top to top of main window.'),\r
1008             Setting(section=self.setting_section, option='main left',\r
1009                     value=0,\r
1010                     help='Pixels from screen left to left of main window.'),            \r
1011             Setting(section=self.setting_section, option='selected command',\r
1012                     value='load playlist',\r
1013                     help='Name of the initially selected command.'),\r
1014             ]\r
1015 \r
1016     def _app(self, commands, ui_to_command_queue, command_to_ui_queue):\r
1017         redirect = True\r
1018         if __debug__:\r
1019             redirect=False\r
1020         app = HookeApp(gui=self,\r
1021                        commands=commands,\r
1022                        inqueue=ui_to_command_queue,\r
1023                        outqueue=command_to_ui_queue,\r
1024                        redirect=redirect)\r
1025         return app\r
1026 \r
1027     def run(self, commands, ui_to_command_queue, command_to_ui_queue):\r
1028         app = self._app(commands, ui_to_command_queue, command_to_ui_queue)\r
1029         app.MainLoop()\r