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-details':'click "Help -> License"'},
\r
248 caption='About Hooke',
\r
249 style=wx.OK|wx.ICON_INFORMATION)
\r
253 def _on_close(self, *args):
\r
255 self.gui.config['main height'] = str(self.GetSize().GetHeight())
\r
256 self.gui.config['main left'] = str(self.GetPosition()[0])
\r
257 self.gui.config['main top'] = str(self.GetPosition()[1])
\r
258 self.gui.config['main width'] = str(self.GetSize().GetWidth())
\r
259 # push changes back to Hooke.config?
\r
260 self._c['manager'].UnInit()
\r
261 del self._c['manager']
\r
268 def _command_by_name(self, name):
\r
269 cs = [c for c in self.commands if c.name == name]
\r
271 raise KeyError(name)
\r
273 raise Exception('Multiple commands named "%s"' % name)
\r
276 def execute_command(self, _class=None, method=None,
\r
277 command=None, args=None):
\r
280 if ('property' in self._c
\r
281 and self.gui.config['selected command'] == command):
\r
282 arg_names = [arg.name for arg in command.arguments]
\r
283 for name,value in self._c['property'].get_values().items():
\r
284 if name in arg_names:
\r
286 print 'executing', command.name, args
\r
287 self.inqueue.put(CommandMessage(command, args))
\r
290 msg = self.outqueue.get()
\r
291 results.append(msg)
\r
292 if isinstance(msg, Exit):
\r
295 elif isinstance(msg, CommandExit):
\r
296 # TODO: display command complete
\r
298 elif isinstance(msg, ReloadUserInterfaceConfig):
\r
299 self.gui.reload_config(msg.config)
\r
301 elif isinstance(msg, Request):
\r
302 h = handler.HANDLERS[msg.type]
\r
303 h.run(self, msg) # TODO: pause for response?
\r
306 self, '_postprocess_%s' % command.name.replace(' ', '_'),
\r
307 self._postprocess_text)
\r
308 pp(command=command, args=args, results=results)
\r
311 def _handle_request(self, msg):
\r
312 """Repeatedly try to get a response to `msg`.
\r
315 raise NotImplementedError('_%s_request_prompt' % msg.type)
\r
316 prompt_string = prompt(msg)
\r
317 parser = getattr(self, '_%s_request_parser' % msg.type, None)
\r
319 raise NotImplementedError('_%s_request_parser' % msg.type)
\r
323 self.cmd.stdout.write(''.join([
\r
324 error.__class__.__name__, ': ', str(error), '\n']))
\r
325 self.cmd.stdout.write(prompt_string)
\r
326 value = parser(msg, self.cmd.stdin.readline())
\r
328 response = msg.response(value)
\r
330 except ValueError, error:
\r
332 self.inqueue.put(response)
\r
336 # Command-specific postprocessing
\r
338 def _postprocess_text(self, command, args={}, results=[]):
\r
339 """Print the string representation of the results to the Results window.
\r
341 This is similar to :class:`~hooke.ui.commandline.DoCommand`'s
\r
342 approach, except that :class:`~hooke.ui.commandline.DoCommand`
\r
343 doesn't print some internally handled messages
\r
344 (e.g. :class:`~hooke.interaction.ReloadUserInterfaceConfig`).
\r
346 for result in results:
\r
347 if isinstance(result, CommandExit):
\r
348 self._c['output'].write(result.__class__.__name__+'\n')
\r
349 self._c['output'].write(str(result).rstrip()+'\n')
\r
351 def _postprocess_load_playlist(self, command, args={}, results=None):
\r
352 """Update `self` to show the playlist.
\r
354 if not isinstance(results[-1], Success):
\r
355 self._postprocess_text(command, results=results)
\r
356 assert len(results) == 2, results
\r
357 playlist = results[0]
\r
358 self._c['playlists']._c['tree'].add_playlist(playlist)
\r
360 def _postprocess_get_playlist(self, command, args={}, results=[]):
\r
361 if not isinstance(results[-1], Success):
\r
362 self._postprocess_text(command, results=results)
\r
363 assert len(results) == 2, results
\r
364 playlist = results[0]
\r
365 self._c['playlists']._c['tree'].update_playlist(playlist)
\r
367 def _postprocess_get_curve(self, command, args={}, results=[]):
\r
368 """Update `self` to show the curve.
\r
370 if not isinstance(results[-1], Success):
\r
371 self._postprocess_text(command, results=results)
\r
372 assert len(results) == 2, results
\r
374 if args.get('curve', None) == None:
\r
375 # the command defaults to the current curve of the current playlist
\r
376 results = self.execute_command(
\r
377 command=self._command_by_name('get playlist'))
\r
378 playlist = results[0]
\r
380 raise NotImplementedError()
\r
381 self._c['playlists']._c['tree'].set_selected_curve(
\r
384 def _postprocess_next_curve(self, command, args={}, results=[]):
\r
385 """No-op. Only call 'next curve' via `self._next_curve()`.
\r
389 def _postprocess_previous_curve(self, command, args={}, results=[]):
\r
390 """No-op. Only call 'previous curve' via `self._previous_curve()`.
\r
397 def _GetActiveFileIndex(self):
\r
398 lib.playlist.Playlist = self.GetActivePlaylist()
\r
399 #get the selected item from the tree
\r
400 selected_item = self._c['playlists']._c['tree'].GetSelection()
\r
401 #test if a playlist or a curve was double-clicked
\r
402 if self._c['playlists']._c['tree'].ItemHasChildren(selected_item):
\r
406 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
407 while selected_item.IsOk():
\r
409 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item)
\r
412 def _GetPlaylistTab(self, name):
\r
413 for index, page in enumerate(self._c['notebook']._tabs._pages):
\r
414 if page.caption == name:
\r
418 def select_plugin(self, _class=None, method=None, plugin=None):
\r
421 def AddPlaylistFromFiles(self, files=[], name='Untitled'):
\r
423 playlist = lib.playlist.Playlist(self, self.drivers)
\r
425 playlist.add_curve(item)
\r
426 if playlist.count > 0:
\r
427 playlist.name = self._GetUniquePlaylistName(name)
\r
429 self.AddTayliss(playlist)
\r
431 def AppliesPlotmanipulator(self, name):
\r
433 Returns True if the plotmanipulator 'name' is applied, False otherwise
\r
434 name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten')
\r
436 return self.GetBoolFromConfig('core', 'plotmanipulators', name)
\r
438 def ApplyPlotmanipulators(self, plot, plot_file):
\r
440 Apply all active plotmanipulators.
\r
442 if plot is not None and plot_file is not None:
\r
443 manipulated_plot = copy.deepcopy(plot)
\r
444 for plotmanipulator in self.plotmanipulators:
\r
445 if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name):
\r
446 manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file)
\r
447 return manipulated_plot
\r
449 def GetActiveFigure(self):
\r
450 playlist_name = self.GetActivePlaylistName()
\r
451 figure = self.playlists[playlist_name].figure
\r
452 if figure is not None:
\r
456 def GetActiveFile(self):
\r
457 playlist = self.GetActivePlaylist()
\r
458 if playlist is not None:
\r
459 return playlist.get_active_file()
\r
462 def GetActivePlot(self):
\r
463 playlist = self.GetActivePlaylist()
\r
464 if playlist is not None:
\r
465 return playlist.get_active_file().plot
\r
468 def GetDisplayedPlot(self):
\r
469 plot = copy.deepcopy(self.displayed_plot)
\r
471 #plot.curves = copy.deepcopy(plot.curves)
\r
474 def GetDisplayedPlotCorrected(self):
\r
475 plot = copy.deepcopy(self.displayed_plot)
\r
477 plot.curves = copy.deepcopy(plot.corrected_curves)
\r
480 def GetDisplayedPlotRaw(self):
\r
481 plot = copy.deepcopy(self.displayed_plot)
\r
483 plot.curves = copy.deepcopy(plot.raw_curves)
\r
486 def GetDockArt(self):
\r
487 return self._c['manager'].GetArtProvider()
\r
489 def GetPlotmanipulator(self, name):
\r
491 Returns a plot manipulator function from its name
\r
493 for plotmanipulator in self.plotmanipulators:
\r
494 if plotmanipulator.name == name:
\r
495 return plotmanipulator
\r
498 def HasPlotmanipulator(self, name):
\r
500 returns True if the plotmanipulator 'name' is loaded, False otherwise
\r
502 for plotmanipulator in self.plotmanipulators:
\r
503 if plotmanipulator.command == name:
\r
508 def _on_dir_ctrl_left_double_click(self, event):
\r
509 file_path = self.panelFolders.GetPath()
\r
510 if os.path.isfile(file_path):
\r
511 if file_path.endswith('.hkp'):
\r
512 self.do_loadlist(file_path)
\r
515 def _on_erase_background(self, event):
\r
518 def _on_notebook_page_close(self, event):
\r
519 ctrl = event.GetEventObject()
\r
520 playlist_name = ctrl.GetPageText(ctrl._curpage)
\r
521 self.DeleteFromPlaylists(playlist_name)
\r
523 def OnPaneClose(self, event):
\r
526 def OnPropGridChanged (self, event):
\r
527 prop = event.GetProperty()
\r
529 item_section = self.panelProperties.SelectedTreeItem
\r
530 item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section)
\r
531 plugin = self._c['commands']._c['tree'].GetItemText(item_plugin)
\r
532 config = self.gui.config[plugin]
\r
533 property_section = self._c['commands']._c['tree'].GetItemText(item_section)
\r
534 property_key = prop.GetName()
\r
535 property_value = prop.GetDisplayedString()
\r
537 config[property_section][property_key]['value'] = property_value
\r
539 def OnResultsCheck(self, index, flag):
\r
540 results = self.GetActivePlot().results
\r
541 if results.has_key(self.results_str):
\r
542 results[self.results_str].results[index].visible = flag
\r
543 results[self.results_str].update()
\r
547 def _on_size(self, event):
\r
550 def OnUpdateNote(self, event):
\r
552 Saves the note to the active file.
\r
554 active_file = self.GetActiveFile()
\r
555 active_file.note = self.panelNote.Editor.GetValue()
\r
557 def UpdateNote(self):
\r
558 #update the note for the active file
\r
559 active_file = self.GetActiveFile()
\r
560 if active_file is not None:
\r
561 self.panelNote.Editor.SetValue(active_file.note)
\r
563 def UpdatePlaylistsTreeSelection(self):
\r
564 playlist = self.GetActivePlaylist()
\r
565 if playlist is not None:
\r
566 if playlist.index >= 0:
\r
567 self._c['status bar'].set_playlist(playlist)
\r
571 def UpdatePlot(self, plot=None):
\r
573 def add_to_plot(curve, set_scale=True):
\r
574 if curve.visible and curve.x and curve.y:
\r
575 #get the index of the subplot to use as destination
\r
576 destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1
\r
577 #set all parameters for the plot
\r
578 axes_list[destination].set_title(curve.title)
\r
580 axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x)
\r
581 axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y)
\r
582 #set the formatting details for the scale
\r
583 formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero)
\r
584 formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero)
\r
585 axes_list[destination].xaxis.set_major_formatter(formatter_x)
\r
586 axes_list[destination].yaxis.set_major_formatter(formatter_y)
\r
587 if curve.style == 'plot':
\r
588 axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1)
\r
589 if curve.style == 'scatter':
\r
590 axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2)
\r
591 #add the legend if necessary
\r
593 axes_list[destination].legend()
\r
596 active_file = self.GetActiveFile()
\r
597 if not active_file.driver:
\r
598 #the first time we identify a file, the following need to be set
\r
599 active_file.identify(self.drivers)
\r
600 for curve in active_file.plot.curves:
\r
601 curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals')
\r
602 curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals')
\r
603 curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend')
\r
604 curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix')
\r
605 curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix')
\r
606 if active_file.driver is None:
\r
607 self.AppendToOutput('Invalid file: ' + active_file.filename)
\r
609 self.displayed_plot = copy.deepcopy(active_file.plot)
\r
610 #add raw curves to plot
\r
611 self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves)
\r
612 #apply all active plotmanipulators
\r
613 self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file)
\r
614 #add corrected curves to plot
\r
615 self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves)
\r
618 self.displayed_plot = copy.deepcopy(plot)
\r
620 figure = self.GetActiveFigure()
\r
623 #use '0' instead of e.g. '0.00' for scales
\r
624 use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero')
\r
625 #optionally remove the extension from the title of the plot
\r
626 hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension')
\r
627 if hide_curve_extension:
\r
628 title = lh.remove_extension(self.displayed_plot.title)
\r
630 title = self.displayed_plot.title
\r
631 figure.suptitle(title, fontsize=14)
\r
632 #create the list of all axes necessary (rows and columns)
\r
634 number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves])
\r
635 number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves])
\r
636 for index in range(number_of_rows * number_of_columns):
\r
637 axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1))
\r
639 #add all curves to the corresponding plots
\r
640 for curve in self.displayed_plot.curves:
\r
643 #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot'
\r
644 figure.subplots_adjust(hspace=0.3)
\r
647 self.panelResults.ClearResults()
\r
648 if self.displayed_plot.results.has_key(self.results_str):
\r
649 for curve in self.displayed_plot.results[self.results_str].results:
\r
650 add_to_plot(curve, set_scale=False)
\r
651 self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str])
\r
653 self.panelResults.ClearResults()
\r
655 figure.canvas.draw()
\r
657 def _on_curve_select(self, playlist, curve):
\r
658 #create the plot tab and add playlist to the dictionary
\r
659 plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists))
\r
660 notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True)
\r
661 #tab_index = self._c['notebook'].GetSelection()
\r
662 playlist.figure = plotPanel.get_figure()
\r
663 self.playlists[playlist.name] = playlist
\r
664 #self.playlists[playlist.name] = [playlist, figure]
\r
665 self._c['status bar'].set_playlist(playlist)
\r
670 def _on_playlist_left_doubleclick(self):
\r
671 index = self._c['notebook'].GetSelection()
\r
672 current_playlist = self._c['notebook'].GetPageText(index)
\r
673 if current_playlist != playlist_name:
\r
674 index = self._GetPlaylistTab(playlist_name)
\r
675 self._c['notebook'].SetSelection(index)
\r
676 self._c['status bar'].set_playlist(playlist)
\r
680 def _on_playlist_delete(self, playlist):
\r
681 notebook = self.Parent.plotNotebook
\r
682 index = self.Parent._GetPlaylistTab(playlist.name)
\r
683 notebook.SetSelection(index)
\r
684 notebook.DeletePage(notebook.GetSelection())
\r
685 self.Parent.DeleteFromPlaylists(playlist_name)
\r
689 # Command panel interface
\r
691 def select_command(self, _class, method, command):
\r
692 #self.select_plugin(plugin=command.plugin)
\r
693 if 'assistant' in self._c:
\r
694 self._c['assitant'].ChangeValue(command.help)
\r
695 self._c['property'].clear()
\r
696 for argument in command.arguments:
\r
697 if argument.name == 'help':
\r
700 results = self.execute_command(
\r
701 command=self._command_by_name('playlists'))
\r
702 if not isinstance(results[-1], Success):
\r
703 self._postprocess_text(command, results=results)
\r
706 playlists = results[0]
\r
708 results = self.execute_command(
\r
709 command=self._command_by_name('playlist curves'))
\r
710 if not isinstance(results[-1], Success):
\r
711 self._postprocess_text(command, results=results)
\r
714 curves = results[0]
\r
716 p = prop_from_argument(
\r
717 argument, curves=curves, playlists=playlists)
\r
719 continue # property intentionally not handled (yet)
\r
720 self._c['property'].append_property(p)
\r
722 self.gui.config['selected command'] = command # TODO: push to engine
\r
726 # Playlist panel interface
\r
728 def _on_user_delete_playlist(self, _class, method, playlist):
\r
731 def _on_delete_playlist(self, _class, method, playlist):
\r
732 if hasattr(playlist, 'path') and playlist.path != None:
\r
733 os.remove(playlist.path)
\r
735 def _on_user_delete_curve(self, _class, method, playlist, curve):
\r
738 def _on_delete_curve(self, _class, method, playlist, curve):
\r
739 os.remove(curve.path)
\r
741 def _on_set_selected_playlist(self, _class, method, playlist):
\r
742 """TODO: playlists plugin with `jump to playlist`.
\r
746 def _on_set_selected_curve(self, _class, method, playlist, curve):
\r
747 """Call the `jump to curve` command.
\r
749 TODO: playlists plugin.
\r
751 # TODO: jump to playlist, get playlist
\r
752 index = playlist.index(curve)
\r
753 results = self.execute_command(
\r
754 command=self._command_by_name('jump to curve'),
\r
755 args={'index':index})
\r
756 if not isinstance(results[-1], Success):
\r
758 #results = self.execute_command(
\r
759 # command=self._command_by_name('get playlist'))
\r
760 #if not isinstance(results[-1], Success):
\r
762 self.execute_command(
\r
763 command=self._command_by_name('get curve'))
\r
769 def _next_curve(self, *args):
\r
770 """Call the `next curve` command.
\r
772 results = self.execute_command(
\r
773 command=self._command_by_name('next curve'))
\r
774 if isinstance(results[-1], Success):
\r
775 self.execute_command(
\r
776 command=self._command_by_name('get curve'))
\r
778 def _previous_curve(self, *args):
\r
779 """Call the `previous curve` command.
\r
781 results = self.execute_command(
\r
782 command=self._command_by_name('previous curve'))
\r
783 if isinstance(results[-1], Success):
\r
784 self.execute_command(
\r
785 command=self._command_by_name('get curve'))
\r
789 # Panel display handling
\r
791 def _on_panel_visibility(self, _class, method, panel_name, visible):
\r
792 pane = self._c['manager'].GetPane(panel_name)
\r
795 #if we don't do the following, the Folders pane does not resize properly on hide/show
\r
796 if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked():
\r
797 #folders_size = pane.GetSize()
\r
798 self.panelFolders.Fit()
\r
799 self._c['manager'].Update()
\r
801 def _setup_perspectives(self):
\r
802 """Add perspectives to menubar and _perspectives.
\r
804 self._perspectives = {
\r
805 'Default': self._c['manager'].SavePerspective(),
\r
807 path = self.gui.config['perspective path']
\r
808 if os.path.isdir(path):
\r
809 files = sorted(os.listdir(path))
\r
810 for fname in files:
\r
811 name, extension = os.path.splitext(fname)
\r
812 if extension != self.gui.config['perspective extension']:
\r
814 fpath = os.path.join(path, fname)
\r
815 if not os.path.isfile(fpath):
\r
818 with open(fpath, 'rU') as f:
\r
819 perspective = f.readline()
\r
821 self._perspectives[name] = perspective
\r
823 selected_perspective = self.gui.config['active perspective']
\r
824 if not self._perspectives.has_key(selected_perspective):
\r
825 self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke
\r
827 self._restore_perspective(selected_perspective)
\r
828 self._update_perspective_menu()
\r
830 def _update_perspective_menu(self):
\r
831 self._c['menu bar']._c['perspective'].update(
\r
832 sorted(self._perspectives.keys()),
\r
833 self.gui.config['active perspective'])
\r
835 def _save_perspective(self, perspective, perspective_dir, name,
\r
837 path = os.path.join(perspective_dir, name)
\r
838 if extension != None:
\r
840 if not os.path.isdir(perspective_dir):
\r
841 os.makedirs(perspective_dir)
\r
842 with open(path, 'w') as f:
\r
843 f.write(perspective)
\r
844 self._perspectives[name] = perspective
\r
845 self._restore_perspective(name)
\r
846 self._update_perspective_menu()
\r
848 def _delete_perspectives(self, perspective_dir, names,
\r
852 path = os.path.join(perspective_dir, name)
\r
853 if extension != None:
\r
856 del(self._perspectives[name])
\r
857 self._update_perspective_menu()
\r
858 if self.gui.config['active perspective'] in names:
\r
859 self._restore_perspective('Default')
\r
860 # TODO: does this bug still apply?
\r
861 # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258
\r
862 # http://trac.wxwidgets.org/ticket/3258
\r
863 # ) that makes the radio item indicator in the menu disappear.
\r
864 # The code should be fine once this issue is fixed.
\r
866 def _restore_perspective(self, name):
\r
867 if name != self.gui.config['active perspective']:
\r
868 print 'restoring perspective:', name
\r
869 self.gui.config['active perspective'] = name # TODO: push to engine's Hooke
\r
870 self._c['manager'].LoadPerspective(self._perspectives[name])
\r
871 self._c['manager'].Update()
\r
872 for pane in self._c['manager'].GetAllPanes():
\r
873 if pane.name in self._c['menu bar']._c['view']._c.keys():
\r
874 pane.Check(pane.window.IsShown())
\r
876 def _on_save_perspective(self, *args):
\r
877 perspective = self._c['manager'].SavePerspective()
\r
878 name = self.gui.config['active perspective']
\r
879 if name == 'Default':
\r
880 name = 'New perspective'
\r
881 name = select_save_file(
\r
882 directory=self.gui.config['perspective path'],
\r
884 extension=self.gui.config['perspective extension'],
\r
886 message='Enter a name for the new perspective:',
\r
887 caption='Save perspective')
\r
890 self._save_perspective(
\r
891 perspective, self.gui.config['perspective path'], name=name,
\r
892 extension=self.gui.config['perspective extension'])
\r
894 def _on_delete_perspective(self, *args, **kwargs):
\r
895 options = sorted([p for p in self._perspectives.keys()
\r
896 if p != 'Default'])
\r
897 dialog = SelectionDialog(
\r
899 message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n",
\r
900 button_id=wx.ID_DELETE,
\r
901 selection_style='multiple',
\r
903 title='Delete perspective(s)',
\r
904 style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
\r
905 dialog.CenterOnScreen()
\r
907 names = [options[i] for i in dialog.selected]
\r
909 self._delete_perspectives(
\r
910 self.gui.config['perspective path'], names=names,
\r
911 extension=self.gui.config['perspective extension'])
\r
913 def _on_select_perspective(self, _class, method, name):
\r
914 self._restore_perspective(name)
\r
918 class HookeApp (wx.App):
\r
919 """A :class:`wx.App` wrapper around :class:`HookeFrame`.
\r
921 Tosses up a splash screen and then loads :class:`HookeFrame` in
\r
924 def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs):
\r
926 self.commands = commands
\r
927 self.inqueue = inqueue
\r
928 self.outqueue = outqueue
\r
929 super(HookeApp, self).__init__(*args, **kwargs)
\r
932 self.SetAppName('Hooke')
\r
933 self.SetVendorName('')
\r
934 self._setup_splash_screen()
\r
936 height = int(self.gui.config['main height']) # HACK: config should convert
\r
937 width = int(self.gui.config['main width'])
\r
938 top = int(self.gui.config['main top'])
\r
939 left = int(self.gui.config['main left'])
\r
941 # Sometimes, the ini file gets confused and sets 'left' and
\r
942 # 'top' to large negative numbers. Here we catch and fix
\r
943 # this. Keep small negative numbers, the user might want
\r
951 'frame': HookeFrame(
\r
952 self.gui, self.commands, self.inqueue, self.outqueue,
\r
953 parent=None, title='Hooke',
\r
954 pos=(left, top), size=(width, height),
\r
955 style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN),
\r
957 self._c['frame'].Show(True)
\r
958 self.SetTopWindow(self._c['frame'])
\r
961 def _setup_splash_screen(self):
\r
962 if self.gui.config['show splash screen'] == 'True': # HACK: config should decode
\r
963 print 'splash', self.gui.config['show splash screen']
\r
964 path = self.gui.config['splash screen image']
\r
965 if os.path.isfile(path):
\r
966 duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types
\r
968 bitmap=wx.Image(path).ConvertToBitmap(),
\r
969 splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT,
\r
970 milliseconds=duration,
\r
973 # For some reason splashDuration and sleep do not
\r
974 # correspond to each other at least not on Windows.
\r
975 # Maybe it's because duration is in milliseconds and
\r
976 # sleep in seconds. Thus we need to increase the
\r
977 # sleep time a bit. A factor of 1.2 seems to work.
\r
979 time.sleep(sleepFactor * duration / 1000)
\r
982 class GUI (UserInterface):
\r
983 """wxWindows graphical user interface.
\r
985 def __init__(self):
\r
986 super(GUI, self).__init__(name='gui')
\r
988 def default_settings(self):
\r
989 """Return a list of :class:`hooke.config.Setting`\s for any
\r
990 configurable UI settings.
\r
992 The suggested section setting is::
\r
994 Setting(section=self.setting_section, help=self.__doc__)
\r
997 Setting(section=self.setting_section, help=self.__doc__),
\r
998 Setting(section=self.setting_section, option='icon image',
\r
999 value=os.path.join('doc', 'img', 'microscope.ico'),
\r
1000 help='Path to the hooke icon image.'),
\r
1001 Setting(section=self.setting_section, option='show splash screen',
\r
1003 help='Enable/disable the splash screen'),
\r
1004 Setting(section=self.setting_section, option='splash screen image',
\r
1005 value=os.path.join('doc', 'img', 'hooke.jpg'),
\r
1006 help='Path to the Hooke splash screen image.'),
\r
1007 Setting(section=self.setting_section, option='splash screen duration',
\r
1009 help='Duration of the splash screen in milliseconds.'),
\r
1010 Setting(section=self.setting_section, option='perspective path',
\r
1011 value=os.path.join('resources', 'gui', 'perspective'),
\r
1012 help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH.
\r
1013 Setting(section=self.setting_section, option='perspective extension',
\r
1015 help='Extension for perspective files.'),
\r
1016 Setting(section=self.setting_section, option='hide extensions',
\r
1018 help='Hide file extensions when displaying names.'),
\r
1019 Setting(section=self.setting_section, option='folders-workdir',
\r
1021 help='This should probably go...'),
\r
1022 Setting(section=self.setting_section, option='folders-filters',
\r
1024 help='This should probably go...'),
\r
1025 Setting(section=self.setting_section, option='active perspective',
\r
1027 help='Name of active perspective file (or "Default").'),
\r
1028 Setting(section=self.setting_section, option='folders-filter-index',
\r
1030 help='This should probably go...'),
\r
1031 Setting(section=self.setting_section, option='main height',
\r
1033 help='Height of main window in pixels.'),
\r
1034 Setting(section=self.setting_section, option='main width',
\r
1036 help='Width of main window in pixels.'),
\r
1037 Setting(section=self.setting_section, option='main top',
\r
1039 help='Pixels from screen top to top of main window.'),
\r
1040 Setting(section=self.setting_section, option='main left',
\r
1042 help='Pixels from screen left to left of main window.'),
\r
1043 Setting(section=self.setting_section, option='selected command',
\r
1044 value='load playlist',
\r
1045 help='Name of the initially selected command.'),
\r
1048 def _app(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1052 app = HookeApp(gui=self,
\r
1053 commands=commands,
\r
1054 inqueue=ui_to_command_queue,
\r
1055 outqueue=command_to_ui_queue,
\r
1056 redirect=redirect)
\r
1059 def run(self, commands, ui_to_command_queue, command_to_ui_queue):
\r
1060 app = self._app(commands, ui_to_command_queue, command_to_ui_queue)
\r