3 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.
\r
9 wxversion.select(WX_GOOD)
\r
19 import wx.aui as aui
\r
20 import wx.lib.evtmgr as evtmgr
\r
23 # wxPropertyGrid is included in wxPython >= 2.9.1, see
\r
24 # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
\r
25 # until then, we'll avoid it because of the *nix build problems.
\r
26 #import wx.propgrid as wxpg
\r
28 from matplotlib.ticker import FuncFormatter
\r
30 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
\r
31 from ...config import Setting
\r
32 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
\r
33 from ...ui import UserInterface, CommandMessage
\r
34 from .dialog.selection import Selection as SelectionDialog
\r
35 from .dialog.save_file import select_save_file
\r
36 from . import menu as menu
\r
37 from . import navbar as navbar
\r
38 from . import panel as panel
\r
39 from . 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 ('property', panel.PANELS['propertyeditor2'](
\r
154 style=wx.WANTS_CHARS,
\r
155 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
157 # ('assistant', wx.TextCtrl(
\r
159 # pos=wx.Point(0, 0),
\r
160 # size=wx.Size(150, 90),
\r
161 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
162 ('output', panel.PANELS['output'](
\r
164 pos=wx.Point(0, 0),
\r
165 size=wx.Size(150, 90),
\r
166 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
168 # ('results', panel.results.Results(self), 'bottom'),
\r
170 self._add_panel(label, p, style)
\r
171 #self._c['assistant'].SetEditable(False)
\r
173 def _add_panel(self, label, panel, style):
\r
174 self._c[label] = panel
\r
175 cap_label = label.capitalize()
\r
176 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
177 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
180 elif style == 'center':
\r
182 elif style == 'left':
\r
184 elif style == 'right':
\r
187 assert style == 'bottom', style
\r
189 self._c['manager'].AddPane(panel, info)
\r
191 def _setup_toolbars(self):
\r
192 self._c['navigation bar'] = navbar.NavBar(
\r
194 'next': self._next_curve,
\r
195 'previous': self._previous_curve,
\r
198 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
199 self._c['manager'].AddPane(
\r
200 self._c['navigation bar'],
\r
201 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
202 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
203 ).RightDockable(False))
\r
205 def _bind_events(self):
\r
206 # TODO: figure out if we can use the eventManager for menu
\r
207 # ranges and events of 'self' without raising an assertion
\r
209 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
210 self.Bind(wx.EVT_SIZE, self._on_size)
\r
211 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
212 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
213 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
215 return # TODO: cleanup
\r
216 for value in self._c['menu bar']._c['view']._c.values():
\r
217 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
219 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
220 self._c['menu bar']._c['perspective']._c['save'])
\r
221 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
222 self._c['menu bar']._c['perspective']._c['delete'])
\r
224 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
225 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
227 # TODO: playlist callbacks
\r
228 return # TODO: cleanup
\r
229 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
231 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
233 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
235 def _on_about(self, *args):
\r
236 dialog = wx.MessageDialog(
\r
238 message=self.gui._splash_text(),
\r
239 caption='About Hooke',
\r
240 style=wx.OK|wx.ICON_INFORMATION)
\r
244 def _on_close(self, *args):
\r
246 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
247 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
248 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
249 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
250 # push changes back to Hooke.config?
\r
251 self._c['manager'].UnInit()
\r
252 del self._c['manager']
\r
259 def _command_by_name(self, name):
\r
260 cs = [c for c in self.commands if c.name == name]
\r
262 raise KeyError(name)
\r
264 raise Exception('Multiple commands named "%s"' % name)
\r
267 def execute_command(self, _class=None, method=None,
\r
268 command=None, args=None):
\r
269 self.inqueue.put(CommandMessage(command, args))
\r
272 msg = self.outqueue.get()
\r
273 results.append(msg)
\r
274 if isinstance(msg, Exit):
\r
277 elif isinstance(msg, CommandExit):
\r
278 # TODO: display command complete
\r
280 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
281 self.gui.reload_config(msg.config)
\r
283 elif isinstance(msg, Request):
\r
284 h = handler.HANDLERS[msg.type]
\r
285 h.run(self, msg) # TODO: pause for response?
\r
288 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
289 self._postprocess_text)
\r
290 pp(command=command, results=results)
\r
293 def _handle_request(self, msg):
\r
294 """Repeatedly try to get a response to `msg`.
\r
297 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
298 prompt_string = prompt(msg)
\r
299 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
301 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
305 self.cmd.stdout.write(''.join([
\r
306 error.__class__.__name__, ': ', str(error), '\n']))
\r
307 self.cmd.stdout.write(prompt_string)
\r
308 value = parser(msg, self.cmd.stdin.readline())
\r
310 response = msg.response(value)
\r
312 except ValueError, error:
\r
314 self.inqueue.put(response)
\r
318 # Command-specific postprocessing
\r
320 def _postprocess_text(self, command, results):
\r
321 """Print the string representation of the results to the Results window.
\r
323 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
324 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
325 doesn't print some internally handled messages
\r
326 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
328 for result in results:
\r
329 if isinstance(result, CommandExit):
\r
330 self._c['output'].write(result.__class__.__name__+'\n')
\r
331 self._c['output'].write(str(result).rstrip()+'\n')
\r
333 def _postprocess_get_curve(self, command, results):
\r
334 """Update `self` to show the curve.
\r
336 if not isinstance(results[-1], Success):
\r
337 return # error executing 'get curve'
\r
338 assert len(results) == 2, results
\r
342 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
343 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
344 #GetFirstChild returns a tuple
\r
345 #we only need the first element
\r
346 next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]
\r
348 next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)
\r
349 if not next_item.IsOk():
\r
350 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)
\r
351 #GetFirstChild returns a tuple
\r
352 #we only need the first element
\r
353 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]
\r
354 self._c['playlists']._c['tree'].SelectItem(next_item, True)
\r
355 if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
356 playlist = self.GetActivePlaylist()
\r
357 if playlist.count > 1:
\r
359 self._c['status bar'].set_playlist(playlist)
\r
367 def _GetActiveFileIndex(self):
\r
368 lib.playlist.Playlist = self.GetActivePlaylist()
\r
369 #get the selected item from the tree
\r
370 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
371 #test if a playlist or a curve was double-clicked
\r
372 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
376 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
377 while selected_item.IsOk():
\r
379 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
382 def _GetPlaylistTab(self, name):
\r
383 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
384 if page.caption == name:
\r
388 def select_plugin(self, _class=None, method=None, plugin=None):
\r
391 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
393 playlist = lib.playlist.Playlist(self, self.drivers)
\r
395 playlist.add_curve(item)
\r
396 if playlist.count > 0:
\r
397 playlist.name = self._GetUniquePlaylistName(name)
\r
399 self.AddTayliss(playlist)
\r
401 def AppliesPlotmanipulator(self, name):
\r
403 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
404 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
406 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
408 def ApplyPlotmanipulators(self, plot, plot_file):
\r
410 Apply all active plotmanipulators.
\r
412 if plot is not None and plot_file is not None:
\r
413 manipulated_plot = copy.deepcopy(plot)
\r
414 for plotmanipulator in self.plotmanipulators:
\r
415 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
416 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
417 return manipulated_plot
\r
419 def GetActiveFigure(self):
\r
420 playlist_name = self.GetActivePlaylistName()
\r
421 figure = self.playlists[playlist_name].figure
\r
422 if figure is not None:
\r
426 def GetActiveFile(self):
\r
427 playlist = self.GetActivePlaylist()
\r
428 if playlist is not None:
\r
429 return playlist.get_active_file()
\r
432 def GetActivePlot(self):
\r
433 playlist = self.GetActivePlaylist()
\r
434 if playlist is not None:
\r
435 return playlist.get_active_file().plot
\r
438 def GetDisplayedPlot(self):
\r
439 plot = copy.deepcopy(self.displayed_plot)
\r
441 #plot.curves = copy.deepcopy(plot.curves)
\r
444 def GetDisplayedPlotCorrected(self):
\r
445 plot = copy.deepcopy(self.displayed_plot)
\r
447 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
450 def GetDisplayedPlotRaw(self):
\r
451 plot = copy.deepcopy(self.displayed_plot)
\r
453 plot.curves = copy.deepcopy(plot.raw_curves)
\r
456 def GetDockArt(self):
\r
457 return self._c['manager'].GetArtProvider()
\r
459 def GetPlotmanipulator(self, name):
\r
461 Returns a plot manipulator function from its name
\r
463 for plotmanipulator in self.plotmanipulators:
\r
464 if plotmanipulator.name == name:
\r
465 return plotmanipulator
\r
468 def HasPlotmanipulator(self, name):
\r
470 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
472 for plotmanipulator in self.plotmanipulators:
\r
473 if plotmanipulator.command == name:
\r
478 def _on_dir_ctrl_left_double_click(self, event):
\r
479 file_path = self.panelFolders.GetPath()
\r
480 if os.path.isfile(file_path):
\r
481 if file_path.endswith('.hkp'):
\r
482 self.do_loadlist(file_path)
\r
485 def _on_erase_background(self, event):
\r
488 def _on_notebook_page_close(self, event):
\r
489 ctrl = event.GetEventObject()
\r
490 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
491 self.DeleteFromPlaylists(playlist_name)
\r
493 def OnPaneClose(self, event):
\r
496 def OnPropGridChanged (self, event):
\r
497 prop = event.GetProperty()
\r
499 item_section = self.panelProperties.SelectedTreeItem
\r
500 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
501 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
502 config = self.gui.config[plugin]
\r
503 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
504 property_key = prop.GetName()
\r
505 property_value = prop.GetDisplayedString()
\r
507 config[property_section][property_key]['value'] = property_value
\r
509 def OnResultsCheck(self, index, flag):
\r
510 results = self.GetActivePlot().results
\r
511 if results.has_key(self.results_str):
\r
512 results[self.results_str].results[index].visible = flag
\r
513 results[self.results_str].update()
\r
517 def _on_size(self, event):
\r
520 def OnUpdateNote(self, event):
\r
522 Saves the note to the active file.
\r
524 active_file = self.GetActiveFile()
\r
525 active_file.note = self.panelNote.Editor.GetValue()
\r
527 def UpdateNote(self):
\r
528 #update the note for the active file
\r
529 active_file = self.GetActiveFile()
\r
530 if active_file is not None:
\r
531 self.panelNote.Editor.SetValue(active_file.note)
\r
533 def UpdatePlaylistsTreeSelection(self):
\r
534 playlist = self.GetActivePlaylist()
\r
535 if playlist is not None:
\r
536 if playlist.index >= 0:
\r
537 self._c['status bar'].set_playlist(playlist)
\r
541 def UpdatePlot(self, plot=None):
\r
543 def add_to_plot(curve, set_scale=True):
\r
544 if curve.visible and curve.x and curve.y:
\r
545 #get the index of the subplot to use as destination
\r
546 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
547 #set all parameters for the plot
\r
548 axes_list[destination].set_title(curve.title)
\r
550 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
551 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
552 #set the formatting details for the scale
\r
553 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
554 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
555 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
556 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
557 if curve.style == 'plot':
\r
558 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
559 if curve.style == 'scatter':
\r
560 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
561 #add the legend if necessary
\r
563 axes_list[destination].legend()
\r
566 active_file = self.GetActiveFile()
\r
567 if not active_file.driver:
\r
568 #the first time we identify a file, the following need to be set
\r
569 active_file.identify(self.drivers)
\r
570 for curve in active_file.plot.curves:
\r
571 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
572 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
573 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
574 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
575 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
576 if active_file.driver is None:
\r
577 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
579 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
580 #add raw curves to plot
\r
581 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
582 #apply all active plotmanipulators
\r
583 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
584 #add corrected curves to plot
\r
585 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
588 self.displayed_plot = copy.deepcopy(plot)
\r
590 figure = self.GetActiveFigure()
\r
593 #use '0' instead of e.g. '0.00' for scales
\r
594 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
595 #optionally remove the extension from the title of the plot
\r
596 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
597 if hide_curve_extension:
\r
598 title = lh.remove_extension(self.displayed_plot.title)
\r
600 title = self.displayed_plot.title
\r
601 figure.suptitle(title, fontsize=14)
\r
602 #create the list of all axes necessary (rows and columns)
\r
604 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
605 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
606 for index in range(number_of_rows * number_of_columns):
\r
607 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
609 #add all curves to the corresponding plots
\r
610 for curve in self.displayed_plot.curves:
\r
613 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
614 figure.subplots_adjust(hspace=0.3)
\r
617 self.panelResults.ClearResults()
\r
618 if self.displayed_plot.results.has_key(self.results_str):
\r
619 for curve in self.displayed_plot.results[self.results_str].results:
\r
620 add_to_plot(curve, set_scale=False)
\r
621 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
623 self.panelResults.ClearResults()
\r
625 figure.canvas.draw()
\r
627 def _on_curve_select(self, playlist, curve):
\r
628 #create the plot tab and add playlist to the dictionary
\r
629 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
630 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
631 #tab_index = self._c['notebook'].GetSelection()
\r
632 playlist.figure = plotPanel.get_figure()
\r
633 self.playlists[playlist.name] = playlist
\r
634 #self.playlists[playlist.name] = [playlist, figure]
\r
635 self._c['status bar'].set_playlist(playlist)
\r
640 def _on_playlist_left_doubleclick(self):
\r
641 index = self._c['notebook'].GetSelection()
\r
642 current_playlist = self._c['notebook'].GetPageText(index)
\r
643 if current_playlist != playlist_name:
\r
644 index = self._GetPlaylistTab(playlist_name)
\r
645 self._c['notebook'].SetSelection(index)
\r
646 self._c['status bar'].set_playlist(playlist)
\r
650 def _on_playlist_delete(self, playlist):
\r
651 notebook = self.Parent.plotNotebook
\r
652 index = self.Parent._GetPlaylistTab(playlist.name)
\r
653 notebook.SetSelection(index)
\r
654 notebook.DeletePage(notebook.GetSelection())
\r
655 self.Parent.DeleteFromPlaylists(playlist_name)
\r
659 # Command panel interface
\r
661 def select_command(self, _class, method, command):
\r
663 self.select_plugin(plugin=command.plugin)
\r
664 plugin = self.GetItemText(selected_item)
\r
665 if plugin != 'core':
\r
666 doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__')
\r
668 doc_string = 'The module "core" contains Hooke core functionality'
\r
669 if doc_string is not None:
\r
670 self.panelAssistant.ChangeValue(doc_string)
\r
672 self.panelAssistant.ChangeValue('')
\r
673 panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties)
\r
674 self.gui.config['selected command'] = command
\r
680 def _next_curve(self, *args):
\r
681 """Call the `next curve` command.
\r
683 results = self.execute_command(
\r
684 command=self._command_by_name('next curve'))
\r
685 if isinstance(results[-1], Success):
\r
686 self.execute_command(
\r
687 command=self._command_by_name('get curve'))
\r
689 def _previous_curve(self, *args):
\r
690 """Call the `previous curve` command.
\r
692 self.execute_command(
\r
693 command=self._command_by_name('previous curve'))
\r
694 if isinstance(results[-1], Success):
\r
695 self.execute_command(
\r
696 command=self._command_by_name('get curve'))
\r
700 # Panel display handling
\r
702 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
703 pane = self._c['manager'].GetPane(panel_name)
\r
706 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
707 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
708 #folders_size = pane.GetSize()
\r
709 self.panelFolders.Fit()
\r
710 self._c['manager'].Update()
\r
712 def _setup_perspectives(self):
\r
713 """Add perspectives to menubar and _perspectives.
\r
715 self._perspectives = {
\r
716 'Default': self._c['manager'].SavePerspective(),
\r
718 path = self.gui.config['perspective path']
\r
719 if os.path.isdir(path):
\r
720 files = sorted(os.listdir(path))
\r
721 for fname in files:
\r
722 name, extension = os.path.splitext(fname)
\r
723 if extension != self.gui.config['perspective extension']:
\r
725 fpath = os.path.join(path, fname)
\r
726 if not os.path.isfile(fpath):
\r
729 with open(fpath, 'rU') as f:
\r
730 perspective = f.readline()
\r
732 self._perspectives[name] = perspective
\r
734 selected_perspective = self.gui.config['active perspective']
\r
735 if not self._perspectives.has_key(selected_perspective):
\r
736 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
738 self._restore_perspective(selected_perspective)
\r
739 self._update_perspective_menu()
\r
741 def _update_perspective_menu(self):
\r
742 self._c['menu bar']._c['perspective'].update(
\r
743 sorted(self._perspectives.keys()),
\r
744 self.gui.config['active perspective'])
\r
746 def _save_perspective(self, perspective, perspective_dir, name,
\r
748 path = os.path.join(perspective_dir, name)
\r
749 if extension != None:
\r
751 if not os.path.isdir(perspective_dir):
\r
752 os.makedirs(perspective_dir)
\r
753 with open(path, 'w') as f:
\r
754 f.write(perspective)
\r
755 self._perspectives[name] = perspective
\r
756 self._restore_perspective(name)
\r
757 self._update_perspective_menu()
\r
759 def _delete_perspectives(self, perspective_dir, names,
\r
763 path = os.path.join(perspective_dir, name)
\r
764 if extension != None:
\r
767 del(self._perspectives[name])
\r
768 self._update_perspective_menu()
\r
769 if self.gui.config['active perspective'] in names:
\r
770 self._restore_perspective('Default')
\r
771 # TODO: does this bug still apply?
\r
772 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
773 # http://trac.wxwidgets.org/ticket/3258
\r
774 # ) that makes the radio item indicator in the menu disappear.
\r
775 # The code should be fine once this issue is fixed.
\r
777 def _restore_perspective(self, name):
\r
778 if name != self.gui.config['active perspective']:
\r
779 print 'restoring perspective:', name
\r
780 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
781 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
782 self._c['manager'].Update()
\r
783 for pane in self._c['manager'].GetAllPanes():
\r
784 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
785 pane.Check(pane.window.IsShown())
\r
787 def _on_save_perspective(self, *args):
\r
788 perspective = self._c['manager'].SavePerspective()
\r
789 name = self.gui.config['active perspective']
\r
790 if name == 'Default':
\r
791 name = 'New perspective'
\r
792 name = select_save_file(
\r
793 directory=self.gui.config['perspective path'],
\r
795 extension=self.gui.config['perspective extension'],
\r
797 message='Enter a name for the new perspective:',
\r
798 caption='Save perspective')
\r
801 self._save_perspective(
\r
802 perspective, self.gui.config['perspective path'], name=name,
\r
803 extension=self.gui.config['perspective extension'])
\r
805 def _on_delete_perspective(self, *args, **kwargs):
\r
806 options = sorted([p for p in self._perspectives.keys()
\r
807 if p != 'Default'])
\r
808 dialog = SelectionDialog(
\r
810 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
811 button_id=wx.ID_DELETE,
\r
812 selection_style='multiple',
\r
814 title='Delete perspective(s)',
\r
815 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
816 dialog.CenterOnScreen()
\r
818 names = [options[i] for i in dialog.selected]
\r
820 self._delete_perspectives(
\r
821 self.gui.config['perspective path'], names=names,
\r
822 extension=self.gui.config['perspective extension'])
\r
824 def _on_select_perspective(self, _class, method, name):
\r
825 self._restore_perspective(name)
\r
829 class HookeApp (wx.App):
\r
830 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
832 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
835 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
837 self.commands = commands
\r
838 self.inqueue = inqueue
\r
839 self.outqueue = outqueue
\r
840 super(HookeApp, self).__init__(*args, **kwargs)
\r
843 self.SetAppName('Hooke')
\r
844 self.SetVendorName('')
\r
845 self._setup_splash_screen()
\r
847 height = int(self.gui.config['main height']) # HACK: config should convert
\r
848 width = int(self.gui.config['main width'])
\r
849 top = int(self.gui.config['main top'])
\r
850 left = int(self.gui.config['main left'])
\r
852 # Sometimes, the ini file gets confused and sets 'left' and
\r
853 # 'top' to large negative numbers. Here we catch and fix
\r
854 # this. Keep small negative numbers, the user might want
\r
862 'frame': HookeFrame(
\r
863 self.gui, self.commands, self.inqueue, self.outqueue,
\r
864 parent=None, title='Hooke',
\r
865 pos=(left, top), size=(width, height),
\r
866 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
868 self._c['frame'].Show(True)
\r
869 self.SetTopWindow(self._c['frame'])
\r
872 def _setup_splash_screen(self):
\r
873 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
874 print 'splash', self.gui.config['show splash screen']
\r
875 path = self.gui.config['splash screen image']
\r
876 if os.path.isfile(path):
\r
877 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
879 bitmap=wx.Image(path).ConvertToBitmap(),
\r
880 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
881 milliseconds=duration,
\r
884 # For some reason splashDuration and sleep do not
\r
885 # correspond to each other at least not on Windows.
\r
886 # Maybe it's because duration is in milliseconds and
\r
887 # sleep in seconds. Thus we need to increase the
\r
888 # sleep time a bit. A factor of 1.2 seems to work.
\r
890 time.sleep(sleepFactor * duration / 1000)
\r
893 class GUI (UserInterface):
\r
894 """wxWindows graphical user interface.
\r
896 def __init__(self):
\r
897 super(GUI, self).__init__(name='gui')
\r
899 def default_settings(self):
\r
900 """Return a list of :class:`hooke.config.Setting`\s for any
\r
901 configurable UI settings.
\r
903 The suggested section setting is::
\r
905 Setting(section=self.setting_section, help=self.__doc__)
\r
908 Setting(section=self.setting_section, help=self.__doc__),
\r
909 Setting(section=self.setting_section, option='icon image',
\r
910 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
911 help='Path to the hooke icon image.'),
\r
912 Setting(section=self.setting_section, option='show splash screen',
\r
914 help='Enable/disable the splash screen'),
\r
915 Setting(section=self.setting_section, option='splash screen image',
\r
916 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
917 help='Path to the Hooke splash screen image.'),
\r
918 Setting(section=self.setting_section, option='splash screen duration',
\r
920 help='Duration of the splash screen in milliseconds.'),
\r
921 Setting(section=self.setting_section, option='perspective path',
\r
922 value=os.path.join('resources', 'gui', 'perspective'),
\r
923 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
924 Setting(section=self.setting_section, option='perspective extension',
\r
926 help='Extension for perspective files.'),
\r
927 Setting(section=self.setting_section, option='hide extensions',
\r
929 help='Hide file extensions when displaying names.'),
\r
930 Setting(section=self.setting_section, option='folders-workdir',
\r
932 help='This should probably go...'),
\r
933 Setting(section=self.setting_section, option='folders-filters',
\r
935 help='This should probably go...'),
\r
936 Setting(section=self.setting_section, option='active perspective',
\r
938 help='Name of active perspective file (or "Default").'),
\r
939 Setting(section=self.setting_section, option='folders-filter-index',
\r
941 help='This should probably go...'),
\r
942 Setting(section=self.setting_section, option='main height',
\r
944 help='Height of main window in pixels.'),
\r
945 Setting(section=self.setting_section, option='main width',
\r
947 help='Width of main window in pixels.'),
\r
948 Setting(section=self.setting_section, option='main top',
\r
950 help='Pixels from screen top to top of main window.'),
\r
951 Setting(section=self.setting_section, option='main left',
\r
953 help='Pixels from screen left to left of main window.'),
\r
954 Setting(section=self.setting_section, option='selected command',
\r
955 value='load playlist',
\r
956 help='Name of the initially selected command.'),
\r
959 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
963 app = HookeApp(gui=self,
\r
965 inqueue=ui_to_command_queue,
\r
966 outqueue=command_to_ui_queue,
\r
970 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
971 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r