diff --git a/src/component.py b/src/component.py
index 0e5144c..dcba082 100644
--- a/src/component.py
+++ b/src/component.py
@@ -12,7 +12,7 @@ import logging
from toolkit.frame import BlankFrame
from toolkit import (
- getWidgetValue, setWidgetValue, connectWidget, rgbFromString
+ getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals
)
@@ -305,14 +305,46 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
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.
+ A component update triggered by the user changing a widget value
+ Call super() at the END when subclassing this.
'''
- for attr, widget in self._trackedWidgets.items():
+ oldWidgetVals = {
+ attr: 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()
+ }
+ if any([val != oldWidgetVals[attr]
+ for attr, val in newWidgetVals.items()
+ ]):
+ action = ComponentUpdate(self, oldWidgetVals, newWidgetVals)
+ self.parent.undoStack.push(action)
+
+ def _update(self):
+ '''An internal component update that is not undoable'''
+
+ 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 preset to
+ the values in the attrDict. Mutates certain widget values if needed
+ '''
+ for attr, val in attrDict.items():
if attr in self._colorWidgets:
# Color Widgets: text stored as tuple & update the button color
- rgbTuple = rgbFromString(widget.text())
+ if type(val) is tuple:
+ rgbTuple = val
+ else:
+ rgbTuple = rgbFromString(val)
btnStyle = (
"QPushButton { background-color : %s; outline: none; }"
% QColor(*rgbTuple).name())
@@ -322,12 +354,11 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
elif attr in self._relativeWidgets:
# Relative widgets: number scales to fit export resolution
self.updateRelativeWidget(attr)
- setattr(self, attr, self._trackedWidgets[attr].value())
+ setattr(self, attr, val)
else:
# Normal tracked widget
- setattr(self, attr, getWidgetValue(widget))
- self.sendUpdateSignal()
+ setattr(self, attr, val)
def sendUpdateSignal(self):
if not self.core.openingProject:
@@ -541,7 +572,6 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass):
pixelVal = self.pixelValForAttr(attr, floatVal)
self._trackedWidgets[attr].setValue(pixelVal)
-
def updateRelativeWidget(self, attr):
try:
oldUserValue = getattr(self, attr)
@@ -628,3 +658,30 @@ 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, newWidgetVals):
+ super().__init__(
+ 'Changed %s component #%s' % (
+ parent.name, parent.compPos
+ )
+ )
+ self.parent = parent
+ self.oldWidgetVals = oldWidgetVals
+ self.newWidgetVals = newWidgetVals
+
+ def redo(self):
+ self.parent.setAttrs(self.newWidgetVals)
+ self.parent.sendUpdateSignal()
+
+ def undo(self):
+ self.parent.setAttrs(self.oldWidgetVals)
+ with blockSignals(self.parent):
+ for attr, widget in self.parent._trackedWidgets.items():
+ val = self.oldWidgetVals[attr]
+ if attr in self.parent._colorWidgets:
+ val = '%s,%s,%s' % val
+ setWidgetValue(widget, val)
+ self.parent.sendUpdateSignal()
diff --git a/src/components/color.py b/src/components/color.py
index 5d1233e..d09cee8 100644
--- a/src/components/color.py
+++ b/src/components/color.py
@@ -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)
diff --git a/src/components/color.ui b/src/components/color.ui
index a9dacea..1865e60 100644
--- a/src/components/color.ui
+++ b/src/components/color.ui
@@ -73,6 +73,9 @@
0
+
+ 0,0,0
+
12
@@ -146,6 +149,9 @@
0
+
+ 133,133,133
+
12
diff --git a/src/components/text.py b/src/components/text.py
index 4d4f5d3..d3afd5c 100644
--- a/src/components/text.py
+++ b/src/components/text.py
@@ -13,8 +13,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 +23,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)
diff --git a/src/components/text.ui b/src/components/text.ui
index 13d3467..b62e0ed 100644
--- a/src/components/text.ui
+++ b/src/components/text.ui
@@ -427,6 +427,9 @@
Qt::NoFocus
+
+ 255,255,255
+
-
@@ -485,6 +488,9 @@
Qt::NoFocus
+
+ 0,0,0
+
-
diff --git a/src/core.py b/src/core.py
index 20b9c1d..cee0f56 100644
--- a/src/core.py
+++ b/src/core.py
@@ -94,12 +94,11 @@ class Core:
compPos,
component
)
- self.componentListChanged()
- if moduleIndex > -1:
- self.updateComponent(compPos)
-
if hasattr(loader, 'insertComponent'):
loader.insertComponent(compPos)
+
+ self.componentListChanged()
+ self.updateComponent(compPos)
return compPos
def moveComponent(self, startI, endI):
@@ -119,7 +118,7 @@ class Core:
def updateComponent(self, i):
log.debug('Updating %s #%s' % (self.selectedComponents[i], str(i)))
- self.selectedComponents[i].update()
+ self.selectedComponents[i]._update()
def moduleIndexFor(self, compName):
try:
@@ -540,6 +539,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():
@@ -552,8 +552,14 @@ 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():
diff --git a/src/background.png b/src/gui/background.png
similarity index 100%
rename from src/background.png
rename to src/gui/background.png
diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py
index 2edb750..47111a0 100644
--- a/src/gui/mainwindow.py
+++ b/src/gui/mainwindow.py
@@ -42,13 +42,22 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(self, window, project):
QtWidgets.QMainWindow.__init__(self)
+ log.debug(
+ 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
self.window = window
self.core = Core()
Core.mode = 'GUI'
- log.debug(
- 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId())))
+ # Find settings created by Core object
+ self.dataDir = Core.dataDir
+ self.presetDir = Core.presetDir
+ self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
+ self.settings = Core.settings
+
+ # Create stack of undoable user actions
self.undoStack = QtWidgets.QUndoStack(self)
+ undoLimit = self.settings.value("pref_undoLimit")
+ self.undoStack.setUndoLimit(undoLimit)
# widgets of component settings
self.pages = []
@@ -58,12 +67,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.autosaveCooldown = 0.2
self.encoding = False
- # Find settings created by Core object
- self.dataDir = Core.dataDir
- self.presetDir = Core.presetDir
- self.autosavePath = os.path.join(self.dataDir, 'autosave.avp')
- self.settings = Core.settings
-
self.presetManager = PresetManager(
uic.loadUi(
os.path.join(Core.wd, 'gui', 'presetmanager.ui')), self)
@@ -302,6 +305,7 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog)
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)
@@ -353,6 +357,9 @@ class MainWindow(QtWidgets.QMainWindow):
QtWidgets.QShortcut(
"Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand
)
+ QtWidgets.QShortcut(
+ "Ctrl+Alt+Shift+U", self.window, self.showUndoStack
+ )
@QtCore.pyqtSlot()
def cleanUp(self, *args):
@@ -658,6 +665,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
@@ -784,6 +799,7 @@ class MainWindow(QtWidgets.QMainWindow):
field.blockSignals(False)
self.progressBarUpdated(0)
self.progressBarSetText('')
+ self.undoStack.clear()
@disableWhenEncoding
def createNewProject(self, prompt=True):
@@ -847,7 +863,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()
diff --git a/src/toolkit/common.py b/src/toolkit/common.py
index eba57d9..51ad023 100644
--- a/src/toolkit/common.py
+++ b/src/toolkit/common.py
@@ -9,6 +9,18 @@ import subprocess
from collections import OrderedDict
+class blockSignals:
+ '''A context manager to temporarily block a Qt widget from updating'''
+ def __init__(self, widget):
+ self.widget = widget
+
+ def __enter__(self):
+ self.widget.blockSignals(True)
+
+ def __exit__(self, *args):
+ self.widget.blockSignals(False)
+
+
def badName(name):
'''Returns whether a name contains non-alphanumeric chars'''
return any([letter in string.punctuation for letter in name])
diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py
index ad8537c..2104978 100644
--- a/src/toolkit/frame.py
+++ b/src/toolkit/frame.py
@@ -98,7 +98,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))