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 'delete_playlist':self._on_user_delete_playlist,
\r
122 '_delete_playlist':self._on_delete_playlist,
\r
123 'delete_curve':self._on_user_delete_curve,
\r
124 '_delete_curve':self._on_delete_curve,
\r
125 '_set_selected_curve':self._on_set_selected_curve,
\r
127 config=self.gui.config,
\r
129 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
130 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
133 # ('note', panel.note.Note(
\r
135 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
136 # size=(160, 200)), 'left'),
\r
137 # ('notebook', Notebook(
\r
139 # pos=wx.Point(client_size.x, client_size.y),
\r
140 # size=wx.Size(430, 200),
\r
141 # style=aui.AUI_NB_DEFAULT_STYLE
\r
142 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
143 ('commands', panel.PANELS['commands'](
\r
144 commands=self.commands,
\r
145 selected=self.gui.config['selected command'],
\r
147 'execute': self.execute_command,
\r
148 'select_plugin': self.select_plugin,
\r
149 'select_command': self.select_command,
\r
150 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
153 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
154 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
157 ('property', panel.PANELS['propertyeditor2'](
\r
160 style=wx.WANTS_CHARS,
\r
161 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
163 # ('assistant', wx.TextCtrl(
\r
165 # pos=wx.Point(0, 0),
\r
166 # size=wx.Size(150, 90),
\r
167 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
168 ('output', panel.PANELS['output'](
\r
170 pos=wx.Point(0, 0),
\r
171 size=wx.Size(150, 90),
\r
172 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
174 # ('results', panel.results.Results(self), 'bottom'),
\r
176 self._add_panel(label, p, style)
\r
177 #self._c['assistant'].SetEditable(False)
\r
179 def _add_panel(self, label, panel, style):
\r
180 self._c[label] = panel
\r
181 cap_label = label.capitalize()
\r
182 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
183 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
186 elif style == 'center':
\r
188 elif style == 'left':
\r
190 elif style == 'right':
\r
193 assert style == 'bottom', style
\r
195 self._c['manager'].AddPane(panel, info)
\r
197 def _setup_toolbars(self):
\r
198 self._c['navigation bar'] = navbar.NavBar(
\r
200 'next': self._next_curve,
\r
201 'previous': self._previous_curve,
\r
204 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
205 self._c['manager'].AddPane(
\r
206 self._c['navigation bar'],
\r
207 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
208 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
209 ).RightDockable(False))
\r
211 def _bind_events(self):
\r
212 # TODO: figure out if we can use the eventManager for menu
\r
213 # ranges and events of 'self' without raising an assertion
\r
215 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
216 self.Bind(wx.EVT_SIZE, self._on_size)
\r
217 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
218 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
219 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
221 return # TODO: cleanup
\r
222 for value in self._c['menu bar']._c['view']._c.values():
\r
223 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
225 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
226 self._c['menu bar']._c['perspective']._c['save'])
\r
227 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
228 self._c['menu bar']._c['perspective']._c['delete'])
\r
230 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
231 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
233 # TODO: playlist callbacks
\r
234 return # TODO: cleanup
\r
235 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
237 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
239 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
241 def _on_about(self, *args):
\r
242 dialog = wx.MessageDialog(
\r
244 message=self.gui._splash_text(),
\r
245 caption='About Hooke',
\r
246 style=wx.OK|wx.ICON_INFORMATION)
\r
250 def _on_close(self, *args):
\r
252 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
253 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
254 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
255 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
256 # push changes back to Hooke.config?
\r
257 self._c['manager'].UnInit()
\r
258 del self._c['manager']
\r
265 def _command_by_name(self, name):
\r
266 cs = [c for c in self.commands if c.name == name]
\r
268 raise KeyError(name)
\r
270 raise Exception('Multiple commands named "%s"' % name)
\r
273 def execute_command(self, _class=None, method=None,
\r
274 command=None, args=None):
\r
277 if ('property' in self._c
\r
278 and self.gui.config['selected command'] == command):
\r
279 arg_names = [arg.name for arg in command.arguments]
\r
280 for name,value in self._c['property'].get_values().items():
\r
281 if name in arg_names:
\r
283 print 'executing', command.name, args
\r
284 self.inqueue.put(CommandMessage(command, args))
\r
287 msg = self.outqueue.get()
\r
288 results.append(msg)
\r
289 if isinstance(msg, Exit):
\r
292 elif isinstance(msg, CommandExit):
\r
293 # TODO: display command complete
\r
295 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
296 self.gui.reload_config(msg.config)
\r
298 elif isinstance(msg, Request):
\r
299 h = handler.HANDLERS[msg.type]
\r
300 h.run(self, msg) # TODO: pause for response?
\r
303 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
304 self._postprocess_text)
\r
305 pp(command=command, results=results)
\r
308 def _handle_request(self, msg):
\r
309 """Repeatedly try to get a response to `msg`.
\r
312 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
313 prompt_string = prompt(msg)
\r
314 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
316 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
320 self.cmd.stdout.write(''.join([
\r
321 error.__class__.__name__, ': ', str(error), '\n']))
\r
322 self.cmd.stdout.write(prompt_string)
\r
323 value = parser(msg, self.cmd.stdin.readline())
\r
325 response = msg.response(value)
\r
327 except ValueError, error:
\r
329 self.inqueue.put(response)
\r
333 # Command-specific postprocessing
\r
335 def _postprocess_text(self, command, results):
\r
336 """Print the string representation of the results to the Results window.
\r
338 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
339 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
340 doesn't print some internally handled messages
\r
341 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
343 for result in results:
\r
344 if isinstance(result, CommandExit):
\r
345 self._c['output'].write(result.__class__.__name__+'\n')
\r
346 self._c['output'].write(str(result).rstrip()+'\n')
\r
348 def _postprocess_text(self, command, results):
\r
349 """Print the string representation of the results to the Results window.
\r
351 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
352 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
353 doesn't print some internally handled messages
\r
354 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
356 for result in results:
\r
357 if isinstance(result, CommandExit):
\r
358 self._c['output'].write(result.__class__.__name__+'\n')
\r
359 self._c['output'].write(str(result).rstrip()+'\n')
\r
361 def _postprocess_load_playlist(self, command, results):
\r
362 """Update `self` to show the playlist.
\r
364 if not isinstance(results[-1], Success):
\r
365 return # error executing 'load playlist'
\r
366 assert len(results) == 2, results
\r
367 playlist = results[0]
\r
370 self._c['playlists']._c['tree'].add_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
687 # Playlist panel interface
\r
689 def _on_user_delete_playlist(self, _class, method, playlist):
\r
692 def _on_delete_playlist(self, _class, method, playlist):
\r
693 if hasattr(playlist, 'path') and playlist.path != None:
\r
694 os.remove(playlist.path)
\r
696 def _on_user_delete_curve(self, _class, method, playlist, curve):
\r
699 def _on_delete_curve(self, _class, method, playlist, curve):
\r
700 os.remove(curve.path)
\r
702 def _on_set_selected_curve(self, _class, method, playlist, curve):
\r
703 print 'selected', playlist.name, curve.name
\r
709 def _next_curve(self, *args):
\r
710 """Call the `next curve` command.
\r
712 results = self.execute_command(
\r
713 command=self._command_by_name('next curve'))
\r
714 if isinstance(results[-1], Success):
\r
715 self.execute_command(
\r
716 command=self._command_by_name('get curve'))
\r
718 def _previous_curve(self, *args):
\r
719 """Call the `previous curve` command.
\r
721 self.execute_command(
\r
722 command=self._command_by_name('previous curve'))
\r
723 if isinstance(results[-1], Success):
\r
724 self.execute_command(
\r
725 command=self._command_by_name('get curve'))
\r
729 # Panel display handling
\r
731 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
732 pane = self._c['manager'].GetPane(panel_name)
\r
735 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
736 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
737 #folders_size = pane.GetSize()
\r
738 self.panelFolders.Fit()
\r
739 self._c['manager'].Update()
\r
741 def _setup_perspectives(self):
\r
742 """Add perspectives to menubar and _perspectives.
\r
744 self._perspectives = {
\r
745 'Default': self._c['manager'].SavePerspective(),
\r
747 path = self.gui.config['perspective path']
\r
748 if os.path.isdir(path):
\r
749 files = sorted(os.listdir(path))
\r
750 for fname in files:
\r
751 name, extension = os.path.splitext(fname)
\r
752 if extension != self.gui.config['perspective extension']:
\r
754 fpath = os.path.join(path, fname)
\r
755 if not os.path.isfile(fpath):
\r
758 with open(fpath, 'rU') as f:
\r
759 perspective = f.readline()
\r
761 self._perspectives[name] = perspective
\r
763 selected_perspective = self.gui.config['active perspective']
\r
764 if not self._perspectives.has_key(selected_perspective):
\r
765 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
767 self._restore_perspective(selected_perspective)
\r
768 self._update_perspective_menu()
\r
770 def _update_perspective_menu(self):
\r
771 self._c['menu bar']._c['perspective'].update(
\r
772 sorted(self._perspectives.keys()),
\r
773 self.gui.config['active perspective'])
\r
775 def _save_perspective(self, perspective, perspective_dir, name,
\r
777 path = os.path.join(perspective_dir, name)
\r
778 if extension != None:
\r
780 if not os.path.isdir(perspective_dir):
\r
781 os.makedirs(perspective_dir)
\r
782 with open(path, 'w') as f:
\r
783 f.write(perspective)
\r
784 self._perspectives[name] = perspective
\r
785 self._restore_perspective(name)
\r
786 self._update_perspective_menu()
\r
788 def _delete_perspectives(self, perspective_dir, names,
\r
792 path = os.path.join(perspective_dir, name)
\r
793 if extension != None:
\r
796 del(self._perspectives[name])
\r
797 self._update_perspective_menu()
\r
798 if self.gui.config['active perspective'] in names:
\r
799 self._restore_perspective('Default')
\r
800 # TODO: does this bug still apply?
\r
801 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
802 # http://trac.wxwidgets.org/ticket/3258
\r
803 # ) that makes the radio item indicator in the menu disappear.
\r
804 # The code should be fine once this issue is fixed.
\r
806 def _restore_perspective(self, name):
\r
807 if name != self.gui.config['active perspective']:
\r
808 print 'restoring perspective:', name
\r
809 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
810 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
811 self._c['manager'].Update()
\r
812 for pane in self._c['manager'].GetAllPanes():
\r
813 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
814 pane.Check(pane.window.IsShown())
\r
816 def _on_save_perspective(self, *args):
\r
817 perspective = self._c['manager'].SavePerspective()
\r
818 name = self.gui.config['active perspective']
\r
819 if name == 'Default':
\r
820 name = 'New perspective'
\r
821 name = select_save_file(
\r
822 directory=self.gui.config['perspective path'],
\r
824 extension=self.gui.config['perspective extension'],
\r
826 message='Enter a name for the new perspective:',
\r
827 caption='Save perspective')
\r
830 self._save_perspective(
\r
831 perspective, self.gui.config['perspective path'], name=name,
\r
832 extension=self.gui.config['perspective extension'])
\r
834 def _on_delete_perspective(self, *args, **kwargs):
\r
835 options = sorted([p for p in self._perspectives.keys()
\r
836 if p != 'Default'])
\r
837 dialog = SelectionDialog(
\r
839 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
840 button_id=wx.ID_DELETE,
\r
841 selection_style='multiple',
\r
843 title='Delete perspective(s)',
\r
844 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
845 dialog.CenterOnScreen()
\r
847 names = [options[i] for i in dialog.selected]
\r
849 self._delete_perspectives(
\r
850 self.gui.config['perspective path'], names=names,
\r
851 extension=self.gui.config['perspective extension'])
\r
853 def _on_select_perspective(self, _class, method, name):
\r
854 self._restore_perspective(name)
\r
858 class HookeApp (wx.App):
\r
859 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
861 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
864 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
866 self.commands = commands
\r
867 self.inqueue = inqueue
\r
868 self.outqueue = outqueue
\r
869 super(HookeApp, self).__init__(*args, **kwargs)
\r
872 self.SetAppName('Hooke')
\r
873 self.SetVendorName('')
\r
874 self._setup_splash_screen()
\r
876 height = int(self.gui.config['main height']) # HACK: config should convert
\r
877 width = int(self.gui.config['main width'])
\r
878 top = int(self.gui.config['main top'])
\r
879 left = int(self.gui.config['main left'])
\r
881 # Sometimes, the ini file gets confused and sets 'left' and
\r
882 # 'top' to large negative numbers. Here we catch and fix
\r
883 # this. Keep small negative numbers, the user might want
\r
891 'frame': HookeFrame(
\r
892 self.gui, self.commands, self.inqueue, self.outqueue,
\r
893 parent=None, title='Hooke',
\r
894 pos=(left, top), size=(width, height),
\r
895 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
897 self._c['frame'].Show(True)
\r
898 self.SetTopWindow(self._c['frame'])
\r
901 def _setup_splash_screen(self):
\r
902 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
903 print 'splash', self.gui.config['show splash screen']
\r
904 path = self.gui.config['splash screen image']
\r
905 if os.path.isfile(path):
\r
906 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
908 bitmap=wx.Image(path).ConvertToBitmap(),
\r
909 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
910 milliseconds=duration,
\r
913 # For some reason splashDuration and sleep do not
\r
914 # correspond to each other at least not on Windows.
\r
915 # Maybe it's because duration is in milliseconds and
\r
916 # sleep in seconds. Thus we need to increase the
\r
917 # sleep time a bit. A factor of 1.2 seems to work.
\r
919 time.sleep(sleepFactor * duration / 1000)
\r
922 class GUI (UserInterface):
\r
923 """wxWindows graphical user interface.
\r
925 def __init__(self):
\r
926 super(GUI, self).__init__(name='gui')
\r
928 def default_settings(self):
\r
929 """Return a list of :class:`hooke.config.Setting`\s for any
\r
930 configurable UI settings.
\r
932 The suggested section setting is::
\r
934 Setting(section=self.setting_section, help=self.__doc__)
\r
937 Setting(section=self.setting_section, help=self.__doc__),
\r
938 Setting(section=self.setting_section, option='icon image',
\r
939 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
940 help='Path to the hooke icon image.'),
\r
941 Setting(section=self.setting_section, option='show splash screen',
\r
943 help='Enable/disable the splash screen'),
\r
944 Setting(section=self.setting_section, option='splash screen image',
\r
945 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
946 help='Path to the Hooke splash screen image.'),
\r
947 Setting(section=self.setting_section, option='splash screen duration',
\r
949 help='Duration of the splash screen in milliseconds.'),
\r
950 Setting(section=self.setting_section, option='perspective path',
\r
951 value=os.path.join('resources', 'gui', 'perspective'),
\r
952 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
953 Setting(section=self.setting_section, option='perspective extension',
\r
955 help='Extension for perspective files.'),
\r
956 Setting(section=self.setting_section, option='hide extensions',
\r
958 help='Hide file extensions when displaying names.'),
\r
959 Setting(section=self.setting_section, option='folders-workdir',
\r
961 help='This should probably go...'),
\r
962 Setting(section=self.setting_section, option='folders-filters',
\r
964 help='This should probably go...'),
\r
965 Setting(section=self.setting_section, option='active perspective',
\r
967 help='Name of active perspective file (or "Default").'),
\r
968 Setting(section=self.setting_section, option='folders-filter-index',
\r
970 help='This should probably go...'),
\r
971 Setting(section=self.setting_section, option='main height',
\r
973 help='Height of main window in pixels.'),
\r
974 Setting(section=self.setting_section, option='main width',
\r
976 help='Width of main window in pixels.'),
\r
977 Setting(section=self.setting_section, option='main top',
\r
979 help='Pixels from screen top to top of main window.'),
\r
980 Setting(section=self.setting_section, option='main left',
\r
982 help='Pixels from screen left to left of main window.'),
\r
983 Setting(section=self.setting_section, option='selected command',
\r
984 value='load playlist',
\r
985 help='Name of the initially selected command.'),
\r
988 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
992 app = HookeApp(gui=self,
\r
994 inqueue=ui_to_command_queue,
\r
995 outqueue=command_to_ui_queue,
\r
999 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1000 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r