Merge branch 'develop' into enhancement/remove-python-2-vendor

This commit is contained in:
Jakub Trllo 2024-06-07 15:07:43 +02:00 committed by GitHub
commit fa42d0217b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 97 additions and 41 deletions

View file

@ -35,14 +35,14 @@ AYON addons should contain separated logic of specific kind of implementation, s
- addon has more logic when used in a tray
- it is possible that addon can be used only in the tray
- abstract methods
- `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_addons`
- `tray_init` - initialization triggered after `initialize` when used in `TrayAddonsManager` and before `connect_with_addons`
- `tray_menu` - add actions to tray widget's menu that represent the addon
- `tray_start` - start of addon's login in tray
- addon is initialized and connected with other addons
- `tray_exit` - addon's cleanup like stop and join threads etc.
- order of calling is based on implementation this order is how it works with `TrayModulesManager`
- order of calling is based on implementation this order is how it works with `TrayAddonsManager`
- it is recommended to import and use GUI implementation only in these methods
- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init`
- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayAddonsManager` to True after `tray_init`
- if addon has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations
### ITrayService

View file

@ -37,14 +37,7 @@ IGNORED_DEFAULT_FILENAMES = (
"base.py",
"interfaces.py",
"click_wrap.py",
"example_addons",
"default_modules",
)
IGNORED_HOSTS_IN_AYON = {
"flame",
"harmony",
}
IGNORED_MODULES_IN_AYON = set()
# When addon was moved from ayon-core codebase
# - this is used to log the missing addon
@ -61,6 +54,7 @@ MOVED_ADDON_MILESTONE_VERSIONS = {
"hiero": VersionInfo(0, 2, 0),
"max": VersionInfo(0, 2, 0),
"photoshop": VersionInfo(0, 2, 0),
"timers_manager": VersionInfo(0, 2, 0),
"traypublisher": VersionInfo(0, 2, 0),
"tvpaint": VersionInfo(0, 2, 0),
"maya": VersionInfo(0, 2, 0),
@ -419,12 +413,6 @@ def _load_addons_in_core(
hosts_dir = os.path.join(AYON_CORE_ROOT, "hosts")
modules_dir = os.path.join(AYON_CORE_ROOT, "modules")
ignored_host_names = set(IGNORED_HOSTS_IN_AYON)
ignored_module_dir_filenames = (
set(IGNORED_DEFAULT_FILENAMES)
| IGNORED_MODULES_IN_AYON
)
for dirpath in {hosts_dir, modules_dir}:
if not os.path.exists(dirpath):
log.warning((
@ -433,10 +421,9 @@ def _load_addons_in_core(
continue
is_in_modules_dir = dirpath == modules_dir
ignored_filenames = set()
if is_in_modules_dir:
ignored_filenames = ignored_module_dir_filenames
else:
ignored_filenames = ignored_host_names
ignored_filenames = set(IGNORED_DEFAULT_FILENAMES)
for filename in os.listdir(dirpath):
# Ignore filenames
@ -502,9 +489,6 @@ def _load_addons_in_core(
def _load_addons():
# Support to use 'openpype' imports
sys.modules["openpype"] = sys.modules["ayon_core"]
# Key under which will be modules imported in `sys.modules`
modules_key = "openpype_modules"

View file

@ -1,10 +0,0 @@
from .version import __version__
from .timers_manager import (
TimersManager
)
__all__ = (
"__version__",
"TimersManager",
)

View file

@ -1,3 +0,0 @@
class InvalidContextError(ValueError):
"""Context for which the timer should be started is invalid."""
pass

View file

@ -1,160 +0,0 @@
import time
from qtpy import QtCore
from pynput import mouse, keyboard
from ayon_core.lib import Logger
class IdleItem:
"""Python object holds information if state of idle changed.
This item is used to be independent from Qt objects.
"""
def __init__(self):
self.changed = False
def reset(self):
self.changed = False
def set_changed(self, changed=True):
self.changed = changed
class IdleManager(QtCore.QThread):
""" Measure user's idle time in seconds.
Idle time resets on keyboard/mouse input.
Is able to emit signals at specific time idle.
"""
time_signals = {}
idle_time = 0
signal_reset_timer = QtCore.Signal()
def __init__(self):
super(IdleManager, self).__init__()
self.log = Logger.get_logger(self.__class__.__name__)
self.signal_reset_timer.connect(self._reset_time)
self.idle_item = IdleItem()
self._is_running = False
self._mouse_thread = None
self._keyboard_thread = None
def add_time_signal(self, emit_time, signal):
""" If any module want to use IdleManager, need to use add_time_signal
Args:
emit_time(int): Time when signal will be emitted.
signal(QtCore.Signal): Signal that will be emitted
(without objects).
"""
if emit_time not in self.time_signals:
self.time_signals[emit_time] = []
self.time_signals[emit_time].append(signal)
@property
def is_running(self):
return self._is_running
def _reset_time(self):
self.idle_time = 0
def stop(self):
self._is_running = False
def _on_mouse_destroy(self):
self._mouse_thread = None
def _on_keyboard_destroy(self):
self._keyboard_thread = None
def run(self):
self.log.info('IdleManager has started')
self._is_running = True
thread_mouse = MouseThread(self.idle_item)
thread_keyboard = KeyboardThread(self.idle_item)
thread_mouse.destroyed.connect(self._on_mouse_destroy)
thread_keyboard.destroyed.connect(self._on_keyboard_destroy)
self._mouse_thread = thread_mouse
self._keyboard_thread = thread_keyboard
thread_mouse.start()
thread_keyboard.start()
# Main loop here is each second checked if idle item changed state
while self._is_running:
if self.idle_item.changed:
self.idle_item.reset()
self.signal_reset_timer.emit()
else:
self.idle_time += 1
if self.idle_time in self.time_signals:
for signal in self.time_signals[self.idle_time]:
signal.emit()
time.sleep(1)
self._post_run()
self.log.info('IdleManager has stopped')
def _post_run(self):
# Stop threads if still exist
if self._mouse_thread is not None:
self._mouse_thread.signal_stop.emit()
self._mouse_thread.terminate()
self._mouse_thread.wait()
if self._keyboard_thread is not None:
self._keyboard_thread.signal_stop.emit()
self._keyboard_thread.terminate()
self._keyboard_thread.wait()
class MouseThread(QtCore.QThread):
"""Listens user's mouse movement."""
signal_stop = QtCore.Signal()
def __init__(self, idle_item):
super(MouseThread, self).__init__()
self.signal_stop.connect(self.stop)
self.m_listener = None
self.idle_item = idle_item
def stop(self):
if self.m_listener is not None:
self.m_listener.stop()
def on_move(self, *args, **kwargs):
self.idle_item.set_changed()
def run(self):
self.m_listener = mouse.Listener(on_move=self.on_move)
self.m_listener.start()
class KeyboardThread(QtCore.QThread):
"""Listens user's keyboard input
"""
signal_stop = QtCore.Signal()
def __init__(self, idle_item):
super(KeyboardThread, self).__init__()
self.signal_stop.connect(self.stop)
self.k_listener = None
self.idle_item = idle_item
def stop(self):
if self.k_listener is not None:
listener = self.k_listener
self.k_listener = None
listener.stop()
def on_press(self, *args, **kwargs):
self.idle_item.set_changed()
def run(self):
self.k_listener = keyboard.Listener(on_press=self.on_press)
self.k_listener.start()

View file

@ -1,44 +0,0 @@
from ayon_applications import PostLaunchHook, LaunchTypes
class PostStartTimerHook(PostLaunchHook):
"""Start timer with TimersManager module.
This module requires enabled TimerManager module.
"""
order = None
launch_types = {LaunchTypes.local}
def execute(self):
project_name = self.data.get("project_name")
folder_path = self.data.get("folder_path")
task_name = self.data.get("task_name")
missing_context_keys = set()
if not project_name:
missing_context_keys.add("project_name")
if not folder_path:
missing_context_keys.add("folder_path")
if not task_name:
missing_context_keys.add("task_name")
if missing_context_keys:
missing_keys_str = ", ".join([
"\"{}\"".format(key) for key in missing_context_keys
])
self.log.debug("Hook {} skipped. Missing data keys: {}".format(
self.__class__.__name__, missing_keys_str
))
return
timers_manager = self.addons_manager.get("timers_manager")
if not timers_manager or not timers_manager.enabled:
self.log.info((
"Skipping starting timer because"
" TimersManager is not available."
))
return
timers_manager.start_timer_with_webserver(
project_name, folder_path, task_name, logger=self.log
)

View file

@ -1,37 +0,0 @@
"""
Requires:
context -> project_settings
context -> ayonAddonsManager
"""
import pyblish.api
class StartTimer(pyblish.api.ContextPlugin):
label = "Start Timer"
order = pyblish.api.IntegratorOrder + 1
hosts = ["*"]
def process(self, context):
timers_manager = context.data["ayonAddonsManager"]["timers_manager"]
if not timers_manager.enabled:
self.log.debug("TimersManager is disabled")
return
project_settings = context.data["project_settings"]
if not project_settings["timers_manager"]["disregard_publishing"]:
self.log.debug("Publish is not affecting running timers.")
return
project_name = context.data["projectName"]
folder_path = context.data.get("folderPath")
task_name = context.data.get("task")
if not project_name or not folder_path or not task_name:
self.log.info((
"Current context does not contain all"
" required information to start a timer."
))
return
timers_manager.start_timer_with_webserver(
project_name, folder_path, task_name, self.log
)

View file

@ -1,27 +0,0 @@
"""
Requires:
context -> project_settings
context -> ayonAddonsManager
"""
import pyblish.api
class StopTimer(pyblish.api.ContextPlugin):
label = "Stop Timer"
order = pyblish.api.ExtractorOrder - 0.49
hosts = ["*"]
def process(self, context):
timers_manager = context.data["ayonAddonsManager"]["timers_manager"]
if not timers_manager.enabled:
self.log.debug("TimersManager is disabled")
return
project_settings = context.data["project_settings"]
if not project_settings["timers_manager"]["disregard_publishing"]:
self.log.debug("Publish is not affecting running timers.")
return
timers_manager.stop_timer_with_webserver(self.log)

View file

@ -1,85 +0,0 @@
import json
from aiohttp.web_response import Response
from ayon_core.lib import Logger
class TimersManagerModuleRestApi:
"""
REST API endpoint used for calling from hosts when context change
happens in Workfile app.
"""
def __init__(self, user_module, server_manager):
self._log = None
self.module = user_module
self.server_manager = server_manager
self.prefix = "/timers_manager"
self.register()
@property
def log(self):
if self._log is None:
self._log = Logger.get_logger(self.__class__.__name__)
return self._log
def register(self):
self.server_manager.add_route(
"POST",
self.prefix + "/start_timer",
self.start_timer
)
self.server_manager.add_route(
"POST",
self.prefix + "/stop_timer",
self.stop_timer
)
self.server_manager.add_route(
"GET",
self.prefix + "/get_task_time",
self.get_task_time
)
async def start_timer(self, request):
data = await request.json()
try:
project_name = data["project_name"]
folder_path = data["folder_path"]
task_name = data["task_name"]
except KeyError:
msg = (
"Payload must contain fields 'project_name,"
" 'folder_path' and 'task_name'"
)
self.log.error(msg)
return Response(status=400, message=msg)
self.module.stop_timers()
try:
self.module.start_timer(project_name, folder_path, task_name)
except Exception as exc:
return Response(status=404, message=str(exc))
return Response(status=200)
async def stop_timer(self, request):
self.module.stop_timers()
return Response(status=200)
async def get_task_time(self, request):
data = await request.json()
try:
project_name = data["project_name"]
folder_path = data["folder_path"]
task_name = data["task_name"]
except KeyError:
message = (
"Payload must contain fields 'project_name, 'folder_path',"
" 'task_name'"
)
self.log.warning(message)
return Response(text=message, status=404)
time = self.module.get_task_time(project_name, folder_path, task_name)
return Response(text=json.dumps(time))

View file

@ -1,488 +0,0 @@
import os
import platform
import ayon_api
from ayon_core.addon import (
AYONAddon,
ITrayService,
IPluginPaths
)
from ayon_core.lib.events import register_event_callback
from .version import __version__
from .exceptions import InvalidContextError
TIMER_MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
class ExampleTimersManagerConnector:
"""Timers manager can handle timers of multiple modules/addons.
Module must have object under `timers_manager_connector` attribute with
few methods. This is example class of the object that could be stored under
module.
Required methods are 'stop_timer' and 'start_timer'.
Example of `data` that are passed during changing timer:
```
data = {
"project_name": project_name,
"folder_id": folder_id,
"folder_path": folder_entity["path"],
"task_name": task_name,
"task_type": task_type,
# Deprecated
"asset_id": folder_id,
"asset_name": folder_entity["name"],
"hierarchy": hierarchy_items,
}
```
"""
# Not needed at all
def __init__(self, module):
# Store timer manager module to be able call it's methods when needed
self._timers_manager_module = None
# Store module which want to use timers manager to have access
self._module = module
# Required
def stop_timer(self):
"""Called by timers manager when module should stop timer."""
self._module.stop_timer()
# Required
def start_timer(self, data):
"""Method called by timers manager when should start timer."""
self._module.start_timer(data)
# Optional
def register_timers_manager(self, timer_manager_module):
"""Method called by timers manager where it's object is passed.
This is moment when timers manager module can be store to be able
call it's callbacks (e.g. timer started).
"""
self._timers_manager_module = timer_manager_module
# Custom implementation
def timer_started(self, data):
"""This is example of possibility to trigger callbacks on manager."""
if self._timers_manager_module is not None:
self._timers_manager_module.timer_started(self._module.id, data)
# Custom implementation
def timer_stopped(self):
if self._timers_manager_module is not None:
self._timers_manager_module.timer_stopped(self._module.id)
class TimersManager(
AYONAddon,
ITrayService,
IPluginPaths
):
""" Handles about Timers.
Should be able to start/stop all timers at once.
To be able use this advantage module has to have attribute with name
`timers_manager_connector` which has two methods 'stop_timer'
and 'start_timer'. Optionally may have `register_timers_manager` where
object of TimersManager module is passed to be able call it's callbacks.
See `ExampleTimersManagerConnector`.
"""
name = "timers_manager"
version = __version__
label = "Timers Service"
_required_methods = (
"stop_timer",
"start_timer"
)
def initialize(self, studio_settings):
timers_settings = studio_settings.get(self.name)
enabled = timers_settings is not None
auto_stop = False
full_time = 0
message_time = 0
if enabled:
# When timer will stop if idle manager is running (minutes)
full_time = int(timers_settings["full_time"] * 60)
# How many minutes before the timer is stopped will popup the message
message_time = int(timers_settings["message_time"] * 60)
auto_stop = timers_settings["auto_stop"]
platform_name = platform.system().lower()
# Turn of auto stop on MacOs because pynput requires root permissions
# and on linux can cause thread locks on application close
if full_time <= 0 or platform_name in ("darwin", "linux"):
auto_stop = False
self.enabled = enabled
self.auto_stop = auto_stop
self.time_show_message = full_time - message_time
self.time_stop_timer = full_time
self.is_running = False
self.last_task = None
# Tray attributes
self._signal_handler = None
self._widget_user_idle = None
self._idle_manager = None
self._connectors_by_module_id = {}
self._modules_by_id = {}
def tray_init(self):
if not self.auto_stop:
return
from .idle_threads import IdleManager
from .widget_user_idle import WidgetUserIdle, SignalHandler
signal_handler = SignalHandler(self)
idle_manager = IdleManager()
widget_user_idle = WidgetUserIdle(self)
widget_user_idle.set_countdown_start(
self.time_stop_timer - self.time_show_message
)
idle_manager.signal_reset_timer.connect(
widget_user_idle.reset_countdown
)
idle_manager.add_time_signal(
self.time_show_message, signal_handler.signal_show_message
)
idle_manager.add_time_signal(
self.time_stop_timer, signal_handler.signal_stop_timers
)
self._signal_handler = signal_handler
self._widget_user_idle = widget_user_idle
self._idle_manager = idle_manager
def tray_start(self, *_a, **_kw):
if self._idle_manager:
self._idle_manager.start()
def tray_exit(self):
if self._idle_manager:
self._idle_manager.stop()
self._idle_manager.wait()
def get_timer_data_for_path(self, task_path):
"""Convert string path to a timer data.
It is expected that first item is project name, last item is task name
and folder path in the middle.
"""
path_items = task_path.split("/")
task_name = path_items.pop(-1)
project_name = path_items.pop(0)
folder_path = "/" + "/".join(path_items)
return self.get_timer_data_for_context(
project_name, folder_path, task_name, self.log
)
def get_launch_hook_paths(self):
"""Implementation for applications launch hooks."""
return [
os.path.join(TIMER_MODULE_DIR, "launch_hooks")
]
def get_plugin_paths(self):
"""Implementation of `IPluginPaths`."""
return {
"publish": [os.path.join(TIMER_MODULE_DIR, "plugins", "publish")]
}
@staticmethod
def get_timer_data_for_context(
project_name, folder_path, task_name, logger=None
):
"""Prepare data for timer related callbacks."""
if not project_name or not folder_path or not task_name:
raise InvalidContextError((
"Missing context information got"
" Project: \"{}\" Folder: \"{}\" Task: \"{}\""
).format(str(project_name), str(folder_path), str(task_name)))
folder_entity = ayon_api.get_folder_by_path(
project_name,
folder_path,
fields={"id", "name", "path"}
)
if not folder_entity:
raise InvalidContextError((
"Folder \"{}\" not found in project \"{}\""
).format(folder_path, project_name))
folder_id = folder_entity["id"]
task_entity = ayon_api.get_task_by_name(
project_name, folder_id, task_name
)
if not task_entity:
raise InvalidContextError((
"Task \"{}\" not found on folder \"{}\" in project \"{}\""
).format(task_name, folder_path, project_name))
task_type = ""
try:
task_type = task_entity["taskType"]
except KeyError:
msg = "Couldn't find task_type for {}".format(task_name)
if logger is not None:
logger.warning(msg)
else:
print(msg)
hierarchy_items = folder_entity["path"].split("/")
hierarchy_items.pop(0)
return {
"project_name": project_name,
"folder_id": folder_id,
"folder_path": folder_entity["path"],
"task_name": task_name,
"task_type": task_type,
"asset_id": folder_id,
"asset_name": folder_entity["name"],
"hierarchy": hierarchy_items,
}
def start_timer(self, project_name, folder_path, task_name):
"""Start timer for passed context.
Args:
project_name (str): Project name.
folder_path (str): Folder path.
task_name (str): Task name.
"""
data = self.get_timer_data_for_context(
project_name, folder_path, task_name, self.log
)
self.timer_started(None, data)
def get_task_time(self, project_name, folder_path, task_name):
"""Get total time for passed context.
TODO:
- convert context to timer data
"""
times = {}
for module_id, connector in self._connectors_by_module_id.items():
if hasattr(connector, "get_task_time"):
module = self._modules_by_id[module_id]
times[module.name] = connector.get_task_time(
project_name, folder_path, task_name
)
return times
def timer_started(self, source_id, data):
"""Connector triggered that timer has started.
New timer has started for context in data.
"""
for module_id, connector in self._connectors_by_module_id.items():
if module_id == source_id:
continue
try:
connector.start_timer(data)
except Exception:
self.log.info(
"Failed to start timer on connector {}".format(
str(connector)
)
)
self.last_task = data
self.is_running = True
def timer_stopped(self, source_id):
"""Connector triggered that hist timer has stopped.
Should stop all other timers.
TODO:
- pass context for which timer has stopped to validate if timers are
same and valid
"""
for module_id, connector in self._connectors_by_module_id.items():
if module_id == source_id:
continue
try:
connector.stop_timer()
except Exception:
self.log.info(
"Failed to stop timer on connector {}".format(
str(connector)
)
)
def restart_timers(self):
if self.last_task is not None:
self.timer_started(None, self.last_task)
def stop_timers(self):
"""Stop all timers."""
if self.is_running is False:
return
if self._widget_user_idle is not None:
self._widget_user_idle.set_timer_stopped()
self.is_running = False
self.timer_stopped(None)
def connect_with_addons(self, enabled_modules):
for module in enabled_modules:
connector = getattr(module, "timers_manager_connector", None)
if connector is None:
continue
missing_methods = set()
for method_name in self._required_methods:
if not hasattr(connector, method_name):
missing_methods.add(method_name)
if missing_methods:
joined = ", ".join(
['"{}"'.format(name for name in missing_methods)]
)
self.log.info((
"Module \"{}\" has missing required methods {}."
).format(module.name, joined))
continue
self._connectors_by_module_id[module.id] = connector
self._modules_by_id[module.id] = module
# Optional method
if hasattr(connector, "register_timers_manager"):
try:
connector.register_timers_manager(self)
except Exception:
self.log.info((
"Failed to register timers manager"
" for connector of module \"{}\"."
).format(module.name))
def show_message(self):
if self.is_running is False:
return
if not self._widget_user_idle.is_showed():
self._widget_user_idle.reset_countdown()
self._widget_user_idle.show()
# Webserver module implementation
def webserver_initialization(self, server_manager):
"""Add routes for timers to be able start/stop with rest api."""
if self.tray_initialized:
from .rest_api import TimersManagerModuleRestApi
self.rest_api_obj = TimersManagerModuleRestApi(
self, server_manager
)
@staticmethod
def start_timer_with_webserver(
project_name, folder_path, task_name, logger=None
):
"""Prepared method for calling change timers on REST api.
Webserver must be active. At the moment is Webserver running only when
OpenPype Tray is used.
Args:
project_name (str): Project name.
folder_path (str): Folder path.
task_name (str): Task name.
logger (logging.Logger): Logger object. Using 'print' if not
passed.
"""
webserver_url = os.environ.get("AYON_WEBSERVER_URL")
if not webserver_url:
msg = "Couldn't find webserver url"
if logger is not None:
logger.warning(msg)
else:
print(msg)
return
rest_api_url = "{}/timers_manager/start_timer".format(webserver_url)
try:
import requests
except Exception:
msg = "Couldn't start timer ('requests' is not available)"
if logger is not None:
logger.warning(msg)
else:
print(msg)
return
data = {
"project_name": project_name,
"folder_path": folder_path,
"task_name": task_name
}
return requests.post(rest_api_url, json=data)
@staticmethod
def stop_timer_with_webserver(logger=None):
"""Prepared method for calling stop timers on REST api.
Args:
logger (logging.Logger): Logger used for logging messages.
"""
webserver_url = os.environ.get("AYON_WEBSERVER_URL")
if not webserver_url:
msg = "Couldn't find webserver url"
if logger is not None:
logger.warning(msg)
else:
print(msg)
return
rest_api_url = "{}/timers_manager/stop_timer".format(webserver_url)
try:
import requests
except Exception:
msg = "Couldn't start timer ('requests' is not available)"
if logger is not None:
logger.warning(msg)
else:
print(msg)
return
return requests.post(rest_api_url)
def on_host_install(self, host, host_name, project_name):
self.log.debug("Installing task changed callback")
register_event_callback("taskChanged", self._on_host_task_change)
def _on_host_task_change(self, event):
project_name = event["project_name"]
folder_path = event["folder_path"]
task_name = event["task_name"]
self.log.debug((
"Sending message that timer should change to"
" Project: {} Folder: {} Task: {}"
).format(project_name, folder_path, task_name))
self.start_timer_with_webserver(
project_name, folder_path, task_name, self.log
)

View file

@ -1 +0,0 @@
__version__ = "0.1.1"

View file

@ -1,196 +0,0 @@
from qtpy import QtCore, QtGui, QtWidgets
from ayon_core import resources, style
class WidgetUserIdle(QtWidgets.QWidget):
SIZE_W = 300
SIZE_H = 160
def __init__(self, module):
super(WidgetUserIdle, self).__init__()
self.setWindowTitle("AYON - Stop timers")
icon = QtGui.QIcon(resources.get_ayon_icon_filepath())
self.setWindowIcon(icon)
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint
| QtCore.Qt.WindowMinimizeButtonHint
| QtCore.Qt.WindowStaysOnTopHint
)
self._is_showed = False
self._timer_stopped = False
self._countdown = 0
self._countdown_start = 0
self.module = module
msg_info = "You didn't work for a long time."
msg_question = "Would you like to stop Timers?"
msg_stopped = (
"Your Timers were stopped. Do you want to start them again?"
)
lbl_info = QtWidgets.QLabel(msg_info, self)
lbl_info.setTextFormat(QtCore.Qt.RichText)
lbl_info.setWordWrap(True)
lbl_question = QtWidgets.QLabel(msg_question, self)
lbl_question.setTextFormat(QtCore.Qt.RichText)
lbl_question.setWordWrap(True)
lbl_stopped = QtWidgets.QLabel(msg_stopped, self)
lbl_stopped.setTextFormat(QtCore.Qt.RichText)
lbl_stopped.setWordWrap(True)
lbl_rest_time = QtWidgets.QLabel(self)
lbl_rest_time.setTextFormat(QtCore.Qt.RichText)
lbl_rest_time.setWordWrap(True)
lbl_rest_time.setAlignment(QtCore.Qt.AlignCenter)
form = QtWidgets.QFormLayout()
form.setContentsMargins(10, 15, 10, 5)
form.addRow(lbl_info)
form.addRow(lbl_question)
form.addRow(lbl_stopped)
form.addRow(lbl_rest_time)
btn_stop = QtWidgets.QPushButton("Stop timer", self)
btn_stop.setToolTip("Stop's All timers")
btn_continue = QtWidgets.QPushButton("Continue", self)
btn_continue.setToolTip("Timer won't stop")
btn_close = QtWidgets.QPushButton("Close", self)
btn_close.setToolTip("Close window")
btn_restart = QtWidgets.QPushButton("Start timers", self)
btn_restart.setToolTip("Timer will be started again")
group_layout = QtWidgets.QHBoxLayout()
group_layout.addStretch(1)
group_layout.addWidget(btn_continue)
group_layout.addWidget(btn_stop)
group_layout.addWidget(btn_restart)
group_layout.addWidget(btn_close)
layout = QtWidgets.QVBoxLayout(self)
layout.addLayout(form)
layout.addLayout(group_layout)
count_timer = QtCore.QTimer()
count_timer.setInterval(1000)
btn_stop.clicked.connect(self._on_stop_clicked)
btn_continue.clicked.connect(self._on_continue_clicked)
btn_close.clicked.connect(self._close_widget)
btn_restart.clicked.connect(self._on_restart_clicked)
count_timer.timeout.connect(self._on_count_timeout)
self.lbl_info = lbl_info
self.lbl_question = lbl_question
self.lbl_stopped = lbl_stopped
self.lbl_rest_time = lbl_rest_time
self.btn_stop = btn_stop
self.btn_continue = btn_continue
self.btn_close = btn_close
self.btn_restart = btn_restart
self._count_timer = count_timer
self.resize(self.SIZE_W, self.SIZE_H)
self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))
self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100))
self.setStyleSheet(style.load_stylesheet())
def set_countdown_start(self, countdown):
self._countdown_start = countdown
if not self.is_showed():
self.reset_countdown()
def reset_countdown(self):
self._countdown = self._countdown_start
self._update_countdown_label()
def is_showed(self):
return self._is_showed
def set_timer_stopped(self):
self._timer_stopped = True
self._refresh_context()
def _update_countdown_label(self):
self.lbl_rest_time.setText(str(self._countdown))
def _on_count_timeout(self):
if self._timer_stopped or not self._is_showed:
self._count_timer.stop()
return
if self._countdown <= 0:
self._stop_timers()
self.set_timer_stopped()
else:
self._countdown -= 1
self._update_countdown_label()
def _refresh_context(self):
self.lbl_question.setVisible(not self._timer_stopped)
self.lbl_rest_time.setVisible(not self._timer_stopped)
self.lbl_stopped.setVisible(self._timer_stopped)
self.btn_continue.setVisible(not self._timer_stopped)
self.btn_stop.setVisible(not self._timer_stopped)
self.btn_restart.setVisible(self._timer_stopped)
self.btn_close.setVisible(self._timer_stopped)
def _stop_timers(self):
self.module.stop_timers()
def _on_stop_clicked(self):
self._stop_timers()
self._close_widget()
def _on_restart_clicked(self):
self.module.restart_timers()
self._close_widget()
def _on_continue_clicked(self):
self._close_widget()
def _close_widget(self):
self._is_showed = False
self._timer_stopped = False
self._refresh_context()
self.hide()
def showEvent(self, event):
if not self._is_showed:
self._is_showed = True
self._refresh_context()
if not self._count_timer.isActive():
self._count_timer.start()
super(WidgetUserIdle, self).showEvent(event)
def closeEvent(self, event):
event.ignore()
if self._timer_stopped:
self._close_widget()
else:
self._on_continue_clicked()
class SignalHandler(QtCore.QObject):
signal_show_message = QtCore.Signal()
signal_stop_timers = QtCore.Signal()
def __init__(self, module):
super(SignalHandler, self).__init__()
self.module = module
self.signal_show_message.connect(module.show_message)
self.signal_stop_timers.connect(module.stop_timers)

View file

@ -13,9 +13,7 @@ qtawesome = "0.7.3"
[ayon.runtimeDependencies]
aiohttp-middlewares = "^2.0.0"
wsrpc_aiohttp = "^3.1.1" # websocket server
Click = "^8"
OpenTimelineIO = "0.16.0"
opencolorio = "2.2.1"
Pillow = "9.5.0"
pynput = "^1.7.2" # Timers manager - TODO remove