diff --git a/openpype/tools/launcher/lib.py b/openpype/tools/launcher/lib.py index 4d678b96ae..b4e6a0c3e9 100644 --- a/openpype/tools/launcher/lib.py +++ b/openpype/tools/launcher/lib.py @@ -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: diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index ecee8b1575..effa283318 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -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"]) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 5dad41c349..30e6531843 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -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) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index a8f65894f2..b5b6368865 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -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() diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index be9f442d10..17164d9e0f 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -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: diff --git a/openpype/tools/utils/tasks_widget.py b/openpype/tools/utils/tasks_widget.py index 6c7787d06a..2a8a45626c 100644 --- a/openpype/tools/utils/tasks_widget.py +++ b/openpype/tools/utils/tasks_widget.py @@ -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)