mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge pull request #2672 from pypeclub/feature/OP-2416_Launcher-tool-model
Launcher: Added Launcher model with more abilities to filter
This commit is contained in:
commit
cfcdc80f74
6 changed files with 630 additions and 140 deletions
|
|
@ -15,7 +15,7 @@ provides a bridge between the file-based project inventory and configuration.
|
|||
"""
|
||||
|
||||
import os
|
||||
from Qt import QtGui, QtCore
|
||||
from Qt import QtGui
|
||||
from avalon.vendor import qtawesome
|
||||
from openpype.api import resources
|
||||
|
||||
|
|
@ -23,77 +23,6 @@ ICON_CACHE = {}
|
|||
NOT_FOUND = type("NotFound", (object, ), {})
|
||||
|
||||
|
||||
class ProjectHandler(QtCore.QObject):
|
||||
"""Handler of project model and current project in Launcher tool.
|
||||
|
||||
Helps to organize two separate widgets handling current project selection.
|
||||
|
||||
It is easier to trigger project change callbacks from one place than from
|
||||
multiple different places without proper handling or sequence changes.
|
||||
|
||||
Args:
|
||||
dbcon(AvalonMongoDB): Mongo connection with Session.
|
||||
model(ProjectModel): Object of projects model which is shared across
|
||||
all widgets using projects. Arg dbcon should be used as source for
|
||||
the model.
|
||||
"""
|
||||
# Project list will be refreshed each 10000 msecs
|
||||
# - this is not part of helper implementation but should be used by widgets
|
||||
# that may require reshing of projects
|
||||
refresh_interval = 10000
|
||||
|
||||
# Signal emitted when project has changed
|
||||
project_changed = QtCore.Signal(str)
|
||||
projects_refreshed = QtCore.Signal()
|
||||
timer_timeout = QtCore.Signal()
|
||||
|
||||
def __init__(self, dbcon, model):
|
||||
super(ProjectHandler, self).__init__()
|
||||
self._active = False
|
||||
# Store project model for usage
|
||||
self.model = model
|
||||
# Store dbcon
|
||||
self.dbcon = dbcon
|
||||
|
||||
self.current_project = dbcon.Session.get("AVALON_PROJECT")
|
||||
|
||||
refresh_timer = QtCore.QTimer()
|
||||
refresh_timer.setInterval(self.refresh_interval)
|
||||
refresh_timer.timeout.connect(self._on_timeout)
|
||||
|
||||
self.refresh_timer = refresh_timer
|
||||
|
||||
def _on_timeout(self):
|
||||
if self._active:
|
||||
self.timer_timeout.emit()
|
||||
self.refresh_model()
|
||||
|
||||
def set_active(self, active):
|
||||
self._active = active
|
||||
|
||||
def start_timer(self, trigger=False):
|
||||
self.refresh_timer.start()
|
||||
if trigger:
|
||||
self._on_timeout()
|
||||
|
||||
def stop_timer(self):
|
||||
self.refresh_timer.stop()
|
||||
|
||||
def set_project(self, project_name):
|
||||
# Change current project of this handler
|
||||
self.current_project = project_name
|
||||
# Change session project to take effect for other widgets using the
|
||||
# dbcon object.
|
||||
self.dbcon.Session["AVALON_PROJECT"] = project_name
|
||||
|
||||
# Trigger change signal when everything is updated to new project
|
||||
self.project_changed.emit(project_name)
|
||||
|
||||
def refresh_model(self):
|
||||
self.model.refresh()
|
||||
self.projects_refreshed.emit()
|
||||
|
||||
|
||||
def get_action_icon(action):
|
||||
icon_name = action.icon
|
||||
if not icon_name:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,30 @@
|
|||
import re
|
||||
import uuid
|
||||
import copy
|
||||
import logging
|
||||
import collections
|
||||
import time
|
||||
|
||||
import appdirs
|
||||
from Qt import QtCore, QtGui
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon import api
|
||||
from openpype.lib import JSONSettingRegistry
|
||||
from openpype.lib.applications import (
|
||||
CUSTOM_LAUNCH_APP_GROUPS,
|
||||
ApplicationManager
|
||||
)
|
||||
from openpype.tools.utils.lib import DynamicQThread
|
||||
from openpype.tools.utils.assets_widget import (
|
||||
AssetModel,
|
||||
ASSET_NAME_ROLE
|
||||
)
|
||||
from openpype.tools.utils.tasks_widget import (
|
||||
TasksModel,
|
||||
TasksProxyModel,
|
||||
TASK_TYPE_ROLE,
|
||||
TASK_ASSIGNEE_ROLE
|
||||
)
|
||||
|
||||
from . import lib
|
||||
from .constants import (
|
||||
|
|
@ -13,17 +35,13 @@ from .constants import (
|
|||
FORCE_NOT_OPEN_WORKFILE_ROLE
|
||||
)
|
||||
from .actions import ApplicationAction
|
||||
from Qt import QtCore, QtGui
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon import api
|
||||
from openpype.lib import JSONSettingRegistry
|
||||
from openpype.lib.applications import (
|
||||
CUSTOM_LAUNCH_APP_GROUPS,
|
||||
ApplicationManager
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Must be different than roles in default asset model
|
||||
ASSET_TASK_TYPES_ROLE = QtCore.Qt.UserRole + 10
|
||||
ASSET_ASSIGNEE_ROLE = QtCore.Qt.UserRole + 11
|
||||
|
||||
|
||||
class ActionModel(QtGui.QStandardItemModel):
|
||||
def __init__(self, dbcon, parent=None):
|
||||
|
|
@ -330,21 +348,483 @@ class ActionModel(QtGui.QStandardItemModel):
|
|||
return compare_data
|
||||
|
||||
|
||||
class LauncherModel(QtCore.QObject):
|
||||
# Refresh interval of projects
|
||||
refresh_interval = 10000
|
||||
|
||||
# Signals
|
||||
# Current project has changed
|
||||
project_changed = QtCore.Signal(str)
|
||||
# Filters has changed (any)
|
||||
filters_changed = QtCore.Signal()
|
||||
|
||||
# Projects were refreshed
|
||||
projects_refreshed = QtCore.Signal()
|
||||
|
||||
# Signals ONLY for assets model!
|
||||
# - other objects should listen to asset model signals
|
||||
# Asset refresh started
|
||||
assets_refresh_started = QtCore.Signal()
|
||||
# Assets refresh finished
|
||||
assets_refreshed = QtCore.Signal()
|
||||
|
||||
# Refresh timer timeout
|
||||
# - give ability to tell parent window that this timer still runs
|
||||
timer_timeout = QtCore.Signal()
|
||||
|
||||
# Duplication from AssetsModel with "data.tasks"
|
||||
_asset_projection = {
|
||||
"name": 1,
|
||||
"parent": 1,
|
||||
"data.visualParent": 1,
|
||||
"data.label": 1,
|
||||
"data.icon": 1,
|
||||
"data.color": 1,
|
||||
"data.tasks": 1
|
||||
}
|
||||
|
||||
def __init__(self, dbcon):
|
||||
super(LauncherModel, self).__init__()
|
||||
# Refresh timer
|
||||
# - should affect only projects
|
||||
refresh_timer = QtCore.QTimer()
|
||||
refresh_timer.setInterval(self.refresh_interval)
|
||||
refresh_timer.timeout.connect(self._on_timeout)
|
||||
|
||||
self._refresh_timer = refresh_timer
|
||||
|
||||
# Launcher is active
|
||||
self._active = False
|
||||
|
||||
# Global data
|
||||
self._dbcon = dbcon
|
||||
# Available project names
|
||||
self._project_names = set()
|
||||
|
||||
# Context data
|
||||
self._asset_docs = []
|
||||
self._asset_docs_by_id = {}
|
||||
self._asset_filter_data_by_id = {}
|
||||
self._assignees = set()
|
||||
self._task_types = set()
|
||||
|
||||
# Filters
|
||||
self._asset_name_filter = ""
|
||||
self._assignee_filters = set()
|
||||
self._task_type_filters = set()
|
||||
|
||||
# Last project for which were assets queried
|
||||
self._last_project_name = None
|
||||
# Asset refresh thread is running
|
||||
self._refreshing_assets = False
|
||||
# Asset refresh thread
|
||||
self._asset_refresh_thread = None
|
||||
|
||||
def _on_timeout(self):
|
||||
"""Refresh timer timeout."""
|
||||
if self._active:
|
||||
self.timer_timeout.emit()
|
||||
self.refresh_projects()
|
||||
|
||||
def set_active(self, active):
|
||||
"""Window change active state."""
|
||||
self._active = active
|
||||
|
||||
def start_refresh_timer(self, trigger=False):
|
||||
"""Start refresh timer."""
|
||||
self._refresh_timer.start()
|
||||
if trigger:
|
||||
self._on_timeout()
|
||||
|
||||
def stop_refresh_timer(self):
|
||||
"""Stop refresh timer."""
|
||||
self._refresh_timer.stop()
|
||||
|
||||
@property
|
||||
def project_name(self):
|
||||
"""Current project name."""
|
||||
return self._dbcon.Session.get("AVALON_PROJECT")
|
||||
|
||||
@property
|
||||
def refreshing_assets(self):
|
||||
"""Refreshing thread is running."""
|
||||
return self._refreshing_assets
|
||||
|
||||
@property
|
||||
def asset_docs(self):
|
||||
"""Access to asset docs."""
|
||||
return self._asset_docs
|
||||
|
||||
@property
|
||||
def project_names(self):
|
||||
"""Available project names."""
|
||||
return self._project_names
|
||||
|
||||
@property
|
||||
def asset_filter_data_by_id(self):
|
||||
"""Prepared filter data by asset id."""
|
||||
return self._asset_filter_data_by_id
|
||||
|
||||
@property
|
||||
def assignees(self):
|
||||
"""All assignees for all assets in current project."""
|
||||
return self._assignees
|
||||
|
||||
@property
|
||||
def task_types(self):
|
||||
"""All task types for all assets in current project.
|
||||
|
||||
TODO: This could be maybe taken from project document where are all
|
||||
task types...
|
||||
"""
|
||||
return self._task_types
|
||||
|
||||
@property
|
||||
def task_type_filters(self):
|
||||
"""Currently set task type filters."""
|
||||
return self._task_type_filters
|
||||
|
||||
@property
|
||||
def assignee_filters(self):
|
||||
"""Currently set assignee filters."""
|
||||
return self._assignee_filters
|
||||
|
||||
@property
|
||||
def asset_name_filter(self):
|
||||
"""Asset name filter (can be used as regex filter)."""
|
||||
return self._asset_name_filter
|
||||
|
||||
def get_asset_doc(self, asset_id):
|
||||
"""Get single asset document by id."""
|
||||
return self._asset_docs_by_id.get(asset_id)
|
||||
|
||||
def set_project_name(self, project_name):
|
||||
"""Change project name and refresh asset documents."""
|
||||
if project_name == self.project_name:
|
||||
return
|
||||
self._dbcon.Session["AVALON_PROJECT"] = project_name
|
||||
self.project_changed.emit(project_name)
|
||||
|
||||
self.refresh_assets(force=True)
|
||||
|
||||
def refresh(self):
|
||||
"""Trigger refresh of whole model."""
|
||||
self.refresh_projects()
|
||||
self.refresh_assets(force=False)
|
||||
|
||||
def refresh_projects(self):
|
||||
"""Refresh projects."""
|
||||
current_project = self.project_name
|
||||
project_names = set()
|
||||
for project_doc in self._dbcon.projects(only_active=True):
|
||||
project_names.add(project_doc["name"])
|
||||
|
||||
self._project_names = project_names
|
||||
self.projects_refreshed.emit()
|
||||
if (
|
||||
current_project is not None
|
||||
and current_project not in project_names
|
||||
):
|
||||
self.set_project_name(None)
|
||||
|
||||
def _set_asset_docs(self, asset_docs=None):
|
||||
"""Set asset documents and all related data.
|
||||
|
||||
Method extract and prepare data needed for assets and tasks widget and
|
||||
prepare filtering data.
|
||||
"""
|
||||
if asset_docs is None:
|
||||
asset_docs = []
|
||||
|
||||
all_task_types = set()
|
||||
all_assignees = set()
|
||||
asset_docs_by_id = {}
|
||||
asset_filter_data_by_id = {}
|
||||
for asset_doc in asset_docs:
|
||||
task_types = set()
|
||||
assignees = set()
|
||||
asset_id = asset_doc["_id"]
|
||||
asset_docs_by_id[asset_id] = asset_doc
|
||||
asset_tasks = asset_doc.get("data", {}).get("tasks")
|
||||
asset_filter_data_by_id[asset_id] = {
|
||||
"assignees": assignees,
|
||||
"task_types": task_types
|
||||
}
|
||||
if not asset_tasks:
|
||||
continue
|
||||
|
||||
for task_data in asset_tasks.values():
|
||||
task_assignees = set()
|
||||
_task_assignees = task_data.get("assignees")
|
||||
if _task_assignees:
|
||||
for assignee in _task_assignees:
|
||||
task_assignees.add(assignee["username"])
|
||||
|
||||
task_type = task_data.get("type")
|
||||
if task_assignees:
|
||||
assignees |= set(task_assignees)
|
||||
if task_type:
|
||||
task_types.add(task_type)
|
||||
|
||||
all_task_types |= task_types
|
||||
all_assignees |= assignees
|
||||
|
||||
self._asset_docs_by_id = asset_docs_by_id
|
||||
self._asset_docs = asset_docs
|
||||
self._asset_filter_data_by_id = asset_filter_data_by_id
|
||||
self._assignees = all_assignees
|
||||
self._task_types = all_task_types
|
||||
|
||||
self.assets_refreshed.emit()
|
||||
|
||||
def set_task_type_filter(self, task_types):
|
||||
"""Change task type filter.
|
||||
|
||||
Args:
|
||||
task_types (set): Set of task types that should be visible.
|
||||
Pass empty set to turn filter off.
|
||||
"""
|
||||
self._task_type_filters = task_types
|
||||
self.filters_changed.emit()
|
||||
|
||||
def set_assignee_filter(self, assignees):
|
||||
"""Change assignees filter.
|
||||
|
||||
Args:
|
||||
assignees (set): Set of assignees that should be visible.
|
||||
Pass empty set to turn filter off.
|
||||
"""
|
||||
self._assignee_filters = assignees
|
||||
self.filters_changed.emit()
|
||||
|
||||
def set_asset_name_filter(self, text_filter):
|
||||
"""Change asset name filter.
|
||||
|
||||
Args:
|
||||
text_filter (str): Asset name filter. Pass empty string to
|
||||
turn filter off.
|
||||
"""
|
||||
self._asset_name_filter = text_filter
|
||||
self.filters_changed.emit()
|
||||
|
||||
def refresh_assets(self, force=True):
|
||||
"""Refresh assets."""
|
||||
self.assets_refresh_started.emit()
|
||||
|
||||
if self.project_name is None:
|
||||
self._set_asset_docs()
|
||||
return
|
||||
|
||||
if (
|
||||
not force
|
||||
and self._last_project_name == self.project_name
|
||||
):
|
||||
return
|
||||
|
||||
self._stop_fetch_thread()
|
||||
|
||||
self._refreshing_assets = True
|
||||
self._last_project_name = self.project_name
|
||||
self._asset_refresh_thread = DynamicQThread(self._refresh_assets)
|
||||
self._asset_refresh_thread.start()
|
||||
|
||||
def _stop_fetch_thread(self):
|
||||
self._refreshing_assets = False
|
||||
if self._asset_refresh_thread is not None:
|
||||
while self._asset_refresh_thread.isRunning():
|
||||
# TODO this is blocking UI should be done in a different way
|
||||
time.sleep(0.01)
|
||||
self._asset_refresh_thread = None
|
||||
|
||||
def _refresh_assets(self):
|
||||
asset_docs = list(self._dbcon.find(
|
||||
{"type": "asset"},
|
||||
self._asset_projection
|
||||
))
|
||||
if not self._refreshing_assets:
|
||||
return
|
||||
self._refreshing_assets = False
|
||||
self._set_asset_docs(asset_docs)
|
||||
|
||||
|
||||
class LauncherTasksProxyModel(TasksProxyModel):
|
||||
"""Tasks proxy model with more filtering.
|
||||
|
||||
TODO:
|
||||
This can be (with few modifications) used in default tasks widget too.
|
||||
"""
|
||||
def __init__(self, launcher_model, *args, **kwargs):
|
||||
self._launcher_model = launcher_model
|
||||
super(LauncherTasksProxyModel, self).__init__(*args, **kwargs)
|
||||
|
||||
launcher_model.filters_changed.connect(self._on_filter_change)
|
||||
|
||||
self._task_types_filter = set()
|
||||
self._assignee_filter = set()
|
||||
|
||||
def _on_filter_change(self):
|
||||
self._task_types_filter = self._launcher_model.task_type_filters
|
||||
self._assignee_filter = self._launcher_model.assignee_filters
|
||||
self.invalidateFilter()
|
||||
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
if not self._task_types_filter and not self._assignee_filter:
|
||||
return True
|
||||
|
||||
model = self.sourceModel()
|
||||
source_index = model.index(row, self.filterKeyColumn(), parent)
|
||||
if not source_index.isValid():
|
||||
return False
|
||||
|
||||
# Check current index itself
|
||||
if self._task_types_filter:
|
||||
task_type = model.data(source_index, TASK_TYPE_ROLE)
|
||||
if task_type not in self._task_types_filter:
|
||||
return False
|
||||
|
||||
if self._assignee_filter:
|
||||
assignee = model.data(source_index, TASK_ASSIGNEE_ROLE)
|
||||
if not self._assignee_filter.intersection(assignee):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class LauncherTaskModel(TasksModel):
|
||||
def __init__(self, launcher_model, *args, **kwargs):
|
||||
self._launcher_model = launcher_model
|
||||
super(LauncherTaskModel, self).__init__(*args, **kwargs)
|
||||
|
||||
def set_asset_id(self, asset_id):
|
||||
asset_doc = None
|
||||
if self._context_is_valid():
|
||||
asset_doc = self._launcher_model.get_asset_doc(asset_id)
|
||||
self._set_asset(asset_doc)
|
||||
|
||||
|
||||
class AssetRecursiveSortFilterModel(QtCore.QSortFilterProxyModel):
|
||||
def __init__(self, launcher_model, *args, **kwargs):
|
||||
self._launcher_model = launcher_model
|
||||
|
||||
super(AssetRecursiveSortFilterModel, self).__init__(*args, **kwargs)
|
||||
|
||||
launcher_model.filters_changed.connect(self._on_filter_change)
|
||||
self._name_filter = ""
|
||||
self._task_types_filter = set()
|
||||
self._assignee_filter = set()
|
||||
|
||||
def _on_filter_change(self):
|
||||
self._name_filter = self._launcher_model.asset_name_filter
|
||||
self._task_types_filter = self._launcher_model.task_type_filters
|
||||
self._assignee_filter = self._launcher_model.assignee_filters
|
||||
self.invalidateFilter()
|
||||
|
||||
"""Filters to the regex if any of the children matches allow parent"""
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
if (
|
||||
not self._name_filter
|
||||
and not self._task_types_filter
|
||||
and not self._assignee_filter
|
||||
):
|
||||
return True
|
||||
|
||||
model = self.sourceModel()
|
||||
source_index = model.index(row, self.filterKeyColumn(), parent)
|
||||
if not source_index.isValid():
|
||||
return False
|
||||
|
||||
# Check current index itself
|
||||
valid = True
|
||||
if self._name_filter:
|
||||
name = model.data(source_index, ASSET_NAME_ROLE)
|
||||
if (
|
||||
name is None
|
||||
or not re.search(self._name_filter, name, re.IGNORECASE)
|
||||
):
|
||||
valid = False
|
||||
|
||||
if valid and self._task_types_filter:
|
||||
task_types = model.data(source_index, ASSET_TASK_TYPES_ROLE)
|
||||
if not self._task_types_filter.intersection(task_types):
|
||||
valid = False
|
||||
|
||||
if valid and self._assignee_filter:
|
||||
assignee = model.data(source_index, ASSET_ASSIGNEE_ROLE)
|
||||
if not self._assignee_filter.intersection(assignee):
|
||||
valid = False
|
||||
|
||||
if valid:
|
||||
return True
|
||||
|
||||
# Check children
|
||||
rows = model.rowCount(source_index)
|
||||
for child_row in range(rows):
|
||||
if self.filterAcceptsRow(child_row, source_index):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class LauncherAssetsModel(AssetModel):
|
||||
def __init__(self, launcher_model, dbcon, parent=None):
|
||||
self._launcher_model = launcher_model
|
||||
# Make sure that variable is available (even if is in AssetModel)
|
||||
self._last_project_name = None
|
||||
|
||||
super(LauncherAssetsModel, self).__init__(dbcon, parent)
|
||||
|
||||
launcher_model.project_changed.connect(self._on_project_change)
|
||||
launcher_model.assets_refresh_started.connect(
|
||||
self._on_launcher_refresh_start
|
||||
)
|
||||
launcher_model.assets_refreshed.connect(self._on_launcher_refresh)
|
||||
|
||||
def _on_launcher_refresh_start(self):
|
||||
self._refreshing = True
|
||||
project_name = self._launcher_model.project_name
|
||||
if self._last_project_name != project_name:
|
||||
self._clear_items()
|
||||
self._last_project_name = project_name
|
||||
|
||||
def _on_launcher_refresh(self):
|
||||
self._fill_assets(self._launcher_model.asset_docs)
|
||||
self._refreshing = False
|
||||
self.refreshed.emit(bool(self._items_by_asset_id))
|
||||
|
||||
def _fill_assets(self, *args, **kwargs):
|
||||
super(LauncherAssetsModel, self)._fill_assets(*args, **kwargs)
|
||||
asset_filter_data_by_id = self._launcher_model.asset_filter_data_by_id
|
||||
for asset_id, item in self._items_by_asset_id.items():
|
||||
filter_data = asset_filter_data_by_id.get(asset_id)
|
||||
|
||||
assignees = filter_data["assignees"]
|
||||
task_types = filter_data["task_types"]
|
||||
|
||||
item.setData(assignees, ASSET_ASSIGNEE_ROLE)
|
||||
item.setData(task_types, ASSET_TASK_TYPES_ROLE)
|
||||
|
||||
def _on_project_change(self):
|
||||
self._clear_items()
|
||||
|
||||
def refresh(self, *args, **kwargs):
|
||||
raise ValueError("This is a bug!")
|
||||
|
||||
def stop_refresh(self, *args, **kwargs):
|
||||
raise ValueError("This is a bug!")
|
||||
|
||||
|
||||
class ProjectModel(QtGui.QStandardItemModel):
|
||||
"""List of projects"""
|
||||
|
||||
def __init__(self, dbcon, parent=None):
|
||||
def __init__(self, launcher_model, parent=None):
|
||||
super(ProjectModel, self).__init__(parent=parent)
|
||||
|
||||
self.dbcon = dbcon
|
||||
self._launcher_model = launcher_model
|
||||
self.project_icon = qtawesome.icon("fa.map", color="white")
|
||||
self._project_names = set()
|
||||
|
||||
def refresh(self):
|
||||
project_names = set()
|
||||
for project_doc in self.get_projects():
|
||||
project_names.add(project_doc["name"])
|
||||
launcher_model.projects_refreshed.connect(self._on_refresh)
|
||||
|
||||
def _on_refresh(self):
|
||||
project_names = set(self._launcher_model.project_names)
|
||||
origin_project_names = set(self._project_names)
|
||||
self._project_names = project_names
|
||||
|
||||
|
|
@ -387,7 +867,3 @@ class ProjectModel(QtGui.QStandardItemModel):
|
|||
items.append(item)
|
||||
|
||||
self.invisibleRootItem().insertRows(row, items)
|
||||
|
||||
def get_projects(self):
|
||||
return sorted(self.dbcon.projects(only_active=True),
|
||||
key=lambda x: x["name"])
|
||||
|
|
|
|||
|
|
@ -4,11 +4,21 @@ import collections
|
|||
from Qt import QtWidgets, QtCore, QtGui
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
from openpype.tools.flickcharm import FlickCharm
|
||||
from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
|
||||
from openpype.tools.utils.tasks_widget import TasksWidget
|
||||
|
||||
from .delegates import ActionDelegate
|
||||
from . import lib
|
||||
from .models import (
|
||||
ActionModel,
|
||||
ProjectModel,
|
||||
LauncherAssetsModel,
|
||||
AssetRecursiveSortFilterModel,
|
||||
LauncherTaskModel,
|
||||
LauncherTasksProxyModel
|
||||
)
|
||||
from .actions import ApplicationAction
|
||||
from .models import ActionModel
|
||||
from openpype.tools.flickcharm import FlickCharm
|
||||
from .constants import (
|
||||
ACTION_ROLE,
|
||||
GROUP_ROLE,
|
||||
|
|
@ -22,15 +32,15 @@ from .constants import (
|
|||
|
||||
|
||||
class ProjectBar(QtWidgets.QWidget):
|
||||
def __init__(self, project_handler, parent=None):
|
||||
def __init__(self, launcher_model, parent=None):
|
||||
super(ProjectBar, self).__init__(parent)
|
||||
|
||||
project_combobox = QtWidgets.QComboBox(self)
|
||||
# Change delegate so stylysheets are applied
|
||||
project_delegate = QtWidgets.QStyledItemDelegate(project_combobox)
|
||||
project_combobox.setItemDelegate(project_delegate)
|
||||
|
||||
project_combobox.setModel(project_handler.model)
|
||||
model = ProjectModel(launcher_model)
|
||||
project_combobox.setModel(model)
|
||||
project_combobox.setRootModelIndex(QtCore.QModelIndex())
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
|
|
@ -42,16 +52,17 @@ class ProjectBar(QtWidgets.QWidget):
|
|||
QtWidgets.QSizePolicy.Maximum
|
||||
)
|
||||
|
||||
self.project_handler = project_handler
|
||||
self._launcher_model = launcher_model
|
||||
self.project_delegate = project_delegate
|
||||
self.project_combobox = project_combobox
|
||||
self._model = model
|
||||
|
||||
# Signals
|
||||
self.project_combobox.currentIndexChanged.connect(self.on_index_change)
|
||||
project_handler.project_changed.connect(self._on_project_change)
|
||||
launcher_model.project_changed.connect(self._on_project_change)
|
||||
|
||||
# Set current project by default if it's set.
|
||||
project_name = project_handler.current_project
|
||||
project_name = launcher_model.project_name
|
||||
if project_name:
|
||||
self.set_project(project_name)
|
||||
|
||||
|
|
@ -67,7 +78,7 @@ class ProjectBar(QtWidgets.QWidget):
|
|||
index = self.project_combobox.findText(project_name)
|
||||
if index < 0:
|
||||
# Try refresh combobox model
|
||||
self.project_handler.refresh_model()
|
||||
self._launcher_model.refresh_projects()
|
||||
index = self.project_combobox.findText(project_name)
|
||||
|
||||
if index >= 0:
|
||||
|
|
@ -78,7 +89,70 @@ class ProjectBar(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
project_name = self.get_current_project()
|
||||
self.project_handler.set_project(project_name)
|
||||
self._launcher_model.set_project_name(project_name)
|
||||
|
||||
|
||||
class LauncherTaskWidget(TasksWidget):
|
||||
def __init__(self, launcher_model, *args, **kwargs):
|
||||
self._launcher_model = launcher_model
|
||||
|
||||
super(LauncherTaskWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
def _create_source_model(self):
|
||||
return LauncherTaskModel(self._launcher_model, self._dbcon)
|
||||
|
||||
def _create_proxy_model(self, source_model):
|
||||
proxy = LauncherTasksProxyModel(self._launcher_model)
|
||||
proxy.setSourceModel(source_model)
|
||||
return proxy
|
||||
|
||||
|
||||
class LauncherAssetsWidget(SingleSelectAssetsWidget):
|
||||
def __init__(self, launcher_model, *args, **kwargs):
|
||||
self._launcher_model = launcher_model
|
||||
|
||||
super(LauncherAssetsWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
launcher_model.assets_refresh_started.connect(self._on_refresh_start)
|
||||
|
||||
self.set_current_asset_btn_visibility(False)
|
||||
|
||||
def _on_refresh_start(self):
|
||||
self._set_loading_state(loading=True, empty=True)
|
||||
self.refresh_triggered.emit()
|
||||
|
||||
@property
|
||||
def refreshing(self):
|
||||
return self._model.refreshing
|
||||
|
||||
def refresh(self):
|
||||
self._launcher_model.refresh_assets(force=True)
|
||||
|
||||
def stop_refresh(self):
|
||||
raise ValueError("bug stop_refresh called")
|
||||
|
||||
def _refresh_model(self, clear=False):
|
||||
raise ValueError("bug _refresh_model called")
|
||||
|
||||
def _create_source_model(self):
|
||||
model = LauncherAssetsModel(self._launcher_model, self.dbcon)
|
||||
model.refreshed.connect(self._on_model_refresh)
|
||||
return model
|
||||
|
||||
def _create_proxy_model(self, source_model):
|
||||
proxy = AssetRecursiveSortFilterModel(self._launcher_model)
|
||||
proxy.setSourceModel(source_model)
|
||||
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
return proxy
|
||||
|
||||
def _on_model_refresh(self, has_item):
|
||||
self._proxy.sort(0)
|
||||
self._set_loading_state(loading=False, empty=not has_item)
|
||||
self.refreshed.emit()
|
||||
|
||||
def _on_filter_text_change(self, new_text):
|
||||
self._launcher_model.set_asset_name_filter(new_text)
|
||||
|
||||
|
||||
class ActionBar(QtWidgets.QWidget):
|
||||
|
|
@ -86,10 +160,10 @@ class ActionBar(QtWidgets.QWidget):
|
|||
|
||||
action_clicked = QtCore.Signal(object)
|
||||
|
||||
def __init__(self, project_handler, dbcon, parent=None):
|
||||
def __init__(self, launcher_model, dbcon, parent=None):
|
||||
super(ActionBar, self).__init__(parent)
|
||||
|
||||
self.project_handler = project_handler
|
||||
self._launcher_model = launcher_model
|
||||
self.dbcon = dbcon
|
||||
|
||||
view = QtWidgets.QListView(self)
|
||||
|
|
@ -136,7 +210,7 @@ class ActionBar(QtWidgets.QWidget):
|
|||
|
||||
self.set_row_height(1)
|
||||
|
||||
project_handler.projects_refreshed.connect(self._on_projects_refresh)
|
||||
launcher_model.projects_refreshed.connect(self._on_projects_refresh)
|
||||
view.clicked.connect(self.on_clicked)
|
||||
view.customContextMenuRequested.connect(self.on_context_menu)
|
||||
|
||||
|
|
@ -182,8 +256,8 @@ class ActionBar(QtWidgets.QWidget):
|
|||
self.update()
|
||||
|
||||
def _start_animation(self, index):
|
||||
# Offset refresh timeout
|
||||
self.project_handler.start_timer()
|
||||
# Offset refresh timout
|
||||
self._launcher_model.start_refresh_timer()
|
||||
action_id = index.data(ACTION_ID_ROLE)
|
||||
item = self.model.items_by_id.get(action_id)
|
||||
if item:
|
||||
|
|
@ -250,8 +324,8 @@ class ActionBar(QtWidgets.QWidget):
|
|||
self.action_clicked.emit(action)
|
||||
return
|
||||
|
||||
# Offset refresh timeout
|
||||
self.project_handler.start_timer()
|
||||
# Offset refresh timout
|
||||
self._launcher_model.start_refresh_timer()
|
||||
|
||||
actions = index.data(ACTION_ROLE)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,17 +8,19 @@ from avalon.api import AvalonMongoDB
|
|||
from openpype import style
|
||||
from openpype.api import resources
|
||||
|
||||
from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget
|
||||
from openpype.tools.utils.tasks_widget import TasksWidget
|
||||
|
||||
from avalon.vendor import qtawesome
|
||||
from .models import ProjectModel
|
||||
from .lib import get_action_label, ProjectHandler
|
||||
from .models import (
|
||||
LauncherModel,
|
||||
ProjectModel
|
||||
)
|
||||
from .lib import get_action_label
|
||||
from .widgets import (
|
||||
ProjectBar,
|
||||
ActionBar,
|
||||
ActionHistory,
|
||||
SlidePageWidget
|
||||
SlidePageWidget,
|
||||
LauncherAssetsWidget,
|
||||
LauncherTaskWidget
|
||||
)
|
||||
|
||||
from openpype.tools.flickcharm import FlickCharm
|
||||
|
|
@ -89,15 +91,15 @@ class ProjectIconView(QtWidgets.QListView):
|
|||
|
||||
class ProjectsPanel(QtWidgets.QWidget):
|
||||
"""Projects Page"""
|
||||
def __init__(self, project_handler, parent=None):
|
||||
def __init__(self, launcher_model, parent=None):
|
||||
super(ProjectsPanel, self).__init__(parent=parent)
|
||||
|
||||
view = ProjectIconView(parent=self)
|
||||
view.setSelectionMode(QtWidgets.QListView.NoSelection)
|
||||
flick = FlickCharm(parent=self)
|
||||
flick.activateOn(view)
|
||||
|
||||
view.setModel(project_handler.model)
|
||||
model = ProjectModel(launcher_model)
|
||||
view.setModel(model)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -105,13 +107,14 @@ class ProjectsPanel(QtWidgets.QWidget):
|
|||
|
||||
view.clicked.connect(self.on_clicked)
|
||||
|
||||
self._model = model
|
||||
self.view = view
|
||||
self.project_handler = project_handler
|
||||
self._launcher_model = launcher_model
|
||||
|
||||
def on_clicked(self, index):
|
||||
if index.isValid():
|
||||
project_name = index.data(QtCore.Qt.DisplayRole)
|
||||
self.project_handler.set_project(project_name)
|
||||
self._launcher_model.set_project_name(project_name)
|
||||
|
||||
|
||||
class AssetsPanel(QtWidgets.QWidget):
|
||||
|
|
@ -119,7 +122,7 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
back_clicked = QtCore.Signal()
|
||||
session_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, project_handler, dbcon, parent=None):
|
||||
def __init__(self, launcher_model, dbcon, parent=None):
|
||||
super(AssetsPanel, self).__init__(parent=parent)
|
||||
|
||||
self.dbcon = dbcon
|
||||
|
|
@ -129,7 +132,7 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
btn_back = QtWidgets.QPushButton(self)
|
||||
btn_back.setIcon(btn_back_icon)
|
||||
|
||||
project_bar = ProjectBar(project_handler, self)
|
||||
project_bar = ProjectBar(launcher_model, self)
|
||||
|
||||
project_bar_layout = QtWidgets.QHBoxLayout()
|
||||
project_bar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
|
@ -138,12 +141,14 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
project_bar_layout.addWidget(project_bar)
|
||||
|
||||
# Assets widget
|
||||
assets_widget = SingleSelectAssetsWidget(dbcon=self.dbcon, parent=self)
|
||||
assets_widget = LauncherAssetsWidget(
|
||||
launcher_model, dbcon=self.dbcon, parent=self
|
||||
)
|
||||
# Make assets view flickable
|
||||
assets_widget.activate_flick_charm()
|
||||
|
||||
# Tasks widget
|
||||
tasks_widget = TasksWidget(self.dbcon, self)
|
||||
tasks_widget = LauncherTaskWidget(launcher_model, self.dbcon, self)
|
||||
|
||||
# Body
|
||||
body = QtWidgets.QSplitter(self)
|
||||
|
|
@ -165,19 +170,20 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
layout.addWidget(body)
|
||||
|
||||
# signals
|
||||
project_handler.project_changed.connect(self._on_project_changed)
|
||||
launcher_model.project_changed.connect(self._on_project_changed)
|
||||
assets_widget.selection_changed.connect(self._on_asset_changed)
|
||||
assets_widget.refreshed.connect(self._on_asset_changed)
|
||||
tasks_widget.task_changed.connect(self._on_task_change)
|
||||
|
||||
btn_back.clicked.connect(self.back_clicked)
|
||||
|
||||
self.project_handler = project_handler
|
||||
self.project_bar = project_bar
|
||||
self.assets_widget = assets_widget
|
||||
self._tasks_widget = tasks_widget
|
||||
self._btn_back = btn_back
|
||||
|
||||
self._launcher_model = launcher_model
|
||||
|
||||
def select_asset(self, asset_name):
|
||||
self.assets_widget.select_asset_by_name(asset_name)
|
||||
|
||||
|
|
@ -196,8 +202,6 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
def _on_project_changed(self):
|
||||
self.session_changed.emit()
|
||||
|
||||
self.assets_widget.refresh()
|
||||
|
||||
def _on_asset_changed(self):
|
||||
"""Callback on asset selection changed
|
||||
|
||||
|
|
@ -250,18 +254,17 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
| QtCore.Qt.WindowCloseButtonHint
|
||||
)
|
||||
|
||||
project_model = ProjectModel(self.dbcon)
|
||||
project_handler = ProjectHandler(self.dbcon, project_model)
|
||||
launcher_model = LauncherModel(self.dbcon)
|
||||
|
||||
project_panel = ProjectsPanel(project_handler)
|
||||
asset_panel = AssetsPanel(project_handler, self.dbcon)
|
||||
project_panel = ProjectsPanel(launcher_model)
|
||||
asset_panel = AssetsPanel(launcher_model, self.dbcon)
|
||||
|
||||
page_slider = SlidePageWidget()
|
||||
page_slider.addWidget(project_panel)
|
||||
page_slider.addWidget(asset_panel)
|
||||
|
||||
# actions
|
||||
actions_bar = ActionBar(project_handler, self.dbcon, self)
|
||||
actions_bar = ActionBar(launcher_model, self.dbcon, self)
|
||||
|
||||
# statusbar
|
||||
message_label = QtWidgets.QLabel(self)
|
||||
|
|
@ -303,8 +306,8 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
# signals
|
||||
actions_bar.action_clicked.connect(self.on_action_clicked)
|
||||
action_history.trigger_history.connect(self.on_history_action)
|
||||
project_handler.project_changed.connect(self.on_project_change)
|
||||
project_handler.timer_timeout.connect(self._on_refresh_timeout)
|
||||
launcher_model.project_changed.connect(self.on_project_change)
|
||||
launcher_model.timer_timeout.connect(self._on_refresh_timeout)
|
||||
asset_panel.back_clicked.connect(self.on_back_clicked)
|
||||
asset_panel.session_changed.connect(self.on_session_changed)
|
||||
|
||||
|
|
@ -314,7 +317,7 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
|
||||
self._message_timer = message_timer
|
||||
|
||||
self.project_handler = project_handler
|
||||
self._launcher_model = launcher_model
|
||||
|
||||
self._message_label = message_label
|
||||
self.project_panel = project_panel
|
||||
|
|
@ -324,19 +327,19 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
self.page_slider = page_slider
|
||||
|
||||
def showEvent(self, event):
|
||||
self.project_handler.set_active(True)
|
||||
self.project_handler.start_timer(True)
|
||||
self._launcher_model.set_active(True)
|
||||
self._launcher_model.start_refresh_timer(True)
|
||||
|
||||
super(LauncherWindow, self).showEvent(event)
|
||||
|
||||
def _on_refresh_timeout(self):
|
||||
# Stop timer if widget is not visible
|
||||
if not self.isVisible():
|
||||
self.project_handler.stop_timer()
|
||||
self._launcher_model.stop_refresh_timer()
|
||||
|
||||
def changeEvent(self, event):
|
||||
if event.type() == QtCore.QEvent.ActivationChange:
|
||||
self.project_handler.set_active(self.isActiveWindow())
|
||||
self._launcher_model.set_active(self.isActiveWindow())
|
||||
super(LauncherWindow, self).changeEvent(event)
|
||||
|
||||
def set_page(self, page):
|
||||
|
|
@ -371,7 +374,7 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
self.discover_actions()
|
||||
|
||||
def on_back_clicked(self):
|
||||
self.project_handler.set_project(None)
|
||||
self._launcher_model.set_project_name(None)
|
||||
self.set_page(0)
|
||||
self.discover_actions()
|
||||
|
||||
|
|
|
|||
|
|
@ -354,7 +354,6 @@ class AssetModel(QtGui.QStandardItemModel):
|
|||
|
||||
Args:
|
||||
force (bool): Stop currently running refresh start new refresh.
|
||||
clear (bool): Clear model before refresh thread starts.
|
||||
"""
|
||||
# Skip fetch if there is already other thread fetching documents
|
||||
if self._refreshing:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from .views import DeselectableTreeView
|
|||
TASK_NAME_ROLE = QtCore.Qt.UserRole + 1
|
||||
TASK_TYPE_ROLE = QtCore.Qt.UserRole + 2
|
||||
TASK_ORDER_ROLE = QtCore.Qt.UserRole + 3
|
||||
TASK_ASSIGNEE_ROLE = QtCore.Qt.UserRole + 4
|
||||
|
||||
|
||||
class TasksModel(QtGui.QStandardItemModel):
|
||||
|
|
@ -144,11 +145,19 @@ class TasksModel(QtGui.QStandardItemModel):
|
|||
task_type_icon = task_type_info.get("icon")
|
||||
icon = self._get_icon(task_icon, task_type_icon)
|
||||
|
||||
task_assignees = set()
|
||||
assignees_data = task_info.get("assignees") or []
|
||||
for assignee in assignees_data:
|
||||
username = assignee.get("username")
|
||||
if username:
|
||||
task_assignees.add(username)
|
||||
|
||||
label = "{} ({})".format(task_name, task_type or "type N/A")
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setData(task_name, TASK_NAME_ROLE)
|
||||
item.setData(task_type, TASK_TYPE_ROLE)
|
||||
item.setData(task_order, TASK_ORDER_ROLE)
|
||||
item.setData(task_assignees, TASK_ASSIGNEE_ROLE)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
|
||||
items.append(item)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue