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