diff --git a/openpype/hooks/pre_copy_template_workfile.py b/openpype/hooks/pre_copy_template_workfile.py index 29a522f933..5c56d721e8 100644 --- a/openpype/hooks/pre_copy_template_workfile.py +++ b/openpype/hooks/pre_copy_template_workfile.py @@ -49,7 +49,7 @@ class CopyTemplateWorkfile(PreLaunchHook): )) return - self.log.info("Last workfile does not exits.") + self.log.info("Last workfile does not exist.") project_name = self.data["project_name"] asset_name = self.data["asset_name"] diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 5f898a9a67..7e7cd27f90 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1660,9 +1660,13 @@ def find_free_space_to_paste_nodes( def launch_workfiles_app(): '''Function letting start workfiles after start of host ''' - # get state from settings - open_at_start = get_current_project_settings()["nuke"].get( - "general", {}).get("open_workfile_at_start") + from openpype.lib import ( + env_value_to_bool + ) + # get all imortant settings + open_at_start = env_value_to_bool( + env_key="OPENPYPE_WORKFILE_TOOL_ON_START", + default=None) # return if none is defined if not open_at_start: diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index e1b304a351..ada194f15f 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1302,10 +1302,18 @@ def _prepare_last_workfile(data, workdir): ) data["start_last_workfile"] = start_last_workfile + workfile_startup = should_workfile_tool_start( + project_name, app.host_name, task_name + ) + data["workfile_startup"] = workfile_startup + # Store boolean as "0"(False) or "1"(True) data["env"]["AVALON_OPEN_LAST_WORKFILE"] = ( str(int(bool(start_last_workfile))) ) + data["env"]["OPENPYPE_WORKFILE_TOOL_ON_START"] = ( + str(int(bool(workfile_startup))) + ) _sub_msg = "" if start_last_workfile else " not" log.debug( @@ -1344,40 +1352,9 @@ def _prepare_last_workfile(data, workdir): data["last_workfile_path"] = last_workfile_path -def should_start_last_workfile( - project_name, host_name, task_name, default_output=False +def get_option_from_settings( + startup_presets, host_name, task_name, default_output ): - """Define if host should start last version workfile if possible. - - Default output is `False`. Can be overriden with environment variable - `AVALON_OPEN_LAST_WORKFILE`, valid values without case sensitivity are - `"0", "1", "true", "false", "yes", "no"`. - - Args: - project_name (str): Name of project. - host_name (str): Name of host which is launched. In avalon's - application context it's value stored in app definition under - key `"application_dir"`. Is not case sensitive. - task_name (str): Name of task which is used for launching the host. - Task name is not case sensitive. - - Returns: - bool: True if host should start workfile. - - """ - - project_settings = get_project_settings(project_name) - startup_presets = ( - project_settings - ["global"] - ["tools"] - ["Workfiles"] - ["last_workfile_on_startup"] - ) - - if not startup_presets: - return default_output - host_name_lowered = host_name.lower() task_name_lowered = task_name.lower() @@ -1421,6 +1398,82 @@ def should_start_last_workfile( return default_output +def should_start_last_workfile( + project_name, host_name, task_name, default_output=False +): + """Define if host should start last version workfile if possible. + + Default output is `False`. Can be overriden with environment variable + `AVALON_OPEN_LAST_WORKFILE`, valid values without case sensitivity are + `"0", "1", "true", "false", "yes", "no"`. + + Args: + project_name (str): Name of project. + host_name (str): Name of host which is launched. In avalon's + application context it's value stored in app definition under + key `"application_dir"`. Is not case sensitive. + task_name (str): Name of task which is used for launching the host. + Task name is not case sensitive. + + Returns: + bool: True if host should start workfile. + + """ + + project_settings = get_project_settings(project_name) + startup_presets = ( + project_settings + ["global"] + ["tools"] + ["Workfiles"] + ["last_workfile_on_startup"] + ) + + if not startup_presets: + return default_output + + return get_option_from_settings( + startup_presets, host_name, task_name, default_output) + + +def should_workfile_tool_start( + project_name, host_name, task_name, default_output=False +): + """Define if host should start workfile tool at host launch. + + Default output is `False`. Can be overriden with environment variable + `OPENPYPE_WORKFILE_TOOL_ON_START`, valid values without case sensitivity are + `"0", "1", "true", "false", "yes", "no"`. + + Args: + project_name (str): Name of project. + host_name (str): Name of host which is launched. In avalon's + application context it's value stored in app definition under + key `"application_dir"`. Is not case sensitive. + task_name (str): Name of task which is used for launching the host. + Task name is not case sensitive. + + Returns: + bool: True if host should start workfile. + + """ + + project_settings = get_project_settings(project_name) + startup_presets = ( + project_settings + ["global"] + ["tools"] + ["Workfiles"] + ["open_workfile_tool_on_startup"] + ) + + if not startup_presets: + return default_output + + return get_option_from_settings( + startup_presets, host_name, task_name, default_output) + + def compile_list_of_regexes(in_list): """Convert strings in entered list to compiled regex objects.""" regexes = list() diff --git a/openpype/modules/ftrack/event_handlers_user/action_applications.py b/openpype/modules/ftrack/event_handlers_user/action_applications.py index 23c96e1b9f..74d14c2fc4 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_applications.py +++ b/openpype/modules/ftrack/event_handlers_user/action_applications.py @@ -11,29 +11,44 @@ from avalon.api import AvalonMongoDB class AppplicationsAction(BaseAction): - """Application Action class. - - Args: - session (ftrack_api.Session): Session where action will be registered. - label (str): A descriptive string identifing your action. - varaint (str, optional): To group actions together, give them the same - label and specify a unique variant per action. - identifier (str): An unique identifier for app. - description (str): A verbose descriptive text for you action. - icon (str): Url path to icon which will be shown in Ftrack web. - """ + """Applications Action class.""" type = "Application" label = "Application action" - identifier = "pype_app.{}.".format(str(uuid4())) + + identifier = "openpype_app" + _launch_identifier_with_id = None + icon_url = os.environ.get("OPENPYPE_STATICS_SERVER") def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + super(AppplicationsAction, self).__init__(*args, **kwargs) self.application_manager = ApplicationManager() self.dbcon = AvalonMongoDB() + @property + def discover_identifier(self): + if self._discover_identifier is None: + self._discover_identifier = "{}.{}".format( + self.identifier, self.process_identifier() + ) + return self._discover_identifier + + @property + def launch_identifier(self): + if self._launch_identifier is None: + self._launch_identifier = "{}.*".format(self.identifier) + return self._launch_identifier + + @property + def launch_identifier_with_id(self): + if self._launch_identifier_with_id is None: + self._launch_identifier_with_id = "{}.{}".format( + self.identifier, self.process_identifier() + ) + return self._launch_identifier_with_id + def construct_requirements_validations(self): # Override validation as this action does not need them return @@ -56,7 +71,7 @@ class AppplicationsAction(BaseAction): " and data.actionIdentifier={0}" " and source.user.username={1}" ).format( - self.identifier + "*", + self.launch_identifier, self.session.api_user ) self.session.event_hub.subscribe( @@ -136,12 +151,29 @@ class AppplicationsAction(BaseAction): "label": app.group.label, "variant": app.label, "description": None, - "actionIdentifier": self.identifier + app_name, + "actionIdentifier": "{}.{}".format( + self.launch_identifier_with_id, app_name + ), "icon": app_icon }) return items + def _launch(self, event): + event_identifier = event["data"]["actionIdentifier"] + # Check if identifier is same + # - show message that acion may not be triggered on this machine + if event_identifier.startswith(self.launch_identifier_with_id): + return BaseAction._launch(self, event) + + return { + "success": False, + "message": ( + "There are running more OpenPype processes" + " where Application can be launched." + ) + } + def launch(self, session, entities, event): """Callback method for the custom action. @@ -162,7 +194,8 @@ class AppplicationsAction(BaseAction): *event* the unmodified original event """ identifier = event["data"]["actionIdentifier"] - app_name = identifier[len(self.identifier):] + id_identifier_len = len(self.launch_identifier_with_id) + 1 + app_name = identifier[id_identifier_len:] entity = entities[0] diff --git a/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py index 8db65fe39b..f5af044de0 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py @@ -9,16 +9,24 @@ class MultipleNotes(BaseAction): #: Action label. label = 'Multiple Notes' #: Action description. - description = 'Add same note to multiple Asset Versions' + description = 'Add same note to multiple entities' icon = statics_icon("ftrack", "action_icons", "MultipleNotes.svg") def discover(self, session, entities, event): ''' Validation ''' valid = True + + # Check for multiple selection. + if len(entities) < 2: + valid = False + + # Check for valid entities. + valid_entity_types = ['assetversion', 'task'] for entity in entities: - if entity.entity_type.lower() != 'assetversion': + if entity.entity_type.lower() not in valid_entity_types: valid = False break + return valid def interface(self, session, entities, event): @@ -58,7 +66,7 @@ class MultipleNotes(BaseAction): splitter = { 'type': 'label', - 'value': '{}'.format(200*"-") + 'value': '{}'.format(200 * "-") } items = [] diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index ea0bfa2971..4b42500e8f 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -428,9 +428,11 @@ class PrepareProjectLocal(BaseAction): # Trigger create project structure action if create_project_structure_checked: - self.trigger_action( - self.create_project_structure_identifier, event + trigger_identifier = "{}.{}".format( + self.create_project_structure_identifier, + self.process_identifier() ) + self.trigger_action(trigger_identifier, event) return True diff --git a/openpype/modules/ftrack/event_handlers_user/action_where_run_show.py b/openpype/modules/ftrack/event_handlers_user/action_where_run_show.py index 4ce1a439a3..b8b49e86cb 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_where_run_show.py +++ b/openpype/modules/ftrack/event_handlers_user/action_where_run_show.py @@ -24,6 +24,10 @@ class ActionShowWhereIRun(BaseAction): return False + @property + def launch_identifier(self): + return self.identifier + def launch(self, session, entities, event): # Don't show info when was launch from this session if session.event_hub.id == event.get("data", {}).get("event_hub_id"): diff --git a/openpype/modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py index 2bff9d8cb3..b24fe5f12a 100644 --- a/openpype/modules/ftrack/lib/ftrack_action_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_action_handler.py @@ -29,6 +29,9 @@ class BaseAction(BaseHandler): icon = None type = 'Action' + _discover_identifier = None + _launch_identifier = None + settings_frack_subkey = "user_handlers" settings_enabled_key = "enabled" @@ -42,6 +45,22 @@ class BaseAction(BaseHandler): super().__init__(session) + @property + def discover_identifier(self): + if self._discover_identifier is None: + self._discover_identifier = "{}.{}".format( + self.identifier, self.process_identifier() + ) + return self._discover_identifier + + @property + def launch_identifier(self): + if self._launch_identifier is None: + self._launch_identifier = "{}.{}".format( + self.identifier, self.process_identifier() + ) + return self._launch_identifier + def register(self): ''' Registers the action, subscribing the the discover and launch topics. @@ -60,7 +79,7 @@ class BaseAction(BaseHandler): ' and data.actionIdentifier={0}' ' and source.user.username={1}' ).format( - self.identifier, + self.launch_identifier, self.session.api_user ) self.session.event_hub.subscribe( @@ -86,7 +105,7 @@ class BaseAction(BaseHandler): 'label': self.label, 'variant': self.variant, 'description': self.description, - 'actionIdentifier': self.identifier, + 'actionIdentifier': self.discover_identifier, 'icon': self.icon, }] } @@ -309,6 +328,78 @@ class BaseAction(BaseHandler): return True +class LocalAction(BaseAction): + """Action that warn user when more Processes with same action are running. + + Action is launched all the time but if id does not match id of current + instanace then message is shown to user. + + Handy for actions where matters if is executed on specific machine. + """ + _full_launch_identifier = None + + @property + def discover_identifier(self): + if self._discover_identifier is None: + self._discover_identifier = "{}.{}".format( + self.identifier, self.process_identifier() + ) + return self._discover_identifier + + @property + def launch_identifier(self): + """Catch all topics with same identifier.""" + if self._launch_identifier is None: + self._launch_identifier = "{}.*".format(self.identifier) + return self._launch_identifier + + @property + def full_launch_identifier(self): + """Catch all topics with same identifier.""" + if self._full_launch_identifier is None: + self._full_launch_identifier = "{}.{}".format( + self.identifier, self.process_identifier() + ) + return self._full_launch_identifier + + def _discover(self, event): + entities = self._translate_event(event) + if not entities: + return + + accepts = self.discover(self.session, entities, event) + if not accepts: + return + + self.log.debug("Discovering action with selection: {0}".format( + event["data"].get("selection", []) + )) + + return { + "items": [{ + "label": self.label, + "variant": self.variant, + "description": self.description, + "actionIdentifier": self.discover_identifier, + "icon": self.icon, + }] + } + + def _launch(self, event): + event_identifier = event["data"]["actionIdentifier"] + # Check if identifier is same + # - show message that acion may not be triggered on this machine + if event_identifier != self.full_launch_identifier: + return { + "success": False, + "message": ( + "There are running more OpenPype processes" + " where this action could be launched." + ) + } + return super(LocalAction, self)._launch(event) + + class ServerAction(BaseAction): """Action class meant to be used on event server. @@ -318,6 +409,14 @@ class ServerAction(BaseAction): settings_frack_subkey = "events" + @property + def discover_identifier(self): + return self.identifier + + @property + def launch_identifier(self): + return self.identifier + def register(self): """Register subcription to Ftrack event hub.""" self.session.event_hub.subscribe( @@ -328,5 +427,5 @@ class ServerAction(BaseAction): launch_subscription = ( "topic=ftrack.action.launch and data.actionIdentifier={0}" - ).format(self.identifier) + ).format(self.launch_identifier) self.session.event_hub.subscribe(launch_subscription, self._launch) diff --git a/openpype/modules/ftrack/lib/ftrack_base_handler.py b/openpype/modules/ftrack/lib/ftrack_base_handler.py index 011ce8db9d..b8be287a03 100644 --- a/openpype/modules/ftrack/lib/ftrack_base_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_base_handler.py @@ -2,6 +2,7 @@ import os import tempfile import json import functools +import uuid import datetime import traceback import time @@ -36,6 +37,7 @@ class BaseHandler(object): - a verbose descriptive text for you action - icon in ftrack ''' + _process_id = None # Default priority is 100 priority = 100 # Type is just for logging purpose (e.g.: Action, Event, Application,...) @@ -70,6 +72,13 @@ class BaseHandler(object): self.register = self.register_decorator(self.register) self.launch = self.launch_log(self.launch) + @staticmethod + def process_identifier(): + """Helper property to have """ + if not BaseHandler._process_id: + BaseHandler._process_id = str(uuid.uuid4()) + return BaseHandler._process_id + # Decorator def register_decorator(self, func): @functools.wraps(func) diff --git a/openpype/modules/log_viewer/tray/app.py b/openpype/modules/log_viewer/tray/app.py index 9aab37cd20..1e8d6483cd 100644 --- a/openpype/modules/log_viewer/tray/app.py +++ b/openpype/modules/log_viewer/tray/app.py @@ -7,12 +7,13 @@ class LogsWindow(QtWidgets.QWidget): def __init__(self, parent=None): super(LogsWindow, self).__init__(parent) - self.setStyleSheet(style.load_stylesheet()) + self.setWindowTitle("Logs viewer") + self.resize(1400, 800) log_detail = OutputWidget(parent=self) logs_widget = LogsWidget(log_detail, parent=self) - main_layout = QtWidgets.QHBoxLayout() + main_layout = QtWidgets.QHBoxLayout(self) log_splitter = QtWidgets.QSplitter(self) log_splitter.setOrientation(QtCore.Qt.Horizontal) @@ -24,5 +25,4 @@ class LogsWindow(QtWidgets.QWidget): self.logs_widget = logs_widget self.log_detail = log_detail - self.setLayout(main_layout) - self.setWindowTitle("Logs") + self.setStyleSheet(style.load_stylesheet()) diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index b9a8499a4c..5a67780413 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -77,12 +77,10 @@ class CustomCombo(QtWidgets.QWidget): toolbutton.setMenu(toolmenu) toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) - layout = QtWidgets.QHBoxLayout() + layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(toolbutton) - self.setLayout(layout) - toolmenu.selection_changed.connect(self.selection_changed) self.toolbutton = toolbutton @@ -141,7 +139,6 @@ class LogsWidget(QtWidgets.QWidget): filter_layout.addWidget(refresh_btn) view = QtWidgets.QTreeView(self) - view.setAllColumnsShowFocus(True) view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) layout = QtWidgets.QVBoxLayout(self) @@ -229,9 +226,9 @@ class OutputWidget(QtWidgets.QWidget): super(OutputWidget, self).__init__(parent=parent) layout = QtWidgets.QVBoxLayout(self) - show_timecode_checkbox = QtWidgets.QCheckBox("Show timestamp") + show_timecode_checkbox = QtWidgets.QCheckBox("Show timestamp", self) - output_text = QtWidgets.QTextEdit() + output_text = QtWidgets.QTextEdit(self) output_text.setReadOnly(True) # output_text.setLineWrapMode(QtWidgets.QTextEdit.FixedPixelWidth) diff --git a/openpype/modules/timers_manager/rest_api.py b/openpype/modules/timers_manager/rest_api.py index 975c1a91f9..ac8d8b7b74 100644 --- a/openpype/modules/timers_manager/rest_api.py +++ b/openpype/modules/timers_manager/rest_api.py @@ -3,6 +3,7 @@ from openpype.api import Logger log = Logger().get_logger("Event processor") + class TimersManagerModuleRestApi: """ REST API endpoint used for calling from hosts when context change @@ -22,6 +23,11 @@ class TimersManagerModuleRestApi: self.prefix + "/start_timer", self.start_timer ) + self.server_manager.add_route( + "POST", + self.prefix + "/stop_timer", + self.stop_timer + ) async def start_timer(self, request): data = await request.json() @@ -38,3 +44,7 @@ class TimersManagerModuleRestApi: self.module.stop_timers() self.module.start_timer(project_name, asset_name, task_name, hierarchy) return Response(status=200) + + async def stop_timer(self, request): + self.module.stop_timers() + return Response(status=200) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 43053c38c0..636acc0d17 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -260,6 +260,13 @@ "enabled": true } ], + "open_workfile_tool_on_startup": [ + { + "hosts": [], + "tasks": [], + "enabled": false + } + ], "sw_folders": { "compositing": [ "nuke", diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index d306eca7ef..4f6a2886bc 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -1,3 +1,4 @@ +import copy from .input_entities import InputEntity from .exceptions import EntitySchemaError from .lib import ( @@ -118,30 +119,43 @@ class HostsEnumEntity(BaseEnumEntity): implementation instead of application name. """ schema_types = ["hosts-enum"] + all_host_names = [ + "aftereffects", + "blender", + "celaction", + "fusion", + "harmony", + "hiero", + "houdini", + "maya", + "nuke", + "photoshop", + "resolve", + "tvpaint", + "unreal", + "standalonepublisher" + ] def _item_initalization(self): self.multiselection = self.schema_data.get("multiselection", True) - self.use_empty_value = self.schema_data.get( - "use_empty_value", not self.multiselection - ) + use_empty_value = False + if not self.multiselection: + use_empty_value = self.schema_data.get( + "use_empty_value", use_empty_value + ) + self.use_empty_value = use_empty_value + + hosts_filter = self.schema_data.get("hosts_filter") or [] + self.hosts_filter = hosts_filter + custom_labels = self.schema_data.get("custom_labels") or {} - host_names = [ - "aftereffects", - "blender", - "celaction", - "fusion", - "harmony", - "hiero", - "houdini", - "maya", - "nuke", - "photoshop", - "resolve", - "tvpaint", - "unreal", - "standalonepublisher" - ] + host_names = copy.deepcopy(self.all_host_names) + if hosts_filter: + for host_name in tuple(host_names): + if host_name not in hosts_filter: + host_names.remove(host_name) + if self.use_empty_value: host_names.insert(0, "") # Add default label for empty value if not available @@ -173,6 +187,44 @@ class HostsEnumEntity(BaseEnumEntity): # GUI attribute self.placeholder = self.schema_data.get("placeholder") + def schema_validations(self): + if self.hosts_filter: + enum_len = len(self.enum_items) + if ( + enum_len == 0 + or (enum_len == 1 and self.use_empty_value) + ): + joined_filters = ", ".join([ + '"{}"'.format(item) + for item in self.hosts_filter + ]) + reason = ( + "All host names were removed after applying" + " host filters. {}" + ).format(joined_filters) + raise EntitySchemaError(self, reason) + + invalid_filters = set() + for item in self.hosts_filter: + if item not in self.all_host_names: + invalid_filters.add(item) + + if invalid_filters: + joined_filters = ", ".join([ + '"{}"'.format(item) + for item in self.hosts_filter + ]) + expected_hosts = ", ".join([ + '"{}"'.format(item) + for item in self.all_host_names + ]) + self.log.warning(( + "Host filters containt invalid host names:" + " \"{}\" Expected values are {}" + ).format(joined_filters, expected_hosts)) + + super(HostsEnumEntity, self).schema_validations() + class AppsEnumEntity(BaseEnumEntity): schema_types = ["apps-enum"] diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index e5122094f6..079d16c506 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -379,6 +379,9 @@ How output of the schema could look like on save: - multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`) - it is possible to add empty value (represented with empty string) with setting `"use_empty_value"` to `True` (Default: `False`) - it is possible to set `"custom_labels"` for host names where key `""` is empty value (Default: `{}`) +- to filter host names it is required to define `"hosts_filter"` which is list of host names that will be available + - do not pass empty string if `use_empty_value` is enabled + - ignoring host names would be more dangerous in some cases ``` { "key": "host", @@ -389,7 +392,10 @@ How output of the schema could look like on save: "custom_labels": { "": "N/A", "nuke": "Nuke" - } + }, + "hosts_filter": [ + "nuke" + ] } ``` diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 8c92a45a56..9e39eeb39e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -78,7 +78,57 @@ "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "multiselection": true + "multiselection": true, + "hosts_filter": [ + "aftereffects", + "blender", + "celaction", + "fusion", + "harmony", + "hiero", + "houdini", + "maya", + "nuke", + "photoshop", + "resolve", + "tvpaint", + "unreal" + ] + }, + { + "key": "tasks", + "label": "Tasks", + "type": "list", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + } + }, + { + "type": "list", + "key": "open_workfile_tool_on_startup", + "label": "Open workfile tool on launch", + "is_group": true, + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true, + "hosts_filter": [ + "nuke" + ] }, { "key": "tasks", diff --git a/openpype/style/style.css b/openpype/style/style.css index c57b9a8da6..b955bdc2a6 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -35,6 +35,10 @@ QWidget:disabled { color: {color:font-disabled}; } +QLabel { + background: transparent; +} + /* Inputs */ QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { border: 1px solid {color:border}; @@ -97,7 +101,7 @@ QToolButton:disabled { background: {color:bg-buttons-disabled}; } -QToolButton[popupMode="1"] { +QToolButton[popupMode="1"], QToolButton[popupMode="MenuButtonPopup"] { /* make way for the popup button */ padding-right: 20px; border: 1px solid {color:bg-buttons}; @@ -340,6 +344,11 @@ QAbstractItemView { selection-background-color: transparent; } +QAbstractItemView::item { + /* `border: none` hide outline of selected item. */ + border: none; +} + QAbstractItemView:disabled{ background: {color:bg-view-disabled}; alternate-background-color: {color:bg-view-alternate-disabled}; diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 0aacd590db..8be3eddfa8 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -294,6 +294,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): msg = "

".join(warnings) dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle("Save warnings") dialog.setText(msg) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.exec_() @@ -303,6 +304,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): except Exception as exc: formatted_traceback = traceback.format_exception(*sys.exc_info()) dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle("Unexpected error") msg = "Unexpected error happened!\n\nError: {}".format(str(exc)) dialog.setText(msg) dialog.setDetailedText("\n".join(formatted_traceback)) @@ -392,6 +394,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): except Exception as exc: formatted_traceback = traceback.format_exception(*sys.exc_info()) dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle("Unexpected error") msg = "Unexpected error happened!\n\nError: {}".format(str(exc)) dialog.setText(msg) dialog.setDetailedText("\n".join(formatted_traceback)) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index a60a2a1d88..4e88301349 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -94,7 +94,8 @@ class MainWidget(QtWidgets.QWidget): super(MainWidget, self).showEvent(event) if self._reset_on_show: self._reset_on_show = False - self.reset() + # Trigger reset with 100ms delay + QtCore.QTimer.singleShot(100, self.reset) def _show_password_dialog(self): if self._password_dialog: @@ -107,6 +108,8 @@ class MainWidget(QtWidgets.QWidget): self._password_dialog = None if password_passed: self.reset() + if not self.isVisible(): + self.show() else: self.close() @@ -141,7 +144,10 @@ class MainWidget(QtWidgets.QWidget): # Don't show dialog if there are not registered slots for # `trigger_restart` signal. # - For example when settings are runnin as standalone tool - if self.receivers(self.trigger_restart) < 1: + # - PySide2 and PyQt5 compatible way how to find out + method_index = self.metaObject().indexOfMethod("trigger_restart()") + method = self.metaObject().method(method_index) + if not self.isSignalConnected(method): return dialog = RestartDialog(self) diff --git a/openpype/tools/standalonepublish/widgets/widget_component_item.py b/openpype/tools/standalonepublish/widgets/widget_component_item.py index 186c8024db..de3cde50cd 100644 --- a/openpype/tools/standalonepublish/widgets/widget_component_item.py +++ b/openpype/tools/standalonepublish/widgets/widget_component_item.py @@ -1,7 +1,6 @@ import os from Qt import QtCore, QtGui, QtWidgets from .resources import get_resource -from avalon import style class ComponentItem(QtWidgets.QFrame): @@ -61,7 +60,7 @@ class ComponentItem(QtWidgets.QFrame): name="menu", size=QtCore.QSize(22, 22) ) - self.action_menu = QtWidgets.QMenu() + self.action_menu = QtWidgets.QMenu(self.btn_action_menu) expanding_sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding @@ -229,7 +228,6 @@ class ComponentItem(QtWidgets.QFrame): if not self.btn_action_menu.isVisible(): self.btn_action_menu.setVisible(True) self.btn_action_menu.clicked.connect(self.show_actions) - self.action_menu.setStyleSheet(style.load_stylesheet()) def set_repre_name_valid(self, valid): self.has_valid_repre = valid diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index d567e26d74..42f0e422ae 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -693,16 +693,16 @@ class FilesWidget(QtWidgets.QWidget): ) return - file_path = os.path.join(self.root, work_file) + file_path = os.path.join(os.path.normpath(self.root), work_file) - pipeline.emit("before.workfile.save", file_path) + pipeline.emit("before.workfile.save", [file_path]) self._enter_session() # Make sure we are in the right session self.host.save_file(file_path) self.set_asset_task(self._asset, self._task) - pipeline.emit("after.workfile.save", file_path) + pipeline.emit("after.workfile.save", [file_path]) self.workfile_created.emit(file_path)