3 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.
\r
9 wxversion.select(WX_GOOD)
\r
19 import wx.aui as aui
\r
20 import wx.lib.evtmgr as evtmgr
\r
23 # wxPropertyGrid is included in wxPython >= 2.9.1, see
\r
24 # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
\r
25 # until then, we'll avoid it because of the *nix build problems.
\r
26 #import wx.propgrid as wxpg
\r
28 from matplotlib.ticker import FuncFormatter
\r
30 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
\r
31 from ...config import Setting
\r
32 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
\r
33 from ...ui import UserInterface, CommandMessage
\r
34 from .dialog.selection import Selection as SelectionDialog
\r
35 from .dialog.save_file import select_save_file
\r
36 from . import menu as menu
\r
37 from . import navbar as navbar
\r
38 from . import panel as panel
\r
39 from .panel.propertyeditor2 import prop_from_argument, prop_from_setting
\r
40 from . import prettyformat as prettyformat
\r
41 from . import statusbar as statusbar
\r
44 class HookeFrame (wx.Frame):
\r
45 """The main Hooke-interface window.
\r
47 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
48 super(HookeFrame, self).__init__(*args, **kwargs)
\r
50 self.commands = commands
\r
51 self.inqueue = inqueue
\r
52 self.outqueue = outqueue
\r
53 self._perspectives = {} # {name: perspective_str}
\r
56 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
\r
58 # setup frame manager
\r
59 self._c['manager'] = aui.AuiManager()
\r
60 self._c['manager'].SetManagedWindow(self)
\r
62 # set the gradient and drag styles
\r
63 self._c['manager'].GetArtProvider().SetMetric(
\r
64 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
\r
65 self._c['manager'].SetFlags(
\r
66 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
\r
68 # Min size for the frame itself isn't completely done. See
\r
69 # the end of FrameManager::Update() for the test code. For
\r
70 # now, just hard code a frame minimum size.
\r
71 self.SetMinSize(wx.Size(500, 500))
\r
73 self._setup_panels()
\r
74 self._setup_toolbars()
\r
75 self._c['manager'].Update() # commit pending changes
\r
77 # Create the menubar after the panes so that the default
\r
78 # perspective is created with all panes open
\r
79 self._c['menu bar'] = menu.HookeMenuBar(
\r
82 'close': self._on_close,
\r
83 'about': self._on_about,
\r
84 'view_panel': self._on_panel_visibility,
\r
85 'save_perspective': self._on_save_perspective,
\r
86 'delete_perspective': self._on_delete_perspective,
\r
87 'select_perspective': self._on_select_perspective,
\r
89 self.SetMenuBar(self._c['menu bar'])
\r
91 self._c['status bar'] = statusbar.StatusBar(
\r
93 style=wx.ST_SIZEGRIP)
\r
94 self.SetStatusBar(self._c['status bar'])
\r
96 self._setup_perspectives()
\r
99 name = self.gui.config['active perspective']
\r
100 return # TODO: cleanup
\r
101 self.playlists = self._c['playlists'].Playlists
\r
102 self._displayed_plot = None
\r
103 #load default list, if possible
\r
104 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist'))
\r
109 def _setup_panels(self):
\r
110 client_size = self.GetClientSize()
\r
111 for label,p,style in [
\r
112 # ('folders', wx.GenericDirCtrl(
\r
114 # dir=self.gui.config['folders-workdir'],
\r
116 # style=wx.DIRCTRL_SHOW_FILTERS,
\r
117 # filter=self.gui.config['folders-filters'],
\r
118 # defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert
\r
119 ('playlists', panel.PANELS['playlist'](
\r
121 'delete_playlist':self._on_user_delete_playlist,
\r
122 '_delete_playlist':self._on_delete_playlist,
\r
123 'delete_curve':self._on_user_delete_curve,
\r
124 '_delete_curve':self._on_delete_curve,
\r
125 '_on_set_selected_playlist':self._on_set_selected_playlist,
\r
126 '_on_set_selected_curve':self._on_set_selected_curve,
\r
128 config=self.gui.config,
\r
130 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
131 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
134 # ('note', panel.note.Note(
\r
136 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
137 # size=(160, 200)), 'left'),
\r
138 # ('notebook', Notebook(
\r
140 # pos=wx.Point(client_size.x, client_size.y),
\r
141 # size=wx.Size(430, 200),
\r
142 # style=aui.AUI_NB_DEFAULT_STYLE
\r
143 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
144 ('commands', panel.PANELS['commands'](
\r
145 commands=self.commands,
\r
146 selected=self.gui.config['selected command'],
\r
148 'execute': self.execute_command,
\r
149 'select_plugin': self.select_plugin,
\r
150 'select_command': self.select_command,
\r
151 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
154 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
155 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
158 ('property', panel.PANELS['propertyeditor2'](
\r
161 style=wx.WANTS_CHARS,
\r
162 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
164 # ('assistant', wx.TextCtrl(
\r
166 # pos=wx.Point(0, 0),
\r
167 # size=wx.Size(150, 90),
\r
168 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
169 ('output', panel.PANELS['output'](
\r
171 pos=wx.Point(0, 0),
\r
172 size=wx.Size(150, 90),
\r
173 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
175 # ('results', panel.results.Results(self), 'bottom'),
\r
177 self._add_panel(label, p, style)
\r
178 #self._c['assistant'].SetEditable(False)
\r
180 def _add_panel(self, label, panel, style):
\r
181 self._c[label] = panel
\r
182 cap_label = label.capitalize()
\r
183 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
184 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
187 elif style == 'center':
\r
189 elif style == 'left':
\r
191 elif style == 'right':
\r
194 assert style == 'bottom', style
\r
196 self._c['manager'].AddPane(panel, info)
\r
198 def _setup_toolbars(self):
\r
199 self._c['navigation bar'] = navbar.NavBar(
\r
201 'next': self._next_curve,
\r
202 'previous': self._previous_curve,
\r
205 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
206 self._c['manager'].AddPane(
\r
207 self._c['navigation bar'],
\r
208 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
209 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
210 ).RightDockable(False))
\r
212 def _bind_events(self):
\r
213 # TODO: figure out if we can use the eventManager for menu
\r
214 # ranges and events of 'self' without raising an assertion
\r
216 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
217 self.Bind(wx.EVT_SIZE, self._on_size)
\r
218 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
219 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
220 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
222 return # TODO: cleanup
\r
223 for value in self._c['menu bar']._c['view']._c.values():
\r
224 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
226 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
227 self._c['menu bar']._c['perspective']._c['save'])
\r
228 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
229 self._c['menu bar']._c['perspective']._c['delete'])
\r
231 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
232 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
234 # TODO: playlist callbacks
\r
235 return # TODO: cleanup
\r
236 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
238 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
240 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
242 def _on_about(self, *args):
\r
243 dialog = wx.MessageDialog(
\r
245 message=self.gui._splash_text(),
\r
246 caption='About Hooke',
\r
247 style=wx.OK|wx.ICON_INFORMATION)
\r
251 def _on_close(self, *args):
\r
253 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
254 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
255 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
256 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
257 # push changes back to Hooke.config?
\r
258 self._c['manager'].UnInit()
\r
259 del self._c['manager']
\r
266 def _command_by_name(self, name):
\r
267 cs = [c for c in self.commands if c.name == name]
\r
269 raise KeyError(name)
\r
271 raise Exception('Multiple commands named "%s"' % name)
\r
274 def execute_command(self, _class=None, method=None,
\r
275 command=None, args=None):
\r
278 if ('property' in self._c
\r
279 and self.gui.config['selected command'] == command):
\r
280 arg_names = [arg.name for arg in command.arguments]
\r
281 for name,value in self._c['property'].get_values().items():
\r
282 if name in arg_names:
\r
284 print 'executing', command.name, args
\r
285 self.inqueue.put(CommandMessage(command, args))
\r
288 msg = self.outqueue.get()
\r
289 results.append(msg)
\r
290 if isinstance(msg, Exit):
\r
293 elif isinstance(msg, CommandExit):
\r
294 # TODO: display command complete
\r
296 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
297 self.gui.reload_config(msg.config)
\r
299 elif isinstance(msg, Request):
\r
300 h = handler.HANDLERS[msg.type]
\r
301 h.run(self, msg) # TODO: pause for response?
\r
304 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
305 self._postprocess_text)
\r
306 pp(command=command, results=results)
\r
309 def _handle_request(self, msg):
\r
310 """Repeatedly try to get a response to `msg`.
\r
313 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
314 prompt_string = prompt(msg)
\r
315 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
317 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
321 self.cmd.stdout.write(''.join([
\r
322 error.__class__.__name__, ': ', str(error), '\n']))
\r
323 self.cmd.stdout.write(prompt_string)
\r
324 value = parser(msg, self.cmd.stdin.readline())
\r
326 response = msg.response(value)
\r
328 except ValueError, error:
\r
330 self.inqueue.put(response)
\r
334 # Command-specific postprocessing
\r
336 def _postprocess_text(self, command, results):
\r
337 """Print the string representation of the results to the Results window.
\r
339 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
340 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
341 doesn't print some internally handled messages
\r
342 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
344 for result in results:
\r
345 if isinstance(result, CommandExit):
\r
346 self._c['output'].write(result.__class__.__name__+'\n')
\r
347 self._c['output'].write(str(result).rstrip()+'\n')
\r
349 def _postprocess_load_playlist(self, command, results):
\r
350 """Update `self` to show the playlist.
\r
352 if not isinstance(results[-1], Success):
\r
353 self._postprocess_text(command, results)
\r
354 assert len(results) == 2, results
\r
355 playlist = results[0]
\r
357 self._c['playlists']._c['tree'].add_playlist(playlist)
\r
359 def _postprocess_get_playlist(self, command, results):
\r
360 if not isinstance(results[-1], Success):
\r
361 self._postprocess_text(command, results)
\r
362 assert len(results) == 2, results
\r
363 playlist = results[0]
\r
365 self._c['playlists']._c['tree'].update_playlist(playlist)
\r
367 def _postprocess_get_curve(self, command, results):
\r
368 """Update `self` to show the curve.
\r
370 if not isinstance(results[-1], Success):
\r
371 self._postprocess_text(command, results)
\r
372 assert len(results) == 2, results
\r
374 playlist = self._c['playlists']._c['tree'].get_selected_playlist()
\r
375 if playlist != None: # TODO: fix once we have hooke.plugin.playlists
\r
376 self._c['playlists']._c['tree'].set_selected_curve(
\r
379 def _postprocess_next_curve(self, command, results):
\r
380 """No-op. Only call 'next curve' via `self._next_curve()`.
\r
384 def _postprocess_previous_curve(self, command, results):
\r
385 """No-op. Only call 'previous curve' via `self._previous_curve()`.
\r
392 def _GetActiveFileIndex(self):
\r
393 lib.playlist.Playlist = self.GetActivePlaylist()
\r
394 #get the selected item from the tree
\r
395 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
396 #test if a playlist or a curve was double-clicked
\r
397 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
401 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
402 while selected_item.IsOk():
\r
404 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
407 def _GetPlaylistTab(self, name):
\r
408 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
409 if page.caption == name:
\r
413 def select_plugin(self, _class=None, method=None, plugin=None):
\r
416 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
418 playlist = lib.playlist.Playlist(self, self.drivers)
\r
420 playlist.add_curve(item)
\r
421 if playlist.count > 0:
\r
422 playlist.name = self._GetUniquePlaylistName(name)
\r
424 self.AddTayliss(playlist)
\r
426 def AppliesPlotmanipulator(self, name):
\r
428 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
429 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
431 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
433 def ApplyPlotmanipulators(self, plot, plot_file):
\r
435 Apply all active plotmanipulators.
\r
437 if plot is not None and plot_file is not None:
\r
438 manipulated_plot = copy.deepcopy(plot)
\r
439 for plotmanipulator in self.plotmanipulators:
\r
440 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
441 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
442 return manipulated_plot
\r
444 def GetActiveFigure(self):
\r
445 playlist_name = self.GetActivePlaylistName()
\r
446 figure = self.playlists[playlist_name].figure
\r
447 if figure is not None:
\r
451 def GetActiveFile(self):
\r
452 playlist = self.GetActivePlaylist()
\r
453 if playlist is not None:
\r
454 return playlist.get_active_file()
\r
457 def GetActivePlot(self):
\r
458 playlist = self.GetActivePlaylist()
\r
459 if playlist is not None:
\r
460 return playlist.get_active_file().plot
\r
463 def GetDisplayedPlot(self):
\r
464 plot = copy.deepcopy(self.displayed_plot)
\r
466 #plot.curves = copy.deepcopy(plot.curves)
\r
469 def GetDisplayedPlotCorrected(self):
\r
470 plot = copy.deepcopy(self.displayed_plot)
\r
472 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
475 def GetDisplayedPlotRaw(self):
\r
476 plot = copy.deepcopy(self.displayed_plot)
\r
478 plot.curves = copy.deepcopy(plot.raw_curves)
\r
481 def GetDockArt(self):
\r
482 return self._c['manager'].GetArtProvider()
\r
484 def GetPlotmanipulator(self, name):
\r
486 Returns a plot manipulator function from its name
\r
488 for plotmanipulator in self.plotmanipulators:
\r
489 if plotmanipulator.name == name:
\r
490 return plotmanipulator
\r
493 def HasPlotmanipulator(self, name):
\r
495 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
497 for plotmanipulator in self.plotmanipulators:
\r
498 if plotmanipulator.command == name:
\r
503 def _on_dir_ctrl_left_double_click(self, event):
\r
504 file_path = self.panelFolders.GetPath()
\r
505 if os.path.isfile(file_path):
\r
506 if file_path.endswith('.hkp'):
\r
507 self.do_loadlist(file_path)
\r
510 def _on_erase_background(self, event):
\r
513 def _on_notebook_page_close(self, event):
\r
514 ctrl = event.GetEventObject()
\r
515 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
516 self.DeleteFromPlaylists(playlist_name)
\r
518 def OnPaneClose(self, event):
\r
521 def OnPropGridChanged (self, event):
\r
522 prop = event.GetProperty()
\r
524 item_section = self.panelProperties.SelectedTreeItem
\r
525 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
526 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
527 config = self.gui.config[plugin]
\r
528 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
529 property_key = prop.GetName()
\r
530 property_value = prop.GetDisplayedString()
\r
532 config[property_section][property_key]['value'] = property_value
\r
534 def OnResultsCheck(self, index, flag):
\r
535 results = self.GetActivePlot().results
\r
536 if results.has_key(self.results_str):
\r
537 results[self.results_str].results[index].visible = flag
\r
538 results[self.results_str].update()
\r
542 def _on_size(self, event):
\r
545 def OnUpdateNote(self, event):
\r
547 Saves the note to the active file.
\r
549 active_file = self.GetActiveFile()
\r
550 active_file.note = self.panelNote.Editor.GetValue()
\r
552 def UpdateNote(self):
\r
553 #update the note for the active file
\r
554 active_file = self.GetActiveFile()
\r
555 if active_file is not None:
\r
556 self.panelNote.Editor.SetValue(active_file.note)
\r
558 def UpdatePlaylistsTreeSelection(self):
\r
559 playlist = self.GetActivePlaylist()
\r
560 if playlist is not None:
\r
561 if playlist.index >= 0:
\r
562 self._c['status bar'].set_playlist(playlist)
\r
566 def UpdatePlot(self, plot=None):
\r
568 def add_to_plot(curve, set_scale=True):
\r
569 if curve.visible and curve.x and curve.y:
\r
570 #get the index of the subplot to use as destination
\r
571 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
572 #set all parameters for the plot
\r
573 axes_list[destination].set_title(curve.title)
\r
575 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
576 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
577 #set the formatting details for the scale
\r
578 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
579 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
580 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
581 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
582 if curve.style == 'plot':
\r
583 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
584 if curve.style == 'scatter':
\r
585 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
586 #add the legend if necessary
\r
588 axes_list[destination].legend()
\r
591 active_file = self.GetActiveFile()
\r
592 if not active_file.driver:
\r
593 #the first time we identify a file, the following need to be set
\r
594 active_file.identify(self.drivers)
\r
595 for curve in active_file.plot.curves:
\r
596 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
597 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
598 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
599 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
600 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
601 if active_file.driver is None:
\r
602 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
604 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
605 #add raw curves to plot
\r
606 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
607 #apply all active plotmanipulators
\r
608 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
609 #add corrected curves to plot
\r
610 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
613 self.displayed_plot = copy.deepcopy(plot)
\r
615 figure = self.GetActiveFigure()
\r
618 #use '0' instead of e.g. '0.00' for scales
\r
619 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
620 #optionally remove the extension from the title of the plot
\r
621 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
622 if hide_curve_extension:
\r
623 title = lh.remove_extension(self.displayed_plot.title)
\r
625 title = self.displayed_plot.title
\r
626 figure.suptitle(title, fontsize=14)
\r
627 #create the list of all axes necessary (rows and columns)
\r
629 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
630 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
631 for index in range(number_of_rows * number_of_columns):
\r
632 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
634 #add all curves to the corresponding plots
\r
635 for curve in self.displayed_plot.curves:
\r
638 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
639 figure.subplots_adjust(hspace=0.3)
\r
642 self.panelResults.ClearResults()
\r
643 if self.displayed_plot.results.has_key(self.results_str):
\r
644 for curve in self.displayed_plot.results[self.results_str].results:
\r
645 add_to_plot(curve, set_scale=False)
\r
646 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
648 self.panelResults.ClearResults()
\r
650 figure.canvas.draw()
\r
652 def _on_curve_select(self, playlist, curve):
\r
653 #create the plot tab and add playlist to the dictionary
\r
654 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
655 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
656 #tab_index = self._c['notebook'].GetSelection()
\r
657 playlist.figure = plotPanel.get_figure()
\r
658 self.playlists[playlist.name] = playlist
\r
659 #self.playlists[playlist.name] = [playlist, figure]
\r
660 self._c['status bar'].set_playlist(playlist)
\r
665 def _on_playlist_left_doubleclick(self):
\r
666 index = self._c['notebook'].GetSelection()
\r
667 current_playlist = self._c['notebook'].GetPageText(index)
\r
668 if current_playlist != playlist_name:
\r
669 index = self._GetPlaylistTab(playlist_name)
\r
670 self._c['notebook'].SetSelection(index)
\r
671 self._c['status bar'].set_playlist(playlist)
\r
675 def _on_playlist_delete(self, playlist):
\r
676 notebook = self.Parent.plotNotebook
\r
677 index = self.Parent._GetPlaylistTab(playlist.name)
\r
678 notebook.SetSelection(index)
\r
679 notebook.DeletePage(notebook.GetSelection())
\r
680 self.Parent.DeleteFromPlaylists(playlist_name)
\r
684 # Command panel interface
\r
686 def select_command(self, _class, method, command):
\r
687 #self.select_plugin(plugin=command.plugin)
\r
688 if 'assistant' in self._c:
\r
689 self._c['assitant'].ChangeValue(command.help)
\r
690 self._c['property'].clear()
\r
691 for argument in command.arguments:
\r
692 if argument.name == 'help':
\r
694 p = prop_from_argument(
\r
695 argument, curves=[], playlists=[]) # TODO: lookup playlists/curves
\r
697 continue # property intentionally not handled (yet)
\r
698 self._c['property'].append_property(p)
\r
700 self.gui.config['selected command'] = command # TODO: push to engine
\r
704 # Playlist panel interface
\r
706 def _on_user_delete_playlist(self, _class, method, playlist):
\r
709 def _on_delete_playlist(self, _class, method, playlist):
\r
710 if hasattr(playlist, 'path') and playlist.path != None:
\r
711 os.remove(playlist.path)
\r
713 def _on_user_delete_curve(self, _class, method, playlist, curve):
\r
716 def _on_delete_curve(self, _class, method, playlist, curve):
\r
717 os.remove(curve.path)
\r
719 def _on_set_selected_playlist(self, _class, method, playlist):
\r
720 """TODO: playlists plugin with `jump to playlist`.
\r
724 def _on_set_selected_curve(self, _class, method, playlist, curve):
\r
725 """Call the `jump to curve` command.
\r
727 TODO: playlists plugin.
\r
729 # TODO: jump to playlist, get playlist
\r
730 index = playlist.index(curve)
\r
731 results = self.execute_command(
\r
732 command=self._command_by_name('jump to curve'),
\r
733 args={'index':index})
\r
734 if not isinstance(results[-1], Success):
\r
736 #results = self.execute_command(
\r
737 # command=self._command_by_name('get playlist'))
\r
738 #if not isinstance(results[-1], Success):
\r
740 self.execute_command(
\r
741 command=self._command_by_name('get curve'))
\r
747 def _next_curve(self, *args):
\r
748 """Call the `next curve` command.
\r
750 results = self.execute_command(
\r
751 command=self._command_by_name('next curve'))
\r
752 if isinstance(results[-1], Success):
\r
753 self.execute_command(
\r
754 command=self._command_by_name('get curve'))
\r
756 def _previous_curve(self, *args):
\r
757 """Call the `previous curve` command.
\r
759 results = self.execute_command(
\r
760 command=self._command_by_name('previous curve'))
\r
761 if isinstance(results[-1], Success):
\r
762 self.execute_command(
\r
763 command=self._command_by_name('get curve'))
\r
767 # Panel display handling
\r
769 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
770 pane = self._c['manager'].GetPane(panel_name)
\r
773 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
774 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
775 #folders_size = pane.GetSize()
\r
776 self.panelFolders.Fit()
\r
777 self._c['manager'].Update()
\r
779 def _setup_perspectives(self):
\r
780 """Add perspectives to menubar and _perspectives.
\r
782 self._perspectives = {
\r
783 'Default': self._c['manager'].SavePerspective(),
\r
785 path = self.gui.config['perspective path']
\r
786 if os.path.isdir(path):
\r
787 files = sorted(os.listdir(path))
\r
788 for fname in files:
\r
789 name, extension = os.path.splitext(fname)
\r
790 if extension != self.gui.config['perspective extension']:
\r
792 fpath = os.path.join(path, fname)
\r
793 if not os.path.isfile(fpath):
\r
796 with open(fpath, 'rU') as f:
\r
797 perspective = f.readline()
\r
799 self._perspectives[name] = perspective
\r
801 selected_perspective = self.gui.config['active perspective']
\r
802 if not self._perspectives.has_key(selected_perspective):
\r
803 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
805 self._restore_perspective(selected_perspective)
\r
806 self._update_perspective_menu()
\r
808 def _update_perspective_menu(self):
\r
809 self._c['menu bar']._c['perspective'].update(
\r
810 sorted(self._perspectives.keys()),
\r
811 self.gui.config['active perspective'])
\r
813 def _save_perspective(self, perspective, perspective_dir, name,
\r
815 path = os.path.join(perspective_dir, name)
\r
816 if extension != None:
\r
818 if not os.path.isdir(perspective_dir):
\r
819 os.makedirs(perspective_dir)
\r
820 with open(path, 'w') as f:
\r
821 f.write(perspective)
\r
822 self._perspectives[name] = perspective
\r
823 self._restore_perspective(name)
\r
824 self._update_perspective_menu()
\r
826 def _delete_perspectives(self, perspective_dir, names,
\r
830 path = os.path.join(perspective_dir, name)
\r
831 if extension != None:
\r
834 del(self._perspectives[name])
\r
835 self._update_perspective_menu()
\r
836 if self.gui.config['active perspective'] in names:
\r
837 self._restore_perspective('Default')
\r
838 # TODO: does this bug still apply?
\r
839 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
840 # http://trac.wxwidgets.org/ticket/3258
\r
841 # ) that makes the radio item indicator in the menu disappear.
\r
842 # The code should be fine once this issue is fixed.
\r
844 def _restore_perspective(self, name):
\r
845 if name != self.gui.config['active perspective']:
\r
846 print 'restoring perspective:', name
\r
847 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
848 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
849 self._c['manager'].Update()
\r
850 for pane in self._c['manager'].GetAllPanes():
\r
851 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
852 pane.Check(pane.window.IsShown())
\r
854 def _on_save_perspective(self, *args):
\r
855 perspective = self._c['manager'].SavePerspective()
\r
856 name = self.gui.config['active perspective']
\r
857 if name == 'Default':
\r
858 name = 'New perspective'
\r
859 name = select_save_file(
\r
860 directory=self.gui.config['perspective path'],
\r
862 extension=self.gui.config['perspective extension'],
\r
864 message='Enter a name for the new perspective:',
\r
865 caption='Save perspective')
\r
868 self._save_perspective(
\r
869 perspective, self.gui.config['perspective path'], name=name,
\r
870 extension=self.gui.config['perspective extension'])
\r
872 def _on_delete_perspective(self, *args, **kwargs):
\r
873 options = sorted([p for p in self._perspectives.keys()
\r
874 if p != 'Default'])
\r
875 dialog = SelectionDialog(
\r
877 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
878 button_id=wx.ID_DELETE,
\r
879 selection_style='multiple',
\r
881 title='Delete perspective(s)',
\r
882 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
883 dialog.CenterOnScreen()
\r
885 names = [options[i] for i in dialog.selected]
\r
887 self._delete_perspectives(
\r
888 self.gui.config['perspective path'], names=names,
\r
889 extension=self.gui.config['perspective extension'])
\r
891 def _on_select_perspective(self, _class, method, name):
\r
892 self._restore_perspective(name)
\r
896 class HookeApp (wx.App):
\r
897 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
899 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
902 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
904 self.commands = commands
\r
905 self.inqueue = inqueue
\r
906 self.outqueue = outqueue
\r
907 super(HookeApp, self).__init__(*args, **kwargs)
\r
910 self.SetAppName('Hooke')
\r
911 self.SetVendorName('')
\r
912 self._setup_splash_screen()
\r
914 height = int(self.gui.config['main height']) # HACK: config should convert
\r
915 width = int(self.gui.config['main width'])
\r
916 top = int(self.gui.config['main top'])
\r
917 left = int(self.gui.config['main left'])
\r
919 # Sometimes, the ini file gets confused and sets 'left' and
\r
920 # 'top' to large negative numbers. Here we catch and fix
\r
921 # this. Keep small negative numbers, the user might want
\r
929 'frame': HookeFrame(
\r
930 self.gui, self.commands, self.inqueue, self.outqueue,
\r
931 parent=None, title='Hooke',
\r
932 pos=(left, top), size=(width, height),
\r
933 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
935 self._c['frame'].Show(True)
\r
936 self.SetTopWindow(self._c['frame'])
\r
939 def _setup_splash_screen(self):
\r
940 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
941 print 'splash', self.gui.config['show splash screen']
\r
942 path = self.gui.config['splash screen image']
\r
943 if os.path.isfile(path):
\r
944 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
946 bitmap=wx.Image(path).ConvertToBitmap(),
\r
947 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
948 milliseconds=duration,
\r
951 # For some reason splashDuration and sleep do not
\r
952 # correspond to each other at least not on Windows.
\r
953 # Maybe it's because duration is in milliseconds and
\r
954 # sleep in seconds. Thus we need to increase the
\r
955 # sleep time a bit. A factor of 1.2 seems to work.
\r
957 time.sleep(sleepFactor * duration / 1000)
\r
960 class GUI (UserInterface):
\r
961 """wxWindows graphical user interface.
\r
963 def __init__(self):
\r
964 super(GUI, self).__init__(name='gui')
\r
966 def default_settings(self):
\r
967 """Return a list of :class:`hooke.config.Setting`\s for any
\r
968 configurable UI settings.
\r
970 The suggested section setting is::
\r
972 Setting(section=self.setting_section, help=self.__doc__)
\r
975 Setting(section=self.setting_section, help=self.__doc__),
\r
976 Setting(section=self.setting_section, option='icon image',
\r
977 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
978 help='Path to the hooke icon image.'),
\r
979 Setting(section=self.setting_section, option='show splash screen',
\r
981 help='Enable/disable the splash screen'),
\r
982 Setting(section=self.setting_section, option='splash screen image',
\r
983 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
984 help='Path to the Hooke splash screen image.'),
\r
985 Setting(section=self.setting_section, option='splash screen duration',
\r
987 help='Duration of the splash screen in milliseconds.'),
\r
988 Setting(section=self.setting_section, option='perspective path',
\r
989 value=os.path.join('resources', 'gui', 'perspective'),
\r
990 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
991 Setting(section=self.setting_section, option='perspective extension',
\r
993 help='Extension for perspective files.'),
\r
994 Setting(section=self.setting_section, option='hide extensions',
\r
996 help='Hide file extensions when displaying names.'),
\r
997 Setting(section=self.setting_section, option='folders-workdir',
\r
999 help='This should probably go...'),
\r
1000 Setting(section=self.setting_section, option='folders-filters',
\r
1002 help='This should probably go...'),
\r
1003 Setting(section=self.setting_section, option='active perspective',
\r
1005 help='Name of active perspective file (or "Default").'),
\r
1006 Setting(section=self.setting_section, option='folders-filter-index',
\r
1008 help='This should probably go...'),
\r
1009 Setting(section=self.setting_section, option='main height',
\r
1011 help='Height of main window in pixels.'),
\r
1012 Setting(section=self.setting_section, option='main width',
\r
1014 help='Width of main window in pixels.'),
\r
1015 Setting(section=self.setting_section, option='main top',
\r
1017 help='Pixels from screen top to top of main window.'),
\r
1018 Setting(section=self.setting_section, option='main left',
\r
1020 help='Pixels from screen left to left of main window.'),
\r
1021 Setting(section=self.setting_section, option='selected command',
\r
1022 value='load playlist',
\r
1023 help='Name of the initially selected command.'),
\r
1026 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1030 app = HookeApp(gui=self,
\r
1031 commands=commands,
\r
1032 inqueue=ui_to_command_queue,
\r
1033 outqueue=command_to_ui_queue,
\r
1034 redirect=redirect)
\r
1037 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1038 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r