mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 08:24:53 +01:00
Merge branch 'feature/1605-settings-task-types-enum-entity' into feature/895-add-option-to-define-paht-to-workfile-template
This commit is contained in:
commit
8019196a5f
29 changed files with 717 additions and 287 deletions
|
|
@ -60,13 +60,6 @@ def tray(debug=False):
|
|||
help="Ftrack api user")
|
||||
@click.option("--ftrack-api-key", envvar="FTRACK_API_KEY",
|
||||
help="Ftrack api key")
|
||||
@click.option("--ftrack-events-path",
|
||||
envvar="FTRACK_EVENTS_PATH",
|
||||
help=("path to ftrack event handlers"))
|
||||
@click.option("--no-stored-credentials", is_flag=True,
|
||||
help="don't use stored credentials")
|
||||
@click.option("--store-credentials", is_flag=True,
|
||||
help="store provided credentials")
|
||||
@click.option("--legacy", is_flag=True,
|
||||
help="run event server without mongo storing")
|
||||
@click.option("--clockify-api-key", envvar="CLOCKIFY_API_KEY",
|
||||
|
|
@ -77,9 +70,6 @@ def eventserver(debug,
|
|||
ftrack_url,
|
||||
ftrack_user,
|
||||
ftrack_api_key,
|
||||
ftrack_events_path,
|
||||
no_stored_credentials,
|
||||
store_credentials,
|
||||
legacy,
|
||||
clockify_api_key,
|
||||
clockify_workspace):
|
||||
|
|
@ -87,10 +77,6 @@ def eventserver(debug,
|
|||
|
||||
This should be ideally used by system service (such us systemd or upstart
|
||||
on linux and window service).
|
||||
|
||||
You have to set either proper environment variables to provide URL and
|
||||
credentials or use option to specify them. If you use --store_credentials
|
||||
provided credentials will be stored for later use.
|
||||
"""
|
||||
if debug:
|
||||
os.environ['OPENPYPE_DEBUG'] = "3"
|
||||
|
|
@ -99,9 +85,6 @@ def eventserver(debug,
|
|||
ftrack_url,
|
||||
ftrack_user,
|
||||
ftrack_api_key,
|
||||
ftrack_events_path,
|
||||
no_stored_credentials,
|
||||
store_credentials,
|
||||
legacy,
|
||||
clockify_api_key,
|
||||
clockify_workspace
|
||||
|
|
|
|||
|
|
@ -680,6 +680,10 @@ class TrayModulesManager(ModulesManager):
|
|||
output.append(module)
|
||||
return output
|
||||
|
||||
def restart_tray(self):
|
||||
if self.tray_manager:
|
||||
self.tray_manager.restart()
|
||||
|
||||
def tray_init(self):
|
||||
report = {}
|
||||
time_start = time.time()
|
||||
|
|
|
|||
|
|
@ -422,17 +422,18 @@ def run_event_server(
|
|||
ftrack_url,
|
||||
ftrack_user,
|
||||
ftrack_api_key,
|
||||
ftrack_events_path,
|
||||
no_stored_credentials,
|
||||
store_credentials,
|
||||
legacy,
|
||||
clockify_api_key,
|
||||
clockify_workspace
|
||||
):
|
||||
if not no_stored_credentials:
|
||||
if not ftrack_user or not ftrack_api_key:
|
||||
print((
|
||||
"Ftrack user/api key were not passed."
|
||||
" Trying to use credentials from user keyring."
|
||||
))
|
||||
cred = credentials.get_credentials(ftrack_url)
|
||||
username = cred.get('username')
|
||||
api_key = cred.get('api_key')
|
||||
ftrack_user = cred.get("username")
|
||||
ftrack_api_key = cred.get("api_key")
|
||||
|
||||
if clockify_workspace and clockify_api_key:
|
||||
os.environ["CLOCKIFY_WORKSPACE"] = clockify_workspace
|
||||
|
|
@ -445,209 +446,16 @@ def run_event_server(
|
|||
return 1
|
||||
|
||||
# Validate entered credentials
|
||||
if not validate_credentials(ftrack_url, username, api_key):
|
||||
if not validate_credentials(ftrack_url, ftrack_user, ftrack_api_key):
|
||||
print('Exiting! < Please enter valid credentials >')
|
||||
return 1
|
||||
|
||||
if store_credentials:
|
||||
credentials.save_credentials(username, api_key, ftrack_url)
|
||||
|
||||
# Set Ftrack environments
|
||||
os.environ["FTRACK_SERVER"] = ftrack_url
|
||||
os.environ["FTRACK_API_USER"] = username
|
||||
os.environ["FTRACK_API_KEY"] = api_key
|
||||
# TODO This won't work probably
|
||||
if ftrack_events_path:
|
||||
if isinstance(ftrack_events_path, (list, tuple)):
|
||||
ftrack_events_path = os.pathsep.join(ftrack_events_path)
|
||||
os.environ["FTRACK_EVENTS_PATH"] = ftrack_events_path
|
||||
os.environ["FTRACK_API_USER"] = ftrack_user
|
||||
os.environ["FTRACK_API_KEY"] = ftrack_api_key
|
||||
|
||||
if legacy:
|
||||
return legacy_server(ftrack_url)
|
||||
|
||||
return main_loop(ftrack_url)
|
||||
|
||||
|
||||
def main(argv):
|
||||
'''
|
||||
There are 4 values neccessary for event server:
|
||||
1.) Ftrack url - "studio.ftrackapp.com"
|
||||
2.) Username - "my.username"
|
||||
3.) API key - "apikey-long11223344-6665588-5565"
|
||||
4.) Path/s to events - "X:/path/to/folder/with/events"
|
||||
|
||||
All these values can be entered with arguments or environment variables.
|
||||
- arguments:
|
||||
"-ftrackurl {url}"
|
||||
"-ftrackuser {username}"
|
||||
"-ftrackapikey {api key}"
|
||||
"-ftrackeventpaths {path to events}"
|
||||
- environment variables:
|
||||
FTRACK_SERVER
|
||||
FTRACK_API_USER
|
||||
FTRACK_API_KEY
|
||||
FTRACK_EVENTS_PATH
|
||||
|
||||
Credentials (Username & API key):
|
||||
- Credentials can be stored for auto load on next start
|
||||
- To *Store/Update* these values add argument "-storecred"
|
||||
- They will be stored to appsdir file when login is successful
|
||||
- To *Update/Override* values with enviromnet variables is also needed to:
|
||||
- *don't enter argument for that value*
|
||||
- add argument "-noloadcred" (currently stored credentials won't be loaded)
|
||||
|
||||
Order of getting values:
|
||||
1.) Arguments are always used when entered.
|
||||
- entered values through args have most priority! (in each case)
|
||||
2.) Credentials are tried to load from appsdir file.
|
||||
- skipped when credentials were entered through args or credentials
|
||||
are not stored yet
|
||||
- can be skipped with "-noloadcred" argument
|
||||
3.) Environment variables are last source of values.
|
||||
- will try to get not yet set values from environments
|
||||
|
||||
Best practice:
|
||||
- set environment variables FTRACK_SERVER & FTRACK_EVENTS_PATH
|
||||
- launch event_server_cli with args:
|
||||
~/event_server_cli.py -ftrackuser "{username}" -ftrackapikey "{API key}" -storecred
|
||||
- next time launch event_server_cli.py only with set environment variables
|
||||
FTRACK_SERVER & FTRACK_EVENTS_PATH
|
||||
'''
|
||||
parser = argparse.ArgumentParser(description='Ftrack event server')
|
||||
parser.add_argument(
|
||||
"-ftrackurl", type=str, metavar='FTRACKURL',
|
||||
help=(
|
||||
"URL to ftrack server where events should handle"
|
||||
" (default from environment: $FTRACK_SERVER)"
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"-ftrackuser", type=str,
|
||||
help=(
|
||||
"Username should be the username of the user in ftrack"
|
||||
" to record operations against."
|
||||
" (default from environment: $FTRACK_API_USER)"
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"-ftrackapikey", type=str,
|
||||
help=(
|
||||
"Should be the API key to use for authentication"
|
||||
" (default from environment: $FTRACK_API_KEY)"
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"-ftrackeventpaths", nargs='+',
|
||||
help=(
|
||||
"List of paths where events are stored."
|
||||
" (default from environment: $FTRACK_EVENTS_PATH)"
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
'-storecred',
|
||||
help=(
|
||||
"Entered credentials will be also stored"
|
||||
" to apps dir for future usage"
|
||||
),
|
||||
action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
'-noloadcred',
|
||||
help="Load creadentials from apps dir",
|
||||
action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
'-legacy',
|
||||
help="Load creadentials from apps dir",
|
||||
action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-clockifyapikey", type=str,
|
||||
help=(
|
||||
"Enter API key for Clockify actions."
|
||||
" (default from environment: $CLOCKIFY_API_KEY)"
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"-clockifyworkspace", type=str,
|
||||
help=(
|
||||
"Enter workspace for Clockify."
|
||||
" (default from module presets or "
|
||||
"environment: $CLOCKIFY_WORKSPACE)"
|
||||
)
|
||||
)
|
||||
ftrack_url = os.environ.get("FTRACK_SERVER")
|
||||
username = os.environ.get("FTRACK_API_USER")
|
||||
api_key = os.environ.get("FTRACK_API_KEY")
|
||||
|
||||
kwargs, args = parser.parse_known_args(argv)
|
||||
|
||||
if kwargs.ftrackurl:
|
||||
ftrack_url = kwargs.ftrackurl
|
||||
|
||||
# Load Ftrack url from settings if not set
|
||||
if not ftrack_url:
|
||||
ftrack_url = get_ftrack_url_from_settings()
|
||||
|
||||
event_paths = None
|
||||
if kwargs.ftrackeventpaths:
|
||||
event_paths = kwargs.ftrackeventpaths
|
||||
|
||||
if not kwargs.noloadcred:
|
||||
cred = credentials.get_credentials(ftrack_url)
|
||||
username = cred.get('username')
|
||||
api_key = cred.get('api_key')
|
||||
|
||||
if kwargs.ftrackuser:
|
||||
username = kwargs.ftrackuser
|
||||
|
||||
if kwargs.ftrackapikey:
|
||||
api_key = kwargs.ftrackapikey
|
||||
|
||||
if kwargs.clockifyworkspace:
|
||||
os.environ["CLOCKIFY_WORKSPACE"] = kwargs.clockifyworkspace
|
||||
|
||||
if kwargs.clockifyapikey:
|
||||
os.environ["CLOCKIFY_API_KEY"] = kwargs.clockifyapikey
|
||||
|
||||
legacy = kwargs.legacy
|
||||
|
||||
# Check url regex and accessibility
|
||||
ftrack_url = check_ftrack_url(ftrack_url)
|
||||
if not ftrack_url:
|
||||
print('Exiting! < Please enter Ftrack server url >')
|
||||
return 1
|
||||
|
||||
# Validate entered credentials
|
||||
if not validate_credentials(ftrack_url, username, api_key):
|
||||
print('Exiting! < Please enter valid credentials >')
|
||||
return 1
|
||||
|
||||
if kwargs.storecred:
|
||||
credentials.save_credentials(username, api_key, ftrack_url)
|
||||
|
||||
# Set Ftrack environments
|
||||
os.environ["FTRACK_SERVER"] = ftrack_url
|
||||
os.environ["FTRACK_API_USER"] = username
|
||||
os.environ["FTRACK_API_KEY"] = api_key
|
||||
if event_paths:
|
||||
if isinstance(event_paths, (list, tuple)):
|
||||
event_paths = os.pathsep.join(event_paths)
|
||||
os.environ["FTRACK_EVENTS_PATH"] = event_paths
|
||||
|
||||
if legacy:
|
||||
return legacy_server(ftrack_url)
|
||||
|
||||
return main_loop(ftrack_url)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Register interupt signal
|
||||
def signal_handler(sig, frame):
|
||||
print("You pressed Ctrl+C. Process ended.")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ class SettingsAction(PypeModule, ITrayAction):
|
|||
return
|
||||
from openpype.tools.settings import MainWidget
|
||||
self.settings_window = MainWidget(self.user_role)
|
||||
self.settings_window.trigger_restart.connect(self._on_trigger_restart)
|
||||
|
||||
def _on_trigger_restart(self):
|
||||
self.manager.restart_tray()
|
||||
|
||||
def show_settings_window(self):
|
||||
"""Show settings tool window.
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class ExtractBurnin(openpype.api.Extractor):
|
|||
"harmony",
|
||||
"fusion",
|
||||
"aftereffects",
|
||||
"tvpaint"
|
||||
# "resolve"
|
||||
]
|
||||
optional = True
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import re
|
|||
import copy
|
||||
import json
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import six
|
||||
|
||||
import clique
|
||||
|
||||
import pyblish.api
|
||||
|
|
@ -873,12 +876,6 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
"""
|
||||
filters = []
|
||||
|
||||
letter_box_def = output_def["letter_box"]
|
||||
letter_box_enabled = letter_box_def["enabled"]
|
||||
|
||||
# Get instance data
|
||||
pixel_aspect = temp_data["pixel_aspect"]
|
||||
|
||||
# NOTE Skipped using instance's resolution
|
||||
full_input_path_single_file = temp_data["full_input_path_single_file"]
|
||||
input_data = ffprobe_streams(
|
||||
|
|
@ -887,6 +884,33 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
input_width = int(input_data["width"])
|
||||
input_height = int(input_data["height"])
|
||||
|
||||
# NOTE Setting only one of `width` or `heigth` is not allowed
|
||||
# - settings value can't have None but has value of 0
|
||||
output_width = output_def.get("width") or None
|
||||
output_height = output_def.get("height") or None
|
||||
|
||||
# Convert overscan value video filters
|
||||
overscan_crop = output_def.get("overscan_crop")
|
||||
overscan = OverscanCrop(input_width, input_height, overscan_crop)
|
||||
overscan_crop_filters = overscan.video_filters()
|
||||
# Add overscan filters to filters if are any and modify input
|
||||
# resolution by it's values
|
||||
if overscan_crop_filters:
|
||||
filters.extend(overscan_crop_filters)
|
||||
input_width = overscan.width()
|
||||
input_height = overscan.height()
|
||||
# Use output resolution as inputs after cropping to skip usage of
|
||||
# instance data resolution
|
||||
if output_width is None or output_height is None:
|
||||
output_width = input_width
|
||||
output_height = input_height
|
||||
|
||||
letter_box_def = output_def["letter_box"]
|
||||
letter_box_enabled = letter_box_def["enabled"]
|
||||
|
||||
# Get instance data
|
||||
pixel_aspect = temp_data["pixel_aspect"]
|
||||
|
||||
# Make sure input width and height is not an odd number
|
||||
input_width_is_odd = bool(input_width % 2 != 0)
|
||||
input_height_is_odd = bool(input_height % 2 != 0)
|
||||
|
|
@ -911,10 +935,6 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
self.log.debug("input_width: `{}`".format(input_width))
|
||||
self.log.debug("input_height: `{}`".format(input_height))
|
||||
|
||||
# NOTE Setting only one of `width` or `heigth` is not allowed
|
||||
# - settings value can't have None but has value of 0
|
||||
output_width = output_def.get("width") or None
|
||||
output_height = output_def.get("height") or None
|
||||
# Use instance resolution if output definition has not set it.
|
||||
if output_width is None or output_height is None:
|
||||
output_width = temp_data["resolution_width"]
|
||||
|
|
@ -1438,3 +1458,291 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
vf_back = "-vf " + ",".join(vf_fixed)
|
||||
|
||||
return vf_back
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class _OverscanValue:
|
||||
def __repr__(self):
|
||||
return "<{}> {}".format(self.__class__.__name__, str(self))
|
||||
|
||||
@abstractmethod
|
||||
def copy(self):
|
||||
"""Create a copy of object."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def size_for(self, value):
|
||||
"""Calculate new value for passed value."""
|
||||
pass
|
||||
|
||||
|
||||
class PixValueExplicit(_OverscanValue):
|
||||
def __init__(self, value):
|
||||
self._value = int(value)
|
||||
|
||||
def __str__(self):
|
||||
return "{}px".format(self._value)
|
||||
|
||||
def copy(self):
|
||||
return PixValueExplicit(self._value)
|
||||
|
||||
def size_for(self, value):
|
||||
if self._value == 0:
|
||||
return value
|
||||
return self._value
|
||||
|
||||
|
||||
class PercentValueExplicit(_OverscanValue):
|
||||
def __init__(self, value):
|
||||
self._value = float(value)
|
||||
|
||||
def __str__(self):
|
||||
return "{}%".format(abs(self._value))
|
||||
|
||||
def copy(self):
|
||||
return PercentValueExplicit(self._value)
|
||||
|
||||
def size_for(self, value):
|
||||
if self._value == 0:
|
||||
return value
|
||||
return int((value / 100) * self._value)
|
||||
|
||||
|
||||
class PixValueRelative(_OverscanValue):
|
||||
def __init__(self, value):
|
||||
self._value = int(value)
|
||||
|
||||
def __str__(self):
|
||||
sign = "-" if self._value < 0 else "+"
|
||||
return "{}{}px".format(sign, abs(self._value))
|
||||
|
||||
def copy(self):
|
||||
return PixValueRelative(self._value)
|
||||
|
||||
def size_for(self, value):
|
||||
return value + self._value
|
||||
|
||||
|
||||
class PercentValueRelative(_OverscanValue):
|
||||
def __init__(self, value):
|
||||
self._value = float(value)
|
||||
|
||||
def __str__(self):
|
||||
return "{}%".format(self._value)
|
||||
|
||||
def copy(self):
|
||||
return PercentValueRelative(self._value)
|
||||
|
||||
def size_for(self, value):
|
||||
if self._value == 0:
|
||||
return value
|
||||
|
||||
offset = int((value / 100) * self._value)
|
||||
|
||||
return value + offset
|
||||
|
||||
|
||||
class PercentValueRelativeSource(_OverscanValue):
|
||||
def __init__(self, value, source_sign):
|
||||
self._value = float(value)
|
||||
if source_sign not in ("-", "+"):
|
||||
raise ValueError(
|
||||
"Invalid sign value \"{}\" expected \"-\" or \"+\"".format(
|
||||
source_sign
|
||||
)
|
||||
)
|
||||
self._source_sign = source_sign
|
||||
|
||||
def __str__(self):
|
||||
return "{}%{}".format(self._value, self._source_sign)
|
||||
|
||||
def copy(self):
|
||||
return PercentValueRelativeSource(self._value, self._source_sign)
|
||||
|
||||
def size_for(self, value):
|
||||
if self._value == 0:
|
||||
return value
|
||||
return int((value * 100) / (100 - self._value))
|
||||
|
||||
|
||||
class OverscanCrop:
|
||||
"""Helper class to read overscan string and calculate output resolution.
|
||||
|
||||
It is possible to enter single value for both width and heigh or two values
|
||||
for width and height. Overscan string may have a few variants. Each variant
|
||||
define output size for input size.
|
||||
|
||||
### Example
|
||||
For input size: 2200px
|
||||
|
||||
| String | Output | Description |
|
||||
|----------|--------|-------------------------------------------------|
|
||||
| "" | 2200px | Empty string does nothing. |
|
||||
| "10%" | 220px | Explicit percent size. |
|
||||
| "-10%" | 1980px | Relative percent size (decrease). |
|
||||
| "+10%" | 2420px | Relative percent size (increase). |
|
||||
| "-10%+" | 2000px | Relative percent size to output size. |
|
||||
| "300px" | 300px | Explicit output size cropped or expanded. |
|
||||
| "-300px" | 1900px | Relative pixel size (decrease). |
|
||||
| "+300px" | 2500px | Relative pixel size (increase). |
|
||||
| "300" | 300px | Value without "%" and "px" is used as has "px". |
|
||||
|
||||
Value without sign (+/-) in is always explicit and value with sign is
|
||||
relative. Output size for "200px" and "+200px" are not the same.
|
||||
Values "0", "0px" or "0%" are ignored.
|
||||
|
||||
All values that cause output resolution smaller than 1 pixel are invalid.
|
||||
|
||||
Value "-10%+" is a special case which says that input's resolution is
|
||||
bigger by 10% than expected output.
|
||||
|
||||
It is possible to combine these variants to define different output for
|
||||
width and height.
|
||||
|
||||
Resolution: 2000px 1000px
|
||||
|
||||
| String | Output |
|
||||
|---------------|---------------|
|
||||
| "100px 120px" | 2100px 1120px |
|
||||
| "-10% -200px" | 1800px 800px |
|
||||
"""
|
||||
|
||||
item_regex = re.compile(r"([\+\-])?([0-9]+)(.+)?")
|
||||
relative_source_regex = re.compile(r"%([\+\-])")
|
||||
|
||||
def __init__(self, input_width, input_height, string_value):
|
||||
# Make sure that is not None
|
||||
string_value = string_value or ""
|
||||
|
||||
self.input_width = input_width
|
||||
self.input_height = input_height
|
||||
|
||||
width, height = self._convert_string_to_values(string_value)
|
||||
self._width_value = width
|
||||
self._height_value = height
|
||||
|
||||
self._string_value = string_value
|
||||
|
||||
def __str__(self):
|
||||
return "{}".format(self._string_value)
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}>".format(self.__class__.__name__)
|
||||
|
||||
def width(self):
|
||||
"""Calculated width."""
|
||||
return self._width_value.size_for(self.input_width)
|
||||
|
||||
def height(self):
|
||||
"""Calculated height."""
|
||||
return self._height_value.size_for(self.input_height)
|
||||
|
||||
def video_filters(self):
|
||||
"""FFmpeg video filters to achieve expected result.
|
||||
|
||||
Filter may be empty, use "crop" filter, "pad" filter or combination of
|
||||
"crop" and "pad".
|
||||
|
||||
Returns:
|
||||
list: FFmpeg video filters.
|
||||
"""
|
||||
# crop=width:height:x:y - explicit start x, y position
|
||||
# crop=width:height - x, y are related to center by width/height
|
||||
# pad=width:heigth:x:y - explicit start x, y position
|
||||
# pad=width:heigth - x, y are set to 0 by default
|
||||
|
||||
width = self.width()
|
||||
height = self.height()
|
||||
|
||||
output = []
|
||||
if self.input_width == width and self.input_height == height:
|
||||
return output
|
||||
|
||||
# Make sure resolution has odd numbers
|
||||
if width % 2 == 1:
|
||||
width -= 1
|
||||
|
||||
if height % 2 == 1:
|
||||
height -= 1
|
||||
|
||||
if width <= self.input_width and height <= self.input_height:
|
||||
output.append("crop={}:{}".format(width, height))
|
||||
|
||||
elif width >= self.input_width and height >= self.input_height:
|
||||
output.append(
|
||||
"pad={}:{}:(iw-ow)/2:(ih-oh)/2".format(width, height)
|
||||
)
|
||||
|
||||
elif width > self.input_width and height < self.input_height:
|
||||
output.append("crop=iw:{}".format(height))
|
||||
output.append("pad={}:ih:(iw-ow)/2:(ih-oh)/2".format(width))
|
||||
|
||||
elif width < self.input_width and height > self.input_height:
|
||||
output.append("crop={}:ih".format(width))
|
||||
output.append("pad=iw:{}:(iw-ow)/2:(ih-oh)/2".format(height))
|
||||
|
||||
return output
|
||||
|
||||
def _convert_string_to_values(self, orig_string_value):
|
||||
string_value = orig_string_value.strip().lower()
|
||||
if not string_value:
|
||||
return
|
||||
|
||||
# Replace "px" (and spaces before) with single space
|
||||
string_value = re.sub(r"([ ]+)?px", " ", string_value)
|
||||
string_value = re.sub(r"([ ]+)%", "%", string_value)
|
||||
# Make sure +/- sign at the beggining of string is next to number
|
||||
string_value = re.sub(r"^([\+\-])[ ]+", "\g<1>", string_value)
|
||||
# Make sure +/- sign in the middle has zero spaces before number under
|
||||
# which belongs
|
||||
string_value = re.sub(
|
||||
r"[ ]([\+\-])[ ]+([0-9])",
|
||||
r" \g<1>\g<2>",
|
||||
string_value
|
||||
)
|
||||
string_parts = [
|
||||
part
|
||||
for part in string_value.split(" ")
|
||||
if part
|
||||
]
|
||||
|
||||
error_msg = "Invalid string for rescaling \"{}\"".format(
|
||||
orig_string_value
|
||||
)
|
||||
if 1 > len(string_parts) > 2:
|
||||
raise ValueError(error_msg)
|
||||
|
||||
output = []
|
||||
for item in string_parts:
|
||||
groups = self.item_regex.findall(item)
|
||||
if not groups:
|
||||
raise ValueError(error_msg)
|
||||
|
||||
relative_sign, value, ending = groups[0]
|
||||
if not relative_sign:
|
||||
if not ending:
|
||||
output.append(PixValueExplicit(value))
|
||||
else:
|
||||
output.append(PercentValueExplicit(value))
|
||||
else:
|
||||
source_sign_group = self.relative_source_regex.findall(ending)
|
||||
if not ending:
|
||||
output.append(PixValueRelative(int(relative_sign + value)))
|
||||
|
||||
elif source_sign_group:
|
||||
source_sign = source_sign_group[0]
|
||||
output.append(PercentValueRelativeSource(
|
||||
float(relative_sign + value), source_sign
|
||||
))
|
||||
else:
|
||||
output.append(
|
||||
PercentValueRelative(float(relative_sign + value))
|
||||
)
|
||||
|
||||
if len(output) == 1:
|
||||
width = output.pop(0)
|
||||
height = width.copy()
|
||||
else:
|
||||
width, height = output
|
||||
|
||||
return width, height
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
"ftrack"
|
||||
]
|
||||
},
|
||||
"overscan_crop": "",
|
||||
"width": 0,
|
||||
"height": 0,
|
||||
"bg_color": [
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ from .enum_entity import (
|
|||
EnumEntity,
|
||||
AppsEnumEntity,
|
||||
ToolsEnumEntity,
|
||||
TaskTypeEnumEntity,
|
||||
ProvidersEnum
|
||||
)
|
||||
|
||||
|
|
@ -154,6 +155,7 @@ __all__ = (
|
|||
"EnumEntity",
|
||||
"AppsEnumEntity",
|
||||
"ToolsEnumEntity",
|
||||
"TaskTypeEnumEntity",
|
||||
"ProvidersEnum",
|
||||
|
||||
"ListEntity",
|
||||
|
|
|
|||
|
|
@ -111,6 +111,8 @@ class BaseItemEntity(BaseEntity):
|
|||
self.file_item = None
|
||||
# Reference to `RootEntity`
|
||||
self.root_item = None
|
||||
# Change of value requires restart of OpenPype
|
||||
self._require_restart_on_change = False
|
||||
|
||||
# Entity is in hierarchy of dynamically created entity
|
||||
self.is_in_dynamic_item = False
|
||||
|
|
@ -171,6 +173,14 @@ class BaseItemEntity(BaseEntity):
|
|||
roles = [roles]
|
||||
self.roles = roles
|
||||
|
||||
@property
|
||||
def require_restart_on_change(self):
|
||||
return self._require_restart_on_change
|
||||
|
||||
@property
|
||||
def require_restart(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def has_studio_override(self):
|
||||
"""Says if entity or it's children has studio overrides."""
|
||||
|
|
@ -261,6 +271,14 @@ class BaseItemEntity(BaseEntity):
|
|||
self, "Dynamic entity has set `is_group` to true."
|
||||
)
|
||||
|
||||
if (
|
||||
self.require_restart_on_change
|
||||
and (self.is_dynamic_item or self.is_in_dynamic_item)
|
||||
):
|
||||
raise EntitySchemaError(
|
||||
self, "Dynamic entity can't require restart."
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def set_override_state(self, state):
|
||||
"""Set override state and trigger it on children.
|
||||
|
|
@ -788,6 +806,15 @@ class ItemEntity(BaseItemEntity):
|
|||
# Root item reference
|
||||
self.root_item = self.parent.root_item
|
||||
|
||||
# Item require restart on value change
|
||||
require_restart_on_change = self.schema_data.get("require_restart")
|
||||
if (
|
||||
require_restart_on_change is None
|
||||
and not (self.is_dynamic_item or self.is_in_dynamic_item)
|
||||
):
|
||||
require_restart_on_change = self.parent.require_restart_on_change
|
||||
self._require_restart_on_change = require_restart_on_change
|
||||
|
||||
# File item reference
|
||||
if self.parent.is_file:
|
||||
self.file_item = self.parent
|
||||
|
|
|
|||
|
|
@ -439,10 +439,10 @@ class DictMutableKeysEntity(EndpointEntity):
|
|||
new_initial_value = []
|
||||
for key, value in _settings_value:
|
||||
if key in initial_value:
|
||||
new_initial_value.append(key, initial_value.pop(key))
|
||||
new_initial_value.append([key, initial_value.pop(key)])
|
||||
|
||||
for key, value in initial_value.items():
|
||||
new_initial_value.append(key, value)
|
||||
new_initial_value.append([key, value])
|
||||
initial_value = new_initial_value
|
||||
else:
|
||||
initial_value = _settings_value
|
||||
|
|
|
|||
|
|
@ -219,6 +219,40 @@ class ToolsEnumEntity(BaseEnumEntity):
|
|||
self._current_value = new_value
|
||||
|
||||
|
||||
class TaskTypeEnumEntity(BaseEnumEntity):
|
||||
schema_types = ["task-types-enum"]
|
||||
|
||||
def _item_initalization(self):
|
||||
self.multiselection = True
|
||||
self.value_on_not_set = []
|
||||
self.enum_items = []
|
||||
self.valid_keys = set()
|
||||
self.valid_value_types = (list, )
|
||||
self.placeholder = None
|
||||
|
||||
def _get_enum_values(self):
|
||||
from ..lib import get_default_anatomy_settings
|
||||
anatomy_settings = get_default_anatomy_settings()
|
||||
|
||||
valid_keys = set()
|
||||
enum_items = []
|
||||
for task_type, _task_attr in anatomy_settings["tasks"].items():
|
||||
enum_items.append({task_type: task_type})
|
||||
valid_keys.add(task_type)
|
||||
|
||||
return enum_items, valid_keys
|
||||
|
||||
def set_override_state(self, *args, **kwargs):
|
||||
super(TaskTypeEnumEntity, self).set_override_state(*args, **kwargs)
|
||||
|
||||
self.enum_items, self.valid_keys = self._get_enum_values()
|
||||
new_value = []
|
||||
for key in self._current_value:
|
||||
if key in self.valid_keys:
|
||||
new_value.append(key)
|
||||
self._current_value = new_value
|
||||
|
||||
|
||||
class ProvidersEnum(BaseEnumEntity):
|
||||
schema_types = ["providers-enum"]
|
||||
|
||||
|
|
|
|||
|
|
@ -68,8 +68,18 @@ class EndpointEntity(ItemEntity):
|
|||
def on_change(self):
|
||||
for callback in self.on_change_callbacks:
|
||||
callback()
|
||||
|
||||
if self.require_restart_on_change:
|
||||
if self.require_restart:
|
||||
self.root_item.add_item_require_restart(self)
|
||||
else:
|
||||
self.root_item.remove_item_require_restart(self)
|
||||
self.parent.on_child_change(self)
|
||||
|
||||
@property
|
||||
def require_restart(self):
|
||||
return self.has_unsaved_changes
|
||||
|
||||
def update_default_value(self, value):
|
||||
value = self._check_update_value(value, "default")
|
||||
self._default_value = value
|
||||
|
|
@ -115,6 +125,10 @@ class InputEntity(EndpointEntity):
|
|||
"""Entity's value without metadata."""
|
||||
return self._current_value
|
||||
|
||||
@property
|
||||
def require_restart(self):
|
||||
return self._value_is_modified
|
||||
|
||||
def _settings_value(self):
|
||||
return copy.deepcopy(self.value)
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ class RootEntity(BaseItemEntity):
|
|||
|
||||
def __init__(self, schema_data, reset):
|
||||
super(RootEntity, self).__init__(schema_data)
|
||||
self._require_restart_callbacks = []
|
||||
self._item_ids_require_restart = set()
|
||||
self._item_initalization()
|
||||
if reset:
|
||||
self.reset()
|
||||
|
|
@ -64,6 +66,31 @@ class RootEntity(BaseItemEntity):
|
|||
"""Current OverrideState."""
|
||||
return self._override_state
|
||||
|
||||
@property
|
||||
def require_restart(self):
|
||||
return bool(self._item_ids_require_restart)
|
||||
|
||||
def add_require_restart_change_callback(self, callback):
|
||||
self._require_restart_callbacks.append(callback)
|
||||
|
||||
def _on_require_restart_change(self):
|
||||
for callback in self._require_restart_callbacks:
|
||||
callback()
|
||||
|
||||
def add_item_require_restart(self, item):
|
||||
was_empty = len(self._item_ids_require_restart) == 0
|
||||
self._item_ids_require_restart.add(item.id)
|
||||
if was_empty:
|
||||
self._on_require_restart_change()
|
||||
|
||||
def remove_item_require_restart(self, item):
|
||||
if item.id not in self._item_ids_require_restart:
|
||||
return
|
||||
|
||||
self._item_ids_require_restart.remove(item.id)
|
||||
if not self._item_ids_require_restart:
|
||||
self._on_require_restart_change()
|
||||
|
||||
@abstractmethod
|
||||
def reset(self):
|
||||
"""Reset values and entities to initial state.
|
||||
|
|
|
|||
|
|
@ -173,6 +173,15 @@
|
|||
{
|
||||
"type": "separator"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Crop input overscan. See the documentation for more information."
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "overscan_crop",
|
||||
"label": "Overscan crop"
|
||||
},
|
||||
{
|
||||
"type": "label",
|
||||
"label": "Width and Height must be both set to higher value than 0 else source resolution is used."
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
"key": "ftrack",
|
||||
"label": "Ftrack",
|
||||
"collapsible": true,
|
||||
"require_restart": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@
|
|||
"key": "environment",
|
||||
"label": "Environment",
|
||||
"type": "raw-json",
|
||||
"env_group_key": "global"
|
||||
"env_group_key": "global",
|
||||
"require_restart": true
|
||||
},
|
||||
{
|
||||
"type": "splitter"
|
||||
|
|
@ -44,7 +45,8 @@
|
|||
"key": "openpype_path",
|
||||
"label": "Versions Repository",
|
||||
"multiplatform": true,
|
||||
"multipath": true
|
||||
"multipath": true,
|
||||
"require_restart": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"key": "avalon",
|
||||
"label": "Avalon",
|
||||
"collapsible": true,
|
||||
"require_restart": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "number",
|
||||
|
|
@ -35,6 +36,7 @@
|
|||
"key": "timers_manager",
|
||||
"label": "Timers Manager",
|
||||
"collapsible": true,
|
||||
"require_restart": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
|
|
@ -66,6 +68,7 @@
|
|||
"key": "clockify",
|
||||
"label": "Clockify",
|
||||
"collapsible": true,
|
||||
"require_restart": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
|
|
@ -84,6 +87,7 @@
|
|||
"key": "sync_server",
|
||||
"label": "Site Sync",
|
||||
"collapsible": true,
|
||||
"require_restart": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
|
|
@ -114,6 +118,7 @@
|
|||
"type": "dict",
|
||||
"key": "deadline",
|
||||
"label": "Deadline",
|
||||
"require_restart": true,
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
|
|
@ -133,6 +138,7 @@
|
|||
"type": "dict",
|
||||
"key": "muster",
|
||||
"label": "Muster",
|
||||
"require_restart": true,
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ class IgnoreInputChangesObj:
|
|||
class SettingsCategoryWidget(QtWidgets.QWidget):
|
||||
state_changed = QtCore.Signal()
|
||||
saved = QtCore.Signal(QtWidgets.QWidget)
|
||||
restart_required_trigger = QtCore.Signal()
|
||||
|
||||
def __init__(self, user_role, parent=None):
|
||||
super(SettingsCategoryWidget, self).__init__(parent)
|
||||
|
|
@ -185,9 +186,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
if self.user_role == "developer":
|
||||
self._add_developer_ui(footer_layout)
|
||||
|
||||
save_btn = QtWidgets.QPushButton("Save")
|
||||
spacer_widget = QtWidgets.QWidget()
|
||||
footer_layout.addWidget(spacer_widget, 1)
|
||||
save_btn = QtWidgets.QPushButton("Save", footer_widget)
|
||||
require_restart_label = QtWidgets.QLabel(footer_widget)
|
||||
require_restart_label.setAlignment(QtCore.Qt.AlignCenter)
|
||||
footer_layout.addWidget(require_restart_label, 1)
|
||||
footer_layout.addWidget(save_btn, 0)
|
||||
|
||||
configurations_layout = QtWidgets.QVBoxLayout(configurations_widget)
|
||||
|
|
@ -205,6 +207,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
save_btn.clicked.connect(self._save)
|
||||
|
||||
self.save_btn = save_btn
|
||||
self.require_restart_label = require_restart_label
|
||||
self.scroll_widget = scroll_widget
|
||||
self.content_layout = content_layout
|
||||
self.content_widget = content_widget
|
||||
|
|
@ -323,6 +326,15 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
def _on_reset_start(self):
|
||||
return
|
||||
|
||||
def _on_require_restart_change(self):
|
||||
value = ""
|
||||
if self.entity.require_restart:
|
||||
value = (
|
||||
"Your changes require restart of"
|
||||
" all running OpenPype processes to take affect."
|
||||
)
|
||||
self.require_restart_label.setText(value)
|
||||
|
||||
def reset(self):
|
||||
self.set_state(CategoryState.Working)
|
||||
|
||||
|
|
@ -339,6 +351,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
dialog = None
|
||||
try:
|
||||
self._create_root_entity()
|
||||
self.entity.add_require_restart_change_callback(
|
||||
self._on_require_restart_change
|
||||
)
|
||||
|
||||
self.add_children_gui()
|
||||
|
||||
|
|
@ -433,6 +448,15 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
def _save(self):
|
||||
# Don't trigger restart if defaults are modified
|
||||
if (
|
||||
self.modify_defaults_checkbox
|
||||
and self.modify_defaults_checkbox.isChecked()
|
||||
):
|
||||
require_restart = False
|
||||
else:
|
||||
require_restart = self.entity.require_restart
|
||||
|
||||
self.set_state(CategoryState.Working)
|
||||
|
||||
if self.items_are_valid():
|
||||
|
|
@ -442,6 +466,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
|
|||
|
||||
self.saved.emit(self)
|
||||
|
||||
if require_restart:
|
||||
self.restart_required_trigger.emit()
|
||||
self.require_restart_label.setText("")
|
||||
|
||||
def _on_refresh(self):
|
||||
self.reset()
|
||||
|
||||
|
|
|
|||
|
|
@ -275,8 +275,6 @@ class UnsavedChangesDialog(QtWidgets.QDialog):
|
|||
layout.addWidget(message_label)
|
||||
layout.addWidget(btns_widget)
|
||||
|
||||
self.state = None
|
||||
|
||||
def on_cancel_pressed(self):
|
||||
self.done(0)
|
||||
|
||||
|
|
@ -287,6 +285,48 @@ class UnsavedChangesDialog(QtWidgets.QDialog):
|
|||
self.done(2)
|
||||
|
||||
|
||||
class RestartDialog(QtWidgets.QDialog):
|
||||
message = (
|
||||
"Your changes require restart of process to take effect."
|
||||
" Do you want to restart now?"
|
||||
)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(RestartDialog, self).__init__(parent)
|
||||
message_label = QtWidgets.QLabel(self.message)
|
||||
|
||||
btns_widget = QtWidgets.QWidget(self)
|
||||
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
|
||||
|
||||
btn_restart = QtWidgets.QPushButton("Restart")
|
||||
btn_restart.clicked.connect(self.on_restart_pressed)
|
||||
btn_cancel = QtWidgets.QPushButton("Cancel")
|
||||
btn_cancel.clicked.connect(self.on_cancel_pressed)
|
||||
|
||||
btns_layout.addStretch(1)
|
||||
btns_layout.addWidget(btn_restart)
|
||||
btns_layout.addWidget(btn_cancel)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(message_label)
|
||||
layout.addWidget(btns_widget)
|
||||
|
||||
self.btn_cancel = btn_cancel
|
||||
self.btn_restart = btn_restart
|
||||
|
||||
def showEvent(self, event):
|
||||
super(RestartDialog, self).showEvent(event)
|
||||
btns_width = max(self.btn_cancel.width(), self.btn_restart.width())
|
||||
self.btn_cancel.setFixedWidth(btns_width)
|
||||
self.btn_restart.setFixedWidth(btns_width)
|
||||
|
||||
def on_cancel_pressed(self):
|
||||
self.done(0)
|
||||
|
||||
def on_restart_pressed(self):
|
||||
self.done(1)
|
||||
|
||||
|
||||
class SpacerWidget(QtWidgets.QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super(SpacerWidget, self).__init__(parent)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from .categories import (
|
|||
SystemWidget,
|
||||
ProjectWidget
|
||||
)
|
||||
from .widgets import ShadowWidget
|
||||
from .widgets import ShadowWidget, RestartDialog
|
||||
from . import style
|
||||
|
||||
from openpype.tools.settings import (
|
||||
|
|
@ -14,6 +14,8 @@ from openpype.tools.settings import (
|
|||
|
||||
|
||||
class MainWidget(QtWidgets.QWidget):
|
||||
trigger_restart = QtCore.Signal()
|
||||
|
||||
widget_width = 1000
|
||||
widget_height = 600
|
||||
|
||||
|
|
@ -60,6 +62,9 @@ class MainWidget(QtWidgets.QWidget):
|
|||
for tab_widget in tab_widgets:
|
||||
tab_widget.saved.connect(self._on_tab_save)
|
||||
tab_widget.state_changed.connect(self._on_state_change)
|
||||
tab_widget.restart_required_trigger.connect(
|
||||
self._on_restart_required
|
||||
)
|
||||
|
||||
self.tab_widgets = tab_widgets
|
||||
|
||||
|
|
@ -132,3 +137,15 @@ class MainWidget(QtWidgets.QWidget):
|
|||
|
||||
for tab_widget in self.tab_widgets:
|
||||
tab_widget.reset()
|
||||
|
||||
def _on_restart_required(self):
|
||||
# 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:
|
||||
return
|
||||
|
||||
dialog = RestartDialog(self)
|
||||
result = dialog.exec_()
|
||||
if result == 1:
|
||||
self.trigger_restart.emit()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import os
|
||||
import sys
|
||||
import atexit
|
||||
import subprocess
|
||||
|
||||
import platform
|
||||
from avalon import style
|
||||
from Qt import QtCore, QtGui, QtWidgets
|
||||
from openpype.api import Logger, resources
|
||||
from openpype.lib import get_pype_execute_args
|
||||
from openpype.modules import TrayModulesManager, ITrayService
|
||||
from openpype.settings.lib import get_system_settings
|
||||
import openpype.version
|
||||
|
|
@ -92,6 +95,34 @@ class TrayManager:
|
|||
self.tray_widget.menu.addAction(version_action)
|
||||
self.tray_widget.menu.addSeparator()
|
||||
|
||||
def restart(self):
|
||||
"""Restart Tray tool.
|
||||
|
||||
First creates new process with same argument and close current tray.
|
||||
"""
|
||||
args = get_pype_execute_args()
|
||||
# Create a copy of sys.argv
|
||||
additional_args = list(sys.argv)
|
||||
# Check last argument from `get_pype_execute_args`
|
||||
# - when running from code it is the same as first from sys.argv
|
||||
if args[-1] == additional_args[0]:
|
||||
additional_args.pop(0)
|
||||
args.extend(additional_args)
|
||||
|
||||
kwargs = {}
|
||||
if platform.system().lower() == "windows":
|
||||
flags = (
|
||||
subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.DETACHED_PROCESS
|
||||
)
|
||||
kwargs["creationflags"] = flags
|
||||
|
||||
subprocess.Popen(args, **kwargs)
|
||||
self.exit()
|
||||
|
||||
def exit(self):
|
||||
self.tray_widget.exit()
|
||||
|
||||
def on_exit(self):
|
||||
self.modules_manager.on_exit()
|
||||
|
||||
|
|
@ -116,6 +147,8 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
|
|||
|
||||
super(SystemTrayIcon, self).__init__(icon, parent)
|
||||
|
||||
self._exited = False
|
||||
|
||||
# Store parent - QtWidgets.QMainWindow()
|
||||
self.parent = parent
|
||||
|
||||
|
|
@ -134,6 +167,8 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
|
|||
# Add menu to Context of SystemTrayIcon
|
||||
self.setContextMenu(self.menu)
|
||||
|
||||
atexit.register(self.exit)
|
||||
|
||||
def on_systray_activated(self, reason):
|
||||
# show contextMenu if left click
|
||||
if reason == QtWidgets.QSystemTrayIcon.Trigger:
|
||||
|
|
@ -145,6 +180,10 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
|
|||
|
||||
- Icon won't stay in tray after exit.
|
||||
"""
|
||||
if self._exited:
|
||||
return
|
||||
self._exited = True
|
||||
|
||||
self.hide()
|
||||
self.tray_man.on_exit()
|
||||
QtCore.QCoreApplication.exit()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue