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
159 pos=wx.Point(0, 0),
\r
160 size=wx.Size(150, 90),
\r
161 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
163 # ('results', panel.results.Results(self), 'bottom'),
\r
165 self._add_panel(label, p, style)
\r
166 #self._c['assistant'].SetEditable(False)
\r
168 def _add_panel(self, label, panel, style):
\r
169 self._c[label] = panel
\r
170 cap_label = label.capitalize()
\r
171 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
172 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
175 elif style == 'center':
\r
177 elif style == 'left':
\r
179 elif style == 'right':
\r
182 assert style == 'bottom', style
\r
184 self._c['manager'].AddPane(panel, info)
\r
186 def _setup_toolbars(self):
\r
187 self._c['navigation bar'] = navbar.NavBar(
\r
189 'next': self._next_curve,
\r
190 'previous': self._previous_curve,
\r
193 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
194 self._c['manager'].AddPane(
\r
195 self._c['navigation bar'],
\r
196 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
197 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
198 ).RightDockable(False))
\r
200 def _bind_events(self):
\r
201 # TODO: figure out if we can use the eventManager for menu
\r
202 # ranges and events of 'self' without raising an assertion
\r
204 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
205 self.Bind(wx.EVT_SIZE, self._on_size)
\r
206 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
207 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
208 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
210 return # TODO: cleanup
\r
211 for value in self._c['menu bar']._c['view']._c.values():
\r
212 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
214 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
215 self._c['menu bar']._c['perspective']._c['save'])
\r
216 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
217 self._c['menu bar']._c['perspective']._c['delete'])
\r
219 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
220 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
222 # TODO: playlist callbacks
\r
223 return # TODO: cleanup
\r
224 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
226 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
228 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
230 def _on_about(self, *args):
\r
231 dialog = wx.MessageDialog(
\r
233 message=self.gui._splash_text(),
\r
234 caption='About Hooke',
\r
235 style=wx.OK|wx.ICON_INFORMATION)
\r
239 def _on_close(self, *args):
\r
241 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
242 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
243 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
244 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
245 # push changes back to Hooke.config?
\r
246 self._c['manager'].UnInit()
\r
247 del self._c['manager']
\r
254 def _command_by_name(self, name):
\r
255 cs = [c for c in self.commands if c.name == name]
\r
257 raise KeyError(name)
\r
259 raise Exception('Multiple commands named "%s"' % name)
\r
262 def execute_command(self, _class=None, method=None,
\r
263 command=None, args=None):
\r
264 self.inqueue.put(CommandMessage(command, args))
\r
267 msg = self.outqueue.get()
\r
268 results.append(msg)
\r
269 if isinstance(msg, Exit):
\r
272 elif isinstance(msg, CommandExit):
\r
273 # TODO: display command complete
\r
275 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
276 self.gui.reload_config(msg.config)
\r
278 elif isinstance(msg, Request):
\r
279 h = handler.HANDLERS[msg.type]
\r
280 h.run(self, msg) # TODO: pause for response?
\r
283 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
284 self._postprocess_text)
\r
285 pp(command=command, results=results)
\r
288 def _handle_request(self, msg):
\r
289 """Repeatedly try to get a response to `msg`.
\r
292 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
293 prompt_string = prompt(msg)
\r
294 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
296 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
300 self.cmd.stdout.write(''.join([
\r
301 error.__class__.__name__, ': ', str(error), '\n']))
\r
302 self.cmd.stdout.write(prompt_string)
\r
303 value = parser(msg, self.cmd.stdin.readline())
\r
305 response = msg.response(value)
\r
307 except ValueError, error:
\r
309 self.inqueue.put(response)
\r
313 # Command-specific postprocessing
\r
315 def _postprocess_text(self, command, results):
\r
316 """Print the string representation of the results to the Results window.
\r
318 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
319 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
320 doesn't print some internally handled messages
\r
321 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
323 for result in results:
\r
324 if isinstance(result, CommandExit):
\r
325 self._c['output'].write(result.__class__.__name__+'\n')
\r
326 self._c['output'].write(str(result).rstrip()+'\n')
\r
328 def _postprocess_get_curve(self, command, results):
\r
329 """Update `self` to show the curve.
\r
331 if not isinstance(results[-1], Success):
\r
332 return # error executing 'get curve'
\r
333 assert len(results) == 2, results
\r
337 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
338 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
339 #GetFirstChild returns a tuple
\r
340 #we only need the first element
\r
341 next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]
\r
343 next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)
\r
344 if not next_item.IsOk():
\r
345 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)
\r
346 #GetFirstChild returns a tuple
\r
347 #we only need the first element
\r
348 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]
\r
349 self._c['playlists']._c['tree'].SelectItem(next_item, True)
\r
350 if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
351 playlist = self.GetActivePlaylist()
\r
352 if playlist.count > 1:
\r
354 self._c['status bar'].set_playlist(playlist)
\r
362 def _GetActiveFileIndex(self):
\r
363 lib.playlist.Playlist = self.GetActivePlaylist()
\r
364 #get the selected item from the tree
\r
365 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
366 #test if a playlist or a curve was double-clicked
\r
367 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
371 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
372 while selected_item.IsOk():
\r
374 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
377 def _GetPlaylistTab(self, name):
\r
378 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
379 if page.caption == name:
\r
383 def select_plugin(self, _class=None, method=None, plugin=None):
\r
386 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
388 playlist = lib.playlist.Playlist(self, self.drivers)
\r
390 playlist.add_curve(item)
\r
391 if playlist.count > 0:
\r
392 playlist.name = self._GetUniquePlaylistName(name)
\r
394 self.AddTayliss(playlist)
\r
396 def AppliesPlotmanipulator(self, name):
\r
398 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
399 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
401 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
403 def ApplyPlotmanipulators(self, plot, plot_file):
\r
405 Apply all active plotmanipulators.
\r
407 if plot is not None and plot_file is not None:
\r
408 manipulated_plot = copy.deepcopy(plot)
\r
409 for plotmanipulator in self.plotmanipulators:
\r
410 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
411 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
412 return manipulated_plot
\r
414 def GetActiveFigure(self):
\r
415 playlist_name = self.GetActivePlaylistName()
\r
416 figure = self.playlists[playlist_name].figure
\r
417 if figure is not None:
\r
421 def GetActiveFile(self):
\r
422 playlist = self.GetActivePlaylist()
\r
423 if playlist is not None:
\r
424 return playlist.get_active_file()
\r
427 def GetActivePlot(self):
\r
428 playlist = self.GetActivePlaylist()
\r
429 if playlist is not None:
\r
430 return playlist.get_active_file().plot
\r
433 def GetDisplayedPlot(self):
\r
434 plot = copy.deepcopy(self.displayed_plot)
\r
436 #plot.curves = copy.deepcopy(plot.curves)
\r
439 def GetDisplayedPlotCorrected(self):
\r
440 plot = copy.deepcopy(self.displayed_plot)
\r
442 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
445 def GetDisplayedPlotRaw(self):
\r
446 plot = copy.deepcopy(self.displayed_plot)
\r
448 plot.curves = copy.deepcopy(plot.raw_curves)
\r
451 def GetDockArt(self):
\r
452 return self._c['manager'].GetArtProvider()
\r
454 def GetPlotmanipulator(self, name):
\r
456 Returns a plot manipulator function from its name
\r
458 for plotmanipulator in self.plotmanipulators:
\r
459 if plotmanipulator.name == name:
\r
460 return plotmanipulator
\r
463 def HasPlotmanipulator(self, name):
\r
465 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
467 for plotmanipulator in self.plotmanipulators:
\r
468 if plotmanipulator.command == name:
\r
473 def _on_dir_ctrl_left_double_click(self, event):
\r
474 file_path = self.panelFolders.GetPath()
\r
475 if os.path.isfile(file_path):
\r
476 if file_path.endswith('.hkp'):
\r
477 self.do_loadlist(file_path)
\r
480 def _on_erase_background(self, event):
\r
483 def _on_notebook_page_close(self, event):
\r
484 ctrl = event.GetEventObject()
\r
485 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
486 self.DeleteFromPlaylists(playlist_name)
\r
488 def OnPaneClose(self, event):
\r
491 def OnPropGridChanged (self, event):
\r
492 prop = event.GetProperty()
\r
494 item_section = self.panelProperties.SelectedTreeItem
\r
495 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
496 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
497 config = self.gui.config[plugin]
\r
498 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
499 property_key = prop.GetName()
\r
500 property_value = prop.GetDisplayedString()
\r
502 config[property_section][property_key]['value'] = property_value
\r
504 def OnResultsCheck(self, index, flag):
\r
505 results = self.GetActivePlot().results
\r
506 if results.has_key(self.results_str):
\r
507 results[self.results_str].results[index].visible = flag
\r
508 results[self.results_str].update()
\r
512 def _on_size(self, event):
\r
515 def OnUpdateNote(self, event):
\r
517 Saves the note to the active file.
\r
519 active_file = self.GetActiveFile()
\r
520 active_file.note = self.panelNote.Editor.GetValue()
\r
522 def UpdateNote(self):
\r
523 #update the note for the active file
\r
524 active_file = self.GetActiveFile()
\r
525 if active_file is not None:
\r
526 self.panelNote.Editor.SetValue(active_file.note)
\r
528 def UpdatePlaylistsTreeSelection(self):
\r
529 playlist = self.GetActivePlaylist()
\r
530 if playlist is not None:
\r
531 if playlist.index >= 0:
\r
532 self._c['status bar'].set_playlist(playlist)
\r
536 def UpdatePlot(self, plot=None):
\r
538 def add_to_plot(curve, set_scale=True):
\r
539 if curve.visible and curve.x and curve.y:
\r
540 #get the index of the subplot to use as destination
\r
541 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
542 #set all parameters for the plot
\r
543 axes_list[destination].set_title(curve.title)
\r
545 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
546 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
547 #set the formatting details for the scale
\r
548 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
549 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
550 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
551 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
552 if curve.style == 'plot':
\r
553 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
554 if curve.style == 'scatter':
\r
555 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
556 #add the legend if necessary
\r
558 axes_list[destination].legend()
\r
561 active_file = self.GetActiveFile()
\r
562 if not active_file.driver:
\r
563 #the first time we identify a file, the following need to be set
\r
564 active_file.identify(self.drivers)
\r
565 for curve in active_file.plot.curves:
\r
566 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
567 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
568 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
569 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
570 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
571 if active_file.driver is None:
\r
572 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
574 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
575 #add raw curves to plot
\r
576 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
577 #apply all active plotmanipulators
\r
578 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
579 #add corrected curves to plot
\r
580 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
583 self.displayed_plot = copy.deepcopy(plot)
\r
585 figure = self.GetActiveFigure()
\r
588 #use '0' instead of e.g. '0.00' for scales
\r
589 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
590 #optionally remove the extension from the title of the plot
\r
591 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
592 if hide_curve_extension:
\r
593 title = lh.remove_extension(self.displayed_plot.title)
\r
595 title = self.displayed_plot.title
\r
596 figure.suptitle(title, fontsize=14)
\r
597 #create the list of all axes necessary (rows and columns)
\r
599 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
600 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
601 for index in range(number_of_rows * number_of_columns):
\r
602 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
604 #add all curves to the corresponding plots
\r
605 for curve in self.displayed_plot.curves:
\r
608 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
609 figure.subplots_adjust(hspace=0.3)
\r
612 self.panelResults.ClearResults()
\r
613 if self.displayed_plot.results.has_key(self.results_str):
\r
614 for curve in self.displayed_plot.results[self.results_str].results:
\r
615 add_to_plot(curve, set_scale=False)
\r
616 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
618 self.panelResults.ClearResults()
\r
620 figure.canvas.draw()
\r
622 def _on_curve_select(self, playlist, curve):
\r
623 #create the plot tab and add playlist to the dictionary
\r
624 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
625 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
626 #tab_index = self._c['notebook'].GetSelection()
\r
627 playlist.figure = plotPanel.get_figure()
\r
628 self.playlists[playlist.name] = playlist
\r
629 #self.playlists[playlist.name] = [playlist, figure]
\r
630 self._c['status bar'].set_playlist(playlist)
\r
635 def _on_playlist_left_doubleclick(self):
\r
636 index = self._c['notebook'].GetSelection()
\r
637 current_playlist = self._c['notebook'].GetPageText(index)
\r
638 if current_playlist != playlist_name:
\r
639 index = self._GetPlaylistTab(playlist_name)
\r
640 self._c['notebook'].SetSelection(index)
\r
641 self._c['status bar'].set_playlist(playlist)
\r
645 def _on_playlist_delete(self, playlist):
\r
646 notebook = self.Parent.plotNotebook
\r
647 index = self.Parent._GetPlaylistTab(playlist.name)
\r
648 notebook.SetSelection(index)
\r
649 notebook.DeletePage(notebook.GetSelection())
\r
650 self.Parent.DeleteFromPlaylists(playlist_name)
\r
654 # Command panel interface
\r
656 def select_command(self, _class, method, command):
\r
658 self.select_plugin(plugin=command.plugin)
\r
659 plugin = self.GetItemText(selected_item)
\r
660 if plugin != 'core':
\r
661 doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')
\r
663 doc_string = 'The module "core" contains Hooke core functionality'
\r
664 if doc_string is not None:
\r
665 self.panelAssistant.ChangeValue(doc_string)
\r
667 self.panelAssistant.ChangeValue('')
\r
668 panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)
\r
669 self.gui.config['selected command'] = command
\r
675 def _next_curve(self, *args):
\r
676 """Call the `next curve` command.
\r
678 results = self.execute_command(
\r
679 command=self._command_by_name('next curve'))
\r
680 if isinstance(results[-1], Success):
\r
681 self.execute_command(
\r
682 command=self._command_by_name('get curve'))
\r
684 def _previous_curve(self, *args):
\r
685 """Call the `previous curve` command.
\r
687 self.execute_command(
\r
688 command=self._command_by_name('previous curve'))
\r
689 if isinstance(results[-1], Success):
\r
690 self.execute_command(
\r
691 command=self._command_by_name('get curve'))
\r
695 # Panel display handling
\r
697 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
698 pane = self._c['manager'].GetPane(panel_name)
\r
701 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
702 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
703 #folders_size = pane.GetSize()
\r
704 self.panelFolders.Fit()
\r
705 self._c['manager'].Update()
\r
707 def _setup_perspectives(self):
\r
708 """Add perspectives to menubar and _perspectives.
\r
710 self._perspectives = {
\r
711 'Default': self._c['manager'].SavePerspective(),
\r
713 path = self.gui.config['perspective path']
\r
714 if os.path.isdir(path):
\r
715 files = sorted(os.listdir(path))
\r
716 for fname in files:
\r
717 name, extension = os.path.splitext(fname)
\r
718 if extension != self.gui.config['perspective extension']:
\r
720 fpath = os.path.join(path, fname)
\r
721 if not os.path.isfile(fpath):
\r
724 with open(fpath, 'rU') as f:
\r
725 perspective = f.readline()
\r
727 self._perspectives[name] = perspective
\r
729 selected_perspective = self.gui.config['active perspective']
\r
730 if not self._perspectives.has_key(selected_perspective):
\r
731 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
733 self._restore_perspective(selected_perspective)
\r
734 self._update_perspective_menu()
\r
736 def _update_perspective_menu(self):
\r
737 self._c['menu bar']._c['perspective'].update(
\r
738 sorted(self._perspectives.keys()),
\r
739 self.gui.config['active perspective'])
\r
741 def _save_perspective(self, perspective, perspective_dir, name,
\r
743 path = os.path.join(perspective_dir, name)
\r
744 if extension != None:
\r
746 if not os.path.isdir(perspective_dir):
\r
747 os.makedirs(perspective_dir)
\r
748 with open(path, 'w') as f:
\r
749 f.write(perspective)
\r
750 self._perspectives[name] = perspective
\r
751 self._restore_perspective(name)
\r
752 self._update_perspective_menu()
\r
754 def _delete_perspectives(self, perspective_dir, names,
\r
758 path = os.path.join(perspective_dir, name)
\r
759 if extension != None:
\r
762 del(self._perspectives[name])
\r
763 self._update_perspective_menu()
\r
764 if self.gui.config['active perspective'] in names:
\r
765 self._restore_perspective('Default')
\r
766 # TODO: does this bug still apply?
\r
767 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
768 # http://trac.wxwidgets.org/ticket/3258
\r
769 # ) that makes the radio item indicator in the menu disappear.
\r
770 # The code should be fine once this issue is fixed.
\r
772 def _restore_perspective(self, name):
\r
773 if name != self.gui.config['active perspective']:
\r
774 print 'restoring perspective:', name
\r
775 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
776 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
777 self._c['manager'].Update()
\r
778 for pane in self._c['manager'].GetAllPanes():
\r
779 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
780 pane.Check(pane.window.IsShown())
\r
782 def _on_save_perspective(self, *args):
\r
783 perspective = self._c['manager'].SavePerspective()
\r
784 name = self.gui.config['active perspective']
\r
785 if name == 'Default':
\r
786 name = 'New perspective'
\r
787 name = select_save_file(
\r
788 directory=self.gui.config['perspective path'],
\r
790 extension=self.gui.config['perspective extension'],
\r
792 message='Enter a name for the new perspective:',
\r
793 caption='Save perspective')
\r
796 self._save_perspective(
\r
797 perspective, self.gui.config['perspective path'], name=name,
\r
798 extension=self.gui.config['perspective extension'])
\r
800 def _on_delete_perspective(self, *args, **kwargs):
\r
801 options = sorted([p for p in self._perspectives.keys()
\r
802 if p != 'Default'])
\r
803 dialog = SelectionDialog(
\r
805 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
806 button_id=wx.ID_DELETE,
\r
807 selection_style='multiple',
\r
809 title='Delete perspective(s)',
\r
810 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
811 dialog.CenterOnScreen()
\r
813 names = [options[i] for i in dialog.selected]
\r
815 self._delete_perspectives(
\r
816 self.gui.config['perspective path'], names=names,
\r
817 extension=self.gui.config['perspective extension'])
\r
819 def _on_select_perspective(self, _class, method, name):
\r
820 self._restore_perspective(name)
\r
824 class HookeApp (wx.App):
\r
825 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
827 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
830 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
832 self.commands = commands
\r
833 self.inqueue = inqueue
\r
834 self.outqueue = outqueue
\r
835 super(HookeApp, self).__init__(*args, **kwargs)
\r
838 self.SetAppName('Hooke')
\r
839 self.SetVendorName('')
\r
840 self._setup_splash_screen()
\r
842 height = int(self.gui.config['main height']) # HACK: config should convert
\r
843 width = int(self.gui.config['main width'])
\r
844 top = int(self.gui.config['main top'])
\r
845 left = int(self.gui.config['main left'])
\r
847 # Sometimes, the ini file gets confused and sets 'left' and
\r
848 # 'top' to large negative numbers. Here we catch and fix
\r
849 # this. Keep small negative numbers, the user might want
\r
857 'frame': HookeFrame(
\r
858 self.gui, self.commands, self.inqueue, self.outqueue,
\r
859 parent=None, title='Hooke',
\r
860 pos=(left, top), size=(width, height),
\r
861 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
863 self._c['frame'].Show(True)
\r
864 self.SetTopWindow(self._c['frame'])
\r
867 def _setup_splash_screen(self):
\r
868 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
869 print 'splash', self.gui.config['show splash screen']
\r
870 path = self.gui.config['splash screen image']
\r
871 if os.path.isfile(path):
\r
872 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
874 bitmap=wx.Image(path).ConvertToBitmap(),
\r
875 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
876 milliseconds=duration,
\r
879 # For some reason splashDuration and sleep do not
\r
880 # correspond to each other at least not on Windows.
\r
881 # Maybe it's because duration is in milliseconds and
\r
882 # sleep in seconds. Thus we need to increase the
\r
883 # sleep time a bit. A factor of 1.2 seems to work.
\r
885 time.sleep(sleepFactor * duration / 1000)
\r
888 class GUI (UserInterface):
\r
889 """wxWindows graphical user interface.
\r
891 def __init__(self):
\r
892 super(GUI, self).__init__(name='gui')
\r
894 def default_settings(self):
\r
895 """Return a list of :class:`hooke.config.Setting`\s for any
\r
896 configurable UI settings.
\r
898 The suggested section setting is::
\r
900 Setting(section=self.setting_section, help=self.__doc__)
\r
903 Setting(section=self.setting_section, help=self.__doc__),
\r
904 Setting(section=self.setting_section, option='icon image',
\r
905 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
906 help='Path to the hooke icon image.'),
\r
907 Setting(section=self.setting_section, option='show splash screen',
\r
909 help='Enable/disable the splash screen'),
\r
910 Setting(section=self.setting_section, option='splash screen image',
\r
911 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
912 help='Path to the Hooke splash screen image.'),
\r
913 Setting(section=self.setting_section, option='splash screen duration',
\r
915 help='Duration of the splash screen in milliseconds.'),
\r
916 Setting(section=self.setting_section, option='perspective path',
\r
917 value=os.path.join('resources', 'gui', 'perspective'),
\r
918 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
919 Setting(section=self.setting_section, option='perspective extension',
\r
921 help='Extension for perspective files.'),
\r
922 Setting(section=self.setting_section, option='hide extensions',
\r
924 help='Hide file extensions when displaying names.'),
\r
925 Setting(section=self.setting_section, option='folders-workdir',
\r
927 help='This should probably go...'),
\r
928 Setting(section=self.setting_section, option='folders-filters',
\r
930 help='This should probably go...'),
\r
931 Setting(section=self.setting_section, option='active perspective',
\r
933 help='Name of active perspective file (or "Default").'),
\r
934 Setting(section=self.setting_section, option='folders-filter-index',
\r
936 help='This should probably go...'),
\r
937 Setting(section=self.setting_section, option='main height',
\r
939 help='Height of main window in pixels.'),
\r
940 Setting(section=self.setting_section, option='main width',
\r
942 help='Width of main window in pixels.'),
\r
943 Setting(section=self.setting_section, option='main top',
\r
945 help='Pixels from screen top to top of main window.'),
\r
946 Setting(section=self.setting_section, option='main left',
\r
948 help='Pixels from screen left to left of main window.'),
\r
949 Setting(section=self.setting_section, option='selected command',
\r
950 value='load playlist',
\r
951 help='Name of the initially selected command.'),
\r
954 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
958 app = HookeApp(gui=self,
\r
960 inqueue=ui_to_command_queue,
\r
961 outqueue=command_to_ui_queue,
\r
965 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
966 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r