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 included in wxPython >= 2.9.1, until then, 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 . import prettyformat as prettyformat
\r
40 from . import statusbar as statusbar
\r
43 class HookeFrame (wx.Frame):
\r
44 """The main Hooke-interface window.
\r
48 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
49 super(HookeFrame, self).__init__(*args, **kwargs)
\r
51 self.commands = commands
\r
52 self.inqueue = inqueue
\r
53 self.outqueue = outqueue
\r
54 self._perspectives = {} # {name: perspective_str}
\r
57 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
\r
59 # setup frame manager
\r
60 self._c['manager'] = aui.AuiManager()
\r
61 self._c['manager'].SetManagedWindow(self)
\r
63 # set the gradient and drag styles
\r
64 self._c['manager'].GetArtProvider().SetMetric(
\r
65 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
\r
66 self._c['manager'].SetFlags(
\r
67 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
\r
69 # Min size for the frame itself isn't completely done. See
\r
70 # the end of FrameManager::Update() for the test code. For
\r
71 # now, just hard code a frame minimum size.
\r
72 self.SetMinSize(wx.Size(500, 500))
\r
74 self._setup_panels()
\r
75 self._setup_toolbars()
\r
76 self._c['manager'].Update() # commit pending changes
\r
78 # Create the menubar after the panes so that the default
\r
79 # perspective is created with all panes open
\r
80 self._c['menu bar'] = menu.HookeMenuBar(
\r
83 'close': self._on_close,
\r
84 'about': self._on_about,
\r
85 'view_panel': self._on_panel_visibility,
\r
86 'save_perspective': self._on_save_perspective,
\r
87 'delete_perspective': self._on_delete_perspective,
\r
88 'select_perspective': self._on_select_perspective,
\r
90 self.SetMenuBar(self._c['menu bar'])
\r
92 self._c['status bar'] = statusbar.StatusBar(
\r
94 style=wx.ST_SIZEGRIP)
\r
95 self.SetStatusBar(self._c['status bar'])
\r
97 self._setup_perspectives()
\r
100 name = self.gui.config['active perspective']
\r
101 return # TODO: cleanup
\r
102 self.playlists = self._c['playlists'].Playlists
\r
103 self._displayed_plot = None
\r
104 #load default list, if possible
\r
105 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist'))
\r
110 def _setup_panels(self):
\r
111 client_size = self.GetClientSize()
\r
112 for label,p,style in [
\r
113 # ('folders', wx.GenericDirCtrl(
\r
115 # dir=self.gui.config['folders-workdir'],
\r
117 # style=wx.DIRCTRL_SHOW_FILTERS,
\r
118 # filter=self.gui.config['folders-filters'],
\r
119 # defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert
\r
120 # ('playlists', panel.PANELS['playlist'](
\r
122 # config=self.gui.config,
\r
124 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
125 # # WANTS_CHARS so the panel doesn't eat the Return key.
\r
126 # size=(160, 200)), 'left'),
\r
127 # ('note', panel.note.Note(
\r
129 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
130 # size=(160, 200)), 'left'),
\r
131 # ('notebook', Notebook(
\r
133 # pos=wx.Point(client_size.x, client_size.y),
\r
134 # size=wx.Size(430, 200),
\r
135 # style=aui.AUI_NB_DEFAULT_STYLE
\r
136 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
137 ('commands', panel.PANELS['commands'](
\r
138 commands=self.commands,
\r
139 selected=self.gui.config['selected command'],
\r
141 'execute': self.execute_command,
\r
142 'select_plugin': self.select_plugin,
\r
143 'select_command': self.select_command,
\r
144 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
147 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
148 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
151 #('properties', panel.propertyeditor.PropertyEditor(self),'right'),
\r
152 # ('assistant', wx.TextCtrl(
\r
154 # pos=wx.Point(0, 0),
\r
155 # size=wx.Size(150, 90),
\r
156 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
157 ('output', panel.PANELS['output'](
\r
160 pos=wx.Point(0, 0),
\r
161 size=wx.Size(150, 90),
\r
162 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
164 # ('results', panel.results.Results(self), 'bottom'),
\r
166 self._add_panel(label, p, style)
\r
167 #self._c['assistant'].SetEditable(False)
\r
169 def _add_panel(self, label, panel, style):
\r
170 self._c[label] = panel
\r
171 cap_label = label.capitalize()
\r
172 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
173 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
176 elif style == 'center':
\r
178 elif style == 'left':
\r
180 elif style == 'right':
\r
183 assert style == 'bottom', style
\r
185 self._c['manager'].AddPane(panel, info)
\r
187 def _setup_toolbars(self):
\r
188 self._c['navigation bar'] = navbar.NavBar(
\r
190 'next': self._next_curve,
\r
191 'previous': self._previous_curve,
\r
194 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
195 self._c['manager'].AddPane(
\r
196 self._c['navigation bar'],
\r
197 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
198 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
199 ).RightDockable(False))
\r
201 def _bind_events(self):
\r
202 # TODO: figure out if we can use the eventManager for menu
\r
203 # ranges and events of 'self' without raising an assertion
\r
205 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
206 self.Bind(wx.EVT_SIZE, self._on_size)
\r
207 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
208 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
209 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
211 return # TODO: cleanup
\r
212 for value in self._c['menu bar']._c['view']._c.values():
\r
213 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
215 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
216 self._c['menu bar']._c['perspective']._c['save'])
\r
217 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
218 self._c['menu bar']._c['perspective']._c['delete'])
\r
220 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
221 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
223 # TODO: playlist callbacks
\r
224 return # TODO: cleanup
\r
225 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
227 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
229 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
231 def _on_about(self, *args):
\r
232 dialog = wx.MessageDialog(
\r
234 message=self.gui._splash_text(),
\r
235 caption='About Hooke',
\r
236 style=wx.OK|wx.ICON_INFORMATION)
\r
240 def _on_close(self, *args):
\r
242 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
243 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
244 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
245 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
246 # push changes back to Hooke.config?
\r
247 self._c['manager'].UnInit()
\r
248 del self._c['manager']
\r
255 def _command_by_name(self, name):
\r
256 cs = [c for c in self.commands if c.name == name]
\r
258 raise KeyError(name)
\r
260 raise Exception('Multiple commands named "%s"' % name)
\r
263 def execute_command(self, _class=None, method=None,
\r
264 command=None, args=None):
\r
265 self.inqueue.put(CommandMessage(command, args))
\r
268 msg = self.outqueue.get()
\r
269 results.append(msg)
\r
270 if isinstance(msg, Exit):
\r
273 elif isinstance(msg, CommandExit):
\r
274 # TODO: display command complete
\r
276 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
277 self.gui.reload_config(msg.config)
\r
279 elif isinstance(msg, Request):
\r
280 h = handler.HANDLERS[msg.type]
\r
281 h.run(self, msg) # TODO: pause for response?
\r
284 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
285 self._postprocess_text)
\r
286 pp(command=command, results=results)
\r
289 def _handle_request(self, msg):
\r
290 """Repeatedly try to get a response to `msg`.
\r
293 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
294 prompt_string = prompt(msg)
\r
295 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
297 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
301 self.cmd.stdout.write(''.join([
\r
302 error.__class__.__name__, ': ', str(error), '\n']))
\r
303 self.cmd.stdout.write(prompt_string)
\r
304 value = parser(msg, self.cmd.stdin.readline())
\r
306 response = msg.response(value)
\r
308 except ValueError, error:
\r
310 self.inqueue.put(response)
\r
314 # Command-specific postprocessing
\r
316 def _postprocess_text(self, command, results):
\r
317 """Print the string representation of the results to the Results window.
\r
319 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
320 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
321 doesn't print some internally handled messages
\r
322 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
324 for result in results:
\r
325 if isinstance(result, CommandExit):
\r
326 self._c['output'].write(result.__class__.__name__+'\n')
\r
327 self._c['output'].write(str(result).rstrip()+'\n')
\r
329 def _postprocess_get_curve(self, command, results):
\r
330 """Update `self` to show the curve.
\r
332 if not isinstance(results[-1], Success):
\r
333 return # error executing 'get curve'
\r
334 assert len(results) == 2, results
\r
338 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
339 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
340 #GetFirstChild returns a tuple
\r
341 #we only need the first element
\r
342 next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]
\r
344 next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)
\r
345 if not next_item.IsOk():
\r
346 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)
\r
347 #GetFirstChild returns a tuple
\r
348 #we only need the first element
\r
349 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]
\r
350 self._c['playlists']._c['tree'].SelectItem(next_item, True)
\r
351 if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
352 playlist = self.GetActivePlaylist()
\r
353 if playlist.count > 1:
\r
355 self._c['status bar'].set_playlist(playlist)
\r
363 def _GetActiveFileIndex(self):
\r
364 lib.playlist.Playlist = self.GetActivePlaylist()
\r
365 #get the selected item from the tree
\r
366 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
367 #test if a playlist or a curve was double-clicked
\r
368 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
372 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
373 while selected_item.IsOk():
\r
375 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
378 def _GetPlaylistTab(self, name):
\r
379 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
380 if page.caption == name:
\r
384 def select_plugin(self, _class=None, method=None, plugin=None):
\r
387 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
389 playlist = lib.playlist.Playlist(self, self.drivers)
\r
391 playlist.add_curve(item)
\r
392 if playlist.count > 0:
\r
393 playlist.name = self._GetUniquePlaylistName(name)
\r
395 self.AddTayliss(playlist)
\r
397 def AppliesPlotmanipulator(self, name):
\r
399 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
400 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
402 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
404 def ApplyPlotmanipulators(self, plot, plot_file):
\r
406 Apply all active plotmanipulators.
\r
408 if plot is not None and plot_file is not None:
\r
409 manipulated_plot = copy.deepcopy(plot)
\r
410 for plotmanipulator in self.plotmanipulators:
\r
411 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
412 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
413 return manipulated_plot
\r
415 def GetActiveFigure(self):
\r
416 playlist_name = self.GetActivePlaylistName()
\r
417 figure = self.playlists[playlist_name].figure
\r
418 if figure is not None:
\r
422 def GetActiveFile(self):
\r
423 playlist = self.GetActivePlaylist()
\r
424 if playlist is not None:
\r
425 return playlist.get_active_file()
\r
428 def GetActivePlot(self):
\r
429 playlist = self.GetActivePlaylist()
\r
430 if playlist is not None:
\r
431 return playlist.get_active_file().plot
\r
434 def GetDisplayedPlot(self):
\r
435 plot = copy.deepcopy(self.displayed_plot)
\r
437 #plot.curves = copy.deepcopy(plot.curves)
\r
440 def GetDisplayedPlotCorrected(self):
\r
441 plot = copy.deepcopy(self.displayed_plot)
\r
443 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
446 def GetDisplayedPlotRaw(self):
\r
447 plot = copy.deepcopy(self.displayed_plot)
\r
449 plot.curves = copy.deepcopy(plot.raw_curves)
\r
452 def GetDockArt(self):
\r
453 return self._c['manager'].GetArtProvider()
\r
455 def GetPlotmanipulator(self, name):
\r
457 Returns a plot manipulator function from its name
\r
459 for plotmanipulator in self.plotmanipulators:
\r
460 if plotmanipulator.name == name:
\r
461 return plotmanipulator
\r
464 def HasPlotmanipulator(self, name):
\r
466 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
468 for plotmanipulator in self.plotmanipulators:
\r
469 if plotmanipulator.command == name:
\r
474 def _on_dir_ctrl_left_double_click(self, event):
\r
475 file_path = self.panelFolders.GetPath()
\r
476 if os.path.isfile(file_path):
\r
477 if file_path.endswith('.hkp'):
\r
478 self.do_loadlist(file_path)
\r
481 def _on_erase_background(self, event):
\r
484 def _on_notebook_page_close(self, event):
\r
485 ctrl = event.GetEventObject()
\r
486 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
487 self.DeleteFromPlaylists(playlist_name)
\r
489 def OnPaneClose(self, event):
\r
492 def OnPropGridChanged (self, event):
\r
493 prop = event.GetProperty()
\r
495 item_section = self.panelProperties.SelectedTreeItem
\r
496 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
497 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
498 config = self.gui.config[plugin]
\r
499 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
500 property_key = prop.GetName()
\r
501 property_value = prop.GetDisplayedString()
\r
503 config[property_section][property_key]['value'] = property_value
\r
505 def OnResultsCheck(self, index, flag):
\r
506 results = self.GetActivePlot().results
\r
507 if results.has_key(self.results_str):
\r
508 results[self.results_str].results[index].visible = flag
\r
509 results[self.results_str].update()
\r
513 def _on_size(self, event):
\r
516 def OnUpdateNote(self, event):
\r
518 Saves the note to the active file.
\r
520 active_file = self.GetActiveFile()
\r
521 active_file.note = self.panelNote.Editor.GetValue()
\r
523 def UpdateNote(self):
\r
524 #update the note for the active file
\r
525 active_file = self.GetActiveFile()
\r
526 if active_file is not None:
\r
527 self.panelNote.Editor.SetValue(active_file.note)
\r
529 def UpdatePlaylistsTreeSelection(self):
\r
530 playlist = self.GetActivePlaylist()
\r
531 if playlist is not None:
\r
532 if playlist.index >= 0:
\r
533 self._c['status bar'].set_playlist(playlist)
\r
537 def UpdatePlot(self, plot=None):
\r
539 def add_to_plot(curve, set_scale=True):
\r
540 if curve.visible and curve.x and curve.y:
\r
541 #get the index of the subplot to use as destination
\r
542 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
543 #set all parameters for the plot
\r
544 axes_list[destination].set_title(curve.title)
\r
546 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
547 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
548 #set the formatting details for the scale
\r
549 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
550 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
551 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
552 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
553 if curve.style == 'plot':
\r
554 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
555 if curve.style == 'scatter':
\r
556 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
557 #add the legend if necessary
\r
559 axes_list[destination].legend()
\r
562 active_file = self.GetActiveFile()
\r
563 if not active_file.driver:
\r
564 #the first time we identify a file, the following need to be set
\r
565 active_file.identify(self.drivers)
\r
566 for curve in active_file.plot.curves:
\r
567 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
568 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
569 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
570 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
571 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
572 if active_file.driver is None:
\r
573 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
575 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
576 #add raw curves to plot
\r
577 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
578 #apply all active plotmanipulators
\r
579 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
580 #add corrected curves to plot
\r
581 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
584 self.displayed_plot = copy.deepcopy(plot)
\r
586 figure = self.GetActiveFigure()
\r
589 #use '0' instead of e.g. '0.00' for scales
\r
590 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
591 #optionally remove the extension from the title of the plot
\r
592 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
593 if hide_curve_extension:
\r
594 title = lh.remove_extension(self.displayed_plot.title)
\r
596 title = self.displayed_plot.title
\r
597 figure.suptitle(title, fontsize=14)
\r
598 #create the list of all axes necessary (rows and columns)
\r
600 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
601 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
602 for index in range(number_of_rows * number_of_columns):
\r
603 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
605 #add all curves to the corresponding plots
\r
606 for curve in self.displayed_plot.curves:
\r
609 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
610 figure.subplots_adjust(hspace=0.3)
\r
613 self.panelResults.ClearResults()
\r
614 if self.displayed_plot.results.has_key(self.results_str):
\r
615 for curve in self.displayed_plot.results[self.results_str].results:
\r
616 add_to_plot(curve, set_scale=False)
\r
617 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
619 self.panelResults.ClearResults()
\r
621 figure.canvas.draw()
\r
623 def _on_curve_select(self, playlist, curve):
\r
624 #create the plot tab and add playlist to the dictionary
\r
625 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
626 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
627 #tab_index = self._c['notebook'].GetSelection()
\r
628 playlist.figure = plotPanel.get_figure()
\r
629 self.playlists[playlist.name] = playlist
\r
630 #self.playlists[playlist.name] = [playlist, figure]
\r
631 self._c['status bar'].set_playlist(playlist)
\r
636 def _on_playlist_left_doubleclick(self):
\r
637 index = self._c['notebook'].GetSelection()
\r
638 current_playlist = self._c['notebook'].GetPageText(index)
\r
639 if current_playlist != playlist_name:
\r
640 index = self._GetPlaylistTab(playlist_name)
\r
641 self._c['notebook'].SetSelection(index)
\r
642 self._c['status bar'].set_playlist(playlist)
\r
646 def _on_playlist_delete(self, playlist):
\r
647 notebook = self.Parent.plotNotebook
\r
648 index = self.Parent._GetPlaylistTab(playlist.name)
\r
649 notebook.SetSelection(index)
\r
650 notebook.DeletePage(notebook.GetSelection())
\r
651 self.Parent.DeleteFromPlaylists(playlist_name)
\r
655 # Command panel interface
\r
657 def select_command(self, _class, method, command):
\r
658 self.select_plugin(plugin=command.plugin)
\r
659 plugin = self.GetItemText(selected_item)
\r
660 if plugin != 'core':
\r
661 doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')
\r
663 doc_string = 'The module "core" contains Hooke core functionality'
\r
664 if doc_string is not None:
\r
665 self.panelAssistant.ChangeValue(doc_string)
\r
667 self.panelAssistant.ChangeValue('')
\r
668 panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)
\r
669 self.gui.config['selected command'] = command
\r
675 def _next_curve(self, *args):
\r
676 """Call the `next curve` command.
\r
678 results = self.execute_command(
\r
679 command=self._command_by_name('next curve'))
\r
680 if isinstance(results[-1], Success):
\r
681 self.execute_command(
\r
682 command=self._command_by_name('get curve'))
\r
684 def _previous_curve(self, *args):
\r
685 """Call the `previous curve` command.
\r
687 self.execute_command(
\r
688 command=self._command_by_name('previous curve'))
\r
689 if isinstance(results[-1], Success):
\r
690 self.execute_command(
\r
691 command=self._command_by_name('get curve'))
\r
695 # Panel display handling
\r
697 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
698 pane = self._c['manager'].GetPane(panel_name)
\r
701 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
702 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
703 #folders_size = pane.GetSize()
\r
704 self.panelFolders.Fit()
\r
705 self._c['manager'].Update()
\r
707 def _setup_perspectives(self):
\r
708 """Add perspectives to menubar and _perspectives.
\r
710 self._perspectives = {
\r
711 'Default': self._c['manager'].SavePerspective(),
\r
713 path = self.gui.config['perspective path']
\r
714 if os.path.isdir(path):
\r
715 files = sorted(os.listdir(path))
\r
716 for fname in files:
\r
717 name, extension = os.path.splitext(fname)
\r
718 if extension != self.gui.config['perspective extension']:
\r
720 fpath = os.path.join(path, fname)
\r
721 if not os.path.isfile(fpath):
\r
724 with open(fpath, 'rU') as f:
\r
725 perspective = f.readline()
\r
727 self._perspectives[name] = perspective
\r
729 selected_perspective = self.gui.config['active perspective']
\r
730 if not self._perspectives.has_key(selected_perspective):
\r
731 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
733 self._restore_perspective(selected_perspective)
\r
734 self._update_perspective_menu()
\r
736 def _update_perspective_menu(self):
\r
737 self._c['menu bar']._c['perspective'].update(
\r
738 sorted(self._perspectives.keys()),
\r
739 self.gui.config['active perspective'])
\r
741 def _save_perspective(self, perspective, perspective_dir, name,
\r
743 path = os.path.join(perspective_dir, name)
\r
744 if extension != None:
\r
746 if not os.path.isdir(perspective_dir):
\r
747 os.makedirs(perspective_dir)
\r
748 with open(path, 'w') as f:
\r
749 f.write(perspective)
\r
750 self._perspectives[name] = perspective
\r
751 self._restore_perspective(name)
\r
752 self._update_perspective_menu()
\r
754 def _delete_perspectives(self, perspective_dir, names,
\r
758 path = os.path.join(perspective_dir, name)
\r
759 if extension != None:
\r
762 del(self._perspectives[name])
\r
763 self._update_perspective_menu()
\r
764 if self.gui.config['active perspective'] in names:
\r
765 self._restore_perspective('Default')
\r
766 # TODO: does this bug still apply?
\r
767 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
768 # http://trac.wxwidgets.org/ticket/3258
\r
769 # ) that makes the radio item indicator in the menu disappear.
\r
770 # The code should be fine once this issue is fixed.
\r
772 def _restore_perspective(self, name):
\r
773 if name != self.gui.config['active perspective']:
\r
774 print 'restoring perspective:', name
\r
775 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
776 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
777 self._c['manager'].Update()
\r
778 for pane in self._c['manager'].GetAllPanes():
\r
779 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
780 pane.Check(pane.window.IsShown())
\r
782 def _on_save_perspective(self, *args):
\r
783 perspective = self._c['manager'].SavePerspective()
\r
784 name = self.gui.config['active perspective']
\r
785 if name == 'Default':
\r
786 name = 'New perspective'
\r
787 name = select_save_file(
\r
788 directory=self.gui.config['perspective path'],
\r
790 extension=self.gui.config['perspective extension'],
\r
792 message='Enter a name for the new perspective:',
\r
793 caption='Save perspective')
\r
796 self._save_perspective(
\r
797 perspective, self.gui.config['perspective path'], name=name,
\r
798 extension=self.gui.config['perspective extension'])
\r
800 def _on_delete_perspective(self, *args, **kwargs):
\r
801 options = sorted([p for p in self._perspectives.keys()
\r
802 if p != 'Default'])
\r
803 dialog = SelectionDialog(
\r
805 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
806 button_id=wx.ID_DELETE,
\r
807 selection_style='multiple',
\r
809 title='Delete perspective(s)',
\r
810 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
811 dialog.CenterOnScreen()
\r
813 names = [options[i] for i in dialog.selected]
\r
815 self._delete_perspectives(
\r
816 self.gui.config['perspective path'], names=names,
\r
817 extension=self.gui.config['perspective extension'])
\r
819 def _on_select_perspective(self, _class, method, name):
\r
820 self._restore_perspective(name)
\r
824 class HookeApp (wx.App):
\r
825 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
827 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
830 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
832 self.commands = commands
\r
833 self.inqueue = inqueue
\r
834 self.outqueue = outqueue
\r
835 super(HookeApp, self).__init__(*args, **kwargs)
\r
838 self.SetAppName('Hooke')
\r
839 self.SetVendorName('')
\r
840 self._setup_splash_screen()
\r
842 height = int(self.gui.config['main height']) # HACK: config should convert
\r
843 width = int(self.gui.config['main width'])
\r
844 top = int(self.gui.config['main top'])
\r
845 left = int(self.gui.config['main left'])
\r
847 # Sometimes, the ini file gets confused and sets 'left' and
\r
848 # 'top' to large negative numbers. Here we catch and fix
\r
849 # this. Keep small negative numbers, the user might want
\r
857 'frame': HookeFrame(
\r
858 self.gui, self.commands, self.inqueue, self.outqueue,
\r
859 parent=None, title='Hooke',
\r
860 pos=(left, top), size=(width, height),
\r
861 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
863 self._c['frame'].Show(True)
\r
864 self.SetTopWindow(self._c['frame'])
\r
867 def _setup_splash_screen(self):
\r
868 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
869 print 'splash', self.gui.config['show splash screen']
\r
870 path = self.gui.config['splash screen image']
\r
871 if os.path.isfile(path):
\r
872 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
874 bitmap=wx.Image(path).ConvertToBitmap(),
\r
875 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
876 milliseconds=duration,
\r
879 # For some reason splashDuration and sleep do not
\r
880 # correspond to each other at least not on Windows.
\r
881 # Maybe it's because duration is in milliseconds and
\r
882 # sleep in seconds. Thus we need to increase the
\r
883 # sleep time a bit. A factor of 1.2 seems to work.
\r
885 time.sleep(sleepFactor * duration / 1000)
\r
888 class GUI (UserInterface):
\r
889 """wxWindows graphical user interface.
\r
891 def __init__(self):
\r
892 super(GUI, self).__init__(name='gui')
\r
894 def default_settings(self):
\r
895 """Return a list of :class:`hooke.config.Setting`\s for any
\r
896 configurable UI settings.
\r
898 The suggested section setting is::
\r
900 Setting(section=self.setting_section, help=self.__doc__)
\r
903 Setting(section=self.setting_section, help=self.__doc__),
\r
904 Setting(section=self.setting_section, option='icon image',
\r
905 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
906 help='Path to the hooke icon image.'),
\r
907 Setting(section=self.setting_section, option='show splash screen',
\r
909 help='Enable/disable the splash screen'),
\r
910 Setting(section=self.setting_section, option='splash screen image',
\r
911 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
912 help='Path to the Hooke splash screen image.'),
\r
913 Setting(section=self.setting_section, option='splash screen duration',
\r
915 help='Duration of the splash screen in milliseconds.'),
\r
916 Setting(section=self.setting_section, option='perspective path',
\r
917 value=os.path.join('resources', 'gui', 'perspective'),
\r
918 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
919 Setting(section=self.setting_section, option='perspective extension',
\r
921 help='Extension for perspective files.'),
\r
922 Setting(section=self.setting_section, option='hide extensions',
\r
924 help='Hide file extensions when displaying names.'),
\r
925 Setting(section=self.setting_section, option='folders-workdir',
\r
927 help='This should probably go...'),
\r
928 Setting(section=self.setting_section, option='folders-filters',
\r
930 help='This should probably go...'),
\r
931 Setting(section=self.setting_section, option='active perspective',
\r
933 help='Name of active perspective file (or "Default").'),
\r
934 Setting(section=self.setting_section, option='folders-filter-index',
\r
936 help='This should probably go...'),
\r
937 Setting(section=self.setting_section, option='main height',
\r
939 help='Height of main window in pixels.'),
\r
940 Setting(section=self.setting_section, option='main width',
\r
942 help='Width of main window in pixels.'),
\r
943 Setting(section=self.setting_section, option='main top',
\r
945 help='Pixels from screen top to top of main window.'),
\r
946 Setting(section=self.setting_section, option='main left',
\r
948 help='Pixels from screen left to left of main window.'),
\r
949 Setting(section=self.setting_section, option='selected command',
\r
950 value='load playlist',
\r
951 help='Name of the initially selected command.'),
\r
954 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
958 app = HookeApp(gui=self,
\r
960 inqueue=ui_to_command_queue,
\r
961 outqueue=command_to_ui_queue,
\r
965 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
966 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r