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 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