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