mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Publisher: Explicit save (#4676)
* added save button class * added save button to window * workfiles is also part of context in CreateContext to be able check if context changed * window cares about trigger of convertors * use abstractproperty with property and abstractmethod decorators * save changes happens using main window and can be blocked * fix pyside compatibility * use create context to get current context names * Fix docstring label * added shortcuts for save and reset * change control string matching for macos * added 'publish_has_started' property * allow save only if publishing did not start yet * rename 'get_selected_convertors' to 'get_selected_legacy_convertors' and added docstrings * Added Saved changes * disable instances toggle when publishing started * Fix reset button tooltip Co-authored-by: Roy Nieterau <roy_nieterau@hotmail.com> * Use QKeySequence to string for tooltips * added example output * use predefined method to emit card message --------- Co-authored-by: Roy Nieterau <roy_nieterau@hotmail.com>
This commit is contained in:
parent
04ecb8afa1
commit
d0f083ec19
11 changed files with 450 additions and 84 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
BIN
openpype/tools/publisher/widgets/images/save.png
Normal file
BIN
openpype/tools/publisher/widgets/images/save.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 │
|
||||
└───────────────────────────────┘
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue