aboutsummaryrefslogtreecommitdiffstats
path: root/src/avp/libcomponent/component.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/avp/libcomponent/component.py (renamed from src/avp/component.py)439
1 files changed, 22 insertions, 417 deletions
diff --git a/src/avp/component.py b/src/avp/libcomponent/component.py
index 5906ab1..1f81e07 100644
--- a/src/avp/component.py
+++ b/src/avp/libcomponent/component.py
@@ -4,274 +4,26 @@ on making a valid component.
"""
from PyQt6 import uic, QtCore, QtWidgets
-from PyQt6.QtGui import QColor, QUndoCommand
+from PyQt6.QtGui import QColor
import os
-import sys
import math
-import time
import logging
from copy import copy
-from .toolkit.frame import BlankFrame
-from .toolkit import (
+from .metaclass import ComponentMetaclass
+from .actions import ComponentUpdate
+from .exceptions import ComponentError
+from ..toolkit.frame import BlankFrame
+
+from ..toolkit import (
getWidgetValue,
setWidgetValue,
- connectWidget,
rgbFromString,
randomColor,
blockSignals,
)
-
-log = logging.getLogger("AVP.ComponentHandler")
-
-
-class ComponentMetaclass(type(QtCore.QObject)):
- """
- Checks the validity of each Component class and mutates some attrs.
- E.g., takes only major version from version string & decorates methods
- """
-
- def initializationWrapper(func):
- def initializationWrapper(self, *args, **kwargs):
- try:
- return func(self, *args, **kwargs)
- except Exception:
- try:
- raise ComponentError(self, "initialization process")
- except ComponentError:
- return
-
- return initializationWrapper
-
- def renderWrapper(func):
- def renderWrapper(self, *args, **kwargs):
- try:
- log.verbose(
- "### %s #%s renders a preview frame ###",
- self.__class__.name,
- str(self.compPos),
- )
- return func(self, *args, **kwargs)
- except Exception as e:
- try:
- if e.__class__.__name__.startswith("Component"):
- raise
- else:
- raise ComponentError(self, "renderer")
- except ComponentError:
- return BlankFrame()
-
- return renderWrapper
-
- def commandWrapper(func):
- """Intercepts the command() method to check for global args"""
-
- def commandWrapper(self, arg):
- if arg.startswith("preset="):
- _, preset = arg.split("=", 1)
- path = os.path.join(self.core.getPresetDir(self), preset)
- if not os.path.exists(path):
- print('Couldn\'t locate preset "%s"' % preset)
- quit(1)
- else:
- print('Opening "%s" preset on layer %s' % (preset, self.compPos))
- self.core.openPreset(path, self.compPos, preset)
- # Don't call the component's command() method
- return
- else:
- return func(self, arg)
-
- return commandWrapper
-
- def propertiesWrapper(func):
- """Intercepts the usual properties if the properties are locked."""
-
- def propertiesWrapper(self):
- if self._lockedProperties is not None:
- return self._lockedProperties
- else:
- try:
- return func(self)
- except Exception:
- try:
- raise ComponentError(self, "properties")
- except ComponentError:
- return []
-
- return propertiesWrapper
-
- def errorWrapper(func):
- """Intercepts the usual error message if it is locked."""
-
- def errorWrapper(self):
- if self._lockedError is not None:
- return self._lockedError
- else:
- return func(self)
-
- return errorWrapper
-
- def loadPresetWrapper(func):
- """Wraps loadPreset to handle the self.openingPreset boolean"""
-
- class openingPreset:
- def __init__(self, comp):
- self.comp = comp
-
- def __enter__(self):
- self.comp.openingPreset = True
-
- def __exit__(self, *args):
- self.comp.openingPreset = False
-
- def presetWrapper(self, *args):
- with openingPreset(self):
- try:
- return func(self, *args)
- except Exception:
- try:
- raise ComponentError(self, "preset loader")
- except ComponentError:
- return
-
- return presetWrapper
-
- def updateWrapper(func):
- """
- Calls _preUpdate before every subclass update().
- Afterwards, for non-user updates, calls _autoUpdate().
- For undoable updates triggered by the user, calls _userUpdate()
- """
-
- class wrap:
- def __init__(self, comp, auto):
- self.comp = comp
- self.auto = auto
-
- def __enter__(self):
- self.comp._preUpdate()
-
- def __exit__(self, *args):
- if (
- self.auto
- or self.comp.openingPreset
- or not hasattr(self.comp.parent, "undoStack")
- ):
- log.verbose("Automatic update")
- self.comp._autoUpdate()
- else:
- log.verbose("User update")
- self.comp._userUpdate()
-
- def updateWrapper(self, **kwargs):
- auto = kwargs["auto"] if "auto" in kwargs else False
- with wrap(self, auto):
- try:
- return func(self)
- except Exception:
- try:
- raise ComponentError(self, "update method")
- except ComponentError:
- return
-
- return updateWrapper
-
- def widgetWrapper(func):
- """Connects all widgets to update method after the subclass's method"""
-
- class wrap:
- def __init__(self, comp):
- self.comp = comp
-
- def __enter__(self):
- pass
-
- def __exit__(self, *args):
- for widgetList in self.comp._allWidgets.values():
- for widget in widgetList:
- log.verbose("Connecting %s", str(widget.__class__.__name__))
- connectWidget(widget, self.comp.update)
-
- def widgetWrapper(self, *args, **kwargs):
- auto = kwargs["auto"] if "auto" in kwargs else False
- with wrap(self):
- try:
- return func(self, *args, **kwargs)
- except Exception:
- try:
- raise ComponentError(self, "widget creation")
- except ComponentError:
- return
-
- return widgetWrapper
-
- def __new__(cls, name, parents, attrs):
- if "ui" not in attrs:
- # Use module name as ui filename by default
- attrs["ui"] = (
- "%s.ui" % os.path.splitext(attrs["__module__"].split(".")[-1])[0]
- )
-
- decorate = (
- "names", # Class methods
- "error",
- "audio",
- "properties", # Properties
- "preFrameRender",
- "previewRender",
- "loadPreset",
- "command",
- "update",
- "widget",
- )
-
- # Auto-decorate methods
- for key in decorate:
- if key not in attrs:
- continue
- if key in ("names"):
- attrs[key] = classmethod(attrs[key])
- elif key in ("audio"):
- attrs[key] = property(attrs[key])
- elif key == "command":
- attrs[key] = cls.commandWrapper(attrs[key])
- elif key == "previewRender":
- attrs[key] = cls.renderWrapper(attrs[key])
- elif key == "preFrameRender":
- attrs[key] = cls.initializationWrapper(attrs[key])
- elif key == "properties":
- attrs[key] = cls.propertiesWrapper(attrs[key])
- elif key == "error":
- attrs[key] = cls.errorWrapper(attrs[key])
- elif key == "loadPreset":
- attrs[key] = cls.loadPresetWrapper(attrs[key])
- elif key == "update":
- attrs[key] = cls.updateWrapper(attrs[key])
- elif key == "widget" and parents[0] != QtCore.QObject:
- attrs[key] = cls.widgetWrapper(attrs[key])
-
- # Turn version string into a number
- try:
- if "version" not in attrs:
- log.error(
- "No version attribute in %s. Defaulting to 1",
- attrs["name"],
- )
- attrs["version"] = 1
- else:
- attrs["version"] = int(attrs["version"].split(".")[0])
- except ValueError:
- log.critical(
- "%s component has an invalid version string:\n%s",
- attrs["name"],
- str(attrs["version"]),
- )
- except KeyError:
- log.critical("%s component has no version string.", attrs["name"])
- else:
- return super().__new__(cls, name, parents, attrs)
- quit(1)
+log = logging.getLogger("AVP.BaseComponent")
class Component(QtCore.QObject, metaclass=ComponentMetaclass):
@@ -340,9 +92,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
pprint.pformat(preset),
)
- # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Render Methods
- # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
def previewRender(self):
image = BlankFrame(self.width, self.height)
@@ -371,15 +123,18 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def postFrameRender(self):
pass
- # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Properties
- # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
def properties(self):
"""
- Return a list of properties to signify if your component is
- non-animated ('static'), returns sound ('audio'), or has
- encountered an error in configuration ('error').
+ Return a list of properties with certain meanings:
+ `static`: non-animated
+ `audio`: has extra sound to add
+ `error`: bad configuration
+ `pcm`: request raw audio data
+ `composite`: request frame to draw on
"""
return []
@@ -403,9 +158,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
https://ffmpeg.org/ffmpeg-filters.html
"""
- # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
# Idle Methods
- # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
def widget(self, parent):
"""
@@ -510,9 +265,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.commandHelp()
quit(0)
- # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
# "Private" Methods
- # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
+ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
def _preUpdate(self):
"""Happens before subclass update()"""
for attr in self._relativeWidgets:
@@ -826,153 +581,3 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
maxRes = int(self.core.resolutions[0].split("x")[0])
newMaximumValue = self.width * (self._relativeMaximums[attr] / maxRes)
self._trackedWidgets[attr].setMaximum(int(newMaximumValue))
-
-
-class ComponentError(RuntimeError):
- """Gives the MainWindow a traceback to display, and cancels the export."""
-
- prevErrors = []
- lastTime = time.time()
-
- def __init__(self, caller, name, msg=None):
- if msg is None and sys.exc_info()[0] is not None:
- msg = str(sys.exc_info()[1])
- else:
- msg = "Unknown error."
- log.error("ComponentError by %s's %s: %s" % (caller.name, name, msg))
-
- # Don't create multiple windows for quickly repeated messages
- if len(ComponentError.prevErrors) > 1:
- ComponentError.prevErrors.pop()
- ComponentError.prevErrors.insert(0, name)
- curTime = time.time()
- if (
- name in ComponentError.prevErrors[1:]
- and curTime - ComponentError.lastTime < 1.0
- ):
- return
- ComponentError.lastTime = time.time()
-
- from .toolkit import formatTraceback
-
- if sys.exc_info()[0] is not None:
- string = "%s component (#%s): %s encountered %s %s: %s" % (
- caller.__class__.name,
- str(caller.compPos),
- name,
- (
- "an"
- if any(
- [
- sys.exc_info()[0].__name__.startswith(vowel)
- for vowel in ("A", "I", "U", "O", "E")
- ]
- )
- else "a"
- ),
- sys.exc_info()[0].__name__,
- str(sys.exc_info()[1]),
- )
- detail = formatTraceback(sys.exc_info()[2])
- else:
- string = name
- detail = "Attributes:\n%s" % (
- "\n".join([m for m in dir(caller) if not m.startswith("_")])
- )
-
- super().__init__(string)
- caller.lockError(string)
- caller._error.emit(string, detail)
-
-
-class ComponentUpdate(QUndoCommand):
- """Command object for making a component action undoable"""
-
- def __init__(self, parent, oldWidgetVals, modifiedVals):
- super().__init__("change %s component #%s" % (parent.name, parent.compPos))
- self.undone = False
- self.res = (int(parent.width), int(parent.height))
- self.parent = parent
- self.oldWidgetVals = {
- attr: (
- copy(val)
- if attr not in self.parent._relativeWidgets
- else self.parent.floatValForAttr(attr, val, axis=self.res)
- )
- for attr, val in oldWidgetVals.items()
- if attr in modifiedVals
- }
- self.modifiedVals = {
- attr: (
- val
- if attr not in self.parent._relativeWidgets
- else self.parent.floatValForAttr(attr, val, axis=self.res)
- )
- for attr, val in modifiedVals.items()
- }
-
- # Because relative widgets change themselves every update based on
- # their previous value, we must store ALL their values in case of undo
- self.relativeWidgetValsAfterUndo = {
- attr: copy(getattr(self.parent, attr))
- for attr in self.parent._relativeWidgets
- }
-
- # Determine if this update is mergeable
- self.id_ = -1
- if self.parent.mergeUndo:
- if len(self.modifiedVals) == 1:
- attr, val = self.modifiedVals.popitem()
- self.id_ = sum([ord(letter) for letter in attr[-14:]])
- self.modifiedVals[attr] = val
- return
- log.warning(
- "%s component settings changed at once. (%s)",
- len(self.modifiedVals),
- repr(self.modifiedVals),
- )
-
- def id(self):
- """If 2 consecutive updates have same id, Qt will call mergeWith()"""
- return self.id_
-
- def mergeWith(self, other):
- self.modifiedVals.update(other.modifiedVals)
- return True
-
- def setWidgetValues(self, attrDict):
- """
- Mask the component's usual method to handle our
- relative widgets in case the resolution has changed.
- """
- newAttrDict = {
- attr: (
- val
- if attr not in self.parent._relativeWidgets
- else self.parent.pixelValForAttr(attr, val)
- )
- for attr, val in attrDict.items()
- }
- self.parent.setWidgetValues(newAttrDict)
-
- def redo(self):
- if self.undone:
- log.info("Redoing component update")
- self.parent.oldAttrs = self.relativeWidgetValsAfterUndo
- self.setWidgetValues(self.modifiedVals)
- self.parent.update(auto=True)
- self.parent.oldAttrs = None
- if not self.undone:
- self.relativeWidgetValsAfterRedo = {
- attr: copy(getattr(self.parent, attr))
- for attr in self.parent._relativeWidgets
- }
- self.parent._sendUpdateSignal()
-
- def undo(self):
- log.info("Undoing component update")
- self.undone = True
- self.parent.oldAttrs = self.relativeWidgetValsAfterRedo
- self.setWidgetValues(self.oldWidgetVals)
- self.parent.update(auto=True)
- self.parent.oldAttrs = None