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