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 config=self.gui.config,
\r
123 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
124 # WANTS_CHARS so the panel doesn't eat the Return key.
\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 ('property', panel.PANELS['propertyeditor2'](
\r
154 style=wx.WANTS_CHARS,
\r
155 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
157 # ('assistant', wx.TextCtrl(
\r
159 # pos=wx.Point(0, 0),
\r
160 # size=wx.Size(150, 90),
\r
161 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
162 ('output', panel.PANELS['output'](
\r
164 pos=wx.Point(0, 0),
\r
165 size=wx.Size(150, 90),
\r
166 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
168 # ('results', panel.results.Results(self), 'bottom'),
\r
170 self._add_panel(label, p, style)
\r
171 #self._c['assistant'].SetEditable(False)
\r
173 def _add_panel(self, label, panel, style):
\r
174 self._c[label] = panel
\r
175 cap_label = label.capitalize()
\r
176 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
177 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
180 elif style == 'center':
\r
182 elif style == 'left':
\r
184 elif style == 'right':
\r
187 assert style == 'bottom', style
\r
189 self._c['manager'].AddPane(panel, info)
\r
191 def _setup_toolbars(self):
\r
192 self._c['navigation bar'] = navbar.NavBar(
\r
194 'next': self._next_curve,
\r
195 'previous': self._previous_curve,
\r
198 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
199 self._c['manager'].AddPane(
\r
200 self._c['navigation bar'],
\r
201 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
202 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
203 ).RightDockable(False))
\r
205 def _bind_events(self):
\r
206 # TODO: figure out if we can use the eventManager for menu
\r
207 # ranges and events of 'self' without raising an assertion
\r
209 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
210 self.Bind(wx.EVT_SIZE, self._on_size)
\r
211 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
212 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
213 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
215 return # TODO: cleanup
\r
216 for value in self._c['menu bar']._c['view']._c.values():
\r
217 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
219 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
220 self._c['menu bar']._c['perspective']._c['save'])
\r
221 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
222 self._c['menu bar']._c['perspective']._c['delete'])
\r
224 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
225 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
227 # TODO: playlist callbacks
\r
228 return # TODO: cleanup
\r
229 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
231 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
233 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
235 def _on_about(self, *args):
\r
236 dialog = wx.MessageDialog(
\r
238 message=self.gui._splash_text(),
\r
239 caption='About Hooke',
\r
240 style=wx.OK|wx.ICON_INFORMATION)
\r
244 def _on_close(self, *args):
\r
246 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
247 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
248 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
249 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
250 # push changes back to Hooke.config?
\r
251 self._c['manager'].UnInit()
\r
252 del self._c['manager']
\r
259 def _command_by_name(self, name):
\r
260 cs = [c for c in self.commands if c.name == name]
\r
262 raise KeyError(name)
\r
264 raise Exception('Multiple commands named "%s"' % name)
\r
267 def execute_command(self, _class=None, method=None,
\r
268 command=None, args=None):
\r
271 if ('property' in self._c
\r
272 and self.gui.config['selected command'] == command):
\r
273 arg_names = [arg.name for arg in command.arguments]
\r
274 for name,value in self._c['property'].get_values().items():
\r
275 if name in arg_names:
\r
277 print 'executing', command.name, args
\r
278 self.inqueue.put(CommandMessage(command, args))
\r
281 msg = self.outqueue.get()
\r
282 results.append(msg)
\r
283 if isinstance(msg, Exit):
\r
286 elif isinstance(msg, CommandExit):
\r
287 # TODO: display command complete
\r
289 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
290 self.gui.reload_config(msg.config)
\r
292 elif isinstance(msg, Request):
\r
293 h = handler.HANDLERS[msg.type]
\r
294 h.run(self, msg) # TODO: pause for response?
\r
297 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
298 self._postprocess_text)
\r
299 pp(command=command, results=results)
\r
302 def _handle_request(self, msg):
\r
303 """Repeatedly try to get a response to `msg`.
\r
306 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
307 prompt_string = prompt(msg)
\r
308 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
310 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
314 self.cmd.stdout.write(''.join([
\r
315 error.__class__.__name__, ': ', str(error), '\n']))
\r
316 self.cmd.stdout.write(prompt_string)
\r
317 value = parser(msg, self.cmd.stdin.readline())
\r
319 response = msg.response(value)
\r
321 except ValueError, error:
\r
323 self.inqueue.put(response)
\r
327 # Command-specific postprocessing
\r
329 def _postprocess_text(self, command, results):
\r
330 """Print the string representation of the results to the Results window.
\r
332 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
333 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
334 doesn't print some internally handled messages
\r
335 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
337 for result in results:
\r
338 if isinstance(result, CommandExit):
\r
339 self._c['output'].write(result.__class__.__name__+'\n')
\r
340 self._c['output'].write(str(result).rstrip()+'\n')
\r
342 def _postprocess_text(self, command, results):
\r
343 """Print the string representation of the results to the Results window.
\r
345 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
346 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
347 doesn't print some internally handled messages
\r
348 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
350 for result in results:
\r
351 if isinstance(result, CommandExit):
\r
352 self._c['output'].write(result.__class__.__name__+'\n')
\r
353 self._c['output'].write(str(result).rstrip()+'\n')
\r
355 def _postprocess_load_playlist(self, command, results):
\r
356 """Update `self` to show the playlist.
\r
358 if not isinstance(results[-1], Success):
\r
359 return # error executing 'load playlist'
\r
360 assert len(results) == 2, results
\r
361 playlist = results[0]
\r
364 self._c['playlists']._c['tree'].add_playlist(playlist)
\r
369 def _GetActiveFileIndex(self):
\r
370 lib.playlist.Playlist = self.GetActivePlaylist()
\r
371 #get the selected item from the tree
\r
372 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
373 #test if a playlist or a curve was double-clicked
\r
374 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
378 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
379 while selected_item.IsOk():
\r
381 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
384 def _GetPlaylistTab(self, name):
\r
385 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
386 if page.caption == name:
\r
390 def select_plugin(self, _class=None, method=None, plugin=None):
\r
393 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
395 playlist = lib.playlist.Playlist(self, self.drivers)
\r
397 playlist.add_curve(item)
\r
398 if playlist.count > 0:
\r
399 playlist.name = self._GetUniquePlaylistName(name)
\r
401 self.AddTayliss(playlist)
\r
403 def AppliesPlotmanipulator(self, name):
\r
405 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
406 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
408 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
410 def ApplyPlotmanipulators(self, plot, plot_file):
\r
412 Apply all active plotmanipulators.
\r
414 if plot is not None and plot_file is not None:
\r
415 manipulated_plot = copy.deepcopy(plot)
\r
416 for plotmanipulator in self.plotmanipulators:
\r
417 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
418 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
419 return manipulated_plot
\r
421 def GetActiveFigure(self):
\r
422 playlist_name = self.GetActivePlaylistName()
\r
423 figure = self.playlists[playlist_name].figure
\r
424 if figure is not None:
\r
428 def GetActiveFile(self):
\r
429 playlist = self.GetActivePlaylist()
\r
430 if playlist is not None:
\r
431 return playlist.get_active_file()
\r
434 def GetActivePlot(self):
\r
435 playlist = self.GetActivePlaylist()
\r
436 if playlist is not None:
\r
437 return playlist.get_active_file().plot
\r
440 def GetDisplayedPlot(self):
\r
441 plot = copy.deepcopy(self.displayed_plot)
\r
443 #plot.curves = copy.deepcopy(plot.curves)
\r
446 def GetDisplayedPlotCorrected(self):
\r
447 plot = copy.deepcopy(self.displayed_plot)
\r
449 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
452 def GetDisplayedPlotRaw(self):
\r
453 plot = copy.deepcopy(self.displayed_plot)
\r
455 plot.curves = copy.deepcopy(plot.raw_curves)
\r
458 def GetDockArt(self):
\r
459 return self._c['manager'].GetArtProvider()
\r
461 def GetPlotmanipulator(self, name):
\r
463 Returns a plot manipulator function from its name
\r
465 for plotmanipulator in self.plotmanipulators:
\r
466 if plotmanipulator.name == name:
\r
467 return plotmanipulator
\r
470 def HasPlotmanipulator(self, name):
\r
472 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
474 for plotmanipulator in self.plotmanipulators:
\r
475 if plotmanipulator.command == name:
\r
480 def _on_dir_ctrl_left_double_click(self, event):
\r
481 file_path = self.panelFolders.GetPath()
\r
482 if os.path.isfile(file_path):
\r
483 if file_path.endswith('.hkp'):
\r
484 self.do_loadlist(file_path)
\r
487 def _on_erase_background(self, event):
\r
490 def _on_notebook_page_close(self, event):
\r
491 ctrl = event.GetEventObject()
\r
492 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
493 self.DeleteFromPlaylists(playlist_name)
\r
495 def OnPaneClose(self, event):
\r
498 def OnPropGridChanged (self, event):
\r
499 prop = event.GetProperty()
\r
501 item_section = self.panelProperties.SelectedTreeItem
\r
502 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
503 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
504 config = self.gui.config[plugin]
\r
505 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
506 property_key = prop.GetName()
\r
507 property_value = prop.GetDisplayedString()
\r
509 config[property_section][property_key]['value'] = property_value
\r
511 def OnResultsCheck(self, index, flag):
\r
512 results = self.GetActivePlot().results
\r
513 if results.has_key(self.results_str):
\r
514 results[self.results_str].results[index].visible = flag
\r
515 results[self.results_str].update()
\r
519 def _on_size(self, event):
\r
522 def OnUpdateNote(self, event):
\r
524 Saves the note to the active file.
\r
526 active_file = self.GetActiveFile()
\r
527 active_file.note = self.panelNote.Editor.GetValue()
\r
529 def UpdateNote(self):
\r
530 #update the note for the active file
\r
531 active_file = self.GetActiveFile()
\r
532 if active_file is not None:
\r
533 self.panelNote.Editor.SetValue(active_file.note)
\r
535 def UpdatePlaylistsTreeSelection(self):
\r
536 playlist = self.GetActivePlaylist()
\r
537 if playlist is not None:
\r
538 if playlist.index >= 0:
\r
539 self._c['status bar'].set_playlist(playlist)
\r
543 def UpdatePlot(self, plot=None):
\r
545 def add_to_plot(curve, set_scale=True):
\r
546 if curve.visible and curve.x and curve.y:
\r
547 #get the index of the subplot to use as destination
\r
548 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
549 #set all parameters for the plot
\r
550 axes_list[destination].set_title(curve.title)
\r
552 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
553 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
554 #set the formatting details for the scale
\r
555 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
556 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
557 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
558 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
559 if curve.style == 'plot':
\r
560 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
561 if curve.style == 'scatter':
\r
562 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
563 #add the legend if necessary
\r
565 axes_list[destination].legend()
\r
568 active_file = self.GetActiveFile()
\r
569 if not active_file.driver:
\r
570 #the first time we identify a file, the following need to be set
\r
571 active_file.identify(self.drivers)
\r
572 for curve in active_file.plot.curves:
\r
573 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
574 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
575 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
576 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
577 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
578 if active_file.driver is None:
\r
579 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
581 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
582 #add raw curves to plot
\r
583 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
584 #apply all active plotmanipulators
\r
585 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
586 #add corrected curves to plot
\r
587 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
590 self.displayed_plot = copy.deepcopy(plot)
\r
592 figure = self.GetActiveFigure()
\r
595 #use '0' instead of e.g. '0.00' for scales
\r
596 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
597 #optionally remove the extension from the title of the plot
\r
598 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
599 if hide_curve_extension:
\r
600 title = lh.remove_extension(self.displayed_plot.title)
\r
602 title = self.displayed_plot.title
\r
603 figure.suptitle(title, fontsize=14)
\r
604 #create the list of all axes necessary (rows and columns)
\r
606 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
607 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
608 for index in range(number_of_rows * number_of_columns):
\r
609 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
611 #add all curves to the corresponding plots
\r
612 for curve in self.displayed_plot.curves:
\r
615 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
616 figure.subplots_adjust(hspace=0.3)
\r
619 self.panelResults.ClearResults()
\r
620 if self.displayed_plot.results.has_key(self.results_str):
\r
621 for curve in self.displayed_plot.results[self.results_str].results:
\r
622 add_to_plot(curve, set_scale=False)
\r
623 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
625 self.panelResults.ClearResults()
\r
627 figure.canvas.draw()
\r
629 def _on_curve_select(self, playlist, curve):
\r
630 #create the plot tab and add playlist to the dictionary
\r
631 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
632 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
633 #tab_index = self._c['notebook'].GetSelection()
\r
634 playlist.figure = plotPanel.get_figure()
\r
635 self.playlists[playlist.name] = playlist
\r
636 #self.playlists[playlist.name] = [playlist, figure]
\r
637 self._c['status bar'].set_playlist(playlist)
\r
642 def _on_playlist_left_doubleclick(self):
\r
643 index = self._c['notebook'].GetSelection()
\r
644 current_playlist = self._c['notebook'].GetPageText(index)
\r
645 if current_playlist != playlist_name:
\r
646 index = self._GetPlaylistTab(playlist_name)
\r
647 self._c['notebook'].SetSelection(index)
\r
648 self._c['status bar'].set_playlist(playlist)
\r
652 def _on_playlist_delete(self, playlist):
\r
653 notebook = self.Parent.plotNotebook
\r
654 index = self.Parent._GetPlaylistTab(playlist.name)
\r
655 notebook.SetSelection(index)
\r
656 notebook.DeletePage(notebook.GetSelection())
\r
657 self.Parent.DeleteFromPlaylists(playlist_name)
\r
661 # Command panel interface
\r
663 def select_command(self, _class, method, command):
\r
664 #self.select_plugin(plugin=command.plugin)
\r
665 if 'assistant' in self._c:
\r
666 self._c['assitant'].ChangeValue(command.help)
\r
667 self._c['property'].clear()
\r
668 for argument in command.arguments:
\r
669 if argument.name == 'help':
\r
671 p = prop_from_argument(
\r
672 argument, curves=[], playlists=[]) # TODO: lookup playlists/curves
\r
674 continue # property intentionally not handled (yet)
\r
675 self._c['property'].append_property(p)
\r
677 self.gui.config['selected command'] = command # TODO: push to engine
\r
683 def _next_curve(self, *args):
\r
684 """Call the `next curve` command.
\r
686 results = self.execute_command(
\r
687 command=self._command_by_name('next curve'))
\r
688 if isinstance(results[-1], Success):
\r
689 self.execute_command(
\r
690 command=self._command_by_name('get curve'))
\r
692 def _previous_curve(self, *args):
\r
693 """Call the `previous curve` command.
\r
695 self.execute_command(
\r
696 command=self._command_by_name('previous curve'))
\r
697 if isinstance(results[-1], Success):
\r
698 self.execute_command(
\r
699 command=self._command_by_name('get curve'))
\r
703 # Panel display handling
\r
705 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
706 pane = self._c['manager'].GetPane(panel_name)
\r
709 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
710 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
711 #folders_size = pane.GetSize()
\r
712 self.panelFolders.Fit()
\r
713 self._c['manager'].Update()
\r
715 def _setup_perspectives(self):
\r
716 """Add perspectives to menubar and _perspectives.
\r
718 self._perspectives = {
\r
719 'Default': self._c['manager'].SavePerspective(),
\r
721 path = self.gui.config['perspective path']
\r
722 if os.path.isdir(path):
\r
723 files = sorted(os.listdir(path))
\r
724 for fname in files:
\r
725 name, extension = os.path.splitext(fname)
\r
726 if extension != self.gui.config['perspective extension']:
\r
728 fpath = os.path.join(path, fname)
\r
729 if not os.path.isfile(fpath):
\r
732 with open(fpath, 'rU') as f:
\r
733 perspective = f.readline()
\r
735 self._perspectives[name] = perspective
\r
737 selected_perspective = self.gui.config['active perspective']
\r
738 if not self._perspectives.has_key(selected_perspective):
\r
739 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
741 self._restore_perspective(selected_perspective)
\r
742 self._update_perspective_menu()
\r
744 def _update_perspective_menu(self):
\r
745 self._c['menu bar']._c['perspective'].update(
\r
746 sorted(self._perspectives.keys()),
\r
747 self.gui.config['active perspective'])
\r
749 def _save_perspective(self, perspective, perspective_dir, name,
\r
751 path = os.path.join(perspective_dir, name)
\r
752 if extension != None:
\r
754 if not os.path.isdir(perspective_dir):
\r
755 os.makedirs(perspective_dir)
\r
756 with open(path, 'w') as f:
\r
757 f.write(perspective)
\r
758 self._perspectives[name] = perspective
\r
759 self._restore_perspective(name)
\r
760 self._update_perspective_menu()
\r
762 def _delete_perspectives(self, perspective_dir, names,
\r
766 path = os.path.join(perspective_dir, name)
\r
767 if extension != None:
\r
770 del(self._perspectives[name])
\r
771 self._update_perspective_menu()
\r
772 if self.gui.config['active perspective'] in names:
\r
773 self._restore_perspective('Default')
\r
774 # TODO: does this bug still apply?
\r
775 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
776 # http://trac.wxwidgets.org/ticket/3258
\r
777 # ) that makes the radio item indicator in the menu disappear.
\r
778 # The code should be fine once this issue is fixed.
\r
780 def _restore_perspective(self, name):
\r
781 if name != self.gui.config['active perspective']:
\r
782 print 'restoring perspective:', name
\r
783 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
784 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
785 self._c['manager'].Update()
\r
786 for pane in self._c['manager'].GetAllPanes():
\r
787 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
788 pane.Check(pane.window.IsShown())
\r
790 def _on_save_perspective(self, *args):
\r
791 perspective = self._c['manager'].SavePerspective()
\r
792 name = self.gui.config['active perspective']
\r
793 if name == 'Default':
\r
794 name = 'New perspective'
\r
795 name = select_save_file(
\r
796 directory=self.gui.config['perspective path'],
\r
798 extension=self.gui.config['perspective extension'],
\r
800 message='Enter a name for the new perspective:',
\r
801 caption='Save perspective')
\r
804 self._save_perspective(
\r
805 perspective, self.gui.config['perspective path'], name=name,
\r
806 extension=self.gui.config['perspective extension'])
\r
808 def _on_delete_perspective(self, *args, **kwargs):
\r
809 options = sorted([p for p in self._perspectives.keys()
\r
810 if p != 'Default'])
\r
811 dialog = SelectionDialog(
\r
813 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
814 button_id=wx.ID_DELETE,
\r
815 selection_style='multiple',
\r
817 title='Delete perspective(s)',
\r
818 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
819 dialog.CenterOnScreen()
\r
821 names = [options[i] for i in dialog.selected]
\r
823 self._delete_perspectives(
\r
824 self.gui.config['perspective path'], names=names,
\r
825 extension=self.gui.config['perspective extension'])
\r
827 def _on_select_perspective(self, _class, method, name):
\r
828 self._restore_perspective(name)
\r
832 class HookeApp (wx.App):
\r
833 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
835 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
838 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
840 self.commands = commands
\r
841 self.inqueue = inqueue
\r
842 self.outqueue = outqueue
\r
843 super(HookeApp, self).__init__(*args, **kwargs)
\r
846 self.SetAppName('Hooke')
\r
847 self.SetVendorName('')
\r
848 self._setup_splash_screen()
\r
850 height = int(self.gui.config['main height']) # HACK: config should convert
\r
851 width = int(self.gui.config['main width'])
\r
852 top = int(self.gui.config['main top'])
\r
853 left = int(self.gui.config['main left'])
\r
855 # Sometimes, the ini file gets confused and sets 'left' and
\r
856 # 'top' to large negative numbers. Here we catch and fix
\r
857 # this. Keep small negative numbers, the user might want
\r
865 'frame': HookeFrame(
\r
866 self.gui, self.commands, self.inqueue, self.outqueue,
\r
867 parent=None, title='Hooke',
\r
868 pos=(left, top), size=(width, height),
\r
869 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
871 self._c['frame'].Show(True)
\r
872 self.SetTopWindow(self._c['frame'])
\r
875 def _setup_splash_screen(self):
\r
876 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
877 print 'splash', self.gui.config['show splash screen']
\r
878 path = self.gui.config['splash screen image']
\r
879 if os.path.isfile(path):
\r
880 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
882 bitmap=wx.Image(path).ConvertToBitmap(),
\r
883 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
884 milliseconds=duration,
\r
887 # For some reason splashDuration and sleep do not
\r
888 # correspond to each other at least not on Windows.
\r
889 # Maybe it's because duration is in milliseconds and
\r
890 # sleep in seconds. Thus we need to increase the
\r
891 # sleep time a bit. A factor of 1.2 seems to work.
\r
893 time.sleep(sleepFactor * duration / 1000)
\r
896 class GUI (UserInterface):
\r
897 """wxWindows graphical user interface.
\r
899 def __init__(self):
\r
900 super(GUI, self).__init__(name='gui')
\r
902 def default_settings(self):
\r
903 """Return a list of :class:`hooke.config.Setting`\s for any
\r
904 configurable UI settings.
\r
906 The suggested section setting is::
\r
908 Setting(section=self.setting_section, help=self.__doc__)
\r
911 Setting(section=self.setting_section, help=self.__doc__),
\r
912 Setting(section=self.setting_section, option='icon image',
\r
913 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
914 help='Path to the hooke icon image.'),
\r
915 Setting(section=self.setting_section, option='show splash screen',
\r
917 help='Enable/disable the splash screen'),
\r
918 Setting(section=self.setting_section, option='splash screen image',
\r
919 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
920 help='Path to the Hooke splash screen image.'),
\r
921 Setting(section=self.setting_section, option='splash screen duration',
\r
923 help='Duration of the splash screen in milliseconds.'),
\r
924 Setting(section=self.setting_section, option='perspective path',
\r
925 value=os.path.join('resources', 'gui', 'perspective'),
\r
926 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
927 Setting(section=self.setting_section, option='perspective extension',
\r
929 help='Extension for perspective files.'),
\r
930 Setting(section=self.setting_section, option='hide extensions',
\r
932 help='Hide file extensions when displaying names.'),
\r
933 Setting(section=self.setting_section, option='folders-workdir',
\r
935 help='This should probably go...'),
\r
936 Setting(section=self.setting_section, option='folders-filters',
\r
938 help='This should probably go...'),
\r
939 Setting(section=self.setting_section, option='active perspective',
\r
941 help='Name of active perspective file (or "Default").'),
\r
942 Setting(section=self.setting_section, option='folders-filter-index',
\r
944 help='This should probably go...'),
\r
945 Setting(section=self.setting_section, option='main height',
\r
947 help='Height of main window in pixels.'),
\r
948 Setting(section=self.setting_section, option='main width',
\r
950 help='Width of main window in pixels.'),
\r
951 Setting(section=self.setting_section, option='main top',
\r
953 help='Pixels from screen top to top of main window.'),
\r
954 Setting(section=self.setting_section, option='main left',
\r
956 help='Pixels from screen left to left of main window.'),
\r
957 Setting(section=self.setting_section, option='selected command',
\r
958 value='load playlist',
\r
959 help='Name of the initially selected command.'),
\r
962 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
966 app = HookeApp(gui=self,
\r
968 inqueue=ui_to_command_queue,
\r
969 outqueue=command_to_ui_queue,
\r
973 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
974 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r