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
__version__ = '2.0.0.rc4'
__version__ = '2.0.0rc5'
def package_files(directory):

View File

@ -8,6 +8,7 @@ import argparse
import os
import sys
import time
import signal
from core import Core
@ -19,6 +20,7 @@ class Command(QtCore.QObject):
def __init__(self):
QtCore.QObject.__init__(self)
self.core = Core()
Core.mode = 'commandline'
self.dataDir = self.core.dataDir
self.canceled = False
@ -90,6 +92,9 @@ class Command(QtCore.QObject):
for arg in args:
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:
errcode, data = self.core.parseAvFile(projPath)
for key, value in data['WindowFields']:
@ -123,6 +128,11 @@ class Command(QtCore.QObject):
self.worker.progressBarSetText.connect(self.progressBarSetText)
self.createVideo.emit()
def stopVideo(self, *args):
self.worker.error = True
self.worker.cancelExport()
self.worker.cancel()
@QtCore.pyqtSlot(str)
def progressBarSetText(self, value):
if 'Export ' in value:

View File

@ -9,10 +9,11 @@ import sys
import math
import time
import logging
from copy import copy
from toolkit.frame import BlankFrame
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(self, *args, **kwargs):
try:
log.verbose('### %s #%s renders%s frame %s###' % (
log.verbose(
'### %s #%s renders a preview frame ###',
self.__class__.name, str(self.compPos),
'' if args else ' a preview',
'' if not args else '%s ' % args[0],
))
)
return func(self, *args, **kwargs)
except Exception as e:
try:
@ -59,9 +59,8 @@ class ComponentMetaclass(type(QtCore.QObject)):
'''Intercepts the command() method to check for global args'''
def commandWrapper(self, arg):
if arg.startswith('preset='):
from presetmanager import getPresetDir
_, 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):
print('Couldn\'t locate preset "%s"' % preset)
quit(1)
@ -100,6 +99,92 @@ class ComponentMetaclass(type(QtCore.QObject)):
return func(self)
return errorWrapper
def loadPresetWrapper(func):
'''Wraps loadPreset to handle the self.openingPreset boolean'''
class openingPreset:
def __init__(self, comp):
self.comp = comp
def __enter__(self):
self.comp.openingPreset = True
def __exit__(self, *args):
self.comp.openingPreset = False
def presetWrapper(self, *args):
with openingPreset(self):
try:
return func(self, *args)
except Exception:
try:
raise ComponentError(self, 'preset loader')
except ComponentError:
return
return presetWrapper
def updateWrapper(func):
'''
Calls _preUpdate before every subclass update().
Afterwards, for non-user updates, calls _autoUpdate().
For undoable updates triggered by the user, calls _userUpdate()
'''
class wrap:
def __init__(self, comp, auto):
self.comp = comp
self.auto = auto
def __enter__(self):
self.comp._preUpdate()
def __exit__(self, *args):
if self.auto or self.comp.openingPreset \
or not hasattr(self.comp.parent, 'undoStack'):
log.verbose('Automatic update')
self.comp._autoUpdate()
else:
log.verbose('User update')
self.comp._userUpdate()
def updateWrapper(self, **kwargs):
auto = kwargs['auto'] if 'auto' in kwargs else False
with wrap(self, auto):
try:
return func(self)
except Exception:
try:
raise ComponentError(self, 'update method')
except ComponentError:
return
return updateWrapper
def widgetWrapper(func):
'''Connects all widgets to update method after the subclass's method'''
class wrap:
def __init__(self, comp):
self.comp = comp
def __enter__(self):
pass
def __exit__(self, *args):
for widgetList in self.comp._allWidgets.values():
for widget in widgetList:
log.verbose('Connecting %s', str(
widget.__class__.__name__))
connectWidget(widget, self.comp.update)
def widgetWrapper(self, *args, **kwargs):
auto = kwargs['auto'] if 'auto' in kwargs else False
with wrap(self):
try:
return func(self, *args, **kwargs)
except Exception:
try:
raise ComponentError(self, 'widget creation')
except ComponentError:
return
return widgetWrapper
def __new__(cls, name, parents, attrs):
if 'ui' not in attrs:
# Use module name as ui filename by default
@ -107,54 +192,55 @@ class ComponentMetaclass(type(QtCore.QObject)):
attrs['__module__'].split('.')[-1]
)[0]
# if parents[0] == QtCore.QObject: else:
decorate = (
'names', # Class methods
'error', 'audio', 'properties', # Properties
'preFrameRender', 'previewRender',
'frameRender', 'command',
'loadPreset', 'command',
'update', 'widget',
)
# Auto-decorate methods
for key in decorate:
if key not in attrs:
continue
if key in ('names'):
attrs[key] = classmethod(attrs[key])
if key in ('audio'):
elif key in ('audio'):
attrs[key] = property(attrs[key])
if key == 'command':
elif key == 'command':
attrs[key] = cls.commandWrapper(attrs[key])
if key in ('previewRender', 'frameRender'):
elif key == 'previewRender':
attrs[key] = cls.renderWrapper(attrs[key])
if key == 'preFrameRender':
elif key == 'preFrameRender':
attrs[key] = cls.initializationWrapper(attrs[key])
if key == 'properties':
elif key == 'properties':
attrs[key] = cls.propertiesWrapper(attrs[key])
if key == 'error':
elif key == 'error':
attrs[key] = cls.errorWrapper(attrs[key])
elif key == 'loadPreset':
attrs[key] = cls.loadPresetWrapper(attrs[key])
elif key == 'update':
attrs[key] = cls.updateWrapper(attrs[key])
elif key == 'widget' and parents[0] != QtCore.QObject:
attrs[key] = cls.widgetWrapper(attrs[key])
# Turn version string into a number
try:
if 'version' not in attrs:
log.error(
'No version attribute in %s. Defaulting to 1' %
'No version attribute in %s. Defaulting to 1',
attrs['name'])
attrs['version'] = 1
else:
attrs['version'] = int(attrs['version'].split('.')[0])
except ValueError:
log.critical('%s component has an invalid version string:\n%s' % (
attrs['name'], str(attrs['version'])))
log.critical(
'%s component has an invalid version string:\n%s',
attrs['name'], str(attrs['version'])
)
except KeyError:
log.critical('%s component has no version string.' % attrs['name'])
log.critical('%s component has no version string.', attrs['name'])
else:
return super().__new__(cls, name, parents, attrs)
quit(1)
@ -180,23 +266,29 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self.moduleIndex = moduleIndex
self.compPos = compPos
self.core = core
self.currentPreset = None
# STATUS VARIABLES
self.currentPreset = None
self._allWidgets = {}
self._trackedWidgets = {}
self._presetNames = {}
self._commandArgs = {}
self._colorWidgets = {}
self._colorFuncs = {}
self._relativeWidgets = {}
# pixel values stored as floats
# Pixel values stored as floats
self._relativeValues = {}
# maximum values of spinBoxes at 1080p (Core.resolutions[0])
# Maximum values of spinBoxes at 1080p (Core.resolutions[0])
self._relativeMaximums = {}
# LOCKING VARIABLES
self.openingPreset = False
self.mergeUndo = True
self._lockedProperties = None
self._lockedError = None
self._lockedSize = None
# If set to a dict, values are used as basis to update relative widgets
self.oldAttrs = None
# Stop lengthy processes in response to this variable
self.canceled = False
@ -204,12 +296,20 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
return self.__class__.name
def __repr__(self):
import pprint
try:
preset = self.savePreset()
except Exception as 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.settings = parent.settings
log.verbose(
'Creating UI for %s #%s\'s widget',
self.__class__.name, self.compPos
)
self.page = self.loadUi(self.__class__.ui)
# Connect widget signals
widgets = {
# Find all normal widgets which will be connected after subclass method
self._allWidgets = {
'lineEdit': self.page.findChildren(QtWidgets.QLineEdit),
'checkBox': self.page.findChildren(QtWidgets.QCheckBox),
'spinBox': self.page.findChildren(QtWidgets.QSpinBox),
'comboBox': self.page.findChildren(QtWidgets.QComboBox),
}
widgets['spinBox'].extend(
self._allWidgets['spinBox'].extend(
self.page.findChildren(QtWidgets.QDoubleSpinBox)
)
for widgetList in widgets.values():
for widget in widgetList:
connectWidget(widget, self.update)
def update(self):
'''
Reads all tracked widget values into instance attributes
and tells the MainWindow that the component was modified.
Call super() at the END if you need to subclass this.
Starting point for a component update. A subclass should override
this method, and the base class will then magically insert a call
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):
'''
@ -348,7 +423,14 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
for attr, widget in self._trackedWidgets.items():
key = attr if attr not in self._presetNames \
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:
widget.setText('%s,%s,%s' % val)
@ -403,6 +485,85 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
# "Private" Methods
# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~
def _preUpdate(self):
'''Happens before subclass update()'''
for attr in self._relativeWidgets:
self.updateRelativeWidget(attr)
def _userUpdate(self):
'''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):
'''
@ -412,6 +573,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
Optional args:
'presetNames': preset variable names to replace attr names
'commandArgs': arg keywords that differ from attr names
'colorWidgets': identify attr as RGB tuple & update button CSS
'relativeWidgets': change value proportionally to resolution
NOTE: Any kwarg key set to None will selectively disable tracking.
'''
@ -424,7 +587,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
'colorWidgets',
'relativeWidgets',
):
setattr(self, '_%s' % kwarg, kwargs[kwarg])
setattr(self, '_{}'.format(kwarg), kwargs[kwarg])
else:
raise ComponentError(
self, 'Nonsensical keywords to trackWidgets.')
@ -434,10 +597,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
if kwarg == 'colorWidgets':
def makeColorFunc(attr):
def pickColor_():
self.mergeUndo = False
self.pickColor(
self._trackedWidgets[attr],
self._colorWidgets[attr]
)
self.mergeUndo = True
return pickColor_
self._colorFuncs = {
attr: makeColorFunc(attr) for attr in kwargs[kwarg]
@ -455,6 +620,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
self._relativeMaximums[attr] = \
self._trackedWidgets[attr].maximum()
self.updateRelativeWidgetMaximum(attr)
setattr(
self, attr, getWidgetValue(self._trackedWidgets[attr])
)
self._preUpdate()
self._autoUpdate()
def pickColor(self, textWidget, button):
'''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(self, attr, *args, **kwargs):
hasVerticalWords = (
lambda attr:
'height' in attr.lower() or
'ypos' in attr.lower() or
attr == 'y'
)
if 'axis' not in kwargs:
axis = self.width
if 'height' in attr.lower() \
or 'ypos' in attr.lower() or attr == 'y':
if hasVerticalWords(attr):
axis = self.height
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 relativeWidgetAxis
@ -529,7 +710,18 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def pixelValForAttr(self, attr, val=None, **kwargs):
if val is None:
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
def floatValForAttr(self, attr, val=None, **kwargs):
@ -540,14 +732,28 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
def setRelativeWidget(self, attr, floatVal):
'''Set a relative widget using a float'''
pixelVal = self.pixelValForAttr(attr, floatVal)
self._trackedWidgets[attr].setValue(pixelVal)
with blockSignals(self._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):
try:
oldUserValue = getattr(self, attr)
except AttributeError:
oldUserValue = self._trackedWidgets[attr].value()
'''Called by _preUpdate() for each relativeWidget before each update'''
oldUserValue = self.getOldAttr(attr)
newUserValue = self._trackedWidgets[attr].value()
newRelativeVal = self.floatValForAttr(attr, newUserValue)
@ -557,13 +763,13 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
and oldRelativeVal != newRelativeVal:
# Float changed without pixel value changing, which
# means the pixel value needs to be updated
log.debug('Updating %s #%s\'s relative widget: %s' % (
self.name, self.compPos, attr))
self._trackedWidgets[attr].blockSignals(True)
self.updateRelativeWidgetMaximum(attr)
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
self._trackedWidgets[attr].setValue(pixelVal)
self._trackedWidgets[attr].blockSignals(False)
log.debug(
'Updating %s #%s\'s relative widget: %s',
self.__class__.name, self.compPos, attr)
with blockSignals(self._trackedWidgets[attr]):
self.updateRelativeWidgetMaximum(attr)
pixelVal = self.pixelValForAttr(attr, oldRelativeVal)
self._trackedWidgets[attr].setValue(pixelVal)
if attr not in self._relativeValues \
or oldUserValue != newUserValue:
@ -629,3 +835,90 @@ class ComponentError(RuntimeError):
super().__init__(string)
caller.lockError(string)
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
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
self.page.lineEdit_color2.setDisabled(True)
self.page.pushButton_color2.setDisabled(True)
@ -85,8 +82,6 @@ class Component(Component):
self.page.pushButton_color2.setEnabled(False)
self.page.fillWidget.setCurrentIndex(fillType)
super().update()
def previewRender(self):
return self.drawFrame(self.width, self.height)
@ -107,7 +102,7 @@ class Component(Component):
# Return a solid image at x, y
if self.fillType == 0:
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))
return frame

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ import toolkit
log = logging.getLogger('AVP.Core')
STDOUT_LOGLVL = logging.WARNING
FILE_LOGLVL = logging.DEBUG
FILE_LOGLVL = None
class Core:
@ -32,6 +32,11 @@ class Core:
self.savedPresets = {} # copies of presets to detect modification
self.openingProject = False
def __repr__(self):
return "\n=~=~=~=\n".join(
[repr(comp) for comp in self.selectedComponents]
)
def importComponents(self):
def findComponents():
for f in os.listdir(Core.componentsPath):
@ -64,34 +69,41 @@ class Core:
for i, component in enumerate(self.selectedComponents):
component.compPos = i
def insertComponent(self, compPos, moduleIndex, loader):
def insertComponent(self, compPos, component, loader):
'''
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):
compPos = len(self.selectedComponents)
if len(self.selectedComponents) > 50:
return None
log.debug('Inserting Component from module #%s' % moduleIndex)
component = self.modules[moduleIndex].Component(
moduleIndex, compPos, self
return -1
if type(component) is int:
# create component using module index in self.modules
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(
compPos,
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'):
loader.insertComponent(compPos)
self.componentListChanged()
self.updateComponent(compPos)
return compPos
def moveComponent(self, startI, endI):
@ -110,8 +122,10 @@ class Core:
self.componentListChanged()
def updateComponent(self, i):
log.debug('Updating %s #%s' % (self.selectedComponents[i], str(i)))
self.selectedComponents[i].update()
log.debug(
'Auto-updating %s #%s',
self.selectedComponents[i], str(i))
self.selectedComponents[i].update(auto=True)
def moduleIndexFor(self, compName):
try:
@ -130,18 +144,11 @@ class Core:
saveValueStore = self.getPreset(filepath)
if not saveValueStore:
return False
try:
comp = self.selectedComponents[compIndex]
comp.loadPreset(
saveValueStore,
presetName
)
except KeyError as e:
log.warning(
'%s #%s\'s preset is missing value: %s' % (
comp.name, str(compIndex), str(e)
)
)
comp = self.selectedComponents[compIndex]
comp.loadPreset(
saveValueStore,
presetName
)
self.savedPresets[presetName] = dict(saveValueStore)
return True
@ -156,6 +163,10 @@ class Core:
break
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):
''' loader is the object calling this method which must have
its own showMessage(**kwargs) method for displaying errors.
@ -171,13 +182,11 @@ class Core:
if hasattr(loader, 'window'):
for widget, value in data['WindowFields']:
widget = eval('loader.window.%s' % widget)
widget.blockSignals(True)
toolkit.setWidgetValue(widget, value)
widget.blockSignals(False)
with toolkit.blockSignals(widget):
toolkit.setWidgetValue(widget, value)
for key, value in data['Settings']:
Core.settings.setValue(key, value)
for tup in data['Components']:
name, vers, preset = tup
clearThis = False
@ -202,7 +211,7 @@ class Core:
self.moduleIndexFor(name),
loader
)
if i is None:
if i == -1:
loader.showMessage(msg="Too many components!")
break
@ -255,7 +264,7 @@ class Core:
Returns dictionary with section names as the keys, each one
contains a list of tuples: (compName, version, compPresetDict)
'''
log.debug('Parsing av file: %s' % filepath)
log.debug('Parsing av file: %s', filepath)
validSections = (
'Components',
'Settings',
@ -374,7 +383,7 @@ class Core:
def createProjectFile(self, filepath, window=None):
'''Create a project file (.avp) using the current program state'''
log.info('Creating %s' % filepath)
log.info('Creating %s', filepath)
settingsKeys = [
'componentDir',
'inputDir',
@ -448,26 +457,30 @@ class Core:
dataDir = QtCore.QStandardPaths.writableLocation(
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:
encoderOptions = json.load(json_file)
settings = {
'canceled': False,
'FFMPEG_BIN': findFfmpeg(),
'dataDir': dataDir,
'settings': QtCore.QSettings(
os.path.join(dataDir, 'settings.ini'),
QtCore.QSettings.IniFormat),
'logDir': os.path.join(dataDir, 'log'),
'presetDir': os.path.join(dataDir, 'presets'),
'componentsPath': os.path.join(wd, 'components'),
'junkStream': os.path.join(wd, 'gui', 'background.png'),
'encoderOptions': encoderOptions,
'resolutions': [
'1920x1080',
'1280x720',
'854x480',
],
'FFMPEG_BIN': findFfmpeg(),
'windowHasFocus': False,
'canceled': False,
'logDir': os.path.join(dataDir, 'log'),
'logEnabled': False,
}
settings['videoFormats'] = toolkit.appendUppercase([
@ -528,6 +541,7 @@ class Core:
"projectDir": os.path.join(cls.dataDir, 'projects'),
"pref_insertCompAtTop": True,
"pref_genericPreview": True,
"pref_undoLimit": 10,
}
for parm, value in cls.defaultSettings.items():
@ -540,47 +554,53 @@ class Core:
if not key.startswith('pref_'):
continue
val = cls.settings.value(key)
if val in ('true', 'false'):
cls.settings.setValue(key, True if val == 'true' else False)
try:
val = int(val)
except ValueError:
if val == 'true':
val = True
elif val == 'false':
val = False
cls.settings.setValue(key, val)
@staticmethod
def makeLogger():
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)
# 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
# send critical log messages to stdout
logStream = logging.StreamHandler()
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(
'<%(name)s> %(message)s'
'<%(name)s> %(levelname)s: %(message)s'
)
logFile.setFormatter(fileFormatter)
libLogFile.setFormatter(fileFormatter)
logStream.setFormatter(streamFormatter)
log = logging.getLogger('AVP')
log.addHandler(logFile)
log.addHandler(logStream)
libLog = logging.getLogger()
libLog.addHandler(libLogFile)
# lowest level must be explicitly set on the root Logger
libLog.setLevel(0)
if FILE_LOGLVL is not None:
# write log files as well!
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
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 os
import signal
import atexit
import filecmp
import time
import logging
from core import Core
import preview_thread
from preview_win import PreviewWindow
from presetmanager import PresetManager
from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput
import gui.preview_thread as preview_thread
from gui.preview_win import PreviewWindow
from gui.presetmanager import PresetManager
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):
@ -41,11 +45,11 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(self, window, project):
QtWidgets.QMainWindow.__init__(self)
self.window = window
self.core = Core()
log.debug(
'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
self.window = window
self.core = Core()
Core.mode = 'GUI'
# widgets of component settings
self.pages = []
self.lastAutosave = time.time()
@ -60,14 +64,24 @@ class MainWindow(QtWidgets.QMainWindow):
self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
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(
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
log.debug('Creating preview window')
self.previewWindow = PreviewWindow(self, os.path.join(
Core.wd, "background.png"))
Core.wd, 'gui', "background.png"))
window.verticalLayout_previewWrapper.addWidget(self.previewWindow)
log.debug('Starting preview thread')
@ -78,16 +92,58 @@ class MainWindow(QtWidgets.QMainWindow):
self.previewWorker.moveToThread(self.previewThread)
self.previewWorker.imageCreated.connect(self.showPreviewImage)
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.timeout.connect(self.processTask.emit)
self.timer.start(500)
self.timer.start(timeout)
# Begin decorating the window and connecting events
self.window.installEventFilter(self)
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':
log.debug(
'Darwin detected: showing progress label below progress bar')
@ -158,7 +214,7 @@ class MainWindow(QtWidgets.QMainWindow):
for i, comp in enumerate(self.core.modules):
action = self.compMenu.addAction(comp.Component.name)
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)
@ -299,6 +355,10 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog)
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
for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert):
QtWidgets.QShortcut(
@ -339,21 +399,41 @@ class MainWindow(QtWidgets.QMainWindow):
activated=lambda: self.moveComponent('bottom')
)
# Debug Hotkeys
QtWidgets.QShortcut(
"Ctrl+Alt+Shift+R", self.window, self.drawPreview
"Ctrl+Shift+F", self.window, self.showFfmpegCommand
)
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):
log.info('Ending the preview thread')
self.timer.stop()
self.previewThread.quit()
self.previewThread.wait()
def terminate(self, *args):
self.cleanUp()
sys.exit(0)
@disableWhenOpeningProject
def updateWindowTitle(self):
appName = 'Audio Visualizer'
@ -366,35 +446,43 @@ class MainWindow(QtWidgets.QMainWindow):
appName += '*'
except AttributeError:
pass
log.debug('Setting window title to %s' % appName)
log.verbose('Setting window title to %s' % appName)
self.window.setWindowTitle(appName)
@QtCore.pyqtSlot(int, dict)
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:
name = presetStore['preset']
if name is None or name not in self.core.savedPresets:
modified = False
else:
modified = (presetStore != self.core.savedPresets[name])
else:
modified = bool(presetStore)
modified = bool(presetStore)
if pos < 0:
pos = len(self.core.selectedComponents)-1
name = str(self.core.selectedComponents[pos])
name = self.core.selectedComponents[pos].name
title = str(name)
if self.core.selectedComponents[pos].currentPreset:
title += ' - %s' % self.core.selectedComponents[pos].currentPreset
if modified:
title += '*'
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
))
)
else:
log.debug('Setting %s #%s\'s title: %s' % (
log.debug(
'Setting %s #%s\'s title: %s',
name, pos, title
))
)
self.window.listWidget_componentList.item(pos).setText(title)
def updateCodecs(self):
@ -471,7 +559,7 @@ class MainWindow(QtWidgets.QMainWindow):
return True
except FileNotFoundError:
log.error(
'Project file couldn\'t be located:', self.currentProject)
'Project file couldn\'t be located: %s', self.currentProject)
return identical
return False
@ -568,6 +656,7 @@ class MainWindow(QtWidgets.QMainWindow):
detail=detail,
icon='Critical',
)
log.info('%s', repr(self))
def changeEncodingStatus(self, status):
self.encoding = status
@ -651,6 +740,14 @@ class MainWindow(QtWidgets.QMainWindow):
def showPreviewImage(self, 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):
from textwrap import wrap
from toolkit.ffmpeg import createFfmpegCommand
@ -664,7 +761,13 @@ class MainWindow(QtWidgets.QMainWindow):
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):
'''Triggered by Core to finish initializing a new component.'''
componentList = self.window.listWidget_componentList
stackedWidget = self.window.stackedWidget
@ -685,41 +788,38 @@ class MainWindow(QtWidgets.QMainWindow):
def removeComponent(self):
componentList = self.window.listWidget_componentList
selected = componentList.selectedItems()
if selected:
action = RemoveComponent(self, selected)
self.undoStack.push(action)
for selected in componentList.selectedItems():
index = componentList.row(selected)
self.window.stackedWidget.removeWidget(self.pages[index])
componentList.takeItem(index)
self.core.removeComponent(index)
self.pages.pop(index)
self.changeComponentWidget()
def _removeComponent(self, index):
stackedWidget = self.window.stackedWidget
componentList = self.window.listWidget_componentList
stackedWidget.removeWidget(self.pages[index])
componentList.takeItem(index)
self.core.removeComponent(index)
self.pages.pop(index)
self.changeComponentWidget()
self.drawPreview()
@disableWhenEncoding
def moveComponent(self, change):
'''Moves a component relatively from its current position'''
componentList = self.window.listWidget_componentList
tag = change
if change == 'top':
change = -componentList.currentRow()
elif change == 'bottom':
change = len(componentList)-componentList.currentRow()-1
stackedWidget = self.window.stackedWidget
else:
tag = 'down' if change == 1 else 'up'
row = componentList.currentRow()
newRow = row + change
if newRow > -1 and newRow < componentList.count():
self.core.moveComponent(row, newRow)
# 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)
action = MoveComponent(self, row, newRow, tag)
self.undoStack.push(action)
def getComponentListMousePos(self, position):
'''
@ -777,11 +877,11 @@ class MainWindow(QtWidgets.QMainWindow):
self.window.lineEdit_audioFile,
self.window.lineEdit_outputFile
):
field.blockSignals(True)
field.setText('')
field.blockSignals(False)
with blockSignals(field):
field.setText('')
self.progressBarUpdated(0)
self.progressBarSetText('')
self.undoStack.clear()
@disableWhenEncoding
def createNewProject(self, prompt=True):
@ -845,7 +945,7 @@ class MainWindow(QtWidgets.QMainWindow):
def openProject(self, filepath, prompt=True):
if not filepath or not os.path.exists(filepath) \
or not filepath.endswith('.avp'):
or not filepath.endswith('.avp'):
return
self.clear()
@ -928,19 +1028,10 @@ class MainWindow(QtWidgets.QMainWindow):
for i, comp in enumerate(self.core.modules):
menuItem = self.submenu.addAction(comp.Component.name)
menuItem.triggered.connect(
lambda _, item=i: self.core.insertComponent(
0 if insertCompAtTop else index, item, self
lambda _, item=i: self.addComponent(
0 if insertCompAtTop else index, item
)
)
self.menu.move(parentPosition + QPos)
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">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<widget class="QPushButton" name="pushButton_undo">
<property name="text">
<string>Undo</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">

View File

@ -5,9 +5,14 @@
from PyQt5 import QtCore, QtWidgets
import string
import os
import logging
from toolkit import badName
from core import Core
from gui.actions import *
log = logging.getLogger('AVP.Gui.PresetManager')
class PresetManager(QtWidgets.QDialog):
@ -130,8 +135,8 @@ class PresetManager(QtWidgets.QDialog):
def clearPreset(self, compI=None):
'''Functions on mainwindow level from the context menu'''
compI = self.parent.window.listWidget_componentList.currentRow()
self.core.clearPreset(compI)
self.parent.updateComponentTitle(compI, False)
action = ClearPreset(self.parent, compI)
self.parent.undoStack.push(action)
def openSavePresetDialog(self):
'''Functions on mainwindow level from the context menu'''
@ -196,12 +201,16 @@ class PresetManager(QtWidgets.QDialog):
def openPreset(self, presetName, compPos=None):
componentList = self.parent.window.listWidget_componentList
selectedComponents = self.core.selectedComponents
index = compPos if compPos is not None else componentList.currentRow()
if index == -1:
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
dirname = os.path.join(self.presetDir, componentName, str(version))
filepath = os.path.join(dirname, presetName)
@ -224,16 +233,10 @@ class PresetManager(QtWidgets.QDialog):
if not ch:
return
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):
filepath = os.path.join(self.presetDir, comp, str(vers), name)
os.remove(filepath)
action = DeletePreset(self, comp, vers, name)
self.parent.undoStack.push(action)
def warnMessage(self, window=None):
self.parent.showMessage(
@ -270,7 +273,6 @@ class PresetManager(QtWidgets.QDialog):
return index
def openRenamePresetDialog(self):
# TODO: maintain consistency by changing this to call createNewPreset()
presetList = self.window.listWidget_presets
index = self.getPresetRow()
if index == -1:
@ -293,22 +295,28 @@ class PresetManager(QtWidgets.QDialog):
path = os.path.join(
self.presetDir, comp, str(vers))
newPath = os.path.join(path, newName)
oldPath = os.path.join(path, oldName)
if self.presetExists(newPath):
return
if os.path.exists(newPath):
os.remove(newPath)
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()
action = RenamePreset(self, path, oldName, newName)
self.parent.undoStack.push(action)
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):
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
self.window, "Import Preset File",
@ -351,8 +359,3 @@ class PresetManager(QtWidgets.QDialog):
def clearPresetListSelection(self):
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
log = logging.getLogger("AVP.PreviewThread")
log = logging.getLogger("AVP.Gui.PreviewThread")
class Worker(QtCore.QObject):
@ -45,8 +45,6 @@ class Worker(QtCore.QObject):
@pyqtSlot()
def process(self):
width = int(self.settings.value('outputWidth'))
height = int(self.settings.value('outputHeight'))
try:
nextPreviewInformation = self.queue.get(block=False)
while self.queue.qsize() >= 2:
@ -54,12 +52,14 @@ class Worker(QtCore.QObject):
self.queue.get(block=False)
except Empty:
continue
width = int(self.settings.value('outputWidth'))
height = int(self.settings.value('outputHeight'))
if self.background.width != width \
or self.background.height != height:
self.background = Checkerboard(width, height)
frame = self.background.copy()
log.debug('Creating new preview frame')
log.info('Creating new preview frame')
components = nextPreviewInformation["components"]
for component in reversed(components):
try:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -179,7 +179,7 @@ class Worker(QtCore.QObject):
for num, component in enumerate(reversed(self.components))
])
print('Loaded Components:', initText)
log.info('Calling preFrameRender for %s' % initText)
log.info('Calling preFrameRender for %s', initText)
self.staticComponents = {}
for compNo, comp in enumerate(reversed(self.components)):
try:
@ -221,12 +221,13 @@ class Worker(QtCore.QObject):
if self.canceled:
if canceledByComponent:
log.error('Export cancelled by component #%s (%s): %s' % (
log.error(
'Export cancelled by component #%s (%s): %s',
compNo,
comp.name,
'No message.' if comp.error() is None else (
comp.error() if type(comp.error()) is str
else comp.error()[0])
else comp.error()[0]
)
)
self.cancelExport()
@ -251,9 +252,14 @@ class Worker(QtCore.QObject):
print('############################')
log.info('Opening pipe to ffmpeg')
log.info(cmd)
self.out_pipe = openPipe(
ffmpegCommand, stdin=sp.PIPE, stdout=sys.stdout, stderr=sys.stdout
)
try:
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