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