diff --git a/pype/tools/launcher/constants.py b/pype/tools/launcher/constants.py new file mode 100644 index 00000000000..e6dbbb6e192 --- /dev/null +++ b/pype/tools/launcher/constants.py @@ -0,0 +1,12 @@ +from Qt import QtCore + + +ACTION_ROLE = QtCore.Qt.UserRole +GROUP_ROLE = QtCore.Qt.UserRole + 1 +VARIANT_GROUP_ROLE = QtCore.Qt.UserRole + 2 +ACTION_ID_ROLE = QtCore.Qt.UserRole + 3 +ANIMATION_START_ROLE = QtCore.Qt.UserRole + 4 +ANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 5 + + +ANIMATION_LEN = 10 diff --git a/pype/tools/launcher/delegates.py b/pype/tools/launcher/delegates.py index e2eecc6ad5f..cef0f5e1a2b 100644 --- a/pype/tools/launcher/delegates.py +++ b/pype/tools/launcher/delegates.py @@ -1,4 +1,9 @@ +import time from Qt import QtCore, QtWidgets, QtGui +from .constants import ( + ANIMATION_START_ROLE, + ANIMATION_STATE_ROLE +) class ActionDelegate(QtWidgets.QStyledItemDelegate): @@ -9,8 +14,60 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, group_roles, *args, **kwargs): super(ActionDelegate, self).__init__(*args, **kwargs) self.group_roles = group_roles + self._anim_start_color = QtGui.QColor(178, 255, 246) + self._anim_end_color = QtGui.QColor(5, 44, 50) + + def _draw_animation(self, painter, option, index): + grid_size = option.widget.gridSize() + x_offset = int( + (grid_size.width() / 2) + - (option.rect.width() / 2) + ) + item_x = option.rect.x() - x_offset + rect_offset = grid_size.width() / 20 + size = grid_size.width() - (rect_offset * 2) + anim_rect = QtCore.QRect( + item_x + rect_offset, + option.rect.y() + rect_offset, + size, + size + ) + + painter.save() + + painter.setBrush(QtCore.Qt.transparent) + painter.setRenderHint(QtGui.QPainter.Antialiasing) + + gradient = QtGui.QConicalGradient() + gradient.setCenter(anim_rect.center()) + gradient.setColorAt(0, self._anim_start_color) + gradient.setColorAt(1, self._anim_end_color) + + time_diff = time.time() - index.data(ANIMATION_START_ROLE) + + # Repeat 4 times + part_anim = 2.5 + part_time = time_diff % part_anim + offset = (part_time / part_anim) * 360 + angle = (offset + 90) % 360 + + gradient.setAngle(-angle) + + pen = QtGui.QPen(QtGui.QBrush(gradient), rect_offset) + pen.setCapStyle(QtCore.Qt.RoundCap) + painter.setPen(pen) + painter.drawArc( + anim_rect, + -16 * (angle + 10), + -16 * offset + ) + + painter.restore() def paint(self, painter, option, index): + if index.data(ANIMATION_STATE_ROLE): + self._draw_animation(painter, option, index) + super(ActionDelegate, self).paint(painter, option, index) is_group = False for group_role in self.group_roles: diff --git a/pype/tools/launcher/models.py b/pype/tools/launcher/models.py index 631f6ddc988..d1742014ef8 100644 --- a/pype/tools/launcher/models.py +++ b/pype/tools/launcher/models.py @@ -1,8 +1,15 @@ +import uuid import copy import logging import collections from . import lib +from .constants import ( + ACTION_ROLE, + GROUP_ROLE, + VARIANT_GROUP_ROLE, + ACTION_ID_ROLE +) from .actions import ApplicationAction from Qt import QtCore, QtGui from avalon.vendor import qtawesome @@ -109,10 +116,6 @@ def headerData(self, section, orientation, role): class ActionModel(QtGui.QStandardItemModel): - ACTION_ROLE = QtCore.Qt.UserRole - GROUP_ROLE = QtCore.Qt.UserRole + 1 - VARIANT_GROUP_ROLE = QtCore.Qt.UserRole + 2 - def __init__(self, dbcon, parent=None): super(ActionModel, self).__init__(parent=parent) self.dbcon = dbcon @@ -123,6 +126,7 @@ def __init__(self, dbcon, parent=None): self.default_icon = qtawesome.icon("fa.cube", color="white") # Cache of available actions self._registered_actions = list() + self.items_by_id = {} def discover(self): """Set up Actions cache. Run this for each new project.""" @@ -134,6 +138,7 @@ def discover(self): actions.extend(app_actions) self._registered_actions = actions + self.items_by_id.clear() def get_application_actions(self): actions = [] @@ -180,6 +185,7 @@ def filter_actions(self): # Validate actions based on compatibility self.clear() + self.items_by_id.clear() self._groups.clear() actions = self.filter_compatible_actions(self._registered_actions) @@ -235,8 +241,8 @@ def filter_actions(self): item = QtGui.QStandardItem(icon, label) item.setData(label, QtCore.Qt.ToolTipRole) - item.setData(actions, self.ACTION_ROLE) - item.setData(True, self.VARIANT_GROUP_ROLE) + item.setData(actions, ACTION_ROLE) + item.setData(True, VARIANT_GROUP_ROLE) items_by_order[order].append(item) for action in single_actions: @@ -244,7 +250,7 @@ def filter_actions(self): label = lib.get_action_label(action) item = QtGui.QStandardItem(icon, label) item.setData(label, QtCore.Qt.ToolTipRole) - item.setData(action, self.ACTION_ROLE) + item.setData(action, ACTION_ROLE) items_by_order[action.order].append(item) for group_name, actions in grouped_actions.items(): @@ -263,13 +269,16 @@ def filter_actions(self): icon = self.default_icon item = QtGui.QStandardItem(icon, group_name) - item.setData(actions, self.ACTION_ROLE) - item.setData(True, self.GROUP_ROLE) + item.setData(actions, ACTION_ROLE) + item.setData(True, GROUP_ROLE) items_by_order[order].append(item) for order in sorted(items_by_order.keys()): for item in items_by_order[order]: + item_id = str(uuid.uuid4()) + item.setData(item_id, ACTION_ID_ROLE) + self.items_by_id[item_id] = item self.appendRow(item) self.endResetModel() diff --git a/pype/tools/launcher/widgets.py b/pype/tools/launcher/widgets.py index 42b24de8cd2..9a7d8ca772c 100644 --- a/pype/tools/launcher/widgets.py +++ b/pype/tools/launcher/widgets.py @@ -1,4 +1,5 @@ import copy +import time import collections from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome @@ -7,6 +8,15 @@ from . import lib from .models import TaskModel, ActionModel, ProjectModel from .flickcharm import FlickCharm +from .constants import ( + ACTION_ROLE, + GROUP_ROLE, + VARIANT_GROUP_ROLE, + ACTION_ID_ROLE, + ANIMATION_START_ROLE, + ANIMATION_STATE_ROLE, + ANIMATION_LEN +) class ProjectBar(QtWidgets.QWidget): @@ -105,7 +115,7 @@ def __init__(self, dbcon, parent=None): # TODO better group delegate delegate = ActionDelegate( - [model.GROUP_ROLE, model.VARIANT_GROUP_ROLE], + [GROUP_ROLE, VARIANT_GROUP_ROLE], self ) view.setItemDelegate(delegate) @@ -115,6 +125,13 @@ def __init__(self, dbcon, parent=None): self.model = model self.view = view + self._animated_items = set() + + animation_timer = QtCore.QTimer() + animation_timer.setInterval(50) + animation_timer.timeout.connect(self._on_animation) + self._animation_timer = animation_timer + # Make view flickable flick = FlickCharm(parent=view) flick.activateOn(view) @@ -132,18 +149,46 @@ def filter_actions(self): def set_row_height(self, rows): self.setMinimumHeight(rows * 75) + def _on_animation(self): + time_now = time.time() + for action_id in tuple(self._animated_items): + item = self.model.items_by_id.get(action_id) + if not item: + self._animated_items.remove(action_id) + continue + + start_time = item.data(ANIMATION_START_ROLE) + if (time_now - start_time) > ANIMATION_LEN: + item.setData(0, ANIMATION_STATE_ROLE) + self._animated_items.remove(action_id) + + if not self._animated_items: + self._animation_timer.stop() + + self.update() + + def _start_animation(self, index): + action_id = index.data(ACTION_ID_ROLE) + item = self.model.items_by_id.get(action_id) + if item: + item.setData(time.time(), ANIMATION_START_ROLE) + item.setData(1, ANIMATION_STATE_ROLE) + self._animated_items.add(action_id) + self._animation_timer.start() + def on_clicked(self, index): - if not index.isValid(): + if not index or not index.isValid(): return - is_group = index.data(self.model.GROUP_ROLE) - is_variant_group = index.data(self.model.VARIANT_GROUP_ROLE) + is_group = index.data(GROUP_ROLE) + is_variant_group = index.data(VARIANT_GROUP_ROLE) if not is_group and not is_variant_group: - action = index.data(self.model.ACTION_ROLE) + action = index.data(ACTION_ROLE) + self._start_animation(index) self.action_clicked.emit(action) return - actions = index.data(self.model.ACTION_ROLE) + actions = index.data(ACTION_ROLE) menu = QtWidgets.QMenu(self) actions_mapping = {} @@ -203,6 +248,7 @@ def on_clicked(self, index): result = menu.exec_(QtGui.QCursor.pos()) if result: action = actions_mapping[result] + self._start_animation(index) self.action_clicked.emit(action)