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
385 for option in config[section]:
\r
386 properties.append([option, config[section][option]])
\r
388 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
390 playlist = lib.playlist.Playlist(self, self.drivers)
\r
392 playlist.add_curve(item)
\r
393 if playlist.count > 0:
\r
394 playlist.name = self._GetUniquePlaylistName(name)
\r
396 self.AddTayliss(playlist)
\r
398 def AppliesPlotmanipulator(self, name):
\r
400 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
401 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
403 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
405 def ApplyPlotmanipulators(self, plot, plot_file):
\r
407 Apply all active plotmanipulators.
\r
409 if plot is not None and plot_file is not None:
\r
410 manipulated_plot = copy.deepcopy(plot)
\r
411 for plotmanipulator in self.plotmanipulators:
\r
412 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
413 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
414 return manipulated_plot
\r
416 def GetActiveFigure(self):
\r
417 playlist_name = self.GetActivePlaylistName()
\r
418 figure = self.playlists[playlist_name].figure
\r
419 if figure is not None:
\r
423 def GetActiveFile(self):
\r
424 playlist = self.GetActivePlaylist()
\r
425 if playlist is not None:
\r
426 return playlist.get_active_file()
\r
429 def GetActivePlot(self):
\r
430 playlist = self.GetActivePlaylist()
\r
431 if playlist is not None:
\r
432 return playlist.get_active_file().plot
\r
435 def GetDisplayedPlot(self):
\r
436 plot = copy.deepcopy(self.displayed_plot)
\r
438 #plot.curves = copy.deepcopy(plot.curves)
\r
441 def GetDisplayedPlotCorrected(self):
\r
442 plot = copy.deepcopy(self.displayed_plot)
\r
444 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
447 def GetDisplayedPlotRaw(self):
\r
448 plot = copy.deepcopy(self.displayed_plot)
\r
450 plot.curves = copy.deepcopy(plot.raw_curves)
\r
453 def GetDockArt(self):
\r
454 return self._c['manager'].GetArtProvider()
\r
456 def GetPlotmanipulator(self, name):
\r
458 Returns a plot manipulator function from its name
\r
460 for plotmanipulator in self.plotmanipulators:
\r
461 if plotmanipulator.name == name:
\r
462 return plotmanipulator
\r
465 def HasPlotmanipulator(self, name):
\r
467 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
469 for plotmanipulator in self.plotmanipulators:
\r
470 if plotmanipulator.command == name:
\r
475 def _on_dir_ctrl_left_double_click(self, event):
\r
476 file_path = self.panelFolders.GetPath()
\r
477 if os.path.isfile(file_path):
\r
478 if file_path.endswith('.hkp'):
\r
479 self.do_loadlist(file_path)
\r
482 def _on_erase_background(self, event):
\r
485 def _on_notebook_page_close(self, event):
\r
486 ctrl = event.GetEventObject()
\r
487 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
488 self.DeleteFromPlaylists(playlist_name)
\r
490 def OnPaneClose(self, event):
\r
493 def OnPropGridChanged (self, event):
\r
494 prop = event.GetProperty()
\r
496 item_section = self.panelProperties.SelectedTreeItem
\r
497 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
498 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
499 config = self.gui.config[plugin]
\r
500 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
501 property_key = prop.GetName()
\r
502 property_value = prop.GetDisplayedString()
\r
504 config[property_section][property_key]['value'] = property_value
\r
506 def OnResultsCheck(self, index, flag):
\r
507 results = self.GetActivePlot().results
\r
508 if results.has_key(self.results_str):
\r
509 results[self.results_str].results[index].visible = flag
\r
510 results[self.results_str].update()
\r
514 def _on_size(self, event):
\r
517 def OnUpdateNote(self, event):
\r
519 Saves the note to the active file.
\r
521 active_file = self.GetActiveFile()
\r
522 active_file.note = self.panelNote.Editor.GetValue()
\r
524 def UpdateNote(self):
\r
525 #update the note for the active file
\r
526 active_file = self.GetActiveFile()
\r
527 if active_file is not None:
\r
528 self.panelNote.Editor.SetValue(active_file.note)
\r
530 def UpdatePlaylistsTreeSelection(self):
\r
531 playlist = self.GetActivePlaylist()
\r
532 if playlist is not None:
\r
533 if playlist.index >= 0:
\r
534 self._c['status bar'].set_playlist(playlist)
\r
538 def UpdatePlot(self, plot=None):
\r
540 def add_to_plot(curve, set_scale=True):
\r
541 if curve.visible and curve.x and curve.y:
\r
542 #get the index of the subplot to use as destination
\r
543 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
544 #set all parameters for the plot
\r
545 axes_list[destination].set_title(curve.title)
\r
547 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
548 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
549 #set the formatting details for the scale
\r
550 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
551 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
552 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
553 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
554 if curve.style == 'plot':
\r
555 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
556 if curve.style == 'scatter':
\r
557 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
558 #add the legend if necessary
\r
560 axes_list[destination].legend()
\r
563 active_file = self.GetActiveFile()
\r
564 if not active_file.driver:
\r
565 #the first time we identify a file, the following need to be set
\r
566 active_file.identify(self.drivers)
\r
567 for curve in active_file.plot.curves:
\r
568 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
569 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
570 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
571 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
572 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
573 if active_file.driver is None:
\r
574 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
576 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
577 #add raw curves to plot
\r
578 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
579 #apply all active plotmanipulators
\r
580 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
581 #add corrected curves to plot
\r
582 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
585 self.displayed_plot = copy.deepcopy(plot)
\r
587 figure = self.GetActiveFigure()
\r
590 #use '0' instead of e.g. '0.00' for scales
\r
591 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
592 #optionally remove the extension from the title of the plot
\r
593 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
594 if hide_curve_extension:
\r
595 title = lh.remove_extension(self.displayed_plot.title)
\r
597 title = self.displayed_plot.title
\r
598 figure.suptitle(title, fontsize=14)
\r
599 #create the list of all axes necessary (rows and columns)
\r
601 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
602 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
603 for index in range(number_of_rows * number_of_columns):
\r
604 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
606 #add all curves to the corresponding plots
\r
607 for curve in self.displayed_plot.curves:
\r
610 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
611 figure.subplots_adjust(hspace=0.3)
\r
614 self.panelResults.ClearResults()
\r
615 if self.displayed_plot.results.has_key(self.results_str):
\r
616 for curve in self.displayed_plot.results[self.results_str].results:
\r
617 add_to_plot(curve, set_scale=False)
\r
618 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
620 self.panelResults.ClearResults()
\r
622 figure.canvas.draw()
\r
624 def _on_curve_select(self, playlist, curve):
\r
625 #create the plot tab and add playlist to the dictionary
\r
626 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
627 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
628 #tab_index = self._c['notebook'].GetSelection()
\r
629 playlist.figure = plotPanel.get_figure()
\r
630 self.playlists[playlist.name] = playlist
\r
631 #self.playlists[playlist.name] = [playlist, figure]
\r
632 self._c['status bar'].set_playlist(playlist)
\r
637 def _on_playlist_left_doubleclick(self):
\r
638 index = self._c['notebook'].GetSelection()
\r
639 current_playlist = self._c['notebook'].GetPageText(index)
\r
640 if current_playlist != playlist_name:
\r
641 index = self._GetPlaylistTab(playlist_name)
\r
642 self._c['notebook'].SetSelection(index)
\r
643 self._c['status bar'].set_playlist(playlist)
\r
647 def _on_playlist_delete(self, playlist):
\r
648 notebook = self.Parent.plotNotebook
\r
649 index = self.Parent._GetPlaylistTab(playlist.name)
\r
650 notebook.SetSelection(index)
\r
651 notebook.DeletePage(notebook.GetSelection())
\r
652 self.Parent.DeleteFromPlaylists(playlist_name)
\r
656 # Command panel interface
\r
658 def select_command(self, _class, method, command):
\r
659 self.select_plugin(plugin=command.plugin)
\r
660 plugin = self.GetItemText(selected_item)
\r
661 if plugin != 'core':
\r
662 doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')
\r
664 doc_string = 'The module "core" contains Hooke core functionality'
\r
665 if doc_string is not None:
\r
666 self.panelAssistant.ChangeValue(doc_string)
\r
668 self.panelAssistant.ChangeValue('')
\r
669 panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)
\r
670 self.gui.config['selected command'] = command
\r
676 def _next_curve(self, *args):
\r
677 """Call the `next curve` command.
\r
679 results = self.execute_command(
\r
680 command=self._command_by_name('next curve'))
\r
681 if isinstance(results[-1], Success):
\r
682 self.execute_command(
\r
683 command=self._command_by_name('get curve'))
\r
685 def _previous_curve(self, *args):
\r
686 """Call the `previous curve` command.
\r
688 self.execute_command(
\r
689 command=self._command_by_name('previous curve'))
\r
690 if isinstance(results[-1], Success):
\r
691 self.execute_command(
\r
692 command=self._command_by_name('get curve'))
\r
696 # Panel display handling
\r
698 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
699 pane = self._c['manager'].GetPane(panel_name)
\r
702 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
703 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
704 #folders_size = pane.GetSize()
\r
705 self.panelFolders.Fit()
\r
706 self._c['manager'].Update()
\r
708 def _setup_perspectives(self):
\r
709 """Add perspectives to menubar and _perspectives.
\r
711 self._perspectives = {
\r
712 'Default': self._c['manager'].SavePerspective(),
\r
714 path = self.gui.config['perspective path']
\r
715 if os.path.isdir(path):
\r
716 files = sorted(os.listdir(path))
\r
717 for fname in files:
\r
718 name, extension = os.path.splitext(fname)
\r
719 if extension != self.gui.config['perspective extension']:
\r
721 fpath = os.path.join(path, fname)
\r
722 if not os.path.isfile(fpath):
\r
725 with open(fpath, 'rU') as f:
\r
726 perspective = f.readline()
\r
728 self._perspectives[name] = perspective
\r
730 selected_perspective = self.gui.config['active perspective']
\r
731 if not self._perspectives.has_key(selected_perspective):
\r
732 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
734 self._restore_perspective(selected_perspective)
\r
735 self._update_perspective_menu()
\r
737 def _update_perspective_menu(self):
\r
738 self._c['menu bar']._c['perspective'].update(
\r
739 sorted(self._perspectives.keys()),
\r
740 self.gui.config['active perspective'])
\r
742 def _save_perspective(self, perspective, perspective_dir, name,
\r
744 path = os.path.join(perspective_dir, name)
\r
745 if extension != None:
\r
747 if not os.path.isdir(perspective_dir):
\r
748 os.makedirs(perspective_dir)
\r
749 with open(path, 'w') as f:
\r
750 f.write(perspective)
\r
751 self._perspectives[name] = perspective
\r
752 self._restore_perspective(name)
\r
753 self._update_perspective_menu()
\r
755 def _delete_perspectives(self, perspective_dir, names,
\r
759 path = os.path.join(perspective_dir, name)
\r
760 if extension != None:
\r
763 del(self._perspectives[name])
\r
764 self._update_perspective_menu()
\r
765 if self.gui.config['active perspective'] in names:
\r
766 self._restore_perspective('Default')
\r
767 # TODO: does this bug still apply?
\r
768 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
769 # http://trac.wxwidgets.org/ticket/3258
\r
770 # ) that makes the radio item indicator in the menu disappear.
\r
771 # The code should be fine once this issue is fixed.
\r
773 def _restore_perspective(self, name):
\r
774 if name != self.gui.config['active perspective']:
\r
775 print 'restoring perspective:', name
\r
776 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
777 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
778 self._c['manager'].Update()
\r
779 for pane in self._c['manager'].GetAllPanes():
\r
780 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
781 pane.Check(pane.window.IsShown())
\r
783 def _on_save_perspective(self, *args):
\r
784 perspective = self._c['manager'].SavePerspective()
\r
785 name = self.gui.config['active perspective']
\r
786 if name == 'Default':
\r
787 name = 'New perspective'
\r
788 name = select_save_file(
\r
789 directory=self.gui.config['perspective path'],
\r
791 extension=self.gui.config['perspective extension'],
\r
793 message='Enter a name for the new perspective:',
\r
794 caption='Save perspective')
\r
797 self._save_perspective(
\r
798 perspective, self.gui.config['perspective path'], name=name,
\r
799 extension=self.gui.config['perspective extension'])
\r
801 def _on_delete_perspective(self, *args, **kwargs):
\r
802 options = sorted([p for p in self._perspectives.keys()
\r
803 if p != 'Default'])
\r
804 dialog = SelectionDialog(
\r
806 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
807 button_id=wx.ID_DELETE,
\r
808 selection_style='multiple',
\r
810 title='Delete perspective(s)',
\r
811 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
812 dialog.CenterOnScreen()
\r
814 names = [options[i] for i in dialog.selected]
\r
816 self._delete_perspectives(
\r
817 self.gui.config['perspective path'], names=names,
\r
818 extension=self.gui.config['perspective extension'])
\r
820 def _on_select_perspective(self, _class, method, name):
\r
821 self._restore_perspective(name)
\r
825 class HookeApp (wx.App):
\r
826 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
828 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
831 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
833 self.commands = commands
\r
834 self.inqueue = inqueue
\r
835 self.outqueue = outqueue
\r
836 super(HookeApp, self).__init__(*args, **kwargs)
\r
839 self.SetAppName('Hooke')
\r
840 self.SetVendorName('')
\r
841 self._setup_splash_screen()
\r
843 height = int(self.gui.config['main height']) # HACK: config should convert
\r
844 width = int(self.gui.config['main width'])
\r
845 top = int(self.gui.config['main top'])
\r
846 left = int(self.gui.config['main left'])
\r
848 # Sometimes, the ini file gets confused and sets 'left' and
\r
849 # 'top' to large negative numbers. Here we catch and fix
\r
850 # this. Keep small negative numbers, the user might want
\r
858 'frame': HookeFrame(
\r
859 self.gui, self.commands, self.inqueue, self.outqueue,
\r
860 parent=None, title='Hooke',
\r
861 pos=(left, top), size=(width, height),
\r
862 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
864 self._c['frame'].Show(True)
\r
865 self.SetTopWindow(self._c['frame'])
\r
868 def _setup_splash_screen(self):
\r
869 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
870 print 'splash', self.gui.config['show splash screen']
\r
871 path = self.gui.config['splash screen image']
\r
872 if os.path.isfile(path):
\r
873 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
875 bitmap=wx.Image(path).ConvertToBitmap(),
\r
876 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
877 milliseconds=duration,
\r
880 # For some reason splashDuration and sleep do not
\r
881 # correspond to each other at least not on Windows.
\r
882 # Maybe it's because duration is in milliseconds and
\r
883 # sleep in seconds. Thus we need to increase the
\r
884 # sleep time a bit. A factor of 1.2 seems to work.
\r
886 time.sleep(sleepFactor * duration / 1000)
\r
889 class GUI (UserInterface):
\r
890 """wxWindows graphical user interface.
\r
892 def __init__(self):
\r
893 super(GUI, self).__init__(name='gui')
\r
895 def default_settings(self):
\r
896 """Return a list of :class:`hooke.config.Setting`\s for any
\r
897 configurable UI settings.
\r
899 The suggested section setting is::
\r
901 Setting(section=self.setting_section, help=self.__doc__)
\r
904 Setting(section=self.setting_section, help=self.__doc__),
\r
905 Setting(section=self.setting_section, option='icon image',
\r
906 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
907 help='Path to the hooke icon image.'),
\r
908 Setting(section=self.setting_section, option='show splash screen',
\r
910 help='Enable/disable the splash screen'),
\r
911 Setting(section=self.setting_section, option='splash screen image',
\r
912 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
913 help='Path to the Hooke splash screen image.'),
\r
914 Setting(section=self.setting_section, option='splash screen duration',
\r
916 help='Duration of the splash screen in milliseconds.'),
\r
917 Setting(section=self.setting_section, option='perspective path',
\r
918 value=os.path.join('resources', 'gui', 'perspective'),
\r
919 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
920 Setting(section=self.setting_section, option='perspective extension',
\r
922 help='Extension for perspective files.'),
\r
923 Setting(section=self.setting_section, option='hide extensions',
\r
925 help='Hide file extensions when displaying names.'),
\r
926 Setting(section=self.setting_section, option='folders-workdir',
\r
928 help='This should probably go...'),
\r
929 Setting(section=self.setting_section, option='folders-filters',
\r
931 help='This should probably go...'),
\r
932 Setting(section=self.setting_section, option='active perspective',
\r
934 help='Name of active perspective file (or "Default").'),
\r
935 Setting(section=self.setting_section, option='folders-filter-index',
\r
937 help='This should probably go...'),
\r
938 Setting(section=self.setting_section, option='main height',
\r
940 help='Height of main window in pixels.'),
\r
941 Setting(section=self.setting_section, option='main width',
\r
943 help='Width of main window in pixels.'),
\r
944 Setting(section=self.setting_section, option='main top',
\r
946 help='Pixels from screen top to top of main window.'),
\r
947 Setting(section=self.setting_section, option='main left',
\r
949 help='Pixels from screen left to left of main window.'),
\r
950 Setting(section=self.setting_section, option='selected command',
\r
951 value='load playlist',
\r
952 help='Name of the initially selected command.'),
\r
955 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
959 app = HookeApp(gui=self,
\r
961 inqueue=ui_to_command_queue,
\r
962 outqueue=command_to_ui_queue,
\r
966 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
967 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r