Source code for schrodinger.ui.qt.navtoolbar

"""
Custom versions of the Matplotlib Qt Navigation toolbar

Copyright Schrodinger, LLC. All rights reserved.
"""

import os
import os.path

import matplotlib
from matplotlib import backend_bases
from matplotlib.backend_bases import cursors
from matplotlib.backends.backend_qt5 import SubplotToolQt
from matplotlib.backends.backend_qt5agg import \
    NavigationToolbar2QT as NavToolbarQt

import schrodinger
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import figureoptions
from schrodinger.ui.qt import icons

Qt = QtCore.Qt
maestro = schrodinger.get_maestro()

cursord = {
    cursors.MOVE: QtCore.Qt.SizeAllCursor,
    cursors.HAND: QtCore.Qt.PointingHandCursor,
    cursors.POINTER: QtCore.Qt.ArrowCursor,
    cursors.SELECT_REGION: QtCore.Qt.CrossCursor,
}


[docs]class ImprovedHistoryToolbar(backend_bases.NavigationToolbar2): """ A matplotlib toolbar that handles history properly when the number of axes changes. This code is planned to be included in matplotlib 1.5 (see PANEL-1924 and matplotlib issue 2511 at https://github.com/matplotlib/matplotlib/issues/2511) """
[docs] def __init__(self, *args, **kwargs): self._home_views = {} super(ImprovedHistoryToolbar, self).__init__(*args, **kwargs)
[docs] def back(self, *args): self._update_home_views() super(ImprovedHistoryToolbar, self).back(*args)
[docs] def forward(self, *args): self._update_home_views() super(ImprovedHistoryToolbar, self).forward(*args)
[docs] def home(self, *args): self._update_home_views() super(ImprovedHistoryToolbar, self).home(*args)
[docs] def push_current(self): """ Push the current view limits and position onto their respective stacks """ super(ImprovedHistoryToolbar, self).push_current() self._update_home_views()
def _axes_view(self, ax): """ Return the current view limits for the specified axes :param ax: The axes to get the view limits for :type ax: `matplotlib.axes.AxesSubplot` :return: A tuple of the view limits :rtype: tuple """ xmin, xmax = ax.get_xlim() ymin, ymax = ax.get_ylim() return (xmin, xmax, ymin, ymax) def _axes_pos(self, ax): """ Return the original and modified positions for the specified axes :param ax: The axes to get the positions for :type ax: `matplotlib.axes.AxesSubplot` :return: A tuple of the original and modified positions :rtype: tuple """ return (ax.get_position(True).frozen(), ax.get_position().frozen()) def _update_home_views(self): """ Make sure that self._home_views has an entry for all axes present in the figure """ for a in self.canvas.figure.get_axes(): if a not in self._home_views: self._home_views[a] = self._axes_view(a) def _update_view(self): """ Update the view limits and position for each axes from the current stack position. If any axes are present in the figure that aren't in the current stack position, use the home view limits for those axes and don't update *any* positions. """ nav_info = self._nav_stack() if nav_info is None: return # Retrieve all items at once to avoid any risk of GC deleting an Axes # while in the middle of the loop below. items = list(nav_info.items()) pos = {} lims = {} for ax, (view, (pos_orig, pos_active)) in items: pos[ax] = [pos_orig, pos_active] lims[ax] = view all_axes = self.canvas.figure.get_axes() for a in all_axes: if a in lims: cur_lim = lims[a] else: cur_lim = self._home_views[a] xmin, xmax, ymin, ymax = cur_lim a.set_xlim((xmin, xmax)) a.set_ylim((ymin, ymax)) if set(all_axes).issubset(list(pos)): for a in all_axes: # Restore both the original and modified positions a.set_position(pos[a][0], 'original') a.set_position(pos[a][1], 'active') self.draw()
[docs] def update(self): """ Reset the axes stack """ self._home_views.clear() super(ImprovedHistoryToolbar, self).update()
[docs]class MaestroNavToolbar(NavToolbar): """ The toolbar for the Maestro Manage Plots panel :cvar DEFAULT_ICON_PREFIX: The default prefix to add to icon names. If the icon name starts with a colon, this prefix will not be used. :vartype DEFAULT_ICON_PREFIX: str """ DEFAULT_ICON_PREFIX = ":projplot_icons/"
[docs] def __init__(self, canvas, parent, projectplot, coordinates=True, show_label_button=False): """ Create the toolbar :param canvas: The matplotlib canvas :type canvas: `schrodinger.mpl_backend_agg.FigureCanvasQTAgg` :param parent: The Qt parent widget :type parent: `QtWidgets.QWidget` :param projectplot: The panel that this toolbar is part of :type projectplot: `projplot.ProjPlot` or `projectPlot.ProjectPlot` :param coordinates: Whether the cursor coordinates should be shown on the right side of the toolbar :type coordinates: bool :param show_label_button: Whether a label button should be included :type show_label_button: bool """ self.show_label_button = show_label_button self.controls_visible = True self.plot = projectplot super(MaestroNavToolbar, self).__init__(canvas, parent, coordinates, clipboard=False)
def _populateToolitems(self): # See parent class for docstring self.toolitems = [ ('Home', 'Reset original view', 'home_view', 'home'), ('Back', 'Back to previous view', 'previous_view', 'back'), ('Forward', 'Forward to next view', 'next_view', 'forward'), ('Pan', 'Pan axes with left mouse, zoom with right', 'pan', 'pan'), ('Zoom', 'Zoom to rectangle', 'zoom', 'zoom'), (None, None, None, None), ('Project Table', 'Open Project Table', 'show_pt', "showPT"), (None, None, None, None), ('Include', 'Pick to include entries', 'include', 'pickInclude'), ('Select', 'Pick to select entries', 'select', 'pickSelect') ] if self.show_label_button: self.toolitems.append( ('Label', 'Hover over datapoint to view ' 'property or click to add label', 'label', 'pickLabel')) self.toolitems.extend([ ('Subplots', 'Configure plot', 'configure', 'configure_subplots'), ('Save', 'Save the figure', 'save_image', 'save_figure'), (None, None, None, None), ('Hide Controls', 'Show or hide plot controls', 'toggleControls', 'toggleControls') ]) def _icon(self, name): # See parent class for docstring if name.startswith("toggleControls"): return QtGui.QIcon() elif name.startswith(":"): return QtGui.QIcon(name) else: return QtGui.QIcon(self.DEFAULT_ICON_PREFIX + name) def _init_toolbar(self): """ Add buttons to the toolbar. We override the base class method so we can set the new toggle-able toolbar buttons as checkable. Note that matplotlib sets "pan" and "zoom" as checkable already. """ super(MaestroNavToolbar, self)._init_toolbar() checkable_actions = ["pickInclude", "pickSelect"] if self.show_label_button: checkable_actions.append("pickLabel") for action_name in checkable_actions: self._actions[action_name].setCheckable(True)
[docs] def toggleControls(self): """ Toggle the visibility of the plot controls. """ action = self._actions["toggleControls"] if self.controls_visible: self.controls_visible = False action.setIconText("Show Controls") else: self.controls_visible = True action.setIconText("Hide Controls") self.plot.controlCB()
def _toggleMode(self, name, mode, mode_func): """ Toggle the specified toolbar button on or off :param name: The name of the action (i.e. the name of the slot function this button calls) :type name: str :param mode: The text to put in the toolbar if activating the mode :type mode: str :param mode_func: The function to call if activating the mode :type mode_func: func """ self._clearToggles(name) if self._actions[name].isChecked(): self.mode = mode mode_func() # Force the toolbar text to redraw immediately. Otherwise, we'd have # to wait until the user moves the cursor over the plot. self.set_message(self.mode) def _clearToggles(self, new_name): """ Turn all of the toolbar buttons off except for the specified one :param new_name: The name of the toolbar button to ignore (i.e. the name of the button that was just clicked) :type new_name: str """ for cur_name, cur_action in self._actions.items(): if cur_action.isChecked() and cur_name != new_name: cur_action.setChecked(False) if cur_name == "pan": super(MaestroNavToolbar, self).pan() elif cur_name == "zoom": super(MaestroNavToolbar, self).zoom() self.plot.pickNoMode() self.mode = ""
[docs] def pickLabel(self): """ The function corresponding to the Label button """ self._toggleMode('pickLabel', 'Pick to label points', self.plot.pickLabeled)
[docs] def pickSelect(self): """ The function corresponding to the Select button """ self._toggleMode('pickSelect', 'Pick to select entries', self.plot.pickSelected)
[docs] def pickInclude(self): """ The function corresponding to the Include button """ self._toggleMode('pickInclude', 'Pick to include entries', self.plot.pickIncluded)
[docs] def pan(self): """ Activate pan mode. We override the matplotlib method to make sure that pickLabel, pickSelect, and pickInclude get turned off if they're active. """ self._clearToggles("pan") super(MaestroNavToolbar, self).pan()
[docs] def zoom(self): """ Activate zoom mode. We override the matplotlib method to make sure that pickLabel, pickSelect, and pickInclude get turned off if they're active. """ self._clearToggles("zoom") super(MaestroNavToolbar, self).zoom()
[docs] def showPT(self): """ Issue Maestro commands to show the project table """ maestro.command("showpanel table")
[docs]class SchrodingerSubplotTool(SubplotToolQt): BETTER_LABELS = { 'left': 'Left Margin', 'right': 'Right Margin', 'top': 'Top Margin', 'bottom': 'Bottom Margin', 'hspace': 'Vertical Gap\nBetween Plots', 'wspace': 'Horizontal Gap\nBetween Plots' }
[docs] def __init__(self, *args): SubplotToolQt.__init__(self, *args) labels = self.findChildren(QtWidgets.QLabel) for label in labels: text = str(label.text()) try: new_label = self.BETTER_LABELS[text] except KeyError: continue label.setText(new_label) label.setAlignment(Qt.AlignCenter)
[docs]class FeedbackSubplotToolQt(SubplotToolQt): """ Allows the parent panel to perform pre and post plot modification actions """
[docs] def __init__(self, targetfig, panel): """ :type panel: AppFramework object :param panel: an object with setWaitCursor and restoreCursor methods """ self.panel = panel SubplotToolQt.__init__(self, targetfig, panel)
[docs] def funcleft(self, val): """ Allow the parent panel to to pre and post plot modification actions """ x, y = self.panel.prePlotAction(what='funcleft') SubplotToolQt.funcleft(self, val) self.panel.postPlotAction(x, y, what='funcleft')
[docs] def funcright(self, val): """ Allow the parent panel to to pre and post plot modification actions """ x, y = self.panel.prePlotAction(what='funcright') SubplotToolQt.funcright(self, val) self.panel.postPlotAction(x, y, what='funcright')
[docs] def funcbottom(self, val): """ Allow the parent panel to to pre and post plot modification actions """ x, y = self.panel.prePlotAction(what='funcbottom') SubplotToolQt.funcbottom(self, val) self.panel.postPlotAction(x, y, what='funcbottom')
[docs] def functop(self, val): """ Allow the parent panel to to pre and post plot modification actions """ x, y = self.panel.prePlotAction(what='functop') SubplotToolQt.functop(self, val) self.panel.postPlotAction(x, y, what='functop')
[docs] def funcwspace(self, val): """ Allow the parent panel to to pre and post plot modification actions """ x, y = self.panel.prePlotAction(what='funcwspace') SubplotToolQt.funcwspace(self, val) self.panel.postPlotAction(x, y, what='funcwspace')
[docs] def funchspace(self, val): """ Allow the parent panel to to pre and post plot modification actions """ x, y = self.panel.prePlotAction(what='funchspace') SubplotToolQt.funchspace(self, val) self.panel.postPlotAction(x, y, what='funchspace')
[docs]class AtomInfoToolbar(NavToolbar): """ Overrides the set_message class of the normal toolbar to enable the following enhancements: 1) Print the atom numbers and their distance. 2) Allow the user to turn off pan/zoom or zoom modes by clicking on the depressed toolbar button 3) Call the parent panel prePlotAction and postPlotAction methods before modifying the plot view in any way to allow the parent to properly prepare for the modification (create a wait cursor, add/remove plot features, etc.) Used by residue_distance_map.py & pose_explorer_dir/multi_canvas_toolbar.py """
[docs] def __init__(self, canvas, parent, coordinates=True, **kwargs): """ Create a new toolbar instance :type canvas: FigureCanvasQTAgg :param canvas: The canvas this toolbar belongs to :type parent: CaDistanceGUI :param parent: The parent panel that contains information about atoms and distances. :type coordinates: bool :param coordinates: not used """ self.panel = parent NavToolbar.__init__(self, canvas, parent, coordinates, **kwargs)
[docs] def set_message(self, x, y=None, axes=None): """ Place the atom numbers and distance between them that corresponds to the atoms currently under the cursor. :type x: float :param x: the current x coordinate (in plot units) of the cursor :type y: float :param y: the current y coordinate (in plot units) of the cursor. If y is not supplied by the calling routine, then x will not be of the proper format and we set the label to blank. """ # Some messages have status labels at the beginning, so chop off # everything up to x=. if (x is None) or (y is None) or (x < 0) or (y < 0) or \ (axes and axes != self.panel.sub_plot): self.panel.updateStatus() return try: # Translate the plot coordinates into atom numbers and find the # distance between them. xint = int(x) yint = int(y) atomx = self.panel.ca_atoms[xint] atomy = self.panel.ca_atoms[yint] dist = self.panel.distance_matrix[xint, yint] except (IndexError, AttributeError): self.panel.updateStatus() return resx = self.panel.residueName(atomx) resy = self.panel.residueName(atomy) text = "".join([ 'Atom:Residue (x,y) (', str(atomx), ':', resx, ', ', str(atomy), ':', resy, ') Distance=%.1f' % dist ]) self.panel.updateStatus(message=text)
[docs] def home(self): """ Allow the parent panel to to pre and post plot modification actions """ x, y = self.panel.prePlotAction(what='home') NavToolbar.home(self) self.panel.postPlotAction(x, y, what='home')
[docs] def back(self): """ Allow the parent panel to to pre and post plot modification actions """ x, y = self.panel.prePlotAction(what='back') NavToolbar.back(self) self.panel.postPlotAction(x, y, what='back')
[docs] def forward(self): """ Allow the parent panel to to pre and post plot modification actions """ x, y = self.panel.prePlotAction(what='forward') NavToolbar.forward(self) self.panel.postPlotAction(x, y, what='forward')
[docs] def drag_pan(self, event): """ Allow the parent panel to to pre and post plot modification actions """ x, y = self.panel.prePlotAction(what='pan') NavToolbar.drag_pan(self, event) self.panel.postPlotAction(x, y, what='pan')
[docs] def release_zoom(self, event): """ Allow the parent panel to to pre and post plot modification actions """ x, y = self.panel.prePlotAction(what='zoom') NavToolbar.release_zoom(self, event) self.panel.postPlotAction(x, y, what='zoom')
[docs] def getSubplotDialog(self): """ Over-rides NavToolbar method, to initialize FeedbackSubplotToolQt class in order to use the wait cursor when configuring the plot. """ return FeedbackSubplotToolQt(self.canvas.figure, self.parent)
[docs] def mouse_move(self, event): """ Overwrites the parent routine to always call set_message with the x and y data for the event. :type event: mouse move event :param event: the event object generated by the mouse movement """ if not event.inaxes or not self._active: if self._lastCursor != cursors.POINTER: self.set_cursor(cursors.POINTER) self._lastCursor = cursors.POINTER else: if self._active == 'ZOOM': if self._lastCursor != cursors.SELECT_REGION: self.set_cursor(cursors.SELECT_REGION) self._lastCursor = cursors.SELECT_REGION if self._xypress and len(self._xypress[0]) == 6: # self._xypress variable is used for 2 purposes internally # in matplotlib. use it only when it contains 6-sized items. x, y = event.x, event.y lastx, lasty, a, ind, lim, trans = self._xypress[0] self.draw_rubberband(event, x, y, lastx, lasty) elif (self._active == 'PAN' and self._lastCursor != cursors.MOVE): self.set_cursor(cursors.MOVE) self._lastCursor = cursors.MOVE if event.inaxes and event.inaxes.get_navigate(): try: s = event.inaxes.format_coord(event.xdata, event.ydata) except ValueError: pass except OverflowError: pass self.set_message(event.xdata, y=event.ydata, axes=event.inaxes)
[docs] def isActive(self, actor): """ Checks to see if the action with the name actor active, and if it is, returns it. :type actor: str :param actor: the name of the action to check. The name is what is returned by the action.text() method. :rtype: Action object or False :return: the Action object if it is active, or False if it is not """ for action in self.actions(): if str(action.text()) == actor and action.isChecked(): return action return False