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
126 config=self.gui.config,
\r
128 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
129 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
132 # ('note', panel.note.Note(
\r
134 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
135 # size=(160, 200)), 'left'),
\r
136 # ('notebook', Notebook(
\r
138 # pos=wx.Point(client_size.x, client_size.y),
\r
139 # size=wx.Size(430, 200),
\r
140 # style=aui.AUI_NB_DEFAULT_STYLE
\r
141 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
142 ('commands', panel.PANELS['commands'](
\r
143 commands=self.commands,
\r
144 selected=self.gui.config['selected command'],
\r
146 'execute': self.execute_command,
\r
147 'select_plugin': self.select_plugin,
\r
148 'select_command': self.select_command,
\r
149 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
152 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
153 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
156 ('property', panel.PANELS['propertyeditor2'](
\r
159 style=wx.WANTS_CHARS,
\r
160 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
162 # ('assistant', wx.TextCtrl(
\r
164 # pos=wx.Point(0, 0),
\r
165 # size=wx.Size(150, 90),
\r
166 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
167 ('output', panel.PANELS['output'](
\r
169 pos=wx.Point(0, 0),
\r
170 size=wx.Size(150, 90),
\r
171 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
173 # ('results', panel.results.Results(self), 'bottom'),
\r
175 self._add_panel(label, p, style)
\r
176 #self._c['assistant'].SetEditable(False)
\r
178 def _add_panel(self, label, panel, style):
\r
179 self._c[label] = panel
\r
180 cap_label = label.capitalize()
\r
181 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
182 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
185 elif style == 'center':
\r
187 elif style == 'left':
\r
189 elif style == 'right':
\r
192 assert style == 'bottom', style
\r
194 self._c['manager'].AddPane(panel, info)
\r
196 def _setup_toolbars(self):
\r
197 self._c['navigation bar'] = navbar.NavBar(
\r
199 'next': self._next_curve,
\r
200 'previous': self._previous_curve,
\r
203 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
204 self._c['manager'].AddPane(
\r
205 self._c['navigation bar'],
\r
206 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
207 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
208 ).RightDockable(False))
\r
210 def _bind_events(self):
\r
211 # TODO: figure out if we can use the eventManager for menu
\r
212 # ranges and events of 'self' without raising an assertion
\r
214 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
215 self.Bind(wx.EVT_SIZE, self._on_size)
\r
216 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
217 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
218 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
220 return # TODO: cleanup
\r
221 for value in self._c['menu bar']._c['view']._c.values():
\r
222 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
224 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
225 self._c['menu bar']._c['perspective']._c['save'])
\r
226 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
227 self._c['menu bar']._c['perspective']._c['delete'])
\r
229 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
230 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
232 # TODO: playlist callbacks
\r
233 return # TODO: cleanup
\r
234 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
236 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
238 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
240 def _on_about(self, *args):
\r
241 dialog = wx.MessageDialog(
\r
243 message=self.gui._splash_text(),
\r
244 caption='About Hooke',
\r
245 style=wx.OK|wx.ICON_INFORMATION)
\r
249 def _on_close(self, *args):
\r
251 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
252 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
253 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
254 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
255 # push changes back to Hooke.config?
\r
256 self._c['manager'].UnInit()
\r
257 del self._c['manager']
\r
264 def _command_by_name(self, name):
\r
265 cs = [c for c in self.commands if c.name == name]
\r
267 raise KeyError(name)
\r
269 raise Exception('Multiple commands named "%s"' % name)
\r
272 def execute_command(self, _class=None, method=None,
\r
273 command=None, args=None):
\r
276 if ('property' in self._c
\r
277 and self.gui.config['selected command'] == command):
\r
278 arg_names = [arg.name for arg in command.arguments]
\r
279 for name,value in self._c['property'].get_values().items():
\r
280 if name in arg_names:
\r
282 print 'executing', command.name, args
\r
283 self.inqueue.put(CommandMessage(command, args))
\r
286 msg = self.outqueue.get()
\r
287 results.append(msg)
\r
288 if isinstance(msg, Exit):
\r
291 elif isinstance(msg, CommandExit):
\r
292 # TODO: display command complete
\r
294 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
295 self.gui.reload_config(msg.config)
\r
297 elif isinstance(msg, Request):
\r
298 h = handler.HANDLERS[msg.type]
\r
299 h.run(self, msg) # TODO: pause for response?
\r
302 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
303 self._postprocess_text)
\r
304 pp(command=command, results=results)
\r
307 def _handle_request(self, msg):
\r
308 """Repeatedly try to get a response to `msg`.
\r
311 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
312 prompt_string = prompt(msg)
\r
313 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
315 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
319 self.cmd.stdout.write(''.join([
\r
320 error.__class__.__name__, ': ', str(error), '\n']))
\r
321 self.cmd.stdout.write(prompt_string)
\r
322 value = parser(msg, self.cmd.stdin.readline())
\r
324 response = msg.response(value)
\r
326 except ValueError, error:
\r
328 self.inqueue.put(response)
\r
332 # Command-specific postprocessing
\r
334 def _postprocess_text(self, command, results):
\r
335 """Print the string representation of the results to the Results window.
\r
337 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
338 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
339 doesn't print some internally handled messages
\r
340 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
342 for result in results:
\r
343 if isinstance(result, CommandExit):
\r
344 self._c['output'].write(result.__class__.__name__+'\n')
\r
345 self._c['output'].write(str(result).rstrip()+'\n')
\r
347 def _postprocess_text(self, command, results):
\r
348 """Print the string representation of the results to the Results window.
\r
350 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
351 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
352 doesn't print some internally handled messages
\r
353 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
355 for result in results:
\r
356 if isinstance(result, CommandExit):
\r
357 self._c['output'].write(result.__class__.__name__+'\n')
\r
358 self._c['output'].write(str(result).rstrip()+'\n')
\r
360 def _postprocess_load_playlist(self, command, results):
\r
361 """Update `self` to show the playlist.
\r
363 if not isinstance(results[-1], Success):
\r
364 return # error executing 'load playlist'
\r
365 assert len(results) == 2, results
\r
366 playlist = results[0]
\r
369 self._c['playlists']._c['tree'].add_playlist(playlist)
\r
374 def _GetActiveFileIndex(self):
\r
375 lib.playlist.Playlist = self.GetActivePlaylist()
\r
376 #get the selected item from the tree
\r
377 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
378 #test if a playlist or a curve was double-clicked
\r
379 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
383 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
384 while selected_item.IsOk():
\r
386 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
389 def _GetPlaylistTab(self, name):
\r
390 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
391 if page.caption == name:
\r
395 def select_plugin(self, _class=None, method=None, plugin=None):
\r
398 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
400 playlist = lib.playlist.Playlist(self, self.drivers)
\r
402 playlist.add_curve(item)
\r
403 if playlist.count > 0:
\r
404 playlist.name = self._GetUniquePlaylistName(name)
\r
406 self.AddTayliss(playlist)
\r
408 def AppliesPlotmanipulator(self, name):
\r
410 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
411 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
413 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
415 def ApplyPlotmanipulators(self, plot, plot_file):
\r
417 Apply all active plotmanipulators.
\r
419 if plot is not None and plot_file is not None:
\r
420 manipulated_plot = copy.deepcopy(plot)
\r
421 for plotmanipulator in self.plotmanipulators:
\r
422 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
423 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
424 return manipulated_plot
\r
426 def GetActiveFigure(self):
\r
427 playlist_name = self.GetActivePlaylistName()
\r
428 figure = self.playlists[playlist_name].figure
\r
429 if figure is not None:
\r
433 def GetActiveFile(self):
\r
434 playlist = self.GetActivePlaylist()
\r
435 if playlist is not None:
\r
436 return playlist.get_active_file()
\r
439 def GetActivePlot(self):
\r
440 playlist = self.GetActivePlaylist()
\r
441 if playlist is not None:
\r
442 return playlist.get_active_file().plot
\r
445 def GetDisplayedPlot(self):
\r
446 plot = copy.deepcopy(self.displayed_plot)
\r
448 #plot.curves = copy.deepcopy(plot.curves)
\r
451 def GetDisplayedPlotCorrected(self):
\r
452 plot = copy.deepcopy(self.displayed_plot)
\r
454 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
457 def GetDisplayedPlotRaw(self):
\r
458 plot = copy.deepcopy(self.displayed_plot)
\r
460 plot.curves = copy.deepcopy(plot.raw_curves)
\r
463 def GetDockArt(self):
\r
464 return self._c['manager'].GetArtProvider()
\r
466 def GetPlotmanipulator(self, name):
\r
468 Returns a plot manipulator function from its name
\r
470 for plotmanipulator in self.plotmanipulators:
\r
471 if plotmanipulator.name == name:
\r
472 return plotmanipulator
\r
475 def HasPlotmanipulator(self, name):
\r
477 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
479 for plotmanipulator in self.plotmanipulators:
\r
480 if plotmanipulator.command == name:
\r
485 def _on_dir_ctrl_left_double_click(self, event):
\r
486 file_path = self.panelFolders.GetPath()
\r
487 if os.path.isfile(file_path):
\r
488 if file_path.endswith('.hkp'):
\r
489 self.do_loadlist(file_path)
\r
492 def _on_erase_background(self, event):
\r
495 def _on_notebook_page_close(self, event):
\r
496 ctrl = event.GetEventObject()
\r
497 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
498 self.DeleteFromPlaylists(playlist_name)
\r
500 def OnPaneClose(self, event):
\r
503 def OnPropGridChanged (self, event):
\r
504 prop = event.GetProperty()
\r
506 item_section = self.panelProperties.SelectedTreeItem
\r
507 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
508 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
509 config = self.gui.config[plugin]
\r
510 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
511 property_key = prop.GetName()
\r
512 property_value = prop.GetDisplayedString()
\r
514 config[property_section][property_key]['value'] = property_value
\r
516 def OnResultsCheck(self, index, flag):
\r
517 results = self.GetActivePlot().results
\r
518 if results.has_key(self.results_str):
\r
519 results[self.results_str].results[index].visible = flag
\r
520 results[self.results_str].update()
\r
524 def _on_size(self, event):
\r
527 def OnUpdateNote(self, event):
\r
529 Saves the note to the active file.
\r
531 active_file = self.GetActiveFile()
\r
532 active_file.note = self.panelNote.Editor.GetValue()
\r
534 def UpdateNote(self):
\r
535 #update the note for the active file
\r
536 active_file = self.GetActiveFile()
\r
537 if active_file is not None:
\r
538 self.panelNote.Editor.SetValue(active_file.note)
\r
540 def UpdatePlaylistsTreeSelection(self):
\r
541 playlist = self.GetActivePlaylist()
\r
542 if playlist is not None:
\r
543 if playlist.index >= 0:
\r
544 self._c['status bar'].set_playlist(playlist)
\r
548 def UpdatePlot(self, plot=None):
\r
550 def add_to_plot(curve, set_scale=True):
\r
551 if curve.visible and curve.x and curve.y:
\r
552 #get the index of the subplot to use as destination
\r
553 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
554 #set all parameters for the plot
\r
555 axes_list[destination].set_title(curve.title)
\r
557 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
558 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
559 #set the formatting details for the scale
\r
560 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
561 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
562 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
563 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
564 if curve.style == 'plot':
\r
565 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
566 if curve.style == 'scatter':
\r
567 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
568 #add the legend if necessary
\r
570 axes_list[destination].legend()
\r
573 active_file = self.GetActiveFile()
\r
574 if not active_file.driver:
\r
575 #the first time we identify a file, the following need to be set
\r
576 active_file.identify(self.drivers)
\r
577 for curve in active_file.plot.curves:
\r
578 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
579 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
580 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
581 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
582 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
583 if active_file.driver is None:
\r
584 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
586 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
587 #add raw curves to plot
\r
588 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
589 #apply all active plotmanipulators
\r
590 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
591 #add corrected curves to plot
\r
592 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
595 self.displayed_plot = copy.deepcopy(plot)
\r
597 figure = self.GetActiveFigure()
\r
600 #use '0' instead of e.g. '0.00' for scales
\r
601 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
602 #optionally remove the extension from the title of the plot
\r
603 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
604 if hide_curve_extension:
\r
605 title = lh.remove_extension(self.displayed_plot.title)
\r
607 title = self.displayed_plot.title
\r
608 figure.suptitle(title, fontsize=14)
\r
609 #create the list of all axes necessary (rows and columns)
\r
611 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
612 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
613 for index in range(number_of_rows * number_of_columns):
\r
614 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
616 #add all curves to the corresponding plots
\r
617 for curve in self.displayed_plot.curves:
\r
620 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
621 figure.subplots_adjust(hspace=0.3)
\r
624 self.panelResults.ClearResults()
\r
625 if self.displayed_plot.results.has_key(self.results_str):
\r
626 for curve in self.displayed_plot.results[self.results_str].results:
\r
627 add_to_plot(curve, set_scale=False)
\r
628 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
630 self.panelResults.ClearResults()
\r
632 figure.canvas.draw()
\r
634 def _on_curve_select(self, playlist, curve):
\r
635 #create the plot tab and add playlist to the dictionary
\r
636 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
637 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
638 #tab_index = self._c['notebook'].GetSelection()
\r
639 playlist.figure = plotPanel.get_figure()
\r
640 self.playlists[playlist.name] = playlist
\r
641 #self.playlists[playlist.name] = [playlist, figure]
\r
642 self._c['status bar'].set_playlist(playlist)
\r
647 def _on_playlist_left_doubleclick(self):
\r
648 index = self._c['notebook'].GetSelection()
\r
649 current_playlist = self._c['notebook'].GetPageText(index)
\r
650 if current_playlist != playlist_name:
\r
651 index = self._GetPlaylistTab(playlist_name)
\r
652 self._c['notebook'].SetSelection(index)
\r
653 self._c['status bar'].set_playlist(playlist)
\r
657 def _on_playlist_delete(self, playlist):
\r
658 notebook = self.Parent.plotNotebook
\r
659 index = self.Parent._GetPlaylistTab(playlist.name)
\r
660 notebook.SetSelection(index)
\r
661 notebook.DeletePage(notebook.GetSelection())
\r
662 self.Parent.DeleteFromPlaylists(playlist_name)
\r
666 # Command panel interface
\r
668 def select_command(self, _class, method, command):
\r
669 #self.select_plugin(plugin=command.plugin)
\r
670 if 'assistant' in self._c:
\r
671 self._c['assitant'].ChangeValue(command.help)
\r
672 self._c['property'].clear()
\r
673 for argument in command.arguments:
\r
674 if argument.name == 'help':
\r
676 p = prop_from_argument(
\r
677 argument, curves=[], playlists=[]) # TODO: lookup playlists/curves
\r
679 continue # property intentionally not handled (yet)
\r
680 self._c['property'].append_property(p)
\r
682 self.gui.config['selected command'] = command # TODO: push to engine
\r
686 # Playlist panel interface
\r
688 def _on_user_delete_playlist(self, _class, method, playlist):
\r
691 def _on_delete_playlist(self, _class, method, playlist):
\r
692 if hasattr(playlist, 'path') and playlist.path != None:
\r
693 os.remove(playlist.path)
\r
695 def _on_user_delete_curve(self, _class, method, playlist, curve):
\r
698 def _on_delete_curve(self, _class, method, playlist, curve):
\r
699 os.remove(curve.path)
\r
705 def _next_curve(self, *args):
\r
706 """Call the `next curve` command.
\r
708 results = self.execute_command(
\r
709 command=self._command_by_name('next curve'))
\r
710 if isinstance(results[-1], Success):
\r
711 self.execute_command(
\r
712 command=self._command_by_name('get curve'))
\r
714 def _previous_curve(self, *args):
\r
715 """Call the `previous curve` command.
\r
717 self.execute_command(
\r
718 command=self._command_by_name('previous curve'))
\r
719 if isinstance(results[-1], Success):
\r
720 self.execute_command(
\r
721 command=self._command_by_name('get curve'))
\r
725 # Panel display handling
\r
727 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
728 pane = self._c['manager'].GetPane(panel_name)
\r
731 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
732 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
733 #folders_size = pane.GetSize()
\r
734 self.panelFolders.Fit()
\r
735 self._c['manager'].Update()
\r
737 def _setup_perspectives(self):
\r
738 """Add perspectives to menubar and _perspectives.
\r
740 self._perspectives = {
\r
741 'Default': self._c['manager'].SavePerspective(),
\r
743 path = self.gui.config['perspective path']
\r
744 if os.path.isdir(path):
\r
745 files = sorted(os.listdir(path))
\r
746 for fname in files:
\r
747 name, extension = os.path.splitext(fname)
\r
748 if extension != self.gui.config['perspective extension']:
\r
750 fpath = os.path.join(path, fname)
\r
751 if not os.path.isfile(fpath):
\r
754 with open(fpath, 'rU') as f:
\r
755 perspective = f.readline()
\r
757 self._perspectives[name] = perspective
\r
759 selected_perspective = self.gui.config['active perspective']
\r
760 if not self._perspectives.has_key(selected_perspective):
\r
761 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
763 self._restore_perspective(selected_perspective)
\r
764 self._update_perspective_menu()
\r
766 def _update_perspective_menu(self):
\r
767 self._c['menu bar']._c['perspective'].update(
\r
768 sorted(self._perspectives.keys()),
\r
769 self.gui.config['active perspective'])
\r
771 def _save_perspective(self, perspective, perspective_dir, name,
\r
773 path = os.path.join(perspective_dir, name)
\r
774 if extension != None:
\r
776 if not os.path.isdir(perspective_dir):
\r
777 os.makedirs(perspective_dir)
\r
778 with open(path, 'w') as f:
\r
779 f.write(perspective)
\r
780 self._perspectives[name] = perspective
\r
781 self._restore_perspective(name)
\r
782 self._update_perspective_menu()
\r
784 def _delete_perspectives(self, perspective_dir, names,
\r
788 path = os.path.join(perspective_dir, name)
\r
789 if extension != None:
\r
792 del(self._perspectives[name])
\r
793 self._update_perspective_menu()
\r
794 if self.gui.config['active perspective'] in names:
\r
795 self._restore_perspective('Default')
\r
796 # TODO: does this bug still apply?
\r
797 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
798 # http://trac.wxwidgets.org/ticket/3258
\r
799 # ) that makes the radio item indicator in the menu disappear.
\r
800 # The code should be fine once this issue is fixed.
\r
802 def _restore_perspective(self, name):
\r
803 if name != self.gui.config['active perspective']:
\r
804 print 'restoring perspective:', name
\r
805 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
806 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
807 self._c['manager'].Update()
\r
808 for pane in self._c['manager'].GetAllPanes():
\r
809 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
810 pane.Check(pane.window.IsShown())
\r
812 def _on_save_perspective(self, *args):
\r
813 perspective = self._c['manager'].SavePerspective()
\r
814 name = self.gui.config['active perspective']
\r
815 if name == 'Default':
\r
816 name = 'New perspective'
\r
817 name = select_save_file(
\r
818 directory=self.gui.config['perspective path'],
\r
820 extension=self.gui.config['perspective extension'],
\r
822 message='Enter a name for the new perspective:',
\r
823 caption='Save perspective')
\r
826 self._save_perspective(
\r
827 perspective, self.gui.config['perspective path'], name=name,
\r
828 extension=self.gui.config['perspective extension'])
\r
830 def _on_delete_perspective(self, *args, **kwargs):
\r
831 options = sorted([p for p in self._perspectives.keys()
\r
832 if p != 'Default'])
\r
833 dialog = SelectionDialog(
\r
835 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
836 button_id=wx.ID_DELETE,
\r
837 selection_style='multiple',
\r
839 title='Delete perspective(s)',
\r
840 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
841 dialog.CenterOnScreen()
\r
843 names = [options[i] for i in dialog.selected]
\r
845 self._delete_perspectives(
\r
846 self.gui.config['perspective path'], names=names,
\r
847 extension=self.gui.config['perspective extension'])
\r
849 def _on_select_perspective(self, _class, method, name):
\r
850 self._restore_perspective(name)
\r
854 class HookeApp (wx.App):
\r
855 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
857 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
860 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
862 self.commands = commands
\r
863 self.inqueue = inqueue
\r
864 self.outqueue = outqueue
\r
865 super(HookeApp, self).__init__(*args, **kwargs)
\r
868 self.SetAppName('Hooke')
\r
869 self.SetVendorName('')
\r
870 self._setup_splash_screen()
\r
872 height = int(self.gui.config['main height']) # HACK: config should convert
\r
873 width = int(self.gui.config['main width'])
\r
874 top = int(self.gui.config['main top'])
\r
875 left = int(self.gui.config['main left'])
\r
877 # Sometimes, the ini file gets confused and sets 'left' and
\r
878 # 'top' to large negative numbers. Here we catch and fix
\r
879 # this. Keep small negative numbers, the user might want
\r
887 'frame': HookeFrame(
\r
888 self.gui, self.commands, self.inqueue, self.outqueue,
\r
889 parent=None, title='Hooke',
\r
890 pos=(left, top), size=(width, height),
\r
891 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
893 self._c['frame'].Show(True)
\r
894 self.SetTopWindow(self._c['frame'])
\r
897 def _setup_splash_screen(self):
\r
898 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
899 print 'splash', self.gui.config['show splash screen']
\r
900 path = self.gui.config['splash screen image']
\r
901 if os.path.isfile(path):
\r
902 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
904 bitmap=wx.Image(path).ConvertToBitmap(),
\r
905 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
906 milliseconds=duration,
\r
909 # For some reason splashDuration and sleep do not
\r
910 # correspond to each other at least not on Windows.
\r
911 # Maybe it's because duration is in milliseconds and
\r
912 # sleep in seconds. Thus we need to increase the
\r
913 # sleep time a bit. A factor of 1.2 seems to work.
\r
915 time.sleep(sleepFactor * duration / 1000)
\r
918 class GUI (UserInterface):
\r
919 """wxWindows graphical user interface.
\r
921 def __init__(self):
\r
922 super(GUI, self).__init__(name='gui')
\r
924 def default_settings(self):
\r
925 """Return a list of :class:`hooke.config.Setting`\s for any
\r
926 configurable UI settings.
\r
928 The suggested section setting is::
\r
930 Setting(section=self.setting_section, help=self.__doc__)
\r
933 Setting(section=self.setting_section, help=self.__doc__),
\r
934 Setting(section=self.setting_section, option='icon image',
\r
935 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
936 help='Path to the hooke icon image.'),
\r
937 Setting(section=self.setting_section, option='show splash screen',
\r
939 help='Enable/disable the splash screen'),
\r
940 Setting(section=self.setting_section, option='splash screen image',
\r
941 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
942 help='Path to the Hooke splash screen image.'),
\r
943 Setting(section=self.setting_section, option='splash screen duration',
\r
945 help='Duration of the splash screen in milliseconds.'),
\r
946 Setting(section=self.setting_section, option='perspective path',
\r
947 value=os.path.join('resources', 'gui', 'perspective'),
\r
948 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
949 Setting(section=self.setting_section, option='perspective extension',
\r
951 help='Extension for perspective files.'),
\r
952 Setting(section=self.setting_section, option='hide extensions',
\r
954 help='Hide file extensions when displaying names.'),
\r
955 Setting(section=self.setting_section, option='folders-workdir',
\r
957 help='This should probably go...'),
\r
958 Setting(section=self.setting_section, option='folders-filters',
\r
960 help='This should probably go...'),
\r
961 Setting(section=self.setting_section, option='active perspective',
\r
963 help='Name of active perspective file (or "Default").'),
\r
964 Setting(section=self.setting_section, option='folders-filter-index',
\r
966 help='This should probably go...'),
\r
967 Setting(section=self.setting_section, option='main height',
\r
969 help='Height of main window in pixels.'),
\r
970 Setting(section=self.setting_section, option='main width',
\r
972 help='Width of main window in pixels.'),
\r
973 Setting(section=self.setting_section, option='main top',
\r
975 help='Pixels from screen top to top of main window.'),
\r
976 Setting(section=self.setting_section, option='main left',
\r
978 help='Pixels from screen left to left of main window.'),
\r
979 Setting(section=self.setting_section, option='selected command',
\r
980 value='load playlist',
\r
981 help='Name of the initially selected command.'),
\r
984 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
988 app = HookeApp(gui=self,
\r
990 inqueue=ui_to_command_queue,
\r
991 outqueue=command_to_ui_queue,
\r
995 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
996 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r