034003b7d5278539d417b1dcddf19c06f7a3de20
[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.gui.config['main height'] = str(self.GetSize().GetHeight())
273         self.gui.config['main left'] = str(self.GetPosition()[0])
274         self.gui.config['main top'] = str(self.GetPosition()[1])
275         self.gui.config['main width'] = str(self.GetSize().GetWidth())
276         # push changes back to Hooke.config?
277         self._c['manager'].UnInit()
278         del self._c['manager']
279         self.Destroy()
280
281
282
283     # Panel utility functions
284
285     def _file_name(self, name):
286         """Cleanup names according to configured preferences.
287         """
288         if self.gui.config['hide extensions'] == True:
289             name,ext = os.path.splitext(name)
290         return name
291
292
293
294     # Command handling
295
296     def _command_by_name(self, name):
297         cs = [c for c in self.commands if c.name == name]
298         if len(cs) == 0:
299             raise KeyError(name)
300         elif len(cs) > 1:
301             raise Exception('Multiple commands named "%s"' % name)
302         return cs[0]
303
304     def execute_command(self, _class=None, method=None,
305                         command=None, args=None):
306         if args == None:
307             args = {}
308         if ('property editor' in self._c
309             and self.gui.config['selected command'] == command):
310             for name,value in self._c['property editor'].get_values().items():
311                 arg = self._c['property editor']._argument_from_label.get(
312                     name, None)
313                 if arg == None:
314                     continue
315                 elif arg.count == 1:
316                     args[arg.name] = value
317                     continue
318                 # deal with counted arguments
319                 if arg.name not in args:
320                     args[arg.name] = {}
321                 index = int(name[len(arg.name):])
322                 args[arg.name][index] = value
323             for arg in command.arguments:
324                 if arg.name not in args:
325                     continue  # undisplayed argument, e.g. 'driver' types.
326                 count = arg.count
327                 if hasattr(arg, '_display_count'):  # support HACK in props_from_argument()
328                     count = arg._display_count
329                 if count != 1 and arg.name in args:
330                     keys = sorted(args[arg.name].keys())
331                     assert keys == range(count), keys
332                     args[arg.name] = [args[arg.name][i]
333                                       for i in range(count)]
334                 if arg.count == -1:
335                     while (len(args[arg.name]) > 0
336                            and args[arg.name][-1] == None):
337                         args[arg.name].pop()
338                     if len(args[arg.name]) == 0:
339                         args[arg.name] = arg.default
340         cm = CommandMessage(command.name, args)
341         self.gui._submit_command(cm, self.inqueue)
342         results = []
343         while True:
344             msg = self.outqueue.get()
345             results.append(msg)
346             if isinstance(msg, Exit):
347                 self._on_close()
348                 break
349             elif isinstance(msg, CommandExit):
350                 # TODO: display command complete
351                 break
352             elif isinstance(msg, ReloadUserInterfaceConfig):
353                 self.gui.reload_config(msg.config)
354                 continue
355             elif isinstance(msg, Request):
356                 h = handler.HANDLERS[msg.type]
357                 h.run(self, msg)  # TODO: pause for response?
358                 continue
359         pp = getattr(
360             self, '_postprocess_%s' % command.name.replace(' ', '_'),
361             self._postprocess_text)
362         pp(command=command, args=args, results=results)
363         return results
364
365     def _handle_request(self, msg):
366         """Repeatedly try to get a response to `msg`.
367         """
368         if prompt == None:
369             raise NotImplementedError('_%s_request_prompt' % msg.type)
370         prompt_string = prompt(msg)
371         parser = getattr(self, '_%s_request_parser' % msg.type, None)
372         if parser == None:
373             raise NotImplementedError('_%s_request_parser' % msg.type)
374         error = None
375         while True:
376             if error != None:
377                 self.cmd.stdout.write(''.join([
378                         error.__class__.__name__, ': ', str(error), '\n']))
379             self.cmd.stdout.write(prompt_string)
380             value = parser(msg, self.cmd.stdin.readline())
381             try:
382                 response = msg.response(value)
383                 break
384             except ValueError, error:
385                 continue
386         self.inqueue.put(response)
387
388
389
390     # Command-specific postprocessing
391
392     def _postprocess_text(self, command, args={}, results=[]):
393         """Print the string representation of the results to the Results window.
394
395         This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
396         approach, except that :class:`~hooke.ui.commandline.DoCommand`
397         doesn't print some internally handled messages
398         (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
399         """
400         for result in results:
401             if isinstance(result, CommandExit):
402                 self._c['output'].write(result.__class__.__name__+'\n')
403             self._c['output'].write(str(result).rstrip()+'\n')
404
405     def _postprocess_playlists(self, command, args={}, results=None):
406         """Update `self` to show the playlists.
407         """
408         if not isinstance(results[-1], Success):
409             self._postprocess_text(command, results=results)
410             return
411         assert len(results) == 2, results
412         playlists = results[0]
413         if 'playlist' in self._c:
414             for playlist in playlists:
415                 if self._c['playlist'].is_playlist_loaded(playlist):
416                     self._c['playlist'].update_playlist(playlist)
417                 else:
418                     self._c['playlist'].add_playlist(playlist)
419
420     def _postprocess_new_playlist(self, command, args={}, results=None):
421         """Update `self` to show the new playlist.
422         """
423         if not isinstance(results[-1], Success):
424             self._postprocess_text(command, results=results)
425             return
426         assert len(results) == 2, results
427         playlist = results[0]
428         if 'playlist' in self._c:
429             loaded = self._c['playlist'].is_playlist_loaded(playlist)
430             assert loaded == False, loaded
431             self._c['playlist'].add_playlist(playlist)
432
433     def _postprocess_load_playlist(self, command, args={}, results=None):
434         """Update `self` to show the playlist.
435         """
436         if not isinstance(results[-1], Success):
437             self._postprocess_text(command, results=results)
438             return
439         assert len(results) == 2, results
440         playlist = results[0]
441         self._c['playlist'].add_playlist(playlist)
442
443     def _postprocess_get_playlist(self, command, args={}, results=[]):
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         if 'playlist' in self._c:
450             loaded = self._c['playlist'].is_playlist_loaded(playlist)
451             assert loaded == True, loaded
452             self._c['playlist'].update_playlist(playlist)
453
454     def _postprocess_get_curve(self, command, args={}, results=[]):
455         """Update `self` to show the curve.
456         """
457         if not isinstance(results[-1], Success):
458             self._postprocess_text(command, results=results)
459             return
460         assert len(results) == 2, results
461         curve = results[0]
462         if args.get('curve', None) == None:
463             # the command defaults to the current curve of the current playlist
464             results = self.execute_command(
465                 command=self._command_by_name('get playlist'))
466             playlist = results[0]
467         else:
468             raise NotImplementedError()
469         if 'note' in self._c:
470             self._c['note'].set_text(curve.info.get('note', ''))
471         if 'playlist' in self._c:
472             self._c['playlist'].set_selected_curve(
473                 playlist, curve)
474         if 'plot' in self._c:
475             self._c['plot'].set_curve(curve, config=self.gui.config)
476
477     def _postprocess_next_curve(self, command, args={}, results=[]):
478         """No-op.  Only call 'next curve' via `self._next_curve()`.
479         """
480         pass
481
482     def _postprocess_previous_curve(self, command, args={}, results=[]):
483         """No-op.  Only call 'previous curve' via `self._previous_curve()`.
484         """
485         pass
486
487     def _postprocess_glob_curves_to_playlist(
488         self, command, args={}, results=[]):
489         """Update `self` to show new curves.
490         """
491         if not isinstance(results[-1], Success):
492             self._postprocess_text(command, results=results)
493             return
494         if 'playlist' in self._c:
495             if args.get('playlist', None) != None:
496                 playlist = args['playlist']
497                 pname = playlist.name
498                 loaded = self._c['playlist'].is_playlist_name_loaded(pname)
499                 assert loaded == True, loaded
500                 for curve in results[:-1]:
501                     self._c['playlist']._add_curve(pname, curve)
502             else:
503                 self.execute_command(
504                     command=self._command_by_name('get playlist'))
505
506     def _postprocess_zero_block_surface_contact_point(
507         self, command, args={}, results=[]):
508         """Update the curve, since the available columns may have changed.
509         """
510         if isinstance(results[-1], Success):
511             self.execute_command(
512                 command=self._command_by_name('get curve'))
513  
514     def _postprocess_add_block_force_array(
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
523
524     # TODO: cruft
525
526     def _GetActiveFileIndex(self):
527         lib.playlist.Playlist = self.GetActivePlaylist()
528         #get the selected item from the tree
529         selected_item = self._c['playlist']._c['tree'].GetSelection()
530         #test if a playlist or a curve was double-clicked
531         if self._c['playlist']._c['tree'].ItemHasChildren(selected_item):
532             return -1
533         else:
534             count = 0
535             selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
536             while selected_item.IsOk():
537                 count += 1
538                 selected_item = self._c['playlist']._c['tree'].GetPrevSibling(selected_item)
539             return count
540
541     def _GetPlaylistTab(self, name):
542         for index, page in enumerate(self._c['notebook']._tabs._pages):
543             if page.caption == name:
544                 return index
545         return -1
546
547     def select_plugin(self, _class=None, method=None, plugin=None):
548         pass
549
550     def AddPlaylistFromFiles(self, files=[], name='Untitled'):
551         if files:
552             playlist = lib.playlist.Playlist(self, self.drivers)
553             for item in files:
554                 playlist.add_curve(item)
555         if playlist.count > 0:
556             playlist.name = self._GetUniquePlaylistName(name)
557             playlist.reset()
558             self.AddTayliss(playlist)
559
560     def AppliesPlotmanipulator(self, name):
561         '''
562         Returns True if the plotmanipulator 'name' is applied, False otherwise
563         name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
564         '''
565         return self.GetBoolFromConfig('core', 'plotmanipulators', name)
566
567     def ApplyPlotmanipulators(self, plot, plot_file):
568         '''
569         Apply all active plotmanipulators.
570         '''
571         if plot is not None and plot_file is not None:
572             manipulated_plot = copy.deepcopy(plot)
573             for plotmanipulator in self.plotmanipulators:
574                 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
575                     manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
576             return manipulated_plot
577
578     def GetActiveFigure(self):
579         playlist_name = self.GetActivePlaylistName()
580         figure = self.playlists[playlist_name].figure
581         if figure is not None:
582             return figure
583         return None
584
585     def GetActiveFile(self):
586         playlist = self.GetActivePlaylist()
587         if playlist is not None:
588             return playlist.get_active_file()
589         return None
590
591     def GetActivePlot(self):
592         playlist = self.GetActivePlaylist()
593         if playlist is not None:
594             return playlist.get_active_file().plot
595         return None
596
597     def GetDisplayedPlot(self):
598         plot = copy.deepcopy(self.displayed_plot)
599         #plot.curves = []
600         #plot.curves = copy.deepcopy(plot.curves)
601         return plot
602
603     def GetDisplayedPlotCorrected(self):
604         plot = copy.deepcopy(self.displayed_plot)
605         plot.curves = []
606         plot.curves = copy.deepcopy(plot.corrected_curves)
607         return plot
608
609     def GetDisplayedPlotRaw(self):
610         plot = copy.deepcopy(self.displayed_plot)
611         plot.curves = []
612         plot.curves = copy.deepcopy(plot.raw_curves)
613         return plot
614
615     def GetDockArt(self):
616         return self._c['manager'].GetArtProvider()
617
618     def GetPlotmanipulator(self, name):
619         '''
620         Returns a plot manipulator function from its name
621         '''
622         for plotmanipulator in self.plotmanipulators:
623             if plotmanipulator.name == name:
624                 return plotmanipulator
625         return None
626
627     def HasPlotmanipulator(self, name):
628         '''
629         returns True if the plotmanipulator 'name' is loaded, False otherwise
630         '''
631         for plotmanipulator in self.plotmanipulators:
632             if plotmanipulator.command == name:
633                 return True
634         return False
635
636
637     def _on_dir_ctrl_left_double_click(self, event):
638         file_path = self.panelFolders.GetPath()
639         if os.path.isfile(file_path):
640             if file_path.endswith('.hkp'):
641                 self.do_loadlist(file_path)
642         event.Skip()
643
644     def _on_erase_background(self, event):
645         event.Skip()
646
647     def _on_notebook_page_close(self, event):
648         ctrl = event.GetEventObject()
649         playlist_name = ctrl.GetPageText(ctrl._curpage)
650         self.DeleteFromPlaylists(playlist_name)
651
652     def OnPaneClose(self, event):
653         event.Skip()
654
655     def OnPropGridChanged (self, event):
656         prop = event.GetProperty()
657         if prop:
658             item_section = self.panelProperties.SelectedTreeItem
659             item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
660             plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
661             config = self.gui.config[plugin]
662             property_section = self._c['commands']._c['tree'].GetItemText(item_section)
663             property_key = prop.GetName()
664             property_value = prop.GetDisplayedString()
665
666             config[property_section][property_key]['value'] = property_value
667
668     def OnResultsCheck(self, index, flag):
669         results = self.GetActivePlot().results
670         if results.has_key(self.results_str):
671             results[self.results_str].results[index].visible = flag
672             results[self.results_str].update()
673             self.UpdatePlot()
674
675
676     def _on_size(self, event):
677         event.Skip()
678
679     def UpdatePlaylistsTreeSelection(self):
680         playlist = self.GetActivePlaylist()
681         if playlist is not None:
682             if playlist.index >= 0:
683                 self._c['status bar'].set_playlist(playlist)
684                 self.UpdateNote()
685                 self.UpdatePlot()
686
687     def _on_curve_select(self, playlist, curve):
688         #create the plot tab and add playlist to the dictionary
689         plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
690         notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
691         #tab_index = self._c['notebook'].GetSelection()
692         playlist.figure = plotPanel.get_figure()
693         self.playlists[playlist.name] = playlist
694         #self.playlists[playlist.name] = [playlist, figure]
695         self._c['status bar'].set_playlist(playlist)
696         self.UpdateNote()
697         self.UpdatePlot()
698
699
700     def _on_playlist_left_doubleclick(self):
701         index = self._c['notebook'].GetSelection()
702         current_playlist = self._c['notebook'].GetPageText(index)
703         if current_playlist != playlist_name:
704             index = self._GetPlaylistTab(playlist_name)
705             self._c['notebook'].SetSelection(index)
706         self._c['status bar'].set_playlist(playlist)
707         self.UpdateNote()
708         self.UpdatePlot()
709
710     def _on_playlist_delete(self, playlist):
711         notebook = self.Parent.plotNotebook
712         index = self.Parent._GetPlaylistTab(playlist.name)
713         notebook.SetSelection(index)
714         notebook.DeletePage(notebook.GetSelection())
715         self.Parent.DeleteFromPlaylists(playlist_name)
716
717
718
719     # Command panel interface
720
721     def select_command(self, _class, method, command):
722         #self.select_plugin(plugin=command.plugin)
723         self._c['property editor'].clear()
724         self._c['property editor']._argument_from_label = {}
725         for argument in command.arguments:
726             if argument.name == 'help':
727                 continue
728
729             results = self.execute_command(
730                 command=self._command_by_name('playlists'))
731             if not isinstance(results[-1], Success):
732                 self._postprocess_text(command, results=results)
733                 playlists = []
734             else:
735                 playlists = results[0]
736
737             results = self.execute_command(
738                 command=self._command_by_name('playlist curves'))
739             if not isinstance(results[-1], Success):
740                 self._postprocess_text(command, results=results)
741                 curves = []
742             else:
743                 curves = results[0]
744
745             ret = props_from_argument(
746                 argument, curves=curves, playlists=playlists)
747             if ret == None:
748                 continue  # property intentionally not handled (yet)
749             for label,p in ret:
750                 self._c['property editor'].append_property(p)
751                 self._c['property editor']._argument_from_label[label] = (
752                     argument)
753
754         self.gui.config['selected command'] = command  # TODO: push to engine
755
756
757
758     # Note panel interface
759
760     def _on_update_note(self, _class, method, text):
761         """Sets the note for the active curve.
762         """
763         self.execute_command(
764             command=self._command_by_name('set note'),
765             args={'note':text})
766
767
768
769     # Playlist panel interface
770
771     def _on_user_delete_playlist(self, _class, method, playlist):
772         pass
773
774     def _on_delete_playlist(self, _class, method, playlist):
775         if hasattr(playlist, 'path') and playlist.path != None:
776             os.remove(playlist.path)
777
778     def _on_user_delete_curve(self, _class, method, playlist, curve):
779         pass
780
781     def _on_delete_curve(self, _class, method, playlist, curve):
782         # TODO: execute_command 'remove curve from playlist'
783         os.remove(curve.path)
784
785     def _on_set_selected_playlist(self, _class, method, playlist):
786         """Call the `jump to playlist` command.
787         """
788         results = self.execute_command(
789             command=self._command_by_name('playlists'))
790         if not isinstance(results[-1], Success):
791             return
792         assert len(results) == 2, results
793         playlists = results[0]
794         matching = [p for p in playlists if p.name == playlist.name]
795         assert len(matching) == 1, matching
796         index = playlists.index(matching[0])
797         results = self.execute_command(
798             command=self._command_by_name('jump to playlist'),
799             args={'index':index})
800
801     def _on_set_selected_curve(self, _class, method, playlist, curve):
802         """Call the `jump to curve` command.
803         """
804         self._on_set_selected_playlist(_class, method, playlist)
805         index = playlist.index(curve)
806         results = self.execute_command(
807             command=self._command_by_name('jump to curve'),
808             args={'index':index})
809         if not isinstance(results[-1], Success):
810             return
811         #results = self.execute_command(
812         #    command=self._command_by_name('get playlist'))
813         #if not isinstance(results[-1], Success):
814         #    return
815         self.execute_command(
816             command=self._command_by_name('get curve'))
817
818
819
820     # Plot panel interface
821
822     def _on_plot_status_text(self, _class, method, text):
823         if 'status bar' in self._c:
824             self._c['status bar'].set_plot_text(text)
825
826
827
828     # Navbar interface
829
830     def _next_curve(self, *args):
831         """Call the `next curve` command.
832         """
833         results = self.execute_command(
834             command=self._command_by_name('next curve'))
835         if isinstance(results[-1], Success):
836             self.execute_command(
837                 command=self._command_by_name('get curve'))
838
839     def _previous_curve(self, *args):
840         """Call the `previous curve` command.
841         """
842         results = self.execute_command(
843             command=self._command_by_name('previous curve'))
844         if isinstance(results[-1], Success):
845             self.execute_command(
846                 command=self._command_by_name('get curve'))
847
848
849
850     # Panel display handling
851
852     def _on_panel_visibility(self, _class, method, panel_name, visible):
853         pane = self._c['manager'].GetPane(panel_name)
854         pane.Show(visible)
855         #if we don't do the following, the Folders pane does not resize properly on hide/show
856         if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
857             #folders_size = pane.GetSize()
858             self.panelFolders.Fit()
859         self._c['manager'].Update()
860
861     def _setup_perspectives(self):
862         """Add perspectives to menubar and _perspectives.
863         """
864         self._perspectives = {
865             'Default': self._c['manager'].SavePerspective(),
866             }
867         path = self.gui.config['perspective path']
868         if os.path.isdir(path):
869             files = sorted(os.listdir(path))
870             for fname in files:
871                 name, extension = os.path.splitext(fname)
872                 if extension != self.gui.config['perspective extension']:
873                     continue
874                 fpath = os.path.join(path, fname)
875                 if not os.path.isfile(fpath):
876                     continue
877                 perspective = None
878                 with open(fpath, 'rU') as f:
879                     perspective = f.readline()
880                 if perspective:
881                     self._perspectives[name] = perspective
882
883         selected_perspective = self.gui.config['active perspective']
884         if not self._perspectives.has_key(selected_perspective):
885             self.gui.config['active perspective'] = 'Default'  # TODO: push to engine's Hooke
886
887         self._restore_perspective(selected_perspective, force=True)
888         self._update_perspective_menu()
889
890     def _update_perspective_menu(self):
891         self._c['menu bar']._c['perspective'].update(
892             sorted(self._perspectives.keys()),
893             self.gui.config['active perspective'])
894
895     def _save_perspective(self, perspective, perspective_dir, name,
896                           extension=None):
897         path = os.path.join(perspective_dir, name)
898         if extension != None:
899             path += extension
900         if not os.path.isdir(perspective_dir):
901             os.makedirs(perspective_dir)
902         with open(path, 'w') as f:
903             f.write(perspective)
904         self._perspectives[name] = perspective
905         self._restore_perspective(name)
906         self._update_perspective_menu()
907
908     def _delete_perspectives(self, perspective_dir, names,
909                              extension=None):
910         self.log.debug('remove perspectives %s from %s'
911                        % (names, perspective_dir))
912         for name in names:
913             path = os.path.join(perspective_dir, name)
914             if extension != None:
915                 path += extension
916             os.remove(path)
917             del(self._perspectives[name])
918         self._update_perspective_menu()
919         if self.gui.config['active perspective'] in names:
920             self._restore_perspective('Default')
921         # TODO: does this bug still apply?
922         # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
923         #   http://trac.wxwidgets.org/ticket/3258 
924         # ) that makes the radio item indicator in the menu disappear.
925         # The code should be fine once this issue is fixed.
926
927     def _restore_perspective(self, name, force=False):
928         if name != self.gui.config['active perspective'] or force == True:
929             self.log.debug('restore perspective %s' % name)
930             self.gui.config['active perspective'] = name  # TODO: push to engine's Hooke
931             self._c['manager'].LoadPerspective(self._perspectives[name])
932             self._c['manager'].Update()
933             for pane in self._c['manager'].GetAllPanes():
934                 view = self._c['menu bar']._c['view']
935                 if pane.name in view._c.keys():
936                     view._c[pane.name].Check(pane.window.IsShown())
937
938     def _on_save_perspective(self, *args):
939         perspective = self._c['manager'].SavePerspective()
940         name = self.gui.config['active perspective']
941         if name == 'Default':
942             name = 'New perspective'
943         name = select_save_file(
944             directory=self.gui.config['perspective path'],
945             name=name,
946             extension=self.gui.config['perspective extension'],
947             parent=self,
948             message='Enter a name for the new perspective:',
949             caption='Save perspective')
950         if name == None:
951             return
952         self._save_perspective(
953             perspective, self.gui.config['perspective path'], name=name,
954             extension=self.gui.config['perspective extension'])
955
956     def _on_delete_perspective(self, *args, **kwargs):
957         options = sorted([p for p in self._perspectives.keys()
958                           if p != 'Default'])
959         dialog = SelectionDialog(
960             options=options,
961             message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
962             button_id=wx.ID_DELETE,
963             selection_style='multiple',
964             parent=self,
965             title='Delete perspective(s)',
966             style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
967         dialog.CenterOnScreen()
968         dialog.ShowModal()
969         if dialog.canceled == True:
970             return
971         names = [options[i] for i in dialog.selected]
972         dialog.Destroy()
973         self._delete_perspectives(
974             self.gui.config['perspective path'], names=names,
975             extension=self.gui.config['perspective extension'])
976
977     def _on_select_perspective(self, _class, method, name):
978         self._restore_perspective(name)
979
980
981
982 class HookeApp (wx.App):
983     """A :class:`wx.App` wrapper around :class:`HookeFrame`.
984
985     Tosses up a splash screen and then loads :class:`HookeFrame` in
986     its own window.
987     """
988     def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
989         self.gui = gui
990         self.commands = commands
991         self.inqueue = inqueue
992         self.outqueue = outqueue
993         super(HookeApp, self).__init__(*args, **kwargs)
994
995     def OnInit(self):
996         self.SetAppName('Hooke')
997         self.SetVendorName('')
998         self._setup_splash_screen()
999
1000         height = self.gui.config['main height']
1001         width = self.gui.config['main width']
1002         top = self.gui.config['main top']
1003         left = self.gui.config['main left']
1004
1005         # Sometimes, the ini file gets confused and sets 'left' and
1006         # 'top' to large negative numbers.  Here we catch and fix
1007         # this.  Keep small negative numbers, the user might want
1008         # those.
1009         if left < -width:
1010             left = 0
1011         if top < -height:
1012             top = 0
1013
1014         self._c = {
1015             'frame': HookeFrame(
1016                 self.gui, self.commands, self.inqueue, self.outqueue,
1017                 parent=None, title='Hooke',
1018                 pos=(left, top), size=(width, height),
1019                 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
1020             }
1021         self._c['frame'].Show(True)
1022         self.SetTopWindow(self._c['frame'])
1023         return True
1024
1025     def _setup_splash_screen(self):
1026         if self.gui.config['show splash screen'] == True:
1027             path = self.gui.config['splash screen image']
1028             if os.path.isfile(path):
1029                 duration = self.gui.config['splash screen duration']
1030                 wx.SplashScreen(
1031                     bitmap=wx.Image(path).ConvertToBitmap(),
1032                     splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
1033                     milliseconds=duration,
1034                     parent=None)
1035                 wx.Yield()
1036                 # For some reason splashDuration and sleep do not
1037                 # correspond to each other at least not on Windows.
1038                 # Maybe it's because duration is in milliseconds and
1039                 # sleep in seconds.  Thus we need to increase the
1040                 # sleep time a bit. A factor of 1.2 seems to work.
1041                 sleepFactor = 1.2
1042                 time.sleep(sleepFactor * duration / 1000)
1043
1044
1045 class GUI (UserInterface):
1046     """wxWindows graphical user interface.
1047     """
1048     def __init__(self):
1049         super(GUI, self).__init__(name='gui')
1050
1051     def default_settings(self):
1052         """Return a list of :class:`hooke.config.Setting`\s for any
1053         configurable UI settings.
1054
1055         The suggested section setting is::
1056
1057             Setting(section=self.setting_section, help=self.__doc__)
1058         """
1059         return [
1060             Setting(section=self.setting_section, help=self.__doc__),
1061             Setting(section=self.setting_section, option='icon image',
1062                     value=os.path.join('doc', 'img', 'microscope.ico'),
1063                     type='file',
1064                     help='Path to the hooke icon image.'),
1065             Setting(section=self.setting_section, option='show splash screen',
1066                     value=True, type='bool',
1067                     help='Enable/disable the splash screen'),
1068             Setting(section=self.setting_section, option='splash screen image',
1069                     value=os.path.join('doc', 'img', 'hooke.jpg'),
1070                     type='file',
1071                     help='Path to the Hooke splash screen image.'),
1072             Setting(section=self.setting_section,
1073                     option='splash screen duration',
1074                     value=1000, type='int',
1075                     help='Duration of the splash screen in milliseconds.'),
1076             Setting(section=self.setting_section, option='perspective path',
1077                     value=os.path.join('resources', 'gui', 'perspective'),
1078                     help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
1079             Setting(section=self.setting_section, option='perspective extension',
1080                     value='.txt',
1081                     help='Extension for perspective files.'),
1082             Setting(section=self.setting_section, option='hide extensions',
1083                     value=False, type='bool',
1084                     help='Hide file extensions when displaying names.'),
1085             Setting(section=self.setting_section, option='plot legend',
1086                     value=True, type='bool',
1087                     help='Enable/disable the plot legend.'),
1088             Setting(section=self.setting_section, option='plot SI format',
1089                     value='True', type='bool',
1090                     help='Enable/disable SI plot axes numbering.'),
1091             Setting(section=self.setting_section, option='plot decimals',
1092                     value=2, type='int',
1093                     help='Number of decimal places to show if "plot SI format" is enabled.'),
1094             Setting(section=self.setting_section, option='folders-workdir',
1095                     value='.', type='path',
1096                     help='This should probably go...'),
1097             Setting(section=self.setting_section, option='folders-filters',
1098                     value='.', type='path',
1099                     help='This should probably go...'),
1100             Setting(section=self.setting_section, option='active perspective',
1101                     value='Default',
1102                     help='Name of active perspective file (or "Default").'),
1103             Setting(section=self.setting_section,
1104                     option='folders-filter-index',
1105                     value=0, type='int',
1106                     help='This should probably go...'),
1107             Setting(section=self.setting_section, option='main height',
1108                     value=450, type='int',
1109                     help='Height of main window in pixels.'),
1110             Setting(section=self.setting_section, option='main width',
1111                     value=800, type='int',
1112                     help='Width of main window in pixels.'),
1113             Setting(section=self.setting_section, option='main top',
1114                     value=0, type='int',
1115                     help='Pixels from screen top to top of main window.'),
1116             Setting(section=self.setting_section, option='main left',
1117                     value=0, type='int',
1118                     help='Pixels from screen left to left of main window.'),
1119             Setting(section=self.setting_section, option='selected command',
1120                     value='load playlist',
1121                     help='Name of the initially selected command.'),
1122             ]
1123
1124     def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
1125         redirect = True
1126         if __debug__:
1127             redirect=False
1128         app = HookeApp(gui=self,
1129                        commands=commands,
1130                        inqueue=ui_to_command_queue,
1131                        outqueue=command_to_ui_queue,
1132                        redirect=redirect)
1133         return app
1134
1135     def run(self, commands, ui_to_command_queue, command_to_ui_queue):
1136         app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
1137         app.MainLoop()