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', 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), 'bottom'),
\r
162 # ('results', panel.results.Results(self), 'bottom'),
\r
164 self._add_panel(label, p, style)
\r
165 #self._c['assistant'].SetEditable(False)
\r
167 def _add_panel(self, label, panel, style):
\r
168 self._c[label] = panel
\r
169 cap_label = label.capitalize()
\r
170 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
171 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
174 elif style == 'center':
\r
176 elif style == 'left':
\r
178 elif style == 'right':
\r
181 assert style == 'bottom', style
\r
183 self._c['manager'].AddPane(panel, info)
\r
185 def _setup_toolbars(self):
\r
186 self._c['navigation bar'] = navbar.NavBar(
\r
188 'next': self._next_curve,
\r
189 'previous': self._previous_curve,
\r
192 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
193 self._c['manager'].AddPane(
\r
194 self._c['navigation bar'],
\r
195 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
196 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
197 ).RightDockable(False))
\r
199 def _bind_events(self):
\r
200 # TODO: figure out if we can use the eventManager for menu
\r
201 # ranges and events of 'self' without raising an assertion
\r
203 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
204 self.Bind(wx.EVT_SIZE, self._on_size)
\r
205 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
206 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
207 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
209 return # TODO: cleanup
\r
210 for value in self._c['menu bar']._c['view']._c.values():
\r
211 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
213 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
214 self._c['menu bar']._c['perspective']._c['save'])
\r
215 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
216 self._c['menu bar']._c['perspective']._c['delete'])
\r
218 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
219 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
221 # TODO: playlist callbacks
\r
222 return # TODO: cleanup
\r
223 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
225 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
227 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
229 def _on_about(self, *args):
\r
230 dialog = wx.MessageDialog(
\r
232 message=self.gui._splash_text(),
\r
233 caption='About Hooke',
\r
234 style=wx.OK|wx.ICON_INFORMATION)
\r
238 def _on_close(self, *args):
\r
240 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
241 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
242 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
243 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
244 # push changes back to Hooke.config?
\r
245 self._c['manager'].UnInit()
\r
246 del self._c['manager']
\r
252 def _command_by_name(self, name):
\r
253 cs = [c for c in self.commands if c.name == name]
\r
255 raise KeyError(name)
\r
257 raise Exception('Multiple commands named "%s"' % name)
\r
260 def execute_command(self, _class=None, method=None,
\r
261 command=None, args=None):
\r
262 self.inqueue.put(CommandMessage(command, args))
\r
265 msg = self.outqueue.get()
\r
266 results.append(msg)
\r
267 print type(msg), msg
\r
268 if isinstance(msg, Exit):
\r
271 elif isinstance(msg, CommandExit):
\r
272 # TODO: display command complete
\r
274 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
275 self.gui.reload_config(msg.config)
\r
277 elif isinstance(msg, Request):
\r
278 h = handler.HANDLERS[msg.type]
\r
279 h.run(self, msg) # TODO: pause for response?
\r
282 self, '_postprocess_%s' % command.name.replace(' ', '_'), None)
\r
284 pp(command=command, results=results)
\r
287 def _handle_request(self, msg):
\r
288 """Repeatedly try to get a response to `msg`.
\r
291 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
292 prompt_string = prompt(msg)
\r
293 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
295 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
299 self.cmd.stdout.write(''.join([
\r
300 error.__class__.__name__, ': ', str(error), '\n']))
\r
301 self.cmd.stdout.write(prompt_string)
\r
302 value = parser(msg, self.cmd.stdin.readline())
\r
304 response = msg.response(value)
\r
306 except ValueError, error:
\r
308 self.inqueue.put(response)
\r
312 # Command-specific postprocessing
\r
314 def _postprocess_get_curve(self, command, results):
\r
315 """Update `self` to show the curve.
\r
317 if not isinstance(results[-1], Success):
\r
318 return # error executing 'get curve'
\r
319 assert len(results) == 2, results
\r
323 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
324 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
325 #GetFirstChild returns a tuple
\r
326 #we only need the first element
\r
327 next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]
\r
329 next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)
\r
330 if not next_item.IsOk():
\r
331 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)
\r
332 #GetFirstChild returns a tuple
\r
333 #we only need the first element
\r
334 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]
\r
335 self._c['playlists']._c['tree'].SelectItem(next_item, True)
\r
336 if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
337 playlist = self.GetActivePlaylist()
\r
338 if playlist.count > 1:
\r
340 self._c['status bar'].set_playlist(playlist)
\r
348 def _GetActiveFileIndex(self):
\r
349 lib.playlist.Playlist = self.GetActivePlaylist()
\r
350 #get the selected item from the tree
\r
351 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
352 #test if a playlist or a curve was double-clicked
\r
353 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
357 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
358 while selected_item.IsOk():
\r
360 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
363 def _GetPlaylistTab(self, name):
\r
364 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
365 if page.caption == name:
\r
369 def select_plugin(self, _class=None, method=None, plugin=None):
\r
370 for option in config[section]:
\r
371 properties.append([option, config[section][option]])
\r
373 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
375 playlist = lib.playlist.Playlist(self, self.drivers)
\r
377 playlist.add_curve(item)
\r
378 if playlist.count > 0:
\r
379 playlist.name = self._GetUniquePlaylistName(name)
\r
381 self.AddTayliss(playlist)
\r
383 def AppendToOutput(self, text):
\r
384 self.panelOutput.AppendText(''.join([text, '\n']))
\r
386 def AppliesPlotmanipulator(self, name):
\r
388 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
389 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
391 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
393 def ApplyPlotmanipulators(self, plot, plot_file):
\r
395 Apply all active plotmanipulators.
\r
397 if plot is not None and plot_file is not None:
\r
398 manipulated_plot = copy.deepcopy(plot)
\r
399 for plotmanipulator in self.plotmanipulators:
\r
400 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
401 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
402 return manipulated_plot
\r
404 def GetActiveFigure(self):
\r
405 playlist_name = self.GetActivePlaylistName()
\r
406 figure = self.playlists[playlist_name].figure
\r
407 if figure is not None:
\r
411 def GetActiveFile(self):
\r
412 playlist = self.GetActivePlaylist()
\r
413 if playlist is not None:
\r
414 return playlist.get_active_file()
\r
417 def GetActivePlot(self):
\r
418 playlist = self.GetActivePlaylist()
\r
419 if playlist is not None:
\r
420 return playlist.get_active_file().plot
\r
423 def GetDisplayedPlot(self):
\r
424 plot = copy.deepcopy(self.displayed_plot)
\r
426 #plot.curves = copy.deepcopy(plot.curves)
\r
429 def GetDisplayedPlotCorrected(self):
\r
430 plot = copy.deepcopy(self.displayed_plot)
\r
432 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
435 def GetDisplayedPlotRaw(self):
\r
436 plot = copy.deepcopy(self.displayed_plot)
\r
438 plot.curves = copy.deepcopy(plot.raw_curves)
\r
441 def GetDockArt(self):
\r
442 return self._c['manager'].GetArtProvider()
\r
444 def GetPlotmanipulator(self, name):
\r
446 Returns a plot manipulator function from its name
\r
448 for plotmanipulator in self.plotmanipulators:
\r
449 if plotmanipulator.name == name:
\r
450 return plotmanipulator
\r
453 def HasPlotmanipulator(self, name):
\r
455 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
457 for plotmanipulator in self.plotmanipulators:
\r
458 if plotmanipulator.command == name:
\r
463 def _on_dir_ctrl_left_double_click(self, event):
\r
464 file_path = self.panelFolders.GetPath()
\r
465 if os.path.isfile(file_path):
\r
466 if file_path.endswith('.hkp'):
\r
467 self.do_loadlist(file_path)
\r
470 def _on_erase_background(self, event):
\r
473 def _on_notebook_page_close(self, event):
\r
474 ctrl = event.GetEventObject()
\r
475 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
476 self.DeleteFromPlaylists(playlist_name)
\r
478 def OnPaneClose(self, event):
\r
481 def OnPropGridChanged (self, event):
\r
482 prop = event.GetProperty()
\r
484 item_section = self.panelProperties.SelectedTreeItem
\r
485 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
486 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
487 config = self.gui.config[plugin]
\r
488 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
489 property_key = prop.GetName()
\r
490 property_value = prop.GetDisplayedString()
\r
492 config[property_section][property_key]['value'] = property_value
\r
494 def OnResultsCheck(self, index, flag):
\r
495 results = self.GetActivePlot().results
\r
496 if results.has_key(self.results_str):
\r
497 results[self.results_str].results[index].visible = flag
\r
498 results[self.results_str].update()
\r
502 def _on_size(self, event):
\r
505 def OnUpdateNote(self, event):
\r
507 Saves the note to the active file.
\r
509 active_file = self.GetActiveFile()
\r
510 active_file.note = self.panelNote.Editor.GetValue()
\r
512 def UpdateNote(self):
\r
513 #update the note for the active file
\r
514 active_file = self.GetActiveFile()
\r
515 if active_file is not None:
\r
516 self.panelNote.Editor.SetValue(active_file.note)
\r
518 def UpdatePlaylistsTreeSelection(self):
\r
519 playlist = self.GetActivePlaylist()
\r
520 if playlist is not None:
\r
521 if playlist.index >= 0:
\r
522 self._c['status bar'].set_playlist(playlist)
\r
526 def UpdatePlot(self, plot=None):
\r
528 def add_to_plot(curve, set_scale=True):
\r
529 if curve.visible and curve.x and curve.y:
\r
530 #get the index of the subplot to use as destination
\r
531 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
532 #set all parameters for the plot
\r
533 axes_list[destination].set_title(curve.title)
\r
535 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
536 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
537 #set the formatting details for the scale
\r
538 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
539 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
540 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
541 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
542 if curve.style == 'plot':
\r
543 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
544 if curve.style == 'scatter':
\r
545 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
546 #add the legend if necessary
\r
548 axes_list[destination].legend()
\r
551 active_file = self.GetActiveFile()
\r
552 if not active_file.driver:
\r
553 #the first time we identify a file, the following need to be set
\r
554 active_file.identify(self.drivers)
\r
555 for curve in active_file.plot.curves:
\r
556 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
557 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
558 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
559 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
560 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
561 if active_file.driver is None:
\r
562 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
564 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
565 #add raw curves to plot
\r
566 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
567 #apply all active plotmanipulators
\r
568 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
569 #add corrected curves to plot
\r
570 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
573 self.displayed_plot = copy.deepcopy(plot)
\r
575 figure = self.GetActiveFigure()
\r
578 #use '0' instead of e.g. '0.00' for scales
\r
579 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
580 #optionally remove the extension from the title of the plot
\r
581 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
582 if hide_curve_extension:
\r
583 title = lh.remove_extension(self.displayed_plot.title)
\r
585 title = self.displayed_plot.title
\r
586 figure.suptitle(title, fontsize=14)
\r
587 #create the list of all axes necessary (rows and columns)
\r
589 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
590 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
591 for index in range(number_of_rows * number_of_columns):
\r
592 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
594 #add all curves to the corresponding plots
\r
595 for curve in self.displayed_plot.curves:
\r
598 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
599 figure.subplots_adjust(hspace=0.3)
\r
602 self.panelResults.ClearResults()
\r
603 if self.displayed_plot.results.has_key(self.results_str):
\r
604 for curve in self.displayed_plot.results[self.results_str].results:
\r
605 add_to_plot(curve, set_scale=False)
\r
606 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
608 self.panelResults.ClearResults()
\r
610 figure.canvas.draw()
\r
612 def _on_curve_select(self, playlist, curve):
\r
613 #create the plot tab and add playlist to the dictionary
\r
614 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
615 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
616 #tab_index = self._c['notebook'].GetSelection()
\r
617 playlist.figure = plotPanel.get_figure()
\r
618 self.playlists[playlist.name] = playlist
\r
619 #self.playlists[playlist.name] = [playlist, figure]
\r
620 self._c['status bar'].set_playlist(playlist)
\r
625 def _on_playlist_left_doubleclick(self):
\r
626 index = self._c['notebook'].GetSelection()
\r
627 current_playlist = self._c['notebook'].GetPageText(index)
\r
628 if current_playlist != playlist_name:
\r
629 index = self._GetPlaylistTab(playlist_name)
\r
630 self._c['notebook'].SetSelection(index)
\r
631 self._c['status bar'].set_playlist(playlist)
\r
635 def _on_playlist_delete(self, playlist):
\r
636 notebook = self.Parent.plotNotebook
\r
637 index = self.Parent._GetPlaylistTab(playlist.name)
\r
638 notebook.SetSelection(index)
\r
639 notebook.DeletePage(notebook.GetSelection())
\r
640 self.Parent.DeleteFromPlaylists(playlist_name)
\r
644 # Command panel interface
\r
646 def select_command(self, _class, method, command):
\r
647 self.select_plugin(plugin=command.plugin)
\r
648 plugin = self.GetItemText(selected_item)
\r
649 if plugin != 'core':
\r
650 doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')
\r
652 doc_string = 'The module "core" contains Hooke core functionality'
\r
653 if doc_string is not None:
\r
654 self.panelAssistant.ChangeValue(doc_string)
\r
656 self.panelAssistant.ChangeValue('')
\r
657 panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)
\r
658 self.gui.config['selected command'] = command
\r
664 def _next_curve(self, *args):
\r
665 """Call the `next curve` command.
\r
667 results = self.execute_command(
\r
668 command=self._command_by_name('next curve'))
\r
669 if isinstance(results[-1], Success):
\r
670 self.execute_command(
\r
671 command=self._command_by_name('get curve'))
\r
673 def _previous_curve(self, *args):
\r
674 """Call the `previous curve` command.
\r
676 self.execute_command(
\r
677 command=self._command_by_name('previous curve'))
\r
678 if isinstance(results[-1], Success):
\r
679 self.execute_command(
\r
680 command=self._command_by_name('get curve'))
\r
684 # Panel display handling
\r
686 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
687 pane = self._c['manager'].GetPane(panel_name)
\r
690 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
691 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
692 #folders_size = pane.GetSize()
\r
693 self.panelFolders.Fit()
\r
694 self._c['manager'].Update()
\r
696 def _setup_perspectives(self):
\r
697 """Add perspectives to menubar and _perspectives.
\r
699 self._perspectives = {
\r
700 'Default': self._c['manager'].SavePerspective(),
\r
702 path = self.gui.config['perspective path']
\r
703 if os.path.isdir(path):
\r
704 files = sorted(os.listdir(path))
\r
705 for fname in files:
\r
706 name, extension = os.path.splitext(fname)
\r
707 if extension != self.gui.config['perspective extension']:
\r
709 fpath = os.path.join(path, fname)
\r
710 if not os.path.isfile(fpath):
\r
713 with open(fpath, 'rU') as f:
\r
714 perspective = f.readline()
\r
716 self._perspectives[name] = perspective
\r
718 selected_perspective = self.gui.config['active perspective']
\r
719 if not self._perspectives.has_key(selected_perspective):
\r
720 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
722 self._restore_perspective(selected_perspective)
\r
723 self._update_perspective_menu()
\r
725 def _update_perspective_menu(self):
\r
726 self._c['menu bar']._c['perspective'].update(
\r
727 sorted(self._perspectives.keys()),
\r
728 self.gui.config['active perspective'])
\r
730 def _save_perspective(self, perspective, perspective_dir, name,
\r
732 path = os.path.join(perspective_dir, name)
\r
733 if extension != None:
\r
735 if not os.path.isdir(perspective_dir):
\r
736 os.makedirs(perspective_dir)
\r
737 with open(path, 'w') as f:
\r
738 f.write(perspective)
\r
739 self._perspectives[name] = perspective
\r
740 self._restore_perspective(name)
\r
741 self._update_perspective_menu()
\r
743 def _delete_perspectives(self, perspective_dir, names,
\r
747 path = os.path.join(perspective_dir, name)
\r
748 if extension != None:
\r
751 del(self._perspectives[name])
\r
752 self._update_perspective_menu()
\r
753 if self.gui.config['active perspective'] in names:
\r
754 self._restore_perspective('Default')
\r
755 # TODO: does this bug still apply?
\r
756 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
757 # http://trac.wxwidgets.org/ticket/3258
\r
758 # ) that makes the radio item indicator in the menu disappear.
\r
759 # The code should be fine once this issue is fixed.
\r
761 def _restore_perspective(self, name):
\r
762 if name != self.gui.config['active perspective']:
\r
763 print 'restoring perspective:', name
\r
764 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
765 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
766 self._c['manager'].Update()
\r
767 for pane in self._c['manager'].GetAllPanes():
\r
768 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
769 pane.Check(pane.window.IsShown())
\r
771 def _on_save_perspective(self, *args):
\r
772 perspective = self._c['manager'].SavePerspective()
\r
773 name = self.gui.config['active perspective']
\r
774 if name == 'Default':
\r
775 name = 'New perspective'
\r
776 name = select_save_file(
\r
777 directory=self.gui.config['perspective path'],
\r
779 extension=self.gui.config['perspective extension'],
\r
781 message='Enter a name for the new perspective:',
\r
782 caption='Save perspective')
\r
785 self._save_perspective(
\r
786 perspective, self.gui.config['perspective path'], name=name,
\r
787 extension=self.gui.config['perspective extension'])
\r
789 def _on_delete_perspective(self, *args, **kwargs):
\r
790 options = sorted([p for p in self._perspectives.keys()
\r
791 if p != 'Default'])
\r
792 dialog = SelectionDialog(
\r
794 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
795 button_id=wx.ID_DELETE,
\r
796 selection_style='multiple',
\r
798 title='Delete perspective(s)',
\r
799 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
800 dialog.CenterOnScreen()
\r
802 names = [options[i] for i in dialog.selected]
\r
804 self._delete_perspectives(
\r
805 self.gui.config['perspective path'], names=names,
\r
806 extension=self.gui.config['perspective extension'])
\r
808 def _on_select_perspective(self, _class, method, name):
\r
809 self._restore_perspective(name)
\r
813 class HookeApp (wx.App):
\r
814 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
816 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
819 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
821 self.commands = commands
\r
822 self.inqueue = inqueue
\r
823 self.outqueue = outqueue
\r
824 super(HookeApp, self).__init__(*args, **kwargs)
\r
827 self.SetAppName('Hooke')
\r
828 self.SetVendorName('')
\r
829 self._setup_splash_screen()
\r
831 height = int(self.gui.config['main height']) # HACK: config should convert
\r
832 width = int(self.gui.config['main width'])
\r
833 top = int(self.gui.config['main top'])
\r
834 left = int(self.gui.config['main left'])
\r
836 # Sometimes, the ini file gets confused and sets 'left' and
\r
837 # 'top' to large negative numbers. Here we catch and fix
\r
838 # this. Keep small negative numbers, the user might want
\r
846 'frame': HookeFrame(
\r
847 self.gui, self.commands, self.inqueue, self.outqueue,
\r
848 parent=None, title='Hooke',
\r
849 pos=(left, top), size=(width, height),
\r
850 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
852 self._c['frame'].Show(True)
\r
853 self.SetTopWindow(self._c['frame'])
\r
856 def _setup_splash_screen(self):
\r
857 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
858 print 'splash', self.gui.config['show splash screen']
\r
859 path = self.gui.config['splash screen image']
\r
860 if os.path.isfile(path):
\r
861 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
863 bitmap=wx.Image(path).ConvertToBitmap(),
\r
864 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
865 milliseconds=duration,
\r
868 # For some reason splashDuration and sleep do not
\r
869 # correspond to each other at least not on Windows.
\r
870 # Maybe it's because duration is in milliseconds and
\r
871 # sleep in seconds. Thus we need to increase the
\r
872 # sleep time a bit. A factor of 1.2 seems to work.
\r
874 time.sleep(sleepFactor * duration / 1000)
\r
877 class GUI (UserInterface):
\r
878 """wxWindows graphical user interface.
\r
880 def __init__(self):
\r
881 super(GUI, self).__init__(name='gui')
\r
883 def default_settings(self):
\r
884 """Return a list of :class:`hooke.config.Setting`\s for any
\r
885 configurable UI settings.
\r
887 The suggested section setting is::
\r
889 Setting(section=self.setting_section, help=self.__doc__)
\r
892 Setting(section=self.setting_section, help=self.__doc__),
\r
893 Setting(section=self.setting_section, option='icon image',
\r
894 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
895 help='Path to the hooke icon image.'),
\r
896 Setting(section=self.setting_section, option='show splash screen',
\r
898 help='Enable/disable the splash screen'),
\r
899 Setting(section=self.setting_section, option='splash screen image',
\r
900 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
901 help='Path to the Hooke splash screen image.'),
\r
902 Setting(section=self.setting_section, option='splash screen duration',
\r
904 help='Duration of the splash screen in milliseconds.'),
\r
905 Setting(section=self.setting_section, option='perspective path',
\r
906 value=os.path.join('resources', 'gui', 'perspective'),
\r
907 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
908 Setting(section=self.setting_section, option='perspective extension',
\r
910 help='Extension for perspective files.'),
\r
911 Setting(section=self.setting_section, option='hide extensions',
\r
913 help='Hide file extensions when displaying names.'),
\r
914 Setting(section=self.setting_section, option='folders-workdir',
\r
916 help='This should probably go...'),
\r
917 Setting(section=self.setting_section, option='folders-filters',
\r
919 help='This should probably go...'),
\r
920 Setting(section=self.setting_section, option='active perspective',
\r
922 help='Name of active perspective file (or "Default").'),
\r
923 Setting(section=self.setting_section, option='folders-filter-index',
\r
925 help='This should probably go...'),
\r
926 Setting(section=self.setting_section, option='main height',
\r
928 help='Height of main window in pixels.'),
\r
929 Setting(section=self.setting_section, option='main width',
\r
931 help='Width of main window in pixels.'),
\r
932 Setting(section=self.setting_section, option='main top',
\r
934 help='Pixels from screen top to top of main window.'),
\r
935 Setting(section=self.setting_section, option='main left',
\r
937 help='Pixels from screen left to left of main window.'),
\r
938 Setting(section=self.setting_section, option='selected command',
\r
939 value='load playlist',
\r
940 help='Name of the initially selected command.'),
\r
943 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
947 app = HookeApp(gui=self,
\r
949 inqueue=ui_to_command_queue,
\r
950 outqueue=command_to_ui_queue,
\r
954 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
955 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r