diff --git a/openpype/cli.py b/openpype/cli.py index 58630bdefb..f6366f4b6b 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -282,6 +282,34 @@ def projectmanager(): PypeCommands().launch_project_manager() +@main.command() +@click.argument("output_path") +@click.option("--project", help="Define project context") +@click.option("--asset", help="Define asset in project (project must be set)") +@click.option( + "--strict", + is_flag=True, + help="Full context must be set otherwise dialog can't be closed." +) +def contextselection( + output_path, + project, + asset, + strict +): + """Show Qt dialog to select context. + + Context is project name, asset name and task name. The result is stored + into json file which path is passed in first argument. + """ + PypeCommands.contextselection( + output_path, + project, + asset, + strict + ) + + @main.command( context_settings=dict( ignore_unknown_options=True, diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index c2153785a8..dfe821fd4a 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -310,6 +310,12 @@ class PypeCommands: project_manager.main() + @staticmethod + def contextselection(output_path, project_name, asset_name, strict): + from openpype.tools.context_dialog import main + + main(output_path, project_name, asset_name, strict) + def texture_copy(self, project, asset, path): pass diff --git a/openpype/tools/context_dialog/__init__.py b/openpype/tools/context_dialog/__init__.py new file mode 100644 index 0000000000..9b10baf903 --- /dev/null +++ b/openpype/tools/context_dialog/__init__.py @@ -0,0 +1,10 @@ +from .window import ( + ContextDialog, + main +) + + +__all__ = ( + "ContextDialog", + "main" +) diff --git a/openpype/tools/context_dialog/window.py b/openpype/tools/context_dialog/window.py new file mode 100644 index 0000000000..124a1beda3 --- /dev/null +++ b/openpype/tools/context_dialog/window.py @@ -0,0 +1,423 @@ +import os +import json + +from Qt import QtWidgets, QtCore, QtGui +from avalon.api import AvalonMongoDB + +from openpype import style +from openpype.tools.utils.lib import center_window +from openpype.tools.utils.widgets import AssetWidget +from openpype.tools.utils.constants import ( + TASK_NAME_ROLE, + PROJECT_NAME_ROLE +) +from openpype.tools.utils.models import ( + ProjectModel, + ProjectSortFilterProxy, + TasksModel, + TasksProxyModel +) + + +class ContextDialog(QtWidgets.QDialog): + """Dialog to select a context. + + Context has 3 parts: + - Project + - Aseet + - Task + + It is possible to predefine project and asset. In that case their widgets + will have passed preselected values and will be disabled. + """ + def __init__(self, parent=None): + super(ContextDialog, self).__init__(parent) + + self.setWindowTitle("Select Context") + self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) + + # Enable minimize and maximize for app + window_flags = QtCore.Qt.Window + if not parent: + window_flags |= QtCore.Qt.WindowStaysOnTopHint + self.setWindowFlags(window_flags) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + dbcon = AvalonMongoDB() + + # UI initialization + main_splitter = QtWidgets.QSplitter(self) + + # Left side widget containt project combobox and asset widget + left_side_widget = QtWidgets.QWidget(main_splitter) + + project_combobox = QtWidgets.QComboBox(left_side_widget) + # Styled delegate to propagate stylessheet + project_delegate = QtWidgets.QStyledItemDelegate(project_combobox) + project_combobox.setItemDelegate(project_delegate) + # Project model with only active projects without default item + project_model = ProjectModel( + dbcon, + only_active=True, + add_default_project=False + ) + # Sorting proxy model + project_proxy = ProjectSortFilterProxy() + project_proxy.setSourceModel(project_model) + project_combobox.setModel(project_proxy) + + # Assets widget + assets_widget = AssetWidget( + dbcon, multiselection=False, parent=left_side_widget + ) + + left_side_layout = QtWidgets.QVBoxLayout(left_side_widget) + left_side_layout.setContentsMargins(0, 0, 0, 0) + left_side_layout.addWidget(project_combobox) + left_side_layout.addWidget(assets_widget) + + # Right side of window contains only tasks + task_view = QtWidgets.QListView(main_splitter) + task_model = TasksModel(dbcon) + task_proxy = TasksProxyModel() + task_proxy.setSourceModel(task_model) + task_view.setModel(task_proxy) + + # Add widgets to main splitter + main_splitter.addWidget(left_side_widget) + main_splitter.addWidget(task_view) + + # Set stretch of both sides + main_splitter.setStretchFactor(0, 7) + main_splitter.setStretchFactor(1, 3) + + # Add confimation button to bottom right + ok_btn = QtWidgets.QPushButton("OK", self) + + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons_layout.addStretch(1) + buttons_layout.addWidget(ok_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(main_splitter, 1) + main_layout.addLayout(buttons_layout, 0) + + # Timer which will trigger asset refresh + # - this is needed because asset widget triggers + # finished refresh before hides spin box so we need to trigger + # refreshing in small offset if we want re-refresh asset widget + assets_timer = QtCore.QTimer() + assets_timer.setInterval(50) + assets_timer.setSingleShot(True) + + assets_timer.timeout.connect(self._on_asset_refresh_timer) + + project_combobox.currentIndexChanged.connect( + self._on_project_combo_change + ) + assets_widget.selection_changed.connect(self._on_asset_change) + assets_widget.refresh_triggered.connect(self._on_asset_refresh_trigger) + assets_widget.refreshed.connect(self._on_asset_widget_refresh_finished) + task_view.selectionModel().selectionChanged.connect( + self._on_task_change + ) + ok_btn.clicked.connect(self._on_ok_click) + + self._dbcon = dbcon + + self._project_combobox = project_combobox + self._project_model = project_model + self._project_proxy = project_proxy + self._project_delegate = project_delegate + + self._assets_widget = assets_widget + + self._task_view = task_view + self._task_model = task_model + self._task_proxy = task_proxy + + self._ok_btn = ok_btn + + self._strict = False + + # Values set by `set_context` method + self._set_context_project = None + self._set_context_asset = None + + # Requirements for asset widget refresh + self._assets_timer = assets_timer + self._rerefresh_assets = True + self._assets_refreshing = False + + # Set stylehseet and resize window on first show + self._first_show = True + + # Helper attributes for handling of refresh + self._ignore_value_changes = False + self._refresh_on_next_show = True + + # Output of dialog + self._context_to_store = { + "project": None, + "asset": None, + "task": None + } + + def closeEvent(self, event): + """Ignore close event if is in strict state and context is not done.""" + if self._strict and not self._ok_btn.isEnabled(): + event.ignore() + return + + if self._strict: + self._confirm_values() + super(ContextDialog, self).closeEvent(event) + + def set_strict(self, strict): + """Change strictness of dialog.""" + self._strict = strict + self._validate_strict() + + def _set_refresh_on_next_show(self): + """Refresh will be called on next showEvent. + + If window is already visible then just execute refresh. + """ + self._refresh_on_next_show = True + if self.isVisible(): + self.refresh() + + def _refresh_assets(self): + """Trigger refreshing of asset widget. + + This will set mart to rerefresh asset when current refreshing is done + or do it immidietely if asset widget is not refreshing at the time. + """ + if self._assets_refreshing: + self._rerefresh_assets = True + else: + self._on_asset_refresh_timer() + + def showEvent(self, event): + """Override show event to do some callbacks.""" + super(ContextDialog, self).showEvent(event) + if self._first_show: + self._first_show = False + # Set stylesheet and resize + self.setStyleSheet(style.load_stylesheet()) + self.resize(600, 700) + center_window(self) + + if self._refresh_on_next_show: + self.refresh() + + def refresh(self): + """Refresh all widget one by one. + + When asset refresh is triggered we have to wait when is done so + this method continues with `_on_asset_widget_refresh_finished`. + """ + # Change state of refreshing (no matter how refresh was called) + self._refresh_on_next_show = False + + # Ignore changes of combobox and asset widget + self._ignore_value_changes = True + + # Get current project name to be able set it afterwards + select_project_name = self._dbcon.Session.get("AVALON_PROJECT") + # Trigger project refresh + self._project_model.refresh() + # Sort projects + self._project_proxy.sort(0) + + # Disable combobox if project was passed to `set_context` + if self._set_context_project: + select_project_name = self._set_context_project + self._project_combobox.setEnabled(False) + else: + # Find new project to select + self._project_combobox.setEnabled(True) + if ( + select_project_name is None + and self._project_proxy.rowCount() > 0 + ): + index = self._project_proxy.index(0, 0) + select_project_name = index.data(PROJECT_NAME_ROLE) + + self._ignore_value_changes = False + + idx = self._project_combobox.findText(select_project_name) + if idx >= 0: + self._project_combobox.setCurrentIndex(idx) + self._dbcon.Session["AVALON_PROJECT"] = ( + self._project_combobox.currentText() + ) + + # Trigger asset refresh + self._refresh_assets() + + def _on_asset_refresh_timer(self): + """This is only way how to trigger refresh asset widget. + + Use `_refresh_assets` method to refresh asset widget. + """ + self._assets_widget.refresh() + + def _on_asset_widget_refresh_finished(self): + """Catch when asset widget finished refreshing.""" + # If should refresh again then skip all other callbacks and trigger + # assets timer directly. + self._assets_refreshing = False + if self._rerefresh_assets: + self._rerefresh_assets = False + self._assets_timer.start() + return + + self._ignore_value_changes = True + if self._set_context_asset: + self._dbcon.Session["AVALON_ASSET"] = self._set_context_asset + self._assets_widget.setEnabled(False) + self._assets_widget.select_assets(self._set_context_asset) + self._set_asset_to_task_model() + else: + self._assets_widget.setEnabled(True) + self._assets_widget.set_current_asset_btn_visibility(False) + + # Refresh tasks + self._task_model.refresh() + # Sort tasks + self._task_proxy.sort(0, QtCore.Qt.AscendingOrder) + + self._ignore_value_changes = False + + self._validate_strict() + + def _on_project_combo_change(self): + if self._ignore_value_changes: + return + project_name = self._project_combobox.currentText() + + if self._dbcon.Session.get("AVALON_PROJECT") == project_name: + return + + self._dbcon.Session["AVALON_PROJECT"] = project_name + + self._refresh_assets() + self._validate_strict() + + def _on_asset_refresh_trigger(self): + self._assets_refreshing = True + self._on_asset_change() + + def _on_asset_change(self): + """Selected assets have changed""" + if self._ignore_value_changes: + return + self._set_asset_to_task_model() + + def _on_task_change(self): + self._validate_strict() + + def _set_asset_to_task_model(self): + # filter None docs they are silo + asset_docs = self._assets_widget.get_selected_assets() + asset_ids = [asset_doc["_id"] for asset_doc in asset_docs] + asset_id = None + if asset_ids: + asset_id = asset_ids[0] + self._task_model.set_asset_id(asset_id) + self._task_proxy.sort(0, QtCore.Qt.AscendingOrder) + + def _confirm_values(self): + """Store values to output.""" + self._context_to_store["project"] = self.get_selected_project() + self._context_to_store["asset"] = self.get_selected_asset() + self._context_to_store["task"] = self.get_selected_task() + + def _on_ok_click(self): + # Store values to output + self._confirm_values() + # Close dialog + self.accept() + + def get_selected_project(self): + """Get selected project.""" + return self._project_combobox.currentText() + + def get_selected_asset(self): + """Currently selected asset in asset widget.""" + asset_name = None + for asset_doc in self._assets_widget.get_selected_assets(): + asset_name = asset_doc["name"] + break + return asset_name + + def get_selected_task(self): + """Currently selected task.""" + task_name = None + index = self._task_view.selectionModel().currentIndex() + if index.isValid(): + task_name = index.data(TASK_NAME_ROLE) + return task_name + + def _validate_strict(self): + if not self._strict: + if not self._ok_btn.isEnabled(): + self._ok_btn.setEnabled(True) + return + + enabled = True + if not self._set_context_project and not self.get_selected_project(): + enabled = False + elif not self._set_context_asset and not self.get_selected_asset(): + enabled = False + elif not self.get_selected_task(): + enabled = False + self._ok_btn.setEnabled(enabled) + + def set_context(self, project_name=None, asset_name=None): + """Set context which will be used and locked in dialog.""" + if project_name is None: + asset_name = None + + self._set_context_project = project_name + self._set_context_asset = asset_name + + self._context_to_store["project"] = project_name + self._context_to_store["asset"] = asset_name + + self._set_refresh_on_next_show() + + def get_context(self): + """Result of dialog.""" + return self._context_to_store + + +def main( + path_to_store, + project_name=None, + asset_name=None, + strict=True +): + # Run Qt application + app = QtWidgets.QApplication.instance() + if app is None: + app = QtWidgets.QApplication([]) + window = ContextDialog() + window.set_strict(strict) + window.set_context(project_name, asset_name) + window.show() + app.exec_() + + # Get result from window + data = window.get_context() + + # Make sure json filepath directory exists + file_dir = os.path.dirname(path_to_store) + if not os.path.exists(file_dir): + os.makedirs(file_dir) + + # Store result into json file + with open(path_to_store, "w") as stream: + json.dump(data, stream) diff --git a/openpype/tools/utils/constants.py b/openpype/tools/utils/constants.py new file mode 100644 index 0000000000..0e940a5595 --- /dev/null +++ b/openpype/tools/utils/constants.py @@ -0,0 +1,10 @@ +from Qt import QtCore + + +DEFAULT_PROJECT_LABEL = "< Default >" +PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 101 +PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 102 + +TASK_NAME_ROLE = QtCore.Qt.UserRole + 301 +TASK_TYPE_ROLE = QtCore.Qt.UserRole + 302 +TASK_ORDER_ROLE = QtCore.Qt.UserRole + 403 diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index d01dbbd169..720c73e16b 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -13,6 +13,16 @@ from openpype.api import get_project_settings from openpype.lib import filter_profiles +def center_window(window): + """Move window to center of it's screen.""" + desktop = QtWidgets.QApplication.desktop() + screen_idx = desktop.screenNumber(window) + screen_geo = desktop.screenGeometry(screen_idx) + geo = window.frameGeometry() + geo.moveCenter(screen_geo.center()) + window.move(geo.topLeft()) + + def format_version(value, hero_version=False): """Formats integer to displayable version name""" label = "v{0:03d}".format(value) diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py index c5e1ce1b12..c488743f36 100644 --- a/openpype/tools/utils/models.py +++ b/openpype/tools/utils/models.py @@ -8,6 +8,14 @@ from Qt import QtCore, QtGui from avalon.vendor import qtawesome from avalon import style, io from . import lib +from .constants import ( + PROJECT_IS_ACTIVE_ROLE, + PROJECT_NAME_ROLE, + DEFAULT_PROJECT_LABEL, + TASK_ORDER_ROLE, + TASK_TYPE_ROLE, + TASK_NAME_ROLE +) log = logging.getLogger(__name__) @@ -498,3 +506,311 @@ class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): return super( RecursiveSortFilterProxyModel, self ).filterAcceptsRow(row, parent) + + +class ProjectModel(QtGui.QStandardItemModel): + def __init__( + self, dbcon=None, only_active=True, add_default_project=False, + *args, **kwargs + ): + super(ProjectModel, self).__init__(*args, **kwargs) + + self.dbcon = dbcon + + self._only_active = only_active + self._add_default_project = add_default_project + + self._default_item = None + self._items_by_name = {} + # Model was at least once refreshed + # - for `set_dbcon` method + self._refreshed = False + + def set_default_project_available(self, available=True): + if available is None: + available = not self._add_default_project + + if self._add_default_project == available: + return + + self._add_default_project = available + if not available and self._default_item is not None: + root_item = self.invisibleRootItem() + root_item.removeRow(self._default_item.row()) + self._default_item = None + + def set_only_active(self, only_active=True): + if only_active is None: + only_active = not self._only_active + + if self._only_active == only_active: + return + + self._only_active = only_active + + if self._refreshed: + self.refresh() + + def set_dbcon(self, dbcon): + """Change mongo connection.""" + self.dbcon = dbcon + # Trigger refresh if was already refreshed + if self._refreshed: + self.refresh() + + def project_name_is_available(self, project_name): + """Check availability of project name in current items.""" + return project_name in self._items_by_name + + def refresh(self): + # Change '_refreshed' state + self._refreshed = True + new_items = [] + # Add default item to model if should + if self._add_default_project and self._default_item is None: + item = QtGui.QStandardItem(DEFAULT_PROJECT_LABEL) + item.setData(None, PROJECT_NAME_ROLE) + item.setData(True, PROJECT_IS_ACTIVE_ROLE) + new_items.append(item) + self._default_item = item + + project_names = set() + if self.dbcon is not None: + for project_doc in self.dbcon.projects( + projection={"name": 1, "data.active": 1}, + only_active=self._only_active + ): + project_name = project_doc["name"] + project_names.add(project_name) + if project_name in self._items_by_name: + item = self._items_by_name[project_name] + else: + item = QtGui.QStandardItem(project_name) + + self._items_by_name[project_name] = item + new_items.append(item) + + is_active = project_doc.get("data", {}).get("active", True) + item.setData(project_name, PROJECT_NAME_ROLE) + item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) + + if not is_active: + font = item.font() + font.setItalic(True) + item.setFont(font) + + root_item = self.invisibleRootItem() + for project_name in tuple(self._items_by_name.keys()): + if project_name not in project_names: + item = self._items_by_name.pop(project_name) + root_item.removeRow(item.row()) + + if new_items: + root_item.appendRows(new_items) + + +class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(ProjectSortFilterProxy, self).__init__(*args, **kwargs) + self._filter_enabled = True + + def lessThan(self, left_index, right_index): + if left_index.data(PROJECT_NAME_ROLE) is None: + return True + + if right_index.data(PROJECT_NAME_ROLE) is None: + return False + + left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE) + right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE) + if right_is_active == left_is_active: + return super(ProjectSortFilterProxy, self).lessThan( + left_index, right_index + ) + + if left_is_active: + return True + return False + + def filterAcceptsRow(self, source_row, source_parent): + index = self.sourceModel().index(source_row, 0, source_parent) + if self._filter_enabled: + result = self._custom_index_filter(index) + if result is not None: + return result + + return super(ProjectSortFilterProxy, self).filterAcceptsRow( + source_row, source_parent + ) + + def _custom_index_filter(self, index): + is_active = bool(index.data(PROJECT_IS_ACTIVE_ROLE)) + + return is_active + + def is_filter_enabled(self): + return self._filter_enabled + + def set_filter_enabled(self, value): + self._filter_enabled = value + self.invalidateFilter() + + +class TasksModel(QtGui.QStandardItemModel): + """A model listing the tasks combined for a list of assets""" + def __init__(self, dbcon, parent=None): + super(TasksModel, self).__init__(parent=parent) + self.dbcon = dbcon + self._default_icon = qtawesome.icon( + "fa.male", + color=style.colors.default + ) + self._no_tasks_icon = qtawesome.icon( + "fa.exclamation-circle", + color=style.colors.mid + ) + self._cached_icons = {} + self._project_task_types = {} + + self._last_asset_id = None + + self.refresh() + + def refresh(self): + if self.dbcon.Session.get("AVALON_PROJECT"): + self._refresh_task_types() + self.set_asset_id(self._last_asset_id) + else: + self.clear() + + def _refresh_task_types(self): + # Get the project configured icons from database + project = self.dbcon.find_one( + {"type": "project"}, + {"config.tasks"} + ) + tasks = project["config"].get("tasks") or {} + self._project_task_types = tasks + + def _try_get_awesome_icon(self, icon_name): + icon = None + if icon_name: + try: + icon = qtawesome.icon( + "fa.{}".format(icon_name), + color=style.colors.default + ) + + except Exception: + pass + return icon + + def headerData(self, section, orientation, role): + # Show nice labels in the header + if ( + role == QtCore.Qt.DisplayRole + and orientation == QtCore.Qt.Horizontal + ): + if section == 0: + return "Tasks" + + return super(TasksModel, self).headerData(section, orientation, role) + + def _get_icon(self, task_icon, task_type_icon): + if task_icon in self._cached_icons: + return self._cached_icons[task_icon] + + icon = self._try_get_awesome_icon(task_icon) + if icon is not None: + self._cached_icons[task_icon] = icon + return icon + + if task_type_icon in self._cached_icons: + icon = self._cached_icons[task_type_icon] + self._cached_icons[task_icon] = icon + return icon + + icon = self._try_get_awesome_icon(task_type_icon) + if icon is None: + icon = self._default_icon + + self._cached_icons[task_icon] = icon + self._cached_icons[task_type_icon] = icon + + return icon + + def set_asset_id(self, asset_id): + asset_doc = None + if asset_id: + asset_doc = self.dbcon.find_one( + {"_id": asset_id}, + {"data.tasks": True} + ) + self.set_asset(asset_doc) + + def set_asset(self, asset_doc): + """Set assets to track by their database id + + Arguments: + asset_doc (dict): Asset document from MongoDB. + """ + self.clear() + + if not asset_doc: + self._last_asset_id = None + return + + self._last_asset_id = asset_doc["_id"] + + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + items = [] + for task_name, task_info in asset_tasks.items(): + task_icon = task_info.get("icon") + task_type = task_info.get("type") + task_order = task_info.get("order") + task_type_info = self._project_task_types.get(task_type) or {} + task_type_icon = task_type_info.get("icon") + icon = self._get_icon(task_icon, task_type_icon) + + 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(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) + items.append(item) + + if not items: + item = QtGui.QStandardItem("No task") + item.setData(self._no_tasks_icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + items.append(item) + + self.invisibleRootItem().appendRows(items) + + +class TasksProxyModel(QtCore.QSortFilterProxyModel): + def lessThan(self, x_index, y_index): + x_order = x_index.data(TASK_ORDER_ROLE) + y_order = y_index.data(TASK_ORDER_ROLE) + if x_order is not None and y_order is not None: + if x_order < y_order: + return True + if x_order > y_order: + return False + + elif x_order is None and y_order is not None: + return True + + elif y_order is None and x_order is not None: + return False + + x_name = x_index.data(QtCore.Qt.DisplayRole) + y_name = y_index.data(QtCore.Qt.DisplayRole) + if x_name == y_name: + return True + + if x_name == tuple(sorted((x_name, y_name)))[0]: + return True + return False diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 1679a18241..0ca5dca650 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -14,13 +14,15 @@ from avalon.tools import lib as tools_lib from avalon.tools.widgets import AssetWidget from avalon.tools.delegates import PrettyTimeDelegate -from .model import ( +from openpype.tools.utils.constants import ( TASK_NAME_ROLE, - TASK_TYPE_ROLE, - FilesModel, + TASK_TYPE_ROLE +) +from openpype.tools.utils.models import ( TasksModel, TasksProxyModel ) +from .model import FilesModel from .view import FilesView from openpype.lib import ( @@ -359,6 +361,7 @@ class TasksWidget(QtWidgets.QWidget): self._last_selected_task = current self._tasks_model.set_asset(asset_doc) + self._tasks_proxy.sort(0, QtCore.Qt.AscendingOrder) if self._last_selected_task: self.select_task(self._last_selected_task) diff --git a/openpype/tools/workfiles/model.py b/openpype/tools/workfiles/model.py index 92fbf76b95..583f495606 100644 --- a/openpype/tools/workfiles/model.py +++ b/openpype/tools/workfiles/model.py @@ -9,10 +9,6 @@ from avalon.tools.models import TreeModel, Item log = logging.getLogger(__name__) -TASK_NAME_ROLE = QtCore.Qt.UserRole + 1 -TASK_TYPE_ROLE = QtCore.Qt.UserRole + 2 -TASK_ORDER_ROLE = QtCore.Qt.UserRole + 3 - class FilesModel(TreeModel): """Model listing files with specified extensions in a root folder""" @@ -155,142 +151,3 @@ class FilesModel(TreeModel): return "Date modified" return super(FilesModel, self).headerData(section, orientation, role) - - -class TasksProxyModel(QtCore.QSortFilterProxyModel): - def lessThan(self, x_index, y_index): - x_order = x_index.data(TASK_ORDER_ROLE) - y_order = y_index.data(TASK_ORDER_ROLE) - if x_order is not None and y_order is not None: - if x_order < y_order: - return True - if x_order > y_order: - return False - - elif x_order is None and y_order is not None: - return True - - elif y_order is None and x_order is not None: - return False - - x_name = x_index.data(QtCore.Qt.DisplayRole) - y_name = y_index.data(QtCore.Qt.DisplayRole) - if x_name == y_name: - return True - - if x_name == tuple(sorted((x_name, y_name)))[0]: - return False - return True - - -class TasksModel(QtGui.QStandardItemModel): - """A model listing the tasks combined for a list of assets""" - def __init__(self, dbcon, parent=None): - super(TasksModel, self).__init__(parent=parent) - self.dbcon = dbcon - self._default_icon = qtawesome.icon( - "fa.male", - color=style.colors.default - ) - self._no_tasks_icon = qtawesome.icon( - "fa.exclamation-circle", - color=style.colors.mid - ) - self._cached_icons = {} - self._project_task_types = {} - - self._refresh_task_types() - - def _refresh_task_types(self): - # Get the project configured icons from database - project = self.dbcon.find_one( - {"type": "project"}, - {"config.tasks"} - ) - tasks = project["config"].get("tasks") or {} - self._project_task_types = tasks - - def _try_get_awesome_icon(self, icon_name): - icon = None - if icon_name: - try: - icon = qtawesome.icon( - "fa.{}".format(icon_name), - color=style.colors.default - ) - - except Exception: - pass - return icon - - def headerData(self, section, orientation, role): - # Show nice labels in the header - if ( - role == QtCore.Qt.DisplayRole - and orientation == QtCore.Qt.Horizontal - ): - if section == 0: - return "Tasks" - - return super(TasksModel, self).headerData(section, orientation, role) - - def _get_icon(self, task_icon, task_type_icon): - if task_icon in self._cached_icons: - return self._cached_icons[task_icon] - - icon = self._try_get_awesome_icon(task_icon) - if icon is not None: - self._cached_icons[task_icon] = icon - return icon - - if task_type_icon in self._cached_icons: - icon = self._cached_icons[task_type_icon] - self._cached_icons[task_icon] = icon - return icon - - icon = self._try_get_awesome_icon(task_type_icon) - if icon is None: - icon = self._default_icon - - self._cached_icons[task_icon] = icon - self._cached_icons[task_type_icon] = icon - - return icon - - def set_asset(self, asset_doc): - """Set assets to track by their database id - - Arguments: - asset_doc (dict): Asset document from MongoDB. - """ - self.clear() - - if not asset_doc: - return - - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - items = [] - for task_name, task_info in asset_tasks.items(): - task_icon = task_info.get("icon") - task_type = task_info.get("type") - task_order = task_info.get("order") - task_type_info = self._project_task_types.get(task_type) or {} - task_type_icon = task_type_info.get("icon") - icon = self._get_icon(task_icon, task_type_icon) - - 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(icon, QtCore.Qt.DecorationRole) - item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) - items.append(item) - - if not items: - item = QtGui.QStandardItem("No task") - item.setData(self._no_tasks_icon, QtCore.Qt.DecorationRole) - item.setFlags(QtCore.Qt.NoItemFlags) - items.append(item) - - self.invisibleRootItem().appendRows(items)