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