mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #1226 from ynput/feature/107-webactions-in-launcher-tool
Launcher tool: Use webactions
This commit is contained in:
commit
b4477649b7
21 changed files with 1705 additions and 607 deletions
|
|
@ -62,6 +62,7 @@ from .execute import (
|
|||
run_subprocess,
|
||||
run_detached_process,
|
||||
run_ayon_launcher_process,
|
||||
run_detached_ayon_launcher_process,
|
||||
path_to_subprocess_arg,
|
||||
CREATE_NO_WINDOW
|
||||
)
|
||||
|
|
@ -131,6 +132,7 @@ from .ayon_info import (
|
|||
is_staging_enabled,
|
||||
is_dev_mode_enabled,
|
||||
is_in_tests,
|
||||
get_settings_variant,
|
||||
)
|
||||
|
||||
terminal = Terminal
|
||||
|
|
@ -160,6 +162,7 @@ __all__ = [
|
|||
"run_subprocess",
|
||||
"run_detached_process",
|
||||
"run_ayon_launcher_process",
|
||||
"run_detached_ayon_launcher_process",
|
||||
"path_to_subprocess_arg",
|
||||
"CREATE_NO_WINDOW",
|
||||
|
||||
|
|
@ -240,4 +243,5 @@ __all__ = [
|
|||
"is_staging_enabled",
|
||||
"is_dev_mode_enabled",
|
||||
"is_in_tests",
|
||||
"get_settings_variant",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -78,15 +78,15 @@ def is_using_ayon_console():
|
|||
return "ayon_console" in executable_filename
|
||||
|
||||
|
||||
def is_headless_mode_enabled():
|
||||
def is_headless_mode_enabled() -> bool:
|
||||
return os.getenv("AYON_HEADLESS_MODE") == "1"
|
||||
|
||||
|
||||
def is_staging_enabled():
|
||||
def is_staging_enabled() -> bool:
|
||||
return os.getenv("AYON_USE_STAGING") == "1"
|
||||
|
||||
|
||||
def is_in_tests():
|
||||
def is_in_tests() -> bool:
|
||||
"""Process is running in automatic tests mode.
|
||||
|
||||
Returns:
|
||||
|
|
@ -96,7 +96,7 @@ def is_in_tests():
|
|||
return os.environ.get("AYON_IN_TESTS") == "1"
|
||||
|
||||
|
||||
def is_dev_mode_enabled():
|
||||
def is_dev_mode_enabled() -> bool:
|
||||
"""Dev mode is enabled in AYON.
|
||||
|
||||
Returns:
|
||||
|
|
@ -106,6 +106,22 @@ def is_dev_mode_enabled():
|
|||
return os.getenv("AYON_USE_DEV") == "1"
|
||||
|
||||
|
||||
def get_settings_variant() -> str:
|
||||
"""Get AYON settings variant.
|
||||
|
||||
Returns:
|
||||
str: Settings variant.
|
||||
|
||||
"""
|
||||
if is_dev_mode_enabled():
|
||||
return os.environ["AYON_BUNDLE_NAME"]
|
||||
|
||||
if is_staging_enabled():
|
||||
return "staging"
|
||||
|
||||
return "production"
|
||||
|
||||
|
||||
def get_ayon_info():
|
||||
executable_args = get_ayon_launcher_args()
|
||||
if is_running_from_build():
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
|
@ -201,29 +202,9 @@ def clean_envs_for_ayon_process(env=None):
|
|||
return env
|
||||
|
||||
|
||||
def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs):
|
||||
"""Execute AYON process with passed arguments and wait.
|
||||
|
||||
Wrapper for 'run_process' which prepends AYON executable arguments
|
||||
before passed arguments and define environments if are not passed.
|
||||
|
||||
Values from 'os.environ' are used for environments if are not passed.
|
||||
They are cleaned using 'clean_envs_for_ayon_process' function.
|
||||
|
||||
Example:
|
||||
```
|
||||
run_ayon_process("run", "<path to .py script>")
|
||||
```
|
||||
|
||||
Args:
|
||||
*args (str): ayon-launcher cli arguments.
|
||||
**kwargs (Any): Keyword arguments for subprocess.Popen.
|
||||
|
||||
Returns:
|
||||
str: Full output of subprocess concatenated stdout and stderr.
|
||||
|
||||
"""
|
||||
args = get_ayon_launcher_args(*args)
|
||||
def _prepare_ayon_launcher_env(
|
||||
add_sys_paths: bool, kwargs: dict
|
||||
) -> dict[str, str]:
|
||||
env = kwargs.pop("env", None)
|
||||
# Keep env untouched if are passed and not empty
|
||||
if not env:
|
||||
|
|
@ -239,8 +220,7 @@ def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs):
|
|||
new_pythonpath.append(path)
|
||||
lookup_set.add(path)
|
||||
env["PYTHONPATH"] = os.pathsep.join(new_pythonpath)
|
||||
|
||||
return run_subprocess(args, env=env, **kwargs)
|
||||
return env
|
||||
|
||||
|
||||
def run_detached_process(args, **kwargs):
|
||||
|
|
@ -314,6 +294,67 @@ def run_detached_process(args, **kwargs):
|
|||
return process
|
||||
|
||||
|
||||
def run_ayon_launcher_process(
|
||||
*args, add_sys_paths: bool = False, **kwargs
|
||||
) -> str:
|
||||
"""Execute AYON process with passed arguments and wait.
|
||||
|
||||
Wrapper for 'run_process' which prepends AYON executable arguments
|
||||
before passed arguments and define environments if are not passed.
|
||||
|
||||
Values from 'os.environ' are used for environments if are not passed.
|
||||
They are cleaned using 'clean_envs_for_ayon_process' function.
|
||||
|
||||
Example:
|
||||
```
|
||||
run_ayon_launcher_process("run", "<path to .py script>")
|
||||
```
|
||||
|
||||
Args:
|
||||
*args (str): ayon-launcher cli arguments.
|
||||
add_sys_paths (bool): Add system paths to PYTHONPATH.
|
||||
**kwargs (Any): Keyword arguments for subprocess.Popen.
|
||||
|
||||
Returns:
|
||||
str: Full output of subprocess concatenated stdout and stderr.
|
||||
|
||||
"""
|
||||
args = get_ayon_launcher_args(*args)
|
||||
env = _prepare_ayon_launcher_env(add_sys_paths, kwargs)
|
||||
return run_subprocess(args, env=env, **kwargs)
|
||||
|
||||
|
||||
def run_detached_ayon_launcher_process(
|
||||
*args, add_sys_paths: bool = False, **kwargs
|
||||
) -> subprocess.Popen:
|
||||
"""Execute AYON process with passed arguments and wait.
|
||||
|
||||
Wrapper for 'run_process' which prepends AYON executable arguments
|
||||
before passed arguments and define environments if are not passed.
|
||||
|
||||
Values from 'os.environ' are used for environments if are not passed.
|
||||
They are cleaned using 'clean_envs_for_ayon_process' function.
|
||||
|
||||
Example:
|
||||
```
|
||||
run_detached_ayon_launcher_process("run", "<path to .py script>")
|
||||
```
|
||||
|
||||
Args:
|
||||
*args (str): ayon-launcher cli arguments.
|
||||
add_sys_paths (bool): Add system paths to PYTHONPATH.
|
||||
**kwargs (Any): Keyword arguments for subprocess.Popen.
|
||||
|
||||
Returns:
|
||||
subprocess.Popen: Pointer to launched process but it is possible that
|
||||
launched process is already killed (on linux).
|
||||
|
||||
"""
|
||||
args = get_ayon_launcher_args(*args)
|
||||
env = _prepare_ayon_launcher_env(add_sys_paths, kwargs)
|
||||
return run_detached_process(args, env=env, **kwargs)
|
||||
|
||||
|
||||
def path_to_subprocess_arg(path):
|
||||
"""Prepare path for subprocess arguments.
|
||||
|
||||
|
|
|
|||
|
|
@ -56,14 +56,9 @@ class _AyonSettingsCache:
|
|||
@classmethod
|
||||
def _get_variant(cls):
|
||||
if _AyonSettingsCache.variant is None:
|
||||
from ayon_core.lib import is_staging_enabled, is_dev_mode_enabled
|
||||
|
||||
variant = "production"
|
||||
if is_dev_mode_enabled():
|
||||
variant = cls._get_bundle_name()
|
||||
elif is_staging_enabled():
|
||||
variant = "staging"
|
||||
from ayon_core.lib import get_settings_variant
|
||||
|
||||
variant = get_settings_variant()
|
||||
# Cache variant
|
||||
_AyonSettingsCache.variant = variant
|
||||
|
||||
|
|
|
|||
|
|
@ -829,6 +829,37 @@ HintedLineEditButton {
|
|||
}
|
||||
|
||||
/* Launcher specific stylesheets */
|
||||
ActionsView[mode="icon"] {
|
||||
/* font size can't be set on items */
|
||||
font-size: 9pt;
|
||||
border: 0px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
ActionsView[mode="icon"]::item {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 4px;
|
||||
border: 0px;
|
||||
border-radius: 0.3em;
|
||||
}
|
||||
|
||||
ActionsView[mode="icon"]::item:hover {
|
||||
color: {color:font-hover};
|
||||
background: #424A57;
|
||||
}
|
||||
|
||||
ActionsView[mode="icon"]::icon {}
|
||||
|
||||
ActionMenuPopup #Wrapper {
|
||||
border-radius: 0.3em;
|
||||
background: #353B46;
|
||||
}
|
||||
ActionMenuPopup ActionsView[mode="icon"] {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#IconView[mode="icon"] {
|
||||
/* font size can't be set on items */
|
||||
font-size: 9pt;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,58 @@
|
|||
from qtpy import QtWidgets
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from qtpy import QtWidgets, QtGui
|
||||
|
||||
from ayon_core.style import load_stylesheet
|
||||
from ayon_core.resources import get_ayon_icon_filepath
|
||||
from ayon_core.lib import AbstractAttrDef
|
||||
|
||||
from .widgets import AttributeDefinitionsWidget
|
||||
|
||||
|
||||
class AttributeDefinitionsDialog(QtWidgets.QDialog):
|
||||
def __init__(self, attr_defs, parent=None):
|
||||
super(AttributeDefinitionsDialog, self).__init__(parent)
|
||||
def __init__(
|
||||
self,
|
||||
attr_defs: list[AbstractAttrDef],
|
||||
title: Optional[str] = None,
|
||||
submit_label: Optional[str] = None,
|
||||
cancel_label: Optional[str] = None,
|
||||
submit_icon: Optional[QtGui.QIcon] = None,
|
||||
cancel_icon: Optional[QtGui.QIcon] = None,
|
||||
parent: Optional[QtWidgets.QWidget] = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
|
||||
if title:
|
||||
self.setWindowTitle(title)
|
||||
|
||||
icon = QtGui.QIcon(get_ayon_icon_filepath())
|
||||
self.setWindowIcon(icon)
|
||||
self.setStyleSheet(load_stylesheet())
|
||||
|
||||
attrs_widget = AttributeDefinitionsWidget(attr_defs, self)
|
||||
|
||||
if submit_label is None:
|
||||
submit_label = "OK"
|
||||
|
||||
if cancel_label is None:
|
||||
cancel_label = "Cancel"
|
||||
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
ok_btn = QtWidgets.QPushButton("OK", btns_widget)
|
||||
cancel_btn = QtWidgets.QPushButton("Cancel", btns_widget)
|
||||
cancel_btn = QtWidgets.QPushButton(cancel_label, btns_widget)
|
||||
submit_btn = QtWidgets.QPushButton(submit_label, btns_widget)
|
||||
|
||||
if submit_icon is not None:
|
||||
submit_btn.setIcon(submit_icon)
|
||||
|
||||
if cancel_icon is not None:
|
||||
cancel_btn.setIcon(cancel_icon)
|
||||
|
||||
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||
btns_layout.setContentsMargins(0, 0, 0, 0)
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(ok_btn, 0)
|
||||
btns_layout.addWidget(submit_btn, 0)
|
||||
btns_layout.addWidget(cancel_btn, 0)
|
||||
|
||||
main_layout = QtWidgets.QVBoxLayout(self)
|
||||
|
|
@ -24,10 +60,33 @@ class AttributeDefinitionsDialog(QtWidgets.QDialog):
|
|||
main_layout.addStretch(1)
|
||||
main_layout.addWidget(btns_widget, 0)
|
||||
|
||||
ok_btn.clicked.connect(self.accept)
|
||||
submit_btn.clicked.connect(self.accept)
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
|
||||
self._attrs_widget = attrs_widget
|
||||
self._submit_btn = submit_btn
|
||||
self._cancel_btn = cancel_btn
|
||||
|
||||
def get_values(self):
|
||||
return self._attrs_widget.current_value()
|
||||
|
||||
def set_values(self, values):
|
||||
self._attrs_widget.set_value(values)
|
||||
|
||||
def set_submit_label(self, text: str):
|
||||
self._submit_btn.setText(text)
|
||||
|
||||
def set_submit_icon(self, icon: QtGui.QIcon):
|
||||
self._submit_btn.setIcon(icon)
|
||||
|
||||
def set_submit_visible(self, visible: bool):
|
||||
self._submit_btn.setVisible(visible)
|
||||
|
||||
def set_cancel_label(self, text: str):
|
||||
self._cancel_btn.setText(text)
|
||||
|
||||
def set_cancel_icon(self, icon: QtGui.QIcon):
|
||||
self._cancel_btn.setIcon(icon)
|
||||
|
||||
def set_cancel_visible(self, visible: bool):
|
||||
self._cancel_btn.setVisible(visible)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ from ayon_core.tools.utils import (
|
|||
FocusSpinBox,
|
||||
FocusDoubleSpinBox,
|
||||
MultiSelectionComboBox,
|
||||
MarkdownLabel,
|
||||
PlaceholderLineEdit,
|
||||
PlaceholderPlainTextEdit,
|
||||
set_style_property,
|
||||
|
|
@ -247,12 +248,10 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
|
|||
|
||||
def set_value(self, value):
|
||||
new_value = copy.deepcopy(value)
|
||||
unused_keys = set(new_value.keys())
|
||||
for widget in self._widgets_by_id.values():
|
||||
attr_def = widget.attr_def
|
||||
if attr_def.key not in new_value:
|
||||
continue
|
||||
unused_keys.remove(attr_def.key)
|
||||
|
||||
widget_value = new_value[attr_def.key]
|
||||
if widget_value is None:
|
||||
|
|
@ -350,7 +349,7 @@ class SeparatorAttrWidget(_BaseAttrDefWidget):
|
|||
|
||||
class LabelAttrWidget(_BaseAttrDefWidget):
|
||||
def _ui_init(self):
|
||||
input_widget = QtWidgets.QLabel(self)
|
||||
input_widget = MarkdownLabel(self)
|
||||
label = self.attr_def.label
|
||||
if label:
|
||||
input_widget.setText(str(label))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,59 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Any
|
||||
|
||||
from ayon_core.tools.common_models import (
|
||||
ProjectItem,
|
||||
FolderItem,
|
||||
FolderTypeItem,
|
||||
TaskItem,
|
||||
TaskTypeItem,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WebactionContext:
|
||||
"""Context used for methods related to webactions."""
|
||||
identifier: str
|
||||
project_name: str
|
||||
folder_id: str
|
||||
task_id: str
|
||||
addon_name: str
|
||||
addon_version: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ActionItem:
|
||||
"""Item representing single action to trigger.
|
||||
|
||||
Attributes:
|
||||
action_type (Literal["webaction", "local"]): Type of action.
|
||||
identifier (str): Unique identifier of action item.
|
||||
order (int): Action ordering.
|
||||
label (str): Action label.
|
||||
variant_label (Union[str, None]): Variant label, full label is
|
||||
concatenated with space. Actions are grouped under single
|
||||
action if it has same 'label' and have set 'variant_label'.
|
||||
full_label (str): Full label, if not set it is generated
|
||||
from 'label' and 'variant_label'.
|
||||
icon (dict[str, str]): Icon definition.
|
||||
addon_name (Optional[str]): Addon name.
|
||||
addon_version (Optional[str]): Addon version.
|
||||
config_fields (list[dict]): Config fields for webaction.
|
||||
|
||||
"""
|
||||
action_type: str
|
||||
identifier: str
|
||||
order: int
|
||||
label: str
|
||||
variant_label: Optional[str]
|
||||
full_label: str
|
||||
icon: Optional[dict[str, str]]
|
||||
config_fields: list[dict]
|
||||
addon_name: Optional[str] = None
|
||||
addon_version: Optional[str] = None
|
||||
|
||||
|
||||
class AbstractLauncherCommon(ABC):
|
||||
|
|
@ -88,7 +143,9 @@ class AbstractLauncherBackend(AbstractLauncherCommon):
|
|||
class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
||||
# Entity items for UI
|
||||
@abstractmethod
|
||||
def get_project_items(self, sender=None):
|
||||
def get_project_items(
|
||||
self, sender: Optional[str] = None
|
||||
) -> list[ProjectItem]:
|
||||
"""Project items for all projects.
|
||||
|
||||
This function may trigger events 'projects.refresh.started' and
|
||||
|
|
@ -106,7 +163,9 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder_type_items(self, project_name, sender=None):
|
||||
def get_folder_type_items(
|
||||
self, project_name: str, sender: Optional[str] = None
|
||||
) -> list[FolderTypeItem]:
|
||||
"""Folder type items for a project.
|
||||
|
||||
This function may trigger events with topics
|
||||
|
|
@ -126,7 +185,9 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_type_items(self, project_name, sender=None):
|
||||
def get_task_type_items(
|
||||
self, project_name: str, sender: Optional[str] = None
|
||||
) -> list[TaskTypeItem]:
|
||||
"""Task type items for a project.
|
||||
|
||||
This function may trigger events with topics
|
||||
|
|
@ -146,7 +207,9 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_folder_items(self, project_name, sender=None):
|
||||
def get_folder_items(
|
||||
self, project_name: str, sender: Optional[str] = None
|
||||
) -> list[FolderItem]:
|
||||
"""Folder items to visualize project hierarchy.
|
||||
|
||||
This function may trigger events 'folders.refresh.started' and
|
||||
|
|
@ -165,7 +228,9 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_task_items(self, project_name, folder_id, sender=None):
|
||||
def get_task_items(
|
||||
self, project_name: str, folder_id: str, sender: Optional[str] = None
|
||||
) -> list[TaskItem]:
|
||||
"""Task items.
|
||||
|
||||
This function may trigger events 'tasks.refresh.started' and
|
||||
|
|
@ -185,7 +250,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_project_name(self):
|
||||
def get_selected_project_name(self) -> Optional[str]:
|
||||
"""Selected project name.
|
||||
|
||||
Returns:
|
||||
|
|
@ -195,7 +260,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_folder_id(self):
|
||||
def get_selected_folder_id(self) -> Optional[str]:
|
||||
"""Selected folder id.
|
||||
|
||||
Returns:
|
||||
|
|
@ -205,7 +270,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_task_id(self):
|
||||
def get_selected_task_id(self) -> Optional[str]:
|
||||
"""Selected task id.
|
||||
|
||||
Returns:
|
||||
|
|
@ -215,7 +280,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_task_name(self):
|
||||
def get_selected_task_name(self) -> Optional[str]:
|
||||
"""Selected task name.
|
||||
|
||||
Returns:
|
||||
|
|
@ -225,7 +290,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_selected_context(self):
|
||||
def get_selected_context(self) -> dict[str, Optional[str]]:
|
||||
"""Get whole selected context.
|
||||
|
||||
Example:
|
||||
|
|
@ -243,7 +308,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_project(self, project_name):
|
||||
def set_selected_project(self, project_name: Optional[str]):
|
||||
"""Change selected folder.
|
||||
|
||||
Args:
|
||||
|
|
@ -254,7 +319,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_folder(self, folder_id):
|
||||
def set_selected_folder(self, folder_id: Optional[str]):
|
||||
"""Change selected folder.
|
||||
|
||||
Args:
|
||||
|
|
@ -265,7 +330,9 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_selected_task(self, task_id, task_name):
|
||||
def set_selected_task(
|
||||
self, task_id: Optional[str], task_name: Optional[str]
|
||||
):
|
||||
"""Change selected task.
|
||||
|
||||
Args:
|
||||
|
|
@ -279,7 +346,12 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
|
||||
# Actions
|
||||
@abstractmethod
|
||||
def get_action_items(self, project_name, folder_id, task_id):
|
||||
def get_action_items(
|
||||
self,
|
||||
project_name: Optional[str],
|
||||
folder_id: Optional[str],
|
||||
task_id: Optional[str],
|
||||
) -> list[ActionItem]:
|
||||
"""Get action items for given context.
|
||||
|
||||
Args:
|
||||
|
|
@ -295,30 +367,67 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def trigger_action(self, project_name, folder_id, task_id, action_id):
|
||||
def trigger_action(
|
||||
self,
|
||||
action_id: str,
|
||||
project_name: Optional[str],
|
||||
folder_id: Optional[str],
|
||||
task_id: Optional[str],
|
||||
):
|
||||
"""Trigger action on given context.
|
||||
|
||||
Args:
|
||||
action_id (str): Action identifier.
|
||||
project_name (Union[str, None]): Project name.
|
||||
folder_id (Union[str, None]): Folder id.
|
||||
task_id (Union[str, None]): Task id.
|
||||
action_id (str): Action identifier.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_application_force_not_open_workfile(
|
||||
self, project_name, folder_id, task_id, action_ids, enabled
|
||||
def trigger_webaction(
|
||||
self,
|
||||
context: WebactionContext,
|
||||
action_label: str,
|
||||
form_data: Optional[dict[str, Any]] = None,
|
||||
):
|
||||
"""This is application action related to force not open last workfile.
|
||||
"""Trigger action on the given context.
|
||||
|
||||
Args:
|
||||
project_name (Union[str, None]): Project name.
|
||||
folder_id (Union[str, None]): Folder id.
|
||||
task_id (Union[str, None]): Task id.
|
||||
action_ids (Iterable[str]): Action identifiers.
|
||||
enabled (bool): New value of force not open workfile.
|
||||
context (WebactionContext): Webaction context.
|
||||
action_label (str): Action label.
|
||||
form_data (Optional[dict[str, Any]]): Form values of action.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_action_config_values(
|
||||
self, context: WebactionContext
|
||||
) -> dict[str, Any]:
|
||||
"""Get action config values.
|
||||
|
||||
Args:
|
||||
context (WebactionContext): Webaction context.
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: Action config values.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def set_action_config_values(
|
||||
self,
|
||||
context: WebactionContext,
|
||||
values: dict[str, Any],
|
||||
):
|
||||
"""Set action config values.
|
||||
|
||||
Args:
|
||||
context (WebactionContext): Webaction context.
|
||||
values (dict[str, Any]): Action config values.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
@ -343,14 +452,16 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_my_tasks_entity_ids(self, project_name: str):
|
||||
def get_my_tasks_entity_ids(
|
||||
self, project_name: str
|
||||
) -> dict[str, list[str]]:
|
||||
"""Get entity ids for my tasks.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
|
||||
Returns:
|
||||
dict[str, Union[list[str]]]: Folder and task ids.
|
||||
dict[str, list[str]]: Folder and task ids.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class BaseLauncherController(
|
|||
|
||||
@property
|
||||
def event_system(self):
|
||||
"""Inner event system for workfiles tool controller.
|
||||
"""Inner event system for launcher tool controller.
|
||||
|
||||
Is used for communication with UI. Event system is created on demand.
|
||||
|
||||
|
|
@ -135,16 +135,30 @@ class BaseLauncherController(
|
|||
return self._actions_model.get_action_items(
|
||||
project_name, folder_id, task_id)
|
||||
|
||||
def set_application_force_not_open_workfile(
|
||||
self, project_name, folder_id, task_id, action_ids, enabled
|
||||
def trigger_action(
|
||||
self,
|
||||
identifier,
|
||||
project_name,
|
||||
folder_id,
|
||||
task_id,
|
||||
):
|
||||
self._actions_model.set_application_force_not_open_workfile(
|
||||
project_name, folder_id, task_id, action_ids, enabled
|
||||
self._actions_model.trigger_action(
|
||||
identifier,
|
||||
project_name,
|
||||
folder_id,
|
||||
task_id,
|
||||
)
|
||||
|
||||
def trigger_action(self, project_name, folder_id, task_id, identifier):
|
||||
self._actions_model.trigger_action(
|
||||
project_name, folder_id, task_id, identifier)
|
||||
def trigger_webaction(self, context, action_label, form_data=None):
|
||||
self._actions_model.trigger_webaction(
|
||||
context, action_label, form_data
|
||||
)
|
||||
|
||||
def get_action_config_values(self, context):
|
||||
return self._actions_model.get_action_config_values(context)
|
||||
|
||||
def set_action_config_values(self, context, values):
|
||||
return self._actions_model.set_action_config_values(context, values)
|
||||
|
||||
# General methods
|
||||
def refresh(self):
|
||||
|
|
|
|||
|
|
@ -1,219 +1,47 @@
|
|||
import os
|
||||
import uuid
|
||||
from dataclasses import dataclass, asdict
|
||||
from urllib.parse import urlencode, urlparse
|
||||
from typing import Any, Optional
|
||||
import webbrowser
|
||||
|
||||
import ayon_api
|
||||
|
||||
from ayon_core import resources
|
||||
from ayon_core.lib import Logger, AYONSettingsRegistry
|
||||
from ayon_core.lib import (
|
||||
Logger,
|
||||
NestedCacheItem,
|
||||
CacheItem,
|
||||
get_settings_variant,
|
||||
run_detached_ayon_launcher_process,
|
||||
)
|
||||
from ayon_core.addon import AddonsManager
|
||||
from ayon_core.pipeline.actions import (
|
||||
discover_launcher_actions,
|
||||
LauncherAction,
|
||||
LauncherActionSelection,
|
||||
register_launcher_action_path,
|
||||
)
|
||||
from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch
|
||||
|
||||
try:
|
||||
# Available since applications addon 0.2.4
|
||||
from ayon_applications.action import ApplicationAction
|
||||
except ImportError:
|
||||
# Backwards compatibility from 0.3.3 (24/06/10)
|
||||
# TODO: Remove in future releases
|
||||
class ApplicationAction(LauncherAction):
|
||||
"""Action to launch an application.
|
||||
|
||||
Application action based on 'ApplicationManager' system.
|
||||
|
||||
Handling of applications in launcher is not ideal and should be
|
||||
completely redone from scratch. This is just a temporary solution
|
||||
to keep backwards compatibility with AYON launcher.
|
||||
|
||||
Todos:
|
||||
Move handling of errors to frontend.
|
||||
"""
|
||||
|
||||
# Application object
|
||||
application = None
|
||||
# Action attributes
|
||||
name = None
|
||||
label = None
|
||||
label_variant = None
|
||||
group = None
|
||||
icon = None
|
||||
color = None
|
||||
order = 0
|
||||
data = {}
|
||||
project_settings = {}
|
||||
project_entities = {}
|
||||
|
||||
_log = None
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = Logger.get_logger(self.__class__.__name__)
|
||||
return self._log
|
||||
|
||||
def is_compatible(self, selection):
|
||||
if not selection.is_task_selected:
|
||||
return False
|
||||
|
||||
project_entity = self.project_entities[selection.project_name]
|
||||
apps = project_entity["attrib"].get("applications")
|
||||
if not apps or self.application.full_name not in apps:
|
||||
return False
|
||||
|
||||
project_settings = self.project_settings[selection.project_name]
|
||||
only_available = project_settings["applications"]["only_available"]
|
||||
if only_available and not self.application.find_executable():
|
||||
return False
|
||||
return True
|
||||
|
||||
def _show_message_box(self, title, message, details=None):
|
||||
from qtpy import QtWidgets, QtGui
|
||||
from ayon_core import style
|
||||
|
||||
dialog = QtWidgets.QMessageBox()
|
||||
icon = QtGui.QIcon(resources.get_ayon_icon_filepath())
|
||||
dialog.setWindowIcon(icon)
|
||||
dialog.setStyleSheet(style.load_stylesheet())
|
||||
dialog.setWindowTitle(title)
|
||||
dialog.setText(message)
|
||||
if details:
|
||||
dialog.setDetailedText(details)
|
||||
dialog.exec_()
|
||||
|
||||
def process(self, selection, **kwargs):
|
||||
"""Process the full Application action"""
|
||||
|
||||
from ayon_applications import (
|
||||
ApplicationExecutableNotFound,
|
||||
ApplicationLaunchFailed,
|
||||
)
|
||||
|
||||
try:
|
||||
self.application.launch(
|
||||
project_name=selection.project_name,
|
||||
folder_path=selection.folder_path,
|
||||
task_name=selection.task_name,
|
||||
**self.data
|
||||
)
|
||||
|
||||
except ApplicationExecutableNotFound as exc:
|
||||
details = exc.details
|
||||
msg = exc.msg
|
||||
log_msg = str(msg)
|
||||
if details:
|
||||
log_msg += "\n" + details
|
||||
self.log.warning(log_msg)
|
||||
self._show_message_box(
|
||||
"Application executable not found", msg, details
|
||||
)
|
||||
|
||||
except ApplicationLaunchFailed as exc:
|
||||
msg = str(exc)
|
||||
self.log.warning(msg, exc_info=True)
|
||||
self._show_message_box("Application launch failed", msg)
|
||||
from ayon_core.tools.launcher.abstract import ActionItem, WebactionContext
|
||||
|
||||
|
||||
# class Action:
|
||||
# def __init__(self, label, icon=None, identifier=None):
|
||||
# self._label = label
|
||||
# self._icon = icon
|
||||
# self._callbacks = []
|
||||
# self._identifier = identifier or uuid.uuid4().hex
|
||||
# self._checked = True
|
||||
# self._checkable = False
|
||||
#
|
||||
# def set_checked(self, checked):
|
||||
# self._checked = checked
|
||||
#
|
||||
# def set_checkable(self, checkable):
|
||||
# self._checkable = checkable
|
||||
#
|
||||
# def set_label(self, label):
|
||||
# self._label = label
|
||||
#
|
||||
# def add_callback(self, callback):
|
||||
# self._callbacks = callback
|
||||
#
|
||||
#
|
||||
# class Menu:
|
||||
# def __init__(self, label, icon=None):
|
||||
# self.label = label
|
||||
# self.icon = icon
|
||||
# self._actions = []
|
||||
#
|
||||
# def add_action(self, action):
|
||||
# self._actions.append(action)
|
||||
@dataclass
|
||||
class WebactionForm:
|
||||
fields: list[dict[str, Any]]
|
||||
title: str
|
||||
submit_label: str
|
||||
submit_icon: str
|
||||
cancel_label: str
|
||||
cancel_icon: str
|
||||
|
||||
|
||||
class ActionItem:
|
||||
"""Item representing single action to trigger.
|
||||
|
||||
Todos:
|
||||
Get rid of application specific logic.
|
||||
|
||||
Args:
|
||||
identifier (str): Unique identifier of action item.
|
||||
label (str): Action label.
|
||||
variant_label (Union[str, None]): Variant label, full label is
|
||||
concatenated with space. Actions are grouped under single
|
||||
action if it has same 'label' and have set 'variant_label'.
|
||||
icon (dict[str, str]): Icon definition.
|
||||
order (int): Action ordering.
|
||||
is_application (bool): Is action application action.
|
||||
force_not_open_workfile (bool): Force not open workfile. Application
|
||||
related.
|
||||
full_label (Optional[str]): Full label, if not set it is generated
|
||||
from 'label' and 'variant_label'.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
identifier,
|
||||
label,
|
||||
variant_label,
|
||||
icon,
|
||||
order,
|
||||
is_application,
|
||||
force_not_open_workfile,
|
||||
full_label=None
|
||||
):
|
||||
self.identifier = identifier
|
||||
self.label = label
|
||||
self.variant_label = variant_label
|
||||
self.icon = icon
|
||||
self.order = order
|
||||
self.is_application = is_application
|
||||
self.force_not_open_workfile = force_not_open_workfile
|
||||
self._full_label = full_label
|
||||
|
||||
def copy(self):
|
||||
return self.from_data(self.to_data())
|
||||
|
||||
@property
|
||||
def full_label(self):
|
||||
if self._full_label is None:
|
||||
if self.variant_label:
|
||||
self._full_label = " ".join([self.label, self.variant_label])
|
||||
else:
|
||||
self._full_label = self.label
|
||||
return self._full_label
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"identifier": self.identifier,
|
||||
"label": self.label,
|
||||
"variant_label": self.variant_label,
|
||||
"icon": self.icon,
|
||||
"order": self.order,
|
||||
"is_application": self.is_application,
|
||||
"force_not_open_workfile": self.force_not_open_workfile,
|
||||
"full_label": self._full_label,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
return cls(**data)
|
||||
@dataclass
|
||||
class WebactionResponse:
|
||||
response_type: str
|
||||
success: bool
|
||||
message: Optional[str] = None
|
||||
clipboard_text: Optional[str] = None
|
||||
form: Optional[WebactionForm] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
def get_action_icon(action):
|
||||
|
|
@ -264,8 +92,6 @@ class ActionsModel:
|
|||
controller (AbstractLauncherBackend): Controller instance.
|
||||
"""
|
||||
|
||||
_not_open_workfile_reg_key = "force_not_open_workfile"
|
||||
|
||||
def __init__(self, controller):
|
||||
self._controller = controller
|
||||
|
||||
|
|
@ -274,11 +100,21 @@ class ActionsModel:
|
|||
self._discovered_actions = None
|
||||
self._actions = None
|
||||
self._action_items = {}
|
||||
|
||||
self._launcher_tool_reg = AYONSettingsRegistry("launcher_tool")
|
||||
self._webaction_items = NestedCacheItem(
|
||||
levels=2, default_factory=list, lifetime=20,
|
||||
)
|
||||
|
||||
self._addons_manager = None
|
||||
|
||||
self._variant = get_settings_variant()
|
||||
|
||||
@staticmethod
|
||||
def calculate_full_label(label: str, variant_label: Optional[str]) -> str:
|
||||
"""Calculate full label from label and variant_label."""
|
||||
if variant_label:
|
||||
return " ".join([label, variant_label])
|
||||
return label
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
|
|
@ -289,39 +125,12 @@ class ActionsModel:
|
|||
self._discovered_actions = None
|
||||
self._actions = None
|
||||
self._action_items = {}
|
||||
self._webaction_items.reset()
|
||||
|
||||
self._controller.emit_event("actions.refresh.started")
|
||||
self._get_action_objects()
|
||||
self._controller.emit_event("actions.refresh.finished")
|
||||
|
||||
def _should_start_last_workfile(
|
||||
self,
|
||||
project_name,
|
||||
task_id,
|
||||
identifier,
|
||||
host_name,
|
||||
not_open_workfile_actions
|
||||
):
|
||||
if identifier in not_open_workfile_actions:
|
||||
return not not_open_workfile_actions[identifier]
|
||||
|
||||
task_name = None
|
||||
task_type = None
|
||||
if task_id is not None:
|
||||
task_entity = self._controller.get_task_entity(
|
||||
project_name, task_id
|
||||
)
|
||||
task_name = task_entity["name"]
|
||||
task_type = task_entity["taskType"]
|
||||
|
||||
output = should_use_last_workfile_on_launch(
|
||||
project_name,
|
||||
host_name,
|
||||
task_name,
|
||||
task_type
|
||||
)
|
||||
return output
|
||||
|
||||
def get_action_items(self, project_name, folder_id, task_id):
|
||||
"""Get actions for project.
|
||||
|
||||
|
|
@ -332,53 +141,31 @@ class ActionsModel:
|
|||
|
||||
Returns:
|
||||
list[ActionItem]: List of actions.
|
||||
|
||||
"""
|
||||
not_open_workfile_actions = self._get_no_last_workfile_for_context(
|
||||
project_name, folder_id, task_id)
|
||||
selection = self._prepare_selection(project_name, folder_id, task_id)
|
||||
output = []
|
||||
action_items = self._get_action_items(project_name)
|
||||
for identifier, action in self._get_action_objects().items():
|
||||
if not action.is_compatible(selection):
|
||||
continue
|
||||
if action.is_compatible(selection):
|
||||
output.append(action_items[identifier])
|
||||
output.extend(self._get_webactions(selection))
|
||||
|
||||
action_item = action_items[identifier]
|
||||
# Handling of 'force_not_open_workfile' for applications
|
||||
if action_item.is_application:
|
||||
action_item = action_item.copy()
|
||||
start_last_workfile = self._should_start_last_workfile(
|
||||
project_name,
|
||||
task_id,
|
||||
identifier,
|
||||
action.application.host_name,
|
||||
not_open_workfile_actions
|
||||
)
|
||||
action_item.force_not_open_workfile = (
|
||||
not start_last_workfile
|
||||
)
|
||||
|
||||
output.append(action_item)
|
||||
return output
|
||||
|
||||
def set_application_force_not_open_workfile(
|
||||
self, project_name, folder_id, task_id, action_ids, enabled
|
||||
def trigger_action(
|
||||
self,
|
||||
identifier,
|
||||
project_name,
|
||||
folder_id,
|
||||
task_id,
|
||||
):
|
||||
no_workfile_reg_data = self._get_no_last_workfile_reg_data()
|
||||
project_data = no_workfile_reg_data.setdefault(project_name, {})
|
||||
folder_data = project_data.setdefault(folder_id, {})
|
||||
task_data = folder_data.setdefault(task_id, {})
|
||||
for action_id in action_ids:
|
||||
task_data[action_id] = enabled
|
||||
self._launcher_tool_reg.set_item(
|
||||
self._not_open_workfile_reg_key, no_workfile_reg_data
|
||||
)
|
||||
|
||||
def trigger_action(self, project_name, folder_id, task_id, identifier):
|
||||
selection = self._prepare_selection(project_name, folder_id, task_id)
|
||||
failed = False
|
||||
error_message = None
|
||||
action_label = identifier
|
||||
action_items = self._get_action_items(project_name)
|
||||
trigger_id = uuid.uuid4().hex
|
||||
try:
|
||||
action = self._actions[identifier]
|
||||
action_item = action_items[identifier]
|
||||
|
|
@ -386,22 +173,11 @@ class ActionsModel:
|
|||
self._controller.emit_event(
|
||||
"action.trigger.started",
|
||||
{
|
||||
"trigger_id": trigger_id,
|
||||
"identifier": identifier,
|
||||
"full_label": action_label,
|
||||
}
|
||||
)
|
||||
if isinstance(action, ApplicationAction):
|
||||
per_action = self._get_no_last_workfile_for_context(
|
||||
project_name, folder_id, task_id
|
||||
)
|
||||
start_last_workfile = self._should_start_last_workfile(
|
||||
project_name,
|
||||
task_id,
|
||||
identifier,
|
||||
action.application.host_name,
|
||||
per_action
|
||||
)
|
||||
action.data["start_last_workfile"] = start_last_workfile
|
||||
|
||||
action.process(selection)
|
||||
except Exception as exc:
|
||||
|
|
@ -412,6 +188,7 @@ class ActionsModel:
|
|||
self._controller.emit_event(
|
||||
"action.trigger.finished",
|
||||
{
|
||||
"trigger_id": trigger_id,
|
||||
"identifier": identifier,
|
||||
"failed": failed,
|
||||
"error_message": error_message,
|
||||
|
|
@ -419,32 +196,148 @@ class ActionsModel:
|
|||
}
|
||||
)
|
||||
|
||||
def trigger_webaction(self, context, action_label, form_data):
|
||||
entity_type = None
|
||||
entity_ids = []
|
||||
identifier = context.identifier
|
||||
folder_id = context.folder_id
|
||||
task_id = context.task_id
|
||||
project_name = context.project_name
|
||||
addon_name = context.addon_name
|
||||
addon_version = context.addon_version
|
||||
|
||||
if task_id:
|
||||
entity_type = "task"
|
||||
entity_ids.append(task_id)
|
||||
elif folder_id:
|
||||
entity_type = "folder"
|
||||
entity_ids.append(folder_id)
|
||||
|
||||
query = {
|
||||
"addonName": addon_name,
|
||||
"addonVersion": addon_version,
|
||||
"identifier": identifier,
|
||||
"variant": self._variant,
|
||||
}
|
||||
url = f"actions/execute?{urlencode(query)}"
|
||||
request_data = {
|
||||
"projectName": project_name,
|
||||
"entityType": entity_type,
|
||||
"entityIds": entity_ids,
|
||||
}
|
||||
if form_data is not None:
|
||||
request_data["formData"] = form_data
|
||||
|
||||
trigger_id = uuid.uuid4().hex
|
||||
failed = False
|
||||
try:
|
||||
self._controller.emit_event(
|
||||
"webaction.trigger.started",
|
||||
{
|
||||
"trigger_id": trigger_id,
|
||||
"identifier": identifier,
|
||||
"full_label": action_label,
|
||||
}
|
||||
)
|
||||
|
||||
conn = ayon_api.get_server_api_connection()
|
||||
# Add 'referer' header to the request
|
||||
# - ayon-api 1.1.1 adds the value to the header automatically
|
||||
headers = conn.get_headers()
|
||||
if "referer" in headers:
|
||||
headers = None
|
||||
else:
|
||||
headers["referer"] = conn.get_base_url()
|
||||
response = ayon_api.raw_post(
|
||||
url, headers=headers, json=request_data
|
||||
)
|
||||
response.raise_for_status()
|
||||
handle_response = self._handle_webaction_response(response.data)
|
||||
|
||||
except Exception:
|
||||
failed = True
|
||||
self.log.warning("Action trigger failed.", exc_info=True)
|
||||
handle_response = WebactionResponse(
|
||||
"unknown",
|
||||
False,
|
||||
error_message="Failed to trigger webaction.",
|
||||
)
|
||||
|
||||
data = asdict(handle_response)
|
||||
data.update({
|
||||
"trigger_failed": failed,
|
||||
"trigger_id": trigger_id,
|
||||
"identifier": identifier,
|
||||
"full_label": action_label,
|
||||
"project_name": project_name,
|
||||
"folder_id": folder_id,
|
||||
"task_id": task_id,
|
||||
"addon_name": addon_name,
|
||||
"addon_version": addon_version,
|
||||
})
|
||||
self._controller.emit_event(
|
||||
"webaction.trigger.finished",
|
||||
data,
|
||||
)
|
||||
|
||||
def get_action_config_values(self, context: WebactionContext):
|
||||
selection = self._prepare_selection(
|
||||
context.project_name, context.folder_id, context.task_id
|
||||
)
|
||||
if not selection.is_project_selected:
|
||||
return {}
|
||||
|
||||
request_data = self._get_webaction_request_data(selection)
|
||||
|
||||
query = {
|
||||
"addonName": context.addon_name,
|
||||
"addonVersion": context.addon_version,
|
||||
"identifier": context.identifier,
|
||||
"variant": self._variant,
|
||||
}
|
||||
url = f"actions/config?{urlencode(query)}"
|
||||
try:
|
||||
response = ayon_api.post(url, **request_data)
|
||||
response.raise_for_status()
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to collect webaction config values.",
|
||||
exc_info=True
|
||||
)
|
||||
return {}
|
||||
return response.data
|
||||
|
||||
def set_action_config_values(self, context, values):
|
||||
selection = self._prepare_selection(
|
||||
context.project_name, context.folder_id, context.task_id
|
||||
)
|
||||
if not selection.is_project_selected:
|
||||
return {}
|
||||
|
||||
request_data = self._get_webaction_request_data(selection)
|
||||
request_data["value"] = values
|
||||
|
||||
query = {
|
||||
"addonName": context.addon_name,
|
||||
"addonVersion": context.addon_version,
|
||||
"identifier": context.identifier,
|
||||
"variant": self._variant,
|
||||
}
|
||||
url = f"actions/config?{urlencode(query)}"
|
||||
try:
|
||||
response = ayon_api.post(url, **request_data)
|
||||
response.raise_for_status()
|
||||
except Exception:
|
||||
self.log.warning(
|
||||
"Failed to store webaction config values.",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
def _get_addons_manager(self):
|
||||
if self._addons_manager is None:
|
||||
self._addons_manager = AddonsManager()
|
||||
return self._addons_manager
|
||||
|
||||
def _get_no_last_workfile_reg_data(self):
|
||||
try:
|
||||
no_workfile_reg_data = self._launcher_tool_reg.get_item(
|
||||
self._not_open_workfile_reg_key)
|
||||
except ValueError:
|
||||
no_workfile_reg_data = {}
|
||||
self._launcher_tool_reg.set_item(
|
||||
self._not_open_workfile_reg_key, no_workfile_reg_data)
|
||||
return no_workfile_reg_data
|
||||
|
||||
def _get_no_last_workfile_for_context(
|
||||
self, project_name, folder_id, task_id
|
||||
):
|
||||
not_open_workfile_reg_data = self._get_no_last_workfile_reg_data()
|
||||
return (
|
||||
not_open_workfile_reg_data
|
||||
.get(project_name, {})
|
||||
.get(folder_id, {})
|
||||
.get(task_id, {})
|
||||
)
|
||||
|
||||
def _prepare_selection(self, project_name, folder_id, task_id):
|
||||
project_entity = None
|
||||
if project_name:
|
||||
|
|
@ -458,6 +351,179 @@ class ActionsModel:
|
|||
project_settings=project_settings,
|
||||
)
|
||||
|
||||
def _get_webaction_request_data(self, selection: LauncherActionSelection):
|
||||
if not selection.is_project_selected:
|
||||
return None
|
||||
|
||||
entity_type = None
|
||||
entity_id = None
|
||||
entity_subtypes = []
|
||||
if selection.is_task_selected:
|
||||
entity_type = "task"
|
||||
entity_id = selection.task_entity["id"]
|
||||
entity_subtypes = [selection.task_entity["taskType"]]
|
||||
|
||||
elif selection.is_folder_selected:
|
||||
entity_type = "folder"
|
||||
entity_id = selection.folder_entity["id"]
|
||||
entity_subtypes = [selection.folder_entity["folderType"]]
|
||||
|
||||
entity_ids = []
|
||||
if entity_id:
|
||||
entity_ids.append(entity_id)
|
||||
|
||||
project_name = selection.project_name
|
||||
return {
|
||||
"projectName": project_name,
|
||||
"entityType": entity_type,
|
||||
"entitySubtypes": entity_subtypes,
|
||||
"entityIds": entity_ids,
|
||||
}
|
||||
|
||||
def _get_webactions(self, selection: LauncherActionSelection):
|
||||
if not selection.is_project_selected:
|
||||
return []
|
||||
|
||||
request_data = self._get_webaction_request_data(selection)
|
||||
project_name = selection.project_name
|
||||
entity_id = None
|
||||
if request_data["entityIds"]:
|
||||
entity_id = request_data["entityIds"][0]
|
||||
|
||||
cache: CacheItem = self._webaction_items[project_name][entity_id]
|
||||
if cache.is_valid:
|
||||
return cache.get_data()
|
||||
|
||||
try:
|
||||
response = ayon_api.post("actions/list", **request_data)
|
||||
response.raise_for_status()
|
||||
except Exception:
|
||||
self.log.warning("Failed to collect webactions.", exc_info=True)
|
||||
return []
|
||||
|
||||
action_items = []
|
||||
for action in response.data["actions"]:
|
||||
# NOTE Settings variant may be important for triggering?
|
||||
# - action["variant"]
|
||||
icon = action.get("icon")
|
||||
if icon and icon["type"] == "url":
|
||||
if not urlparse(icon["url"]).scheme:
|
||||
icon["type"] = "ayon_url"
|
||||
|
||||
config_fields = action.get("configFields") or []
|
||||
variant_label = action["label"]
|
||||
group_label = action.get("groupLabel")
|
||||
if not group_label:
|
||||
group_label = variant_label
|
||||
variant_label = None
|
||||
|
||||
full_label = self.calculate_full_label(
|
||||
group_label, variant_label
|
||||
)
|
||||
action_items.append(ActionItem(
|
||||
action_type="webaction",
|
||||
identifier=action["identifier"],
|
||||
order=action["order"],
|
||||
label=group_label,
|
||||
variant_label=variant_label,
|
||||
full_label=full_label,
|
||||
icon=icon,
|
||||
addon_name=action["addonName"],
|
||||
addon_version=action["addonVersion"],
|
||||
config_fields=config_fields,
|
||||
# category=action["category"],
|
||||
))
|
||||
|
||||
cache.update_data(action_items)
|
||||
return cache.get_data()
|
||||
|
||||
def _handle_webaction_response(self, data) -> WebactionResponse:
|
||||
response_type = data["type"]
|
||||
# Backwards compatibility -> 'server' type is not available since
|
||||
# AYON backend 1.8.3
|
||||
if response_type == "server":
|
||||
return WebactionResponse(
|
||||
response_type,
|
||||
False,
|
||||
error_message="Please use AYON web UI to run the action.",
|
||||
)
|
||||
|
||||
payload = data.get("payload") or {}
|
||||
|
||||
download_uri = payload.get("extra_download")
|
||||
if download_uri is not None:
|
||||
# Find out if is relative or absolute URL
|
||||
if not urlparse(download_uri).scheme:
|
||||
ayon_url = ayon_api.get_base_url().rstrip("/")
|
||||
path = download_uri.lstrip("/")
|
||||
download_uri = f"{ayon_url}/{path}"
|
||||
|
||||
# Use webbrowser to open file
|
||||
webbrowser.open_new_tab(download_uri)
|
||||
|
||||
response = WebactionResponse(
|
||||
response_type,
|
||||
data["success"],
|
||||
data.get("message"),
|
||||
payload.get("extra_clipboard"),
|
||||
)
|
||||
if response_type == "simple":
|
||||
pass
|
||||
|
||||
elif response_type == "redirect":
|
||||
# NOTE unused 'newTab' key because we always have to
|
||||
# open new tab from desktop app.
|
||||
if not webbrowser.open_new_tab(payload["uri"]):
|
||||
payload.error_message = "Failed to open web browser."
|
||||
|
||||
elif response_type == "form":
|
||||
submit_icon = payload["submit_icon"] or None
|
||||
cancel_icon = payload["cancel_icon"] or None
|
||||
if submit_icon:
|
||||
submit_icon = {
|
||||
"type": "material-symbols",
|
||||
"name": submit_icon,
|
||||
}
|
||||
|
||||
if cancel_icon:
|
||||
cancel_icon = {
|
||||
"type": "material-symbols",
|
||||
"name": cancel_icon,
|
||||
}
|
||||
|
||||
response.form = WebactionForm(
|
||||
fields=payload["fields"],
|
||||
title=payload["title"],
|
||||
submit_label=payload["submit_label"],
|
||||
cancel_label=payload["cancel_label"],
|
||||
submit_icon=submit_icon,
|
||||
cancel_icon=cancel_icon,
|
||||
)
|
||||
|
||||
elif response_type == "launcher":
|
||||
# Run AYON launcher process with uri in arguments
|
||||
# NOTE This does pass environment variables of current process
|
||||
# to the subprocess.
|
||||
# NOTE We could 'take action' directly and use the arguments here
|
||||
if payload is not None:
|
||||
uri = payload["uri"]
|
||||
else:
|
||||
uri = data["uri"]
|
||||
run_detached_ayon_launcher_process(uri)
|
||||
|
||||
elif response_type in ("query", "navigate"):
|
||||
response.error_message = (
|
||||
"Please use AYON web UI to run the action."
|
||||
)
|
||||
|
||||
else:
|
||||
self.log.warning(
|
||||
f"Unknown webaction response type '{response_type}'"
|
||||
)
|
||||
response.error_message = "Unknown webaction response type."
|
||||
|
||||
return response
|
||||
|
||||
def _get_discovered_action_classes(self):
|
||||
if self._discovered_actions is None:
|
||||
# NOTE We don't need to register the paths, but that would
|
||||
|
|
@ -470,7 +536,6 @@ class ActionsModel:
|
|||
register_launcher_action_path(path)
|
||||
self._discovered_actions = (
|
||||
discover_launcher_actions()
|
||||
+ self._get_applications_action_classes()
|
||||
)
|
||||
return self._discovered_actions
|
||||
|
||||
|
|
@ -498,62 +563,29 @@ class ActionsModel:
|
|||
|
||||
action_items = {}
|
||||
for identifier, action in self._get_action_objects().items():
|
||||
is_application = isinstance(action, ApplicationAction)
|
||||
# Backwards compatibility from 0.3.3 (24/06/10)
|
||||
# TODO: Remove in future releases
|
||||
if is_application and hasattr(action, "project_settings"):
|
||||
if hasattr(action, "project_settings"):
|
||||
action.project_entities[project_name] = project_entity
|
||||
action.project_settings[project_name] = project_settings
|
||||
|
||||
label = action.label or identifier
|
||||
variant_label = getattr(action, "label_variant", None)
|
||||
full_label = self.calculate_full_label(
|
||||
label, variant_label
|
||||
)
|
||||
icon = get_action_icon(action)
|
||||
|
||||
item = ActionItem(
|
||||
identifier,
|
||||
label,
|
||||
variant_label,
|
||||
icon,
|
||||
action.order,
|
||||
is_application,
|
||||
False
|
||||
action_type="local",
|
||||
identifier=identifier,
|
||||
order=action.order,
|
||||
label=label,
|
||||
variant_label=variant_label,
|
||||
full_label=full_label,
|
||||
icon=icon,
|
||||
config_fields=[],
|
||||
)
|
||||
action_items[identifier] = item
|
||||
self._action_items[project_name] = action_items
|
||||
return action_items
|
||||
|
||||
def _get_applications_action_classes(self):
|
||||
addons_manager = self._get_addons_manager()
|
||||
applications_addon = addons_manager.get_enabled_addon("applications")
|
||||
if hasattr(applications_addon, "get_applications_action_classes"):
|
||||
return applications_addon.get_applications_action_classes()
|
||||
|
||||
# Backwards compatibility from 0.3.3 (24/06/10)
|
||||
# TODO: Remove in future releases
|
||||
actions = []
|
||||
if applications_addon is None:
|
||||
return actions
|
||||
|
||||
manager = applications_addon.get_applications_manager()
|
||||
for full_name, application in manager.applications.items():
|
||||
if not application.enabled:
|
||||
continue
|
||||
|
||||
action = type(
|
||||
"app_{}".format(full_name),
|
||||
(ApplicationAction,),
|
||||
{
|
||||
"identifier": "application.{}".format(full_name),
|
||||
"application": application,
|
||||
"name": application.name,
|
||||
"label": application.group.label,
|
||||
"label_variant": application.label,
|
||||
"group": None,
|
||||
"icon": application.icon,
|
||||
"color": getattr(application, "color", None),
|
||||
"order": getattr(application, "order", None) or 0,
|
||||
"data": {}
|
||||
}
|
||||
)
|
||||
actions.append(action)
|
||||
return actions
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +0,0 @@
|
|||
import os
|
||||
|
||||
RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def get_options_image_path():
|
||||
return os.path.join(RESOURCES_DIR, "options.png")
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,9 +1,9 @@
|
|||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
|
||||
from ayon_core import style
|
||||
from ayon_core import resources
|
||||
from ayon_core import style, resources
|
||||
|
||||
from ayon_core.tools.launcher.control import BaseLauncherController
|
||||
from ayon_core.tools.utils import MessageOverlayObject
|
||||
|
||||
from .projects_widget import ProjectsWidget
|
||||
from .hierarchy_page import HierarchyPage
|
||||
|
|
@ -41,6 +41,8 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
|
||||
self._controller = controller
|
||||
|
||||
overlay_object = MessageOverlayObject(self)
|
||||
|
||||
# Main content - Pages & Actions
|
||||
content_body = QtWidgets.QSplitter(self)
|
||||
|
||||
|
|
@ -78,26 +80,18 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
content_body.setSizes([580, 160])
|
||||
|
||||
# Footer
|
||||
footer_widget = QtWidgets.QWidget(self)
|
||||
|
||||
# - Message label
|
||||
message_label = QtWidgets.QLabel(footer_widget)
|
||||
|
||||
# footer_widget = QtWidgets.QWidget(self)
|
||||
#
|
||||
# action_history = ActionHistory(footer_widget)
|
||||
# action_history.setStatusTip("Show Action History")
|
||||
|
||||
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
footer_layout.addWidget(message_label, 1)
|
||||
#
|
||||
# footer_layout = QtWidgets.QHBoxLayout(footer_widget)
|
||||
# footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# footer_layout.addWidget(action_history, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(content_body, 1)
|
||||
layout.addWidget(footer_widget, 0)
|
||||
|
||||
message_timer = QtCore.QTimer()
|
||||
message_timer.setInterval(self.message_interval)
|
||||
message_timer.setSingleShot(True)
|
||||
# layout.addWidget(footer_widget, 0)
|
||||
|
||||
actions_refresh_timer = QtCore.QTimer()
|
||||
actions_refresh_timer.setInterval(self.refresh_interval)
|
||||
|
|
@ -109,7 +103,6 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
page_slide_anim.setEasingCurve(QtCore.QEasingCurve.OutQuad)
|
||||
|
||||
projects_page.refreshed.connect(self._on_projects_refresh)
|
||||
message_timer.timeout.connect(self._on_message_timeout)
|
||||
actions_refresh_timer.timeout.connect(
|
||||
self._on_actions_refresh_timeout)
|
||||
page_slide_anim.valueChanged.connect(
|
||||
|
|
@ -128,6 +121,16 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
"action.trigger.finished",
|
||||
self._on_action_trigger_finished,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"webaction.trigger.started",
|
||||
self._on_webaction_trigger_started,
|
||||
)
|
||||
controller.register_event_callback(
|
||||
"webaction.trigger.finished",
|
||||
self._on_webaction_trigger_finished,
|
||||
)
|
||||
|
||||
self._overlay_object = overlay_object
|
||||
|
||||
self._controller = controller
|
||||
|
||||
|
|
@ -141,11 +144,8 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
self._projects_page = projects_page
|
||||
self._hierarchy_page = hierarchy_page
|
||||
self._actions_widget = actions_widget
|
||||
|
||||
self._message_label = message_label
|
||||
# self._action_history = action_history
|
||||
|
||||
self._message_timer = message_timer
|
||||
self._actions_refresh_timer = actions_refresh_timer
|
||||
self._page_slide_anim = page_slide_anim
|
||||
|
||||
|
|
@ -185,13 +185,6 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
else:
|
||||
self._refresh_on_activate = True
|
||||
|
||||
def _echo(self, message):
|
||||
self._message_label.setText(str(message))
|
||||
self._message_timer.start()
|
||||
|
||||
def _on_message_timeout(self):
|
||||
self._message_label.setText("")
|
||||
|
||||
def _on_project_selection_change(self, event):
|
||||
project_name = event["project_name"]
|
||||
self._selected_project_name = project_name
|
||||
|
|
@ -215,13 +208,69 @@ class LauncherWindow(QtWidgets.QWidget):
|
|||
self._hierarchy_page.refresh()
|
||||
self._actions_widget.refresh()
|
||||
|
||||
def _show_toast_message(self, message, success=True, message_id=None):
|
||||
message_type = None
|
||||
if not success:
|
||||
message_type = "error"
|
||||
|
||||
self._overlay_object.add_message(
|
||||
message, message_type, message_id=message_id
|
||||
)
|
||||
|
||||
def _on_action_trigger_started(self, event):
|
||||
self._echo("Running action: {}".format(event["full_label"]))
|
||||
self._show_toast_message(
|
||||
"Running: {}".format(event["full_label"]),
|
||||
message_id=event["trigger_id"],
|
||||
)
|
||||
|
||||
def _on_action_trigger_finished(self, event):
|
||||
if not event["failed"]:
|
||||
action_label = event["full_label"]
|
||||
if event["failed"]:
|
||||
message = f"Failed to run: {action_label}"
|
||||
else:
|
||||
message = f"Finished: {action_label}"
|
||||
self._show_toast_message(
|
||||
message,
|
||||
not event["failed"],
|
||||
message_id=event["trigger_id"],
|
||||
)
|
||||
|
||||
def _on_webaction_trigger_started(self, event):
|
||||
self._show_toast_message(
|
||||
"Running: {}".format(event["full_label"]),
|
||||
message_id=event["trigger_id"],
|
||||
)
|
||||
|
||||
def _on_webaction_trigger_finished(self, event):
|
||||
clipboard_text = event["clipboard_text"]
|
||||
if clipboard_text:
|
||||
clipboard = QtWidgets.QApplication.clipboard()
|
||||
clipboard.setText(clipboard_text)
|
||||
|
||||
action_label = event["full_label"]
|
||||
# Avoid to show exception message
|
||||
if event["trigger_failed"]:
|
||||
self._show_toast_message(
|
||||
f"Failed to run: {action_label}",
|
||||
message_id=event["trigger_id"]
|
||||
)
|
||||
return
|
||||
self._echo("Failed: {}".format(event["error_message"]))
|
||||
|
||||
# Failed to run webaction, e.g. because of missing webaction handling
|
||||
# - not reported by server
|
||||
if event["error_message"]:
|
||||
self._show_toast_message(
|
||||
event["error_message"],
|
||||
success=False,
|
||||
message_id=event["trigger_id"]
|
||||
)
|
||||
return
|
||||
|
||||
if event["message"]:
|
||||
self._show_toast_message(event["message"], event["success"])
|
||||
|
||||
if event["form"]:
|
||||
self._actions_widget.handle_webaction_form_event(event)
|
||||
|
||||
def _is_page_slide_anim_running(self):
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -84,15 +84,17 @@ def _get_options(action, action_item, parent):
|
|||
if not getattr(action, "optioned", False) or not options:
|
||||
return {}
|
||||
|
||||
dialog_title = action.label + " Options"
|
||||
if isinstance(options[0], AbstractAttrDef):
|
||||
qargparse_options = False
|
||||
dialog = AttributeDefinitionsDialog(options, parent)
|
||||
dialog = AttributeDefinitionsDialog(
|
||||
options, title=dialog_title, parent=parent
|
||||
)
|
||||
else:
|
||||
qargparse_options = True
|
||||
dialog = OptionDialog(parent)
|
||||
dialog.create(options)
|
||||
|
||||
dialog.setWindowTitle(action.label + " Options")
|
||||
dialog.setWindowTitle(dialog_title)
|
||||
|
||||
if not dialog.exec_():
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from .widgets import (
|
|||
CustomTextComboBox,
|
||||
PlaceholderLineEdit,
|
||||
PlaceholderPlainTextEdit,
|
||||
MarkdownLabel,
|
||||
ElideLabel,
|
||||
HintedLineEdit,
|
||||
ExpandingTextEdit,
|
||||
|
|
@ -91,6 +92,7 @@ __all__ = (
|
|||
"CustomTextComboBox",
|
||||
"PlaceholderLineEdit",
|
||||
"PlaceholderPlainTextEdit",
|
||||
"MarkdownLabel",
|
||||
"ElideLabel",
|
||||
"HintedLineEdit",
|
||||
"ExpandingTextEdit",
|
||||
|
|
|
|||
|
|
@ -14,3 +14,4 @@ except AttributeError:
|
|||
DEFAULT_PROJECT_LABEL = "< Default >"
|
||||
PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 101
|
||||
PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 102
|
||||
DEFAULT_WEB_ICON_COLOR = "#f4f5f5"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import os
|
||||
import sys
|
||||
import io
|
||||
import contextlib
|
||||
import collections
|
||||
import traceback
|
||||
import urllib.request
|
||||
from functools import partial
|
||||
from typing import Union, Any
|
||||
|
||||
import ayon_api
|
||||
from qtpy import QtWidgets, QtCore, QtGui
|
||||
import qtawesome
|
||||
import qtmaterialsymbols
|
||||
|
|
@ -17,7 +20,12 @@ from ayon_core.style import (
|
|||
from ayon_core.resources import get_image_path
|
||||
from ayon_core.lib import Logger
|
||||
|
||||
from .constants import CHECKED_INT, UNCHECKED_INT, PARTIALLY_CHECKED_INT
|
||||
from .constants import (
|
||||
CHECKED_INT,
|
||||
UNCHECKED_INT,
|
||||
PARTIALLY_CHECKED_INT,
|
||||
DEFAULT_WEB_ICON_COLOR,
|
||||
)
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
|
@ -480,11 +488,27 @@ class _IconsCache:
|
|||
if icon_type == "path":
|
||||
parts = [icon_type, icon_def["path"]]
|
||||
|
||||
elif icon_type in {"awesome-font", "material-symbols"}:
|
||||
color = icon_def["color"] or ""
|
||||
elif icon_type == "awesome-font":
|
||||
color = icon_def.get("color") or ""
|
||||
if isinstance(color, QtGui.QColor):
|
||||
color = color.name()
|
||||
parts = [icon_type, icon_def["name"] or "", color]
|
||||
|
||||
elif icon_type == "material-symbols":
|
||||
color = icon_def.get("color") or DEFAULT_WEB_ICON_COLOR
|
||||
if isinstance(color, QtGui.QColor):
|
||||
color = color.name()
|
||||
parts = [icon_type, icon_def["name"] or "", color]
|
||||
|
||||
elif icon_type in {"url", "ayon_url"}:
|
||||
parts = [icon_type, icon_def["url"]]
|
||||
|
||||
elif icon_type == "transparent":
|
||||
size = icon_def.get("size")
|
||||
if size is None:
|
||||
size = 256
|
||||
parts = [icon_type, str(size)]
|
||||
|
||||
return "|".join(parts)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -505,7 +529,7 @@ class _IconsCache:
|
|||
|
||||
elif icon_type == "awesome-font":
|
||||
icon_name = icon_def["name"]
|
||||
icon_color = icon_def["color"]
|
||||
icon_color = icon_def.get("color")
|
||||
icon = cls.get_qta_icon_by_name_and_color(icon_name, icon_color)
|
||||
if icon is None:
|
||||
icon = cls.get_qta_icon_by_name_and_color(
|
||||
|
|
@ -513,10 +537,40 @@ class _IconsCache:
|
|||
|
||||
elif icon_type == "material-symbols":
|
||||
icon_name = icon_def["name"]
|
||||
icon_color = icon_def["color"]
|
||||
icon_color = icon_def.get("color") or DEFAULT_WEB_ICON_COLOR
|
||||
if qtmaterialsymbols.get_icon_name_char(icon_name) is not None:
|
||||
icon = qtmaterialsymbols.get_icon(icon_name, icon_color)
|
||||
|
||||
elif icon_type == "url":
|
||||
url = icon_def["url"]
|
||||
try:
|
||||
content = urllib.request.urlopen(url).read()
|
||||
pix = QtGui.QPixmap()
|
||||
pix.loadFromData(content)
|
||||
icon = QtGui.QIcon(pix)
|
||||
except Exception:
|
||||
log.warning(
|
||||
"Failed to download image '%s'", url, exc_info=True
|
||||
)
|
||||
icon = None
|
||||
|
||||
elif icon_type == "ayon_url":
|
||||
url = icon_def["url"].lstrip("/")
|
||||
url = f"{ayon_api.get_base_url()}/{url}"
|
||||
stream = io.BytesIO()
|
||||
ayon_api.download_file_to_stream(url, stream)
|
||||
pix = QtGui.QPixmap()
|
||||
pix.loadFromData(stream.getvalue())
|
||||
icon = QtGui.QIcon(pix)
|
||||
|
||||
elif icon_type == "transparent":
|
||||
size = icon_def.get("size")
|
||||
if size is None:
|
||||
size = 256
|
||||
pix = QtGui.QPixmap(size, size)
|
||||
pix.fill(QtCore.Qt.transparent)
|
||||
icon = QtGui.QIcon(pix)
|
||||
|
||||
if icon is None:
|
||||
icon = cls.get_default()
|
||||
cls._cache[cache_key] = icon
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ from qtpy import QtWidgets, QtCore, QtGui
|
|||
import qargparse
|
||||
import qtawesome
|
||||
|
||||
try:
|
||||
import markdown
|
||||
except Exception:
|
||||
markdown = None
|
||||
|
||||
from ayon_core.style import (
|
||||
get_objected_colors,
|
||||
get_style_image_path,
|
||||
|
|
@ -131,6 +136,37 @@ class PlaceholderPlainTextEdit(QtWidgets.QPlainTextEdit):
|
|||
viewport.setPalette(filter_palette)
|
||||
|
||||
|
||||
class MarkdownLabel(QtWidgets.QLabel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Enable word wrap by default
|
||||
self.setWordWrap(True)
|
||||
|
||||
text_format_available = hasattr(QtCore.Qt, "MarkdownText")
|
||||
if text_format_available:
|
||||
self.setTextFormat(QtCore.Qt.MarkdownText)
|
||||
|
||||
self._text_format_available = text_format_available
|
||||
|
||||
self.setText(self.text())
|
||||
|
||||
def setText(self, text):
|
||||
if not self._text_format_available:
|
||||
text = self._md_to_html(text)
|
||||
super().setText(text)
|
||||
|
||||
@staticmethod
|
||||
def _md_to_html(text):
|
||||
if markdown is None:
|
||||
# This does add style definition to the markdown which does not
|
||||
# feel natural in the UI (but still better than raw MD).
|
||||
doc = QtGui.QTextDocument()
|
||||
doc.setMarkdown(text)
|
||||
return doc.toHtml()
|
||||
return markdown.markdown(text)
|
||||
|
||||
|
||||
class ElideLabel(QtWidgets.QLabel):
|
||||
"""Label which elide text.
|
||||
|
||||
|
|
@ -459,15 +495,15 @@ class ClickableLabel(QtWidgets.QLabel):
|
|||
"""Label that catch left mouse click and can trigger 'clicked' signal."""
|
||||
clicked = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent):
|
||||
super(ClickableLabel, self).__init__(parent)
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._mouse_pressed = False
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
self._mouse_pressed = True
|
||||
super(ClickableLabel, self).mousePressEvent(event)
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if self._mouse_pressed:
|
||||
|
|
@ -475,7 +511,7 @@ class ClickableLabel(QtWidgets.QLabel):
|
|||
if self.rect().contains(event.pos()):
|
||||
self.clicked.emit()
|
||||
|
||||
super(ClickableLabel, self).mouseReleaseEvent(event)
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
|
||||
class ExpandBtnLabel(QtWidgets.QLabel):
|
||||
|
|
@ -704,7 +740,7 @@ class PixmapLabel(QtWidgets.QLabel):
|
|||
|
||||
def resizeEvent(self, event):
|
||||
self._set_resized_pix()
|
||||
super(PixmapLabel, self).resizeEvent(event)
|
||||
super().resizeEvent(event)
|
||||
|
||||
|
||||
class PixmapButtonPainter(QtWidgets.QWidget):
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ description="AYON core addon."
|
|||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9.1,<3.10"
|
||||
markdown = "^3.4.1"
|
||||
clique = "1.6.*"
|
||||
jsonschema = "^2.6.0"
|
||||
pyblish-base = "^1.8.11"
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ client_dir = "ayon_core"
|
|||
|
||||
plugin_for = ["ayon_server"]
|
||||
|
||||
ayon_server_version = ">=1.7.6,<2.0.0"
|
||||
ayon_server_version = ">=1.8.4,<2.0.0"
|
||||
ayon_launcher_version = ">=1.0.2"
|
||||
ayon_required_addons = {}
|
||||
ayon_compatible_addons = {
|
||||
"ayon_ocio": ">=1.2.1",
|
||||
"applications": ">=1.1.2",
|
||||
"harmony": ">0.4.0",
|
||||
"fusion": ">=0.3.3",
|
||||
"openrv": ">=1.0.2",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue