"""
Contains classes for dispalying dials that allow the user to spin continuously
Copyright Schrodinger, LLC. All rights reserved.
"""
# Contributors: Quentin McDonald, Dave Giesen
from past.utils import old_div
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtSvg
from schrodinger.Qt import QtWidgets
from schrodinger.ui.qt import swidgets
from .dialbox_dir import dialbox_qrc # noqa: F401, import for side effects
[docs]class Dial(QtWidgets.QDial):
"""
This class is a modified version of the QDial. Because we are only
concerned about increment or decrement of the value there's no need for
a pointer showing a 'current' value as in the standard Qt dial. Therefore
we handle the drawing of the dial ourselves and use a series of SVG images
to indicate the movement. As the value of the dial is changed we move
through the array of SVG images and display the current one.
This dial only emits the dial_changed_delta signal when the mouse is
dragged over it. Simply clicking on a different location in the dial has no
effect.
"""
# The number of images we will use to display the motion:
NUM_IMAGES = 36
# Emitted with the amount the dial has changed since the last time the
# dial changed value
dial_changed_delta = QtCore.pyqtSignal((str, int))
[docs] def __init__(self, parent=None, identifier=""):
"""
Create a Dial instance
:type parent: QWidget
:param parent: The parent widget
:type identifier: str
:param identifier: The string identifier for this dial - will be emitted
as part of the dial_changed_delta signals
"""
# Initialize the base class - the QDial:
QtWidgets.QDial.__init__(self, parent)
self.setWrapping(True)
self.svg = []
self.current_image = 0
self.identifier = identifier
# Read each image file from the resource module. Note we are using
# SVG format as these look good at any scale. Each image is rotated
# by five degrees relative to the previous one.
for i in range(self.NUM_IMAGES):
self.svg.append(QtSvg.QSvgRenderer(":dial%d.svg" % (i * 5)))
self.degrees_per_image = old_div(360, self.NUM_IMAGES)
self.setMinimum(0)
self.setMaximum(360)
# InvertedAppearance is necessary so that a clockwise rotation about the
# dial translates to a clockwise rotation in angle space (on an XY axis,
# we general think of a rotation from 0-90 as being counterclockwise,
# but on a QDial with default appearance that would be clockwise.
self.setInvertedAppearance(True)
self.last_value = None
self.setValue(0)
policy = self.sizePolicy()
policy.setHeightForWidth(True)
policy.setHorizontalPolicy(policy.Fixed)
policy.setVerticalPolicy(policy.Fixed)
self.setSizePolicy(policy)
[docs] def paintEvent(self, event):
"""
Redefine the paint event as we want to handle the drawing for the
dial widget. This is simply a matter of displaying the current
image as the index for this has already been set by sliderChanged()
method.
:type event: QPaintEvent
:param event: The current QPaintEvent object
"""
painter = QtGui.QPainter(self)
painter.setViewport(0, 0, self.width(), self.height())
self.svg[self.current_image].render(painter)
[docs] def sliderChange(self, change):
"""
We want to know when the value has changed. This allows us to
traverse through the array of images in order to give the impression
of rotation. The paintEvent() method will update the actual image.
Emits signals when the slider value changes
:type change: QAbstractSlider.SliderChange enum
:param change: enum describing what changed
"""
if change == self.SliderValueChange:
current_val = self.value()
if self.last_value is not None:
delta = get_circular_delta(current_val, self.last_value)
increment = old_div(abs(delta), self.degrees_per_image) + 1
if delta < 0:
self.current_image += increment
if self.current_image >= self.NUM_IMAGES:
self.current_image = self.current_image - self.NUM_IMAGES
elif delta > 0:
self.current_image -= increment
if self.current_image < 0:
self.current_image = self.NUM_IMAGES + self.current_image
if delta:
self.dial_changed_delta.emit(self.identifier, delta)
self.last_value = current_val
# Call the base class in order to update the image, this will
# generate a paintEvent()
return QtWidgets.QDial.sliderChange(self, change)
[docs] def sizeHint(self):
"""
Redefine sizeHint() so that we by default set the widget size to
the size of the SVG image.
:rtype: QSize
:return: The size hint for this widget, set to the size of the SVG
image.
"""
if self.svg:
sz = self.svg[self.current_image].defaultSize()
# Return width for height to make it square:
return QtCore.QSize(sz.width(), sz.width())
return QtWidgets.QWidget.sizeHint()
[docs] def heightForWidth(self, width):
"""
Return the width as the recommended height to keep the widget square.
:rtype: int
:return: The recommended height for this widget
"""
return width
[docs] def mousePressEvent(self, event):
"""
Reset the last_value as a sign that the user is starting to drag in the
dial area
:type event: QMousePressEvent
:param event: The event object
"""
self.last_value = None
return QtWidgets.QDial.mousePressEvent(self, event)
[docs]def get_circular_delta(current_value, last_value):
"""
Get the change between current_value and last_value, accounting for cases
where the 0/360 boundary is crossed. For instance, going from 5 to 355 will
have a value of -10, while going from 350 to 3 will have a value of +13.
:type current_value: int
:param current_value: The current value
:type last_value: int
:param last_value: The previous value
:rtype: int
:return: The delta on going from last_value to current_value.
"""
diff = current_value - last_value
if abs(diff) > 90:
# Crossing from 0 to 360: calculate the distance across this boundary
abs_diff = abs(diff)
sign = old_div(diff, abs_diff)
diff = -sign * (360 - abs_diff)
return diff
[docs]class XYZDialBox(QtWidgets.QFrame):
"""
A class that displays a 'virtual dialbox' in a QFrame. The dialbox has
three dials (custom widgets based on QDial), each assigned to the X, Y and Z
axes of rotation.
This class has a dial_changed_delta signal that is emitted each time one of
the dial changes value. The signal contains the identifier for the dial that
change ('x', 'y', or 'z') and the delta value change.
"""
DIAL_NAMES = ['x', 'y', 'z']
# Emitted when one of the dials changes value - gives the change from the
# last time that dial emitted the signal
dial_changed_delta = QtCore.pyqtSignal((str, int))
[docs] def __init__(self):
"""
Create a XYZDialBox instance
"""
QtWidgets.QFrame.__init__(self)
# Use a grid layout for the dials and labels:
self.master_layout = swidgets.SHBoxLayout(self)
self.master_layout.addStretch()
self.grid_layout = swidgets.SGridLayout(layout=self.master_layout)
self.master_layout.addStretch()
self.dials = {}
self.labels = {}
for column, axis in enumerate(self.DIAL_NAMES):
dial = Dial(self, identifier=axis)
self.dials[axis] = dial
self.grid_layout.addWidget(dial, 0, column, 1, 1)
self.labels[axis] = QtWidgets.QLabel("%s Axis" % axis.upper())
self.labels[axis].setAlignment(QtCore.Qt.AlignHCenter)
self.grid_layout.addWidget(self.labels[axis], 1, column, 1, 1)
for dial in self.dials.values():
dial.dial_changed_delta.connect(self.dialChangedDelta)
[docs] def dialChangedDelta(self, which_dial, delta):
"""
This method is called whenever any of the dials change value.
:type which_dial: str
:param which_dial: A string identifier for the dial that changed
:type delta: int
:param delta: The amount the dial value has changed since the last time
this signal was emitted during this drag event.
"""
self.dial_changed_delta.emit(which_dial, delta)
[docs] def values(self):
"""
Get the current value of all dials
:rtype: list
:return: list of ints, each item is the value of a dial
"""
return [self.dials[x].value() for x in self.DIAL_NAMES]