Source code for schrodinger.ui.qt.dialbox

"""

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]