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