[Automated] Merged develop into main

This commit is contained in:
pypebot 2021-07-31 05:37:18 +02:00 committed by GitHub
commit 5faf287756
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 451 additions and 101 deletions

View file

@ -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"]

View file

@ -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:

View file

@ -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()

View file

@ -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]

View file

@ -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 = []

View file

@ -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

View file

@ -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"):

View file

@ -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)

View file

@ -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):
<description> - a verbose descriptive text for you action
<icon> - 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)

View file

@ -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())

View file

@ -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)

View file

@ -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)

View file

@ -260,6 +260,13 @@
"enabled": true
}
],
"open_workfile_tool_on_startup": [
{
"hosts": [],
"tasks": [],
"enabled": false
}
],
"sw_folders": {
"compositing": [
"nuke",

View file

@ -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"]

View file

@ -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"
]
}
```

View file

@ -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",

View file

@ -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};

View file

@ -294,6 +294,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
msg = "<br><br>".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))

View file

@ -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)

View file

@ -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

View file

@ -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)