From 8a21439b99e6d43426cad8c47d29631b73e5706c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Jun 2022 16:37:52 +0200 Subject: [PATCH 1/4] aded ability to parse value from clipboard that does not come from settings ui --- openpype/tools/settings/settings/base.py | 96 ++++++++++++++++++------ 1 file changed, 71 insertions(+), 25 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 44ec09b2ca2..64e0a95fee7 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -1,3 +1,4 @@ +import os import sys import json import traceback @@ -190,24 +191,29 @@ def remove_from_project_override(): actions_mapping[action] = remove_from_project_override menu.addAction(action) + def _get_mime_data_from_entity(self): + if self.entity.is_dynamic_item or self.entity.is_in_dynamic_item: + entity_path = None + else: + entity_path = "/".join( + [self.entity.root_key, self.entity.path] + ) + + value = self.entity.value + + # Copy for settings tool + return { + "value": value, + "__root_key__": self.entity.root_key, + "__settings_path__": entity_path + } + def _copy_value_actions(self, menu): def copy_value(): mime_data = QtCore.QMimeData() - if self.entity.is_dynamic_item or self.entity.is_in_dynamic_item: - entity_path = None - else: - entity_path = "/".join( - [self.entity.root_key, self.entity.path] - ) - - value = self.entity.value # Copy for settings tool - settings_data = { - "root_key": self.entity.root_key, - "value": value, - "path": entity_path - } + settings_data = self._get_mime_data_from_entity() settings_encoded_data = QtCore.QByteArray() settings_stream = QtCore.QDataStream( settings_encoded_data, QtCore.QIODevice.WriteOnly @@ -218,6 +224,7 @@ def copy_value(): ) # Copy as json + value = settings_data["value"] json_encoded_data = None if isinstance(value, (dict, list)): json_encoded_data = QtCore.QByteArray() @@ -241,25 +248,64 @@ def copy_value(): action = QtWidgets.QAction("Copy", menu) return [(action, copy_value)] + def _parse_source_data_for_paste(self, data): + settings_path = None + root_key = None + if isinstance(data, dict): + settings_path = data.pop("__settings_path__", settings_path) + root_key = data.pop("__root_key__", root_key) + data = data.pop("__value__", data) + + return { + "value": data, + "__settings_path__": settings_path, + "__root_key__": root_key + } + + def _get_value_from_clipboard(self): + clipboard = QtWidgets.QApplication.clipboard() + mime_data = clipboard.mimeData() + app_value = mime_data.data("application/copy_settings_value") + if app_value: + settings_stream = QtCore.QDataStream( + app_value, QtCore.QIODevice.ReadOnly + ) + mime_data_value_str = settings_stream.readQString() + return json.loads(mime_data_value_str) + + if mime_data.hasUrls(): + for url in mime_data.urls(): + local_file = url.toLocalFile() + try: + with open(local_file, "r") as stream: + value = json.load(stream) + except Exception: + continue + if value: + return self._parse_source_data_for_paste(value) + + if mime_data.hasText(): + text = mime_data.text() + try: + value = json.loads(text) + except Exception: + try: + value = self.entity.convert_to_valid_type(text) + except Exception: + return None + return self._parse_source_data_for_paste(value) + def _paste_value_actions(self, menu): output = [] # Allow paste of value only if were copied from this UI - clipboard = QtWidgets.QApplication.clipboard() - mime_data = clipboard.mimeData() - mime_value = mime_data.data("application/copy_settings_value") + mime_data_value = self._get_value_from_clipboard() # Skip if there is nothing to do - if not mime_value: + if not mime_data_value: return output - settings_stream = QtCore.QDataStream( - mime_value, QtCore.QIODevice.ReadOnly - ) - mime_data_value_str = settings_stream.readQString() - mime_data_value = json.loads(mime_data_value_str) - value = mime_data_value["value"] - path = mime_data_value["path"] - root_key = mime_data_value["root_key"] + path = mime_data_value["__settings_path__"] + root_key = mime_data_value["__root_key__"] # Try to find matching entity to be able paste values to same spot # - entity can't by dynamic or in dynamic item From c77f9ce2e52c6f97cb9b4e8cb86ebcfeb5a11153 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Jun 2022 17:51:55 +0200 Subject: [PATCH 2/4] added option to extract settings to file --- openpype/tools/settings/settings/base.py | 102 +++++++++++++++++++---- 1 file changed, 86 insertions(+), 16 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 64e0a95fee7..3ade5e5ef85 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -3,6 +3,7 @@ import json import traceback import functools +import datetime from Qt import QtWidgets, QtGui, QtCore @@ -13,10 +14,26 @@ from .lib import create_deffered_value_change_timer from .constants import DEFAULT_PROJECT_LABEL +_MENU_SEPARATOR_REQ = object() +SETTINGS_PATH_KEY = "__settings_path__" +ROOT_KEY = "__root_key__" +VALUE_KEY = "__value__" +SAVE_TIME_KEY = "__extracted__" +PROJECT_NAME_KEY = "__project_name__" + class BaseWidget(QtWidgets.QWidget): + _last_save_dir = os.path.expanduser("~") allow_actions = True + @staticmethod + def get_last_save_dir(): + return BaseWidget._last_save_dir + + @staticmethod + def set_last_save_dir(save_dir): + BaseWidget._last_save_dir = save_dir + def __init__(self, category_widget, entity, entity_widget): self.category_widget = category_widget self.entity = entity @@ -203,9 +220,9 @@ def _get_mime_data_from_entity(self): # Copy for settings tool return { - "value": value, - "__root_key__": self.entity.root_key, - "__settings_path__": entity_path + VALUE_KEY: value, + ROOT_KEY: self.entity.root_key, + SETTINGS_PATH_KEY: entity_path } def _copy_value_actions(self, menu): @@ -248,18 +265,62 @@ def copy_value(): action = QtWidgets.QAction("Copy", menu) return [(action, copy_value)] + def _extract_to_file(self): + dialog = QtWidgets.QFileDialog( + self, + "Save settings values", + self.get_last_save_dir(), + "Values (*.json)" + ) + # dialog.setOption(dialog.DontUseNativeDialog) + dialog.setAcceptMode(dialog.AcceptSave) + if dialog.exec() != dialog.Accepted: + return + + selected_urls = dialog.selectedUrls() + if not selected_urls: + return + + filepath = selected_urls[0].toLocalFile() + if not filepath: + return + + if not filepath.lower().endswith(".json"): + filepath += ".json" + + settings_data = self._get_mime_data_from_entity() + now = datetime.datetime.now() + settings_data[SAVE_TIME_KEY] = now.strftime("%Y-%m-%d %H:%M:%S") + if hasattr(self.category_widget, "project_name"): + settings_data[PROJECT_NAME_KEY] = self.category_widget.project_name + + with open(filepath, "w") as stream: + json.dump(settings_data, stream, indent=4) + + new_dir = os.path.dirname(filepath) + self.set_last_save_dir(new_dir) + + def _extract_value_to_file_actions(self, menu): + save_action = QtWidgets.QAction("Extract to file", menu) + return [ + _MENU_SEPARATOR_REQ, + (save_action, self._extract_to_file) + ] + def _parse_source_data_for_paste(self, data): settings_path = None root_key = None if isinstance(data, dict): - settings_path = data.pop("__settings_path__", settings_path) - root_key = data.pop("__root_key__", root_key) - data = data.pop("__value__", data) + data.pop(SAVE_TIME_KEY, None) + data.pop(PROJECT_NAME_KEY, None) + settings_path = data.pop(SETTINGS_PATH_KEY, settings_path) + root_key = data.pop(ROOT_KEY, root_key) + data = data.pop(VALUE_KEY, data) return { - "value": data, - "__settings_path__": settings_path, - "__root_key__": root_key + VALUE_KEY: data, + SETTINGS_PATH_KEY: settings_path, + ROOT_KEY: root_key } def _get_value_from_clipboard(self): @@ -303,9 +364,9 @@ def _paste_value_actions(self, menu): if not mime_data_value: return output - value = mime_data_value["value"] - path = mime_data_value["__settings_path__"] - root_key = mime_data_value["__root_key__"] + value = mime_data_value[VALUE_KEY] + path = mime_data_value[SETTINGS_PATH_KEY] + root_key = mime_data_value[ROOT_KEY] # Try to find matching entity to be able paste values to same spot # - entity can't by dynamic or in dynamic item @@ -437,10 +498,19 @@ def show_actions_menu(self, event=None): ui_actions.extend(self._copy_value_actions(menu)) ui_actions.extend(self._paste_value_actions(menu)) if ui_actions: - menu.addSeparator() - for action, callback in ui_actions: - menu.addAction(action) - actions_mapping[action] = callback + ui_actions.insert(0, _MENU_SEPARATOR_REQ) + + ui_actions.extend(self._extract_value_to_file_actions(menu)) + + for item in ui_actions: + if item is _MENU_SEPARATOR_REQ: + if len(menu.actions()) > 0: + menu.addSeparator() + continue + + action, callback = item + menu.addAction(action) + actions_mapping[action] = callback if not actions_mapping: action = QtWidgets.QAction("< No action >") From e21caf109fc3405afb36e86ab7f9ff9dbb8b0726 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Jun 2022 18:26:05 +0200 Subject: [PATCH 3/4] extraction can be called on whole settings --- openpype/tools/settings/settings/base.py | 108 +++++++++++------- .../tools/settings/settings/categories.py | 35 +++++- openpype/tools/settings/settings/constants.py | 13 +++ 3 files changed, 111 insertions(+), 45 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 3ade5e5ef85..aa2b448ccf3 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -12,27 +12,70 @@ from .widgets import ExpandingWidget from .lib import create_deffered_value_change_timer -from .constants import DEFAULT_PROJECT_LABEL +from .constants import ( + DEFAULT_PROJECT_LABEL, + SETTINGS_PATH_KEY, + ROOT_KEY, + VALUE_KEY, + SAVE_TIME_KEY, + PROJECT_NAME_KEY, +) _MENU_SEPARATOR_REQ = object() -SETTINGS_PATH_KEY = "__settings_path__" -ROOT_KEY = "__root_key__" -VALUE_KEY = "__value__" -SAVE_TIME_KEY = "__extracted__" -PROJECT_NAME_KEY = "__project_name__" -class BaseWidget(QtWidgets.QWidget): +class ExtractHelper: _last_save_dir = os.path.expanduser("~") - allow_actions = True - @staticmethod - def get_last_save_dir(): - return BaseWidget._last_save_dir + @classmethod + def get_last_save_dir(cls): + return cls._last_save_dir - @staticmethod - def set_last_save_dir(save_dir): - BaseWidget._last_save_dir = save_dir + @classmethod + def set_last_save_dir(cls, save_dir): + cls._last_save_dir = save_dir + + @classmethod + def ask_for_save_filepath(cls, parent): + dialog = QtWidgets.QFileDialog( + parent, + "Save settings values", + cls.get_last_save_dir(), + "Values (*.json)" + ) + # dialog.setOption(dialog.DontUseNativeDialog) + dialog.setAcceptMode(dialog.AcceptSave) + if dialog.exec() != dialog.Accepted: + return + + selected_urls = dialog.selectedUrls() + if not selected_urls: + return + + filepath = selected_urls[0].toLocalFile() + if not filepath: + return + + if not filepath.lower().endswith(".json"): + filepath += ".json" + return filepath + + @classmethod + def extract_settings_to_json(cls, filepath, settings_data, project_name): + now = datetime.datetime.now() + settings_data[SAVE_TIME_KEY] = now.strftime("%Y-%m-%d %H:%M:%S") + if project_name != 0: + settings_data[PROJECT_NAME_KEY] = project_name + + with open(filepath, "w") as stream: + json.dump(settings_data, stream, indent=4) + + new_dir = os.path.dirname(filepath) + cls.set_last_save_dir(new_dir) + + +class BaseWidget(QtWidgets.QWidget): + allow_actions = True def __init__(self, category_widget, entity, entity_widget): self.category_widget = category_widget @@ -266,45 +309,24 @@ def copy_value(): return [(action, copy_value)] def _extract_to_file(self): - dialog = QtWidgets.QFileDialog( - self, - "Save settings values", - self.get_last_save_dir(), - "Values (*.json)" - ) - # dialog.setOption(dialog.DontUseNativeDialog) - dialog.setAcceptMode(dialog.AcceptSave) - if dialog.exec() != dialog.Accepted: - return - - selected_urls = dialog.selectedUrls() - if not selected_urls: - return - - filepath = selected_urls[0].toLocalFile() + filepath = ExtractHelper.ask_for_save_filepath(self) if not filepath: return - if not filepath.lower().endswith(".json"): - filepath += ".json" - settings_data = self._get_mime_data_from_entity() - now = datetime.datetime.now() - settings_data[SAVE_TIME_KEY] = now.strftime("%Y-%m-%d %H:%M:%S") + project_name = 0 if hasattr(self.category_widget, "project_name"): - settings_data[PROJECT_NAME_KEY] = self.category_widget.project_name + project_name = self.category_widget.project_name - with open(filepath, "w") as stream: - json.dump(settings_data, stream, indent=4) - - new_dir = os.path.dirname(filepath) - self.set_last_save_dir(new_dir) + ExtractHelper.extract_settings_to_json( + filepath, settings_data, project_name + ) def _extract_value_to_file_actions(self, menu): - save_action = QtWidgets.QAction("Extract to file", menu) + extract_action = QtWidgets.QAction("Extract to file", menu) return [ _MENU_SEPARATOR_REQ, - (save_action, self._extract_to_file) + (extract_action, self._extract_to_file) ] def _parse_source_data_for_paste(self, data): diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index c8ade5fcdbc..764f42f1a35 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -45,8 +45,15 @@ SystemSettingsBreadcrumbs, ProjectSettingsBreadcrumbs ) - -from .base import GUIWidget +from .constants import ( + SETTINGS_PATH_KEY, + ROOT_KEY, + VALUE_KEY, +) +from .base import ( + ExtractHelper, + GUIWidget, +) from .list_item_widget import ListWidget from .list_strict_widget import ListStrictWidget from .dict_mutable_widget import DictMutableKeysWidget @@ -627,11 +634,35 @@ def add_context_actions(self, menu): self._on_context_version_trigger ) submenu.addAction(action) + menu.addMenu(submenu) + extract_action = QtWidgets.QAction("Extract to file", menu) + extract_action.triggered.connect(self._on_extract_to_file) + + menu.addAction(extract_action) + def _on_context_version_trigger(self, version): self._on_source_version_change(version) + def _on_extract_to_file(self): + filepath = ExtractHelper.ask_for_save_filepath(self) + if not filepath: + return + + settings_data = { + SETTINGS_PATH_KEY: self.entity.root_key, + ROOT_KEY: self.entity.root_key, + VALUE_KEY: self.entity.value + } + project_name = 0 + if hasattr(self, "project_name"): + project_name = self.project_name + + ExtractHelper.extract_settings_to_json( + filepath, settings_data, project_name + ) + def _on_reset_crash(self): self.save_btn.setEnabled(False) diff --git a/openpype/tools/settings/settings/constants.py b/openpype/tools/settings/settings/constants.py index 9d6d7904d79..d98d18c8bf5 100644 --- a/openpype/tools/settings/settings/constants.py +++ b/openpype/tools/settings/settings/constants.py @@ -7,6 +7,12 @@ PROJECT_IS_SELECTED_ROLE = QtCore.Qt.UserRole + 3 PROJECT_VERSION_ROLE = QtCore.Qt.UserRole + 4 +# Save/Extract keys +SETTINGS_PATH_KEY = "__settings_path__" +ROOT_KEY = "__root_key__" +VALUE_KEY = "__value__" +SAVE_TIME_KEY = "__extracted__" +PROJECT_NAME_KEY = "__project_name__" __all__ = ( "DEFAULT_PROJECT_LABEL", @@ -15,4 +21,11 @@ "PROJECT_IS_ACTIVE_ROLE", "PROJECT_IS_SELECTED_ROLE", "PROJECT_VERSION_ROLE", + + "SETTINGS_PATH_KEY", + "ROOT_KEY", + "SETTINGS_PATH_KEY", + "VALUE_KEY", + "SAVE_TIME_KEY", + "PROJECT_NAME_KEY", ) From d2de1539136ad7d4da5902d78aed56dd204f4a50 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Jun 2022 18:47:42 +0200 Subject: [PATCH 4/4] fix copy value to string --- openpype/tools/settings/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index aa2b448ccf3..6def284a831 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -284,7 +284,7 @@ def copy_value(): ) # Copy as json - value = settings_data["value"] + value = settings_data[VALUE_KEY] json_encoded_data = None if isinstance(value, (dict, list)): json_encoded_data = QtCore.QByteArray()