Source code for overlay_widget.shotgun_spinning_widget

# Copyright (c) 2013 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.


import math

from tank.platform.qt import QtCore, QtGui

# load resources
from .ui import resources_rc  # noqa


[docs]class ShotgunSpinningWidget(QtGui.QWidget): """ Overlay widget that can be placed on top over any QT widget. Once you have placed the overlay widget, you can use it to display a spinner or report progress in the form of an arc that goes from 0 to 360 degrees. """ MODE_OFF = 0 MODE_SPIN = 1 MODE_PROGRESS = 2 # Indicates how many times per second does the spinner update. This means every 40ms. _UPDATES_PER_SECOND = 25 def __init__(self, parent): """ :param parent: Widget to attach the overlay to :type parent: :class:`PySide.QtGui.QWidget` """ QtGui.QWidget.__init__(self, parent) # turn off the widget self.setVisible(False) self._mode = self.MODE_OFF # setup spinner timer self._timer = QtCore.QTimer(self) self._timer.timeout.connect(self._on_animation) # This is the current spin angle self._spin_angle = 0 # This is where we need to scroll to. self._spin_angle_to = 0 # This is where we were told last time where we need to spin to self._previous_spin_angle_to = 0 # This counts how many times we've ticked in the last second to know how big the heartbeat # needs to be. self._heartbeat = 0 self._sg_icon = QtGui.QPixmap( ":/tk_framework_qtwidgets.overlay_widget/sg_logo.png" ) ############################################################################################ # public interface
[docs] def start_spin(self): """ Enables the overlay and shows an animated spinner. If you want to stop the spinning, call :meth:`hide`. """ self._timer.start(40) self.setVisible(True) self._mode = self.MODE_SPIN
[docs] def start_progress(self): """ Enables the overlay and shows an animated progress arc. If you want to stop the progress, call :meth:`hide`. """ self.setVisible(True) self._timer.start(1000 / self._UPDATES_PER_SECOND) self._mode = self.MODE_PROGRESS self._spin_angle_to = 0 self._spin_angle = 0
[docs] def report_progress(self, current): """ Updates the widget current progress value. :param current: New value for the progress arc. Must be between 0.0 (nothing) and 1.0 (complete). :type current: float """ # We're about to ask the cursor to reach another point. Make sure that # we are at least caught up with where we were requested to be last time. self._spin_angle = max(self._previous_spin_angle_to, self._spin_angle) self._spin_angle_to = 360 * current self.repaint()
[docs] def hide(self): """ Hides the overlay. """ self._timer.stop() self._mode = self.MODE_OFF self.setVisible(False)
############################################################################################ # internal methods def _on_animation(self): """ Async callback to help animate the widget. """ if self._mode == self.MODE_SPIN: self._spin_angle += 1 if self._spin_angle == 90: self._spin_angle = 0 elif self._mode == self.MODE_PROGRESS: # If the current spin angle has not reached the destination yet, # increment it, but not past where we are supposed to end at. # The progress tries to give a smooth impression of the progress. Instead of jumping straight # to the requested value, it will slide over to that value. Sliding from 0 to 1 however is done in # a single second, so the sliding is still quick to the eye. If there are more than # _UPDATES_PER_SECOND steps, this sliding effect is actually not visible since individual increments # between steps will be smaller than 1 / _UPDATES_PER_SECOND of the circumference. self._spin_angle = min( self._spin_angle_to, self._spin_angle + 360 / self._UPDATES_PER_SECOND ) self._heartbeat = (self._heartbeat + 1) % 25 self.repaint() def _draw_opened_circle(self, painter, start_angle, span_angle): """ Draws an arc around the SG logo. :param painter: Painter object we will draw with. :param start_angle: Angle at which we will start drawing the arc. :param span_angle: Degrees the arc covers. """ # show the spinner painter.translate( (painter.device().width() / 2) - 40, (painter.device().height() / 2) - 40 ) pen = QtGui.QPen(QtGui.QColor("#424141")) pen.setWidth(3) painter.setPen(pen) painter.drawPixmap(QtCore.QPoint(8, 24), self._sg_icon) r = QtCore.QRectF(0.0, 0.0, 80.0, 80.0) # drawArc accepts 1/16th on angles. painter.drawArc(r, start_angle * 16, span_angle * 16) def paintEvent(self, event): """ Render the UI. :param event: Qt Paint event. """ if self._mode == self.MODE_OFF: return painter = QtGui.QPainter() painter.begin(self) try: # set up semi transparent backdrop painter.setRenderHint(QtGui.QPainter.Antialiasing) # now draw different things depending on mode if self._mode == self.MODE_SPIN: self._draw_opened_circle(painter, self._spin_angle * 4, 340) elif self._mode == self.MODE_PROGRESS: self._draw_opened_circle( painter, # Start at noon 90, # Go clockwise -self._spin_angle, ) self._draw_heartbeat(painter) finally: painter.end() def _draw_heartbeat(self, painter): """ Draws the heartbeat of the progress reporter so it doesn't look like the UI has frozen when progress is not updated in a while. :param painter: Painter object that will be used to render. """ # The heartbeat beats one per second. At the halfway point it is at it's # max amplitude. half_update = self._UPDATES_PER_SECOND / 2.0 amplitude = (math.fabs(self._heartbeat - half_update) / half_update) * 6 # Progress reporting starts at -90, which is (0, 1) in Cartesian coordinates. angle = self._spin_angle - 90 y = math.sin(math.radians(angle)) x = math.cos(math.radians(angle)) pen = QtGui.QPen(QtGui.QColor("#424141")) brush = QtGui.QBrush(QtGui.QColor("#424141")) pen.setWidth(1) painter.setPen(pen) painter.setBrush(brush) # Draws the ellipse around the head of the arc. painter.drawEllipse( QtCore.QRectF( x * 40 + 40 - amplitude / 2, y * 40 + 40 - amplitude / 2, amplitude, amplitude, ) )