fixed issues with undoing relative widgets

This commit is contained in:
tassaron 2017-08-19 18:32:12 -04:00
parent 87e762a8aa
commit c07f2426ce
17 changed files with 215 additions and 81 deletions

View File

@ -9,6 +9,7 @@ import sys
import math
import time
import logging
from copy import copy
from toolkit.frame import BlankFrame
from toolkit import (
@ -113,14 +114,20 @@ class ComponentMetaclass(type(QtCore.QObject)):
def presetWrapper(self, *args):
with openingPreset(self):
return func(self, *args)
try:
return func(self, *args)
except Exception:
try:
raise ComponentError(self, 'preset loader')
except ComponentError:
return
return presetWrapper
def updateWrapper(func):
'''
For undoable updates triggered by the user,
call _userUpdate() after the subclass's update() method.
For non-user updates, call _autoUpdate()
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):
@ -128,24 +135,57 @@ class ComponentMetaclass(type(QtCore.QObject)):
self.auto = auto
def __enter__(self):
pass
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 = False
if 'auto' in kwargs:
auto = kwargs['auto']
auto = kwargs['auto'] if 'auto' in kwargs else False
with wrap(self, auto):
return func(self)
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
@ -153,13 +193,12 @@ class ComponentMetaclass(type(QtCore.QObject)):
attrs['__module__'].split('.')[-1]
)[0]
# if parents[0] == QtCore.QObject: else:
decorate = (
'names', # Class methods
'error', 'audio', 'properties', # Properties
'preFrameRender', 'previewRender',
'frameRender', 'command',
'loadPreset', 'update'
'loadPreset', 'update', 'widget',
)
# Auto-decorate methods
@ -184,6 +223,8 @@ class ComponentMetaclass(type(QtCore.QObject)):
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:
@ -224,23 +265,28 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.moduleIndex = moduleIndex
self.compPos = compPos
self.core = core
self.currentPreset = None
self.openingPreset = False
# STATUS VARIABLES
self.currentPreset = None
self._allWidgets = {}
self._trackedWidgets = {}
self._presetNames = {}
self._commandArgs = {}
self._colorWidgets = {}
self._colorFuncs = {}
self._relativeWidgets = {}
# pixel values stored as floats
# Pixel values stored as floats
self._relativeValues = {}
# maximum values of spinBoxes at 1080p (Core.resolutions[0])
# Maximum values of spinBoxes at 1080p (Core.resolutions[0])
self._relativeMaximums = {}
# LOCKING VARIABLES
self.openingPreset = False
self._lockedProperties = None
self._lockedError = None
self._lockedSize = None
# If set to a dict, values are used as basis to update relative widgets
self.oldAttrs = None
# Stop lengthy processes in response to this variable
self.canceled = False
@ -338,21 +384,21 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'''
self.parent = parent
self.settings = parent.settings
log.verbose('Creating UI for %s #%s\'s widget' % (
self.name, self.compPos
))
self.page = self.loadUi(self.__class__.ui)
# Connect widget signals
widgets = {
# Find all normal widgets which will be connected after subclass method
self._allWidgets = {
'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
'comboBox': self.page.findChildren(QtWidgets.QComboBox),
}
widgets['spinBox'].extend(
self._allWidgets['spinBox'].extend(
self.page.findChildren(QtWidgets.QDoubleSpinBox)
)
for widgetList in widgets.values():
for widget in widgetList:
connectWidget(widget, self.update)
def update(self):
'''
@ -427,10 +473,15 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# "Private" Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def _preUpdate(self):
'''Happens before subclass update()'''
for attr in self._relativeWidgets:
self.updateRelativeWidget(attr)
def _userUpdate(self):
'''An undoable component update triggered by the user'''
'''Happens after subclass update() for an undoable update by user.'''
oldWidgetVals = {
attr: getattr(self, attr)
attr: copy(getattr(self, attr))
for attr in self._trackedWidgets
}
newWidgetVals = {
@ -443,13 +494,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
for attr, val in newWidgetVals.items()
if val != oldWidgetVals[attr]
}
if modifiedWidgets:
action = ComponentUpdate(self, oldWidgetVals, modifiedWidgets)
self.parent.undoStack.push(action)
def _autoUpdate(self):
'''An internal component update that is not undoable'''
'''Happens after subclass update() for an internal component update.'''
newWidgetVals = {
attr: getWidgetValue(widget)
for attr, widget in self._trackedWidgets.items()
@ -459,12 +509,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def setAttrs(self, attrDict):
'''
Sets attrs (linked to trackedWidgets) in this preset to
Sets attrs (linked to trackedWidgets) in this component to
the values in the attrDict. Mutates certain widget values if needed
'''
for attr, val in attrDict.items():
if attr in self._colorWidgets:
# Color Widgets: text stored as tuple & update the button color
# Color Widgets must have a tuple & have a button to update
if type(val) is tuple:
rgbTuple = val
else:
@ -475,15 +525,25 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._colorWidgets[attr].setStyleSheet(btnStyle)
setattr(self, attr, rgbTuple)
elif attr in self._relativeWidgets:
# Relative widgets: number scales to fit export resolution
self.updateRelativeWidget(attr)
setattr(self, attr, val)
else:
# Normal tracked widget
setattr(self, attr, val)
def setWidgetValues(self, attrDict):
'''
Sets widgets defined by keys in trackedWidgets in this preset to
the values in the attrDict.
'''
affectedWidgets = [
self._trackedWidgets[attr] for attr in attrDict
]
with blockSignals(affectedWidgets):
for attr, val in attrDict.items():
widget = self._trackedWidgets[attr]
if attr in self._colorWidgets:
val = '%s,%s,%s' % val
setWidgetValue(widget, val)
def _sendUpdateSignal(self):
if not self.core.openingProject:
self.parent.drawPreview()
@ -499,6 +559,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
Optional args:
'presetNames': preset variable names to replace attr names
'commandArgs': arg keywords that differ from attr names
'colorWidgets': identify attr as RGB tuple & update button CSS
'relativeWidgets': change value proportionally to resolution
NOTE: Any kwarg key set to None will selectively disable tracking.
'''
@ -542,6 +604,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._relativeMaximums[attr] = \
self._trackedWidgets[attr].maximum()
self.updateRelativeWidgetMaximum(attr)
self._preUpdate()
self._autoUpdate()
def pickColor(self, textWidget, button):
'''Use color picker to get color input from the user.'''
@ -627,12 +691,28 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def setRelativeWidget(self, attr, floatVal):
'''Set a relative widget using a float'''
pixelVal = self.pixelValForAttr(attr, floatVal)
self._trackedWidgets[attr].setValue(pixelVal)
with blockSignals(self._allWidgets):
self._trackedWidgets[attr].setValue(pixelVal)
self.update(auto=True)
def getOldAttr(self, attr):
'''
Returns previous state of this attr. Used to determine whether
a relative widget must be updated. Required because undoing/redoing
can make determining the 'previous' value tricky.
'''
if self.oldAttrs is not None:
log.verbose('Using nonstandard oldAttr for %s' % attr)
return self.oldAttrs[attr]
else:
return getattr(self, attr)
def updateRelativeWidget(self, attr):
'''Called by _preUpdate() for each relativeWidget before each update'''
try:
oldUserValue = getattr(self, attr)
except AttributeError:
oldUserValue = self.getOldAttr(attr)
except (AttributeError, KeyError):
log.info('Using visible values as basis for relative widgets')
oldUserValue = self._trackedWidgets[attr].value()
newUserValue = self._trackedWidgets[attr].value()
newRelativeVal = self.floatValForAttr(attr, newUserValue)
@ -645,11 +725,10 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
# means the pixel value needs to be updated
log.debug('Updating %s #%s\'s relative widget: %s' % (
self.name, self.compPos, attr))
self._trackedWidgets[attr].blockSignals(True)
self.updateRelativeWidgetMaximum(attr)
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
self._trackedWidgets[attr].setValue(pixelVal)
self._trackedWidgets[attr].blockSignals(False)
with blockSignals(self._trackedWidgets[attr]):
self.updateRelativeWidgetMaximum(attr)
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
self._trackedWidgets[attr].setValue(pixelVal)
if attr not in self._relativeValues \
or oldUserValue != newUserValue:
@ -725,14 +804,22 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
parent.name, parent.compPos
)
)
self.undone = False
self.parent = parent
self.oldWidgetVals = {
attr: val
attr: copy(val)
for attr, val in oldWidgetVals.items()
if attr in modifiedVals
}
self.modifiedVals = modifiedVals
# Because relative widgets change themselves every update based on
# their previous value, we must store ALL their values in case of undo
self.redoRelativeWidgetVals = {
attr: copy(getattr(self.parent, attr))
for attr in self.parent._relativeWidgets
}
# Determine if this update is mergeable
self.id_ = -1
if len(self.modifiedVals) == 1:
@ -755,15 +842,26 @@ class ComponentUpdate(QtWidgets.QUndoCommand):
return True
def redo(self):
if self.undone:
log.debug('Redoing component update')
self.parent.setWidgetValues(self.modifiedVals)
self.parent.setAttrs(self.modifiedVals)
self.parent._sendUpdateSignal()
if self.undone:
self.parent.oldAttrs = self.redoRelativeWidgetVals
self.parent.update(auto=True)
self.parent.oldAttrs = None
else:
self.undoRelativeWidgetVals = {
attr: copy(getattr(self.parent, attr))
for attr in self.parent._relativeWidgets
}
self.parent._sendUpdateSignal()
def undo(self):
log.debug('Undoing component update')
self.undone = True
self.parent.oldAttrs = self.undoRelativeWidgetVals
self.parent.setWidgetValues(self.oldWidgetVals)
self.parent.setAttrs(self.oldWidgetVals)
with blockSignals(self.parent):
for attr, val in self.oldWidgetVals.items():
widget = self.parent._trackedWidgets[attr]
if attr in self.parent._colorWidgets:
val = '%s,%s,%s' % val
setWidgetValue(widget, val)
self.parent._sendUpdateSignal()
self.parent.update(auto=True)
self.parent.oldAttrs = None

View File

@ -82,8 +82,6 @@ class Component(Component):
self.page.pushButton_color2.setEnabled(False)
self.page.fillWidget.setCurrentIndex(fillType)
super().update()
def previewRender(self):
return self.drawFrame(self.width, self.height)

View File

@ -84,7 +84,6 @@ class Component(Component):
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_image.setText(filename)
self.update()
def command(self, arg):
if '=' in arg:
@ -123,4 +122,3 @@ class Component(Component):
else:
scaleBox.setVisible(True)
stretchScaleBox.setVisible(False)
super().update()

View File

@ -53,7 +53,6 @@ class Component(Component):
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_image.setText(filename)
self.update()
def shiftGrid(self, d):
def newGrid(Xchange, Ychange):

View File

@ -53,7 +53,6 @@ class Component(Component):
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_sound.setText(filename)
self.update()
def commandHelp(self):
print('Path to audio file:\n path=/filepath/to/sound.ogg')

View File

@ -76,8 +76,6 @@ class Component(Component):
else:
self.page.checkBox_mono.setEnabled(True)
super().update()
def previewRender(self):
changedSize = self.updateChunksize()
if not changedSize \
@ -138,7 +136,7 @@ class Component(Component):
'-r', self.settings.value("outputFrameRate"),
'-ss', "{0:.3f}".format(startPt),
'-i',
os.path.join(self.core.wd, 'background.png')
self.core.junkStream
if genericPreview else inputFile,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',

View File

@ -68,7 +68,6 @@ class Component(Component):
self.page.spinBox_shadY.setHidden(True)
self.page.label_shadBlur.setHidden(True)
self.page.spinBox_shadBlur.setHidden(True)
super().update()
def centerXY(self):
self.setRelativeWidget('xPosition', 0.5)

View File

@ -52,7 +52,6 @@ class Component(Component):
else:
self.page.label_volume.setEnabled(False)
self.page.spinBox_volume.setEnabled(False)
super().update()
def previewRender(self):
self.updateChunksize()
@ -119,7 +118,6 @@ class Component(Component):
if filename:
self.settings.setValue("componentDir", os.path.dirname(filename))
self.page.lineEdit_video.setText(filename)
self.update()
def getPreviewFrame(self, width, height):
if not self.videoPath or not os.path.exists(self.videoPath):

View File

@ -98,7 +98,7 @@ class Component(Component):
'-r', self.settings.value("outputFrameRate"),
'-ss', "{0:.3f}".format(startPt),
'-i',
os.path.join(self.core.wd, 'background.png')
self.core.junkStream
if genericPreview else inputFile,
'-f', 'image2pipe',
'-pix_fmt', 'rgba',

View File

@ -13,7 +13,7 @@ import toolkit
log = logging.getLogger('AVP.Core')
STDOUT_LOGLVL = logging.WARNING
STDOUT_LOGLVL = logging.VERBOSE
FILE_LOGLVL = logging.DEBUG
@ -81,10 +81,7 @@ class Core:
component = self.modules[moduleIndex].Component(
moduleIndex, compPos, self
)
# init component's widget for loading/saving presets
component.widget(loader)
# use autoUpdate() method before update() this 1 time to set attrs
component._autoUpdate()
else:
moduleIndex = -1
log.debug(
@ -186,9 +183,8 @@ class Core:
if hasattr(loader, 'window'):
for widget, value in data['WindowFields']:
widget = eval('loader.window.%s' % widget)
widget.blockSignals(True)
toolkit.setWidgetValue(widget, value)
widget.blockSignals(False)
with toolkit.blockSignals(widget):
toolkit.setWidgetValue(widget, value)
for key, value in data['Settings']:
Core.settings.setValue(key, value)
@ -474,6 +470,7 @@ class Core:
'logDir': os.path.join(dataDir, 'log'),
'presetDir': os.path.join(dataDir, 'presets'),
'componentsPath': os.path.join(wd, 'components'),
'junkStream': os.path.join(wd, 'gui', 'background.png'),
'encoderOptions': encoderOptions,
'resolutions': [
'1920x1080',

View File

@ -20,11 +20,20 @@ class AddComponent(QUndoCommand):
self.parent = parent
self.moduleI = moduleI
self.compI = compI
self.comp = None
def redo(self):
self.parent.core.insertComponent(self.compI, self.moduleI, self.parent)
if self.comp is None:
self.parent.core.insertComponent(
self.compI, self.moduleI, self.parent)
else:
# inserting previously-created component
self.parent.core.insertComponent(
self.compI, self.comp, self.parent)
def undo(self):
self.comp = self.parent.core.selectedComponents[self.compI]
self.parent._removeComponent(self.compI)

View File

@ -25,7 +25,7 @@ from toolkit import (
)
log = logging.getLogger('AVP.MainWindow')
log = logging.getLogger('AVP.Gui.MainWindow')
class MainWindow(QtWidgets.QMainWindow):
@ -76,7 +76,7 @@ class MainWindow(QtWidgets.QMainWindow):
# Create the preview window and its thread, queues, and timers
log.debug('Creating preview window')
self.previewWindow = PreviewWindow(self, os.path.join(
Core.wd, "background.png"))
Core.wd, 'gui', "background.png"))
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
log.debug('Starting preview thread')

View File

@ -5,12 +5,16 @@
from PyQt5 import QtCore, QtWidgets
import string
import os
import logging
from toolkit import badName
from core import Core
from gui.actions import *
log = logging.getLogger('AVP.Gui.PresetManager')
class PresetManager(QtWidgets.QDialog):
def __init__(self, window, parent):
super().__init__(parent.window)

View File

@ -14,7 +14,7 @@ from toolkit.frame import Checkerboard
from toolkit import disableWhenOpeningProject
log = logging.getLogger("AVP.PreviewThread")
log = logging.getLogger("AVP.Gui.PreviewThread")
class Worker(QtCore.QObject):

View File

@ -7,7 +7,7 @@ class PreviewWindow(QtWidgets.QLabel):
Paints the preview QLabel in MainWindow and maintains the aspect ratio
when the window is resized.
'''
log = logging.getLogger('AVP.PreviewWindow')
log = logging.getLogger('AVP.Gui.PreviewWindow')
def __init__(self, parent, img):
super(PreviewWindow, self).__init__()

View File

@ -6,7 +6,7 @@ import logging
from __init__ import wd
log = logging.getLogger('AVP.Entrypoint')
log = logging.getLogger('AVP.Main')
def main():

View File

@ -6,19 +6,53 @@ import string
import os
import sys
import subprocess
import logging
from copy import copy
from collections import OrderedDict
log = logging.getLogger('AVP.Toolkit.Common')
class blockSignals:
'''A context manager to temporarily block a Qt widget from updating'''
def __init__(self, widget):
self.widget = widget
'''
Context manager to temporarily block list of QtWidgets from updating,
and guarantee restoring the previous state afterwards.
'''
def __init__(self, widgets):
if type(widgets) is dict:
self.widgets = concatDictVals(widgets)
else:
self.widgets = (
widgets if hasattr(widgets, '__iter__')
else [widgets]
)
def __enter__(self):
self.widget.blockSignals(True)
log.verbose('Blocking signals for %s' % ", ".join([
str(w.__class__.__name__) for w in self.widgets
]))
self.oldStates = [w.signalsBlocked() for w in self.widgets]
for w in self.widgets:
w.blockSignals(True)
def __exit__(self, *args):
self.widget.blockSignals(False)
log.verbose('Resetting blockSignals to %s' % sum(self.oldStates))
for w, state in zip(self.widgets, self.oldStates):
w.blockSignals(state)
def concatDictVals(d):
'''Concatenates all values in given dict into one list.'''
key, value = d.popitem()
d[key] = value
final = copy(value)
if type(final) is not list:
final = [final]
final.extend([val for val in d.values()])
else:
value.extend([item for val in d.values() for item in val])
return final
def badName(name):
@ -119,12 +153,14 @@ def connectWidget(widget, func):
elif type(widget) == QtWidgets.QComboBox:
widget.currentIndexChanged.connect(func)
else:
log.warning('Failed to connect %s ' % str(widget.__class__.__name__))
return False
return True
def setWidgetValue(widget, val):
'''Generic setValue method for use with any typical QtWidget'''
log.verbose('Setting %s to %s' % (str(widget.__class__.__name__), val))
if type(widget) == QtWidgets.QLineEdit:
widget.setText(val)
elif type(widget) == QtWidgets.QSpinBox \
@ -135,6 +171,7 @@ def setWidgetValue(widget, val):
elif type(widget) == QtWidgets.QComboBox:
widget.setCurrentIndex(val)
else:
log.warning('Failed to set %s ' % str(widget.__class__.__name__))
return False
return True