diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index acc2bb054f..22cab28e4b 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -22,7 +22,7 @@ from openpype.lib.attribute_definitions import ( deserialize_attr_defs, get_default_values, ) -from openpype.host import IPublishHost +from openpype.host import IPublishHost, IWorkfileHost from openpype.pipeline import legacy_io from openpype.pipeline.plugin_discover import DiscoverResult @@ -1374,6 +1374,7 @@ class CreateContext: self._current_project_name = None self._current_asset_name = None self._current_task_name = None + self._current_workfile_path = None self._host_is_valid = host_is_valid # Currently unused variable @@ -1503,14 +1504,62 @@ class CreateContext: return os.environ["AVALON_APP"] def get_current_project_name(self): + """Project name which was used as current context on context reset. + + Returns: + Union[str, None]: Project name. + """ + return self._current_project_name def get_current_asset_name(self): + """Asset name which was used as current context on context reset. + + Returns: + Union[str, None]: Asset name. + """ + return self._current_asset_name def get_current_task_name(self): + """Task name which was used as current context on context reset. + + Returns: + Union[str, None]: Task name. + """ + return self._current_task_name + def get_current_workfile_path(self): + """Workfile path which was opened on context reset. + + Returns: + Union[str, None]: Workfile path. + """ + + return self._current_workfile_path + + @property + def context_has_changed(self): + """Host context has changed. + + As context is used project, asset, task name and workfile path if + host does support workfiles. + + Returns: + bool: Context changed. + """ + + project_name, asset_name, task_name, workfile_path = ( + self._get_current_host_context() + ) + return ( + self._current_project_name != project_name + or self._current_asset_name != asset_name + or self._current_task_name != task_name + or self._current_workfile_path != workfile_path + ) + project_name = property(get_current_project_name) @property @@ -1575,6 +1624,28 @@ class CreateContext: self._collection_shared_data = None self.refresh_thumbnails() + def _get_current_host_context(self): + project_name = asset_name = task_name = workfile_path = None + if hasattr(self.host, "get_current_context"): + host_context = self.host.get_current_context() + if host_context: + project_name = host_context.get("project_name") + asset_name = host_context.get("asset_name") + task_name = host_context.get("task_name") + + if isinstance(self.host, IWorkfileHost): + workfile_path = self.host.get_current_workfile() + + # --- TODO remove these conditions --- + if not project_name: + project_name = legacy_io.Session.get("AVALON_PROJECT") + if not asset_name: + asset_name = legacy_io.Session.get("AVALON_ASSET") + if not task_name: + task_name = legacy_io.Session.get("AVALON_TASK") + # --- + return project_name, asset_name, task_name, workfile_path + def reset_current_context(self): """Refresh current context. @@ -1593,24 +1664,14 @@ class CreateContext: are stored. We should store the workfile (if is available) too. """ - project_name = asset_name = task_name = None - if hasattr(self.host, "get_current_context"): - host_context = self.host.get_current_context() - if host_context: - project_name = host_context.get("project_name") - asset_name = host_context.get("asset_name") - task_name = host_context.get("task_name") - - if not project_name: - project_name = legacy_io.Session.get("AVALON_PROJECT") - if not asset_name: - asset_name = legacy_io.Session.get("AVALON_ASSET") - if not task_name: - task_name = legacy_io.Session.get("AVALON_TASK") + project_name, asset_name, task_name, workfile_path = ( + self._get_current_host_context() + ) self._current_project_name = project_name self._current_asset_name = asset_name self._current_task_name = task_name + self._current_workfile_path = workfile_path def reset_plugins(self, discover_publish_plugins=True): """Reload plugins. diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index b2bfd7dd5c..5d23886aa8 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -1,4 +1,4 @@ -from qtpy import QtCore +from qtpy import QtCore, QtGui # ID of context item in instance view CONTEXT_ID = "context" @@ -26,6 +26,9 @@ GROUP_ROLE = QtCore.Qt.UserRole + 7 CONVERTER_IDENTIFIER_ROLE = QtCore.Qt.UserRole + 8 CREATOR_SORT_ROLE = QtCore.Qt.UserRole + 9 +ResetKeySequence = QtGui.QKeySequence( + QtCore.Qt.ControlModifier | QtCore.Qt.Key_R +) __all__ = ( "CONTEXT_ID", diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 49e7eeb4f7..b62ae7ecc1 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -6,7 +6,7 @@ import collections import uuid import tempfile import shutil -from abc import ABCMeta, abstractmethod, abstractproperty +from abc import ABCMeta, abstractmethod import six import pyblish.api @@ -964,7 +964,8 @@ class AbstractPublisherController(object): access objects directly but by using wrappers that can be serialized. """ - @abstractproperty + @property + @abstractmethod def log(self): """Controller's logger object. @@ -974,13 +975,15 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod def event_system(self): """Inner event system for publisher controller.""" pass - @abstractproperty + @property + @abstractmethod def project_name(self): """Current context project name. @@ -990,7 +993,8 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod def current_asset_name(self): """Current context asset name. @@ -1000,7 +1004,8 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod def current_task_name(self): """Current context task name. @@ -1010,7 +1015,21 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod + def host_context_has_changed(self): + """Host context changed after last reset. + + 'CreateContext' has this option available using 'context_has_changed'. + + Returns: + bool: Context has changed. + """ + + pass + + @property + @abstractmethod def host_is_valid(self): """Host is valid for creation part. @@ -1023,7 +1042,8 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod def instances(self): """Collected/created instances. @@ -1134,7 +1154,13 @@ class AbstractPublisherController(object): @abstractmethod def save_changes(self): - """Save changes in create context.""" + """Save changes in create context. + + Save can crash because of unexpected errors. + + Returns: + bool: Save was successful. + """ pass @@ -1145,7 +1171,19 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod + def publish_has_started(self): + """Has publishing finished. + + Returns: + bool: If publishing finished and all plugins were iterated. + """ + + pass + + @property + @abstractmethod def publish_has_finished(self): """Has publishing finished. @@ -1155,7 +1193,8 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod def publish_is_running(self): """Publishing is running right now. @@ -1165,7 +1204,8 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod def publish_has_validated(self): """Publish validation passed. @@ -1175,7 +1215,8 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod def publish_has_crashed(self): """Publishing crashed for any reason. @@ -1185,7 +1226,8 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod def publish_has_validation_errors(self): """During validation happened at least one validation error. @@ -1195,7 +1237,8 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod def publish_max_progress(self): """Get maximum possible progress number. @@ -1205,7 +1248,8 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod def publish_progress(self): """Current progress number. @@ -1215,7 +1259,8 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod def publish_error_msg(self): """Current error message which cause fail of publishing. @@ -1267,7 +1312,8 @@ class AbstractPublisherController(object): pass - @abstractproperty + @property + @abstractmethod def convertor_items(self): pass @@ -1356,6 +1402,7 @@ class BasePublisherController(AbstractPublisherController): self._publish_has_validation_errors = False self._publish_has_crashed = False # All publish plugins are processed + self._publish_has_started = False self._publish_has_finished = False self._publish_max_progress = 0 self._publish_progress = 0 @@ -1386,7 +1433,8 @@ class BasePublisherController(AbstractPublisherController): "show.card.message" - Show card message request (UI related). "instances.refresh.finished" - Instances are refreshed. "plugins.refresh.finished" - Plugins refreshed. - "publish.reset.finished" - Publish context reset finished. + "publish.reset.finished" - Reset finished. + "controller.reset.started" - Controller reset started. "controller.reset.finished" - Controller reset finished. "publish.process.started" - Publishing started. Can be started from paused state. @@ -1425,7 +1473,16 @@ class BasePublisherController(AbstractPublisherController): def _set_host_is_valid(self, value): if self._host_is_valid != value: self._host_is_valid = value - self._emit_event("publish.host_is_valid.changed", {"value": value}) + self._emit_event( + "publish.host_is_valid.changed", {"value": value} + ) + + def _get_publish_has_started(self): + return self._publish_has_started + + def _set_publish_has_started(self, value): + if value != self._publish_has_started: + self._publish_has_started = value def _get_publish_has_finished(self): return self._publish_has_finished @@ -1449,7 +1506,9 @@ class BasePublisherController(AbstractPublisherController): def _set_publish_has_validated(self, value): if self._publish_has_validated != value: self._publish_has_validated = value - self._emit_event("publish.has_validated.changed", {"value": value}) + self._emit_event( + "publish.has_validated.changed", {"value": value} + ) def _get_publish_has_crashed(self): return self._publish_has_crashed @@ -1497,6 +1556,9 @@ class BasePublisherController(AbstractPublisherController): host_is_valid = property( _get_host_is_valid, _set_host_is_valid ) + publish_has_started = property( + _get_publish_has_started, _set_publish_has_started + ) publish_has_finished = property( _get_publish_has_finished, _set_publish_has_finished ) @@ -1526,6 +1588,7 @@ class BasePublisherController(AbstractPublisherController): """Reset most of attributes that can be reset.""" self.publish_is_running = False + self.publish_has_started = False self.publish_has_validated = False self.publish_has_crashed = False self.publish_has_validation_errors = False @@ -1645,10 +1708,7 @@ class PublisherController(BasePublisherController): str: Project name. """ - if not hasattr(self._host, "get_current_context"): - return legacy_io.active_project() - - return self._host.get_current_context()["project_name"] + return self._create_context.get_current_project_name() @property def current_asset_name(self): @@ -1658,10 +1718,7 @@ class PublisherController(BasePublisherController): Union[str, None]: Asset name or None if asset is not set. """ - if not hasattr(self._host, "get_current_context"): - return legacy_io.Session["AVALON_ASSET"] - - return self._host.get_current_context()["asset_name"] + return self._create_context.get_current_asset_name() @property def current_task_name(self): @@ -1671,10 +1728,11 @@ class PublisherController(BasePublisherController): Union[str, None]: Task name or None if task is not set. """ - if not hasattr(self._host, "get_current_context"): - return legacy_io.Session["AVALON_TASK"] + return self._create_context.get_current_task_name() - return self._host.get_current_context()["task_name"] + @property + def host_context_has_changed(self): + return self._create_context.context_has_changed @property def instances(self): @@ -1751,6 +1809,8 @@ class PublisherController(BasePublisherController): """Reset everything related to creation and publishing.""" self.stop_publish() + self._emit_event("controller.reset.started") + self.host_is_valid = self._create_context.host_is_valid self._create_context.reset_preparation() @@ -1992,7 +2052,15 @@ class PublisherController(BasePublisherController): ) def trigger_convertor_items(self, convertor_identifiers): - self.save_changes() + """Trigger legacy item convertors. + + This functionality requires to save and reset CreateContext. The reset + is needed so Creators can collect converted items. + + Args: + convertor_identifiers (list[str]): Identifiers of convertor + plugins. + """ success = True try: @@ -2039,13 +2107,33 @@ class PublisherController(BasePublisherController): self._on_create_instance_change() return success - def save_changes(self): - """Save changes happened during creation.""" + def save_changes(self, show_message=True): + """Save changes happened during creation. + + Trigger save of changes using host api. This functionality does not + validate anything. It is required to do checks before this method is + called to be able to give user actionable response e.g. check of + context using 'host_context_has_changed'. + + Args: + show_message (bool): Show message that changes were + saved successfully. + + Returns: + bool: Save of changes was successful. + """ + if not self._create_context.host_is_valid: - return + # TODO remove + # Fake success save when host is not valid for CreateContext + # this is for testing as experimental feature + return True try: self._create_context.save_changes() + if show_message: + self.emit_card_message("Saved changes..") + return True except CreatorsOperationFailed as exc: self._emit_event( @@ -2056,16 +2144,17 @@ class PublisherController(BasePublisherController): } ) + return False + def remove_instances(self, instance_ids): """Remove instances based on instance ids. Args: instance_ids (List[str]): List of instance ids to remove. """ - # QUESTION Expect that instances are really removed? In that case save - # reset is not required and save changes too. - self.save_changes() + # QUESTION Expect that instances are really removed? In that case reset + # is not required. self._remove_instances_from_context(instance_ids) self._on_create_instance_change() @@ -2136,12 +2225,22 @@ class PublisherController(BasePublisherController): self._publish_comment_is_set = True def publish(self): - """Run publishing.""" + """Run publishing. + + Make sure all changes are saved before method is called (Call + 'save_changes' and check output). + """ + self._publish_up_validation = False self._start_publish() def validate(self): - """Run publishing and stop after Validation.""" + """Run publishing and stop after Validation. + + Make sure all changes are saved before method is called (Call + 'save_changes' and check output). + """ + if self.publish_has_validated: return self._publish_up_validation = True @@ -2152,10 +2251,8 @@ class PublisherController(BasePublisherController): if self.publish_is_running: return - # Make sure changes are saved - self.save_changes() - self.publish_is_running = True + self.publish_has_started = True self._emit_event("publish.process.started") diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index 042985b007..f18e6cc61e 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -4,8 +4,9 @@ from .icons import ( get_icon ) from .widgets import ( - StopBtn, + SaveBtn, ResetBtn, + StopBtn, ValidateBtn, PublishBtn, CreateNextPageOverlay, @@ -25,8 +26,9 @@ __all__ = ( "get_pixmap", "get_icon", - "StopBtn", + "SaveBtn", "ResetBtn", + "StopBtn", "ValidateBtn", "PublishBtn", "CreateNextPageOverlay", diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 3fd5243ce9..0734e1bc27 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -164,6 +164,11 @@ class BaseGroupWidget(QtWidgets.QWidget): def _on_widget_selection(self, instance_id, group_id, selection_type): self.selected.emit(instance_id, group_id, selection_type) + def set_active_toggle_enabled(self, enabled): + for widget in self._widgets_by_id.values(): + if isinstance(widget, InstanceCardWidget): + widget.set_active_toggle_enabled(enabled) + class ConvertorItemsGroupWidget(BaseGroupWidget): def update_items(self, items_by_id): @@ -437,6 +442,9 @@ class InstanceCardWidget(CardWidget): self.update_instance_values() + def set_active_toggle_enabled(self, enabled): + self._active_checkbox.setEnabled(enabled) + def set_active(self, new_value): """Set instance as active.""" checkbox_value = self._active_checkbox.isChecked() @@ -551,6 +559,7 @@ class InstanceCardView(AbstractInstanceView): self._context_widget = None self._convertor_items_group = None + self._active_toggle_enabled = True self._widgets_by_group = {} self._ordered_groups = [] @@ -667,6 +676,9 @@ class InstanceCardView(AbstractInstanceView): group_widget.update_instances( instances_by_group[group_name] ) + group_widget.set_active_toggle_enabled( + self._active_toggle_enabled + ) self._update_ordered_group_names() @@ -1091,3 +1103,10 @@ class InstanceCardView(AbstractInstanceView): self._explicitly_selected_groups = selected_groups self._explicitly_selected_instance_ids = selected_instances + + def set_active_toggle_enabled(self, enabled): + if self._active_toggle_enabled is enabled: + return + self._active_toggle_enabled = enabled + for group_widget in self._widgets_by_group.values(): + group_widget.set_active_toggle_enabled(enabled) diff --git a/openpype/tools/publisher/widgets/images/save.png b/openpype/tools/publisher/widgets/images/save.png new file mode 100644 index 0000000000..0db48d74ae Binary files /dev/null and b/openpype/tools/publisher/widgets/images/save.png differ diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 172563d15c..227ae7bda9 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -198,6 +198,9 @@ class InstanceListItemWidget(QtWidgets.QWidget): self.instance["active"] = new_value self.active_changed.emit(self.instance.id, new_value) + def set_active_toggle_enabled(self, enabled): + self._active_checkbox.setEnabled(enabled) + class ListContextWidget(QtWidgets.QFrame): """Context (or global attributes) widget.""" @@ -302,6 +305,9 @@ class InstanceListGroupWidget(QtWidgets.QFrame): else: self.expand_btn.setArrowType(QtCore.Qt.RightArrow) + def set_active_toggle_enabled(self, enabled): + self.toggle_checkbox.setEnabled(enabled) + class InstanceTreeView(QtWidgets.QTreeView): """View showing instances and their groups.""" @@ -461,6 +467,8 @@ class InstanceListView(AbstractInstanceView): self._instance_model = instance_model self._proxy_model = proxy_model + self._active_toggle_enabled = True + def _on_expand(self, index): self._update_widget_expand_state(index, True) @@ -667,6 +675,9 @@ class InstanceListView(AbstractInstanceView): widget = InstanceListItemWidget( instance, self._instance_view ) + widget.set_active_toggle_enabled( + self._active_toggle_enabled + ) widget.active_changed.connect(self._on_active_changed) self._instance_view.setIndexWidget(proxy_index, widget) self._widgets_by_id[instance.id] = widget @@ -802,6 +813,9 @@ class InstanceListView(AbstractInstanceView): proxy_index = self._proxy_model.mapFromSource(index) group_name = group_item.data(GROUP_ROLE) widget = InstanceListGroupWidget(group_name, self._instance_view) + widget.set_active_toggle_enabled( + self._active_toggle_enabled + ) widget.expand_changed.connect(self._on_group_expand_request) widget.toggle_requested.connect(self._on_group_toggle_request) self._group_widgets[group_name] = widget @@ -1051,3 +1065,16 @@ class InstanceListView(AbstractInstanceView): QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows ) + + def set_active_toggle_enabled(self, enabled): + if self._active_toggle_enabled is enabled: + return + + self._active_toggle_enabled = enabled + for widget in self._widgets_by_id.values(): + if isinstance(widget, InstanceListItemWidget): + widget.set_active_toggle_enabled(enabled) + + for widget in self._group_widgets.values(): + if isinstance(widget, InstanceListGroupWidget): + widget.set_active_toggle_enabled(enabled) diff --git a/openpype/tools/publisher/widgets/overview_widget.py b/openpype/tools/publisher/widgets/overview_widget.py index 8706daeda6..25fff73134 100644 --- a/openpype/tools/publisher/widgets/overview_widget.py +++ b/openpype/tools/publisher/widgets/overview_widget.py @@ -17,6 +17,7 @@ class OverviewWidget(QtWidgets.QFrame): active_changed = QtCore.Signal() instance_context_changed = QtCore.Signal() create_requested = QtCore.Signal() + convert_requested = QtCore.Signal() anim_end_value = 200 anim_duration = 200 @@ -132,6 +133,9 @@ class OverviewWidget(QtWidgets.QFrame): controller.event_system.add_callback( "publish.process.started", self._on_publish_start ) + controller.event_system.add_callback( + "controller.reset.started", self._on_controller_reset_start + ) controller.event_system.add_callback( "publish.reset.finished", self._on_publish_reset ) @@ -336,13 +340,31 @@ class OverviewWidget(QtWidgets.QFrame): self.instance_context_changed.emit() def _on_convert_requested(self): - _, _, convertor_identifiers = self.get_selected_items() - self._controller.trigger_convertor_items(convertor_identifiers) + self.convert_requested.emit() def get_selected_items(self): + """Selected items in current view widget. + + Returns: + tuple[list[str], bool, list[str]]: Selected items. List of + instance ids, context is selected, list of selected legacy + convertor plugins. + """ + view = self._subset_views_layout.currentWidget() return view.get_selected_items() + def get_selected_legacy_convertors(self): + """Selected legacy convertor identifiers. + + Returns: + list[str]: Selected legacy convertor identifiers. + Example: ['io.openpype.creators.houdini.legacy'] + """ + + _, _, convertor_identifiers = self.get_selected_items() + return convertor_identifiers + def _change_view_type(self): idx = self._subset_views_layout.currentIndex() new_idx = (idx + 1) % self._subset_views_layout.count() @@ -391,9 +413,19 @@ class OverviewWidget(QtWidgets.QFrame): self._create_btn.setEnabled(False) self._subset_attributes_wrap.setEnabled(False) + for idx in range(self._subset_views_layout.count()): + widget = self._subset_views_layout.widget(idx) + widget.set_active_toggle_enabled(False) + + def _on_controller_reset_start(self): + """Controller reset started.""" + + for idx in range(self._subset_views_layout.count()): + widget = self._subset_views_layout.widget(idx) + widget.set_active_toggle_enabled(True) def _on_publish_reset(self): - """Context in controller has been refreshed.""" + """Context in controller has been reseted.""" self._create_btn.setEnabled(True) self._subset_attributes_wrap.setEnabled(True) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 86475460aa..d2ce1fbcb2 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -34,7 +34,8 @@ from .icons import ( ) from ..constants import ( - VARIANT_TOOLTIP + VARIANT_TOOLTIP, + ResetKeySequence, ) @@ -198,12 +199,26 @@ class CreateBtn(PublishIconBtn): self.setLayoutDirection(QtCore.Qt.RightToLeft) +class SaveBtn(PublishIconBtn): + """Save context and instances information.""" + def __init__(self, parent=None): + icon_path = get_icon_path("save") + super(SaveBtn, self).__init__(icon_path, parent) + self.setToolTip( + "Save changes ({})".format( + QtGui.QKeySequence(QtGui.QKeySequence.Save).toString() + ) + ) + + class ResetBtn(PublishIconBtn): """Publish reset button.""" def __init__(self, parent=None): icon_path = get_icon_path("refresh") super(ResetBtn, self).__init__(icon_path, parent) - self.setToolTip("Refresh publishing") + self.setToolTip( + "Reset & discard changes ({})".format(ResetKeySequence.toString()) + ) class StopBtn(PublishIconBtn): @@ -348,6 +363,19 @@ class AbstractInstanceView(QtWidgets.QWidget): "{} Method 'set_selected_items' is not implemented." ).format(self.__class__.__name__)) + def set_active_toggle_enabled(self, enabled): + """Instances are disabled for changing enabled state. + + Active state should stay the same until is "unset". + + Args: + enabled (bool): Instance state can be changed. + """ + + raise NotImplementedError(( + "{} Method 'set_active_toggle_enabled' is not implemented." + ).format(self.__class__.__name__)) + class ClickableLineEdit(QtWidgets.QLineEdit): """QLineEdit capturing left mouse click. @@ -1533,7 +1561,7 @@ class SubsetAttributesWidget(QtWidgets.QWidget): │ attributes │ Thumbnail │ TOP │ │ │ ├─────────────┬───┴─────────────┤ - │ Family │ Publish │ + │ Creator │ Publish │ │ attributes │ plugin │ BOTTOM │ │ attributes │ └───────────────────────────────┘ diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 74977d65d8..c72b60d826 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -13,6 +13,7 @@ from openpype.tools.utils import ( PixmapLabel, ) +from .constants import ResetKeySequence from .publish_report_viewer import PublishReportViewerWidget from .control_qt import QtPublisherController from .widgets import ( @@ -22,8 +23,9 @@ from .widgets import ( PublisherTabsWidget, - StopBtn, + SaveBtn, ResetBtn, + StopBtn, ValidateBtn, PublishBtn, @@ -121,6 +123,7 @@ class PublisherWindow(QtWidgets.QDialog): "Attach a comment to your publish" ) + save_btn = SaveBtn(footer_widget) reset_btn = ResetBtn(footer_widget) stop_btn = StopBtn(footer_widget) validate_btn = ValidateBtn(footer_widget) @@ -129,6 +132,7 @@ class PublisherWindow(QtWidgets.QDialog): footer_bottom_layout = QtWidgets.QHBoxLayout(footer_bottom_widget) footer_bottom_layout.setContentsMargins(0, 0, 0, 0) footer_bottom_layout.addStretch(1) + footer_bottom_layout.addWidget(save_btn, 0) footer_bottom_layout.addWidget(reset_btn, 0) footer_bottom_layout.addWidget(stop_btn, 0) footer_bottom_layout.addWidget(validate_btn, 0) @@ -250,7 +254,11 @@ class PublisherWindow(QtWidgets.QDialog): overview_widget.create_requested.connect( self._on_create_request ) + overview_widget.convert_requested.connect( + self._on_convert_requested + ) + save_btn.clicked.connect(self._on_save_clicked) reset_btn.clicked.connect(self._on_reset_clicked) stop_btn.clicked.connect(self._on_stop_clicked) validate_btn.clicked.connect(self._on_validate_clicked) @@ -330,8 +338,9 @@ class PublisherWindow(QtWidgets.QDialog): self._comment_input = comment_input self._footer_spacer = footer_spacer - self._stop_btn = stop_btn + self._save_btn = save_btn self._reset_btn = reset_btn + self._stop_btn = stop_btn self._validate_btn = validate_btn self._publish_btn = publish_btn @@ -388,7 +397,9 @@ class PublisherWindow(QtWidgets.QDialog): def closeEvent(self, event): self._window_is_visible = False self._uninstall_app_event_listener() - self.save_changes() + # TODO capture changes and ask user if wants to save changes on close + if not self._controller.host_context_has_changed: + self._save_changes(False) self._reset_on_show = True self._controller.clear_thumbnail_temp_dir_path() super(PublisherWindow, self).closeEvent(event) @@ -421,6 +432,19 @@ class PublisherWindow(QtWidgets.QDialog): if event.key() == QtCore.Qt.Key_Escape: event.accept() return + + if event.matches(QtGui.QKeySequence.Save): + if not self._controller.publish_has_started: + self._save_changes(True) + event.accept() + return + + if event.matches(ResetKeySequence): + if not self.controller.publish_is_running: + self.reset() + event.accept() + return + super(PublisherWindow, self).keyPressEvent(event) def _on_overlay_message(self, event): @@ -455,8 +479,65 @@ class PublisherWindow(QtWidgets.QDialog): self._reset_on_show = False self.reset() - def save_changes(self): - self._controller.save_changes() + def _checks_before_save(self, explicit_save): + """Save of changes may trigger some issues. + + Check if context did change and ask user if he is really sure the + save should happen. A dialog can be shown during this method. + + Args: + explicit_save (bool): Method was called when user explicitly asked + for save. Value affects shown message. + + Returns: + bool: Save can happen. + """ + + if not self._controller.host_context_has_changed: + return True + + title = "Host context changed" + if explicit_save: + message = ( + "Context has changed since Publisher window was refreshed last" + " time.\n\nAre you sure you want to save changes?" + ) + else: + message = ( + "Your action requires save of changes but context has changed" + " since Publisher window was refreshed last time.\n\nAre you" + " sure you want to continue and save changes?" + ) + + result = QtWidgets.QMessageBox.question( + self, + title, + message, + QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.Cancel + ) + return result == QtWidgets.QMessageBox.Save + + def _save_changes(self, explicit_save): + """Save changes of Creation part. + + All possible triggers of save changes were moved to main window (here), + so it can handle possible issues with save at one place. Do checks, + so user don't accidentally save changes to different file or using + different context. + Moving responsibility to this place gives option to show the dialog and + wait for user's response without breaking action he wanted to do. + + Args: + explicit_save (bool): Method was called when user explicitly asked + for save. Value affects shown message. + + Returns: + bool: Save happened successfully. + """ + + if not self._checks_before_save(explicit_save): + return False + return self._controller.save_changes() def reset(self): self._controller.reset() @@ -491,15 +572,18 @@ class PublisherWindow(QtWidgets.QDialog): self._help_dialog.show() window = self.window() - desktop = QtWidgets.QApplication.desktop() - screen_idx = desktop.screenNumber(window) - screen = desktop.screen(screen_idx) - screen_rect = screen.geometry() + if hasattr(QtWidgets.QApplication, "desktop"): + desktop = QtWidgets.QApplication.desktop() + screen_idx = desktop.screenNumber(window) + screen_geo = desktop.screenGeometry(screen_idx) + else: + screen = window.screen() + screen_geo = screen.geometry() window_geo = window.geometry() dialog_x = window_geo.x() + window_geo.width() dialog_right = (dialog_x + self._help_dialog.width()) - 1 - diff = dialog_right - screen_rect.right() + diff = dialog_right - screen_geo.right() if diff > 0: dialog_x -= diff @@ -549,6 +633,14 @@ class PublisherWindow(QtWidgets.QDialog): def _on_create_request(self): self._go_to_create_tab() + def _on_convert_requested(self): + if not self._save_changes(False): + return + convertor_identifiers = ( + self._overview_widget.get_selected_legacy_convertors() + ) + self._controller.trigger_convertor_items(convertor_identifiers) + def _set_current_tab(self, identifier): self._tabs_widget.set_current_tab(identifier) @@ -599,8 +691,10 @@ class PublisherWindow(QtWidgets.QDialog): self._publish_frame.setVisible(visible) self._update_publish_frame_rect() + def _on_save_clicked(self): + self._save_changes(True) + def _on_reset_clicked(self): - self.save_changes() self.reset() def _on_stop_clicked(self): @@ -610,14 +704,17 @@ class PublisherWindow(QtWidgets.QDialog): self._controller.set_comment(self._comment_input.text()) def _on_validate_clicked(self): - self._set_publish_comment() - self._controller.validate() + if self._save_changes(False): + self._set_publish_comment() + self._controller.validate() def _on_publish_clicked(self): - self._set_publish_comment() - self._controller.publish() + if self._save_changes(False): + self._set_publish_comment() + self._controller.publish() def _set_footer_enabled(self, enabled): + self._save_btn.setEnabled(True) self._reset_btn.setEnabled(True) if enabled: self._stop_btn.setEnabled(False) diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index 3007fa66a5..3ac1b4c4ad 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -247,7 +247,7 @@ class TrayPublishWindow(PublisherWindow): def _on_project_select(self, project_name): # TODO register project specific plugin paths - self._controller.save_changes() + self._controller.save_changes(False) self._controller.reset_project_data_cache() self.reset()