# Copyright """Defines :class:`GUI` providing a wxWindows interface to Hooke. """ WX_GOOD=['2.8'] import wxversion wxversion.select(WX_GOOD) import copy import os.path import platform import shutil import time import wx.html import wx.aui as aui import wx.lib.evtmgr as evtmgr # wxPropertyGrid included in wxPython >= 2.9.1, until then, see # http://wxpropgrid.sourceforge.net/cgi-bin/index?page=download # until then, we'll avoid it because of the *nix build problems. #import wx.propgrid as wxpg from matplotlib.ticker import FuncFormatter from ... import version from ...command import CommandExit, Exit, Command, Argument, StoreValue from ...config import Setting from ...interaction import Request, BooleanRequest, ReloadUserInterfaceConfig from ...ui import UserInterface, CommandMessage from . import panel as panel from . import prettyformat as prettyformat class Notebook (aui.AuiNotebook): def __init__(self, *args, **kwargs): super(Notebook, self).__init__(*args, **kwargs) self.SetArtProvider(aui.AuiDefaultTabArt()) #uncomment if we find a nice icon #page_bmp = wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, wx.Size(16, 16)) self.AddPage(self._welcome_window(), 'Welcome') def _welcome_window(self): #TODO: move into panel.welcome ctrl = wx.html.HtmlWindow(parent=self, size=wx.Size(400, 300)) lines = [ '
See the DocumentationIndex' % 'http://code.google.com/p/hooke/wiki/DocumentationIndex', 'for more information
', ] ctrl.SetPage('\n'.join(lines)) return ctrl class NavBar (wx.ToolBar): def __init__(self, *args, **kwargs): super(NavBar, self).__init__(*args, **kwargs) self.SetToolBitmapSize(wx.Size(16,16)) self._c = { 'previous': self.AddLabelTool( id=wx.ID_PREVIEW_PREVIOUS, label='Previous', bitmap=wx.ArtProvider_GetBitmap( wx.ART_GO_BACK, wx.ART_OTHER, wx.Size(16, 16)), shortHelp='Previous curve'), 'next': self.AddLabelTool( id=wx.ID_PREVIEW_NEXT, label='Next', bitmap=wx.ArtProvider_GetBitmap( wx.ART_GO_FORWARD, wx.ART_OTHER, wx.Size(16, 16)), shortHelp='Next curve'), } self.Realize() class FileMenu (wx.Menu): def __init__(self, *args, **kwargs): super(FileMenu, self).__init__(*args, **kwargs) self._c = {'exit': self.Append(wx.ID_EXIT)} class ViewMenu (wx.Menu): def __init__(self, *args, **kwargs): super(ViewMenu, self).__init__(*args, **kwargs) self._c = { 'folders': self.AppendCheckItem(id=wx.ID_ANY, text='Folders\tF5'), 'playlist': self.AppendCheckItem( id=wx.ID_ANY, text='Playlists\tF6'), 'commands': self.AppendCheckItem( id=wx.ID_ANY, text='Commands\tF7'), 'assistant': self.AppendCheckItem( id=wx.ID_ANY, text='Assistant\tF9'), 'properties': self.AppendCheckItem( id=wx.ID_ANY, text='Properties\tF8'), 'results': self.AppendCheckItem(id=wx.ID_ANY, text='Results\tF10'), 'output': self.AppendCheckItem(id=wx.ID_ANY, text='Output\tF11'), 'note': self.AppendCheckItem(id=wx.ID_ANY, text='Note\tF12'), } for item in self._c.values(): item.Check() class PerspectiveMenu (wx.Menu): def __init__(self, *args, **kwargs): super(PerspectiveMenu, self).__init__(*args, **kwargs) self._c = {} def update(self, perspectives, selected, callback): """Rebuild the perspectives menu. """ for item in self.GetMenuItems(): self.UnBind(item) self.DeleteItem(item) self._c = { 'save': self.Append(id=wx.ID_ANY, text='Save Perspective'), 'delete': self.Append(id=wx.ID_ANY, text='Delete Perspective'), } self.AppendSeparator() for label in perspectives: self._c[label] = self.AppendRadioItem(id=wx.ID_ANY, text=label) self.Bind(wx.EVT_MENU, callback, self._c[label]) if label == selected: self._c[label].Check(True) class HelpMenu (wx.Menu): def __init__(self, *args, **kwargs): super(HelpMenu, self).__init__(*args, **kwargs) self._c = {'about':self.Append(id=wx.ID_ABOUT)} class MenuBar (wx.MenuBar): def __init__(self, *args, **kwargs): super(MenuBar, self).__init__(*args, **kwargs) self._c = { 'file': FileMenu(), 'view': ViewMenu(), 'perspective': PerspectiveMenu(), 'help': HelpMenu(), } self.Append(self._c['file'], 'File') self.Append(self._c['view'], 'View') self.Append(self._c['perspective'], 'Perspective') self.Append(self._c['help'], 'Help') class StatusBar (wx.StatusBar): def __init__(self, *args, **kwargs): super(StatusBar, self).__init__(*args, **kwargs) self.SetStatusWidths([-2, -3]) self.SetStatusText('Ready', 0) self.SetStatusText(u'Welcome to Hooke (version %s)' % version(), 1) class HookeFrame (wx.Frame): def __init__(self, gui, commands, *args, **kwargs): super(HookeFrame, self).__init__(*args, **kwargs) self.gui = gui self.commands = commands self._perspectives = {} # {name: perspective_str} self._c = {} self.SetIcon(wx.Icon(self.gui.config['icon image'], wx.BITMAP_TYPE_ICO)) # setup frame manager self._c['manager'] = aui.AuiManager() self._c['manager'].SetManagedWindow(self) # set the gradient and drag styles self._c['manager'].GetArtProvider().SetMetric( aui.AUI_DOCKART_GRADIENT_TYPE, aui.AUI_GRADIENT_NONE) self._c['manager'].SetFlags( self._c['manager'].GetFlags() ^ aui.AUI_MGR_TRANSPARENT_DRAG) # Min size for the frame itself isn't completely done. See # the end of FrameManager::Update() for the test code. For # now, just hard code a frame minimum size. self.SetMinSize(wx.Size(500, 500)) self._setup_panels() self._setup_toolbars() self._c['manager'].Update() # commit pending changes # Create the menubar after the panes so that the default # perspective is created with all panes open self._c['menu bar'] = MenuBar( ) self.SetMenuBar(self._c['menu bar']) self._c['status bar'] = StatusBar(self, style=wx.ST_SIZEGRIP) self._update_perspectives() self._bind_events() name = self.gui.config['active perspective'] return # TODO: cleanup menu_item = self.GetPerspectiveMenuItem(name) if menu_item is not None: self._on_restore_perspective(menu_item) #TODO: config setting to remember playlists from last session self.playlists = self._c['playlists'].Playlists self._displayed_plot = None #load default list, if possible self.do_loadlist(self.GetStringFromConfig('core', 'preferences', 'playlist')) def _setup_panels(self): client_size = self.GetClientSize() for label,p,style in [ ('folders', wx.GenericDirCtrl( parent=self, dir=self.gui.config['folders-workdir'], size=(200, 250), style=wx.DIRCTRL_SHOW_FILTERS, filter=self.gui.config['folders-filters'], defaultFilter=int(self.gui.config['folders-filter-index'])), 'left'), #HACK: config should convert ('playlists', panel.playlist.Playlist( config=self.gui.config, callbacks={}, parent=self, style=wx.WANTS_CHARS|wx.NO_BORDER, # WANTS_CHARS so the panel doesn't eat the Return key. size=(160, 200)), 'left'), ('note', panel.note.Note(self), 'left'), ('notebook', Notebook( parent=self, pos=wx.Point(client_size.x, client_size.y), size=wx.Size(430, 200), style=aui.AUI_NB_DEFAULT_STYLE | aui.AUI_NB_TAB_EXTERNAL_MOVE | wx.NO_BORDER), 'center'), ('commands', panel.commands.Commands( commands=self.commands, selected=self.gui.config['selected command'], callbacks={ 'execute': self.execute_command, 'select_plugin': self.select_plugin, 'select_command': self.select_command, # 'selection_changed': self.panelProperties.select(self, method, command), #SelectedTreeItem = selected_item, }, parent=self, style=wx.WANTS_CHARS|wx.NO_BORDER, # WANTS_CHARS so the panel doesn't eat the Return key. size=(160, 200)), 'right'), #('properties', panel.propertyeditor.PropertyEditor(self),'right'), ('assistant', wx.TextCtrl( parent=self, pos=wx.Point(0, 0), size=wx.Size(150, 90), style=wx.NO_BORDER|wx.TE_MULTILINE), 'right'), ('output', wx.TextCtrl( parent=self, pos=wx.Point(0, 0), size=wx.Size(150, 90), style=wx.NO_BORDER|wx.TE_MULTILINE), 'bottom'), ('results', panel.results.Results(self), 'bottom'), ]: self._add_panel(label, p, style) self._c['assistant'].SetEditable(False) def _add_panel(self, label, panel, style): self._c[label] = panel cap_label = label.capitalize() info = aui.AuiPaneInfo().Name(cap_label).Caption(cap_label) if style == 'left': info.Left().CloseButton(True).MaximizeButton(False) elif style == 'center': info.CenterPane().PaneBorder(False) elif style == 'right': info.Right().CloseButton(True).MaximizeButton(False) else: assert style == 'bottom', style info.Bottom().CloseButton(True).MaximizeButton(False) self._c['manager'].AddPane(panel, info) def _setup_toolbars(self): self._c['navbar'] = NavBar(self, style=wx.TB_FLAT | wx.TB_NODIVIDER) self._c['manager'].AddPane( self._c['navbar'], aui.AuiPaneInfo().Name('Navigation').Caption('Navigation' ).ToolbarPane().Top().Layer(1).Row(1).LeftDockable(False ).RightDockable(False)) def _bind_events(self): # TODO: figure out if we can use the eventManager for menu # ranges and events of 'self' without raising an assertion # fail error. self.Bind(wx.EVT_ERASE_BACKGROUND, self._on_erase_background) self.Bind(wx.EVT_SIZE, self._on_size) self.Bind(wx.EVT_CLOSE, self._on_close) self.Bind(wx.EVT_MENU, self._on_close, id=wx.ID_EXIT) self.Bind(wx.EVT_MENU, self._on_about, id=wx.ID_ABOUT) self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose) self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self._on_notebook_page_close) for value in self._c['menu bar']._c['view']._c.values(): self.Bind(wx.EVT_MENU_RANGE, self._on_view, value) self.Bind(wx.EVT_MENU, self._on_save_perspective, self._c['menu bar']._c['perspective']._c['save']) self.Bind(wx.EVT_MENU, self._on_delete_perspective, self._c['menu bar']._c['perspective']._c['delete']) self.Bind(wx.EVT_TOOL, self._on_next, self._c['navbar']._c['next']) self.Bind(wx.EVT_TOOL, self._on_previous,self._c['navbar']._c['previous']) treeCtrl = self._c['folders'].GetTreeCtrl() treeCtrl.Bind(wx.EVT_LEFT_DCLICK, self._on_dir_ctrl_left_double_click) # TODO: playlist callbacks return # TODO: cleanup evtmgr.eventManager.Register(self.OnUpdateNote, wx.EVT_BUTTON, self.panelNote.UpdateButton) #property editor self.panelProperties.pg.Bind(wxpg.EVT_PG_CHANGED, self.OnPropGridChanged) #results panel self.panelResults.results_list.OnCheckItem = self.OnResultsCheck def _GetActiveFileIndex(self): lib.playlist.Playlist = self.GetActivePlaylist() #get the selected item from the tree selected_item = self._c['playlists']._c['tree'].GetSelection() #test if a playlist or a curve was double-clicked if self._c['playlists']._c['tree'].ItemHasChildren(selected_item): return -1 else: count = 0 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item) while selected_item.IsOk(): count += 1 selected_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item) return count def _GetPlaylistTab(self, name): for index, page in enumerate(self._c['notebook']._tabs._pages): if page.caption == name: return index return -1 def _restore_perspective(self, name): # TODO: cleanup self.gui.config['active perspective'] = name # TODO: push to engine's Hooke self._c['manager'].LoadPerspective(self._perspectives[name]) self._c['manager'].Update() for pane in self._c['manager'].GetAllPanes(): if pane.name in self._c['menu bar']._c['view']._c.keys(): pane.Check(pane.window.IsShown()) def _SavePerspectiveToFile(self, name, perspective): filename = ''.join([name, '.txt']) filename = lh.get_file_path(filename, ['perspective']) perspectivesFile = open(filename, 'w') perspectivesFile.write(perspective) perspectivesFile.close() def execute_command(self, _class, method, command, args): self.cmd.inqueue.put(CommandMessage(command, args)) while True: msg = self.cmd.outqueue.get() if isinstance(msg, Exit): return True elif isinstance(msg, CommandExit): self.cmd.stdout.write(msg.__class__.__name__+'\n') self.cmd.stdout.write(str(msg).rstrip()+'\n') break elif isinstance(msg, ReloadUserInterfaceConfig): self.cmd.ui.reload_config(msg.config) continue elif isinstance(msg, Request): self._handle_request(msg) continue self.cmd.stdout.write(str(msg).rstrip()+'\n') #TODO: run the command #command = ''.join(['self.do_', item_text, '()']) #self.AppendToOutput(command + '\n') #exec(command) def select_plugin(self, _class, method, plugin): for option in config[section]: properties.append([option, config[section][option]]) def select_command(self, _class, method, command): self.select_plugin(command.plugin) plugin = self.GetItemText(selected_item) if plugin != 'core': doc_string = eval('plugins.' + plugin + '.' + plugin + 'Commands.__doc__') else: doc_string = 'The module "core" contains Hooke core functionality' if doc_string is not None: self.panelAssistant.ChangeValue(doc_string) else: self.panelAssistant.ChangeValue('') panel.propertyeditor.PropertyEditor.Initialize(self.panelProperties, properties) self.gui.config['selected command'] = command def AddPlaylistFromFiles(self, files=[], name='Untitled'): if files: playlist = lib.playlist.Playlist(self, self.drivers) for item in files: playlist.add_curve(item) if playlist.count > 0: playlist.name = self._GetUniquePlaylistName(name) playlist.reset() self.AddTayliss(playlist) def AppendToOutput(self, text): self.panelOutput.AppendText(''.join([text, '\n'])) def AppliesPlotmanipulator(self, name): ''' Returns True if the plotmanipulator 'name' is applied, False otherwise name does not contain 'plotmanip_', just the name of the plotmanipulator (e.g. 'flatten') ''' return self.GetBoolFromConfig('core', 'plotmanipulators', name) def ApplyPlotmanipulators(self, plot, plot_file): ''' Apply all active plotmanipulators. ''' if plot is not None and plot_file is not None: manipulated_plot = copy.deepcopy(plot) for plotmanipulator in self.plotmanipulators: if self.GetBoolFromConfig('core', 'plotmanipulators', plotmanipulator.name): manipulated_plot = plotmanipulator.method(manipulated_plot, plot_file) return manipulated_plot def GetActiveFigure(self): playlist_name = self.GetActivePlaylistName() figure = self.playlists[playlist_name].figure if figure is not None: return figure return None def GetActiveFile(self): playlist = self.GetActivePlaylist() if playlist is not None: return playlist.get_active_file() return None def GetActivePlot(self): playlist = self.GetActivePlaylist() if playlist is not None: return playlist.get_active_file().plot return None def GetDisplayedPlot(self): plot = copy.deepcopy(self.displayed_plot) #plot.curves = [] #plot.curves = copy.deepcopy(plot.curves) return plot def GetDisplayedPlotCorrected(self): plot = copy.deepcopy(self.displayed_plot) plot.curves = [] plot.curves = copy.deepcopy(plot.corrected_curves) return plot def GetDisplayedPlotRaw(self): plot = copy.deepcopy(self.displayed_plot) plot.curves = [] plot.curves = copy.deepcopy(plot.raw_curves) return plot def GetDockArt(self): return self._c['manager'].GetArtProvider() def GetPlotmanipulator(self, name): ''' Returns a plot manipulator function from its name ''' for plotmanipulator in self.plotmanipulators: if plotmanipulator.name == name: return plotmanipulator return None def GetPerspectiveMenuItem(self, name): if self._perspectives.has_key(name): perspectives_list = [key for key, value in self._perspectives.iteritems()] perspectives_list.sort() index = perspectives_list.index(name) perspective_Id = ID_FirstPerspective + index menu_item = self.MenuBar.FindItemById(perspective_Id) return menu_item else: return None def HasPlotmanipulator(self, name): ''' returns True if the plotmanipulator 'name' is loaded, False otherwise ''' for plotmanipulator in self.plotmanipulators: if plotmanipulator.command == name: return True return False def _on_about(self, event): message = 'Hooke\n\n'+\ 'A free, open source data analysis platform\n\n'+\ 'Copyright 2006-2008 by Massimo Sandal\n'+\ 'Copyright 2010 by Dr. Rolf Schmidt\n\n'+\ 'Hooke is released under the GNU General Public License version 2.' dialog = wx.MessageDialog(self, message, 'About Hooke', wx.OK | wx.ICON_INFORMATION) dialog.ShowModal() dialog.Destroy() def _on_close(self, event): # apply changes self.gui.config['main height'] = str(self.GetSize().GetHeight()) self.gui.config['main left'] = str(self.GetPosition()[0]) self.gui.config['main top'] = str(self.GetPosition()[1]) self.gui.config['main width'] = str(self.GetSize().GetWidth()) # push changes back to Hooke.config? self._c['manager'].UnInit() del self._c['manager'] self.Destroy() def _update_perspectives(self): """Add perspectives to menubar and _perspectives. """ self._perspectives = { 'Default': self._c['manager'].SavePerspective(), } path = self.gui.config['perspective path'] if os.path.isdir(path): files = sorted(os.listdir(path)) for fname in files: name, extension = os.path.splitext(fname) if extension != '.txt': continue fpath = os.path.join(path, fpath) if not os.path.isfile(fpath): continue perspective = None with open(fpath, 'rU') as f: perspective = f.readline() if perspective: self._perspectives[name] = perspective selected_perspective = self.gui.config['active perspective'] if not self._perspectives.has_key(selected_perspective): self.gui.config['active perspective'] = 'Default' # TODO: push to engine's Hooke self._update_perspective_menu() self._restore_perspective(selected_perspective) def _update_perspective_menu(self): self._c['menu bar']._c['perspective'].update( sorted(self._perspectives.keys()), self.gui.config['active perspective'], self._on_restore_perspective) def _on_restore_perspective(self, event): name = self.MenuBar.FindItemById(event.GetId()).GetLabel() self._restore_perspective(name) def _on_save_perspective(self, event): def nameExists(name): menu_position = self.MenuBar.FindMenu('Perspective') menu = self.MenuBar.GetMenu(menu_position) for item in menu.GetMenuItems(): if item.GetText() == name: return True return False done = False while not done: dialog = wx.TextEntryDialog(self, 'Enter a name for the new perspective:', 'Save perspective') dialog.SetValue('New perspective') if dialog.ShowModal() != wx.ID_OK: return else: name = dialog.GetValue() if nameExists(name): dialogConfirm = wx.MessageDialog(self, 'A file with this name already exists.\n\nDo you want to replace it?', 'Confirm', wx.YES_NO|wx.ICON_QUESTION|wx.CENTER) if dialogConfirm.ShowModal() == wx.ID_YES: done = True else: done = True perspective = self._c['manager'].SavePerspective() self._SavePerspectiveToFile(name, perspective) self.gui.config['active perspectives'] = name self._update_perspective_menu() # if nameExists(name): # #check the corresponding menu item # menu_item = self.GetPerspectiveMenuItem(name) # #replace the perspectiveStr in _pespectives # self._perspectives[name] = perspective # else: # #because we deal with radio items, we need to do some extra work # #delete all menu items from the perspectives menu # for item in self._perspectives_menu.GetMenuItems(): # self._perspectives_menu.DeleteItem(item) # #recreate the perspectives menu # self._perspectives_menu.Append(ID_SavePerspective, 'Save Perspective') # self._perspectives_menu.Append(ID_DeletePerspective, 'Delete Perspective') # self._perspectives_menu.AppendSeparator() # #convert the perspectives dictionary into a list # # the list contains: # #[0]: name of the perspective # #[1]: perspective # perspectives_list = [key for key, value in self._perspectives.iteritems()] # perspectives_list.append(name) # perspectives_list.sort() # #add all previous perspectives # for index, item in enumerate(perspectives_list): # menu_item = self._perspectives_menu.AppendRadioItem(ID_FirstPerspective + index, item) # if item == name: # menu_item.Check() # #add the new perspective to _perspectives # self._perspectives[name] = perspective def _on_delete_perspective(self, event): dialog = panel.selection.Selection( options=sorted(os.listdir(self.gui.config['perspective path'])), message="\nPlease check the perspectives\n\nyou want to delete and click 'Delete'.\n", button_id=wx.ID_DELETE, button_callback=self._on_delete_perspective, parent=self, label='Delete perspective(s)', style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) dialog.CenterOnScreen() dialog.ShowModal() dialog.Destroy() self._update_perspective_menu() # Unfortunately, there is a bug in wxWidgets for win32 (Ticket #3258 # http://trac.wxwidgets.org/ticket/3258 # ) that makes the radio item indicator in the menu disappear. # The code should be fine once this issue is fixed. def _on_delete_perspective(self, event, items, selected_items): for item in selected_items: self._perspectives.remove(item) if item == self.gui.config['active perspective']: self.gui.config['active perspective'] = 'Default' path = os.path.join(self.gui.config['perspective path'], item+'.txt') remove(path) self._update_perspective_menu() def _on_dir_ctrl_left_double_click(self, event): file_path = self.panelFolders.GetPath() if os.path.isfile(file_path): if file_path.endswith('.hkp'): self.do_loadlist(file_path) event.Skip() def _on_erase_background(self, event): event.Skip() def OnExit(self, event): self.Close() def _on_next(self, event): ''' NEXT Go to the next curve in the playlist. If we are at the last curve, we come back to the first. ----- Syntax: next, n ''' selected_item = self._c['playlists']._c['tree'].GetSelection() if self._c['playlists']._c['tree'].ItemHasChildren(selected_item): #GetFirstChild returns a tuple #we only need the first element next_item = self._c['playlists']._c['tree'].GetFirstChild(selected_item)[0] else: next_item = self._c['playlists']._c['tree'].GetNextSibling(selected_item) if not next_item.IsOk(): parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item) #GetFirstChild returns a tuple #we only need the first element next_item = self._c['playlists']._c['tree'].GetFirstChild(parent_item)[0] self._c['playlists']._c['tree'].SelectItem(next_item, True) if not self._c['playlists']._c['tree'].ItemHasChildren(selected_item): playlist = self.GetActivePlaylist() if playlist.count > 1: playlist.next() self.statusbar.SetStatusText(playlist.get_status_string(), 0) self.UpdateNote() self.UpdatePlot() def _on_notebook_page_close(self, event): ctrl = event.GetEventObject() playlist_name = ctrl.GetPageText(ctrl._curpage) self.DeleteFromPlaylists(playlist_name) def OnPaneClose(self, event): event.Skip() def _on_previous(self, event): ''' PREVIOUS Go to the previous curve in the playlist. If we are at the first curve, we jump to the last. ------- Syntax: previous, p ''' #playlist = self.playlists[self.GetActivePlaylistName()][0] #select the previous curve and tell the user if we wrapped around #self.AppendToOutput(playlist.previous()) selected_item = self._c['playlists']._c['tree'].GetSelection() if self._c['playlists']._c['tree'].ItemHasChildren(selected_item): previous_item = self._c['playlists']._c['tree'].GetLastChild(selected_item) else: previous_item = self._c['playlists']._c['tree'].GetPrevSibling(selected_item) if not previous_item.IsOk(): parent_item = self._c['playlists']._c['tree'].GetItemParent(selected_item) previous_item = self._c['playlists']._c['tree'].GetLastChild(parent_item) self._c['playlists']._c['tree'].SelectItem(previous_item, True) playlist = self.GetActivePlaylist() if playlist.count > 1: playlist.previous() self.statusbar.SetStatusText(playlist.get_status_string(), 0) self.UpdateNote() self.UpdatePlot() def OnPropGridChanged (self, event): prop = event.GetProperty() if prop: item_section = self.panelProperties.SelectedTreeItem item_plugin = self._c['commands']._c['tree'].GetItemParent(item_section) plugin = self._c['commands']._c['tree'].GetItemText(item_plugin) config = self.gui.config[plugin] property_section = self._c['commands']._c['tree'].GetItemText(item_section) property_key = prop.GetName() property_value = prop.GetDisplayedString() config[property_section][property_key]['value'] = property_value def OnResultsCheck(self, index, flag): results = self.GetActivePlot().results if results.has_key(self.results_str): results[self.results_str].results[index].visible = flag results[self.results_str].update() self.UpdatePlot() def _on_size(self, event): event.Skip() def OnUpdateNote(self, event): ''' Saves the note to the active file. ''' active_file = self.GetActiveFile() active_file.note = self.panelNote.Editor.GetValue() def _on_view(self, event): menu_id = event.GetId() menu_item = self.MenuBar.FindItemById(menu_id) menu_label = menu_item.GetLabel() pane = self._c['manager'].GetPane(menu_label) pane.Show(not pane.IsShown()) #if we don't do the following, the Folders pane does not resize properly on hide/show if pane.caption == 'Folders' and pane.IsShown() and pane.IsDocked(): #folders_size = pane.GetSize() self.panelFolders.Fit() self._c['manager'].Update() def _clickize(self, xvector, yvector, index): ''' Returns a ClickedPoint() object from an index and vectors of x, y coordinates ''' point = lib.clickedpoint.ClickedPoint() point.index = index point.absolute_coords = xvector[index], yvector[index] point.find_graph_coords(xvector, yvector) return point def _delta(self, message='Click 2 points', block=0): ''' Calculates the difference between two clicked points ''' clicked_points = self._measure_N_points(N=2, message=message, block=block) plot = self.GetDisplayedPlotCorrected() curve = plot.curves[block] delta = lib.delta.Delta() delta.point1.x = clicked_points[0].graph_coords[0] delta.point1.y = clicked_points[0].graph_coords[1] delta.point2.x = clicked_points[1].graph_coords[0] delta.point2.y = clicked_points[1].graph_coords[1] delta.units.x = curve.units.x delta.units.y = curve.units.y return delta def _measure_N_points(self, N, message='', block=0): ''' General helper function for N-points measurements By default, measurements are done on the retraction ''' if message: dialog = wx.MessageDialog(None, message, 'Info', wx.OK) dialog.ShowModal() figure = self.GetActiveFigure() xvector = self.displayed_plot.curves[block].x yvector = self.displayed_plot.curves[block].y clicked_points = figure.ginput(N, timeout=-1, show_clicks=True) points = [] for clicked_point in clicked_points: point = lib.clickedpoint.ClickedPoint() point.absolute_coords = clicked_point[0], clicked_point[1] point.dest = 0 #TODO: make this optional? #so far, the clicked point is taken, not the corresponding data point point.find_graph_coords(xvector, yvector) point.is_line_edge = True point.is_marker = True points.append(point) return points def do_copylog(self): ''' Copies all files in the current playlist that have a note to the destination folder. destination: select folder where you want the files to be copied use_LVDT_folder: when checked, the files will be copied to a folder called 'LVDT' in the destination folder (for MFP-1D files only) ''' playlist = self.GetActivePlaylist() if playlist is not None: destination = self.GetStringFromConfig('core', 'copylog', 'destination') if not os.path.isdir(destination): os.makedirs(destination) for current_file in playlist.files: if current_file.note: shutil.copy(current_file.filename, destination) if current_file.driver.filetype == 'mfp1d': filename = current_file.filename.replace('deflection', 'LVDT', 1) path, name = os.path.split(filename) filename = os.path.join(path, 'lvdt', name) use_LVDT_folder = self.GetBoolFromConfig('core', 'copylog', 'use_LVDT_folder') if use_LVDT_folder: destination = os.path.join(destination, 'LVDT') shutil.copy(filename, destination) def do_plotmanipulators(self): ''' Please select the plotmanipulators you would like to use and define the order in which they will be applied to the data. Click 'Execute' to apply your changes. ''' self.UpdatePlot() def do_preferences(self): ''' Please set general preferences for Hooke here. hide_curve_extension: hides the extension of the force curve files. not recommended for 'picoforce' files ''' pass def do_test(self): ''' Use this command for testing purposes. You find do_test in hooke.py. ''' pass def do_version(self): ''' VERSION ------ Prints the current version and codename, plus library version. Useful for debugging. ''' self.AppendToOutput('Hooke ' + __version__ + ' (' + __codename__ + ')') self.AppendToOutput('Released on: ' + __releasedate__) self.AppendToOutput('---') self.AppendToOutput('Python version: ' + python_version) self.AppendToOutput('WxPython version: ' + wx_version) self.AppendToOutput('Matplotlib version: ' + mpl_version) self.AppendToOutput('SciPy version: ' + scipy_version) self.AppendToOutput('NumPy version: ' + numpy_version) self.AppendToOutput('ConfigObj version: ' + configobj_version) self.AppendToOutput('wxPropertyGrid version: ' + '.'.join([str(PROPGRID_MAJOR), str(PROPGRID_MINOR), str(PROPGRID_RELEASE)])) self.AppendToOutput('---') self.AppendToOutput('Platform: ' + str(platform.uname())) self.AppendToOutput('******************************') self.AppendToOutput('Loaded plugins') self.AppendToOutput('---') #sort the plugins into alphabetical order plugins_list = [key for key, value in self.plugins.iteritems()] plugins_list.sort() for plugin in plugins_list: self.AppendToOutput(plugin) def UpdateNote(self): #update the note for the active file active_file = self.GetActiveFile() if active_file is not None: self.panelNote.Editor.SetValue(active_file.note) def UpdatePlaylistsTreeSelection(self): playlist = self.GetActivePlaylist() if playlist is not None: if playlist.index >= 0: self.statusbar.SetStatusText(playlist.get_status_string(), 0) self.UpdateNote() self.UpdatePlot() def UpdatePlot(self, plot=None): def add_to_plot(curve, set_scale=True): if curve.visible and curve.x and curve.y: #get the index of the subplot to use as destination destination = (curve.destination.column - 1) * number_of_rows + curve.destination.row - 1 #set all parameters for the plot axes_list[destination].set_title(curve.title) if set_scale: axes_list[destination].set_xlabel(curve.prefix.x + curve.units.x) axes_list[destination].set_ylabel(curve.prefix.y + curve.units.y) #set the formatting details for the scale formatter_x = lib.curve.PrefixFormatter(curve.decimals.x, curve.prefix.x, use_zero) formatter_y = lib.curve.PrefixFormatter(curve.decimals.y, curve.prefix.y, use_zero) axes_list[destination].xaxis.set_major_formatter(formatter_x) axes_list[destination].yaxis.set_major_formatter(formatter_y) if curve.style == 'plot': axes_list[destination].plot(curve.x, curve.y, color=curve.color, label=curve.label, lw=curve.linewidth, zorder=1) if curve.style == 'scatter': axes_list[destination].scatter(curve.x, curve.y, color=curve.color, label=curve.label, s=curve.size, zorder=2) #add the legend if necessary if curve.legend: axes_list[destination].legend() if plot is None: active_file = self.GetActiveFile() if not active_file.driver: #the first time we identify a file, the following need to be set active_file.identify(self.drivers) for curve in active_file.plot.curves: curve.decimals.x = self.GetIntFromConfig('core', 'preferences', 'x_decimals') curve.decimals.y = self.GetIntFromConfig('core', 'preferences', 'y_decimals') curve.legend = self.GetBoolFromConfig('core', 'preferences', 'legend') curve.prefix.x = self.GetStringFromConfig('core', 'preferences', 'x_prefix') curve.prefix.y = self.GetStringFromConfig('core', 'preferences', 'y_prefix') if active_file.driver is None: self.AppendToOutput('Invalid file: ' + active_file.filename) return self.displayed_plot = copy.deepcopy(active_file.plot) #add raw curves to plot self.displayed_plot.raw_curves = copy.deepcopy(self.displayed_plot.curves) #apply all active plotmanipulators self.displayed_plot = self.ApplyPlotmanipulators(self.displayed_plot, active_file) #add corrected curves to plot self.displayed_plot.corrected_curves = copy.deepcopy(self.displayed_plot.curves) else: active_file = None self.displayed_plot = copy.deepcopy(plot) figure = self.GetActiveFigure() figure.clear() #use '0' instead of e.g. '0.00' for scales use_zero = self.GetBoolFromConfig('core', 'preferences', 'use_zero') #optionally remove the extension from the title of the plot hide_curve_extension = self.GetBoolFromConfig('core', 'preferences', 'hide_curve_extension') if hide_curve_extension: title = lh.remove_extension(self.displayed_plot.title) else: title = self.displayed_plot.title figure.suptitle(title, fontsize=14) #create the list of all axes necessary (rows and columns) axes_list =[] number_of_columns = max([curve.destination.column for curve in self.displayed_plot.curves]) number_of_rows = max([curve.destination.row for curve in self.displayed_plot.curves]) for index in range(number_of_rows * number_of_columns): axes_list.append(figure.add_subplot(number_of_rows, number_of_columns, index + 1)) #add all curves to the corresponding plots for curve in self.displayed_plot.curves: add_to_plot(curve) #make sure the titles of 'subplots' do not overlap with the axis labels of the 'main plot' figure.subplots_adjust(hspace=0.3) #display results self.panelResults.ClearResults() if self.displayed_plot.results.has_key(self.results_str): for curve in self.displayed_plot.results[self.results_str].results: add_to_plot(curve, set_scale=False) self.panelResults.DisplayResults(self.displayed_plot.results[self.results_str]) else: self.panelResults.ClearResults() #refresh the plot figure.canvas.draw() def _on_curve_select(self, playlist, curve): #create the plot tab and add playlist to the dictionary plotPanel = panel.plot.PlotPanel(self, ID_FirstPlot + len(self.playlists)) notebook_tab = self._c['notebook'].AddPage(plotPanel, playlist.name, True) #tab_index = self._c['notebook'].GetSelection() playlist.figure = plotPanel.get_figure() self.playlists[playlist.name] = playlist #self.playlists[playlist.name] = [playlist, figure] self.statusbar.SetStatusText(playlist.get_status_string(), 0) self.UpdateNote() self.UpdatePlot() def _on_playlist_left_doubleclick(self): index = self._c['notebook'].GetSelection() current_playlist = self._c['notebook'].GetPageText(index) if current_playlist != playlist_name: index = self._GetPlaylistTab(playlist_name) self._c['notebook'].SetSelection(index) self.statusbar.SetStatusText(playlist.get_status_string(), 0) self.UpdateNote() self.UpdatePlot() def _on_playlist_delete(self, playlist): notebook = self.Parent.plotNotebook index = self.Parent._GetPlaylistTab(playlist.name) notebook.SetSelection(index) notebook.DeletePage(notebook.GetSelection()) self.Parent.DeleteFromPlaylists(playlist_name) class HookeApp (wx.App): def __init__(self, gui, commands, inqueue, outqueue, *args, **kwargs): self.gui = gui self.commands = commands self.inqueue = inqueue self.outqueue = outqueue super(HookeApp, self).__init__(*args, **kwargs) def OnInit(self): self.SetAppName('Hooke') self.SetVendorName('') self._setup_splash_screen() height = int(self.gui.config['main height']) # HACK: config should convert width = int(self.gui.config['main width']) top = int(self.gui.config['main top']) left = int(self.gui.config['main left']) # Sometimes, the ini file gets confused and sets 'left' and # 'top' to large negative numbers. Here we catch and fix # this. Keep small negative numbers, the user might want # those. if left < -width: left = 0 if top < -height: top = 0 self._c = { 'frame': HookeFrame( self.gui, self.commands, parent=None, title='Hooke', pos=(left, top), size=(width, height), style=wx.DEFAULT_FRAME_STYLE|wx.SUNKEN_BORDER|wx.CLIP_CHILDREN), } self._c['frame'].Show(True) self.SetTopWindow(self._c['frame']) return True def _setup_splash_screen(self): if self.gui.config['show splash screen']: path = self.gui.config['splash screen image'] if os.path.isfile(path): duration = int(self.gui.config['splash screen duration']) # HACK: config should decode types wx.SplashScreen( bitmap=wx.Image(path).ConvertToBitmap(), splashStyle=wx.SPLASH_CENTRE_ON_SCREEN|wx.SPLASH_TIMEOUT, milliseconds=duration, parent=None) wx.Yield() # For some reason splashDuration and sleep do not # correspond to each other at least not on Windows. # Maybe it's because duration is in milliseconds and # sleep in seconds. Thus we need to increase the # sleep time a bit. A factor of 1.2 seems to work. sleepFactor = 1.2 time.sleep(sleepFactor * duration / 1000) def OnExit(self): return True class GUI (UserInterface): """wxWindows graphical user interface. """ def __init__(self): super(GUI, self).__init__(name='gui') def default_settings(self): """Return a list of :class:`hooke.config.Setting`\s for any configurable UI settings. The suggested section setting is:: Setting(section=self.setting_section, help=self.__doc__) """ return [ Setting(section=self.setting_section, help=self.__doc__), Setting(section=self.setting_section, option='icon image', value=os.path.join('doc', 'img', 'microscope.ico'), help='Path to the hooke icon image.'), Setting(section=self.setting_section, option='show splash screen', value=True, help='Enable/disable the splash screen'), Setting(section=self.setting_section, option='splash screen image', value=os.path.join('doc', 'img', 'hooke.jpg'), help='Path to the Hooke splash screen image.'), Setting(section=self.setting_section, option='splash screen duration', value=1000, help='Duration of the splash screen in milliseconds.'), Setting(section=self.setting_section, option='perspective path', value=os.path.join('resources', 'gui', 'perspective'), help='Directory containing perspective files.'), # TODO: allow colon separated list, like $PATH. Setting(section=self.setting_section, option='hide extensions', value=False, help='Hide file extensions when displaying names.'), Setting(section=self.setting_section, option='folders-workdir', value='.', help='This should probably go...'), Setting(section=self.setting_section, option='folders-filters', value='.', help='This should probably go...'), Setting(section=self.setting_section, option='active perspective', value='Default', help='Name of active perspective file (or "Default").'), Setting(section=self.setting_section, option='folders-filter-index', value='0', help='This should probably go...'), Setting(section=self.setting_section, option='main height', value=500, help='Height of main window in pixels.'), Setting(section=self.setting_section, option='main width', value=500, help='Width of main window in pixels.'), Setting(section=self.setting_section, option='main top', value=0, help='Pixels from screen top to top of main window.'), Setting(section=self.setting_section, option='main left', value=0, help='Pixels from screen left to left of main window.'), Setting(section=self.setting_section, option='selected command', value='load playlist', help='Name of the initially selected command.'), ] def _app(self, commands, ui_to_command_queue, command_to_ui_queue): redirect = True if __debug__: redirect=False app = HookeApp(gui=self, commands=commands, inqueue=ui_to_command_queue, outqueue=command_to_ui_queue, redirect=redirect) return app def run(self, commands, ui_to_command_queue, command_to_ui_queue): app = self._app(commands, ui_to_command_queue, command_to_ui_queue) app.MainLoop()