From f66eb99465c61232a7f649e66bee59504bb0e52c Mon Sep 17 00:00:00 2001 From: Brianna Rainey Date: Wed, 28 Jan 2026 17:49:58 -0500 Subject: v2.2.1 - fix #74, fix #92, add optional 64th bar to Classic Visualizer, improve Conway default (#93) * update gitignore ignore profiling and coverage data * F1 opens help window, create appName variable, move undostack class * fix kaleidoscope effect, increase default Y values by +4 the increased y values allow the cells to continue animating for more than 60 minutes instead of 30 (at default 60f/t) * update version number * add minimumWidth to undo history window * Classic Visualizer: option to include 64th bar * Waveform component: fix #74 - new animation speed option * move shared visualizer code into toolkit * Waveform component: compress audio by default * Waveform component: fix 100% animation speed * new components receive random color * update to Qt 6 * fix pushbutton stylesheet * fix #92: replace ok/cancel with save/discard/cancel * remove obsolete PaintColor subclass * mv common shadow code into addShadow func * add 3rd option of ok/cancel back to showMessage the 3 options are: - ok - ok/cancel - save/discard/cancel * Image component: add shadow option * small test of rgbFromString * fix color tuple string * test another way to get comp names from CLI * rename component tests, add some more * Image component: scale shadow based on resolution * catch AttributeError if previewRender returns None * Text component: fix blur radius only able to increase the relativeWidgets system causes QDoubleSpinbox to only allow increases, because it really only works with integeres, so I changed the blur radius into a normal QSpinBox. I noted where the problem exists within component.py for future reference. This commit also removes an unneeded VerticalLayout from the ui file * remove unnecessary QVBoxLayout * paste shadow at x,y instead of using offset method * fix tests due to shadow change * don't print warning in connectWidget due to QFontComboBox--- src/avp/gui/mainwindow.py | 80 ++++++++++++++++++++++++++----------------- src/avp/gui/presetmanager.py | 4 +-- src/avp/gui/preview_thread.py | 13 +++---- src/avp/gui/undostack.py | 16 +++++++++ 4 files changed, 73 insertions(+), 40 deletions(-) create mode 100644 src/avp/gui/undostack.py (limited to 'src/avp/gui') diff --git a/src/avp/gui/mainwindow.py b/src/avp/gui/mainwindow.py index e7a5fe3..5a051fd 100644 --- a/src/avp/gui/mainwindow.py +++ b/src/avp/gui/mainwindow.py @@ -7,7 +7,7 @@ projects and exporting the video at a later time. from PyQt6 import QtCore, QtWidgets, uic import PyQt6.QtWidgets as QtWidgets -from PyQt6.QtGui import QUndoStack, QShortcut +from PyQt6.QtGui import QShortcut from PIL import Image from queue import Queue import sys @@ -16,12 +16,16 @@ import signal import filecmp import time import logging +from textwrap import wrap -from ..core import Core +from ..__init__ import __version__ +from ..core import Core, appName +from .undostack import UndoStack from . import preview_thread from .preview_win import PreviewWindow from .presetmanager import PresetManager from .actions import * +from ..toolkit.ffmpeg import createFfmpegCommand from ..toolkit import ( disableWhenEncoding, disableWhenOpeningProject, @@ -30,25 +34,9 @@ from ..toolkit import ( ) -appName = "Audio Visualizer" log = logging.getLogger("AVP.Gui.MainWindow") -class MyQUndoStack(QUndoStack): - # FIXME move this class - @property - def encoding(self): - return self.parent().encoding - - @disableWhenEncoding - def undo(self, *args, **kwargs): - super().undo(*args, **kwargs) - - @disableWhenEncoding - def redo(self, *args, **kwargs): - super().redo(*args, **kwargs) - - class MainWindow(QtWidgets.QMainWindow): """ The MainWindow wraps many Core methods in order to update the GUI @@ -91,7 +79,7 @@ class MainWindow(QtWidgets.QMainWindow): self.settings = Core.settings # Create stack of undoable user actions - self.undoStack = MyQUndoStack(self) + self.undoStack = UndoStack(self) undoLimit = self.settings.value("pref_undoLimit") self.undoStack.setUndoLimit(undoLimit) @@ -102,6 +90,7 @@ class MainWindow(QtWidgets.QMainWindow): layout = QtWidgets.QVBoxLayout() layout.addWidget(undoView) self.undoDialog.setLayout(layout) + self.undoDialog.setMinimumWidth(int(self.width() / 2)) # Create Preset Manager self.presetManager = PresetManager(self) @@ -325,7 +314,11 @@ class MainWindow(QtWidgets.QMainWindow): self.drawPreview(True) log.info("Pillow version %s", Image.__version__) - log.info("PyQt version %s (Qt version %s)", QtCore.PYQT_VERSION_STR, QtCore.QT_VERSION_STR) + log.info( + "PyQt version %s (Qt version %s)", + QtCore.PYQT_VERSION_STR, + QtCore.QT_VERSION_STR, + ) # verify Ffmpeg version if not self.core.FFMPEG_BIN: @@ -408,6 +401,7 @@ class MainWindow(QtWidgets.QMainWindow): activated=lambda: self.moveComponent("bottom"), ) + QShortcut("F1", self, self.showHelpWindow) QShortcut("Ctrl+Shift+F", self, self.showFfmpegCommand) QShortcut("Ctrl+Shift+U", self, self.showUndoStack) @@ -422,6 +416,12 @@ class MainWindow(QtWidgets.QMainWindow): if not self.core.selectedComponents: self.core.insertComponent(0, 0, self) self.core.insertComponent(1, 1, self) + # set colors to white and black to match classic appearance of program + self.core.selectedComponents[0].page.lineEdit_visColor.setText( + "255,255,255" + ) + self.core.selectedComponents[1].page.lineEdit_color1.setText("0,0,0") + self.undoStack.clear() def __repr__(self): return ( @@ -762,10 +762,10 @@ class MainWindow(QtWidgets.QMainWindow): def showUndoStack(self): self.undoDialog.show() - def showFfmpegCommand(self): - from textwrap import wrap - from ..toolkit.ffmpeg import createFfmpegCommand + def showHelpWindow(self): + self.showMessage(msg=f"{appName} v{__version__}") + def showFfmpegCommand(self): command = createFfmpegCommand( self.lineEdit_audioFile.text(), self.lineEdit_outputFile.text(), @@ -899,7 +899,9 @@ class MainWindow(QtWidgets.QMainWindow): @disableWhenEncoding def createNewProject(self, prompt=True): if prompt: - self.openSaveChangesDialog("starting a new project") + ch = self.openSaveChangesDialog("starting a new project") + if ch is None: + return self.clear() self.currentProject = None @@ -919,18 +921,19 @@ class MainWindow(QtWidgets.QMainWindow): def openSaveChangesDialog(self, phrase): success = True + ch = True if self.autosaveExists(identical=False): ch = self.showMessage( msg="You have unsaved changes in project '%s'. " "Save before %s?" % (os.path.basename(self.currentProject)[:-4], phrase), - showCancel=True, + showDiscard=True, ) if ch: success = self.saveProjectChanges() - - if success and os.path.exists(self.autosavePath): + if ch is not None and success and os.path.exists(self.autosavePath): os.remove(self.autosavePath) + return success and ch def openSaveProjectDialog(self): filename, _ = QtWidgets.QFileDialog.getSaveFileName( @@ -967,10 +970,12 @@ class MainWindow(QtWidgets.QMainWindow): ): return - self.clear() # ask to save any changes that are about to get deleted if prompt: - self.openSaveChangesDialog("opening another project") + ch = self.openSaveChangesDialog("opening another project") + if ch is None: + return + self.clear() self.currentProject = filepath self.settings.setValue("currentProject", filepath) @@ -992,7 +997,13 @@ class MainWindow(QtWidgets.QMainWindow): else QtWidgets.QMessageBox.Icon.Information ) msg.setDetailedText(kwargs["detail"] if "detail" in kwargs else None) - if "showCancel" in kwargs and kwargs["showCancel"]: + if "showDiscard" in kwargs and kwargs["showDiscard"]: + msg.setStandardButtons( + QtWidgets.QMessageBox.StandardButton.Save + | QtWidgets.QMessageBox.StandardButton.Discard + | QtWidgets.QMessageBox.StandardButton.Cancel + ) + elif "showCancel" in kwargs and kwargs["showCancel"]: msg.setStandardButtons( QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel @@ -1000,9 +1011,14 @@ class MainWindow(QtWidgets.QMainWindow): else: msg.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok) ch = msg.exec() - if ch == 1024: + if ch == 1024 or ch == 2048: + # OK or Save return True - return False + elif ch > 8000000: + # Discard + return False + # Cancel + return None @disableWhenEncoding def componentContextMenu(self, QPos): diff --git a/src/avp/gui/presetmanager.py b/src/avp/gui/presetmanager.py index 980a969..ca0029d 100644 --- a/src/avp/gui/presetmanager.py +++ b/src/avp/gui/presetmanager.py @@ -9,7 +9,7 @@ import os import logging from ..toolkit import badName -from ..core import Core +from ..core import Core, appName from .actions import * @@ -137,7 +137,7 @@ class PresetManager(QtWidgets.QDialog): currentPreset = selectedComponents[index].currentPreset newName, OK = QtWidgets.QInputDialog.getText( self.parent, - "Audio Visualizer", + appName, "New Preset Name:", QtWidgets.QLineEdit.EchoMode.Normal, currentPreset, diff --git a/src/avp/gui/preview_thread.py b/src/avp/gui/preview_thread.py index 1d78516..a59652a 100644 --- a/src/avp/gui/preview_thread.py +++ b/src/avp/gui/preview_thread.py @@ -65,17 +65,18 @@ class Worker(QtCore.QObject): component.unlockSize() frame = Image.alpha_composite(frame, newFrame) - except ValueError as e: + except (AttributeError, ValueError) as e: errMsg = ( "Bad frame returned by %s's preview renderer. " - "%s. New frame size was %s*%s; should be %s*%s." + "%s. New frame %s." % ( str(component), str(e).capitalize(), - newFrame.width, - newFrame.height, - width, - height, + "is None" if newFrame is None else "size was %s*%s; should be %s*%s" % ( + newFrame.width, + newFrame.height, + width, + height), ) ) log.critical(errMsg) diff --git a/src/avp/gui/undostack.py b/src/avp/gui/undostack.py new file mode 100644 index 0000000..fd1a3e9 --- /dev/null +++ b/src/avp/gui/undostack.py @@ -0,0 +1,16 @@ +from PyQt6.QtGui import QUndoStack +from ..toolkit.common import disableWhenEncoding + + +class UndoStack(QUndoStack): + @property + def encoding(self): + return self.parent().encoding + + @disableWhenEncoding + def undo(self, *args, **kwargs): + super().undo(*args, **kwargs) + + @disableWhenEncoding + def redo(self, *args, **kwargs): + super().redo(*args, **kwargs) -- cgit v1.2.3