3 """Defines :class:`GUI` providing a wxWidgets interface to Hooke.
\r
9 wxversion.select(WX_GOOD)
\r
19 import wx.aui as aui
\r
20 import wx.lib.evtmgr as evtmgr
\r
23 # wxPropertyGrid is included in wxPython >= 2.9.1, see
\r
24 # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download
\r
25 # until then, we'll avoid it because of the *nix build problems.
\r
26 #import wx.propgrid as wxpg
\r
28 from matplotlib.ticker import FuncFormatter
\r
30 from ...command import CommandExit, Exit, Success, Failure, Command, Argument
\r
31 from ...config import Setting
\r
32 from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig
\r
33 from ...ui import UserInterface, CommandMessage
\r
34 from .dialog.selection import Selection as SelectionDialog
\r
35 from .dialog.save_file import select_save_file
\r
36 from . import menu as menu
\r
37 from . import navbar as navbar
\r
38 from . import panel as panel
\r
39 from .panel.propertyeditor2 import prop_from_argument, prop_from_setting
\r
40 from . import prettyformat as prettyformat
\r
41 from . import statusbar as statusbar
\r
44 class HookeFrame (wx.Frame):
\r
45 """The main Hooke-interface window.
\r
47 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
48 super(HookeFrame, self).__init__(*args, **kwargs)
\r
50 self.commands = commands
\r
51 self.inqueue = inqueue
\r
52 self.outqueue = outqueue
\r
53 self._perspectives = {} # {name: perspective_str}
\r
56 self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO))
\r
58 # setup frame manager
\r
59 self._c['manager'] = aui.AuiManager()
\r
60 self._c['manager'].SetManagedWindow(self)
\r
62 # set the gradient and drag styles
\r
63 self._c['manager'].GetArtProvider().SetMetric(
\r
64 aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE)
\r
65 self._c['manager'].SetFlags(
\r
66 self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG)
\r
68 # Min size for the frame itself isn't completely done. See
\r
69 # the end of FrameManager::Update() for the test code. For
\r
70 # now, just hard code a frame minimum size.
\r
71 self.SetMinSize(wx.Size(500, 500))
\r
73 self._setup_panels()
\r
74 self._setup_toolbars()
\r
75 self._c['manager'].Update() # commit pending changes
\r
77 # Create the menubar after the panes so that the default
\r
78 # perspective is created with all panes open
\r
79 self._c['menu bar'] = menu.HookeMenuBar(
\r
82 'close': self._on_close,
\r
83 'about': self._on_about,
\r
84 'view_panel': self._on_panel_visibility,
\r
85 'save_perspective': self._on_save_perspective,
\r
86 'delete_perspective': self._on_delete_perspective,
\r
87 'select_perspective': self._on_select_perspective,
\r
89 self.SetMenuBar(self._c['menu bar'])
\r
91 self._c['status bar'] = statusbar.StatusBar(
\r
93 style=wx.ST_SIZEGRIP)
\r
94 self.SetStatusBar(self._c['status bar'])
\r
96 self._setup_perspectives()
\r
99 name = self.gui.config['active perspective']
\r
100 return # TODO: cleanup
\r
101 self.playlists = self._c['playlists'].Playlists
\r
102 self._displayed_plot = None
\r
103 #load default list, if possible
\r
104 self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist'))
\r
109 def _setup_panels(self):
\r
110 client_size = self.GetClientSize()
\r
111 for label,p,style in [
\r
112 # ('folders', wx.GenericDirCtrl(
\r
114 # dir=self.gui.config['folders-workdir'],
\r
116 # style=wx.DIRCTRL_SHOW_FILTERS,
\r
117 # filter=self.gui.config['folders-filters'],
\r
118 # defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert
\r
119 # ('playlists', panel.PANELS['playlist'](
\r
121 # config=self.gui.config,
\r
123 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
124 # # WANTS_CHARS so the panel doesn't eat the Return key.
\r
125 # size=(160, 200)), 'left'),
\r
126 # ('note', panel.note.Note(
\r
128 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
129 # size=(160, 200)), 'left'),
\r
130 # ('notebook', Notebook(
\r
132 # pos=wx.Point(client_size.x, client_size.y),
\r
133 # size=wx.Size(430, 200),
\r
134 # style=aui.AUI_NB_DEFAULT_STYLE
\r
135 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
136 ('commands', panel.PANELS['commands'](
\r
137 commands=self.commands,
\r
138 selected=self.gui.config['selected command'],
\r
140 'execute': self.execute_command,
\r
141 'select_plugin': self.select_plugin,
\r
142 'select_command': self.select_command,
\r
143 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
146 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
147 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
150 ('property', panel.PANELS['propertyeditor2'](
\r
153 style=wx.WANTS_CHARS,
\r
154 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
156 # ('assistant', wx.TextCtrl(
\r
158 # pos=wx.Point(0, 0),
\r
159 # size=wx.Size(150, 90),
\r
160 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
161 ('output', panel.PANELS['output'](
\r
163 pos=wx.Point(0, 0),
\r
164 size=wx.Size(150, 90),
\r
165 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
167 # ('results', panel.results.Results(self), 'bottom'),
\r
169 self._add_panel(label, p, style)
\r
170 #self._c['assistant'].SetEditable(False)
\r
172 def _add_panel(self, label, panel, style):
\r
173 self._c[label] = panel
\r
174 cap_label = label.capitalize()
\r
175 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
176 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
179 elif style == 'center':
\r
181 elif style == 'left':
\r
183 elif style == 'right':
\r
186 assert style == 'bottom', style
\r
188 self._c['manager'].AddPane(panel, info)
\r
190 def _setup_toolbars(self):
\r
191 self._c['navigation bar'] = navbar.NavBar(
\r
193 'next': self._next_curve,
\r
194 'previous': self._previous_curve,
\r
197 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
198 self._c['manager'].AddPane(
\r
199 self._c['navigation bar'],
\r
200 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
201 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
202 ).RightDockable(False))
\r
204 def _bind_events(self):
\r
205 # TODO: figure out if we can use the eventManager for menu
\r
206 # ranges and events of 'self' without raising an assertion
\r
208 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
209 self.Bind(wx.EVT_SIZE, self._on_size)
\r
210 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
211 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
212 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
214 return # TODO: cleanup
\r
215 for value in self._c['menu bar']._c['view']._c.values():
\r
216 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
218 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
219 self._c['menu bar']._c['perspective']._c['save'])
\r
220 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
221 self._c['menu bar']._c['perspective']._c['delete'])
\r
223 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
224 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
226 # TODO: playlist callbacks
\r
227 return # TODO: cleanup
\r
228 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
230 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
232 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
234 def _on_about(self, *args):
\r
235 dialog = wx.MessageDialog(
\r
237 message=self.gui._splash_text(),
\r
238 caption='About Hooke',
\r
239 style=wx.OK|wx.ICON_INFORMATION)
\r
243 def _on_close(self, *args):
\r
245 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
246 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
247 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
248 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
249 # push changes back to Hooke.config?
\r
250 self._c['manager'].UnInit()
\r
251 del self._c['manager']
\r
258 def _command_by_name(self, name):
\r
259 cs = [c for c in self.commands if c.name == name]
\r
261 raise KeyError(name)
\r
263 raise Exception('Multiple commands named "%s"' % name)
\r
266 def execute_command(self, _class=None, method=None,
\r
267 command=None, args=None):
\r
270 if ('property' in self._c
\r
271 and self.gui.config['selected command'] == command):
\r
272 arg_names = [arg.name for arg in command.arguments]
\r
273 for name,value in self._c['property'].get_values().items():
\r
274 if name in arg_names:
\r
276 print 'executing', command.name, args
\r
277 self.inqueue.put(CommandMessage(command, args))
\r
280 msg = self.outqueue.get()
\r
281 results.append(msg)
\r
282 if isinstance(msg, Exit):
\r
285 elif isinstance(msg, CommandExit):
\r
286 # TODO: display command complete
\r
288 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
289 self.gui.reload_config(msg.config)
\r
291 elif isinstance(msg, Request):
\r
292 h = handler.HANDLERS[msg.type]
\r
293 h.run(self, msg) # TODO: pause for response?
\r
296 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
297 self._postprocess_text)
\r
298 pp(command=command, results=results)
\r
301 def _handle_request(self, msg):
\r
302 """Repeatedly try to get a response to `msg`.
\r
305 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
306 prompt_string = prompt(msg)
\r
307 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
309 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
313 self.cmd.stdout.write(''.join([
\r
314 error.__class__.__name__, ': ', str(error), '\n']))
\r
315 self.cmd.stdout.write(prompt_string)
\r
316 value = parser(msg, self.cmd.stdin.readline())
\r
318 response = msg.response(value)
\r
320 except ValueError, error:
\r
322 self.inqueue.put(response)
\r
326 # Command-specific postprocessing
\r
328 def _postprocess_text(self, command, results):
\r
329 """Print the string representation of the results to the Results window.
\r
331 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
332 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
333 doesn't print some internally handled messages
\r
334 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
336 for result in results:
\r
337 if isinstance(result, CommandExit):
\r
338 self._c['output'].write(result.__class__.__name__+'\n')
\r
339 self._c['output'].write(str(result).rstrip()+'\n')
\r
341 def _postprocess_get_curve(self, command, results):
\r
342 """Update `self` to show the curve.
\r
344 if not isinstance(results[-1], Success):
\r
345 return # error executing 'get curve'
\r
346 assert len(results) == 2, results
\r
350 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
351 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
352 #GetFirstChild returns a tuple
\r
353 #we only need the first element
\r
354 next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0]
\r
356 next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item)
\r
357 if not next_item.IsOk():
\r
358 parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item)
\r
359 #GetFirstChild returns a tuple
\r
360 #we only need the first element
\r
361 next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0]
\r
362 self._c['playlists']._c['tree'].SelectItem(next_item, True)
\r
363 if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
364 playlist = self.GetActivePlaylist()
\r
365 if playlist.count > 1:
\r
367 self._c['status bar'].set_playlist(playlist)
\r
375 def _GetActiveFileIndex(self):
\r
376 lib.playlist.Playlist = self.GetActivePlaylist()
\r
377 #get the selected item from the tree
\r
378 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
379 #test if a playlist or a curve was double-clicked
\r
380 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
384 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
385 while selected_item.IsOk():
\r
387 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
390 def _GetPlaylistTab(self, name):
\r
391 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
392 if page.caption == name:
\r
396 def select_plugin(self, _class=None, method=None, plugin=None):
\r
399 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
401 playlist = lib.playlist.Playlist(self, self.drivers)
\r
403 playlist.add_curve(item)
\r
404 if playlist.count > 0:
\r
405 playlist.name = self._GetUniquePlaylistName(name)
\r
407 self.AddTayliss(playlist)
\r
409 def AppliesPlotmanipulator(self, name):
\r
411 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
412 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
414 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
416 def ApplyPlotmanipulators(self, plot, plot_file):
\r
418 Apply all active plotmanipulators.
\r
420 if plot is not None and plot_file is not None:
\r
421 manipulated_plot = copy.deepcopy(plot)
\r
422 for plotmanipulator in self.plotmanipulators:
\r
423 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
424 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
425 return manipulated_plot
\r
427 def GetActiveFigure(self):
\r
428 playlist_name = self.GetActivePlaylistName()
\r
429 figure = self.playlists[playlist_name].figure
\r
430 if figure is not None:
\r
434 def GetActiveFile(self):
\r
435 playlist = self.GetActivePlaylist()
\r
436 if playlist is not None:
\r
437 return playlist.get_active_file()
\r
440 def GetActivePlot(self):
\r
441 playlist = self.GetActivePlaylist()
\r
442 if playlist is not None:
\r
443 return playlist.get_active_file().plot
\r
446 def GetDisplayedPlot(self):
\r
447 plot = copy.deepcopy(self.displayed_plot)
\r
449 #plot.curves = copy.deepcopy(plot.curves)
\r
452 def GetDisplayedPlotCorrected(self):
\r
453 plot = copy.deepcopy(self.displayed_plot)
\r
455 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
458 def GetDisplayedPlotRaw(self):
\r
459 plot = copy.deepcopy(self.displayed_plot)
\r
461 plot.curves = copy.deepcopy(plot.raw_curves)
\r
464 def GetDockArt(self):
\r
465 return self._c['manager'].GetArtProvider()
\r
467 def GetPlotmanipulator(self, name):
\r
469 Returns a plot manipulator function from its name
\r
471 for plotmanipulator in self.plotmanipulators:
\r
472 if plotmanipulator.name == name:
\r
473 return plotmanipulator
\r
476 def HasPlotmanipulator(self, name):
\r
478 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
480 for plotmanipulator in self.plotmanipulators:
\r
481 if plotmanipulator.command == name:
\r
486 def _on_dir_ctrl_left_double_click(self, event):
\r
487 file_path = self.panelFolders.GetPath()
\r
488 if os.path.isfile(file_path):
\r
489 if file_path.endswith('.hkp'):
\r
490 self.do_loadlist(file_path)
\r
493 def _on_erase_background(self, event):
\r
496 def _on_notebook_page_close(self, event):
\r
497 ctrl = event.GetEventObject()
\r
498 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
499 self.DeleteFromPlaylists(playlist_name)
\r
501 def OnPaneClose(self, event):
\r
504 def OnPropGridChanged (self, event):
\r
505 prop = event.GetProperty()
\r
507 item_section = self.panelProperties.SelectedTreeItem
\r
508 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
509 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
510 config = self.gui.config[plugin]
\r
511 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
512 property_key = prop.GetName()
\r
513 property_value = prop.GetDisplayedString()
\r
515 config[property_section][property_key]['value'] = property_value
\r
517 def OnResultsCheck(self, index, flag):
\r
518 results = self.GetActivePlot().results
\r
519 if results.has_key(self.results_str):
\r
520 results[self.results_str].results[index].visible = flag
\r
521 results[self.results_str].update()
\r
525 def _on_size(self, event):
\r
528 def OnUpdateNote(self, event):
\r
530 Saves the note to the active file.
\r
532 active_file = self.GetActiveFile()
\r
533 active_file.note = self.panelNote.Editor.GetValue()
\r
535 def UpdateNote(self):
\r
536 #update the note for the active file
\r
537 active_file = self.GetActiveFile()
\r
538 if active_file is not None:
\r
539 self.panelNote.Editor.SetValue(active_file.note)
\r
541 def UpdatePlaylistsTreeSelection(self):
\r
542 playlist = self.GetActivePlaylist()
\r
543 if playlist is not None:
\r
544 if playlist.index >= 0:
\r
545 self._c['status bar'].set_playlist(playlist)
\r
549 def UpdatePlot(self, plot=None):
\r
551 def add_to_plot(curve, set_scale=True):
\r
552 if curve.visible and curve.x and curve.y:
\r
553 #get the index of the subplot to use as destination
\r
554 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
555 #set all parameters for the plot
\r
556 axes_list[destination].set_title(curve.title)
\r
558 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
559 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
560 #set the formatting details for the scale
\r
561 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
562 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
563 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
564 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
565 if curve.style == 'plot':
\r
566 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
567 if curve.style == 'scatter':
\r
568 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
569 #add the legend if necessary
\r
571 axes_list[destination].legend()
\r
574 active_file = self.GetActiveFile()
\r
575 if not active_file.driver:
\r
576 #the first time we identify a file, the following need to be set
\r
577 active_file.identify(self.drivers)
\r
578 for curve in active_file.plot.curves:
\r
579 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
580 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
581 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
582 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
583 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
584 if active_file.driver is None:
\r
585 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
587 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
588 #add raw curves to plot
\r
589 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
590 #apply all active plotmanipulators
\r
591 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
592 #add corrected curves to plot
\r
593 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
596 self.displayed_plot = copy.deepcopy(plot)
\r
598 figure = self.GetActiveFigure()
\r
601 #use '0' instead of e.g. '0.00' for scales
\r
602 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
603 #optionally remove the extension from the title of the plot
\r
604 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
605 if hide_curve_extension:
\r
606 title = lh.remove_extension(self.displayed_plot.title)
\r
608 title = self.displayed_plot.title
\r
609 figure.suptitle(title, fontsize=14)
\r
610 #create the list of all axes necessary (rows and columns)
\r
612 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
613 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
614 for index in range(number_of_rows * number_of_columns):
\r
615 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
617 #add all curves to the corresponding plots
\r
618 for curve in self.displayed_plot.curves:
\r
621 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
622 figure.subplots_adjust(hspace=0.3)
\r
625 self.panelResults.ClearResults()
\r
626 if self.displayed_plot.results.has_key(self.results_str):
\r
627 for curve in self.displayed_plot.results[self.results_str].results:
\r
628 add_to_plot(curve, set_scale=False)
\r
629 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
631 self.panelResults.ClearResults()
\r
633 figure.canvas.draw()
\r
635 def _on_curve_select(self, playlist, curve):
\r
636 #create the plot tab and add playlist to the dictionary
\r
637 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
638 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
639 #tab_index = self._c['notebook'].GetSelection()
\r
640 playlist.figure = plotPanel.get_figure()
\r
641 self.playlists[playlist.name] = playlist
\r
642 #self.playlists[playlist.name] = [playlist, figure]
\r
643 self._c['status bar'].set_playlist(playlist)
\r
648 def _on_playlist_left_doubleclick(self):
\r
649 index = self._c['notebook'].GetSelection()
\r
650 current_playlist = self._c['notebook'].GetPageText(index)
\r
651 if current_playlist != playlist_name:
\r
652 index = self._GetPlaylistTab(playlist_name)
\r
653 self._c['notebook'].SetSelection(index)
\r
654 self._c['status bar'].set_playlist(playlist)
\r
658 def _on_playlist_delete(self, playlist):
\r
659 notebook = self.Parent.plotNotebook
\r
660 index = self.Parent._GetPlaylistTab(playlist.name)
\r
661 notebook.SetSelection(index)
\r
662 notebook.DeletePage(notebook.GetSelection())
\r
663 self.Parent.DeleteFromPlaylists(playlist_name)
\r
667 # Command panel interface
\r
669 def select_command(self, _class, method, command):
\r
670 #self.select_plugin(plugin=command.plugin)
\r
671 if 'assistant' in self._c:
\r
672 self._c['assitant'].ChangeValue(command.help)
\r
673 self._c['property'].clear()
\r
674 for argument in command.arguments:
\r
675 if argument.name == 'help':
\r
677 p = prop_from_argument(
\r
678 argument, curves=[], playlists=[]) # TODO: lookup playlists/curves
\r
680 continue # property intentionally not handled (yet)
\r
681 self._c['property'].append_property(p)
\r
683 self.gui.config['selected command'] = command # TODO: push to engine
\r
689 def _next_curve(self, *args):
\r
690 """Call the `next curve` command.
\r
692 results = self.execute_command(
\r
693 command=self._command_by_name('next curve'))
\r
694 if isinstance(results[-1], Success):
\r
695 self.execute_command(
\r
696 command=self._command_by_name('get curve'))
\r
698 def _previous_curve(self, *args):
\r
699 """Call the `previous curve` command.
\r
701 self.execute_command(
\r
702 command=self._command_by_name('previous curve'))
\r
703 if isinstance(results[-1], Success):
\r
704 self.execute_command(
\r
705 command=self._command_by_name('get curve'))
\r
709 # Panel display handling
\r
711 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
712 pane = self._c['manager'].GetPane(panel_name)
\r
715 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
716 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
717 #folders_size = pane.GetSize()
\r
718 self.panelFolders.Fit()
\r
719 self._c['manager'].Update()
\r
721 def _setup_perspectives(self):
\r
722 """Add perspectives to menubar and _perspectives.
\r
724 self._perspectives = {
\r
725 'Default': self._c['manager'].SavePerspective(),
\r
727 path = self.gui.config['perspective path']
\r
728 if os.path.isdir(path):
\r
729 files = sorted(os.listdir(path))
\r
730 for fname in files:
\r
731 name, extension = os.path.splitext(fname)
\r
732 if extension != self.gui.config['perspective extension']:
\r
734 fpath = os.path.join(path, fname)
\r
735 if not os.path.isfile(fpath):
\r
738 with open(fpath, 'rU') as f:
\r
739 perspective = f.readline()
\r
741 self._perspectives[name] = perspective
\r
743 selected_perspective = self.gui.config['active perspective']
\r
744 if not self._perspectives.has_key(selected_perspective):
\r
745 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
747 self._restore_perspective(selected_perspective)
\r
748 self._update_perspective_menu()
\r
750 def _update_perspective_menu(self):
\r
751 self._c['menu bar']._c['perspective'].update(
\r
752 sorted(self._perspectives.keys()),
\r
753 self.gui.config['active perspective'])
\r
755 def _save_perspective(self, perspective, perspective_dir, name,
\r
757 path = os.path.join(perspective_dir, name)
\r
758 if extension != None:
\r
760 if not os.path.isdir(perspective_dir):
\r
761 os.makedirs(perspective_dir)
\r
762 with open(path, 'w') as f:
\r
763 f.write(perspective)
\r
764 self._perspectives[name] = perspective
\r
765 self._restore_perspective(name)
\r
766 self._update_perspective_menu()
\r
768 def _delete_perspectives(self, perspective_dir, names,
\r
772 path = os.path.join(perspective_dir, name)
\r
773 if extension != None:
\r
776 del(self._perspectives[name])
\r
777 self._update_perspective_menu()
\r
778 if self.gui.config['active perspective'] in names:
\r
779 self._restore_perspective('Default')
\r
780 # TODO: does this bug still apply?
\r
781 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
782 # http://trac.wxwidgets.org/ticket/3258
\r
783 # ) that makes the radio item indicator in the menu disappear.
\r
784 # The code should be fine once this issue is fixed.
\r
786 def _restore_perspective(self, name):
\r
787 if name != self.gui.config['active perspective']:
\r
788 print 'restoring perspective:', name
\r
789 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
790 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
791 self._c['manager'].Update()
\r
792 for pane in self._c['manager'].GetAllPanes():
\r
793 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
794 pane.Check(pane.window.IsShown())
\r
796 def _on_save_perspective(self, *args):
\r
797 perspective = self._c['manager'].SavePerspective()
\r
798 name = self.gui.config['active perspective']
\r
799 if name == 'Default':
\r
800 name = 'New perspective'
\r
801 name = select_save_file(
\r
802 directory=self.gui.config['perspective path'],
\r
804 extension=self.gui.config['perspective extension'],
\r
806 message='Enter a name for the new perspective:',
\r
807 caption='Save perspective')
\r
810 self._save_perspective(
\r
811 perspective, self.gui.config['perspective path'], name=name,
\r
812 extension=self.gui.config['perspective extension'])
\r
814 def _on_delete_perspective(self, *args, **kwargs):
\r
815 options = sorted([p for p in self._perspectives.keys()
\r
816 if p != 'Default'])
\r
817 dialog = SelectionDialog(
\r
819 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
820 button_id=wx.ID_DELETE,
\r
821 selection_style='multiple',
\r
823 title='Delete perspective(s)',
\r
824 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
825 dialog.CenterOnScreen()
\r
827 names = [options[i] for i in dialog.selected]
\r
829 self._delete_perspectives(
\r
830 self.gui.config['perspective path'], names=names,
\r
831 extension=self.gui.config['perspective extension'])
\r
833 def _on_select_perspective(self, _class, method, name):
\r
834 self._restore_perspective(name)
\r
838 class HookeApp (wx.App):
\r
839 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
841 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
844 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
846 self.commands = commands
\r
847 self.inqueue = inqueue
\r
848 self.outqueue = outqueue
\r
849 super(HookeApp, self).__init__(*args, **kwargs)
\r
852 self.SetAppName('Hooke')
\r
853 self.SetVendorName('')
\r
854 self._setup_splash_screen()
\r
856 height = int(self.gui.config['main height']) # HACK: config should convert
\r
857 width = int(self.gui.config['main width'])
\r
858 top = int(self.gui.config['main top'])
\r
859 left = int(self.gui.config['main left'])
\r
861 # Sometimes, the ini file gets confused and sets 'left' and
\r
862 # 'top' to large negative numbers. Here we catch and fix
\r
863 # this. Keep small negative numbers, the user might want
\r
871 'frame': HookeFrame(
\r
872 self.gui, self.commands, self.inqueue, self.outqueue,
\r
873 parent=None, title='Hooke',
\r
874 pos=(left, top), size=(width, height),
\r
875 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
877 self._c['frame'].Show(True)
\r
878 self.SetTopWindow(self._c['frame'])
\r
881 def _setup_splash_screen(self):
\r
882 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
883 print 'splash', self.gui.config['show splash screen']
\r
884 path = self.gui.config['splash screen image']
\r
885 if os.path.isfile(path):
\r
886 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
888 bitmap=wx.Image(path).ConvertToBitmap(),
\r
889 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
890 milliseconds=duration,
\r
893 # For some reason splashDuration and sleep do not
\r
894 # correspond to each other at least not on Windows.
\r
895 # Maybe it's because duration is in milliseconds and
\r
896 # sleep in seconds. Thus we need to increase the
\r
897 # sleep time a bit. A factor of 1.2 seems to work.
\r
899 time.sleep(sleepFactor * duration / 1000)
\r
902 class GUI (UserInterface):
\r
903 """wxWindows graphical user interface.
\r
905 def __init__(self):
\r
906 super(GUI, self).__init__(name='gui')
\r
908 def default_settings(self):
\r
909 """Return a list of :class:`hooke.config.Setting`\s for any
\r
910 configurable UI settings.
\r
912 The suggested section setting is::
\r
914 Setting(section=self.setting_section, help=self.__doc__)
\r
917 Setting(section=self.setting_section, help=self.__doc__),
\r
918 Setting(section=self.setting_section, option='icon image',
\r
919 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
920 help='Path to the hooke icon image.'),
\r
921 Setting(section=self.setting_section, option='show splash screen',
\r
923 help='Enable/disable the splash screen'),
\r
924 Setting(section=self.setting_section, option='splash screen image',
\r
925 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
926 help='Path to the Hooke splash screen image.'),
\r
927 Setting(section=self.setting_section, option='splash screen duration',
\r
929 help='Duration of the splash screen in milliseconds.'),
\r
930 Setting(section=self.setting_section, option='perspective path',
\r
931 value=os.path.join('resources', 'gui', 'perspective'),
\r
932 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
933 Setting(section=self.setting_section, option='perspective extension',
\r
935 help='Extension for perspective files.'),
\r
936 Setting(section=self.setting_section, option='hide extensions',
\r
938 help='Hide file extensions when displaying names.'),
\r
939 Setting(section=self.setting_section, option='folders-workdir',
\r
941 help='This should probably go...'),
\r
942 Setting(section=self.setting_section, option='folders-filters',
\r
944 help='This should probably go...'),
\r
945 Setting(section=self.setting_section, option='active perspective',
\r
947 help='Name of active perspective file (or "Default").'),
\r
948 Setting(section=self.setting_section, option='folders-filter-index',
\r
950 help='This should probably go...'),
\r
951 Setting(section=self.setting_section, option='main height',
\r
953 help='Height of main window in pixels.'),
\r
954 Setting(section=self.setting_section, option='main width',
\r
956 help='Width of main window in pixels.'),
\r
957 Setting(section=self.setting_section, option='main top',
\r
959 help='Pixels from screen top to top of main window.'),
\r
960 Setting(section=self.setting_section, option='main left',
\r
962 help='Pixels from screen left to left of main window.'),
\r
963 Setting(section=self.setting_section, option='selected command',
\r
964 value='load playlist',
\r
965 help='Name of the initially selected command.'),
\r
968 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
972 app = HookeApp(gui=self,
\r
974 inqueue=ui_to_command_queue,
\r
975 outqueue=command_to_ui_queue,
\r
979 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
980 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r