mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge branch 'develop' into enhancement/remove-python-2-vendor
This commit is contained in:
commit
fa42d0217b
53 changed files with 97 additions and 41 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
from .version import __version__
|
||||
from .timers_manager import (
|
||||
TimersManager
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"__version__",
|
||||
|
||||
"TimersManager",
|
||||
)
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
class InvalidContextError(ValueError):
|
||||
"""Context for which the timer should be started is invalid."""
|
||||
pass
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -1 +0,0 @@
|
|||
__version__ = "0.1.1"
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue