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