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