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 '_on_set_selected_playlist':self._on_set_selected_playlist,
\r
126 '_on_set_selected_curve':self._on_set_selected_curve,
\r
128 config=self.gui.config,
\r
130 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
131 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
134 # ('note', panel.note.Note(
\r
136 # style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
137 # size=(160, 200)), 'left'),
\r
138 # ('notebook', Notebook(
\r
140 # pos=wx.Point(client_size.x, client_size.y),
\r
141 # size=wx.Size(430, 200),
\r
142 # style=aui.AUI_NB_DEFAULT_STYLE
\r
143 # | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'),
\r
144 ('commands', panel.PANELS['commands'](
\r
145 commands=self.commands,
\r
146 selected=self.gui.config['selected command'],
\r
148 'execute': self.execute_command,
\r
149 'select_plugin': self.select_plugin,
\r
150 'select_command': self.select_command,
\r
151 # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item,
\r
154 style=wx.WANTS_CHARS|wx.NO_BORDER,
\r
155 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
158 ('property', panel.PANELS['propertyeditor2'](
\r
161 style=wx.WANTS_CHARS,
\r
162 # WANTS_CHARS so the panel doesn't eat the Return key.
\r
164 # ('assistant', wx.TextCtrl(
\r
166 # pos=wx.Point(0, 0),
\r
167 # size=wx.Size(150, 90),
\r
168 # style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'),
\r
169 ('output', panel.PANELS['output'](
\r
171 pos=wx.Point(0, 0),
\r
172 size=wx.Size(150, 90),
\r
173 style=wx.TE_READONLY|wx.NO_BORDER|wx.TE_MULTILINE),
\r
175 # ('results', panel.results.Results(self), 'bottom'),
\r
177 self._add_panel(label, p, style)
\r
178 #self._c['assistant'].SetEditable(False)
\r
180 def _add_panel(self, label, panel, style):
\r
181 self._c[label] = panel
\r
182 cap_label = label.capitalize()
\r
183 info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label)
\r
184 info.PaneBorder(False).CloseButton(True).MaximizeButton(False)
\r
187 elif style == 'center':
\r
189 elif style == 'left':
\r
191 elif style == 'right':
\r
194 assert style == 'bottom', style
\r
196 self._c['manager'].AddPane(panel, info)
\r
198 def _setup_toolbars(self):
\r
199 self._c['navigation bar'] = navbar.NavBar(
\r
201 'next': self._next_curve,
\r
202 'previous': self._previous_curve,
\r
205 style=wx.TB_FLAT | wx.TB_NODIVIDER)
\r
206 self._c['manager'].AddPane(
\r
207 self._c['navigation bar'],
\r
208 aui.AuiPaneInfo().Name('Navigation').Caption('Navigation'
\r
209 ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False
\r
210 ).RightDockable(False))
\r
212 def _bind_events(self):
\r
213 # TODO: figure out if we can use the eventManager for menu
\r
214 # ranges and events of 'self' without raising an assertion
\r
216 self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background)
\r
217 self.Bind(wx.EVT_SIZE, self._on_size)
\r
218 self.Bind(wx.EVT_CLOSE, self._on_close)
\r
219 self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)
\r
220 self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close)
\r
222 return # TODO: cleanup
\r
223 for value in self._c['menu bar']._c['view']._c.values():
\r
224 self.Bind(wx.EVT_MENU_RANGE, self._on_view, value)
\r
226 self.Bind(wx.EVT_MENU, self._on_save_perspective,
\r
227 self._c['menu bar']._c['perspective']._c['save'])
\r
228 self.Bind(wx.EVT_MENU, self._on_delete_perspective,
\r
229 self._c['menu bar']._c['perspective']._c['delete'])
\r
231 treeCtrl = self._c['folders'].GetTreeCtrl()
\r
232 treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click)
\r
234 # TODO: playlist callbacks
\r
235 return # TODO: cleanup
\r
236 evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton)
\r
238 self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged)
\r
240 self.panelResults.results_list.OnCheckItem = self.OnResultsCheck
\r
242 def _on_about(self, *args):
\r
243 dialog = wx.MessageDialog(
\r
245 message=self.gui._splash_text(extra_info={
\r
246 'get-warrenty':'click "help->warrenty"',
\r
247 'get-details':'click "help->license"',
\r
249 caption='About Hooke',
\r
250 style=wx.OK|wx.ICON_INFORMATION)
\r
254 def _on_close(self, *args):
\r
256 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
257 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
258 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
259 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
260 # push changes back to Hooke.config?
\r
261 self._c['manager'].UnInit()
\r
262 del self._c['manager']
\r
269 def _command_by_name(self, name):
\r
270 cs = [c for c in self.commands if c.name == name]
\r
272 raise KeyError(name)
\r
274 raise Exception('Multiple commands named "%s"' % name)
\r
277 def execute_command(self, _class=None, method=None,
\r
278 command=None, args=None):
\r
281 if ('property' in self._c
\r
282 and self.gui.config['selected command'] == command):
\r
283 arg_names = [arg.name for arg in command.arguments]
\r
284 for name,value in self._c['property'].get_values().items():
\r
285 if name in arg_names:
\r
287 print 'executing', command.name, args
\r
288 self.inqueue.put(CommandMessage(command, args))
\r
291 msg = self.outqueue.get()
\r
292 results.append(msg)
\r
293 if isinstance(msg, Exit):
\r
296 elif isinstance(msg, CommandExit):
\r
297 # TODO: display command complete
\r
299 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
300 self.gui.reload_config(msg.config)
\r
302 elif isinstance(msg, Request):
\r
303 h = handler.HANDLERS[msg.type]
\r
304 h.run(self, msg) # TODO: pause for response?
\r
307 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
308 self._postprocess_text)
\r
309 pp(command=command, results=results)
\r
312 def _handle_request(self, msg):
\r
313 """Repeatedly try to get a response to `msg`.
\r
316 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
317 prompt_string = prompt(msg)
\r
318 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
320 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
324 self.cmd.stdout.write(''.join([
\r
325 error.__class__.__name__, ': ', str(error), '\n']))
\r
326 self.cmd.stdout.write(prompt_string)
\r
327 value = parser(msg, self.cmd.stdin.readline())
\r
329 response = msg.response(value)
\r
331 except ValueError, error:
\r
333 self.inqueue.put(response)
\r
337 # Command-specific postprocessing
\r
339 def _postprocess_text(self, command, results):
\r
340 """Print the string representation of the results to the Results window.
\r
342 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
343 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
344 doesn't print some internally handled messages
\r
345 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
347 for result in results:
\r
348 if isinstance(result, CommandExit):
\r
349 self._c['output'].write(result.__class__.__name__+'\n')
\r
350 self._c['output'].write(str(result).rstrip()+'\n')
\r
352 def _postprocess_load_playlist(self, command, results):
\r
353 """Update `self` to show the playlist.
\r
355 if not isinstance(results[-1], Success):
\r
356 self._postprocess_text(command, results)
\r
357 assert len(results) == 2, results
\r
358 playlist = results[0]
\r
360 self._c['playlists']._c['tree'].add_playlist(playlist)
\r
362 def _postprocess_get_playlist(self, command, results):
\r
363 if not isinstance(results[-1], Success):
\r
364 self._postprocess_text(command, results)
\r
365 assert len(results) == 2, results
\r
366 playlist = results[0]
\r
368 self._c['playlists']._c['tree'].update_playlist(playlist)
\r
370 def _postprocess_get_curve(self, command, results):
\r
371 """Update `self` to show the curve.
\r
373 if not isinstance(results[-1], Success):
\r
374 self._postprocess_text(command, results)
\r
375 assert len(results) == 2, results
\r
377 playlist = self._c['playlists']._c['tree'].get_selected_playlist()
\r
378 if playlist != None: # TODO: fix once we have hooke.plugin.playlists
\r
379 self._c['playlists']._c['tree'].set_selected_curve(
\r
382 def _postprocess_next_curve(self, command, results):
\r
383 """No-op. Only call 'next curve' via `self._next_curve()`.
\r
387 def _postprocess_previous_curve(self, command, results):
\r
388 """No-op. Only call 'previous curve' via `self._previous_curve()`.
\r
395 def _GetActiveFileIndex(self):
\r
396 lib.playlist.Playlist = self.GetActivePlaylist()
\r
397 #get the selected item from the tree
\r
398 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
399 #test if a playlist or a curve was double-clicked
\r
400 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
404 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
405 while selected_item.IsOk():
\r
407 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
410 def _GetPlaylistTab(self, name):
\r
411 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
412 if page.caption == name:
\r
416 def select_plugin(self, _class=None, method=None, plugin=None):
\r
419 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
421 playlist = lib.playlist.Playlist(self, self.drivers)
\r
423 playlist.add_curve(item)
\r
424 if playlist.count > 0:
\r
425 playlist.name = self._GetUniquePlaylistName(name)
\r
427 self.AddTayliss(playlist)
\r
429 def AppliesPlotmanipulator(self, name):
\r
431 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
432 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
434 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
436 def ApplyPlotmanipulators(self, plot, plot_file):
\r
438 Apply all active plotmanipulators.
\r
440 if plot is not None and plot_file is not None:
\r
441 manipulated_plot = copy.deepcopy(plot)
\r
442 for plotmanipulator in self.plotmanipulators:
\r
443 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
444 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
445 return manipulated_plot
\r
447 def GetActiveFigure(self):
\r
448 playlist_name = self.GetActivePlaylistName()
\r
449 figure = self.playlists[playlist_name].figure
\r
450 if figure is not None:
\r
454 def GetActiveFile(self):
\r
455 playlist = self.GetActivePlaylist()
\r
456 if playlist is not None:
\r
457 return playlist.get_active_file()
\r
460 def GetActivePlot(self):
\r
461 playlist = self.GetActivePlaylist()
\r
462 if playlist is not None:
\r
463 return playlist.get_active_file().plot
\r
466 def GetDisplayedPlot(self):
\r
467 plot = copy.deepcopy(self.displayed_plot)
\r
469 #plot.curves = copy.deepcopy(plot.curves)
\r
472 def GetDisplayedPlotCorrected(self):
\r
473 plot = copy.deepcopy(self.displayed_plot)
\r
475 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
478 def GetDisplayedPlotRaw(self):
\r
479 plot = copy.deepcopy(self.displayed_plot)
\r
481 plot.curves = copy.deepcopy(plot.raw_curves)
\r
484 def GetDockArt(self):
\r
485 return self._c['manager'].GetArtProvider()
\r
487 def GetPlotmanipulator(self, name):
\r
489 Returns a plot manipulator function from its name
\r
491 for plotmanipulator in self.plotmanipulators:
\r
492 if plotmanipulator.name == name:
\r
493 return plotmanipulator
\r
496 def HasPlotmanipulator(self, name):
\r
498 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
500 for plotmanipulator in self.plotmanipulators:
\r
501 if plotmanipulator.command == name:
\r
506 def _on_dir_ctrl_left_double_click(self, event):
\r
507 file_path = self.panelFolders.GetPath()
\r
508 if os.path.isfile(file_path):
\r
509 if file_path.endswith('.hkp'):
\r
510 self.do_loadlist(file_path)
\r
513 def _on_erase_background(self, event):
\r
516 def _on_notebook_page_close(self, event):
\r
517 ctrl = event.GetEventObject()
\r
518 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
519 self.DeleteFromPlaylists(playlist_name)
\r
521 def OnPaneClose(self, event):
\r
524 def OnPropGridChanged (self, event):
\r
525 prop = event.GetProperty()
\r
527 item_section = self.panelProperties.SelectedTreeItem
\r
528 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
529 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
530 config = self.gui.config[plugin]
\r
531 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
532 property_key = prop.GetName()
\r
533 property_value = prop.GetDisplayedString()
\r
535 config[property_section][property_key]['value'] = property_value
\r
537 def OnResultsCheck(self, index, flag):
\r
538 results = self.GetActivePlot().results
\r
539 if results.has_key(self.results_str):
\r
540 results[self.results_str].results[index].visible = flag
\r
541 results[self.results_str].update()
\r
545 def _on_size(self, event):
\r
548 def OnUpdateNote(self, event):
\r
550 Saves the note to the active file.
\r
552 active_file = self.GetActiveFile()
\r
553 active_file.note = self.panelNote.Editor.GetValue()
\r
555 def UpdateNote(self):
\r
556 #update the note for the active file
\r
557 active_file = self.GetActiveFile()
\r
558 if active_file is not None:
\r
559 self.panelNote.Editor.SetValue(active_file.note)
\r
561 def UpdatePlaylistsTreeSelection(self):
\r
562 playlist = self.GetActivePlaylist()
\r
563 if playlist is not None:
\r
564 if playlist.index >= 0:
\r
565 self._c['status bar'].set_playlist(playlist)
\r
569 def UpdatePlot(self, plot=None):
\r
571 def add_to_plot(curve, set_scale=True):
\r
572 if curve.visible and curve.x and curve.y:
\r
573 #get the index of the subplot to use as destination
\r
574 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
575 #set all parameters for the plot
\r
576 axes_list[destination].set_title(curve.title)
\r
578 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
579 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
580 #set the formatting details for the scale
\r
581 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
582 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
583 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
584 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
585 if curve.style == 'plot':
\r
586 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
587 if curve.style == 'scatter':
\r
588 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
589 #add the legend if necessary
\r
591 axes_list[destination].legend()
\r
594 active_file = self.GetActiveFile()
\r
595 if not active_file.driver:
\r
596 #the first time we identify a file, the following need to be set
\r
597 active_file.identify(self.drivers)
\r
598 for curve in active_file.plot.curves:
\r
599 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
600 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
601 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
602 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
603 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
604 if active_file.driver is None:
\r
605 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
607 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
608 #add raw curves to plot
\r
609 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
610 #apply all active plotmanipulators
\r
611 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
612 #add corrected curves to plot
\r
613 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
616 self.displayed_plot = copy.deepcopy(plot)
\r
618 figure = self.GetActiveFigure()
\r
621 #use '0' instead of e.g. '0.00' for scales
\r
622 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
623 #optionally remove the extension from the title of the plot
\r
624 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
625 if hide_curve_extension:
\r
626 title = lh.remove_extension(self.displayed_plot.title)
\r
628 title = self.displayed_plot.title
\r
629 figure.suptitle(title, fontsize=14)
\r
630 #create the list of all axes necessary (rows and columns)
\r
632 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
633 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
634 for index in range(number_of_rows * number_of_columns):
\r
635 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
637 #add all curves to the corresponding plots
\r
638 for curve in self.displayed_plot.curves:
\r
641 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
642 figure.subplots_adjust(hspace=0.3)
\r
645 self.panelResults.ClearResults()
\r
646 if self.displayed_plot.results.has_key(self.results_str):
\r
647 for curve in self.displayed_plot.results[self.results_str].results:
\r
648 add_to_plot(curve, set_scale=False)
\r
649 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
651 self.panelResults.ClearResults()
\r
653 figure.canvas.draw()
\r
655 def _on_curve_select(self, playlist, curve):
\r
656 #create the plot tab and add playlist to the dictionary
\r
657 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
658 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
659 #tab_index = self._c['notebook'].GetSelection()
\r
660 playlist.figure = plotPanel.get_figure()
\r
661 self.playlists[playlist.name] = playlist
\r
662 #self.playlists[playlist.name] = [playlist, figure]
\r
663 self._c['status bar'].set_playlist(playlist)
\r
668 def _on_playlist_left_doubleclick(self):
\r
669 index = self._c['notebook'].GetSelection()
\r
670 current_playlist = self._c['notebook'].GetPageText(index)
\r
671 if current_playlist != playlist_name:
\r
672 index = self._GetPlaylistTab(playlist_name)
\r
673 self._c['notebook'].SetSelection(index)
\r
674 self._c['status bar'].set_playlist(playlist)
\r
678 def _on_playlist_delete(self, playlist):
\r
679 notebook = self.Parent.plotNotebook
\r
680 index = self.Parent._GetPlaylistTab(playlist.name)
\r
681 notebook.SetSelection(index)
\r
682 notebook.DeletePage(notebook.GetSelection())
\r
683 self.Parent.DeleteFromPlaylists(playlist_name)
\r
687 # Command panel interface
\r
689 def select_command(self, _class, method, command):
\r
690 #self.select_plugin(plugin=command.plugin)
\r
691 if 'assistant' in self._c:
\r
692 self._c['assitant'].ChangeValue(command.help)
\r
693 self._c['property'].clear()
\r
694 for argument in command.arguments:
\r
695 if argument.name == 'help':
\r
697 p = prop_from_argument(
\r
698 argument, curves=[], playlists=[]) # TODO: lookup playlists/curves
\r
700 continue # property intentionally not handled (yet)
\r
701 self._c['property'].append_property(p)
\r
703 self.gui.config['selected command'] = command # TODO: push to engine
\r
707 # Playlist panel interface
\r
709 def _on_user_delete_playlist(self, _class, method, playlist):
\r
712 def _on_delete_playlist(self, _class, method, playlist):
\r
713 if hasattr(playlist, 'path') and playlist.path != None:
\r
714 os.remove(playlist.path)
\r
716 def _on_user_delete_curve(self, _class, method, playlist, curve):
\r
719 def _on_delete_curve(self, _class, method, playlist, curve):
\r
720 os.remove(curve.path)
\r
722 def _on_set_selected_playlist(self, _class, method, playlist):
\r
723 """TODO: playlists plugin with `jump to playlist`.
\r
727 def _on_set_selected_curve(self, _class, method, playlist, curve):
\r
728 """Call the `jump to curve` command.
\r
730 TODO: playlists plugin.
\r
732 # TODO: jump to playlist, get playlist
\r
733 index = playlist.index(curve)
\r
734 results = self.execute_command(
\r
735 command=self._command_by_name('jump to curve'),
\r
736 args={'index':index})
\r
737 if not isinstance(results[-1], Success):
\r
739 #results = self.execute_command(
\r
740 # command=self._command_by_name('get playlist'))
\r
741 #if not isinstance(results[-1], Success):
\r
743 self.execute_command(
\r
744 command=self._command_by_name('get curve'))
\r
750 def _next_curve(self, *args):
\r
751 """Call the `next curve` command.
\r
753 results = self.execute_command(
\r
754 command=self._command_by_name('next curve'))
\r
755 if isinstance(results[-1], Success):
\r
756 self.execute_command(
\r
757 command=self._command_by_name('get curve'))
\r
759 def _previous_curve(self, *args):
\r
760 """Call the `previous curve` command.
\r
762 results = self.execute_command(
\r
763 command=self._command_by_name('previous curve'))
\r
764 if isinstance(results[-1], Success):
\r
765 self.execute_command(
\r
766 command=self._command_by_name('get curve'))
\r
770 # Panel display handling
\r
772 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
773 pane = self._c['manager'].GetPane(panel_name)
\r
776 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
777 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
778 #folders_size = pane.GetSize()
\r
779 self.panelFolders.Fit()
\r
780 self._c['manager'].Update()
\r
782 def _setup_perspectives(self):
\r
783 """Add perspectives to menubar and _perspectives.
\r
785 self._perspectives = {
\r
786 'Default': self._c['manager'].SavePerspective(),
\r
788 path = self.gui.config['perspective path']
\r
789 if os.path.isdir(path):
\r
790 files = sorted(os.listdir(path))
\r
791 for fname in files:
\r
792 name, extension = os.path.splitext(fname)
\r
793 if extension != self.gui.config['perspective extension']:
\r
795 fpath = os.path.join(path, fname)
\r
796 if not os.path.isfile(fpath):
\r
799 with open(fpath, 'rU') as f:
\r
800 perspective = f.readline()
\r
802 self._perspectives[name] = perspective
\r
804 selected_perspective = self.gui.config['active perspective']
\r
805 if not self._perspectives.has_key(selected_perspective):
\r
806 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
808 self._restore_perspective(selected_perspective)
\r
809 self._update_perspective_menu()
\r
811 def _update_perspective_menu(self):
\r
812 self._c['menu bar']._c['perspective'].update(
\r
813 sorted(self._perspectives.keys()),
\r
814 self.gui.config['active perspective'])
\r
816 def _save_perspective(self, perspective, perspective_dir, name,
\r
818 path = os.path.join(perspective_dir, name)
\r
819 if extension != None:
\r
821 if not os.path.isdir(perspective_dir):
\r
822 os.makedirs(perspective_dir)
\r
823 with open(path, 'w') as f:
\r
824 f.write(perspective)
\r
825 self._perspectives[name] = perspective
\r
826 self._restore_perspective(name)
\r
827 self._update_perspective_menu()
\r
829 def _delete_perspectives(self, perspective_dir, names,
\r
833 path = os.path.join(perspective_dir, name)
\r
834 if extension != None:
\r
837 del(self._perspectives[name])
\r
838 self._update_perspective_menu()
\r
839 if self.gui.config['active perspective'] in names:
\r
840 self._restore_perspective('Default')
\r
841 # TODO: does this bug still apply?
\r
842 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
843 # http://trac.wxwidgets.org/ticket/3258
\r
844 # ) that makes the radio item indicator in the menu disappear.
\r
845 # The code should be fine once this issue is fixed.
\r
847 def _restore_perspective(self, name):
\r
848 if name != self.gui.config['active perspective']:
\r
849 print 'restoring perspective:', name
\r
850 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
851 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
852 self._c['manager'].Update()
\r
853 for pane in self._c['manager'].GetAllPanes():
\r
854 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
855 pane.Check(pane.window.IsShown())
\r
857 def _on_save_perspective(self, *args):
\r
858 perspective = self._c['manager'].SavePerspective()
\r
859 name = self.gui.config['active perspective']
\r
860 if name == 'Default':
\r
861 name = 'New perspective'
\r
862 name = select_save_file(
\r
863 directory=self.gui.config['perspective path'],
\r
865 extension=self.gui.config['perspective extension'],
\r
867 message='Enter a name for the new perspective:',
\r
868 caption='Save perspective')
\r
871 self._save_perspective(
\r
872 perspective, self.gui.config['perspective path'], name=name,
\r
873 extension=self.gui.config['perspective extension'])
\r
875 def _on_delete_perspective(self, *args, **kwargs):
\r
876 options = sorted([p for p in self._perspectives.keys()
\r
877 if p != 'Default'])
\r
878 dialog = SelectionDialog(
\r
880 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
881 button_id=wx.ID_DELETE,
\r
882 selection_style='multiple',
\r
884 title='Delete perspective(s)',
\r
885 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
886 dialog.CenterOnScreen()
\r
888 names = [options[i] for i in dialog.selected]
\r
890 self._delete_perspectives(
\r
891 self.gui.config['perspective path'], names=names,
\r
892 extension=self.gui.config['perspective extension'])
\r
894 def _on_select_perspective(self, _class, method, name):
\r
895 self._restore_perspective(name)
\r
899 class HookeApp (wx.App):
\r
900 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
902 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
905 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
907 self.commands = commands
\r
908 self.inqueue = inqueue
\r
909 self.outqueue = outqueue
\r
910 super(HookeApp, self).__init__(*args, **kwargs)
\r
913 self.SetAppName('Hooke')
\r
914 self.SetVendorName('')
\r
915 self._setup_splash_screen()
\r
917 height = int(self.gui.config['main height']) # HACK: config should convert
\r
918 width = int(self.gui.config['main width'])
\r
919 top = int(self.gui.config['main top'])
\r
920 left = int(self.gui.config['main left'])
\r
922 # Sometimes, the ini file gets confused and sets 'left' and
\r
923 # 'top' to large negative numbers. Here we catch and fix
\r
924 # this. Keep small negative numbers, the user might want
\r
932 'frame': HookeFrame(
\r
933 self.gui, self.commands, self.inqueue, self.outqueue,
\r
934 parent=None, title='Hooke',
\r
935 pos=(left, top), size=(width, height),
\r
936 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
938 self._c['frame'].Show(True)
\r
939 self.SetTopWindow(self._c['frame'])
\r
942 def _setup_splash_screen(self):
\r
943 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
944 print 'splash', self.gui.config['show splash screen']
\r
945 path = self.gui.config['splash screen image']
\r
946 if os.path.isfile(path):
\r
947 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
949 bitmap=wx.Image(path).ConvertToBitmap(),
\r
950 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
951 milliseconds=duration,
\r
954 # For some reason splashDuration and sleep do not
\r
955 # correspond to each other at least not on Windows.
\r
956 # Maybe it's because duration is in milliseconds and
\r
957 # sleep in seconds. Thus we need to increase the
\r
958 # sleep time a bit. A factor of 1.2 seems to work.
\r
960 time.sleep(sleepFactor * duration / 1000)
\r
963 class GUI (UserInterface):
\r
964 """wxWindows graphical user interface.
\r
966 def __init__(self):
\r
967 super(GUI, self).__init__(name='gui')
\r
969 def default_settings(self):
\r
970 """Return a list of :class:`hooke.config.Setting`\s for any
\r
971 configurable UI settings.
\r
973 The suggested section setting is::
\r
975 Setting(section=self.setting_section, help=self.__doc__)
\r
978 Setting(section=self.setting_section, help=self.__doc__),
\r
979 Setting(section=self.setting_section, option='icon image',
\r
980 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
981 help='Path to the hooke icon image.'),
\r
982 Setting(section=self.setting_section, option='show splash screen',
\r
984 help='Enable/disable the splash screen'),
\r
985 Setting(section=self.setting_section, option='splash screen image',
\r
986 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
987 help='Path to the Hooke splash screen image.'),
\r
988 Setting(section=self.setting_section, option='splash screen duration',
\r
990 help='Duration of the splash screen in milliseconds.'),
\r
991 Setting(section=self.setting_section, option='perspective path',
\r
992 value=os.path.join('resources', 'gui', 'perspective'),
\r
993 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
994 Setting(section=self.setting_section, option='perspective extension',
\r
996 help='Extension for perspective files.'),
\r
997 Setting(section=self.setting_section, option='hide extensions',
\r
999 help='Hide file extensions when displaying names.'),
\r
1000 Setting(section=self.setting_section, option='folders-workdir',
\r
1002 help='This should probably go...'),
\r
1003 Setting(section=self.setting_section, option='folders-filters',
\r
1005 help='This should probably go...'),
\r
1006 Setting(section=self.setting_section, option='active perspective',
\r
1008 help='Name of active perspective file (or "Default").'),
\r
1009 Setting(section=self.setting_section, option='folders-filter-index',
\r
1011 help='This should probably go...'),
\r
1012 Setting(section=self.setting_section, option='main height',
\r
1014 help='Height of main window in pixels.'),
\r
1015 Setting(section=self.setting_section, option='main width',
\r
1017 help='Width of main window in pixels.'),
\r
1018 Setting(section=self.setting_section, option='main top',
\r
1020 help='Pixels from screen top to top of main window.'),
\r
1021 Setting(section=self.setting_section, option='main left',
\r
1023 help='Pixels from screen left to left of main window.'),
\r
1024 Setting(section=self.setting_section, option='selected command',
\r
1025 value='load playlist',
\r
1026 help='Name of the initially selected command.'),
\r
1029 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1033 app = HookeApp(gui=self,
\r
1034 commands=commands,
\r
1035 inqueue=ui_to_command_queue,
\r
1036 outqueue=command_to_ui_queue,
\r
1037 redirect=redirect)
\r
1040 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1041 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r