From 9868b09c9bbd546d98148c7a80c087b87f84a766 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 10 Oct 2023 15:58:34 +0200 Subject: [PATCH] prepared context dialog using AYON calls --- openpype/tools/context_dialog/_ayon_window.py | 783 ++++++++++++++++++ .../tools/context_dialog/_openpype_window.py | 396 +++++++++ openpype/tools/context_dialog/window.py | 402 +-------- 3 files changed, 1188 insertions(+), 393 deletions(-) create mode 100644 openpype/tools/context_dialog/_ayon_window.py create mode 100644 openpype/tools/context_dialog/_openpype_window.py diff --git a/openpype/tools/context_dialog/_ayon_window.py b/openpype/tools/context_dialog/_ayon_window.py new file mode 100644 index 0000000000..6514780236 --- /dev/null +++ b/openpype/tools/context_dialog/_ayon_window.py @@ -0,0 +1,783 @@ +import os +import json + +import ayon_api +from qtpy import QtWidgets, QtCore, QtGui + +from openpype import style +from openpype.lib.events import QueuedEventSystem +from openpype.tools.ayon_utils.models import ( + ProjectsModel, + HierarchyModel, +) +from openpype.tools.ayon_utils.widgets import ( + ProjectsCombobox, + FoldersWidget, + TasksWidget, +) +from openpype.tools.utils.lib import ( + center_window, + get_openpype_qt_app, +) + + +class SelectionModel(object): + """Model handling selection changes. + + Triggering events: + - "selection.project.changed" + - "selection.folder.changed" + - "selection.task.changed" + """ + + event_source = "selection.model" + + def __init__(self, controller): + self._controller = controller + + self._project_name = None + self._folder_id = None + self._task_id = None + self._task_name = None + + def get_selected_project_name(self): + return self._project_name + + def set_selected_project(self, project_name): + self._project_name = project_name + self._controller.emit_event( + "selection.project.changed", + {"project_name": project_name}, + self.event_source + ) + + def get_selected_folder_id(self): + return self._folder_id + + def set_selected_folder(self, folder_id): + if folder_id == self._folder_id: + return + self._folder_id = folder_id + self._controller.emit_event( + "selection.folder.changed", + { + "project_name": self._project_name, + "folder_id": folder_id, + }, + self.event_source + ) + + def get_selected_task_name(self): + return self._task_name + + def get_selected_task_id(self): + return self._task_id + + def set_selected_task(self, task_id, task_name): + if task_id == self._task_id: + return + + self._task_name = task_name + self._task_id = task_id + self._controller.emit_event( + "selection.task.changed", + { + "project_name": self._project_name, + "folder_id": self._folder_id, + "task_name": task_name, + "task_id": task_id, + }, + self.event_source + ) + + +class ExpectedSelection: + def __init__(self, controller): + self._project_name = None + self._folder_id = None + + self._project_selected = True + self._folder_selected = True + + self._controller = controller + + def _emit_change(self): + self._controller.emit_event( + "expected_selection_changed", + self.get_expected_selection_data(), + ) + + def set_expected_selection(self, project_name, folder_id): + self._project_name = project_name + self._folder_id = folder_id + + self._project_selected = False + self._folder_selected = False + self._emit_change() + + def get_expected_selection_data(self): + project_current = False + folder_current = False + if not self._project_selected: + project_current = True + elif not self._folder_selected: + folder_current = True + return { + "project": { + "name": self._project_name, + "current": project_current, + "selected": self._project_selected, + }, + "folder": { + "id": self._folder_id, + "current": folder_current, + "selected": self._folder_selected, + }, + } + + def is_expected_project_selected(self, project_name): + return project_name == self._project_name and self._project_selected + + def is_expected_folder_selected(self, folder_id): + return folder_id == self._folder_id and self._folder_selected + + def expected_project_selected(self, project_name): + if project_name != self._project_name: + return False + self._project_selected = True + self._emit_change() + return True + + def expected_folder_selected(self, folder_id): + if folder_id != self._folder_id: + return False + self._folder_selected = True + self._emit_change() + return True + + +class ContextDialogController: + def __init__(self): + self._event_system = None + + self._projects_model = ProjectsModel(self) + self._hierarchy_model = HierarchyModel(self) + self._selection_model = SelectionModel(self) + self._expected_selection = ExpectedSelection(self) + + self._confirmed = False + self._is_strict = False + self._output_path = None + + self._initial_project_name = None + self._initial_folder_id = None + self._initial_folder_label = None + self._initial_project_found = True + self._initial_folder_found = True + self._initial_tasks_found = True + + def reset(self): + self._emit_event("controller.reset.started") + + self._confirmed = False + self._output_path = None + + self._initial_project_name = None + self._initial_folder_id = None + self._initial_folder_label = None + self._initial_project_found = True + self._initial_folder_found = True + self._initial_tasks_found = True + + self._projects_model.reset() + self._hierarchy_model.reset() + + self._emit_event("controller.reset.finished") + + def refresh(self): + self._emit_event("controller.refresh.started") + + self._projects_model.reset() + self._hierarchy_model.reset() + + self._emit_event("controller.refresh.finished") + + # Event handling + def emit_event(self, topic, data=None, source=None): + """Use implemented event system to trigger event.""" + + if data is None: + data = {} + self._get_event_system().emit(topic, data, source) + + def register_event_callback(self, topic, callback): + self._get_event_system().add_callback(topic, callback) + + def set_output_json_path(self, output_path): + self._output_path = output_path + + def is_strict(self): + return self._is_strict + + def set_strict(self, enabled): + if self._is_strict is enabled: + return + self._is_strict = enabled + self._emit_event("strict.changed", {"strict": enabled}) + + # Data model functions + def get_project_items(self, sender=None): + return self._projects_model.get_project_items(sender) + + def get_folder_items(self, project_name, sender=None): + return self._hierarchy_model.get_folder_items(project_name, sender) + + def get_task_items(self, project_name, folder_id, sender=None): + return self._hierarchy_model.get_task_items( + project_name, folder_id, sender + ) + + # Expected selection helpers + def set_expected_selection(self, project_name, folder_id): + return self._expected_selection.set_expected_selection( + project_name, folder_id + ) + + def get_expected_selection_data(self): + return self._expected_selection.get_expected_selection_data() + + def expected_project_selected(self, project_name): + self._expected_selection.expected_project_selected(project_name) + + def expected_folder_selected(self, folder_id): + self._expected_selection.expected_folder_selected(folder_id) + + # Selection handling + def get_selected_project_name(self): + return self._selection_model.get_selected_project_name() + + def set_selected_project(self, project_name): + self._selection_model.set_selected_project(project_name) + + def get_selected_folder_id(self): + return self._selection_model.get_selected_folder_id() + + def set_selected_folder(self, folder_id): + self._selection_model.set_selected_folder(folder_id) + + def get_selected_task_name(self): + return self._selection_model.get_selected_task_name() + + def get_selected_task_id(self): + return self._selection_model.get_selected_task_id() + + def set_selected_task(self, task_id, task_name): + self._selection_model.set_selected_task(task_id, task_name) + + def is_initial_context_valid(self): + return self._initial_folder_found and self._initial_project_found + + def set_initial_context( + self, project_name=None, asset_name=None, folder_path=None + ): + if project_name is None: + project_found = True + asset_name = None + folder_path = None + + else: + project = ayon_api.get_project(project_name) + project_found = project is not None + + folder_id = None + folder_found = True + folder_label = None + if folder_path: + folder_label = folder_path + folder = ayon_api.get_folder_by_path(project_name, folder_path) + if folder: + folder_id = folder["id"] + else: + folder_found = False + elif asset_name: + folder_label = asset_name + for folder in ayon_api.get_folders( + project_name, folder_names=[asset_name] + ): + folder_id = folder["id"] + break + if not folder_id: + folder_found = False + + tasks_found = True + if folder_found and (folder_path or asset_name): + tasks = list(ayon_api.get_tasks( + project_name, folder_ids=[folder_id], fields=["id"] + )) + if not tasks: + tasks_found = False + + self._initial_project_name = project_name + self._initial_folder_id = folder_id + self._initial_folder_label = folder_label + self._initial_folder_found = project_found + self._initial_folder_found = folder_found + self._initial_tasks_found = tasks_found + self._emit_event( + "initial.context.changed", + self.get_initial_context() + ) + + def get_initial_context(self): + return { + "project_name": self._initial_project_name, + "folder_id": self._initial_folder_id, + "folder_label": self._initial_folder_label, + "project_found": self._initial_project_found, + "folder_found": self._initial_folder_found, + "tasks_found": self._initial_tasks_found, + "valid": ( + self._initial_project_found + and self._initial_folder_found + and self._initial_tasks_found + ) + } + + # Result of this tool + def get_selected_context(self): + return { + "project": None, + "project_name": None, + "asset": None, + "folder_id": None, + "folder_path": None, + "task": None, + "task_id": None, + "task_name": None, + } + + def window_closed(self): + if not self._confirmed and not self._is_strict: + return + + self._store_output() + + def confirm_selection(self): + self._confirmed = True + self._emit_event( + "selection.confirmed", + {"confirmed": True} + ) + + def _store_output(self): + if not self._output_path: + return + + dirpath = os.path.dirname(self._output_path) + os.makedirs(dirpath, exist_ok=True) + with open(self._output_path, "w") as stream: + json.dump(self.get_selected_context(), stream) + + def _get_event_system(self): + """Inner event system for workfiles tool controller. + + Is used for communication with UI. Event system is created on demand. + + Returns: + QueuedEventSystem: Event system which can trigger callbacks + for topics. + """ + + if self._event_system is None: + self._event_system = QueuedEventSystem() + return self._event_system + + def _emit_event(self, topic, data=None): + self.emit_event(topic, data, "controller") + + +class InvalidContextOverlay(QtWidgets.QFrame): + confirmed = QtCore.Signal() + + def __init__(self, parent): + super(InvalidContextOverlay, self).__init__(parent) + self.setObjectName("OverlayFrame") + + mid_widget = QtWidgets.QWidget(self) + label_widget = QtWidgets.QLabel( + "Requested context was not found...", + mid_widget + ) + + confirm_btn = QtWidgets.QPushButton("Close", mid_widget) + + mid_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + mid_layout = QtWidgets.QVBoxLayout(mid_widget) + mid_layout.setContentsMargins(0, 0, 0, 0) + mid_layout.addWidget(label_widget, 0) + mid_layout.addSpacing(30) + mid_layout.addWidget(confirm_btn, 0) + + main_layout = QtWidgets.QGridLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(mid_widget, 1, 1) + main_layout.setRowStretch(0, 1) + main_layout.setRowStretch(1, 0) + main_layout.setRowStretch(2, 1) + main_layout.setColumnStretch(0, 1) + main_layout.setColumnStretch(1, 0) + main_layout.setColumnStretch(2, 1) + + confirm_btn.clicked.connect(self.confirmed) + + self._label_widget = label_widget + self._confirm_btn = confirm_btn + + def set_context( + self, + project_name, + folder_label, + project_found, + folder_found, + tasks_found, + ): + lines = [] + if not project_found: + lines.extend([ + "Requested project {} was not found...".format(project_name), + ]) + + elif not folder_found: + lines.extend([ + "Requested folder was not found...", + "", + "Project: {}".format(project_name), + "Folder: {}".format(folder_label), + ]) + elif not tasks_found: + lines.extend([ + "Requested folder does not have any tasks...", + "", + "Project: {}".format(project_name), + "Folder: {}".format(folder_label), + ]) + else: + lines.append("Requested context was not found...") + self._label_widget.setText("
".join(lines)) + + +class ContextDialog(QtWidgets.QDialog): + """Dialog to select a context. + + Context has 3 parts: + - Project + - Asset + - 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, controller=None, parent=None): + super(ContextDialog, self).__init__(parent) + + self.setWindowTitle("Select Context") + self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) + + if controller is None: + controller = ContextDialogController() + + # 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) + + # UI initialization + main_splitter = QtWidgets.QSplitter(self) + + # Left side widget contains project combobox and asset widget + left_side_widget = QtWidgets.QWidget(main_splitter) + + project_combobox = ProjectsCombobox( + controller, + parent=left_side_widget, + handle_expected_selection=True + ) + + # Assets widget + folders_widget = FoldersWidget( + controller, + parent=left_side_widget, + handle_expected_selection=True + ) + + left_side_layout = QtWidgets.QVBoxLayout(left_side_widget) + left_side_layout.setContentsMargins(0, 0, 0, 0) + left_side_layout.addWidget(project_combobox, 0) + left_side_layout.addWidget(folders_widget, 1) + + # Right side of window contains only tasks + tasks_widget = TasksWidget(controller, parent=main_splitter) + + # Add widgets to main splitter + main_splitter.addWidget(left_side_widget) + main_splitter.addWidget(tasks_widget) + + # 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) + + overlay_widget = InvalidContextOverlay(self) + overlay_widget.setVisible(False) + + ok_btn.clicked.connect(self._on_ok_click) + project_combobox.refreshed.connect(self._on_projects_refresh) + overlay_widget.confirmed.connect(self._on_overlay_confirm) + + controller.register_event_callback( + "selection.project.changed", + self._on_project_selection_change + ) + controller.register_event_callback( + "selection.folder.changed", + self._on_folder_selection_change + ) + controller.register_event_callback( + "selection.task.changed", + self._on_task_selection_change + ) + controller.register_event_callback( + "initial.context.changed", + self._on_init_context_change + ) + controller.register_event_callback( + "strict.changed", + self._on_strict_changed + ) + controller.register_event_callback( + "controller.reset.finished", + self._on_controller_reset + ) + controller.register_event_callback( + "controller.refresh.finished", + self._on_controller_refresh + ) + + # Set stylehseet and resize window on first show + self._first_show = True + self._visible = False + + self._controller = controller + + self._project_combobox = project_combobox + self._folders_widget = folders_widget + self._tasks_widget = tasks_widget + + self._ok_btn = ok_btn + + self._overlay_widget = overlay_widget + + self._apply_strict_changes(self.is_strict()) + + def is_strict(self): + return self._controller.is_strict() + + def showEvent(self, event): + """Override show event to do some callbacks.""" + super(ContextDialog, self).showEvent(event) + self._visible = True + + if self._first_show: + self._first_show = False + # Set stylesheet and resize + self.setStyleSheet(style.load_stylesheet()) + self.resize(600, 700) + center_window(self) + self._controller.refresh() + + initial_context = self._controller.get_initial_context() + self._set_init_context(initial_context) + self._overlay_widget.resize(self.size()) + + def resizeEvent(self, event): + super(ContextDialog, self).resizeEvent(event) + self._overlay_widget.resize(self.size()) + + def closeEvent(self, event): + """Ignore close event if is in strict state and context is not done.""" + if self.is_strict() and not self._ok_btn.isEnabled(): + # Allow to close window when initial context is not valid + if self._controller.is_initial_context_valid(): + event.ignore() + return + + if self.is_strict(): + self._controller.confirm_selection() + self._visible = False + super(ContextDialog, self).closeEvent(event) + + def set_strict(self, enabled): + """Change strictness of dialog.""" + + self._controller.set_strict(enabled) + + 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`. + """ + + self._controller.reset() + + def get_context(self): + """Result of dialog.""" + return self._controller.get_selected_context() + + def set_context(self, project_name=None, asset_name=None): + """Set context which will be used and locked in dialog.""" + + self._controller.set_initial_context(project_name, asset_name) + + def _on_projects_refresh(self): + initial_context = self._controller.get_initial_context() + self._controller.set_expected_selection( + initial_context["project_name"], + initial_context["folder_id"] + ) + + def _on_overlay_confirm(self): + self.close() + + def _on_ok_click(self): + # Store values to output + self._controller.confirm_selection() + # Close dialog + self.accept() + + def _on_project_selection_change(self, event): + self._on_selection_change( + event["project_name"], + ) + + def _on_folder_selection_change(self, event): + self._on_selection_change( + event["project_name"], + event["folder_id"], + ) + + def _on_task_selection_change(self, event): + self._on_selection_change( + event["project_name"], + event["folder_id"], + event["task_name"], + ) + + def _on_selection_change( + self, project_name, folder_id=None, task_name=None + ): + self._validate_strict(project_name, folder_id, task_name) + + def _on_init_context_change(self, event): + self._set_init_context(event.data) + if self._visible: + self._controller.set_expected_selection( + event["project_name"], event["folder_id"] + ) + + def _set_init_context(self, init_context): + project_name = init_context["project_name"] + if not init_context["valid"]: + self._overlay_widget.setVisible(True) + self._overlay_widget.set_context( + project_name, + init_context["folder_label"], + init_context["project_found"], + init_context["folder_found"], + init_context["tasks_found"] + ) + return + + self._overlay_widget.setVisible(False) + if project_name: + self._project_combobox.setEnabled(False) + if init_context["folder_id"]: + self._folders_widget.setEnabled(False) + else: + self._project_combobox.setEnabled(True) + self._folders_widget.setEnabled(True) + + def _on_strict_changed(self, event): + self._apply_strict_changes(event["strict"]) + + def _on_controller_reset(self): + self._apply_strict_changes(self.is_strict()) + self._project_combobox.refresh() + + def _on_controller_refresh(self): + self._project_combobox.refresh() + + def _apply_strict_changes(self, is_strict): + if not is_strict: + if not self._ok_btn.isEnabled(): + self._ok_btn.setEnabled(True) + return + context = self._controller.get_selected_context() + self._validate_strict( + context["project_name"], + context["folder_id"], + context["task_name"] + ) + + def _validate_strict(self, project_name, folder_id, task_name): + if not self.is_strict(): + return + + enabled = True + if not project_name or not folder_id or not task_name: + enabled = False + self._ok_btn.setEnabled(enabled) + + +def main( + path_to_store, + project_name=None, + asset_name=None, + strict=True +): + # Run Qt application + app = get_openpype_qt_app() + controller = ContextDialogController() + controller.set_strict(strict) + controller.set_initial_context(project_name, asset_name) + controller.set_output_json_path(path_to_store) + window = ContextDialog(controller=controller) + 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/context_dialog/_openpype_window.py b/openpype/tools/context_dialog/_openpype_window.py new file mode 100644 index 0000000000..d370772a7f --- /dev/null +++ b/openpype/tools/context_dialog/_openpype_window.py @@ -0,0 +1,396 @@ +import os +import json + +from qtpy import QtWidgets, QtCore, QtGui + +from openpype import style +from openpype.pipeline import AvalonMongoDB +from openpype.tools.utils.lib import center_window, get_openpype_qt_app +from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget +from openpype.tools.utils.constants import ( + PROJECT_NAME_ROLE +) +from openpype.tools.utils.tasks_widget import TasksWidget +from openpype.tools.utils.models import ( + ProjectModel, + ProjectSortFilterProxy +) + + +class ContextDialog(QtWidgets.QDialog): + """Dialog to select a context. + + Context has 3 parts: + - Project + - Asset + - 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 contains 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 = SingleSelectAssetsWidget( + dbcon, 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 + tasks_widget = TasksWidget(dbcon, main_splitter) + + # Add widgets to main splitter + main_splitter.addWidget(left_side_widget) + main_splitter.addWidget(tasks_widget) + + # 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) + tasks_widget.task_changed.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._tasks_widget = tasks_widget + + 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_asset_by_name(self._set_context_asset) + self._set_asset_to_tasks_widget() + else: + self._assets_widget.setEnabled(True) + self._assets_widget.set_current_asset_btn_visibility(False) + + # Refresh tasks + self._tasks_widget.refresh() + + 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_tasks_widget() + + def _on_task_change(self): + self._validate_strict() + + def _set_asset_to_tasks_widget(self): + asset_id = self._assets_widget.get_selected_asset_id() + + self._tasks_widget.set_asset_id(asset_id) + + 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.""" + return self._assets_widget.get_selected_asset_name() + + def get_selected_task(self): + """Currently selected task.""" + return self._tasks_widget.get_selected_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 = get_openpype_qt_app() + 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/context_dialog/window.py b/openpype/tools/context_dialog/window.py index 4fe41c9949..15b90463da 100644 --- a/openpype/tools/context_dialog/window.py +++ b/openpype/tools/context_dialog/window.py @@ -1,396 +1,12 @@ -import os -import json +from openpype import AYON_SERVER_ENABLED -from qtpy import QtWidgets, QtCore, QtGui +if AYON_SERVER_ENABLED: + from ._ayon_window import ContextDialog, main +else: + from ._openpype_window import ContextDialog, main -from openpype import style -from openpype.pipeline import AvalonMongoDB -from openpype.tools.utils.lib import center_window, get_openpype_qt_app -from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget -from openpype.tools.utils.constants import ( - PROJECT_NAME_ROLE + +__all__ = ( + "ContextDialog", + "main", ) -from openpype.tools.utils.tasks_widget import TasksWidget -from openpype.tools.utils.models import ( - ProjectModel, - ProjectSortFilterProxy -) - - -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 contains 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 = SingleSelectAssetsWidget( - dbcon, 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 - tasks_widget = TasksWidget(dbcon, main_splitter) - - # Add widgets to main splitter - main_splitter.addWidget(left_side_widget) - main_splitter.addWidget(tasks_widget) - - # 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) - tasks_widget.task_changed.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._tasks_widget = tasks_widget - - 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_tasks_widget() - else: - self._assets_widget.setEnabled(True) - self._assets_widget.set_current_asset_btn_visibility(False) - - # Refresh tasks - self._tasks_widget.refresh() - - 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_tasks_widget() - - def _on_task_change(self): - self._validate_strict() - - def _set_asset_to_tasks_widget(self): - asset_id = self._assets_widget.get_selected_asset_id() - - self._tasks_widget.set_asset_id(asset_id) - - 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.""" - return self._assets_widget.get_selected_asset_name() - - def get_selected_task(self): - """Currently selected task.""" - return self._tasks_widget.get_selected_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 = get_openpype_qt_app() - 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)