Undo feature

This commit is contained in:
Brianna 2017-09-02 09:49:35 -04:00 committed by GitHub
commit 22978a0635
28 changed files with 1160 additions and 387 deletions

View File

@ -2,7 +2,7 @@ from setuptools import setup
import os import os
__version__ = '2.0.0.rc4' __version__ = '2.0.0rc5'
def package_files(directory): def package_files(directory):

View File

@ -8,6 +8,7 @@ import argparse
import os import os
import sys import sys
import time import time
import signal
from core import Core from core import Core
@ -19,6 +20,7 @@ class Command(QtCore.QObject):
def __init__(self): def __init__(self):
QtCore.QObject.__init__(self) QtCore.QObject.__init__(self)
self.core = Core() self.core = Core()
Core.mode = 'commandline'
self.dataDir = self.core.dataDir self.dataDir = self.core.dataDir
self.canceled = False self.canceled = False
@ -90,6 +92,9 @@ class Command(QtCore.QObject):
for arg in args: for arg in args:
self.core.selectedComponents[i].command(arg) self.core.selectedComponents[i].command(arg)
# ctrl-c stops the export thread
signal.signal(signal.SIGINT, self.stopVideo)
if self.args.export and self.args.projpath: if self.args.export and self.args.projpath:
errcode, data = self.core.parseAvFile(projPath) errcode, data = self.core.parseAvFile(projPath)
for key, value in data['WindowFields']: for key, value in data['WindowFields']:
@ -123,6 +128,11 @@ class Command(QtCore.QObject):
self.worker.progressBarSetText.connect(self.progressBarSetText) self.worker.progressBarSetText.connect(self.progressBarSetText)
self.createVideo.emit() self.createVideo.emit()
def stopVideo(self, *args):
self.worker.error = True
self.worker.cancelExport()
self.worker.cancel()
@QtCore.pyqtSlot(str) @QtCore.pyqtSlot(str)
def progressBarSetText(self, value): def progressBarSetText(self, value):
if 'Export ' in value: if 'Export ' in value:

View File

