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