import copy import time import collections from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome from .delegates import ActionDelegate 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): project_changed = QtCore.Signal(int) def __init__(self, dbcon, parent=None): super(ProjectBar, self).__init__(parent) self.dbcon = dbcon self.model = ProjectModel(self.dbcon) self.model.hide_invisible = True self.project_combobox = QtWidgets.QComboBox() self.project_combobox.setModel(self.model) self.project_combobox.setRootModelIndex(QtCore.QModelIndex()) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.project_combobox) self.setSizePolicy( QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Maximum ) # Initialize self.refresh() # Signals self.project_combobox.currentIndexChanged.connect(self.project_changed) # Set current project by default if it's set. project_name = self.dbcon.Session.get("AVALON_PROJECT") if project_name: self.set_project(project_name) def get_current_project(self): return self.project_combobox.currentText() def set_project(self, project_name): index = self.project_combobox.findText(project_name) if index < 0: # Try refresh combobox model self.project_combobox.blockSignals(True) self.model.refresh() self.project_combobox.blockSignals(False) index = self.project_combobox.findText(project_name) if index >= 0: self.project_combobox.setCurrentIndex(index) def refresh(self): prev_project_name = self.get_current_project() # Refresh without signals self.project_combobox.blockSignals(True) self.model.refresh() self.set_project(prev_project_name) self.project_combobox.blockSignals(False) self.project_changed.emit(self.project_combobox.currentIndex()) class ActionBar(QtWidgets.QWidget): """Launcher interface""" action_clicked = QtCore.Signal(object) def __init__(self, dbcon, parent=None): super(ActionBar, self).__init__(parent) self.dbcon = dbcon layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(8, 0, 8, 0) view = QtWidgets.QListView(self) view.setProperty("mode", "icon") view.setObjectName("IconView") view.setViewMode(QtWidgets.QListView.IconMode) view.setResizeMode(QtWidgets.QListView.Adjust) view.setSelectionMode(QtWidgets.QListView.NoSelection) view.setEditTriggers(QtWidgets.QListView.NoEditTriggers) view.setWrapping(True) view.setGridSize(QtCore.QSize(70, 75)) view.setIconSize(QtCore.QSize(30, 30)) view.setSpacing(0) view.setWordWrap(True) model = ActionModel(self.dbcon, self) view.setModel(model) # TODO better group delegate delegate = ActionDelegate( [GROUP_ROLE, VARIANT_GROUP_ROLE], self ) view.setItemDelegate(delegate) layout.addWidget(view) 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) self.set_row_height(1) view.clicked.connect(self.on_clicked) def discover_actions(self): self.model.discover() def filter_actions(self): self.model.filter_actions() 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 or not index.isValid(): return 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(ACTION_ROLE) self._start_animation(index) self.action_clicked.emit(action) return actions = index.data(ACTION_ROLE) menu = QtWidgets.QMenu(self) actions_mapping = {} if is_variant_group: for action in actions: menu_action = QtWidgets.QAction( lib.get_action_label(action) ) menu.addAction(menu_action) actions_mapping[menu_action] = action else: by_variant_label = collections.defaultdict(list) orders = [] for action in actions: # Lable variants label = getattr(action, "label", None) label_variant = getattr(action, "label_variant", None) if label_variant and not label: label_variant = None if not label_variant: orders.append(action) continue if label not in orders: orders.append(label) by_variant_label[label].append(action) for action_item in orders: actions = by_variant_label.get(action_item) if not actions: action = action_item elif len(actions) == 1: action = actions[0] else: action = None if action: menu_action = QtWidgets.QAction( lib.get_action_label(action) ) menu.addAction(menu_action) actions_mapping[menu_action] = action continue sub_menu = QtWidgets.QMenu(label, menu) for action in actions: menu_action = QtWidgets.QAction( lib.get_action_label(action) ) sub_menu.addAction(menu_action) actions_mapping[menu_action] = action menu.addMenu(sub_menu) result = menu.exec_(QtGui.QCursor.pos()) if result: action = actions_mapping[result] self._start_animation(index) self.action_clicked.emit(action) class TasksWidget(QtWidgets.QWidget): """Widget showing active Tasks""" task_changed = QtCore.Signal() selection_mode = ( QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows ) def __init__(self, dbcon, parent=None): super(TasksWidget, self).__init__(parent) self.dbcon = dbcon view = QtWidgets.QTreeView(self) view.setIndentation(0) view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) model = TaskModel(self.dbcon) view.setModel(model) layout = QtWidgets.QVBoxLayout(self) layout.addWidget(view) view.selectionModel().selectionChanged.connect(self.task_changed) self.model = model self.view = view self._last_selected_task = None def set_asset(self, asset_id): if asset_id is None: # Asset deselected self.model.set_assets() return # Try and preserve the last selected task and reselect it # after switching assets. If there's no currently selected # asset keep whatever the "last selected" was prior to it. current = self.get_current_task() if current: self._last_selected_task = current self.model.set_assets([asset_id]) if self._last_selected_task: self.select_task(self._last_selected_task) # Force a task changed emit. self.task_changed.emit() def select_task(self, task_name): """Select a task by name. If the task does not exist in the current model then selection is only cleared. Args: task (str): Name of the task to select. """ # Clear selection self.view.selectionModel().clearSelection() # Select the task for row in range(self.model.rowCount()): index = self.model.index(row, 0) _task_name = index.data(QtCore.Qt.DisplayRole) if _task_name == task_name: self.view.selectionModel().select(index, self.selection_mode) # Set the currently active index self.view.setCurrentIndex(index) break def get_current_task(self): """Return name of task at current index (selected) Returns: str: Name of the current task. """ index = self.view.currentIndex() if self.view.selectionModel().isSelected(index): return index.data(QtCore.Qt.DisplayRole) class ActionHistory(QtWidgets.QPushButton): trigger_history = QtCore.Signal(tuple) def __init__(self, parent=None): super(ActionHistory, self).__init__(parent=parent) self.max_history = 15 self.setFixedWidth(25) self.setFixedHeight(25) self.setIcon(qtawesome.icon("fa.history", color="#CCCCCC")) self.setIconSize(QtCore.QSize(15, 15)) self._history = [] self.clicked.connect(self.show_history) def show_history(self): # Show history popup if not self._history: return widget = QtWidgets.QListWidget() widget.setSelectionMode(widget.NoSelection) widget.setStyleSheet(""" * { font-family: "Courier New"; } """) largest_label_num_chars = 0 largest_action_label = max(len(x[0].label) for x in self._history) action_session_role = QtCore.Qt.UserRole + 1 for action, session in reversed(self._history): project = session.get("AVALON_PROJECT") asset = session.get("AVALON_ASSET") task = session.get("AVALON_TASK") breadcrumb = " > ".join(x for x in [project, asset, task] if x) m = "{{action:{0}}} | {{breadcrumb}}".format(largest_action_label) label = m.format(action=action.label, breadcrumb=breadcrumb) icon = lib.get_action_icon(action) item = QtWidgets.QListWidgetItem(icon, label) item.setData(action_session_role, (action, session)) largest_label_num_chars = max(largest_label_num_chars, len(label)) widget.addItem(item) # Show history dialog = QtWidgets.QDialog(parent=self) dialog.setWindowTitle("Action History") dialog.setWindowFlags( QtCore.Qt.FramelessWindowHint | QtCore.Qt.Popup ) dialog.setSizePolicy( QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored ) layout = QtWidgets.QVBoxLayout(dialog) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(widget) def on_clicked(index): data = index.data(action_session_role) self.trigger_history.emit(data) dialog.close() widget.clicked.connect(on_clicked) # padding + icon + text width = 40 + (largest_label_num_chars * 7) entry_height = 21 height = entry_height * len(self._history) point = QtGui.QCursor().pos() dialog.setGeometry( point.x() - width, point.y() - height, width, height ) dialog.exec_() self.widget_popup = widget def add_action(self, action, session): key = (action, copy.deepcopy(session)) # Remove entry if already exists if key in self._history: self._history.remove(key) self._history.append(key) # Slice the end of the list if we exceed the max history if len(self._history) > self.max_history: self._history = self._history[-self.max_history:] def clear_history(self): self._history.clear() class SlidePageWidget(QtWidgets.QStackedWidget): """Stacked widget that nicely slides between its pages""" directions = { "left": QtCore.QPoint(-1, 0), "right": QtCore.QPoint(1, 0), "up": QtCore.QPoint(0, 1), "down": QtCore.QPoint(0, -1) } def slide_view(self, index, direction="right"): if self.currentIndex() == index: return offset_direction = self.directions.get(direction) if offset_direction is None: print("BUG: invalid slide direction: {}".format(direction)) return width = self.frameRect().width() height = self.frameRect().height() offset = QtCore.QPoint( offset_direction.x() * width, offset_direction.y() * height ) new_page = self.widget(index) new_page.setGeometry(0, 0, width, height) curr_pos = new_page.pos() new_page.move(curr_pos + offset) new_page.show() new_page.raise_() current_page = self.currentWidget() b_pos = QtCore.QByteArray(b"pos") anim_old = QtCore.QPropertyAnimation(current_page, b_pos, self) anim_old.setDuration(250) anim_old.setStartValue(curr_pos) anim_old.setEndValue(curr_pos - offset) anim_old.setEasingCurve(QtCore.QEasingCurve.OutQuad) anim_new = QtCore.QPropertyAnimation(new_page, b_pos, self) anim_new.setDuration(250) anim_new.setStartValue(curr_pos + offset) anim_new.setEndValue(curr_pos) anim_new.setEasingCurve(QtCore.QEasingCurve.OutQuad) anim_group = QtCore.QParallelAnimationGroup(self) anim_group.addAnimation(anim_old) anim_group.addAnimation(anim_new) def slide_finished(): self.setCurrentWidget(new_page) anim_group.finished.connect(slide_finished) anim_group.start()