@ -9,10 +9,11 @@ import sys
import math import math
import time import time
import logging import logging
from copy import copy
from toolkit.frame import BlankFrame from toolkit.frame import BlankFrame
from toolkit import ( from toolkit import (
getWidgetValue, setWidgetValue, connectWidget, rgbFromString getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals
) )
@ -39,11 +40,10 @@ class ComponentMetaclass(type(QtCore.QObject)):
def renderWrapper(func): def renderWrapper(func):
def renderWrapper(self, *args, **kwargs): def renderWrapper(self, *args, **kwargs):
try: try:
log.verbose('### %s #%s renders%s frame %s###' % ( log.verbose(
'### %s #%s renders a preview frame ###',
self.__class__.name, str(self.compPos), self.__class__.name, str(self.compPos),
'' if args else ' a preview', )
'' if not args else '%s ' % args[0],
))
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
except Exception as e: except Exception as e:
try: try:
@ -59,9 +59,8 @@ class ComponentMetaclass(type(QtCore.QObject)):
'''Intercepts the command() method to check for global args''' '''Intercepts the command() method to check for global args'''
def commandWrapper(self, arg): def commandWrapper(self, arg):
if arg.startswith('preset='): if arg.startswith('preset='):
from presetmanager import getPresetDir
_, preset = arg.split('=', 1) _, preset = arg.split('=', 1)
path = os.path.join(getPresetDir(self), preset) path = os.path.join(self.core.getPresetDir(self), preset)
if not os.path.exists(path): if not os.path.exists(path):
print('Couldn\'t locate preset "%s"' % preset) print('Couldn\'t locate preset "%s"' % preset)
quit(1) quit(1)
@ -100,6 +99,92 @@ class ComponentMetaclass(type(QtCore.QObject)):
return func(self) return func(self)
return errorWrapper 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): def __new__(cls, name, parents, attrs):
if 'ui' not in attrs: if 'ui' not in attrs:
# Use module name as ui filename by default # Use module name as ui filename by default
@ -107,54 +192,55 @@ class ComponentMetaclass(type(QtCore.QObject)):
attrs['__module__'].split('.')[-1] attrs['__module__'].split('.')[-1]
)[0] )[0]
# if parents[0] == QtCore.QObject: else:
decorate = ( decorate = (
'names', # Class methods 'names', # Class methods
'error', 'audio', 'properties', # Properties 'error', 'audio', 'properties', # Properties
'preFrameRender', 'previewRender', 'preFrameRender', 'previewRender',
'frameRender', 'command', 'loadPreset', 'command',
'update', 'widget',
) )
# Auto-decorate methods # Auto-decorate methods
for key in decorate: for key in decorate:
if key not in attrs: if key not in attrs:
continue continue
if key in ('names'): if key in ('names'):
attrs[key] = classmethod(attrs[key]) attrs[key] = classmethod(attrs[key])
elif key in ('audio'):
if key in ('audio'):
attrs[key] = property(attrs[key]) attrs[key] = property(attrs[key])
elif key == 'command':
if key == 'command':
attrs[key] = cls.commandWrapper(attrs[key]) attrs[key] = cls.commandWrapper(attrs[key])
elif key == 'previewRender':
if key in ('previewRender', 'frameRender'):
attrs[key] = cls.renderWrapper(attrs[key]) attrs[key] = cls.renderWrapper(attrs[key])
elif key == 'preFrameRender':
if key == 'preFrameRender':
attrs[key] = cls.initializationWrapper(attrs[key]) attrs[key] = cls.initializationWrapper(attrs[key])
elif key == 'properties':
if key == 'properties':
attrs[key] = cls.propertiesWrapper(attrs[key]) attrs[key] = cls.propertiesWrapper(attrs[key])
elif key == 'error':
if key == 'error':
attrs[key] = cls.errorWrapper(attrs[key]) 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 # Turn version string into a number
try: try:
if 'version' not in attrs: if 'version' not in attrs:
log.error( log.error(
'No version attribute in %s. Defaulting to 1' % 'No version attribute in %s. Defaulting to 1',
attrs['name']) attrs['name'])
attrs['version'] = 1 attrs['version'] = 1
else: else:
attrs['version'] = int(attrs['version'].split('.')[0]) attrs['version'] = int(attrs['version'].split('.')[0])
except ValueError: except ValueError:
log.critical('%s component has an invalid version string:\n%s' % ( log.critical(
attrs['name'], str(attrs['version']))) '%s component has an invalid version string:\n%s',
attrs['name'], str(attrs['version'])
)
except KeyError: except KeyError:
log.critical('%s component has no version string.' % attrs['name']) log.critical('%s component has no version string.', attrs['name'])
else: else:
return super().__new__(cls, name, parents, attrs) return super().__new__(cls, name, parents, attrs)
quit(1) quit(1)
@ -180,23 +266,29 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.moduleIndex = moduleIndex self.moduleIndex = moduleIndex
self.compPos = compPos self.compPos = compPos
self.core = core self.core = core
self.currentPreset = None
# STATUS VARIABLES
self.currentPreset = None
self._allWidgets = {}
self._trackedWidgets = {} self._trackedWidgets = {}
self._presetNames = {} self._presetNames = {}
self._commandArgs = {} self._commandArgs = {}
self._colorWidgets = {} self._colorWidgets = {}
self._colorFuncs = {} self._colorFuncs = {}
self._relativeWidgets = {} self._relativeWidgets = {}
# pixel values stored as floats # Pixel values stored as floats
self._relativeValues = {} self._relativeValues = {}
# maximum values of spinBoxes at 1080p (Core.resolutions[0]) # Maximum values of spinBoxes at 1080p (Core.resolutions[0])
self._relativeMaximums = {} self._relativeMaximums = {}
# LOCKING VARIABLES
self.openingPreset = False
self.mergeUndo = True
self._lockedProperties = None self._lockedProperties = None
self._lockedError = None self._lockedError = None
self._lockedSize = 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 # Stop lengthy processes in response to this variable
self.canceled = False self.canceled = False
@ -204,12 +296,20 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
return self.__class__.name return self.__class__.name
def __repr__(self): def __repr__(self):
import pprint
try: try:
preset = self.savePreset() preset = self.savePreset()
except Exception as e: except Exception as e:
preset = '%s occurred while saving preset' % str(e) preset = '%s occurred while saving preset' % str(e)
return '%s\n%s\n%s' % (
self.__class__.name, str(self.__class__.version), preset return (
'Component(module %s, pos %s) (%s)\n'
'Name: %s v%s\nPreset: %s' % (
self.moduleIndex, self.compPos,
object.__repr__(self),
self.__class__.name, str(self.__class__.version),
pprint.pformat(preset)
)
) )
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
@ -288,54 +388,29 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
''' '''
self.parent = parent self.parent = parent
self.settings = parent.settings self.settings = parent.settings
log.verbose(
'Creating UI for %s #%s\'s widget',
self.__class__.name, self.compPos
)
self.page = self.loadUi(self.__class__.ui) self.page = self.loadUi(self.__class__.ui)
# Connect widget signals # Find all normal widgets which will be connected after subclass method
widgets = { self._allWidgets = {
'lineEdit': self.page.findChildren(QtWidgets.QLineEdit), 'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
'checkBox': self.page.findChildren(QtWidgets.QCheckBox), 'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
'spinBox': self.page.findChildren(QtWidgets.QSpinBox), 'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
'comboBox': self.page.findChildren(QtWidgets.QComboBox), 'comboBox': self.page.findChildren(QtWidgets.QComboBox),
} }
widgets['spinBox'].extend( self._allWidgets['spinBox'].extend(
self.page.findChildren(QtWidgets.QDoubleSpinBox) self.page.findChildren(QtWidgets.QDoubleSpinBox)
) )
for widgetList in widgets.values():
for widget in widgetList:
connectWidget(widget, self.update)
def update(self): def update(self):
''' '''
Reads all tracked widget values into instance attributes Starting point for a component update. A subclass should override
and tells the MainWindow that the component was modified. this method, and the base class will then magically insert a call
Call super() at the END if you need to subclass this. to either _autoUpdate() or _userUpdate() at the end.
''' '''
for attr, widget in self._trackedWidgets.items():
if attr in self._colorWidgets:
# Color Widgets: text stored as tuple & update the button color
rgbTuple = rgbFromString(widget.text())
btnStyle = (
"QPushButton { background-color : %s; outline: none; }"
% QColor(*rgbTuple).name())
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, self._trackedWidgets[attr].value())
else:
# Normal tracked widget
setattr(self, attr, getWidgetValue(widget))
self.sendUpdateSignal()
def sendUpdateSignal(self):
if not self.core.openingProject:
self.parent.drawPreview()
saveValueStore = self.savePreset()
saveValueStore['preset'] = self.currentPreset
self.modified.emit(self.compPos, saveValueStore)
def loadPreset(self, presetDict, presetName=None): def loadPreset(self, presetDict, presetName=None):
''' '''
@ -348,7 +423,14 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
for attr, widget in self._trackedWidgets.items(): for attr, widget in self._trackedWidgets.items():
key = attr if attr not in self._presetNames \ key = attr if attr not in self._presetNames \
else self._presetNames[attr] else self._presetNames[attr]
val = presetDict[key] try:
val = presetDict[key]
except KeyError as e:
log.info(
'%s missing value %s. Outdated preset?',
self.currentPreset, str(e)
)
val = getattr(self, key)
if attr in self._colorWidgets: if attr in self._colorWidgets:
widget.setText('%s,%s,%s' % val) widget.setText('%s,%s,%s' % val)
@ -403,6 +485,85 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# "Private" Methods # "Private" Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def _preUpdate(self):
'''Happens before subclass update()'''
for attr in self._relativeWidgets:
self.updateRelativeWidget(attr)
def _userUpdate(self):
'''Happens after subclass update() for an undoable update by user.'''
oldWidgetVals = {
attr: copy(getattr(self, attr))
for attr in self._trackedWidgets
}
newWidgetVals = {
attr: getWidgetValue(widget)
if attr not in self._colorWidgets else rgbFromString(widget.text())
for attr, widget in self._trackedWidgets.items()
}
modifiedWidgets = {
attr: val
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):
'''Happens after subclass update() for an internal component update.'''
newWidgetVals = {
attr: getWidgetValue(widget)
for attr, widget in self._trackedWidgets.items()
}
self.setAttrs(newWidgetVals)
self._sendUpdateSignal()
def setAttrs(self, attrDict):
'''
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 must have a tuple & have a button to update
if type(val) is tuple:
rgbTuple = val
else:
rgbTuple = rgbFromString(val)
btnStyle = (
"QPushButton { background-color : %s; outline: none; }"
% QColor(*rgbTuple).name())
self._colorWidgets[attr].setStyleSheet(btnStyle)
setattr(self, attr, rgbTuple)
else:
# Normal tracked widget
setattr(self, attr, val)
log.verbose('Setting %s self.%s to %s' % (
self.__class__.name, 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()
saveValueStore = self.savePreset()
saveValueStore['preset'] = self.currentPreset
self.modified.emit(self.compPos, saveValueStore)
def trackWidgets(self, trackDict, **kwargs): def trackWidgets(self, trackDict, **kwargs):
''' '''
@ -412,6 +573,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
Optional args: Optional args:
'presetNames': preset variable names to replace attr names 'presetNames': preset variable names to replace attr names
'commandArgs': arg keywords that differ from 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. NOTE: Any kwarg key set to None will selectively disable tracking.
''' '''
@ -424,7 +587,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'colorWidgets', 'colorWidgets',
'relativeWidgets', 'relativeWidgets',
): ):
setattr(self, '_%s' % kwarg, kwargs[kwarg]) setattr(self, '_{}'.format(kwarg), kwargs[kwarg])
else: else:
raise ComponentError( raise ComponentError(
self, 'Nonsensical keywords to trackWidgets.') self, 'Nonsensical keywords to trackWidgets.')
@ -434,10 +597,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
if kwarg == 'colorWidgets': if kwarg == 'colorWidgets':
def makeColorFunc(attr): def makeColorFunc(attr):
def pickColor_(): def pickColor_():
self.mergeUndo = False
self.pickColor( self.pickColor(
self._trackedWidgets[attr], self._trackedWidgets[attr],
self._colorWidgets[attr] self._colorWidgets[attr]
) )
self.mergeUndo = True
return pickColor_ return pickColor_
self._colorFuncs = { self._colorFuncs = {
attr: makeColorFunc(attr) for attr in kwargs[kwarg] attr: makeColorFunc(attr) for attr in kwargs[kwarg]
@ -455,6 +620,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._relativeMaximums[attr] = \ self._relativeMaximums[attr] = \
self._trackedWidgets[attr].maximum() self._trackedWidgets[attr].maximum()
self.updateRelativeWidgetMaximum(attr) self.updateRelativeWidgetMaximum(attr)
setattr(
self, attr, getWidgetValue(self._trackedWidgets[attr])
)
self._preUpdate()
self._autoUpdate()
def pickColor(self, textWidget, button): def pickColor(self, textWidget, button):
'''Use color picker to get color input from the user.''' '''Use color picker to get color input from the user.'''
@ -516,12 +687,22 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def relativeWidgetAxis(func): def relativeWidgetAxis(func):
def relativeWidgetAxis(self, attr, *args, **kwargs): def relativeWidgetAxis(self, attr, *args, **kwargs):
hasVerticalWords = (
lambda attr:
'height' in attr.lower() or
'ypos' in attr.lower() or
attr == 'y'
)
if 'axis' not in kwargs: if 'axis' not in kwargs:
axis = self.width axis = self.width
if 'height' in attr.lower() \ if hasVerticalWords(attr):
or 'ypos' in attr.lower() or attr == 'y':
axis = self.height axis = self.height
kwargs['axis'] = axis kwargs['axis'] = axis
if 'axis' in kwargs and type(kwargs['axis']) is tuple:
axis = kwargs['axis'][0]
if hasVerticalWords(attr):
axis = kwargs['axis'][1]
kwargs['axis'] = axis
return func(self, attr, *args, **kwargs) return func(self, attr, *args, **kwargs)
return relativeWidgetAxis return relativeWidgetAxis
@ -529,7 +710,18 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def pixelValForAttr(self, attr, val=None, **kwargs): def pixelValForAttr(self, attr, val=None, **kwargs):
if val is None: if val is None:
val = self._relativeValues[attr] val = self._relativeValues[attr]
return math.ceil(kwargs['axis'] * val) if val > 50.0:
log.warning(
'%s #%s attempted to set %s to dangerously high number %s',
self.__class__.name, self.compPos, attr, val
)
val = 50.0
result = math.ceil(kwargs['axis'] * val)
log.verbose(
'Converting %s: f%s to px%s using axis %s',
attr, val, result, kwargs['axis']
)
return result
@relativeWidgetAxis @relativeWidgetAxis
def floatValForAttr(self, attr, val=None, **kwargs): def floatValForAttr(self, attr, val=None, **kwargs):
@ -540,14 +732,28 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def setRelativeWidget(self, attr, floatVal): def setRelativeWidget(self, attr, floatVal):
'''Set a relative widget using a float''' '''Set a relative widget using a float'''
pixelVal = self.pixelValForAttr(attr, floatVal) pixelVal = self.pixelValForAttr(attr, floatVal)
self._trackedWidgets[attr].setValue(pixelVal) with blockSignals(self._trackedWidgets[attr]):
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:
return self.oldAttrs[attr]
else:
try:
return getattr(self, attr)
except AttributeError:
log.error('Using visible values instead of oldAttrs')
return self._trackedWidgets[attr].value()
def updateRelativeWidget(self, attr): def updateRelativeWidget(self, attr):
try: '''Called by _preUpdate() for each relativeWidget before each update'''
oldUserValue = getattr(self, attr) oldUserValue = self.getOldAttr(attr)
except AttributeError:
oldUserValue = self._trackedWidgets[attr].value()
newUserValue = self._trackedWidgets[attr].value() newUserValue = self._trackedWidgets[attr].value()
newRelativeVal = self.floatValForAttr(attr, newUserValue) newRelativeVal = self.floatValForAttr(attr, newUserValue)
@ -557,13 +763,13 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
and oldRelativeVal != newRelativeVal: and oldRelativeVal != newRelativeVal:
# Float changed without pixel value changing, which # Float changed without pixel value changing, which
# means the pixel value needs to be updated # means the pixel value needs to be updated
log.debug('Updating %s #%s\'s relative widget: %s' % ( log.debug(
self.name, self.compPos, attr)) 'Updating %s #%s\'s relative widget: %s',
self._trackedWidgets[attr].blockSignals(True) self.__class__.name, self.compPos, attr)
self.updateRelativeWidgetMaximum(attr) with blockSignals(self._trackedWidgets[attr]):
pixelVal = self.pixelValForAttr(attr, oldRelativeVal) self.updateRelativeWidgetMaximum(attr)
self._trackedWidgets[attr].setValue(pixelVal) pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
self._trackedWidgets[attr].blockSignals(False) self._trackedWidgets[attr].setValue(pixelVal)
if attr not in self._relativeValues \ if attr not in self._relativeValues \
or oldUserValue != newUserValue: or oldUserValue != newUserValue:
@ -629,3 +835,90 @@ class ComponentError(RuntimeError):
super().__init__(string) super().__init__(string)
caller.lockError(string) caller.lockError(string)
caller._error.emit(string, detail) caller._error.emit(string, detail)
class ComponentUpdate(QtWidgets.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 len(self.modifiedVals) == 1 and self.parent.mergeUndo:
attr, val = self.modifiedVals.popitem()
self.id_ = sum([ord(letter) for letter in attr[-14:]])
self.modifiedVals[attr] = val
else:
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

View File

@ -17,9 +17,6 @@ class Component(Component):
self.y = 0 self.y = 0
super().widget(*args) super().widget(*args)
self.page.lineEdit_color1.setText('0,0,0')
self.page.lineEdit_color2.setText('133,133,133')
# disable color #2 until non-default 'fill' option gets changed # disable color #2 until non-default 'fill' option gets changed
self.page.lineEdit_color2.setDisabled(True) self.page.lineEdit_color2.setDisabled(True)
self.page.pushButton_color2.setDisabled(True) self.page.pushButton_color2.setDisabled(True)
@ -85,8 +82,6 @@ class Component(Component):
self.page.pushButton_color2.setEnabled(False) self.page.pushButton_color2.setEnabled(False)
self.page.fillWidget.setCurrentIndex(fillType) self.page.fillWidget.setCurrentIndex(fillType)
super().update()
def previewRender(self): def previewRender(self):
return self.drawFrame(self.width, self.height) return self.drawFrame(self.width, self.height)
@ -107,7 +102,7 @@ class Component(Component):
# Return a solid image at x, y # Return a solid image at x, y
if self.fillType == 0: if self.fillType == 0:
frame = BlankFrame(width, height) frame = BlankFrame(width, height)
image = Image.new("RGBA", shapeSize, (r, g, b, 255)) image = FloodFrame(self.sizeWidth, self.sizeHeight, (r, g, b, 255))
frame.paste(image, box=(self.x, self.y)) frame.paste(image, box=(self.x, self.y))
return frame return frame

View File

@ -73,6 +73,9 @@
<height>0</height> <height>0</height>
</size> </size>
</property> </property>
<property name="text">
<string>0,0,0</string>
</property>
<property name="maxLength"> <property name="maxLength">
<number>12</number> <number>12</number>
</property> </property>
@ -146,6 +149,9 @@
<height>0</height> <height>0</height>
</size> </size>
</property> </property>
<property name="text">
<string>133,133,133</string>
</property>
<property name="maxLength"> <property name="maxLength">
<number>12</number> <number>12</number>
</property> </property>
@ -198,7 +204,7 @@
<number>0</number> <number>0</number>
</property> </property>
<property name="maximum"> <property name="maximum">
<number>999999999</number> <number>19200</number>
</property> </property>
<property name="value"> <property name="value">
<number>0</number> <number>0</number>
@ -233,7 +239,7 @@
</size> </size>
</property> </property>
<property name="maximum"> <property name="maximum">
<number>999999999</number> <number>10800</number>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -83,8 +83,9 @@ class Component(Component):
"Image Files (%s)" % " ".join(self.core.imageFormats)) "Image Files (%s)" % " ".join(self.core.imageFormats))
if filename: if filename:
self.settings.setValue("componentDir", os.path.dirname(filename)) self.settings.setValue("componentDir", os.path.dirname(filename))
self.mergeUndo = False
self.page.lineEdit_image.setText(filename) self.page.lineEdit_image.setText(filename)
self.update() self.mergeUndo = True
def command(self, arg): def command(self, arg):
if '=' in arg: if '=' in arg:
@ -123,4 +124,3 @@ class Component(Component):
else: else:
scaleBox.setVisible(True) scaleBox.setVisible(True)
stretchScaleBox.setVisible(False) stretchScaleBox.setVisible(False)
super().update()

View File

@ -1,4 +1,5 @@
from PyQt5 import QtGui, QtCore, QtWidgets from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtWidgets import QUndoCommand
from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter
import os import os
import math import math
@ -35,6 +36,7 @@ class Component(Component):
self.page.toolButton_left, self.page.toolButton_left,
self.page.toolButton_right, self.page.toolButton_right,
) )
def shiftFunc(i): def shiftFunc(i):
def shift(): def shift():
self.shiftGrid(i) self.shiftGrid(i)
@ -52,26 +54,13 @@ class Component(Component):
"Image Files (%s)" % " ".join(self.core.imageFormats)) "Image Files (%s)" % " ".join(self.core.imageFormats))
if filename: if filename:
self.settings.setValue("componentDir", os.path.dirname(filename)) self.settings.setValue("componentDir", os.path.dirname(filename))
self.mergeUndo = False
self.page.lineEdit_image.setText(filename) self.page.lineEdit_image.setText(filename)
self.update() self.mergeUndo = True
def shiftGrid(self, d): def shiftGrid(self, d):
def newGrid(Xchange, Ychange): action = ShiftGrid(self, d)
return { self.parent.undoStack.push(action)
(x + Xchange, y + Ychange)
for x, y in self.startingGrid
}
if d == 0:
newGrid = newGrid(0, -1)
elif d == 1:
newGrid = newGrid(0, 1)
elif d == 2:
newGrid = newGrid(-1, 0)
elif d == 3:
newGrid = newGrid(1, 0)
self.startingGrid = newGrid
self.sendUpdateSignal()
def update(self): def update(self):
self.updateGridSize() self.updateGridSize()
@ -96,17 +85,14 @@ class Component(Component):
enabled = (len(self.startingGrid) > 0) enabled = (len(self.startingGrid) > 0)
for widget in self.shiftButtons: for widget in self.shiftButtons:
widget.setEnabled(enabled) widget.setEnabled(enabled)
super().update()
def previewClickEvent(self, pos, size, button): def previewClickEvent(self, pos, size, button):
pos = ( pos = (
math.ceil((pos[0] / size[0]) * self.gridWidth) - 1, math.ceil((pos[0] / size[0]) * self.gridWidth) - 1,
math.ceil((pos[1] / size[1]) * self.gridHeight) - 1 math.ceil((pos[1] / size[1]) * self.gridHeight) - 1
) )
if button == 1: action = ClickGrid(self, pos, button)
self.startingGrid.add(pos) self.parent.undoStack.push(action)
elif button == 2:
self.startingGrid.discard(pos)
def updateGridSize(self): def updateGridSize(self):
w, h = self.core.resolutions[-1].split('x') w, h = self.core.resolutions[-1].split('x')
@ -198,7 +184,7 @@ class Component(Component):
# Circle # Circle
if shape == 'circle': if shape == 'circle':
drawer.ellipse(outlineShape, fill=self.color) drawer.ellipse(outlineShape, fill=self.color)
drawer.ellipse(smallerShape, fill=(0,0,0,0)) drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
# Lilypad # Lilypad
elif shape == 'lilypad': elif shape == 'lilypad':
@ -208,9 +194,9 @@ class Component(Component):
elif shape == 'pac-man': elif shape == 'pac-man':
drawer.pieslice(outlineShape, 35, 320, fill=self.color) drawer.pieslice(outlineShape, 35, 320, fill=self.color)
hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline
tX, tY = scale(33, self.pxWidth, self.pxHeight, int) # thirdline tX, tY = scale(33, self.pxWidth, self.pxHeight, int) # thirdline
qX, qY = scale(20, self.pxWidth, self.pxHeight, int) # quarterline qX, qY = scale(20, self.pxWidth, self.pxHeight, int) # quarterline
# Path # Path
if shape == 'path': if shape == 'path':
@ -221,7 +207,7 @@ class Component(Component):
'up', 'down', 'left', 'right', 'up', 'down', 'left', 'right',
) )
} }
for cell in nearbyCoords(x, y): for cell in self.nearbyCoords(x, y):
if cell not in grid: if cell not in grid:
continue continue
if cell[0] == x: if cell[0] == x:
@ -246,19 +232,19 @@ class Component(Component):
sect = ( sect = (
(drawPtX, drawPtY + hY), (drawPtX, drawPtY + hY),
(drawPtX + self.pxWidth, (drawPtX + self.pxWidth,
drawPtY + self.pxHeight) drawPtY + self.pxHeight)
) )
elif direction == 'left': elif direction == 'left':
sect = ( sect = (
(drawPtX, drawPtY), (drawPtX, drawPtY),
(drawPtX + hX, (drawPtX + hX,
drawPtY + self.pxHeight) drawPtY + self.pxHeight)
) )
elif direction == 'right': elif direction == 'right':
sect = ( sect = (
(drawPtX + hX, drawPtY), (drawPtX + hX, drawPtY),
(drawPtX + self.pxWidth, (drawPtX + self.pxWidth,
drawPtY + self.pxHeight) drawPtY + self.pxHeight)
) )
drawer.rectangle(sect, fill=self.color) drawer.rectangle(sect, fill=self.color)
@ -288,20 +274,25 @@ class Component(Component):
# Peace # Peace
elif shape == 'peace': elif shape == 'peace':
line = ( line = ((
(drawPtX + hX - int(tenthX / 2), drawPtY + int(tenthY / 2)), drawPtX + hX - int(tenthX / 2), drawPtY + int(tenthY / 2)),
(drawPtX + hX + int(tenthX / 2), (drawPtX + hX + int(tenthX / 2),
drawPtY + self.pxHeight - int(tenthY / 2)) drawPtY + self.pxHeight - int(tenthY / 2))
) )
drawer.ellipse(outlineShape, fill=self.color) drawer.ellipse(outlineShape, fill=self.color)
drawer.ellipse(smallerShape, fill=(0,0,0,0)) drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
drawer.rectangle(line, fill=self.color) drawer.rectangle(line, fill=self.color)
slantLine = lambda difference: (
((drawPtX + difference), def slantLine(difference):
(drawPtY + self.pxHeight - qY)), return (
((drawPtX + hX), (drawPtX + difference),
(drawPtY + hY)), (drawPtY + self.pxHeight - qY)
) ),
(
(drawPtX + hX),
(drawPtY + hY)
)
drawer.line( drawer.line(
slantLine(qX), slantLine(qX),
fill=self.color, fill=self.color,
@ -338,13 +329,13 @@ class Component(Component):
for x in range(self.pxWidth, self.width, self.pxWidth): for x in range(self.pxWidth, self.width, self.pxWidth):
drawer.rectangle( drawer.rectangle(
((x, 0), ((x, 0),
(x + w, self.height)), (x + w, self.height)),
fill=self.color, fill=self.color,
) )
for y in range(self.pxHeight, self.height, self.pxHeight): for y in range(self.pxHeight, self.height, self.pxHeight):
drawer.rectangle( drawer.rectangle(
((0, y), ((0, y),
(self.width, y + h)), (self.width, y + h)),
fill=self.color, fill=self.color,
) )
@ -356,7 +347,7 @@ class Component(Component):
def neighbours(x, y): def neighbours(x, y):
return { return {
cell for cell in nearbyCoords(x, y) cell for cell in self.nearbyCoords(x, y)
if cell in lastGrid if cell in lastGrid
} }
@ -367,7 +358,7 @@ class Component(Component):
newGrid.add((x, y)) newGrid.add((x, y))
potentialNewCells = { potentialNewCells = {
coordTup for origin in lastGrid coordTup for origin in lastGrid
for coordTup in list(nearbyCoords(*origin)) for coordTup in list(self.nearbyCoords(*origin))
} }
for x, y in potentialNewCells: for x, y in potentialNewCells:
if (x, y) in newGrid: if (x, y) in newGrid:
@ -390,13 +381,95 @@ class Component(Component):
widget.setEnabled(True) widget.setEnabled(True)
super().loadPreset(pr, *args) super().loadPreset(pr, *args)
def nearbyCoords(self, x, y):
yield x + 1, y + 1
yield x + 1, y - 1
yield x - 1, y + 1
yield x - 1, y - 1
yield x, y + 1
yield x, y - 1
yield x + 1, y
yield x - 1, y
def nearbyCoords(x, y):
yield x + 1, y + 1 class ClickGrid(QUndoCommand):
yield x + 1, y - 1 def __init__(self, comp, pos, id_):
yield x - 1, y + 1 super().__init__(
yield x - 1, y - 1 "click %s component #%s" % (comp.name, comp.compPos))
yield x, y + 1 self.comp = comp
yield x, y - 1 self.pos = [pos]
yield x + 1, y self.id_ = id_
yield x - 1, y
def id(self):
return self.id_
def mergeWith(self, other):
self.pos.extend(other.pos)
return True
def add(self):
for pos in self.pos[:]:
self.comp.startingGrid.add(pos)
self.comp.update(auto=True)
def remove(self):
for pos in self.pos[:]:
self.comp.startingGrid.discard(pos)
self.comp.update(auto=True)
def redo(self):
if self.id_ == 1: # Left-click
self.add()
elif self.id_ == 2: # Right-click
self.remove()
def undo(self):
if self.id_ == 1: # Left-click
self.remove()
elif self.id_ == 2: # Right-click
self.add()
class ShiftGrid(QUndoCommand):
def __init__(self, comp, direction):
super().__init__(
"change %s component #%s" % (comp.name, comp.compPos))
self.comp = comp
self.direction = direction
self.distance = 1
def id(self):
return self.direction
def mergeWith(self, other):
self.distance += other.distance
return True
def newGrid(self, Xchange, Ychange):
return {
(x + Xchange, y + Ychange)
for x, y in self.comp.startingGrid
}
def redo(self):
if self.direction == 0:
newGrid = self.newGrid(0, -self.distance)
elif self.direction == 1:
newGrid = self.newGrid(0, self.distance)
elif self.direction == 2:
newGrid = self.newGrid(-self.distance, 0)
elif self.direction == 3:
newGrid = self.newGrid(self.distance, 0)
self.comp.startingGrid = newGrid
self.comp._sendUpdateSignal()
def undo(self):
if self.direction == 0:
newGrid = self.newGrid(0, self.distance)
elif self.direction == 1:
newGrid = self.newGrid(0, -self.distance)
elif self.direction == 2:
newGrid = self.newGrid(self.distance, 0)
elif self.direction == 3:
newGrid = self.newGrid(-self.distance, 0)
self.comp.startingGrid = newGrid
self.comp._sendUpdateSignal()

View File

@ -52,8 +52,9 @@ class Component(Component):
"Audio Files (%s)" % " ".join(self.core.audioFormats)) "Audio Files (%s)" % " ".join(self.core.audioFormats))
if filename: if filename:
self.settings.setValue("componentDir", os.path.dirname(filename)) self.settings.setValue("componentDir", os.path.dirname(filename))
self.mergeUndo = False
self.page.lineEdit_sound.setText(filename) self.page.lineEdit_sound.setText(filename)
self.update() self.mergeUndo = True
def commandHelp(self): def commandHelp(self):
print('Path to audio file:\n path=/filepath/to/sound.ogg') print('Path to audio file:\n path=/filepath/to/sound.ogg')

View File

@ -76,8 +76,6 @@ class Component(Component):
else: else:
self.page.checkBox_mono.setEnabled(True) self.page.checkBox_mono.setEnabled(True)
super().update()
def previewRender(self): def previewRender(self):
changedSize = self.updateChunksize() changedSize = self.updateChunksize()
if not changedSize \ if not changedSize \
@ -100,7 +98,8 @@ class Component(Component):
def preFrameRender(self, **kwargs): def preFrameRender(self, **kwargs):
super().preFrameRender(**kwargs) super().preFrameRender(**kwargs)
self.previewPipe.wait() if self.previewPipe is not None:
self.previewPipe.wait()
self.updateChunksize() self.updateChunksize()
w, h = scale(self.scale, self.width, self.height, str) w, h = scale(self.scale, self.width, self.height, str)
self.video = FfmpegVideo( self.video = FfmpegVideo(
@ -138,7 +137,7 @@ class Component(Component):
'-r', self.settings.value("outputFrameRate"), '-r', self.settings.value("outputFrameRate"),
'-ss', "{0:.3f}".format(startPt), '-ss', "{0:.3f}".format(startPt),
'-i', '-i',
os.path.join(self.core.wd, 'background.png') self.core.junkStream
if genericPreview else inputFile, if genericPreview else inputFile,
'-f', 'image2pipe', '-f', 'image2pipe',
'-pix_fmt', 'rgba', '-pix_fmt', 'rgba',
@ -150,15 +149,22 @@ class Component(Component):
'-codec:v', 'rawvideo', '-', '-codec:v', 'rawvideo', '-',
'-frames:v', '1', '-frames:v', '1',
]) ])
logFilename = os.path.join(
self.core.logDir, 'preview_%s.log' % str(self.compPos)) if self.core.logEnabled:
log.debug('Creating ffmpeg process (log at %s)' % logFilename) logFilename = os.path.join(
with open(logFilename, 'w') as logf: self.core.logDir, 'preview_%s.log' % str(self.compPos))
logf.write(" ".join(command) + '\n\n') log.debug('Creating ffmpeg process (log at %s)' % logFilename)
with open(logFilename, 'a') as logf: with open(logFilename, 'w') as logf:
logf.write(" ".join(command) + '\n\n')
with open(logFilename, 'a') as logf:
self.previewPipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=logf, bufsize=10**8
)
else:
self.previewPipe = openPipe( self.previewPipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=logf, bufsize=10**8 stderr=subprocess.DEVNULL, bufsize=10**8
) )
byteFrame = self.previewPipe.stdout.read(self.chunkSize) byteFrame = self.previewPipe.stdout.read(self.chunkSize)
closePipe(self.previewPipe) closePipe(self.previewPipe)

View File

@ -2,10 +2,13 @@ from PIL import ImageEnhance, ImageFilter, ImageChops
from PyQt5.QtGui import QColor, QFont from PyQt5.QtGui import QColor, QFont
from PyQt5 import QtGui, QtCore, QtWidgets from PyQt5 import QtGui, QtCore, QtWidgets
import os import os
import logging
from component import Component from component import Component
from toolkit.frame import FramePainter, PaintColor from toolkit.frame import FramePainter, PaintColor
log = logging.getLogger('AVP.Components.Text')
class Component(Component): class Component(Component):
name = 'Title Text' name = 'Title Text'
@ -13,8 +16,6 @@ class Component(Component):
def widget(self, *args): def widget(self, *args):
super().widget(*args) super().widget(*args)
self.textColor = (255, 255, 255)
self.strokeColor = (0, 0, 0)
self.title = 'Text' self.title = 'Text'
self.alignment = 1 self.alignment = 1
self.titleFont = QFont() self.titleFont = QFont()
@ -25,8 +26,6 @@ class Component(Component):
self.page.comboBox_textAlign.addItem("Right") self.page.comboBox_textAlign.addItem("Right")
self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment)) self.page.comboBox_textAlign.setCurrentIndex(int(self.alignment))
self.page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor)
self.page.lineEdit_strokeColor.setText('%s,%s,%s' % self.strokeColor)
self.page.spinBox_fontSize.setValue(int(self.fontSize)) self.page.spinBox_fontSize.setValue(int(self.fontSize))
self.page.lineEdit_title.setText(self.title) self.page.lineEdit_title.setText(self.title)
@ -72,7 +71,6 @@ class Component(Component):
self.page.spinBox_shadY.setHidden(True) self.page.spinBox_shadY.setHidden(True)
self.page.label_shadBlur.setHidden(True) self.page.label_shadBlur.setHidden(True)
self.page.spinBox_shadBlur.setHidden(True) self.page.spinBox_shadBlur.setHidden(True)
super().update()
def centerXY(self): def centerXY(self):
self.setRelativeWidget('xPosition', 0.5) self.setRelativeWidget('xPosition', 0.5)
@ -81,16 +79,15 @@ class Component(Component):
def getXY(self): def getXY(self):
'''Returns true x, y after considering alignment settings''' '''Returns true x, y after considering alignment settings'''
fm = QtGui.QFontMetrics(self.titleFont) fm = QtGui.QFontMetrics(self.titleFont)
if self.alignment == 0: # Left x = self.pixelValForAttr('xPosition')
x = int(self.xPosition)
if self.alignment == 1: # Middle if self.alignment == 1: # Middle
offset = int(fm.width(self.title)/2) offset = int(fm.width(self.title)/2)
x = self.xPosition - offset x -= offset
if self.alignment == 2: # Right if self.alignment == 2: # Right
offset = fm.width(self.title) offset = fm.width(self.title)
x = self.xPosition - offset x -= offset
return x, self.yPosition return x, self.yPosition
def loadPreset(self, pr, *args): def loadPreset(self, pr, *args):
@ -142,6 +139,7 @@ class Component(Component):
image = FramePainter(width, height) image = FramePainter(width, height)
x, y = self.getXY() x, y = self.getXY()
log.debug('Text position translates to %s, %s', x, y)
if self.stroke > 0: if self.stroke > 0:
outliner = QtGui.QPainterPathStroker() outliner = QtGui.QPainterPathStroker()
outliner.setWidth(self.stroke) outliner.setWidth(self.stroke)

View File

@ -427,6 +427,9 @@
<property name="focusPolicy"> <property name="focusPolicy">
<enum>Qt::NoFocus</enum> <enum>Qt::NoFocus</enum>
</property> </property>
<property name="text">
<string>255,255,255</string>
</property>
</widget> </widget>
</item> </item>
<item> <item>
@ -485,6 +488,9 @@
<property name="focusPolicy"> <property name="focusPolicy">
<enum>Qt::NoFocus</enum> <enum>Qt::NoFocus</enum>
</property> </property>
<property name="text">
<string>0,0,0</string>
</property>
</widget> </widget>
</item> </item>
<item> <item>

View File

@ -52,7 +52,6 @@ class Component(Component):
else: else:
self.page.label_volume.setEnabled(False) self.page.label_volume.setEnabled(False)
self.page.spinBox_volume.setEnabled(False) self.page.spinBox_volume.setEnabled(False)
super().update()
def previewRender(self): def previewRender(self):
self.updateChunksize() self.updateChunksize()
@ -118,8 +117,9 @@ class Component(Component):
) )
if filename: if filename:
self.settings.setValue("componentDir", os.path.dirname(filename)) self.settings.setValue("componentDir", os.path.dirname(filename))
self.mergeUndo = False
self.page.lineEdit_video.setText(filename) self.page.lineEdit_video.setText(filename)
self.update() self.mergeUndo = True
def getPreviewFrame(self, width, height): def getPreviewFrame(self, width, height):
if not self.videoPath or not os.path.exists(self.videoPath): if not self.videoPath or not os.path.exists(self.videoPath):
@ -139,16 +139,23 @@ class Component(Component):
'-frames:v', '1', '-frames:v', '1',
]) ])
logFilename = os.path.join( if self.core.logEnabled:
self.core.logDir, 'preview_%s.log' % str(self.compPos)) logFilename = os.path.join(
log.debug('Creating ffmpeg process (log at %s)' % logFilename) self.core.logDir, 'preview_%s.log' % str(self.compPos))
with open(logFilename, 'w') as logf: log.debug('Creating ffmpeg process (log at %s)' % logFilename)
logf.write(" ".join(command) + '\n\n') with open(logFilename, 'w') as logf:
with open(logFilename, 'a') as logf: logf.write(" ".join(command) + '\n\n')
with open(logFilename, 'a') as logf:
pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=logf, bufsize=10**8
)
else:
pipe = openPipe( pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=logf, bufsize=10**8 stderr=subprocess.DEVNULL, bufsize=10**8
) )
byteFrame = pipe.stdout.read(self.chunkSize) byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe) closePipe(pipe)

View File

@ -98,7 +98,7 @@ class Component(Component):
'-r', self.settings.value("outputFrameRate"), '-r', self.settings.value("outputFrameRate"),
'-ss', "{0:.3f}".format(startPt), '-ss', "{0:.3f}".format(startPt),
'-i', '-i',
os.path.join(self.core.wd, 'background.png') self.core.junkStream
if genericPreview else inputFile, if genericPreview else inputFile,
'-f', 'image2pipe', '-f', 'image2pipe',
'-pix_fmt', 'rgba', '-pix_fmt', 'rgba',
@ -110,15 +110,21 @@ class Component(Component):
'-codec:v', 'rawvideo', '-', '-codec:v', 'rawvideo', '-',
'-frames:v', '1', '-frames:v', '1',
]) ])
logFilename = os.path.join( if self.core.logEnabled:
self.core.logDir, 'preview_%s.log' % str(self.compPos)) logFilename = os.path.join(
log.debug('Creating ffmpeg process (log at %s)' % logFilename) self.core.logDir, 'preview_%s.log' % str(self.compPos))
with open(logFilename, 'w') as logf: log.debug('Creating ffmpeg log at %s', logFilename)
logf.write(" ".join(command) + '\n\n') with open(logFilename, 'w') as logf:
with open(logFilename, 'a') as logf: logf.write(" ".join(command) + '\n\n')
with open(logFilename, 'a') as logf:
pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=logf, bufsize=10**8
)
else:
pipe = openPipe( pipe = openPipe(
command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=logf, bufsize=10**8 stderr=subprocess.DEVNULL, bufsize=10**8
) )
byteFrame = pipe.stdout.read(self.chunkSize) byteFrame = pipe.stdout.read(self.chunkSize)
closePipe(pipe) closePipe(pipe)

View File

@ -14,7 +14,7 @@ import toolkit
log = logging.getLogger('AVP.Core') log = logging.getLogger('AVP.Core')
STDOUT_LOGLVL = logging.WARNING STDOUT_LOGLVL = logging.WARNING
FILE_LOGLVL = logging.DEBUG FILE_LOGLVL = None
class Core: class Core:
@ -32,6 +32,11 @@ class Core:
self.savedPresets = {} # copies of presets to detect modification self.savedPresets = {} # copies of presets to detect modification
self.openingProject = False self.openingProject = False
def __repr__(self):
return "\n=~=~=~=\n".join(
[repr(comp) for comp in self.selectedComponents]
)
def importComponents(self): def importComponents(self):
def findComponents(): def findComponents():
for f in os.listdir(Core.componentsPath): for f in os.listdir(Core.componentsPath):
@ -64,34 +69,41 @@ class Core:
for i, component in enumerate(self.selectedComponents): for i, component in enumerate(self.selectedComponents):
component.compPos = i component.compPos = i
def insertComponent(self, compPos, moduleIndex, loader): def insertComponent(self, compPos, component, loader):
''' '''
Creates a new component using these args: Creates a new component using these args:
(compPos, moduleIndex in self.modules, MWindow/Command/Core obj) (compPos, component obj or moduleIndex, MWindow/Command/Core obj)
''' '''
if compPos < 0 or compPos > len(self.selectedComponents): if compPos < 0 or compPos > len(self.selectedComponents):
compPos = len(self.selectedComponents) compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50: if len(self.selectedComponents) > 50:
return None return -1
log.debug('Inserting Component from module #%s' % moduleIndex) if type(component) is int:
component = self.modules[moduleIndex].Component( # create component using module index in self.modules
moduleIndex, compPos, self moduleIndex = int(component)
log.debug(
'Creating new component from module #%s', str(moduleIndex))
component = self.modules[moduleIndex].Component(
moduleIndex, compPos, self
)
component.widget(loader)
else:
moduleIndex = -1
log.debug(
'Inserting previously-created %s component', component.name)
component._error.connect(
loader.videoThreadError
) )
self.selectedComponents.insert( self.selectedComponents.insert(
compPos, compPos,
component component
) )
self.componentListChanged()
self.selectedComponents[compPos]._error.connect(
loader.videoThreadError
)
# init component's widget for loading/saving presets
self.selectedComponents[compPos].widget(loader)
self.updateComponent(compPos)
if hasattr(loader, 'insertComponent'): if hasattr(loader, 'insertComponent'):
loader.insertComponent(compPos) loader.insertComponent(compPos)
self.componentListChanged()
self.updateComponent(compPos)
return compPos return compPos
def moveComponent(self, startI, endI): def moveComponent(self, startI, endI):
@ -110,8 +122,10 @@ class Core:
self.componentListChanged() self.componentListChanged()
def updateComponent(self, i): def updateComponent(self, i):
log.debug('Updating %s #%s' % (self.selectedComponents[i], str(i))) log.debug(
self.selectedComponents[i].update() 'Auto-updating %s #%s',
self.selectedComponents[i], str(i))
self.selectedComponents[i].update(auto=True)
def moduleIndexFor(self, compName): def moduleIndexFor(self, compName):
try: try:
@ -130,18 +144,11 @@ class Core:
saveValueStore = self.getPreset(filepath) saveValueStore = self.getPreset(filepath)
if not saveValueStore: if not saveValueStore:
return False return False
try: comp = self.selectedComponents[compIndex]
comp = self.selectedComponents[compIndex] comp.loadPreset(
comp.loadPreset( saveValueStore,
saveValueStore, presetName
presetName )
)
except KeyError as e:
log.warning(
'%s #%s\'s preset is missing value: %s' % (
comp.name, str(compIndex), str(e)
)
)
self.savedPresets[presetName] = dict(saveValueStore) self.savedPresets[presetName] = dict(saveValueStore)
return True return True
@ -156,6 +163,10 @@ class Core:
break break
return saveValueStore return saveValueStore
def getPresetDir(self, comp):
'''Get the preset subdir for a particular version of a component'''
return os.path.join(Core.presetDir, comp.name, str(comp.version))
def openProject(self, loader, filepath): def openProject(self, loader, filepath):
''' loader is the object calling this method which must have ''' loader is the object calling this method which must have
its own showMessage(**kwargs) method for displaying errors. its own showMessage(**kwargs) method for displaying errors.
@ -171,13 +182,11 @@ class Core:
if hasattr(loader, 'window'): if hasattr(loader, 'window'):
for widget, value in data['WindowFields']: for widget, value in data['WindowFields']:
widget = eval('loader.window.%s' % widget) widget = eval('loader.window.%s' % widget)
widget.blockSignals(True) with toolkit.blockSignals(widget):
toolkit.setWidgetValue(widget, value) toolkit.setWidgetValue(widget, value)
widget.blockSignals(False)
for key, value in data['Settings']: for key, value in data['Settings']:
Core.settings.setValue(key, value) Core.settings.setValue(key, value)
for tup in data['Components']: for tup in data['Components']:
name, vers, preset = tup name, vers, preset = tup
clearThis = False clearThis = False
@ -202,7 +211,7 @@ class Core:
self.moduleIndexFor(name), self.moduleIndexFor(name),
loader loader
) )
if i is None: if i == -1:
loader.showMessage(msg="Too many components!") loader.showMessage(msg="Too many components!")
break break
@ -255,7 +264,7 @@ class Core:
Returns dictionary with section names as the keys, each one Returns dictionary with section names as the keys, each one
contains a list of tuples: (compName, version, compPresetDict) contains a list of tuples: (compName, version, compPresetDict)
''' '''
log.debug('Parsing av file: %s' % filepath) log.debug('Parsing av file: %s', filepath)
validSections = ( validSections = (
'Components', 'Components',
'Settings', 'Settings',
@ -374,7 +383,7 @@ class Core:
def createProjectFile(self, filepath, window=None): def createProjectFile(self, filepath, window=None):
'''Create a project file (.avp) using the current program state''' '''Create a project file (.avp) using the current program state'''
log.info('Creating %s' % filepath) log.info('Creating %s', filepath)
settingsKeys = [ settingsKeys = [
'componentDir', 'componentDir',
'inputDir', 'inputDir',
@ -448,26 +457,30 @@ class Core:
dataDir = QtCore.QStandardPaths.writableLocation( dataDir = QtCore.QStandardPaths.writableLocation(
QtCore.QStandardPaths.AppConfigLocation QtCore.QStandardPaths.AppConfigLocation
) )
# Windows: C:/Users/<USER>/AppData/Local/audio-visualizer
# macOS: ~/Library/Preferences/audio-visualizer
# Linux: ~/.config/audio-visualizer
with open(os.path.join(wd, 'encoder-options.json')) as json_file: with open(os.path.join(wd, 'encoder-options.json')) as json_file:
encoderOptions = json.load(json_file) encoderOptions = json.load(json_file)
settings = { settings = {
'canceled': False,
'FFMPEG_BIN': findFfmpeg(),
'dataDir': dataDir, 'dataDir': dataDir,
'settings': QtCore.QSettings( 'settings': QtCore.QSettings(
os.path.join(dataDir, 'settings.ini'), os.path.join(dataDir, 'settings.ini'),
QtCore.QSettings.IniFormat), QtCore.QSettings.IniFormat),
'logDir': os.path.join(dataDir, 'log'),
'presetDir': os.path.join(dataDir, 'presets'), 'presetDir': os.path.join(dataDir, 'presets'),
'componentsPath': os.path.join(wd, 'components'), 'componentsPath': os.path.join(wd, 'components'),
'junkStream': os.path.join(wd, 'gui', 'background.png'),
'encoderOptions': encoderOptions, 'encoderOptions': encoderOptions,
'resolutions': [ 'resolutions': [
'1920x1080', '1920x1080',
'1280x720', '1280x720',
'854x480', '854x480',
], ],
'FFMPEG_BIN': findFfmpeg(), 'logDir': os.path.join(dataDir, 'log'),
'windowHasFocus': False, 'logEnabled': False,
'canceled': False,
} }
settings['videoFormats'] = toolkit.appendUppercase([ settings['videoFormats'] = toolkit.appendUppercase([
@ -528,6 +541,7 @@ class Core:
"projectDir": os.path.join(cls.dataDir, 'projects'), "projectDir": os.path.join(cls.dataDir, 'projects'),
"pref_insertCompAtTop": True, "pref_insertCompAtTop": True,
"pref_genericPreview": True, "pref_genericPreview": True,
"pref_undoLimit": 10,
} }
for parm, value in cls.defaultSettings.items(): for parm, value in cls.defaultSettings.items():
@ -540,47 +554,53 @@ class Core:
if not key.startswith('pref_'): if not key.startswith('pref_'):
continue continue
val = cls.settings.value(key) val = cls.settings.value(key)
if val in ('true', 'false'): try:
cls.settings.setValue(key, True if val == 'true' else False) val = int(val)
except ValueError:
if val == 'true':
val = True
elif val == 'false':
val = False
cls.settings.setValue(key, val)
@staticmethod @staticmethod
def makeLogger(): def makeLogger():
logFilename = os.path.join(Core.logDir, 'avp_debug.log') # send critical log messages to stdout
libLogFilename = os.path.join(Core.logDir, 'global_debug.log')
# delete old logs
for log in (logFilename, libLogFilename):
if os.path.exists(log):
os.remove(log)
# create file handlers to capture every log message somewhere
logFile = logging.FileHandler(logFilename)
logFile.setLevel(FILE_LOGLVL)
libLogFile = logging.FileHandler(libLogFilename)
libLogFile.setLevel(FILE_LOGLVL)
# send some critical log messages to stdout as well
logStream = logging.StreamHandler() logStream = logging.StreamHandler()
logStream.setLevel(STDOUT_LOGLVL) logStream.setLevel(STDOUT_LOGLVL)
# create formatters for each stream
fileFormatter = logging.Formatter(
'[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: '
'%(message)s'
)
streamFormatter = logging.Formatter( streamFormatter = logging.Formatter(
'<%(name)s> %(message)s' '<%(name)s> %(levelname)s: %(message)s'
) )
logFile.setFormatter(fileFormatter)
libLogFile.setFormatter(fileFormatter)
logStream.setFormatter(streamFormatter) logStream.setFormatter(streamFormatter)
log = logging.getLogger('AVP') log = logging.getLogger('AVP')
log.addHandler(logFile)
log.addHandler(logStream) log.addHandler(logStream)
libLog = logging.getLogger()
libLog.addHandler(libLogFile) if FILE_LOGLVL is not None:
# lowest level must be explicitly set on the root Logger # write log files as well!
libLog.setLevel(0) Core.logEnabled = True
logFilename = os.path.join(Core.logDir, 'avp_debug.log')
libLogFilename = os.path.join(Core.logDir, 'global_debug.log')
# delete old logs
for log_ in (logFilename, libLogFilename):
if os.path.exists(log_):
os.remove(log_)
logFile = logging.FileHandler(logFilename)
logFile.setLevel(FILE_LOGLVL)
libLogFile = logging.FileHandler(libLogFilename)
libLogFile.setLevel(FILE_LOGLVL)
fileFormatter = logging.Formatter(
'[%(asctime)s] %(threadName)-10.10s %(name)-23.23s %(levelname)s: '
'%(message)s'
)
logFile.setFormatter(fileFormatter)
libLogFile.setFormatter(fileFormatter)
libLog = logging.getLogger()
log.addHandler(logFile)
libLog.addHandler(libLogFile)
# lowest level must be explicitly set on the root Logger
libLog.setLevel(0)
# always store settings in class variables even if a Core object is not created # always store settings in class variables even if a Core object is not created
Core.storeSettings() Core.storeSettings()

0
src/gui/__init__.py Normal file
View File

191
src/gui/actions.py Normal file
View File

@ -0,0 +1,191 @@
'''
QCommand classes for every undoable user action performed in the MainWindow
'''
from PyQt5.QtWidgets import QUndoCommand
import os
from copy import copy
from core import Core
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# COMPONENT ACTIONS
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
class AddComponent(QUndoCommand):
def __init__(self, parent, compI, moduleI):
super().__init__(
"create new %s component" %
parent.core.modules[moduleI].Component.name
)
self.parent = parent
self.moduleI = moduleI
self.compI = compI
self.comp = None
def redo(self):
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)
class RemoveComponent(QUndoCommand):
def __init__(self, parent, selectedRows):
super().__init__('remove component')
self.parent = parent
componentList = self.parent.window.listWidget_componentList
self.selectedRows = [
componentList.row(selected) for selected in selectedRows
]
self.components = [
parent.core.selectedComponents[i] for i in self.selectedRows
]
def redo(self):
self.parent._removeComponent(self.selectedRows[0])
def undo(self):
componentList = self.parent.window.listWidget_componentList
for index, comp in zip(self.selectedRows, self.components):
self.parent.core.insertComponent(
index, comp, self.parent
)
self.parent.drawPreview()
class MoveComponent(QUndoCommand):
def __init__(self, parent, row, newRow, tag):
super().__init__("move component %s" % tag)
self.parent = parent
self.row = row
self.newRow = newRow
self.id_ = ord(tag[0])
def id(self):
'''If 2 consecutive updates have same id, Qt will call mergeWith()'''
return self.id_
def mergeWith(self, other):
self.newRow = other.newRow
return True
def do(self, rowa, rowb):
componentList = self.parent.window.listWidget_componentList
page = self.parent.pages.pop(rowa)
self.parent.pages.insert(rowb, page)
item = componentList.takeItem(rowa)
componentList.insertItem(rowb, item)
stackedWidget = self.parent.window.stackedWidget
widget = stackedWidget.removeWidget(page)
stackedWidget.insertWidget(rowb, page)
componentList.setCurrentRow(rowb)
stackedWidget.setCurrentIndex(rowb)
self.parent.core.moveComponent(rowa, rowb)
self.parent.drawPreview(True)
def redo(self):
self.do(self.row, self.newRow)
def undo(self):
self.do(self.newRow, self.row)
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# PRESET ACTIONS
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
class ClearPreset(QUndoCommand):
def __init__(self, parent, compI):
super().__init__("clear preset")
self.parent = parent
self.compI = compI
self.component = self.parent.core.selectedComponents[compI]
self.store = self.component.savePreset()
self.store['preset'] = self.component.currentPreset
def redo(self):
self.parent.core.clearPreset(self.compI)
self.parent.updateComponentTitle(self.compI, False)
def undo(self):
self.parent.core.selectedComponents[self.compI].loadPreset(self.store)
self.parent.updateComponentTitle(self.compI, self.store)
class OpenPreset(QUndoCommand):
def __init__(self, parent, presetName, compI):
super().__init__("open %s preset" % presetName)
self.parent = parent
self.presetName = presetName
self.compI = compI
comp = self.parent.core.selectedComponents[compI]
self.store = comp.savePreset()
self.store['preset'] = copy(comp.currentPreset)
def redo(self):
self.parent._openPreset(self.presetName, self.compI)
def undo(self):
self.parent.core.selectedComponents[self.compI].loadPreset(
self.store)
self.parent.parent.updateComponentTitle(self.compI, self.store)
class RenamePreset(QUndoCommand):
def __init__(self, parent, path, oldName, newName):
super().__init__('rename preset')
self.parent = parent
self.path = path
self.oldName = oldName
self.newName = newName
def redo(self):
self.parent.renamePreset(self.path, self.oldName, self.newName)
def undo(self):
self.parent.renamePreset(self.path, self.newName, self.oldName)
class DeletePreset(QUndoCommand):
def __init__(self, parent, compName, vers, presetFile):
self.parent = parent
self.preset = (compName, vers, presetFile)
self.path = os.path.join(
Core.presetDir, compName, str(vers), presetFile
)
self.store = self.parent.core.getPreset(self.path)
self.presetName = self.store['preset']
super().__init__('delete %s preset (%s)' % (self.presetName, compName))
self.loadedPresets = [
i for i, comp in enumerate(self.parent.core.selectedComponents)
if self.presetName == str(comp.currentPreset)
]
def redo(self):
os.remove(self.path)
for i in self.loadedPresets:
self.parent.core.clearPreset(i)
self.parent.parent.updateComponentTitle(i, False)
self.parent.findPresets()
self.parent.drawPresetList()
def undo(self):
self.parent.createNewPreset(*self.preset, self.store)
selectedComponents = self.parent.core.selectedComponents
for i in self.loadedPresets:
selectedComponents[i].currentPreset = self.presetName
self.parent.parent.updateComponentTitle(i)
self.parent.findPresets()
self.parent.drawPresetList()

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -11,18 +11,22 @@ from queue import Queue
import sys import sys
import os import os
import signal import signal
import atexit
import filecmp import filecmp
import time import time
import logging import logging
from core import Core from core import Core
import preview_thread import gui.preview_thread as preview_thread
from preview_win import PreviewWindow from gui.preview_win import PreviewWindow
from presetmanager import PresetManager from gui.presetmanager import PresetManager
from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput from gui.actions import *
from toolkit import (
disableWhenEncoding, disableWhenOpeningProject, checkOutput, blockSignals
)
log = logging.getLogger('AVP.MainWindow') log = logging.getLogger('AVP.Gui.MainWindow')
class MainWindow(QtWidgets.QMainWindow): class MainWindow(QtWidgets.QMainWindow):
@ -41,11 +45,11 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(self, window, project): def __init__(self, window, project):
QtWidgets.QMainWindow.__init__(self) QtWidgets.QMainWindow.__init__(self)
self.window = window
self.core = Core()
log.debug( log.debug(
'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId()))) 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
self.window = window
self.core = Core()
Core.mode = 'GUI'
# widgets of component settings # widgets of component settings
self.pages = [] self.pages = []
self.lastAutosave = time.time() self.lastAutosave = time.time()
@ -60,14 +64,24 @@ class MainWindow(QtWidgets.QMainWindow):
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
self.settings = Core.settings self.settings = Core.settings
# Register clean-up functions
signal.signal(signal.SIGINT, self.terminate)
atexit.register(self.cleanUp)
# Create stack of undoable user actions
self.undoStack = QtWidgets.QUndoStack(self)
undoLimit = self.settings.value("pref_undoLimit")
self.undoStack.setUndoLimit(undoLimit)
# Create Preset Manager
self.presetManager = PresetManager( self.presetManager = PresetManager(
uic.loadUi( uic.loadUi(
os.path.join(Core.wd, 'presetmanager.ui')), self) os.path.join(Core.wd, 'gui', 'presetmanager.ui')), self)
# Create the preview window and its thread, queues, and timers # Create the preview window and its thread, queues, and timers
log.debug('Creating preview window') log.debug('Creating preview window')
self.previewWindow = PreviewWindow(self, os.path.join( self.previewWindow = PreviewWindow(self, os.path.join(
Core.wd, "background.png")) Core.wd, 'gui', "background.png"))
window.verticalLayout_previewWrapper.addWidget(self.previewWindow) window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
log.debug('Starting preview thread') log.debug('Starting preview thread')
@ -78,16 +92,58 @@ class MainWindow(QtWidgets.QMainWindow):
self.previewWorker.moveToThread(self.previewThread) self.previewWorker.moveToThread(self.previewThread)
self.previewWorker.imageCreated.connect(self.showPreviewImage) self.previewWorker.imageCreated.connect(self.showPreviewImage)
self.previewThread.start() self.previewThread.start()
self.previewThread.finished.connect(
lambda:
log.critical('PREVIEW THREAD DIED! This should never happen.')
)
log.debug('Starting preview timer') timeout = 500
log.debug(
'Preview timer set to trigger when idle for %sms' % str(timeout)
)
self.timer = QtCore.QTimer(self) self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.processTask.emit) self.timer.timeout.connect(self.processTask.emit)
self.timer.start(500) self.timer.start(timeout)
# Begin decorating the window and connecting events # Begin decorating the window and connecting events
self.window.installEventFilter(self)
componentList = self.window.listWidget_componentList componentList = self.window.listWidget_componentList
style = window.pushButton_undo.style()
undoButton = window.pushButton_undo
undoButton.setIcon(
style.standardIcon(QtWidgets.QStyle.SP_FileDialogBack)
)
undoButton.clicked.connect(self.undoStack.undo)
undoButton.setEnabled(False)
self.undoStack.cleanChanged.connect(
lambda change: undoButton.setEnabled(self.undoStack.count())
)
self.undoMenu = QMenu()
self.undoMenu.addAction(
self.undoStack.createUndoAction(self)
)
self.undoMenu.addAction(
self.undoStack.createRedoAction(self)
)
action = self.undoMenu.addAction('Show History...')
action.triggered.connect(
lambda _: self.showUndoStack()
)
undoButton.setMenu(self.undoMenu)
style = window.pushButton_listMoveUp.style()
window.pushButton_listMoveUp.setIcon(
style.standardIcon(QtWidgets.QStyle.SP_ArrowUp)
)
style = window.pushButton_listMoveDown.style()
window.pushButton_listMoveDown.setIcon(
style.standardIcon(QtWidgets.QStyle.SP_ArrowDown)
)
style = window.pushButton_removeComponent.style()
window.pushButton_removeComponent.setIcon(
style.standardIcon(QtWidgets.QStyle.SP_DialogDiscardButton)
)
if sys.platform == 'darwin': if sys.platform == 'darwin':
log.debug( log.debug(
'Darwin detected: showing progress label below progress bar') 'Darwin detected: showing progress label below progress bar')
@ -158,7 +214,7 @@ class MainWindow(QtWidgets.QMainWindow):
for i, comp in enumerate(self.core.modules): for i, comp in enumerate(self.core.modules):
action = self.compMenu.addAction(comp.Component.name) action = self.compMenu.addAction(comp.Component.name)
action.triggered.connect( action.triggered.connect(
lambda _, item=i: self.core.insertComponent(0, item, self) lambda _, item=i: self.addComponent(0, item)
) )
self.window.pushButton_addComponent.setMenu(self.compMenu) self.window.pushButton_addComponent.setMenu(self.compMenu)
@ -299,6 +355,10 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog) QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject) QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject)
QtWidgets.QShortcut("Ctrl+Z", self.window, self.undoStack.undo)
QtWidgets.QShortcut("Ctrl+Y", self.window, self.undoStack.redo)
QtWidgets.QShortcut("Ctrl+Shift+Z", self.window, self.undoStack.redo)
# Hotkeys for component list # Hotkeys for component list
for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert): for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert):
QtWidgets.QShortcut( QtWidgets.QShortcut(
@ -339,21 +399,41 @@ class MainWindow(QtWidgets.QMainWindow):
activated=lambda: self.moveComponent('bottom') activated=lambda: self.moveComponent('bottom')
) )
# Debug Hotkeys
QtWidgets.QShortcut( QtWidgets.QShortcut(
"Ctrl+Alt+Shift+R", self.window, self.drawPreview "Ctrl+Shift+F", self.window, self.showFfmpegCommand
) )
QtWidgets.QShortcut( QtWidgets.QShortcut(
"Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand "Ctrl+Shift+U", self.window, self.showUndoStack
)
if log.isEnabledFor(logging.DEBUG):
QtWidgets.QShortcut(
"Ctrl+Alt+Shift+R", self.window, self.drawPreview
)
QtWidgets.QShortcut(
"Ctrl+Alt+Shift+A", self.window, lambda: log.debug(repr(self))
)
def __repr__(self):
return (
'\n%s\n'
'#####\n'
'Preview thread is %s\n' % (
repr(self.core),
'live' if self.previewThread.isRunning() else 'dead',
)
) )
@QtCore.pyqtSlot()
def cleanUp(self, *args): def cleanUp(self, *args):
log.info('Ending the preview thread') log.info('Ending the preview thread')
self.timer.stop() self.timer.stop()
self.previewThread.quit() self.previewThread.quit()
self.previewThread.wait() self.previewThread.wait()
def terminate(self, *args):
self.cleanUp()
sys.exit(0)
@disableWhenOpeningProject @disableWhenOpeningProject
def updateWindowTitle(self): def updateWindowTitle(self):
appName = 'Audio Visualizer' appName = 'Audio Visualizer'
@ -366,35 +446,43 @@ class MainWindow(QtWidgets.QMainWindow):
appName += '*' appName += '*'
except AttributeError: except AttributeError:
pass pass
log.debug('Setting window title to %s' % appName) log.verbose('Setting window title to %s' % appName)
self.window.setWindowTitle(appName) self.window.setWindowTitle(appName)
@QtCore.pyqtSlot(int, dict) @QtCore.pyqtSlot(int, dict)
def updateComponentTitle(self, pos, presetStore=False): def updateComponentTitle(self, pos, presetStore=False):
'''
Sets component title to modified or unmodified when given boolean.
If given a preset dict, compares it against the component to
determine if it is modified.
A component with no preset is always unmodified.
'''
if type(presetStore) is dict: if type(presetStore) is dict:
name = presetStore['preset'] name = presetStore['preset']
if name is None or name not in self.core.savedPresets: if name is None or name not in self.core.savedPresets:
modified = False modified = False
else: else:
modified = (presetStore != self.core.savedPresets[name]) modified = (presetStore != self.core.savedPresets[name])
else:
modified = bool(presetStore) modified = bool(presetStore)
if pos < 0: if pos < 0:
pos = len(self.core.selectedComponents)-1 pos = len(self.core.selectedComponents)-1
name = str(self.core.selectedComponents[pos]) name = self.core.selectedComponents[pos].name
title = str(name) title = str(name)
if self.core.selectedComponents[pos].currentPreset: if self.core.selectedComponents[pos].currentPreset:
title += ' - %s' % self.core.selectedComponents[pos].currentPreset title += ' - %s' % self.core.selectedComponents[pos].currentPreset
if modified: if modified:
title += '*' title += '*'
if type(presetStore) is bool: if type(presetStore) is bool:
log.debug('Forcing %s #%s\'s modified status to %s: %s' % ( log.debug(
'Forcing %s #%s\'s modified status to %s: %s',
name, pos, modified, title name, pos, modified, title
)) )
else: else:
log.debug('Setting %s #%s\'s title: %s' % ( log.debug(
'Setting %s #%s\'s title: %s',
name, pos, title name, pos, title
)) )
self.window.listWidget_componentList.item(pos).setText(title) self.window.listWidget_componentList.item(pos).setText(title)
def updateCodecs(self): def updateCodecs(self):
@ -471,7 +559,7 @@ class MainWindow(QtWidgets.QMainWindow):
return True return True
except FileNotFoundError: except FileNotFoundError:
log.error( log.error(
'Project file couldn\'t be located:', self.currentProject) 'Project file couldn\'t be located: %s', self.currentProject)
return identical return identical
return False return False
@ -568,6 +656,7 @@ class MainWindow(QtWidgets.QMainWindow):
detail=detail, detail=detail,
icon='Critical', icon='Critical',
) )
log.info('%s', repr(self))
def changeEncodingStatus(self, status): def changeEncodingStatus(self, status):
self.encoding = status self.encoding = status
@ -651,6 +740,14 @@ class MainWindow(QtWidgets.QMainWindow):
def showPreviewImage(self, image): def showPreviewImage(self, image):
self.previewWindow.changePixmap(image) self.previewWindow.changePixmap(image)
def showUndoStack(self):
dialog = QtWidgets.QDialog(self.window)
undoView = QtWidgets.QUndoView(self.undoStack)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(undoView)
dialog.setLayout(layout)
dialog.show()
def showFfmpegCommand(self): def showFfmpegCommand(self):
from textwrap import wrap from textwrap import wrap
from toolkit.ffmpeg import createFfmpegCommand from toolkit.ffmpeg import createFfmpegCommand
@ -664,7 +761,13 @@ class MainWindow(QtWidgets.QMainWindow):
msg="Current FFmpeg command:\n\n %s" % " ".join(lines) msg="Current FFmpeg command:\n\n %s" % " ".join(lines)
) )
def addComponent(self, compPos, moduleIndex):
'''Creates an undoable action that adds a new component.'''
action = AddComponent(self, compPos, moduleIndex)
self.undoStack.push(action)
def insertComponent(self, index): def insertComponent(self, index):
'''Triggered by Core to finish initializing a new component.'''
componentList = self.window.listWidget_componentList componentList = self.window.listWidget_componentList
stackedWidget = self.window.stackedWidget stackedWidget = self.window.stackedWidget
@ -685,41 +788,38 @@ class MainWindow(QtWidgets.QMainWindow):
def removeComponent(self): def removeComponent(self):
componentList = self.window.listWidget_componentList componentList = self.window.listWidget_componentList
selected = componentList.selectedItems()
if selected:
action = RemoveComponent(self, selected)
self.undoStack.push(action)
for selected in componentList.selectedItems(): def _removeComponent(self, index):
index = componentList.row(selected) stackedWidget = self.window.stackedWidget
self.window.stackedWidget.removeWidget(self.pages[index]) componentList = self.window.listWidget_componentList
componentList.takeItem(index) stackedWidget.removeWidget(self.pages[index])
self.core.removeComponent(index) componentList.takeItem(index)
self.pages.pop(index) self.core.removeComponent(index)
self.changeComponentWidget() self.pages.pop(index)
self.changeComponentWidget()
self.drawPreview() self.drawPreview()
@disableWhenEncoding @disableWhenEncoding
def moveComponent(self, change): def moveComponent(self, change):
'''Moves a component relatively from its current position''' '''Moves a component relatively from its current position'''
componentList = self.window.listWidget_componentList componentList = self.window.listWidget_componentList
tag = change
if change == 'top': if change == 'top':
change = -componentList.currentRow() change = -componentList.currentRow()
elif change == 'bottom': elif change == 'bottom':
change = len(componentList)-componentList.currentRow()-1 change = len(componentList)-componentList.currentRow()-1
stackedWidget = self.window.stackedWidget else:
tag = 'down' if change == 1 else 'up'
row = componentList.currentRow() row = componentList.currentRow()
newRow = row + change newRow = row + change
if newRow > -1 and newRow < componentList.count(): if newRow > -1 and newRow < componentList.count():
self.core.moveComponent(row, newRow) action = MoveComponent(self, row, newRow, tag)
self.undoStack.push(action)
# update widgets
page = self.pages.pop(row)
self.pages.insert(newRow, page)
item = componentList.takeItem(row)
newItem = componentList.insertItem(newRow, item)
widget = stackedWidget.removeWidget(page)
stackedWidget.insertWidget(newRow, page)
componentList.setCurrentRow(newRow)
stackedWidget.setCurrentIndex(newRow)
self.drawPreview(True)
def getComponentListMousePos(self, position): def getComponentListMousePos(self, position):
''' '''
@ -777,11 +877,11 @@ class MainWindow(QtWidgets.QMainWindow):
self.window.lineEdit_audioFile, self.window.lineEdit_audioFile,
self.window.lineEdit_outputFile self.window.lineEdit_outputFile
): ):
field.blockSignals(True) with blockSignals(field):
field.setText('') field.setText('')
field.blockSignals(False)
self.progressBarUpdated(0) self.progressBarUpdated(0)
self.progressBarSetText('') self.progressBarSetText('')
self.undoStack.clear()
@disableWhenEncoding @disableWhenEncoding
def createNewProject(self, prompt=True): def createNewProject(self, prompt=True):
@ -845,7 +945,7 @@ class MainWindow(QtWidgets.QMainWindow):
def openProject(self, filepath, prompt=True): def openProject(self, filepath, prompt=True):
if not filepath or not os.path.exists(filepath) \ if not filepath or not os.path.exists(filepath) \
or not filepath.endswith('.avp'): or not filepath.endswith('.avp'):
return return
self.clear() self.clear()
@ -928,19 +1028,10 @@ class MainWindow(QtWidgets.QMainWindow):
for i, comp in enumerate(self.core.modules): for i, comp in enumerate(self.core.modules):
menuItem = self.submenu.addAction(comp.Component.name) menuItem = self.submenu.addAction(comp.Component.name)
menuItem.triggered.connect( menuItem.triggered.connect(
lambda _, item=i: self.core.insertComponent( lambda _, item=i: self.addComponent(
0 if insertCompAtTop else index, item, self 0 if insertCompAtTop else index, item
) )
) )
self.menu.move(parentPosition + QPos) self.menu.move(parentPosition + QPos)
self.menu.show() self.menu.show()
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.WindowActivate \
or event.type() == QtCore.QEvent.FocusIn:
Core.windowHasFocus = True
elif event.type() == QtCore.QEvent.WindowDeactivate \
or event.type() == QtCore.QEvent.FocusOut:
Core.windowHasFocus = False
return False

View File

@ -110,6 +110,13 @@
<property name="sizeConstraint"> <property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum> <enum>QLayout::SetMinimumSize</enum>
</property> </property>
<item>
<widget class="QPushButton" name="pushButton_undo">
<property name="text">
<string>Undo</string>
</property>
</widget>
</item>
<item> <item>
<spacer name="horizontalSpacer_6"> <spacer name="horizontalSpacer_6">
<property name="orientation"> <property name="orientation">

View File

@ -5,9 +5,14 @@
from PyQt5 import QtCore, QtWidgets from PyQt5 import QtCore, QtWidgets
import string import string
import os import os
import logging
from toolkit import badName from toolkit import badName
from core import Core from core import Core
from gui.actions import *
log = logging.getLogger('AVP.Gui.PresetManager')
class PresetManager(QtWidgets.QDialog): class PresetManager(QtWidgets.QDialog):
@ -130,8 +135,8 @@ class PresetManager(QtWidgets.QDialog):
def clearPreset(self, compI=None): def clearPreset(self, compI=None):
'''Functions on mainwindow level from the context menu''' '''Functions on mainwindow level from the context menu'''
compI = self.parent.window.listWidget_componentList.currentRow() compI = self.parent.window.listWidget_componentList.currentRow()
self.core.clearPreset(compI) action = ClearPreset(self.parent, compI)
self.parent.updateComponentTitle(compI, False) self.parent.undoStack.push(action)
def openSavePresetDialog(self): def openSavePresetDialog(self):
'''Functions on mainwindow level from the context menu''' '''Functions on mainwindow level from the context menu'''
@ -196,12 +201,16 @@ class PresetManager(QtWidgets.QDialog):
def openPreset(self, presetName, compPos=None): def openPreset(self, presetName, compPos=None):
componentList = self.parent.window.listWidget_componentList componentList = self.parent.window.listWidget_componentList
selectedComponents = self.core.selectedComponents
index = compPos if compPos is not None else componentList.currentRow() index = compPos if compPos is not None else componentList.currentRow()
if index == -1: if index == -1:
return return
componentName = str(selectedComponents[index]).strip() action = OpenPreset(self, presetName, index)
self.parent.undoStack.push(action)
def _openPreset(self, presetName, index):
selectedComponents = self.core.selectedComponents
componentName = selectedComponents[index].name.strip()
version = selectedComponents[index].version version = selectedComponents[index].version
dirname = os.path.join(self.presetDir, componentName, str(version)) dirname = os.path.join(self.presetDir, componentName, str(version))
filepath = os.path.join(dirname, presetName) filepath = os.path.join(dirname, presetName)
@ -224,16 +233,10 @@ class PresetManager(QtWidgets.QDialog):
if not ch: if not ch:
return return
self.deletePreset(comp, vers, name) self.deletePreset(comp, vers, name)
self.findPresets()
self.drawPresetList()
for i, comp in enumerate(self.core.selectedComponents):
if comp.currentPreset == name:
self.clearPreset(i)
def deletePreset(self, comp, vers, name): def deletePreset(self, comp, vers, name):
filepath = os.path.join(self.presetDir, comp, str(vers), name) action = DeletePreset(self, comp, vers, name)
os.remove(filepath) self.parent.undoStack.push(action)
def warnMessage(self, window=None): def warnMessage(self, window=None):
self.parent.showMessage( self.parent.showMessage(
@ -270,7 +273,6 @@ class PresetManager(QtWidgets.QDialog):
return index return index
def openRenamePresetDialog(self): def openRenamePresetDialog(self):
# TODO: maintain consistency by changing this to call createNewPreset()
presetList = self.window.listWidget_presets presetList = self.window.listWidget_presets
index = self.getPresetRow() index = self.getPresetRow()
if index == -1: if index == -1:
@ -293,22 +295,28 @@ class PresetManager(QtWidgets.QDialog):
path = os.path.join( path = os.path.join(
self.presetDir, comp, str(vers)) self.presetDir, comp, str(vers))
newPath = os.path.join(path, newName) newPath = os.path.join(path, newName)
oldPath = os.path.join(path, oldName)
if self.presetExists(newPath): if self.presetExists(newPath):
return return
if os.path.exists(newPath): action = RenamePreset(self, path, oldName, newName)
os.remove(newPath) self.parent.undoStack.push(action)
os.rename(oldPath, newPath)
self.findPresets()
self.drawPresetList()
for i, comp in enumerate(self.core.selectedComponents):
if getPresetDir(comp) == path \
and comp.currentPreset == oldName:
self.core.openPreset(newPath, i, newName)
self.parent.updateComponentTitle(i, False)
self.parent.drawPreview()
break break
def renamePreset(self, path, oldName, newName):
oldPath = os.path.join(path, oldName)
newPath = os.path.join(path, newName)
if os.path.exists(newPath):
os.remove(newPath)
os.rename(oldPath, newPath)
self.findPresets()
self.drawPresetList()
path = os.path.dirname(newPath)
for i, comp in enumerate(self.core.selectedComponents):
if self.core.getPresetDir(comp) == path \
and comp.currentPreset == oldName:
self.core.openPreset(newPath, i, newName)
self.parent.updateComponentTitle(i, False)
self.parent.drawPreview()
def openImportDialog(self): def openImportDialog(self):
filename, _ = QtWidgets.QFileDialog.getOpenFileName( filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Import Preset File", self.window, "Import Preset File",
@ -351,8 +359,3 @@ class PresetManager(QtWidgets.QDialog):
def clearPresetListSelection(self): def clearPresetListSelection(self):
self.window.listWidget_presets.setCurrentRow(-1) self.window.listWidget_presets.setCurrentRow(-1)
def getPresetDir(comp):
'''Get the preset subdir for a particular version of a component'''
return os.path.join(Core.presetDir, str(comp), str(comp.version))

View File

@ -14,7 +14,7 @@ from toolkit.frame import Checkerboard
from toolkit import disableWhenOpeningProject from toolkit import disableWhenOpeningProject
log = logging.getLogger("AVP.PreviewThread") log = logging.getLogger("AVP.Gui.PreviewThread")
class Worker(QtCore.QObject): class Worker(QtCore.QObject):
@ -45,8 +45,6 @@ class Worker(QtCore.QObject):
@pyqtSlot() @pyqtSlot()
def process(self): def process(self):
width = int(self.settings.value('outputWidth'))
height = int(self.settings.value('outputHeight'))
try: try:
nextPreviewInformation = self.queue.get(block=False) nextPreviewInformation = self.queue.get(block=False)
while self.queue.qsize() >= 2: while self.queue.qsize() >= 2:
@ -54,12 +52,14 @@ class Worker(QtCore.QObject):
self.queue.get(block=False) self.queue.get(block=False)
except Empty: except Empty:
continue continue
width = int(self.settings.value('outputWidth'))
height = int(self.settings.value('outputHeight'))
if self.background.width != width \ if self.background.width != width \
or self.background.height != height: or self.background.height != height:
self.background = Checkerboard(width, height) self.background = Checkerboard(width, height)
frame = self.background.copy() frame = self.background.copy()
log.debug('Creating new preview frame') log.info('Creating new preview frame')
components = nextPreviewInformation["components"] components = nextPreviewInformation["components"]
for component in reversed(components): for component in reversed(components):
try: try:

View File

@ -1,14 +1,14 @@
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
import logging import logging
log = logging.getLogger('AVP.Gui.PreviewWindow')
class PreviewWindow(QtWidgets.QLabel): class PreviewWindow(QtWidgets.QLabel):
''' '''
Paints the preview QLabel in MainWindow and maintains the aspect ratio Paints the preview QLabel in MainWindow and maintains the aspect ratio
when the window is resized. when the window is resized.
''' '''
log = logging.getLogger('AVP.PreviewWindow')
def __init__(self, parent, img): def __init__(self, parent, img):
super(PreviewWindow, self).__init__() super(PreviewWindow, self).__init__()
self.parent = parent self.parent = parent
@ -41,17 +41,15 @@ class PreviewWindow(QtWidgets.QLabel):
if i >= 0: if i >= 0:
component = self.parent.core.selectedComponents[i] component = self.parent.core.selectedComponents[i]
if not hasattr(component, 'previewClickEvent'): if not hasattr(component, 'previewClickEvent'):
self.log.info('Ignored click event')
return return
pos = (event.x(), event.y()) pos = (event.x(), event.y())
size = (self.width(), self.height()) size = (self.width(), self.height())
butt = event.button() butt = event.button()
self.log.info('Click event for #%s: %s button %s' % ( log.info('Click event for #%s: %s button %s' % (
i, pos, butt)) i, pos, butt))
component.previewClickEvent( component.previewClickEvent(
pos, size, butt pos, size, butt
) )
self.parent.core.updateComponent(i)
@QtCore.pyqtSlot(str) @QtCore.pyqtSlot(str)
def threadError(self, msg): def threadError(self, msg):
@ -60,3 +58,4 @@ class PreviewWindow(QtWidgets.QLabel):
icon='Critical', icon='Critical',
parent=self parent=self
) )
log.info('%', repr(self.parent))

View File

@ -6,7 +6,7 @@ import logging
from __init__ import wd from __init__ import wd
log = logging.getLogger('AVP.Entrypoint') log = logging.getLogger('AVP.Main')
def main(): def main():
@ -35,11 +35,9 @@ def main():
log.debug("Finished creating command object") log.debug("Finished creating command object")
elif mode == 'GUI': elif mode == 'GUI':
from mainwindow import MainWindow from gui.mainwindow import MainWindow
import atexit
import signal
window = uic.loadUi(os.path.join(wd, "mainwindow.ui")) window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui"))
# window.adjustSize() # window.adjustSize()
desc = QtWidgets.QDesktopWidget() desc = QtWidgets.QDesktopWidget()
dpi = desc.physicalDpiX() dpi = desc.physicalDpiX()
@ -56,9 +54,6 @@ def main():
log.debug("Finished creating main window") log.debug("Finished creating main window")
window.raise_() window.raise_()
signal.signal(signal.SIGINT, main.cleanUp)
atexit.register(main.cleanUp)
sys.exit(app.exec_()) sys.exit(app.exec_())
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -6,9 +6,59 @@ import string
import os import os
import sys import sys
import subprocess import subprocess
import logging
from copy import copy
from collections import OrderedDict from collections import OrderedDict
log = logging.getLogger('AVP.Toolkit.Common')
class blockSignals:
'''
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):
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):
log.verbose(
'Resetting blockSignals to %s', str(bool(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): def badName(name):
'''Returns whether a name contains non-alphanumeric chars''' '''Returns whether a name contains non-alphanumeric chars'''
return any([letter in string.punctuation for letter in name]) return any([letter in string.punctuation for letter in name])
@ -34,6 +84,7 @@ def appendUppercase(lst):
lst.append(form.upper()) lst.append(form.upper())
return lst return lst
def pipeWrapper(func): def pipeWrapper(func):
'''A decorator to insert proper kwargs into Popen objects.''' '''A decorator to insert proper kwargs into Popen objects.'''
def pipeWrapper(commandList, **kwargs): def pipeWrapper(commandList, **kwargs):
@ -107,12 +158,14 @@ def connectWidget(widget, func):
elif type(widget) == QtWidgets.QComboBox: elif type(widget) == QtWidgets.QComboBox:
widget.currentIndexChanged.connect(func) widget.currentIndexChanged.connect(func)
else: else:
log.warning('Failed to connect %s ', str(widget.__class__.__name__))
return False return False
return True return True
def setWidgetValue(widget, val): def setWidgetValue(widget, val):
'''Generic setValue method for use with any typical QtWidget''' '''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: if type(widget) == QtWidgets.QLineEdit:
widget.setText(val) widget.setText(val)
elif type(widget) == QtWidgets.QSpinBox \ elif type(widget) == QtWidgets.QSpinBox \
@ -123,6 +176,7 @@ def setWidgetValue(widget, val):
elif type(widget) == QtWidgets.QComboBox: elif type(widget) == QtWidgets.QComboBox:
widget.setCurrentIndex(val) widget.setCurrentIndex(val)
else: else:
log.warning('Failed to set %s ', str(widget.__class__.__name__))
return False return False
return True return True

View File

@ -91,16 +91,24 @@ class FfmpegVideo:
def fillBuffer(self): def fillBuffer(self):
from component import ComponentError from component import ComponentError
logFilename = os.path.join( if core.Core.logEnabled:
core.Core.logDir, 'render_%s.log' % str(self.component.compPos)) logFilename = os.path.join(
log.debug('Creating ffmpeg process (log at %s)' % logFilename) core.Core.logDir, 'render_%s.log' % str(self.component.compPos)
with open(logFilename, 'w') as logf: )
logf.write(" ".join(self.command) + '\n\n') log.debug('Creating ffmpeg process (log at %s)', logFilename)
with open(logFilename, 'a') as logf: with open(logFilename, 'w') as logf:
logf.write(" ".join(self.command) + '\n\n')
with open(logFilename, 'a') as logf:
self.pipe = openPipe(
self.command, stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE, stderr=logf, bufsize=10**8
)
else:
self.pipe = openPipe( self.pipe = openPipe(
self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, self.command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
stderr=logf, bufsize=10**8 stderr=subprocess.DEVNULL, bufsize=10**8
) )
while True: while True:
if self.parent.canceled: if self.parent.canceled:
break break
@ -157,7 +165,7 @@ def findFfmpeg():
['ffmpeg', '-version'], stderr=f ['ffmpeg', '-version'], stderr=f
) )
return "ffmpeg" return "ffmpeg"
except subprocess.CalledProcessError: except (subprocess.CalledProcessError, FileNotFoundError):
return "avconv" return "avconv"

View File

@ -21,7 +21,6 @@ class FramePainter(QtGui.QPainter):
Pillow image with finalize() Pillow image with finalize()
''' '''
def __init__(self, width, height): def __init__(self, width, height):
log.verbose('Creating new FramePainter')
image = BlankFrame(width, height) image = BlankFrame(width, height)
self.image = QtGui.QImage(ImageQt(image)) self.image = QtGui.QImage(ImageQt(image))
super().__init__(self.image) super().__init__(self.image)
@ -33,6 +32,7 @@ class FramePainter(QtGui.QPainter):
super().setPen(penStyle) super().setPen(penStyle)
def finalize(self): def finalize(self):
log.verbose("Finalizing FramePainter")
imBytes = self.image.bits().asstring(self.image.byteCount()) imBytes = self.image.bits().asstring(self.image.byteCount())
frame = Image.frombytes( frame = Image.frombytes(
'RGBA', (self.image.width(), self.image.height()), imBytes 'RGBA', (self.image.width(), self.image.height()), imBytes
@ -78,8 +78,6 @@ def defaultSize(framefunc):
def FloodFrame(width, height, RgbaTuple): def FloodFrame(width, height, RgbaTuple):
log.verbose('Creating new %s*%s %s flood frame' % (
width, height, RgbaTuple))
return Image.new("RGBA", (width, height), RgbaTuple) return Image.new("RGBA", (width, height), RgbaTuple)
@ -98,7 +96,7 @@ def Checkerboard(width, height):
log.debug('Creating new %s*%s checkerboard' % (width, height)) log.debug('Creating new %s*%s checkerboard' % (width, height))
image = FloodFrame(1920, 1080, (0, 0, 0, 0)) image = FloodFrame(1920, 1080, (0, 0, 0, 0))
image.paste(Image.open( image.paste(Image.open(
os.path.join(core.Core.wd, "background.png")), os.path.join(core.Core.wd, 'gui', "background.png")),
(0, 0) (0, 0)
) )
image = image.resize((width, height)) image = image.resize((width, height))

View File

@ -179,7 +179,7 @@ class Worker(QtCore.QObject):
for num, component in enumerate(reversed(self.components)) for num, component in enumerate(reversed(self.components))
]) ])
print('Loaded Components:', initText) print('Loaded Components:', initText)
log.info('Calling preFrameRender for %s' % initText) log.info('Calling preFrameRender for %s', initText)
self.staticComponents = {} self.staticComponents = {}
for compNo, comp in enumerate(reversed(self.components)): for compNo, comp in enumerate(reversed(self.components)):
try: try:
@ -221,12 +221,13 @@ class Worker(QtCore.QObject):
if self.canceled: if self.canceled:
if canceledByComponent: if canceledByComponent:
log.error('Export cancelled by component #%s (%s): %s' % ( log.error(
'Export cancelled by component #%s (%s): %s',
compNo, compNo,
comp.name, comp.name,
'No message.' if comp.error() is None else ( 'No message.' if comp.error() is None else (
comp.error() if type(comp.error()) is str comp.error() if type(comp.error()) is str
else comp.error()[0]) else comp.error()[0]
) )
) )
self.cancelExport() self.cancelExport()
@ -251,9 +252,14 @@ class Worker(QtCore.QObject):
print('############################') print('############################')
log.info('Opening pipe to ffmpeg') log.info('Opening pipe to ffmpeg')
log.info(cmd) log.info(cmd)
self.out_pipe = openPipe( try:
ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout self.out_pipe = openPipe(
) ffmpegCommand,
stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
)
except sp.CalledProcessError:
log.critical('Ffmpeg pipe couldn\'t be created!')
raise
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# START CREATING THE VIDEO # START CREATING THE VIDEO