diff --git a/pype/api.py b/pype/api.py index 71c31efa93..44caa7632f 100644 --- a/pype/api.py +++ b/pype/api.py @@ -22,7 +22,9 @@ from .lib import ( get_app_environments_for_context, source_hash, get_latest_version, - get_global_environments + get_global_environments, + get_local_site_id, + change_pype_mongo_url ) from .lib.mongo import ( @@ -109,5 +111,8 @@ __all__ = [ "run_subprocess", "get_latest_version", - "get_global_environments" + "get_global_environments", + + "get_local_site_id", + "change_pype_mongo_url" ] diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 4b7e59ab3d..67a31d1737 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -23,6 +23,7 @@ from .mongo import ( decompose_url, compose_url, get_default_components, + validate_mongo_connection, PypeMongoConnection ) from .anatomy import ( @@ -91,10 +92,12 @@ from .plugin_tools import ( should_decompress ) -from .user_settings import ( +from .local_settings import ( IniSettingRegistry, JSONSettingRegistry, - PypeSettingsRegistry + PypeSettingsRegistry, + get_local_site_id, + change_pype_mongo_url ) from .path_tools import ( @@ -191,11 +194,15 @@ __all__ = [ "decompose_url", "compose_url", "get_default_components", + "validate_mongo_connection", "PypeMongoConnection", "IniSettingRegistry", "JSONSettingRegistry", "PypeSettingsRegistry", + "get_local_site_id", + "change_pype_mongo_url", + "timeit", "is_overlapping_otio_ranges", diff --git a/pype/lib/user_settings.py b/pype/lib/local_settings.py similarity index 92% rename from pype/lib/user_settings.py rename to pype/lib/local_settings.py index 00ce68cb0b..aa372a52d2 100644 --- a/pype/lib/user_settings.py +++ b/pype/lib/local_settings.py @@ -28,6 +28,8 @@ import platform import appdirs import six +from .import validate_mongo_connection + @six.add_metaclass(ABCMeta) class ASettingRegistry(): @@ -118,7 +120,7 @@ class ASettingRegistry(): """Delete item from settings. Note: - see :meth:`pype.lib.user_settings.ARegistrySettings.delete_item` + see :meth:`pype.lib.local_settings.ARegistrySettings.delete_item` """ pass @@ -464,3 +466,43 @@ class PypeSettingsRegistry(JSONSettingRegistry): self.product = "pype" path = appdirs.user_data_dir(self.product, self.vendor) super(PypeSettingsRegistry, self).__init__("pype_settings", path) + + +def _create_local_site_id(registry=None): + """Create a local site identifier.""" + from uuid import uuid4 + + if registry is None: + registry = PypeSettingsRegistry() + + new_id = str(uuid4()) + + print("Created local site id \"{}\"".format(new_id)) + + registry.set_item("localId", new_id) + + return new_id + + +def get_local_site_id(): + """Get local site identifier. + + Identifier is created if does not exists yet. + """ + registry = PypeSettingsRegistry() + try: + return registry.get_item("localId") + except ValueError: + return _create_local_site_id() + + +def change_pype_mongo_url(new_mongo_url): + """Change mongo url in pype registry. + + Change of Pype mongo URL require restart of running pype processes or + processes using pype. + """ + + validate_mongo_connection(new_mongo_url) + registry = PypeSettingsRegistry() + registry.set_secure_item("pypeMongo", new_mongo_url) diff --git a/pype/lib/mongo.py b/pype/lib/mongo.py index f82c8b5e23..04798d88ff 100644 --- a/pype/lib/mongo.py +++ b/pype/lib/mongo.py @@ -93,6 +93,42 @@ def extract_port_from_url(url): return parsed_url.port +def validate_mongo_connection(mongo_uri): + """Check if provided mongodb URL is valid. + + Args: + mongo_uri (str): URL to validate. + + Raises: + ValueError: When port in mongo uri is not valid. + pymongo.errors.InvalidURI: If passed mongo is invalid. + pymongo.errors.ServerSelectionTimeoutError: If connection timeout + passed so probably couldn't connect to mongo server. + + """ + parsed = urlparse(mongo_uri) + # Force validation of scheme + if parsed.scheme not in ["mongodb", "mongodb+srv"]: + raise pymongo.errors.InvalidURI(( + "Invalid URI scheme:" + " URI must begin with 'mongodb://' or 'mongodb+srv://'" + )) + # we have mongo connection string. Let's try if we can connect. + components = decompose_url(mongo_uri) + mongo_args = { + "host": compose_url(**components), + "serverSelectionTimeoutMS": 1000 + } + port = components.get("port") + if port is not None: + mongo_args["port"] = int(port) + + # Create connection + client = pymongo.MongoClient(**mongo_args) + client.server_info() + client.close() + + class PypeMongoConnection: """Singleton MongoDB connection. diff --git a/pype/modules/__init__.py b/pype/modules/__init__.py index 93fc92f9d5..e0481d0c92 100644 --- a/pype/modules/__init__.py +++ b/pype/modules/__init__.py @@ -9,7 +9,10 @@ from .base import ( ModulesManager, TrayModulesManager ) -from .settings_action import SettingsAction +from .settings_action import ( + SettingsAction, + LocalSettingsAction +) from .rest_api import ( RestApiModule, IRestApi @@ -52,6 +55,7 @@ __all__ = ( "TrayModulesManager", "SettingsAction", + "LocalSettingsAction", "UserModule", "IUserModule", diff --git a/pype/modules/base.py b/pype/modules/base.py index ad0fecb8f7..7efd00e39e 100644 --- a/pype/modules/base.py +++ b/pype/modules/base.py @@ -627,6 +627,7 @@ class TrayModulesManager(ModulesManager): "clockify", "standalonepublish_tool", "log_viewer", + "local_settings", "settings" ) diff --git a/pype/modules/settings_action.py b/pype/modules/settings_action.py index c1fa8a68bc..aab10e9ebf 100644 --- a/pype/modules/settings_action.py +++ b/pype/modules/settings_action.py @@ -58,3 +58,58 @@ class SettingsAction(PypeModule, ITrayAction): # Reset content if was not visible if not was_visible: self.settings_window.reset() + + +class LocalSettingsAction(PypeModule, ITrayAction): + """Action to show Setttings tool.""" + name = "local_settings" + label = "Local Settings" + + def initialize(self, _modules_settings): + # This action is always enabled + self.enabled = True + + # Tray attributes + self.settings_window = None + + def connect_with_modules(self, *_a, **_kw): + return + + def tray_init(self): + """Initialization in tray implementation of ITrayAction.""" + self.create_settings_window() + + def on_action_trigger(self): + """Implementation for action trigger of ITrayAction.""" + self.show_settings_window() + + def create_settings_window(self): + """Initializa Settings Qt window.""" + if self.settings_window: + return + from pype.tools.settings import LocalSettingsWindow + self.settings_window = LocalSettingsWindow() + + def show_settings_window(self): + """Show settings tool window. + + Raises: + AssertionError: Window must be already created. Call + `create_settings_window` before callint this method. + """ + if not self.settings_window: + raise AssertionError("Window is not initialized.") + + # Store if was visible + was_visible = self.settings_window.isVisible() + + # Show settings gui + self.settings_window.show() + + # Pull window to the front. + self.settings_window.raise_() + self.settings_window.activateWindow() + + # Reset content if was not visible + if not was_visible: + self.settings_window.reset() diff --git a/pype/settings/constants.py b/pype/settings/constants.py index c68826f45b..ce19ad3f93 100644 --- a/pype/settings/constants.py +++ b/pype/settings/constants.py @@ -15,7 +15,9 @@ METADATA_KEYS = ( SYSTEM_SETTINGS_KEY = "system_settings" PROJECT_SETTINGS_KEY = "project_settings" PROJECT_ANATOMY_KEY = "project_anatomy" +LOCAL_SETTING_KEY = "local_settings" +DEFAULT_PROJECT_KEY = "__default_project__" __all__ = ( "M_OVERRIDEN_KEY", @@ -26,5 +28,6 @@ __all__ = ( "SYSTEM_SETTINGS_KEY", "PROJECT_SETTINGS_KEY", - "PROJECT_ANATOMY_KEY" + "PROJECT_ANATOMY_KEY", + "LOCAL_SETTING_KEY" ) diff --git a/pype/settings/handlers.py b/pype/settings/handlers.py index 0df4c98820..89f9645be7 100644 --- a/pype/settings/handlers.py +++ b/pype/settings/handlers.py @@ -10,7 +10,8 @@ import pype from .constants import ( SYSTEM_SETTINGS_KEY, PROJECT_SETTINGS_KEY, - PROJECT_ANATOMY_KEY + PROJECT_ANATOMY_KEY, + LOCAL_SETTING_KEY ) from .lib import load_json_file @@ -103,6 +104,28 @@ class SettingsHandler: pass +@six.add_metaclass(ABCMeta) +class LocalSettingsHandler: + """Handler that should handle about storing and loading of local settings. + + Local settings are "workstation" specific modifications that modify how + system and project settings look on the workstation and only there. + """ + @abstractmethod + def save_local_settings(self, data): + """Save local data of local settings. + + Args: + data(dict): Data of local data with override metadata. + """ + pass + + @abstractmethod + def get_local_settings(self): + """Studio overrides of system settings.""" + pass + + class SettingsFileHandler(SettingsHandler): def __init__(self): self.log = logging.getLogger("SettingsFileHandler") @@ -495,3 +518,76 @@ class MongoSettingsHandler(SettingsHandler): if not project_name: return {} return self._get_project_anatomy_overrides(project_name) + + +class MongoLocalSettingsHandler(LocalSettingsHandler): + """Settings handler that use mongo for store and load local settings. + + Data have 2 query criteria. First is key "type" stored in constant + `LOCAL_SETTING_KEY`. Second is key "site_id" which value can be obstained + with `get_local_site_id` function. + """ + + def __init__(self, local_site_id=None): + # Get mongo connection + from pype.lib import ( + PypeMongoConnection, + get_local_site_id + ) + + if local_site_id is None: + local_site_id = get_local_site_id() + settings_collection = PypeMongoConnection.get_mongo_client() + + # TODO prepare version of pype + # - pype version should define how are settings saved and loaded + + # TODO modify to not use hardcoded keys + database_name = "pype" + collection_name = "settings" + + self.settings_collection = settings_collection + + self.database_name = database_name + self.collection_name = collection_name + + self.collection = settings_collection[database_name][collection_name] + + self.local_site_id = local_site_id + + self.local_settings_cache = CacheValues() + + def save_local_settings(self, data): + """Save local settings. + + Args: + data(dict): Data of studio overrides with override metadata. + """ + data = data or {} + + self.local_settings_cache.update_data(data) + + self.collection.replace_one( + { + "type": LOCAL_SETTING_KEY, + "site_id": self.local_site_id + }, + { + "type": LOCAL_SETTING_KEY, + "site_id": self.local_site_id, + "value": self.local_settings_cache.to_json_string() + }, + upsert=True + ) + + def get_local_settings(self): + """Local settings for local site id.""" + if self.local_settings_cache.is_outdated: + document = self.collection.find_one({ + "type": LOCAL_SETTING_KEY, + "site_id": self.local_site_id + }) + + self.local_settings_cache.update_from_document(document) + + return self.local_settings_cache.data_copy() diff --git a/pype/settings/lib.py b/pype/settings/lib.py index dfc46e1a5a..feeeaf3813 100644 --- a/pype/settings/lib.py +++ b/pype/settings/lib.py @@ -2,6 +2,7 @@ import os import json import functools import logging +import platform import copy from .constants import ( M_OVERRIDEN_KEY, @@ -11,7 +12,8 @@ from .constants import ( SYSTEM_SETTINGS_KEY, PROJECT_SETTINGS_KEY, - PROJECT_ANATOMY_KEY + PROJECT_ANATOMY_KEY, + DEFAULT_PROJECT_KEY ) log = logging.getLogger(__name__) @@ -32,6 +34,9 @@ _DEFAULT_SETTINGS = None # Handler of studio overrides _SETTINGS_HANDLER = None +# Handler of local settings +_LOCAL_SETTINGS_HANDLER = None + def require_handler(func): @functools.wraps(func) @@ -43,6 +48,16 @@ def require_handler(func): return wrapper +def require_local_handler(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + global _LOCAL_SETTINGS_HANDLER + if _LOCAL_SETTINGS_HANDLER is None: + _LOCAL_SETTINGS_HANDLER = create_local_settings_handler() + return func(*args, **kwargs) + return wrapper + + def create_settings_handler(): from .handlers import MongoSettingsHandler # Handler can't be created in global space on initialization but only when @@ -50,6 +65,11 @@ def create_settings_handler(): return MongoSettingsHandler() +def create_local_settings_handler(): + from .handlers import MongoLocalSettingsHandler + return MongoLocalSettingsHandler() + + @require_handler def save_studio_settings(data): return _SETTINGS_HANDLER.save_studio_settings(data) @@ -90,6 +110,16 @@ def get_project_anatomy_overrides(project_name): return _SETTINGS_HANDLER.get_project_anatomy_overrides(project_name) +@require_local_handler +def save_local_settings(data): + return _LOCAL_SETTINGS_HANDLER.save_local_settings(data) + + +@require_local_handler +def get_local_settings(): + return _LOCAL_SETTINGS_HANDLER.get_local_settings() + + class DuplicatedEnvGroups(Exception): def __init__(self, duplicated): self.origin_duplicated = duplicated @@ -309,6 +339,109 @@ def apply_overrides(source_data, override_data): return merge_overrides(_source_data, override_data) +def apply_local_settings_on_system_settings(system_settings, local_settings): + """Apply local settings on studio system settings. + + ATM local settings can modify only application executables. Executable + values are not overriden but prepended. + """ + if not local_settings or "applications" not in local_settings: + return + + current_platform = platform.system().lower() + for app_group_name, value in local_settings["applications"].items(): + if not value or app_group_name not in system_settings["applications"]: + continue + + variants = system_settings["applications"][app_group_name]["variants"] + for app_name, app_value in value.items(): + if not app_value or app_name not in variants: + continue + + executable = app_value.get("executable") + if not executable: + continue + platform_executables = variants[app_name]["executables"].get( + current_platform + ) + # TODO This is temporary fix until launch arguments will be stored + # per platform and not per executable. + # - local settings store only executable + new_executables = [[executable, ""]] + new_executables.extend(platform_executables) + variants[app_name]["executables"] = new_executables + + +def apply_local_settings_on_anatomy_settings( + anatomy_settings, local_settings, project_name +): + """Apply local settings on anatomy settings. + + ATM local settings can modify project roots. Project name is required as + local settings have data stored data by project's name. + + Local settings override root values in this order: + 1.) Check if local settings contain overrides for default project and + apply it's values on roots if there are any. + 2.) If passed `project_name` is not None then check project specific + overrides in local settings for the project and apply it's value on + roots if there are any. + + NOTE: Root values of default project from local settings are always applied + if are set. + + Args: + anatomy_settings (dict): Data for anatomy settings. + local_settings (dict): Data of local settings. + project_name (str): Name of project for which anatomy data are. + """ + if not local_settings: + return + + local_project_settings = local_settings.get("projects") + if not local_project_settings: + return + + project_locals = local_project_settings.get(project_name) or {} + default_locals = local_project_settings.get(DEFAULT_PROJECT_KEY) or {} + active_site = project_locals.get("active_site") + if not active_site: + active_site = default_locals.get("active_site") + + if not active_site: + project_settings = get_project_settings(project_name) + active_site = ( + project_settings + ["global"] + ["sync_server"] + ["config"] + ["active_site"] + ) + + # QUESTION should raise an exception? + if not active_site: + return + + roots_locals = default_locals.get("roots", {}).get(active_site, {}) + if project_name != DEFAULT_PROJECT_KEY: + roots_locals.update( + project_locals.get("roots", {}).get(active_site, {}) + ) + + if not roots_locals: + return + + current_platform = platform.system().lower() + + root_data = anatomy_settings["roots"] + for root_name, path in roots_locals.items(): + if root_name not in root_data: + continue + anatomy_settings["roots"][root_name][current_platform] = ( + path + ) + + def get_system_settings(clear_metadata=True): """System settings with applied studio overrides.""" default_values = get_default_settings()[SYSTEM_SETTINGS_KEY] @@ -316,6 +449,10 @@ def get_system_settings(clear_metadata=True): result = apply_overrides(default_values, studio_values) if clear_metadata: clear_metadata_from_settings(result) + # TODO local settings may be required to apply for environments + local_settings = get_local_settings() + apply_local_settings_on_system_settings(result, local_settings) + return result @@ -343,6 +480,8 @@ def get_default_anatomy_settings(clear_metadata=True): result[key] = value if clear_metadata: clear_metadata_from_settings(result) + local_settings = get_local_settings() + apply_local_settings_on_anatomy_settings(result, local_settings, None) return result @@ -368,6 +507,10 @@ def get_anatomy_settings(project_name, clear_metadata=True): result[key] = value if clear_metadata: clear_metadata_from_settings(result) + local_settings = get_local_settings() + apply_local_settings_on_anatomy_settings( + result, local_settings, project_name + ) return result diff --git a/pype/settings/local_settings.md b/pype/settings/local_settings.md new file mode 100644 index 0000000000..fbb5cf3df1 --- /dev/null +++ b/pype/settings/local_settings.md @@ -0,0 +1,79 @@ +# Structure of local settings +- local settings do not have any validation schemas right now this should help to see what is stored to local settings and how it works +- they are stored by identifier site_id which should be unified identifier of workstation +- all keys may and may not available on load +- contain main categories: `general`, `applications`, `projects` + +## Categories +### General +- ATM contain only label of site +```json +{ + "general": { + "site_label": "MySite" + } +} +``` + +### Applications +- modifications of application executables +- output should match application groups and variants +```json +{ + "applications": { + "": { + "": { + "executable": "/my/path/to/nuke_12_2" + } + } + } +} +``` + +### Projects +- project specific modifications +- default project is stored under constant key defined in `pype.settings.contants` +```json +{ + "projects": { + "": { + "active_site": "", + "remote_site": "", + "roots": { + "": { + "": "" + } + } + } + } +} +``` + +## Final document +```json +{ + "_id": "", + "site_id": "", + "general": { + "site_label": "MySite" + }, + "applications": { + "": { + "": { + "executable": "" + } + } + }, + "projects": { + "": { + "active_site": "", + "remote_site": "", + "roots": { + "": { + "": "" + } + } + } + } +} +``` diff --git a/pype/tools/settings/__init__.py b/pype/tools/settings/__init__.py index 7bc54f9ab6..3f47d1c2c3 100644 --- a/pype/tools/settings/__init__.py +++ b/pype/tools/settings/__init__.py @@ -1,6 +1,6 @@ import sys from Qt import QtWidgets, QtGui - +from .local_settings import LocalSettingsWindow from .settings import ( style, MainWidget, @@ -33,5 +33,6 @@ __all__ = ( "style", "MainWidget", "ProjectListWidget", + "LocalSettingsWindow", "main" ) diff --git a/pype/tools/settings/local_settings/__init__.py b/pype/tools/settings/local_settings/__init__.py new file mode 100644 index 0000000000..135a719a09 --- /dev/null +++ b/pype/tools/settings/local_settings/__init__.py @@ -0,0 +1,6 @@ +from .window import LocalSettingsWindow + + +__all__ = ( + "LocalSettingsWindow", +) diff --git a/pype/tools/settings/local_settings/apps_widget.py b/pype/tools/settings/local_settings/apps_widget.py new file mode 100644 index 0000000000..d63cd6a834 --- /dev/null +++ b/pype/tools/settings/local_settings/apps_widget.py @@ -0,0 +1,205 @@ +import platform +from Qt import QtWidgets +from .widgets import ( + Separator, + ExpandingWidget +) +from .constants import CHILD_OFFSET + + +class AppVariantWidget(QtWidgets.QWidget): + exec_placeholder = "< Specific path for this machine >" + + def __init__(self, group_label, variant_entity, parent): + super(AppVariantWidget, self).__init__(parent) + + self.executable_input_widget = None + + label = " ".join([group_label, variant_entity.label]) + + expading_widget = ExpandingWidget(label, self) + content_widget = QtWidgets.QWidget(expading_widget) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) + + expading_widget.set_content_widget(content_widget) + + # Add expanding widget to main layout + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(expading_widget) + + # TODO For celaction - not sure what is "Celaction publish" for + if not variant_entity["executables"].multiplatform: + warn_label = QtWidgets.QLabel( + "Application without multiplatform paths" + ) + content_layout.addWidget(warn_label) + return + + executable_input_widget = QtWidgets.QLineEdit(content_widget) + executable_input_widget.setPlaceholderText(self.exec_placeholder) + content_layout.addWidget(executable_input_widget) + + self.executable_input_widget = executable_input_widget + + studio_executables = ( + variant_entity["executables"][platform.system().lower()] + ) + if len(studio_executables) < 1: + return + + content_layout.addWidget(Separator(parent=self)) + content_layout.addWidget( + QtWidgets.QLabel("Studio paths:", self) + ) + + for item in studio_executables: + path_widget = QtWidgets.QLineEdit(content_widget) + path_widget.setText(item.value[0]) + path_widget.setEnabled(False) + content_layout.addWidget(path_widget) + + def update_local_settings(self, value): + if not self.executable_input_widget: + return + + if not value: + value = {} + elif not isinstance(value, dict): + print("Got invalid value type {}. Expected {}".format( + type(value), dict + )) + value = {} + + executable_path = value.get("executable") + if not executable_path: + executable_path = "" + elif isinstance(executable_path, list): + print("Got list in executable path so using first item as value") + executable_path = executable_path[0] + + if not isinstance(executable_path, str): + executable_path = "" + print(( + "Got invalid value type of app executable {}. Expected {}" + ).format(type(value), str)) + + self.executable_input_widget.setText(executable_path) + + def settings_value(self): + if not self.executable_input_widget: + return None + value = self.executable_input_widget.text() + if not value: + return None + return {"executable": value} + + +class AppGroupWidget(QtWidgets.QWidget): + def __init__(self, group_entity, parent): + super(AppGroupWidget, self).__init__(parent) + + valid_variants = {} + for key, entity in group_entity["variants"].items(): + if entity["enabled"].value: + valid_variants[key] = entity + + group_label = group_entity.label + expading_widget = ExpandingWidget(group_label, self) + content_widget = QtWidgets.QWidget(expading_widget) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) + + widgets_by_variant_name = {} + for variant_name, variant_entity in valid_variants.items(): + variant_widget = AppVariantWidget( + group_label, variant_entity, content_widget + ) + widgets_by_variant_name[variant_name] = variant_widget + content_layout.addWidget(variant_widget) + + expading_widget.set_content_widget(content_widget) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(expading_widget) + + self.widgets_by_variant_name = widgets_by_variant_name + + def update_local_settings(self, value): + if not value: + value = {} + + for variant_name, widget in self.widgets_by_variant_name.items(): + widget.update_local_settings(value.get(variant_name)) + + def settings_value(self): + output = {} + for variant_name, widget in self.widgets_by_variant_name.items(): + value = widget.settings_value() + if value: + output[variant_name] = value + + if not output: + return None + return output + + +class LocalApplicationsWidgets(QtWidgets.QWidget): + def __init__(self, system_settings_entity, parent): + super(LocalApplicationsWidgets, self).__init__(parent) + + self.widgets_by_group_name = {} + self.system_settings_entity = system_settings_entity + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + self.content_layout = layout + + def _reset_app_widgets(self): + while self.content_layout.count() > 0: + item = self.content_layout.itemAt(0) + item.widget().hide() + self.content_layout.removeItem(item) + self.widgets_by_group_name.clear() + + for key, entity in self.system_settings_entity["applications"].items(): + # Filter not enabled app groups + if not entity["enabled"].value: + continue + + # Check if has enabled any variant + enabled_variant = False + for variant_entity in entity["variants"].values(): + if variant_entity["enabled"].value: + enabled_variant = True + break + + if not enabled_variant: + continue + + # Create App group specific widget and store it by the key + group_widget = AppGroupWidget(entity, self) + self.widgets_by_group_name[key] = group_widget + self.content_layout.addWidget(group_widget) + + def update_local_settings(self, value): + if not value: + value = {} + + self._reset_app_widgets() + + for group_name, widget in self.widgets_by_group_name.items(): + widget.update_local_settings(value.get(group_name)) + + def settings_value(self): + output = {} + for group_name, widget in self.widgets_by_group_name.items(): + value = widget.settings_value() + if value: + output[group_name] = value + if not output: + return None + return output diff --git a/pype/tools/settings/local_settings/constants.py b/pype/tools/settings/local_settings/constants.py new file mode 100644 index 0000000000..83c45afba8 --- /dev/null +++ b/pype/tools/settings/local_settings/constants.py @@ -0,0 +1,32 @@ +# Action labels +LABEL_REMOVE_DEFAULT = "Remove from default" +LABEL_ADD_DEFAULT = "Add to default" +LABEL_REMOVE_PROJECT = "Remove from project" +LABEL_ADD_PROJECT = "Add to project" +LABEL_DISCARD_CHANGES = "Discard changes" + +# Local setting contants +# TODO move to settings constants +LOCAL_GENERAL_KEY = "general" +LOCAL_PROJECTS_KEY = "projects" +LOCAL_APPS_KEY = "applications" + +# Roots key constant +LOCAL_ROOTS_KEY = "roots" + +# Child offset in expandable widget +CHILD_OFFSET = 15 + +__all__ = ( + "LABEL_REMOVE_DEFAULT", + "LABEL_ADD_DEFAULT", + "LABEL_REMOVE_PROJECT", + "LABEL_ADD_PROJECT", + "LABEL_DISCARD_CHANGES", + + "LOCAL_GENERAL_KEY", + "LOCAL_PROJECTS_KEY", + "LOCAL_APPS_KEY", + + "LOCAL_ROOTS_KEY" +) diff --git a/pype/tools/settings/local_settings/general_widget.py b/pype/tools/settings/local_settings/general_widget.py new file mode 100644 index 0000000000..7732157122 --- /dev/null +++ b/pype/tools/settings/local_settings/general_widget.py @@ -0,0 +1,32 @@ +from Qt import QtWidgets + + +class LocalGeneralWidgets(QtWidgets.QWidget): + def __init__(self, parent): + super(LocalGeneralWidgets, self).__init__(parent) + + local_site_name_input = QtWidgets.QLineEdit(self) + + layout = QtWidgets.QFormLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + layout.addRow("Local site label", local_site_name_input) + + self.local_site_name_input = local_site_name_input + + def update_local_settings(self, value): + site_label = "" + if value: + site_label = value.get("site_label", site_label) + self.local_site_name_input.setText(site_label) + + def settings_value(self): + # Add changed + # If these have changed then + output = {} + local_site_name = self.local_site_name_input.text() + if local_site_name: + output["site_label"] = local_site_name + # Do not return output yet since we don't have mechanism to save or + # load these data through api calls + return output diff --git a/pype/tools/settings/local_settings/mongo_widget.py b/pype/tools/settings/local_settings/mongo_widget.py new file mode 100644 index 0000000000..c6f6ab1591 --- /dev/null +++ b/pype/tools/settings/local_settings/mongo_widget.py @@ -0,0 +1,80 @@ +import os +import sys +import traceback + +from Qt import QtWidgets +from pymongo.errors import ServerSelectionTimeoutError + +from pype.api import change_pype_mongo_url + + +class PypeMongoWidget(QtWidgets.QWidget): + def __init__(self, parent): + super(PypeMongoWidget, self).__init__(parent) + + # Warning label + warning_label = QtWidgets.QLabel(( + "WARNING: Requires restart. Change of Pype Mongo requires to" + " restart of all running Pype processes and process using Pype" + " (Including this)." + "\n- all changes in different categories won't be saved." + ), self) + warning_label.setStyleSheet("font-weight: bold;") + + # Label + mongo_url_label = QtWidgets.QLabel("Pype Mongo URL", self) + + # Input + mongo_url_input = QtWidgets.QLineEdit(self) + mongo_url_input.setPlaceholderText("< Pype Mongo URL >") + mongo_url_input.setText(os.environ["PYPE_MONGO"]) + + # Confirm button + mongo_url_change_btn = QtWidgets.QPushButton("Confirm Change", self) + + layout = QtWidgets.QGridLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(warning_label, 0, 0, 1, 3) + layout.addWidget(mongo_url_label, 1, 0) + layout.addWidget(mongo_url_input, 1, 1) + layout.addWidget(mongo_url_change_btn, 1, 2) + + mongo_url_change_btn.clicked.connect(self._on_confirm_click) + + self.mongo_url_input = mongo_url_input + + def _on_confirm_click(self): + value = self.mongo_url_input.text() + + dialog = QtWidgets.QMessageBox(self) + + title = "Pype mongo changed" + message = ( + "Pype mongo url was successfully changed. Restart Pype please." + ) + details = None + + try: + change_pype_mongo_url(value) + except Exception as exc: + if isinstance(exc, ServerSelectionTimeoutError): + error_message = ( + "Connection timeout passed." + " Probably can't connect to the Mongo server." + ) + else: + error_message = str(exc) + + title = "Pype mongo change failed" + # TODO catch exception message more gracefully + message = ( + "Pype mongo change was not successful." + " Full traceback can be found in details section.\n\n" + "Error message:\n{}" + ).format(error_message) + details = "\n".join(traceback.format_exception(*sys.exc_info())) + dialog.setWindowTitle(title) + dialog.setText(message) + if details: + dialog.setDetailedText(details) + dialog.exec_() diff --git a/pype/tools/settings/local_settings/projects_widget.py b/pype/tools/settings/local_settings/projects_widget.py new file mode 100644 index 0000000000..28765155c2 --- /dev/null +++ b/pype/tools/settings/local_settings/projects_widget.py @@ -0,0 +1,731 @@ +import platform +import copy +from Qt import QtWidgets, QtCore, QtGui +from pype.tools.settings.settings import ProjectListWidget +from pype.settings.constants import ( + PROJECT_ANATOMY_KEY, + DEFAULT_PROJECT_KEY +) +from .widgets import ( + SpacerWidget, + ProxyLabelWidget +) +from .constants import ( + LABEL_REMOVE_DEFAULT, + LABEL_ADD_DEFAULT, + LABEL_REMOVE_PROJECT, + LABEL_ADD_PROJECT, + LABEL_DISCARD_CHANGES, + LOCAL_ROOTS_KEY +) + +NOT_SET = type("NOT_SET", (), {})() + + +def get_active_sites(project_settings): + global_entity = project_settings["project_settings"]["global"] + sites_entity = global_entity["sync_server"]["sites"] + return tuple(sites_entity.keys()) + + +class _ProjectListWidget(ProjectListWidget): + def on_item_clicked(self, new_index): + new_project_name = new_index.data(QtCore.Qt.DisplayRole) + if new_project_name is None: + return + + if self.current_project == new_project_name: + return + + self.select_project(new_project_name) + self.current_project = new_project_name + self.project_changed.emit() + + +class RootInputWidget(QtWidgets.QWidget): + def __init__( + self, + local_project_settings, + local_project_settings_orig, + platform_root_entity, + root_name, + project_name, + site_name, + parent + ): + super(RootInputWidget, self).__init__(parent) + + self.local_project_settings = local_project_settings + self.local_project_settings_orig = local_project_settings_orig + self.platform_root_entity = platform_root_entity + self.root_name = root_name + self.site_name = site_name + self.project_name = project_name + + self.origin_value = self._get_site_value_for_project( + self.project_name, self.local_project_settings_orig + ) or "" + + is_default_project = bool(project_name == DEFAULT_PROJECT_KEY) + + default_input_value = self._get_site_value_for_project( + DEFAULT_PROJECT_KEY + ) + if is_default_project: + input_value = default_input_value + project_value = None + else: + input_value = self._get_site_value_for_project(self.project_name) + project_value = input_value + + # Placeholder + placeholder = None + if not is_default_project: + placeholder = default_input_value + + if not placeholder: + placeholder = platform_root_entity.value + + key_label = ProxyLabelWidget( + root_name, + self._mouse_release_callback, + self + ) + value_input = QtWidgets.QLineEdit(self) + value_input.setPlaceholderText("< {} >".format(placeholder)) + + # Root value + if input_value: + value_input.setText(input_value) + + value_input.textChanged.connect(self._on_value_change) + + root_layout = QtWidgets.QHBoxLayout(self) + root_layout.addWidget(key_label) + root_layout.addWidget(value_input) + + self.value_input = value_input + self.label_widget = key_label + + self.studio_value = platform_root_entity.value + self.default_value = default_input_value + self.project_value = project_value + self.placeholder_value = placeholder + + self._update_style() + + def is_modified(self): + return self.origin_value != self.value_input.text() + + def _mouse_release_callback(self, event): + if event.button() != QtCore.Qt.RightButton: + return + self._show_actions() + event.accept() + + def _get_style_state(self): + if self.project_name is None: + return "" + + if self.is_modified(): + return "modified" + + current_value = self.value_input.text() + if self.project_name == DEFAULT_PROJECT_KEY: + if current_value: + return "studio" + else: + if current_value: + return "overriden" + + studio_value = self._get_site_value_for_project( + DEFAULT_PROJECT_KEY + ) + if studio_value: + return "studio" + return "" + + def _update_style(self): + state = self._get_style_state() + + self.value_input.setProperty("input-state", state) + self.value_input.style().polish(self.value_input) + + self.label_widget.set_label_property("state", state) + + def _remove_from_local(self): + self.value_input.setText("") + self._update_style() + + def _add_to_local(self): + self.value_input.setText(self.placeholder_value) + self._update_style() + + def discard_changes(self): + self.value_input.setText(self.origin_value) + self._update_style() + + def _show_actions(self): + if self.project_name is None: + return + + menu = QtWidgets.QMenu(self) + actions_mapping = {} + + if self.project_name == DEFAULT_PROJECT_KEY: + remove_label = LABEL_REMOVE_DEFAULT + add_label = LABEL_ADD_DEFAULT + else: + remove_label = LABEL_REMOVE_PROJECT + add_label = LABEL_ADD_PROJECT + + if self.value_input.text(): + action = QtWidgets.QAction(remove_label) + callback = self._remove_from_local + else: + action = QtWidgets.QAction(add_label) + callback = self._add_to_local + + actions_mapping[action] = callback + menu.addAction(action) + + if self.is_modified(): + discard_changes_action = QtWidgets.QAction(LABEL_DISCARD_CHANGES) + actions_mapping[discard_changes_action] = self.discard_changes + menu.addAction(discard_changes_action) + + result = menu.exec_(QtGui.QCursor.pos()) + if result: + to_run = actions_mapping[result] + if to_run: + to_run() + + def _get_site_value_for_project(self, project_name, data=None): + if data is None: + data = self.local_project_settings + project_values = data.get(project_name) + site_value = {} + if project_values: + root_value = project_values.get(LOCAL_ROOTS_KEY) + if root_value: + site_value = root_value.get(self.site_name) or {} + return site_value.get(self.root_name) + + def _on_value_change(self): + value = self.value_input.text() + data = self.local_project_settings + for key in (self.project_name, LOCAL_ROOTS_KEY, self.site_name): + if key not in data: + data[key] = {} + data = data[key] + data[self.root_name] = value + self._update_style() + + +class RootsWidget(QtWidgets.QWidget): + def __init__(self, project_settings, parent): + super(RootsWidget, self).__init__(parent) + + self.project_settings = project_settings + self.site_widgets = [] + self.local_project_settings = None + self.local_project_settings_orig = None + self._project_name = None + + self.content_layout = QtWidgets.QVBoxLayout(self) + + def _clear_widgets(self): + while self.content_layout.count(): + item = self.content_layout.itemAt(0) + item.widget().hide() + self.content_layout.removeItem(item) + self.site_widgets = [] + + def refresh(self): + self._clear_widgets() + + if self._project_name is None: + return + + roots_entity = ( + self.project_settings[PROJECT_ANATOMY_KEY][LOCAL_ROOTS_KEY] + ) + # Site label + for site_name in get_active_sites(self.project_settings): + site_widget = QtWidgets.QWidget(self) + site_layout = QtWidgets.QVBoxLayout(site_widget) + + site_label = QtWidgets.QLabel(site_name, site_widget) + + site_layout.addWidget(site_label) + + # Root inputs + for root_name, path_entity in roots_entity.items(): + platform_entity = path_entity[platform.system().lower()] + root_widget = RootInputWidget( + self.local_project_settings, + self.local_project_settings_orig, + platform_entity, + root_name, + self._project_name, + site_name, + site_widget + ) + + site_layout.addWidget(root_widget) + + self.site_widgets.append(site_widget) + self.content_layout.addWidget(site_widget) + + # Add spacer so other widgets are squeezed to top + self.content_layout.addWidget(SpacerWidget(self), 1) + + def update_local_settings(self, local_project_settings): + self.local_project_settings = local_project_settings + self.local_project_settings_orig = copy.deepcopy( + dict(local_project_settings) + ) + + def change_project(self, project_name): + self._project_name = project_name + self.refresh() + + +class _SiteCombobox(QtWidgets.QWidget): + input_label = None + + def __init__(self, project_settings, parent): + super(_SiteCombobox, self).__init__(parent) + self.project_settings = project_settings + + self.local_project_settings = None + self.local_project_settings_orig = None + self.project_name = None + + self.default_override_value = None + self.project_override_value = None + + label_widget = ProxyLabelWidget( + self.input_label, + self._mouse_release_callback, + self + ) + combobox_input = QtWidgets.QComboBox(self) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.addWidget(label_widget) + main_layout.addWidget(combobox_input) + + combobox_input.currentIndexChanged.connect(self._on_index_change) + self.label_widget = label_widget + self.combobox_input = combobox_input + + def _set_current_text(self, text): + index = None + if text: + idx = self.combobox_input.findText(text) + if idx >= 0: + index = idx + + if index is not None: + self.combobox_input.setCurrentIndex(index) + return True + return False + + def is_modified(self, current_value=NOT_SET, orig_value=NOT_SET): + if current_value is NOT_SET: + current_value = self._get_local_settings_item(self.project_name) + if orig_value is NOT_SET: + orig_value = self._get_local_settings_item( + self.project_name, self.local_project_settings_orig + ) + if current_value and orig_value: + modified = current_value != orig_value + elif not current_value and not orig_value: + modified = False + else: + modified = True + return modified + + def _get_style_state(self): + if self.project_name is None: + return "" + + current_value = self._get_local_settings_item(self.project_name) + orig_value = self._get_local_settings_item( + self.project_name, self.local_project_settings_orig + ) + + if self.is_modified(current_value, orig_value): + return "modified" + + if self.project_name == DEFAULT_PROJECT_KEY: + if current_value: + return "studio" + else: + if current_value: + return "overriden" + + studio_value = self._get_local_settings_item(DEFAULT_PROJECT_KEY) + if studio_value: + return "studio" + return "" + + def _update_style(self): + state = self._get_style_state() + + self.combobox_input.setProperty("input-state", state) + self.combobox_input.style().polish(self.combobox_input) + + self.label_widget.set_label_property("state", state) + + def _mouse_release_callback(self, event): + if event.button() != QtCore.Qt.RightButton: + return + self._show_actions() + + def _remove_from_local(self): + settings_value = self._get_value_from_project_settings() + combobox_value = None + if self.project_name == DEFAULT_PROJECT_KEY: + combobox_value = self._get_local_settings_item(DEFAULT_PROJECT_KEY) + if combobox_value: + idx = self.combobox_input.findText(combobox_value) + if idx < 0: + combobox_value = None + + if not combobox_value: + combobox_value = settings_value + + if combobox_value: + _project_name = self.project_name + self.project_name = None + self._set_current_text(combobox_value) + self.project_name = _project_name + + self._set_local_settings_value("") + self._update_style() + + def _add_to_local(self): + self._set_local_settings_value(self.current_text()) + self._update_style() + + def discard_changes(self): + orig_value = self._get_local_settings_item( + self.project_name, self.local_project_settings_orig + ) + self._set_current_text(orig_value) + + def _show_actions(self): + if self.project_name is None: + return + + menu = QtWidgets.QMenu(self) + actions_mapping = {} + + if self.project_name == DEFAULT_PROJECT_KEY: + remove_label = LABEL_REMOVE_DEFAULT + add_label = LABEL_ADD_DEFAULT + else: + remove_label = LABEL_REMOVE_PROJECT + add_label = LABEL_ADD_PROJECT + + has_value = self._get_local_settings_item(self.project_name) + if has_value: + action = QtWidgets.QAction(remove_label) + callback = self._remove_from_local + else: + action = QtWidgets.QAction(add_label) + callback = self._add_to_local + + actions_mapping[action] = callback + menu.addAction(action) + + if self.is_modified(): + discard_changes_action = QtWidgets.QAction(LABEL_DISCARD_CHANGES) + actions_mapping[discard_changes_action] = self.discard_changes + menu.addAction(discard_changes_action) + + result = menu.exec_(QtGui.QCursor.pos()) + if result: + to_run = actions_mapping[result] + if to_run: + to_run() + + def update_local_settings(self, local_project_settings): + self.local_project_settings = local_project_settings + self.local_project_settings_orig = copy.deepcopy( + dict(local_project_settings) + ) + + def current_text(self): + return self.combobox_input.currentText() + + def change_project(self, project_name): + self.default_override_value = None + self.project_override_value = None + + self.project_name = None + self.combobox_input.clear() + if project_name is None: + self._update_style() + return + + is_default_project = bool(project_name == DEFAULT_PROJECT_KEY) + site_items = self._get_project_sites() + self.combobox_input.addItems(site_items) + + default_item = self._get_local_settings_item(DEFAULT_PROJECT_KEY) + if is_default_project: + project_item = None + else: + project_item = self._get_local_settings_item(project_name) + + index = None + if project_item: + idx = self.combobox_input.findText(project_item) + if idx >= 0: + self.project_override_value = project_item + index = idx + + if default_item: + idx = self.combobox_input.findText(default_item) + if idx >= 0: + self.default_override_value = default_item + if index is None: + index = idx + + if index is None: + settings_value = self._get_value_from_project_settings() + idx = self.combobox_input.findText(settings_value) + if idx >= 0: + index = idx + + if index is not None: + self.combobox_input.setCurrentIndex(index) + + self.project_name = project_name + self._update_style() + + def _on_index_change(self): + if self.project_name is None: + return + + self._set_local_settings_value(self.current_text()) + self._update_style() + + def _set_local_settings_value(self, value): + raise NotImplementedError( + "{} `_set_local_settings_value` not implemented".format( + self.__class__.__name__ + ) + ) + + def _get_project_sites(self): + raise NotImplementedError( + "{} `_get_project_sites` not implemented".format( + self.__class__.__name__ + ) + ) + + def _get_local_settings_item(self, project_name=None, data=None): + raise NotImplementedError( + "{}`_get_local_settings_item` not implemented".format( + self.__class__.__name__ + ) + ) + + def _get_value_from_project_settings(self): + raise NotImplementedError( + "{}`_get_value_from_project_settings` not implemented".format( + self.__class__.__name__ + ) + ) + + +class AciveSiteCombo(_SiteCombobox): + input_label = "Active site" + + def _get_project_sites(self): + return get_active_sites(self.project_settings) + + def _get_local_settings_item(self, project_name=None, data=None): + if project_name is None: + project_name = self.project_name + + if data is None: + data = self.local_project_settings + project_values = data.get(project_name) + value = None + if project_values: + value = project_values.get("active_site") + return value + + def _get_value_from_project_settings(self): + global_entity = self.project_settings["project_settings"]["global"] + return global_entity["sync_server"]["config"]["active_site"].value + + def _set_local_settings_value(self, value): + if self.project_name not in self.local_project_settings: + self.local_project_settings[self.project_name] = {} + self.local_project_settings[self.project_name]["active_site"] = value + + +class RemoteSiteCombo(_SiteCombobox): + input_label = "Remote site" + + def _get_project_sites(self): + global_entity = self.project_settings["project_settings"]["global"] + sites_entity = global_entity["sync_server"]["sites"] + return tuple(sites_entity.keys()) + + def _get_local_settings_item(self, project_name=None, data=None): + if project_name is None: + project_name = self.project_name + if data is None: + data = self.local_project_settings + project_values = data.get(project_name) + value = None + if project_values: + value = project_values.get("remote_site") + return value + + def _get_value_from_project_settings(self): + global_entity = self.project_settings["project_settings"]["global"] + return global_entity["sync_server"]["config"]["remote_site"].value + + def _set_local_settings_value(self, value): + if self.project_name not in self.local_project_settings: + self.local_project_settings[self.project_name] = {} + self.local_project_settings[self.project_name]["remote_site"] = value + + +class RootSiteWidget(QtWidgets.QWidget): + def __init__(self, project_settings, parent): + self._parent_widget = parent + super(RootSiteWidget, self).__init__(parent) + + self.project_settings = project_settings + self._project_name = None + + sites_widget = QtWidgets.QWidget(self) + + active_site_widget = AciveSiteCombo(project_settings, sites_widget) + remote_site_widget = RemoteSiteCombo(project_settings, sites_widget) + + sites_layout = QtWidgets.QHBoxLayout(sites_widget) + sites_layout.setContentsMargins(0, 0, 0, 0) + sites_layout.addWidget(active_site_widget) + sites_layout.addWidget(remote_site_widget) + sites_layout.addWidget(SpacerWidget(self), 1) + + roots_widget = RootsWidget(project_settings, self) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addWidget(sites_widget) + main_layout.addWidget(roots_widget) + main_layout.addWidget(SpacerWidget(self), 1) + + self.active_site_widget = active_site_widget + self.remote_site_widget = remote_site_widget + self.roots_widget = roots_widget + + def _active_site_values(self): + global_entity = self.project_settings["project_settings"]["global"] + sites_entity = global_entity["sync_server"]["sites"] + return tuple(sites_entity.keys()) + + def _remote_site_values(self): + global_entity = self.project_settings["project_settings"]["global"] + sites_entity = global_entity["sync_server"]["sites"] + return tuple(sites_entity.keys()) + + def update_local_settings(self, local_project_settings): + self.local_project_settings = local_project_settings + self.active_site_widget.update_local_settings(local_project_settings) + self.remote_site_widget.update_local_settings(local_project_settings) + self.roots_widget.update_local_settings(local_project_settings) + project_name = self._project_name + if project_name is None: + project_name = DEFAULT_PROJECT_KEY + + self.change_project(project_name) + + def change_project(self, project_name): + self._project_name = project_name + # Set roots project to None so all changes below are ignored + self.roots_widget.change_project(None) + + # Aply changes in site comboboxes + self.active_site_widget.change_project(project_name) + self.remote_site_widget.change_project(project_name) + + # Change project name in roots widget + self.roots_widget.change_project(project_name) + + +class ProjectValue(dict): + pass + + +class ProjectSettingsWidget(QtWidgets.QWidget): + def __init__(self, project_settings, parent): + super(ProjectSettingsWidget, self).__init__(parent) + + self.local_project_settings = {} + + projects_widget = _ProjectListWidget(self) + roos_site_widget = RootSiteWidget(project_settings, self) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(projects_widget, 0) + main_layout.addWidget(roos_site_widget, 1) + + projects_widget.project_changed.connect(self._on_project_change) + + self.project_settings = project_settings + + self.projects_widget = projects_widget + self.roos_site_widget = roos_site_widget + + def project_name(self): + return self.projects_widget.project_name() + + def _on_project_change(self): + project_name = self.project_name() + self.project_settings.change_project(project_name) + if project_name is None: + project_name = DEFAULT_PROJECT_KEY + self.roos_site_widget.change_project(project_name) + + def update_local_settings(self, value): + if not value: + value = {} + self.local_project_settings = ProjectValue(value) + + self.roos_site_widget.update_local_settings( + self.local_project_settings + ) + + self.projects_widget.refresh() + + def _clear_value(self, value): + if not value: + return None + + if not isinstance(value, dict): + return value + + output = {} + for _key, _value in value.items(): + _modified_value = self._clear_value(_value) + if _modified_value: + output[_key] = _modified_value + return output + + def settings_value(self): + output = self._clear_value(self.local_project_settings) + if not output: + return None + return output diff --git a/pype/tools/settings/local_settings/widgets.py b/pype/tools/settings/local_settings/widgets.py new file mode 100644 index 0000000000..1b077f93be --- /dev/null +++ b/pype/tools/settings/local_settings/widgets.py @@ -0,0 +1,59 @@ +from Qt import QtWidgets, QtCore +from pype.tools.settings.settings.widgets.widgets import ( + ExpandingWidget, + SpacerWidget +) + + +class Separator(QtWidgets.QFrame): + def __init__(self, height=None, parent=None): + super(Separator, self).__init__(parent) + if height is None: + height = 2 + + splitter_item = QtWidgets.QWidget(self) + splitter_item.setStyleSheet("background-color: #21252B;") + splitter_item.setMinimumHeight(height) + splitter_item.setMaximumHeight(height) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + layout.addWidget(splitter_item) + + +class ProxyLabelWidget(QtWidgets.QWidget): + def __init__(self, label, mouse_release_callback, parent=None): + super(ProxyLabelWidget, self).__init__(parent) + + self.mouse_release_callback = mouse_release_callback + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + label_widget = QtWidgets.QLabel(label, self) + layout.addWidget(label_widget) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self.label_widget = label_widget + + def setText(self, text): + self.label_widget.setText(text) + + def set_label_property(self, *args, **kwargs): + self.label_widget.setProperty(*args, **kwargs) + self.label_widget.style().polish(self.label_widget) + + def mouseReleaseEvent(self, event): + if self.mouse_release_callback: + return self.mouse_release_callback(event) + return super(ProxyLabelWidget, self).mouseReleaseEvent(event) + + +__all__ = ( + "ExpandingWidget", + "SpacerWidget", + "Separator", + "SpacerWidget" +) diff --git a/pype/tools/settings/local_settings/window.py b/pype/tools/settings/local_settings/window.py new file mode 100644 index 0000000000..87a276c78c --- /dev/null +++ b/pype/tools/settings/local_settings/window.py @@ -0,0 +1,204 @@ +import logging +from Qt import QtWidgets, QtGui + +from ..settings import style + +from pype.settings.lib import ( + get_local_settings, + save_local_settings +) +from pype.api import ( + SystemSettings, + ProjectSettings +) + +from .widgets import ( + SpacerWidget, + ExpandingWidget +) +from .mongo_widget import PypeMongoWidget +from .general_widget import LocalGeneralWidgets +from .apps_widget import LocalApplicationsWidgets +from .projects_widget import ProjectSettingsWidget + +from .constants import ( + CHILD_OFFSET, + LOCAL_GENERAL_KEY, + LOCAL_PROJECTS_KEY, + LOCAL_APPS_KEY +) + +log = logging.getLogger(__name__) + + +class LocalSettingsWidget(QtWidgets.QWidget): + def __init__(self, parent=None): + super(LocalSettingsWidget, self).__init__(parent) + + self.system_settings = SystemSettings() + self.project_settings = ProjectSettings() + + self.main_layout = QtWidgets.QVBoxLayout(self) + + self.pype_mongo_widget = None + self.general_widget = None + self.apps_widget = None + self.projects_widget = None + + self._create_pype_mongo_ui() + self._create_general_ui() + self._create_app_ui() + self._create_project_ui() + + # Add spacer to main layout + self.main_layout.addWidget(SpacerWidget(self), 1) + + def _create_pype_mongo_ui(self): + pype_mongo_expand_widget = ExpandingWidget("Pype Mongo URL", self) + pype_mongo_content = QtWidgets.QWidget(self) + pype_mongo_layout = QtWidgets.QVBoxLayout(pype_mongo_content) + pype_mongo_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) + pype_mongo_expand_widget.set_content_widget(pype_mongo_content) + + pype_mongo_widget = PypeMongoWidget(self) + pype_mongo_layout.addWidget(pype_mongo_widget) + + self.main_layout.addWidget(pype_mongo_expand_widget) + + self.pype_mongo_widget = pype_mongo_widget + + def _create_general_ui(self): + # General + general_expand_widget = ExpandingWidget("General", self) + + general_content = QtWidgets.QWidget(self) + general_layout = QtWidgets.QVBoxLayout(general_content) + general_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) + general_expand_widget.set_content_widget(general_content) + + general_widget = LocalGeneralWidgets(general_content) + general_layout.addWidget(general_widget) + + self.main_layout.addWidget(general_expand_widget) + + self.general_widget = general_widget + + def _create_app_ui(self): + # Applications + app_expand_widget = ExpandingWidget("Applications", self) + + app_content = QtWidgets.QWidget(self) + app_layout = QtWidgets.QVBoxLayout(app_content) + app_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) + app_expand_widget.set_content_widget(app_content) + + app_widget = LocalApplicationsWidgets( + self.system_settings, app_content + ) + app_layout.addWidget(app_widget) + + self.main_layout.addWidget(app_expand_widget) + + self.app_widget = app_widget + + def _create_project_ui(self): + project_expand_widget = ExpandingWidget("Project settings", self) + project_content = QtWidgets.QWidget(self) + project_layout = QtWidgets.QVBoxLayout(project_content) + project_layout.setContentsMargins(CHILD_OFFSET, 5, 0, 0) + project_expand_widget.set_content_widget(project_content) + + projects_widget = ProjectSettingsWidget(self.project_settings, self) + project_layout.addWidget(projects_widget) + + self.main_layout.addWidget(project_expand_widget) + + self.projects_widget = projects_widget + + def update_local_settings(self, value): + if not value: + value = {} + + self.system_settings.reset() + self.project_settings.reset() + + self.general_widget.update_local_settings( + value.get(LOCAL_GENERAL_KEY) + ) + self.app_widget.update_local_settings( + value.get(LOCAL_APPS_KEY) + ) + self.projects_widget.update_local_settings( + value.get(LOCAL_PROJECTS_KEY) + ) + + def settings_value(self): + output = {} + general_value = self.general_widget.settings_value() + if general_value: + output[LOCAL_GENERAL_KEY] = general_value + + app_value = self.app_widget.settings_value() + if app_value: + output[LOCAL_APPS_KEY] = app_value + + projects_value = self.projects_widget.settings_value() + if projects_value: + output[LOCAL_PROJECTS_KEY] = projects_value + return output + + +class LocalSettingsWindow(QtWidgets.QWidget): + def __init__(self, parent=None): + super(LocalSettingsWindow, self).__init__(parent) + + self.resize(1000, 600) + + self.setWindowTitle("Pype Local settings") + + stylesheet = style.load_stylesheet() + self.setStyleSheet(stylesheet) + self.setWindowIcon(QtGui.QIcon(style.app_icon_path())) + + scroll_widget = QtWidgets.QScrollArea(self) + scroll_widget.setObjectName("GroupWidget") + settings_widget = LocalSettingsWidget(scroll_widget) + + scroll_widget.setWidget(settings_widget) + scroll_widget.setWidgetResizable(True) + + footer = QtWidgets.QWidget(self) + + save_btn = QtWidgets.QPushButton("Save", footer) + reset_btn = QtWidgets.QPushButton("Reset", footer) + + footer_layout = QtWidgets.QHBoxLayout(footer) + footer_layout.addWidget(reset_btn, 0) + footer_layout.addWidget(SpacerWidget(footer), 1) + footer_layout.addWidget(save_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(scroll_widget, 1) + main_layout.addWidget(footer, 0) + + save_btn.clicked.connect(self._on_save_clicked) + reset_btn.clicked.connect(self._on_reset_clicked) + + self.settings_widget = settings_widget + self.reset_btn = reset_btn + self.save_btn = save_btn + + self.reset() + + def reset(self): + value = get_local_settings() + self.settings_widget.update_local_settings(value) + + def _on_reset_clicked(self): + self.reset() + + def _on_save_clicked(self): + value = self.settings_widget.settings_value() + save_local_settings(value) + self.reset() diff --git a/pype/tools/settings/settings/widgets/widgets.py b/pype/tools/settings/settings/widgets/widgets.py index fd8d9d753c..656aaaa652 100644 --- a/pype/tools/settings/settings/widgets/widgets.py +++ b/pype/tools/settings/settings/widgets/widgets.py @@ -664,9 +664,8 @@ class ProjectListWidget(QtWidgets.QWidget): self.current_project = None if self.dbcon: - for project_doc in tuple(self.dbcon.projects()): - items.append(project_doc["name"]) - + for project_name in self.dbcon.database.collection_names(): + items.append(project_name) for item in items: model.appendRow(QtGui.QStandardItem(item))