From a327bec4e42cc572fb84e559025e888a4a20edd3 Mon Sep 17 00:00:00 2001 From: tassaron Date: Mon, 14 Aug 2017 16:39:53 -0400 Subject: organizing GUImode-specific code --- src/gui/__init__.py | 0 src/gui/mainwindow.py | 946 ++++++++++++++++++++++++++++++++++++++++++++++ src/gui/mainwindow.ui | 828 ++++++++++++++++++++++++++++++++++++++++ src/gui/presetmanager.py | 358 ++++++++++++++++++ src/gui/presetmanager.ui | 150 ++++++++ src/gui/preview_thread.py | 90 +++++ src/gui/preview_win.py | 62 +++ src/mainwindow.py | 946 ---------------------------------------------- src/mainwindow.ui | 828 ---------------------------------------- src/presetmanager.py | 358 ------------------ src/presetmanager.ui | 150 -------- src/preview_thread.py | 90 ----- src/preview_win.py | 62 --- 13 files changed, 2434 insertions(+), 2434 deletions(-) create mode 100644 src/gui/__init__.py create mode 100644 src/gui/mainwindow.py create mode 100644 src/gui/mainwindow.ui create mode 100644 src/gui/presetmanager.py create mode 100644 src/gui/presetmanager.ui create mode 100644 src/gui/preview_thread.py create mode 100644 src/gui/preview_win.py delete mode 100644 src/mainwindow.py delete mode 100644 src/mainwindow.ui delete mode 100644 src/presetmanager.py delete mode 100644 src/presetmanager.ui delete mode 100644 src/preview_thread.py delete mode 100644 src/preview_win.py (limited to 'src') diff --git a/src/gui/__init__.py b/src/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py new file mode 100644 index 0000000..af6e190 --- /dev/null +++ b/src/gui/mainwindow.py @@ -0,0 +1,946 @@ +''' + When using GUI mode, this module's object (the main window) takes + user input to construct a program state (stored in the Core object). + This shows a preview of the video being created and allows for saving + projects and exporting the video at a later time. +''' +from PyQt5 import QtCore, QtGui, uic, QtWidgets +from PyQt5.QtWidgets import QMenu, QShortcut +from PIL import Image +from queue import Queue +import sys +import os +import signal +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 + + +log = logging.getLogger('AVP.MainWindow') + + +class MainWindow(QtWidgets.QMainWindow): + ''' + The MainWindow wraps many Core methods in order to update the GUI + accordingly. E.g., instead of self.core.openProject(), it will use + self.openProject() and update the window titlebar within the wrapper. + + MainWindow manages the autosave feature, although Core has the + primary functions for opening and creating project files. + ''' + + createVideo = QtCore.pyqtSignal() + newTask = QtCore.pyqtSignal(list) # for the preview window + processTask = QtCore.pyqtSignal() + + 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()))) + + # widgets of component settings + self.pages = [] + self.lastAutosave = time.time() + # list of previous five autosave times, used to reduce update spam + self.autosaveTimes = [] + 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, '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")) + window.verticalLayout_previewWrapper.addWidget(self.previewWindow) + + log.debug('Starting preview thread') + self.previewQueue = Queue() + self.previewThread = QtCore.QThread(self) + self.previewWorker = preview_thread.Worker(self, self.previewQueue) + self.previewWorker.error.connect(self.previewWindow.threadError) + self.previewWorker.moveToThread(self.previewThread) + self.previewWorker.imageCreated.connect(self.showPreviewImage) + self.previewThread.start() + + log.debug('Starting preview timer') + self.timer = QtCore.QTimer(self) + self.timer.timeout.connect(self.processTask.emit) + self.timer.start(500) + + # Begin decorating the window and connecting events + self.window.installEventFilter(self) + componentList = self.window.listWidget_componentList + + if sys.platform == 'darwin': + log.debug( + 'Darwin detected: showing progress label below progress bar') + window.progressBar_createVideo.setTextVisible(False) + else: + window.progressLabel.setHidden(True) + + window.toolButton_selectAudioFile.clicked.connect( + self.openInputFileDialog) + + window.toolButton_selectOutputFile.clicked.connect( + self.openOutputFileDialog) + + def changedField(): + self.autosave() + self.updateWindowTitle() + + window.lineEdit_audioFile.textChanged.connect(changedField) + window.lineEdit_outputFile.textChanged.connect(changedField) + + window.progressBar_createVideo.setValue(0) + + window.pushButton_createVideo.clicked.connect( + self.createAudioVisualisation) + + window.pushButton_Cancel.clicked.connect(self.stopVideo) + + for i, container in enumerate(Core.encoderOptions['containers']): + window.comboBox_videoContainer.addItem(container['name']) + if container['name'] == self.settings.value('outputContainer'): + selectedContainer = i + + window.comboBox_videoContainer.setCurrentIndex(selectedContainer) + window.comboBox_videoContainer.currentIndexChanged.connect( + self.updateCodecs + ) + + self.updateCodecs() + + for i in range(window.comboBox_videoCodec.count()): + codec = window.comboBox_videoCodec.itemText(i) + if codec == self.settings.value('outputVideoCodec'): + window.comboBox_videoCodec.setCurrentIndex(i) + + for i in range(window.comboBox_audioCodec.count()): + codec = window.comboBox_audioCodec.itemText(i) + if codec == self.settings.value('outputAudioCodec'): + window.comboBox_audioCodec.setCurrentIndex(i) + + window.comboBox_videoCodec.currentIndexChanged.connect( + self.updateCodecSettings + ) + + window.comboBox_audioCodec.currentIndexChanged.connect( + self.updateCodecSettings + ) + + vBitrate = int(self.settings.value('outputVideoBitrate')) + aBitrate = int(self.settings.value('outputAudioBitrate')) + + window.spinBox_vBitrate.setValue(vBitrate) + window.spinBox_aBitrate.setValue(aBitrate) + window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings) + window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings) + + # Make component buttons + self.compMenu = QMenu() + 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) + ) + + self.window.pushButton_addComponent.setMenu(self.compMenu) + + componentList.dropEvent = self.dragComponent + componentList.itemSelectionChanged.connect( + self.changeComponentWidget + ) + componentList.itemSelectionChanged.connect( + self.presetManager.clearPresetListSelection + ) + self.window.pushButton_removeComponent.clicked.connect( + lambda: self.removeComponent() + ) + + componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + componentList.customContextMenuRequested.connect( + self.componentContextMenu + ) + + currentRes = str(self.settings.value('outputWidth'))+'x' + \ + str(self.settings.value('outputHeight')) + for i, res in enumerate(Core.resolutions): + window.comboBox_resolution.addItem(res) + if res == currentRes: + currentRes = i + window.comboBox_resolution.setCurrentIndex(currentRes) + window.comboBox_resolution.currentIndexChanged.connect( + self.updateResolution + ) + + self.window.pushButton_listMoveUp.clicked.connect( + lambda: self.moveComponent(-1) + ) + self.window.pushButton_listMoveDown.clicked.connect( + lambda: self.moveComponent(1) + ) + + # Configure the Projects Menu + self.projectMenu = QMenu() + self.window.menuButton_newProject = self.projectMenu.addAction( + "New Project" + ) + self.window.menuButton_newProject.triggered.connect( + lambda: self.createNewProject() + ) + self.window.menuButton_openProject = self.projectMenu.addAction( + "Open Project" + ) + self.window.menuButton_openProject.triggered.connect( + lambda: self.openOpenProjectDialog() + ) + + action = self.projectMenu.addAction("Save Project") + action.triggered.connect(self.saveCurrentProject) + + action = self.projectMenu.addAction("Save Project As") + action.triggered.connect(self.openSaveProjectDialog) + + self.window.pushButton_projects.setMenu(self.projectMenu) + + # Configure the Presets Button + self.window.pushButton_presets.clicked.connect( + self.openPresetManager + ) + + self.updateWindowTitle() + log.debug('Showing main window') + window.show() + + if project and project != self.autosavePath: + if not project.endswith('.avp'): + project += '.avp' + # open a project from the commandline + if not os.path.dirname(project): + project = os.path.join( + self.settings.value("projectDir"), project + ) + self.currentProject = project + self.settings.setValue("currentProject", project) + if os.path.exists(self.autosavePath): + os.remove(self.autosavePath) + else: + # open the last currentProject from settings + self.currentProject = self.settings.value("currentProject") + + # delete autosave if it's identical to this project + if self.autosaveExists(identical=True): + os.remove(self.autosavePath) + + if self.currentProject and os.path.exists(self.autosavePath): + ch = self.showMessage( + msg="Restore unsaved changes in project '%s'?" + % os.path.basename(self.currentProject)[:-4], + showCancel=True) + if ch: + self.saveProjectChanges() + else: + os.remove(self.autosavePath) + + self.openProject(self.currentProject, prompt=False) + self.drawPreview(True) + + # verify Pillow version + if not self.settings.value("pilMsgShown") \ + and 'post' not in Image.PILLOW_VERSION: + self.showMessage( + msg="You are using the standard version of the " + "Python imaging library (Pillow %s). Upgrade " + "to the Pillow-SIMD fork to enable hardware accelerations " + "and export videos faster." % Image.PILLOW_VERSION + ) + self.settings.setValue("pilMsgShown", True) + + # verify Ffmpeg version + if not self.settings.value("ffmpegMsgShown"): + try: + with open(os.devnull, "w") as f: + ffmpegVers = checkOutput( + ['ffmpeg', '-version'], stderr=f + ) + goodVersion = str(ffmpegVers).split()[2].startswith('3') + except Exception: + goodVersion = False + else: + goodVersion = True + + if not goodVersion: + self.showMessage( + msg="You're using an old version of Ffmpeg. " + "Some features may not work as expected." + ) + self.settings.setValue("ffmpegMsgShown", True) + + # Hotkeys for projects + QtWidgets.QShortcut("Ctrl+S", self.window, self.saveCurrentProject) + QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog) + QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog) + QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject) + + # Hotkeys for component list + for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert): + QtWidgets.QShortcut( + inskey, self.window, + activated=lambda: self.window.pushButton_addComponent.click() + ) + for delkey in ("Ctrl+R", QtCore.Qt.Key_Delete): + QtWidgets.QShortcut( + delkey, self.window.listWidget_componentList, + self.removeComponent + ) + QtWidgets.QShortcut( + "Ctrl+Space", self.window, + activated=lambda: self.window.listWidget_componentList.setFocus() + ) + QtWidgets.QShortcut( + "Ctrl+Shift+S", self.window, + self.presetManager.openSavePresetDialog + ) + QtWidgets.QShortcut( + "Ctrl+Shift+C", self.window, self.presetManager.clearPreset + ) + + QtWidgets.QShortcut( + "Ctrl+Up", self.window.listWidget_componentList, + activated=lambda: self.moveComponent(-1) + ) + QtWidgets.QShortcut( + "Ctrl+Down", self.window.listWidget_componentList, + activated=lambda: self.moveComponent(1) + ) + QtWidgets.QShortcut( + "Ctrl+Home", self.window.listWidget_componentList, + activated=lambda: self.moveComponent('top') + ) + QtWidgets.QShortcut( + "Ctrl+End", self.window.listWidget_componentList, + activated=lambda: self.moveComponent('bottom') + ) + + # Debug Hotkeys + QtWidgets.QShortcut( + "Ctrl+Alt+Shift+R", self.window, self.drawPreview + ) + QtWidgets.QShortcut( + "Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand + ) + + @QtCore.pyqtSlot() + def cleanUp(self, *args): + log.info('Ending the preview thread') + self.timer.stop() + self.previewThread.quit() + self.previewThread.wait() + + @disableWhenOpeningProject + def updateWindowTitle(self): + appName = 'Audio Visualizer' + try: + if self.currentProject: + appName += ' - %s' % \ + os.path.splitext( + os.path.basename(self.currentProject))[0] + if self.autosaveExists(identical=False): + appName += '*' + except AttributeError: + pass + log.debug('Setting window title to %s' % appName) + self.window.setWindowTitle(appName) + + @QtCore.pyqtSlot(int, dict) + def updateComponentTitle(self, pos, presetStore=False): + 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) + if pos < 0: + pos = len(self.core.selectedComponents)-1 + name = str(self.core.selectedComponents[pos]) + 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' % ( + name, pos, modified, title + )) + else: + log.debug('Setting %s #%s\'s title: %s' % ( + name, pos, title + )) + self.window.listWidget_componentList.item(pos).setText(title) + + def updateCodecs(self): + containerWidget = self.window.comboBox_videoContainer + vCodecWidget = self.window.comboBox_videoCodec + aCodecWidget = self.window.comboBox_audioCodec + index = containerWidget.currentIndex() + name = containerWidget.itemText(index) + self.settings.setValue('outputContainer', name) + + vCodecWidget.clear() + aCodecWidget.clear() + + for container in Core.encoderOptions['containers']: + if container['name'] == name: + for vCodec in container['video-codecs']: + vCodecWidget.addItem(vCodec) + for aCodec in container['audio-codecs']: + aCodecWidget.addItem(aCodec) + + def updateCodecSettings(self): + '''Updates settings.ini to match encoder option widgets''' + vCodecWidget = self.window.comboBox_videoCodec + vBitrateWidget = self.window.spinBox_vBitrate + aBitrateWidget = self.window.spinBox_aBitrate + aCodecWidget = self.window.comboBox_audioCodec + currentVideoCodec = vCodecWidget.currentIndex() + currentVideoCodec = vCodecWidget.itemText(currentVideoCodec) + currentVideoBitrate = vBitrateWidget.value() + currentAudioCodec = aCodecWidget.currentIndex() + currentAudioCodec = aCodecWidget.itemText(currentAudioCodec) + currentAudioBitrate = aBitrateWidget.value() + self.settings.setValue('outputVideoCodec', currentVideoCodec) + self.settings.setValue('outputAudioCodec', currentAudioCodec) + self.settings.setValue('outputVideoBitrate', currentVideoBitrate) + self.settings.setValue('outputAudioBitrate', currentAudioBitrate) + + @disableWhenOpeningProject + def autosave(self, force=False): + if not self.currentProject: + if os.path.exists(self.autosavePath): + os.remove(self.autosavePath) + elif force or time.time() - self.lastAutosave >= self.autosaveCooldown: + self.core.createProjectFile(self.autosavePath, self.window) + self.lastAutosave = time.time() + if len(self.autosaveTimes) >= 5: + # Do some math to reduce autosave spam. This gives a smooth + # curve up to 5 seconds cooldown and maintains that for 30 secs + # if a component is continuously updated + timeDiff = self.lastAutosave - self.autosaveTimes.pop() + if not force and timeDiff >= 1.0 \ + and timeDiff <= 10.0: + if self.autosaveCooldown / 4.0 < 0.5: + self.autosaveCooldown += 1.0 + self.autosaveCooldown = ( + 5.0 * (self.autosaveCooldown / 5.0) + ) + (self.autosaveCooldown / 5.0) * 2 + elif force or timeDiff >= self.autosaveCooldown * 5: + self.autosaveCooldown = 0.2 + self.autosaveTimes.insert(0, self.lastAutosave) + else: + log.debug('Autosave rejected by cooldown') + + def autosaveExists(self, identical=True): + '''Determines if creating the autosave should be blocked.''' + try: + if self.currentProject and os.path.exists(self.autosavePath) \ + and filecmp.cmp( + self.autosavePath, self.currentProject) == identical: + log.debug( + 'Autosave found %s to be identical' + % 'not' if not identical else '' + ) + return True + except FileNotFoundError: + log.error( + 'Project file couldn\'t be located:', self.currentProject) + return identical + return False + + def saveProjectChanges(self): + '''Overwrites project file with autosave file''' + try: + os.remove(self.currentProject) + os.rename(self.autosavePath, self.currentProject) + return True + except (FileNotFoundError, IsADirectoryError) as e: + self.showMessage( + msg='Project file couldn\'t be saved.', + detail=str(e)) + return False + + def openInputFileDialog(self): + inputDir = self.settings.value("inputDir", os.path.expanduser("~")) + + fileName, _ = QtWidgets.QFileDialog.getOpenFileName( + self.window, "Open Audio File", + inputDir, "Audio Files (%s)" % " ".join(Core.audioFormats)) + + if fileName: + self.settings.setValue("inputDir", os.path.dirname(fileName)) + self.window.lineEdit_audioFile.setText(fileName) + + def openOutputFileDialog(self): + outputDir = self.settings.value("outputDir", os.path.expanduser("~")) + + fileName, _ = QtWidgets.QFileDialog.getSaveFileName( + self.window, "Set Output Video File", + outputDir, + "Video Files (%s);; All Files (*)" % " ".join( + Core.videoFormats)) + + if fileName: + self.settings.setValue("outputDir", os.path.dirname(fileName)) + self.window.lineEdit_outputFile.setText(fileName) + + def stopVideo(self): + log.info('Export cancelled') + self.videoWorker.cancel() + self.canceled = True + + def createAudioVisualisation(self): + # create output video if mandatory settings are filled in + audioFile = self.window.lineEdit_audioFile.text() + outputPath = self.window.lineEdit_outputFile.text() + + if audioFile and outputPath and self.core.selectedComponents: + if not os.path.dirname(outputPath): + outputPath = os.path.join( + os.path.expanduser("~"), outputPath) + if outputPath and os.path.isdir(outputPath): + self.showMessage( + msg='Chosen filename matches a directory, which ' + 'cannot be overwritten. Please choose a different ' + 'filename or move the directory.', + icon='Warning', + ) + return + else: + if not audioFile or not outputPath: + self.showMessage( + msg="You must select an audio file and output filename." + ) + elif not self.core.selectedComponents: + self.showMessage( + msg="Not enough components." + ) + return + + self.canceled = False + self.progressBarUpdated(-1) + self.videoWorker = self.core.newVideoWorker( + self, audioFile, outputPath + ) + self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated) + self.videoWorker.progressBarSetText.connect( + self.progressBarSetText) + self.videoWorker.imageCreated.connect(self.showPreviewImage) + self.videoWorker.encoding.connect(self.changeEncodingStatus) + self.createVideo.emit() + + @QtCore.pyqtSlot(str, str) + def videoThreadError(self, msg, detail): + try: + self.stopVideo() + except AttributeError as e: + if 'videoWorker' not in str(e): + raise + self.showMessage( + msg=msg, + detail=detail, + icon='Critical', + ) + + def changeEncodingStatus(self, status): + self.encoding = status + if status: + self.window.pushButton_createVideo.setEnabled(False) + self.window.pushButton_Cancel.setEnabled(True) + self.window.comboBox_resolution.setEnabled(False) + self.window.stackedWidget.setEnabled(False) + self.window.tab_encoderSettings.setEnabled(False) + self.window.label_audioFile.setEnabled(False) + self.window.toolButton_selectAudioFile.setEnabled(False) + self.window.label_outputFile.setEnabled(False) + self.window.toolButton_selectOutputFile.setEnabled(False) + self.window.lineEdit_audioFile.setEnabled(False) + self.window.lineEdit_outputFile.setEnabled(False) + self.window.pushButton_addComponent.setEnabled(False) + self.window.pushButton_removeComponent.setEnabled(False) + self.window.pushButton_listMoveDown.setEnabled(False) + self.window.pushButton_listMoveUp.setEnabled(False) + self.window.menuButton_newProject.setEnabled(False) + self.window.menuButton_openProject.setEnabled(False) + if sys.platform == 'darwin': + self.window.progressLabel.setHidden(False) + else: + self.window.listWidget_componentList.setEnabled(False) + else: + self.window.pushButton_createVideo.setEnabled(True) + self.window.pushButton_Cancel.setEnabled(False) + self.window.comboBox_resolution.setEnabled(True) + self.window.stackedWidget.setEnabled(True) + self.window.tab_encoderSettings.setEnabled(True) + self.window.label_audioFile.setEnabled(True) + self.window.toolButton_selectAudioFile.setEnabled(True) + self.window.lineEdit_audioFile.setEnabled(True) + self.window.label_outputFile.setEnabled(True) + self.window.toolButton_selectOutputFile.setEnabled(True) + self.window.lineEdit_outputFile.setEnabled(True) + self.window.pushButton_addComponent.setEnabled(True) + self.window.pushButton_removeComponent.setEnabled(True) + self.window.pushButton_listMoveDown.setEnabled(True) + self.window.pushButton_listMoveUp.setEnabled(True) + self.window.menuButton_newProject.setEnabled(True) + self.window.menuButton_openProject.setEnabled(True) + self.window.listWidget_componentList.setEnabled(True) + self.window.progressLabel.setHidden(True) + self.drawPreview(True) + + @QtCore.pyqtSlot(int) + def progressBarUpdated(self, value): + self.window.progressBar_createVideo.setValue(value) + + @QtCore.pyqtSlot(str) + def progressBarSetText(self, value): + if sys.platform == 'darwin': + self.window.progressLabel.setText(value) + else: + self.window.progressBar_createVideo.setFormat(value) + + def updateResolution(self): + resIndex = int(self.window.comboBox_resolution.currentIndex()) + res = Core.resolutions[resIndex].split('x') + changed = res[0] != self.settings.value("outputWidth") + self.settings.setValue('outputWidth', res[0]) + self.settings.setValue('outputHeight', res[1]) + if changed: + for i in range(len(self.core.selectedComponents)): + self.core.updateComponent(i) + + def drawPreview(self, force=False, **kwargs): + '''Use autosave keyword arg to force saving or not saving if needed''' + self.newTask.emit(self.core.selectedComponents) + # self.processTask.emit() + if force or 'autosave' in kwargs: + if force or kwargs['autosave']: + self.autosave(True) + else: + self.autosave() + self.updateWindowTitle() + + @QtCore.pyqtSlot(QtGui.QImage) + def showPreviewImage(self, image): + self.previewWindow.changePixmap(image) + + def showFfmpegCommand(self): + from textwrap import wrap + from toolkit.ffmpeg import createFfmpegCommand + command = createFfmpegCommand( + self.window.lineEdit_audioFile.text(), + self.window.lineEdit_outputFile.text(), + self.core.selectedComponents + ) + lines = wrap(" ".join(command), 49) + self.showMessage( + msg="Current FFmpeg command:\n\n %s" % " ".join(lines) + ) + + def insertComponent(self, index): + componentList = self.window.listWidget_componentList + stackedWidget = self.window.stackedWidget + + componentList.insertItem( + index, + self.core.selectedComponents[index].name) + componentList.setCurrentRow(index) + + # connect to signal that adds an asterisk when modified + self.core.selectedComponents[index].modified.connect( + self.updateComponentTitle) + + self.pages.insert(index, self.core.selectedComponents[index].page) + stackedWidget.insertWidget(index, self.pages[index]) + stackedWidget.setCurrentIndex(index) + + return index + + def removeComponent(self): + componentList = self.window.listWidget_componentList + + 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() + self.drawPreview() + + @disableWhenEncoding + def moveComponent(self, change): + '''Moves a component relatively from its current position''' + componentList = self.window.listWidget_componentList + if change == 'top': + change = -componentList.currentRow() + elif change == 'bottom': + change = len(componentList)-componentList.currentRow()-1 + stackedWidget = self.window.stackedWidget + + 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) + + def getComponentListMousePos(self, position): + ''' + Given a QPos, returns the component index under the mouse cursor + or -1 if no component is there. + ''' + componentList = self.window.listWidget_componentList + + modelIndexes = [ + componentList.model().index(i) + for i in range(componentList.count()) + ] + rects = [ + componentList.visualRect(modelIndex) + for modelIndex in modelIndexes + ] + mousePos = [rect.contains(position) for rect in rects] + if not any(mousePos): + # Not clicking a component + mousePos = -1 + else: + mousePos = mousePos.index(True) + log.debug('Click component list row %s' % mousePos) + return mousePos + + @disableWhenEncoding + def dragComponent(self, event): + '''Used as Qt drop event for the component listwidget''' + componentList = self.window.listWidget_componentList + mousePos = self.getComponentListMousePos(event.pos()) + if mousePos > -1: + change = (componentList.currentRow() - mousePos) * -1 + else: + change = (componentList.count() - componentList.currentRow() - 1) + self.moveComponent(change) + + def changeComponentWidget(self): + selected = self.window.listWidget_componentList.selectedItems() + if selected: + index = self.window.listWidget_componentList.row(selected[0]) + self.window.stackedWidget.setCurrentIndex(index) + + def openPresetManager(self): + '''Preset manager for importing, exporting, renaming, deleting''' + self.presetManager.show() + + def clear(self): + '''Get a blank slate''' + self.core.clearComponents() + self.window.listWidget_componentList.clear() + for widget in self.pages: + self.window.stackedWidget.removeWidget(widget) + self.pages = [] + for field in ( + self.window.lineEdit_audioFile, + self.window.lineEdit_outputFile + ): + field.blockSignals(True) + field.setText('') + field.blockSignals(False) + self.progressBarUpdated(0) + self.progressBarSetText('') + + @disableWhenEncoding + def createNewProject(self, prompt=True): + if prompt: + self.openSaveChangesDialog('starting a new project') + + self.clear() + self.currentProject = None + self.settings.setValue("currentProject", None) + self.drawPreview(True) + + def saveCurrentProject(self): + if self.currentProject: + self.core.createProjectFile(self.currentProject, self.window) + try: + os.remove(self.autosavePath) + except FileNotFoundError: + pass + self.updateWindowTitle() + else: + self.openSaveProjectDialog() + + def openSaveChangesDialog(self, phrase): + success = 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) + if ch: + success = self.saveProjectChanges() + + if success and os.path.exists(self.autosavePath): + os.remove(self.autosavePath) + + def openSaveProjectDialog(self): + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + self.window, "Create Project File", + self.settings.value("projectDir"), + "Project Files (*.avp)") + if not filename: + return + if not filename.endswith(".avp"): + filename += '.avp' + self.settings.setValue("projectDir", os.path.dirname(filename)) + self.settings.setValue("currentProject", filename) + self.currentProject = filename + self.core.createProjectFile(filename, self.window) + self.updateWindowTitle() + + @disableWhenEncoding + def openOpenProjectDialog(self): + filename, _ = QtWidgets.QFileDialog.getOpenFileName( + self.window, "Open Project File", + self.settings.value("projectDir"), + "Project Files (*.avp)") + self.openProject(filename) + + def openProject(self, filepath, prompt=True): + if not filepath or not os.path.exists(filepath) \ + or not filepath.endswith('.avp'): + return + + self.clear() + # ask to save any changes that are about to get deleted + if prompt: + self.openSaveChangesDialog('opening another project') + + self.currentProject = filepath + self.settings.setValue("currentProject", filepath) + self.settings.setValue("projectDir", os.path.dirname(filepath)) + # actually load the project using core method + self.core.openProject(self, filepath) + self.drawPreview(autosave=False) + self.updateWindowTitle() + + def showMessage(self, **kwargs): + parent = kwargs['parent'] if 'parent' in kwargs else self.window + msg = QtWidgets.QMessageBox(parent) + msg.setModal(True) + msg.setText(kwargs['msg']) + msg.setIcon( + eval('QtWidgets.QMessageBox.%s' % kwargs['icon']) + if 'icon' in kwargs else QtWidgets.QMessageBox.Information + ) + msg.setDetailedText(kwargs['detail'] if 'detail' in kwargs else None) + if 'showCancel'in kwargs and kwargs['showCancel']: + msg.setStandardButtons( + QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) + else: + msg.setStandardButtons(QtWidgets.QMessageBox.Ok) + ch = msg.exec_() + if ch == 1024: + return True + return False + + @disableWhenEncoding + def componentContextMenu(self, QPos): + '''Appears when right-clicking the component list''' + componentList = self.window.listWidget_componentList + self.menu = QMenu() + parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0)) + + index = self.getComponentListMousePos(QPos) + if index > -1: + # Show preset menu if clicking a component + self.presetManager.findPresets() + menuItem = self.menu.addAction("Save Preset") + menuItem.triggered.connect( + self.presetManager.openSavePresetDialog + ) + + # submenu for opening presets + try: + presets = self.presetManager.presets[ + str(self.core.selectedComponents[index]) + ] + self.presetSubmenu = QMenu("Open Preset") + self.menu.addMenu(self.presetSubmenu) + + for version, presetName in presets: + menuItem = self.presetSubmenu.addAction(presetName) + menuItem.triggered.connect( + lambda _, presetName=presetName: + self.presetManager.openPreset(presetName) + ) + except KeyError: + pass + + if self.core.selectedComponents[index].currentPreset: + menuItem = self.menu.addAction("Clear Preset") + menuItem.triggered.connect( + self.presetManager.clearPreset + ) + self.menu.addSeparator() + + # "Add Component" submenu + self.submenu = QMenu("Add") + self.menu.addMenu(self.submenu) + insertCompAtTop = self.settings.value("pref_insertCompAtTop") + 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 + ) + ) + + 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 diff --git a/src/gui/mainwindow.ui b/src/gui/mainwindow.ui new file mode 100644 index 0000000..b43d375 --- /dev/null +++ b/src/gui/mainwindow.ui @@ -0,0 +1,828 @@ + + + MainWindow + + + + 0 + 0 + 1008 + 575 + + + + + 0 + 0 + + + + + 0 + 0 + + + + Qt::StrongFocus + + + MainWindow + + + + + 0 + 0 + + + + false + + + + 9 + + + 0 + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 0 + 360 + + + + + + + + QLayout::SetDefaultConstraint + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 420 + 0 + + + + + + + + + + QLayout::SetMinimumSize + + + 3 + + + + + QLayout::SetMinimumSize + + + 3 + + + + + QLayout::SetMinimumSize + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 140 + 20 + + + + + + + + Projects + + + + + + + Presets + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 20 + 2 + + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + true + + + QFrame::StyledPanel + + + QFrame::Sunken + + + 1 + + + true + + + true + + + false + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + + + + + + + Add + + + + + + + Remove + + + + + + + Up + + + + + + + Down + + + + + + + + + + + 4 + + + 2 + + + + + + + + + + + QLayout::SetFixedSize + + + 4 + + + 0 + + + + + + 0 + 0 + + + + + 500 + 0 + + + + + 16777215 + 180 + + + + QTabWidget::North + + + QTabWidget::Rounded + + + 0 + + + + Export Video + + + + 10 + + + + + 0 + + + + + + 0 + 0 + + + + + 85 + 0 + + + + + 80 + 16777215 + + + + + 80 + 0 + + + + Audio File + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + + 0 + 0 + + + + + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + ... + + + + + + + + + + + + + + 0 + 0 + + + + + 85 + 0 + + + + + 0 + 0 + + + + Output File + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + + + + + + 0 + 28 + + + + + 16777215 + 28 + + + + ... + + + + + + + + + + + 0 + + + + + + 0 + 0 + + + + 24 + + + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 10 + 20 + + + + + + + + Create Video + + + + + + + false + + + Cancel + + + + + + + + + + + + true + + + Qt::AlignCenter + + + -1 + + + + + + + progressLabel + + + + Encoder Settings + + + + 10 + + + + + + + + 0 + 0 + + + + + 85 + 0 + + + + Container + + + + + + + + 150 + 0 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 5 + 5 + + + + + + + + + 0 + 0 + + + + Resolution + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + + + + + + + + + 0 + 0 + + + + + 85 + 0 + + + + Video Codec + + + + + + + + 150 + 0 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 5 + + + + + + + + + 0 + 0 + + + + Video Bitrate (Kbps) + + + + + + + 99999 + + + + + + + + + + + + 0 + 0 + + + + + 85 + 0 + + + + Audio Codec + + + + + + + + 150 + 0 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 10 + + + + + + + + + 0 + 0 + + + + Audio Bitrate (Kbps) + + + + + + + 9999 + + + + + + + + + + + + + QLayout::SetDefaultConstraint + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 500 + 0 + + + + + + + + + 0 + 0 + + + + + 0 + 180 + + + + + 16777215 + 180 + + + + -1 + + + + + + + + + + + + + diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py new file mode 100644 index 0000000..b1eeb34 --- /dev/null +++ b/src/gui/presetmanager.py @@ -0,0 +1,358 @@ +''' + Preset manager object handles all interactions with presets, including + the context menu accessed from MainWindow. +''' +from PyQt5 import QtCore, QtWidgets +import string +import os + +from toolkit import badName +from core import Core + + +class PresetManager(QtWidgets.QDialog): + def __init__(self, window, parent): + super().__init__(parent.window) + self.parent = parent + self.core = parent.core + self.settings = parent.settings + self.presetDir = parent.presetDir + if not self.settings.value('presetDir'): + self.settings.setValue( + "presetDir", + os.path.join(parent.dataDir, 'projects')) + + self.findPresets() + + # window + self.lastFilter = '*' + self.presetRows = [] # list of (comp, vers, name) tuples + self.window = window + self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + + # connect button signals + self.window.pushButton_delete.clicked.connect( + self.openDeletePresetDialog + ) + self.window.pushButton_rename.clicked.connect( + self.openRenamePresetDialog + ) + self.window.pushButton_import.clicked.connect( + self.openImportDialog + ) + self.window.pushButton_export.clicked.connect( + self.openExportDialog + ) + self.window.pushButton_close.clicked.connect( + self.window.close + ) + + # create filter box and preset list + self.drawFilterList() + self.window.comboBox_filter.currentIndexChanged.connect( + lambda: self.drawPresetList( + self.window.comboBox_filter.currentText(), + self.window.lineEdit_search.text() + ) + ) + + # make auto-completion for search bar + self.autocomplete = QtCore.QStringListModel() + completer = QtWidgets.QCompleter() + completer.setModel(self.autocomplete) + self.window.lineEdit_search.setCompleter(completer) + self.window.lineEdit_search.textChanged.connect( + lambda: self.drawPresetList( + self.window.comboBox_filter.currentText(), + self.window.lineEdit_search.text() + ) + ) + self.drawPresetList('*') + + def show(self): + '''Open a new preset manager window from the mainwindow''' + self.findPresets() + self.drawFilterList() + self.drawPresetList('*') + self.window.show() + + def findPresets(self): + parseList = [] + for dirpath, dirnames, filenames in os.walk(self.presetDir): + # anything without a subdirectory must be a preset folder + if dirnames: + continue + for preset in filenames: + compName = os.path.basename(os.path.dirname(dirpath)) + if compName not in self.core.compNames: + continue + compVers = os.path.basename(dirpath) + try: + parseList.append((compName, int(compVers), preset)) + except ValueError: + continue + self.presets = { + compName: [ + (vers, preset) + for name, vers, preset in parseList + if name == compName + ] + for compName, _, __ in parseList + } + + def drawPresetList(self, compFilter=None, presetFilter=''): + self.window.listWidget_presets.clear() + if compFilter: + self.lastFilter = str(compFilter) + else: + compFilter = str(self.lastFilter) + self.presetRows = [] + presetNames = [] + for component, presets in self.presets.items(): + if compFilter != '*' and component != compFilter: + continue + for vers, preset in presets: + if not presetFilter or presetFilter in preset: + self.window.listWidget_presets.addItem( + '%s: %s' % (component, preset) + ) + self.presetRows.append((component, vers, preset)) + if preset not in presetNames: + presetNames.append(preset) + self.autocomplete.setStringList(presetNames) + + def drawFilterList(self): + self.window.comboBox_filter.clear() + self.window.comboBox_filter.addItem('*') + for component in self.presets: + self.window.comboBox_filter.addItem(component) + + 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) + + def openSavePresetDialog(self): + '''Functions on mainwindow level from the context menu''' + window = self.parent.window + selectedComponents = self.core.selectedComponents + componentList = self.parent.window.listWidget_componentList + + if componentList.currentRow() == -1: + return + while True: + index = componentList.currentRow() + currentPreset = selectedComponents[index].currentPreset + newName, OK = QtWidgets.QInputDialog.getText( + self.parent.window, + 'Audio Visualizer', + 'New Preset Name:', + QtWidgets.QLineEdit.Normal, + currentPreset + ) + if OK: + if badName(newName): + self.warnMessage(self.parent.window) + continue + if newName: + if index != -1: + selectedComponents[index].currentPreset = newName + saveValueStore = \ + selectedComponents[index].savePreset() + saveValueStore['preset'] = newName + componentName = str(selectedComponents[index]).strip() + vers = selectedComponents[index].version + self.createNewPreset( + componentName, vers, newName, + saveValueStore, window=self.parent.window) + self.findPresets() + self.drawPresetList() + self.openPreset(newName, index) + break + + def createNewPreset( + self, compName, vers, filename, saveValueStore, **kwargs): + path = os.path.join(self.presetDir, compName, str(vers), filename) + if self.presetExists(path, **kwargs): + return + self.core.createPresetFile(compName, vers, filename, saveValueStore) + + def presetExists(self, path, **kwargs): + if os.path.exists(path): + window = self.window \ + if 'window' not in kwargs else kwargs['window'] + ch = self.parent.showMessage( + msg="%s already exists! Overwrite it?" % + os.path.basename(path), + showCancel=True, + icon='Warning', + parent=window) + if not ch: + # user clicked cancel + return True + + return False + + 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() + version = selectedComponents[index].version + dirname = os.path.join(self.presetDir, componentName, str(version)) + filepath = os.path.join(dirname, presetName) + self.core.openPreset(filepath, index, presetName) + + self.parent.updateComponentTitle(index) + self.parent.drawPreview() + + def openDeletePresetDialog(self): + row = self.getPresetRow() + if row == -1: + return + comp, vers, name = self.presetRows[row] + ch = self.parent.showMessage( + msg='Really delete %s?' % name, + showCancel=True, + icon='Warning', + parent=self.window + ) + 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) + + def warnMessage(self, window=None): + self.parent.showMessage( + msg='Preset names must contain only letters, ' + 'numbers, and spaces.', + parent=window if window else self.window) + + def getPresetRow(self): + row = self.window.listWidget_presets.currentRow() + if row > -1: + return row + + # check if component selected in MainWindow has preset loaded + componentList = self.parent.window.listWidget_componentList + compIndex = componentList.currentRow() + if compIndex == -1: + return compIndex + + preset = self.core.selectedComponents[compIndex].currentPreset + if preset is None: + return -1 + else: + rowTuple = ( + self.core.selectedComponents[compIndex].name, + self.core.selectedComponents[compIndex].version, + preset + ) + for i, tup in enumerate(self.presetRows): + if rowTuple == tup: + index = i + break + else: + return -1 + 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: + return + + while True: + newName, OK = QtWidgets.QInputDialog.getText( + self.window, + 'Preset Manager', + 'Rename Preset:', + QtWidgets.QLineEdit.Normal, + self.presetRows[index][2] + ) + if OK: + if badName(newName): + self.warnMessage() + continue + if newName: + comp, vers, oldName = self.presetRows[index] + 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() + break + + def openImportDialog(self): + filename, _ = QtWidgets.QFileDialog.getOpenFileName( + self.window, "Import Preset File", + self.settings.value("presetDir"), + "Preset Files (*.avl)") + if filename: + # get installed path & ask user to overwrite if needed + path = '' + while True: + if path: + if self.presetExists(path): + break + else: + if os.path.exists(path): + os.remove(path) + success, path = self.core.importPreset(filename) + if success: + break + + self.findPresets() + self.drawPresetList() + self.settings.setValue("presetDir", os.path.dirname(filename)) + + def openExportDialog(self): + index = self.getPresetRow() + if index == -1: + return + filename, _ = QtWidgets.QFileDialog.getSaveFileName( + self.window, "Export Preset", + self.settings.value("presetDir"), + "Preset Files (*.avl)") + if filename: + comp, vers, name = self.presetRows[index] + if not self.core.exportPreset(filename, comp, vers, name): + self.parent.showMessage( + msg='Couldn\'t export %s.' % filename, + parent=self.window + ) + self.settings.setValue("presetDir", os.path.dirname(filename)) + + 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)) diff --git a/src/gui/presetmanager.ui b/src/gui/presetmanager.ui new file mode 100644 index 0000000..5257b1c --- /dev/null +++ b/src/gui/presetmanager.ui @@ -0,0 +1,150 @@ + + + presetmanager + + + Qt::NonModal + + + true + + + + 0 + 0 + 497 + 377 + + + + Preset Manager + + + + + + + + + + + Filter by name + + + + + + + + 200 + 0 + + + + + + + + + + + + + 0 + 0 + + + + true + + + + + + + + + QLayout::SetMinimumSize + + + + + Import + + + + + + + Export + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + true + + + Rename + + + + + + + Delete + + + + + + + + + + + <html><head/><body><p><span style=" font-size:10pt; font-style:italic;">Right-click components in the main window to create presets</span></p></body></html> + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + + + + + + diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py new file mode 100644 index 0000000..9615884 --- /dev/null +++ b/src/gui/preview_thread.py @@ -0,0 +1,90 @@ +''' + Thread that runs to create QImages for MainWindow's preview label. + Processes a queue of component lists. +''' +from PyQt5 import QtCore, QtGui, uic +from PyQt5.QtCore import pyqtSignal, pyqtSlot +from PIL import Image +from PIL.ImageQt import ImageQt +from queue import Queue, Empty +import os +import logging + +from toolkit.frame import Checkerboard +from toolkit import disableWhenOpeningProject + + +log = logging.getLogger("AVP.PreviewThread") + + +class Worker(QtCore.QObject): + + imageCreated = pyqtSignal(QtGui.QImage) + error = pyqtSignal(str) + + def __init__(self, parent=None, queue=None): + QtCore.QObject.__init__(self) + parent.newTask.connect(self.createPreviewImage) + parent.processTask.connect(self.process) + self.parent = parent + self.core = parent.core + self.settings = parent.settings + self.queue = queue + + width = int(self.settings.value('outputWidth')) + height = int(self.settings.value('outputHeight')) + self.background = Checkerboard(width, height) + + @disableWhenOpeningProject + @pyqtSlot(list) + def createPreviewImage(self, components): + dic = { + "components": components, + } + self.queue.put(dic) + + @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: + try: + self.queue.get(block=False) + except Empty: + continue + 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') + components = nextPreviewInformation["components"] + for component in reversed(components): + try: + component.lockSize(width, height) + newFrame = component.previewRender() + component.unlockSize() + frame = Image.alpha_composite( + frame, newFrame + ) + + except ValueError as e: + errMsg = "Bad frame returned by %s's preview renderer. " \ + "%s. New frame size was %s*%s; should be %s*%s." % ( + str(component), str(e).capitalize(), + newFrame.width, newFrame.height, + width, height + ) + log.critical(errMsg) + self.error.emit(errMsg) + break + except RuntimeError as e: + log.error(str(e)) + else: + self.frame = ImageQt(frame) + self.imageCreated.emit(QtGui.QImage(self.frame)) + + except Empty: + True diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py new file mode 100644 index 0000000..40c19c6 --- /dev/null +++ b/src/gui/preview_win.py @@ -0,0 +1,62 @@ +from PyQt5 import QtCore, QtGui, QtWidgets +import logging + + +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 + self.setFrameStyle(QtWidgets.QFrame.StyledPanel) + self.pixmap = QtGui.QPixmap(img) + + def paintEvent(self, event): + size = self.size() + painter = QtGui.QPainter(self) + point = QtCore.QPoint(0, 0) + scaledPix = self.pixmap.scaled( + size, + QtCore.Qt.KeepAspectRatio, + transformMode=QtCore.Qt.SmoothTransformation) + + # start painting the label from left upper corner + point.setX((size.width() - scaledPix.width())/2) + point.setY((size.height() - scaledPix.height())/2) + painter.drawPixmap(point, scaledPix) + + def changePixmap(self, img): + self.pixmap = QtGui.QPixmap(img) + self.repaint() + + def mousePressEvent(self, event): + if self.parent.encoding: + return + + i = self.parent.window.listWidget_componentList.currentRow() + 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' % ( + i, pos, butt)) + component.previewClickEvent( + pos, size, butt + ) + self.parent.core.updateComponent(i) + + @QtCore.pyqtSlot(str) + def threadError(self, msg): + self.parent.showMessage( + msg=msg, + icon='Critical', + parent=self + ) diff --git a/src/mainwindow.py b/src/mainwindow.py deleted file mode 100644 index af6e190..0000000 --- a/src/mainwindow.py +++ /dev/null @@ -1,946 +0,0 @@ -''' - When using GUI mode, this module's object (the main window) takes - user input to construct a program state (stored in the Core object). - This shows a preview of the video being created and allows for saving - projects and exporting the video at a later time. -''' -from PyQt5 import QtCore, QtGui, uic, QtWidgets -from PyQt5.QtWidgets import QMenu, QShortcut -from PIL import Image -from queue import Queue -import sys -import os -import signal -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 - - -log = logging.getLogger('AVP.MainWindow') - - -class MainWindow(QtWidgets.QMainWindow): - ''' - The MainWindow wraps many Core methods in order to update the GUI - accordingly. E.g., instead of self.core.openProject(), it will use - self.openProject() and update the window titlebar within the wrapper. - - MainWindow manages the autosave feature, although Core has the - primary functions for opening and creating project files. - ''' - - createVideo = QtCore.pyqtSignal() - newTask = QtCore.pyqtSignal(list) # for the preview window - processTask = QtCore.pyqtSignal() - - 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()))) - - # widgets of component settings - self.pages = [] - self.lastAutosave = time.time() - # list of previous five autosave times, used to reduce update spam - self.autosaveTimes = [] - 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, '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")) - window.verticalLayout_previewWrapper.addWidget(self.previewWindow) - - log.debug('Starting preview thread') - self.previewQueue = Queue() - self.previewThread = QtCore.QThread(self) - self.previewWorker = preview_thread.Worker(self, self.previewQueue) - self.previewWorker.error.connect(self.previewWindow.threadError) - self.previewWorker.moveToThread(self.previewThread) - self.previewWorker.imageCreated.connect(self.showPreviewImage) - self.previewThread.start() - - log.debug('Starting preview timer') - self.timer = QtCore.QTimer(self) - self.timer.timeout.connect(self.processTask.emit) - self.timer.start(500) - - # Begin decorating the window and connecting events - self.window.installEventFilter(self) - componentList = self.window.listWidget_componentList - - if sys.platform == 'darwin': - log.debug( - 'Darwin detected: showing progress label below progress bar') - window.progressBar_createVideo.setTextVisible(False) - else: - window.progressLabel.setHidden(True) - - window.toolButton_selectAudioFile.clicked.connect( - self.openInputFileDialog) - - window.toolButton_selectOutputFile.clicked.connect( - self.openOutputFileDialog) - - def changedField(): - self.autosave() - self.updateWindowTitle() - - window.lineEdit_audioFile.textChanged.connect(changedField) - window.lineEdit_outputFile.textChanged.connect(changedField) - - window.progressBar_createVideo.setValue(0) - - window.pushButton_createVideo.clicked.connect( - self.createAudioVisualisation) - - window.pushButton_Cancel.clicked.connect(self.stopVideo) - - for i, container in enumerate(Core.encoderOptions['containers']): - window.comboBox_videoContainer.addItem(container['name']) - if container['name'] == self.settings.value('outputContainer'): - selectedContainer = i - - window.comboBox_videoContainer.setCurrentIndex(selectedContainer) - window.comboBox_videoContainer.currentIndexChanged.connect( - self.updateCodecs - ) - - self.updateCodecs() - - for i in range(window.comboBox_videoCodec.count()): - codec = window.comboBox_videoCodec.itemText(i) - if codec == self.settings.value('outputVideoCodec'): - window.comboBox_videoCodec.setCurrentIndex(i) - - for i in range(window.comboBox_audioCodec.count()): - codec = window.comboBox_audioCodec.itemText(i) - if codec == self.settings.value('outputAudioCodec'): - window.comboBox_audioCodec.setCurrentIndex(i) - - window.comboBox_videoCodec.currentIndexChanged.connect( - self.updateCodecSettings - ) - - window.comboBox_audioCodec.currentIndexChanged.connect( - self.updateCodecSettings - ) - - vBitrate = int(self.settings.value('outputVideoBitrate')) - aBitrate = int(self.settings.value('outputAudioBitrate')) - - window.spinBox_vBitrate.setValue(vBitrate) - window.spinBox_aBitrate.setValue(aBitrate) - window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings) - window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings) - - # Make component buttons - self.compMenu = QMenu() - 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) - ) - - self.window.pushButton_addComponent.setMenu(self.compMenu) - - componentList.dropEvent = self.dragComponent - componentList.itemSelectionChanged.connect( - self.changeComponentWidget - ) - componentList.itemSelectionChanged.connect( - self.presetManager.clearPresetListSelection - ) - self.window.pushButton_removeComponent.clicked.connect( - lambda: self.removeComponent() - ) - - componentList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - componentList.customContextMenuRequested.connect( - self.componentContextMenu - ) - - currentRes = str(self.settings.value('outputWidth'))+'x' + \ - str(self.settings.value('outputHeight')) - for i, res in enumerate(Core.resolutions): - window.comboBox_resolution.addItem(res) - if res == currentRes: - currentRes = i - window.comboBox_resolution.setCurrentIndex(currentRes) - window.comboBox_resolution.currentIndexChanged.connect( - self.updateResolution - ) - - self.window.pushButton_listMoveUp.clicked.connect( - lambda: self.moveComponent(-1) - ) - self.window.pushButton_listMoveDown.clicked.connect( - lambda: self.moveComponent(1) - ) - - # Configure the Projects Menu - self.projectMenu = QMenu() - self.window.menuButton_newProject = self.projectMenu.addAction( - "New Project" - ) - self.window.menuButton_newProject.triggered.connect( - lambda: self.createNewProject() - ) - self.window.menuButton_openProject = self.projectMenu.addAction( - "Open Project" - ) - self.window.menuButton_openProject.triggered.connect( - lambda: self.openOpenProjectDialog() - ) - - action = self.projectMenu.addAction("Save Project") - action.triggered.connect(self.saveCurrentProject) - - action = self.projectMenu.addAction("Save Project As") - action.triggered.connect(self.openSaveProjectDialog) - - self.window.pushButton_projects.setMenu(self.projectMenu) - - # Configure the Presets Button - self.window.pushButton_presets.clicked.connect( - self.openPresetManager - ) - - self.updateWindowTitle() - log.debug('Showing main window') - window.show() - - if project and project != self.autosavePath: - if not project.endswith('.avp'): - project += '.avp' - # open a project from the commandline - if not os.path.dirname(project): - project = os.path.join( - self.settings.value("projectDir"), project - ) - self.currentProject = project - self.settings.setValue("currentProject", project) - if os.path.exists(self.autosavePath): - os.remove(self.autosavePath) - else: - # open the last currentProject from settings - self.currentProject = self.settings.value("currentProject") - - # delete autosave if it's identical to this project - if self.autosaveExists(identical=True): - os.remove(self.autosavePath) - - if self.currentProject and os.path.exists(self.autosavePath): - ch = self.showMessage( - msg="Restore unsaved changes in project '%s'?" - % os.path.basename(self.currentProject)[:-4], - showCancel=True) - if ch: - self.saveProjectChanges() - else: - os.remove(self.autosavePath) - - self.openProject(self.currentProject, prompt=False) - self.drawPreview(True) - - # verify Pillow version - if not self.settings.value("pilMsgShown") \ - and 'post' not in Image.PILLOW_VERSION: - self.showMessage( - msg="You are using the standard version of the " - "Python imaging library (Pillow %s). Upgrade " - "to the Pillow-SIMD fork to enable hardware accelerations " - "and export videos faster." % Image.PILLOW_VERSION - ) - self.settings.setValue("pilMsgShown", True) - - # verify Ffmpeg version - if not self.settings.value("ffmpegMsgShown"): - try: - with open(os.devnull, "w") as f: - ffmpegVers = checkOutput( - ['ffmpeg', '-version'], stderr=f - ) - goodVersion = str(ffmpegVers).split()[2].startswith('3') - except Exception: - goodVersion = False - else: - goodVersion = True - - if not goodVersion: - self.showMessage( - msg="You're using an old version of Ffmpeg. " - "Some features may not work as expected." - ) - self.settings.setValue("ffmpegMsgShown", True) - - # Hotkeys for projects - QtWidgets.QShortcut("Ctrl+S", self.window, self.saveCurrentProject) - QtWidgets.QShortcut("Ctrl+A", self.window, self.openSaveProjectDialog) - QtWidgets.QShortcut("Ctrl+O", self.window, self.openOpenProjectDialog) - QtWidgets.QShortcut("Ctrl+N", self.window, self.createNewProject) - - # Hotkeys for component list - for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert): - QtWidgets.QShortcut( - inskey, self.window, - activated=lambda: self.window.pushButton_addComponent.click() - ) - for delkey in ("Ctrl+R", QtCore.Qt.Key_Delete): - QtWidgets.QShortcut( - delkey, self.window.listWidget_componentList, - self.removeComponent - ) - QtWidgets.QShortcut( - "Ctrl+Space", self.window, - activated=lambda: self.window.listWidget_componentList.setFocus() - ) - QtWidgets.QShortcut( - "Ctrl+Shift+S", self.window, - self.presetManager.openSavePresetDialog - ) - QtWidgets.QShortcut( - "Ctrl+Shift+C", self.window, self.presetManager.clearPreset - ) - - QtWidgets.QShortcut( - "Ctrl+Up", self.window.listWidget_componentList, - activated=lambda: self.moveComponent(-1) - ) - QtWidgets.QShortcut( - "Ctrl+Down", self.window.listWidget_componentList, - activated=lambda: self.moveComponent(1) - ) - QtWidgets.QShortcut( - "Ctrl+Home", self.window.listWidget_componentList, - activated=lambda: self.moveComponent('top') - ) - QtWidgets.QShortcut( - "Ctrl+End", self.window.listWidget_componentList, - activated=lambda: self.moveComponent('bottom') - ) - - # Debug Hotkeys - QtWidgets.QShortcut( - "Ctrl+Alt+Shift+R", self.window, self.drawPreview - ) - QtWidgets.QShortcut( - "Ctrl+Alt+Shift+F", self.window, self.showFfmpegCommand - ) - - @QtCore.pyqtSlot() - def cleanUp(self, *args): - log.info('Ending the preview thread') - self.timer.stop() - self.previewThread.quit() - self.previewThread.wait() - - @disableWhenOpeningProject - def updateWindowTitle(self): - appName = 'Audio Visualizer' - try: - if self.currentProject: - appName += ' - %s' % \ - os.path.splitext( - os.path.basename(self.currentProject))[0] - if self.autosaveExists(identical=False): - appName += '*' - except AttributeError: - pass - log.debug('Setting window title to %s' % appName) - self.window.setWindowTitle(appName) - - @QtCore.pyqtSlot(int, dict) - def updateComponentTitle(self, pos, presetStore=False): - 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) - if pos < 0: - pos = len(self.core.selectedComponents)-1 - name = str(self.core.selectedComponents[pos]) - 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' % ( - name, pos, modified, title - )) - else: - log.debug('Setting %s #%s\'s title: %s' % ( - name, pos, title - )) - self.window.listWidget_componentList.item(pos).setText(title) - - def updateCodecs(self): - containerWidget = self.window.comboBox_videoContainer - vCodecWidget = self.window.comboBox_videoCodec - aCodecWidget = self.window.comboBox_audioCodec - index = containerWidget.currentIndex() - name = containerWidget.itemText(index) - self.settings.setValue('outputContainer', name) - - vCodecWidget.clear() - aCodecWidget.clear() - - for container in Core.encoderOptions['containers']: - if container['name'] == name: - for vCodec in container['video-codecs']: - vCodecWidget.addItem(vCodec) - for aCodec in container['audio-codecs']: - aCodecWidget.addItem(aCodec) - - def updateCodecSettings(self): - '''Updates settings.ini to match encoder option widgets''' - vCodecWidget = self.window.comboBox_videoCodec - vBitrateWidget = self.window.spinBox_vBitrate - aBitrateWidget = self.window.spinBox_aBitrate - aCodecWidget = self.window.comboBox_audioCodec - currentVideoCodec = vCodecWidget.currentIndex() - currentVideoCodec = vCodecWidget.itemText(currentVideoCodec) - currentVideoBitrate = vBitrateWidget.value() - currentAudioCodec = aCodecWidget.currentIndex() - currentAudioCodec = aCodecWidget.itemText(currentAudioCodec) - currentAudioBitrate = aBitrateWidget.value() - self.settings.setValue('outputVideoCodec', currentVideoCodec) - self.settings.setValue('outputAudioCodec', currentAudioCodec) - self.settings.setValue('outputVideoBitrate', currentVideoBitrate) - self.settings.setValue('outputAudioBitrate', currentAudioBitrate) - - @disableWhenOpeningProject - def autosave(self, force=False): - if not self.currentProject: - if os.path.exists(self.autosavePath): - os.remove(self.autosavePath) - elif force or time.time() - self.lastAutosave >= self.autosaveCooldown: - self.core.createProjectFile(self.autosavePath, self.window) - self.lastAutosave = time.time() - if len(self.autosaveTimes) >= 5: - # Do some math to reduce autosave spam. This gives a smooth - # curve up to 5 seconds cooldown and maintains that for 30 secs - # if a component is continuously updated - timeDiff = self.lastAutosave - self.autosaveTimes.pop() - if not force and timeDiff >= 1.0 \ - and timeDiff <= 10.0: - if self.autosaveCooldown / 4.0 < 0.5: - self.autosaveCooldown += 1.0 - self.autosaveCooldown = ( - 5.0 * (self.autosaveCooldown / 5.0) - ) + (self.autosaveCooldown / 5.0) * 2 - elif force or timeDiff >= self.autosaveCooldown * 5: - self.autosaveCooldown = 0.2 - self.autosaveTimes.insert(0, self.lastAutosave) - else: - log.debug('Autosave rejected by cooldown') - - def autosaveExists(self, identical=True): - '''Determines if creating the autosave should be blocked.''' - try: - if self.currentProject and os.path.exists(self.autosavePath) \ - and filecmp.cmp( - self.autosavePath, self.currentProject) == identical: - log.debug( - 'Autosave found %s to be identical' - % 'not' if not identical else '' - ) - return True - except FileNotFoundError: - log.error( - 'Project file couldn\'t be located:', self.currentProject) - return identical - return False - - def saveProjectChanges(self): - '''Overwrites project file with autosave file''' - try: - os.remove(self.currentProject) - os.rename(self.autosavePath, self.currentProject) - return True - except (FileNotFoundError, IsADirectoryError) as e: - self.showMessage( - msg='Project file couldn\'t be saved.', - detail=str(e)) - return False - - def openInputFileDialog(self): - inputDir = self.settings.value("inputDir", os.path.expanduser("~")) - - fileName, _ = QtWidgets.QFileDialog.getOpenFileName( - self.window, "Open Audio File", - inputDir, "Audio Files (%s)" % " ".join(Core.audioFormats)) - - if fileName: - self.settings.setValue("inputDir", os.path.dirname(fileName)) - self.window.lineEdit_audioFile.setText(fileName) - - def openOutputFileDialog(self): - outputDir = self.settings.value("outputDir", os.path.expanduser("~")) - - fileName, _ = QtWidgets.QFileDialog.getSaveFileName( - self.window, "Set Output Video File", - outputDir, - "Video Files (%s);; All Files (*)" % " ".join( - Core.videoFormats)) - - if fileName: - self.settings.setValue("outputDir", os.path.dirname(fileName)) - self.window.lineEdit_outputFile.setText(fileName) - - def stopVideo(self): - log.info('Export cancelled') - self.videoWorker.cancel() - self.canceled = True - - def createAudioVisualisation(self): - # create output video if mandatory settings are filled in - audioFile = self.window.lineEdit_audioFile.text() - outputPath = self.window.lineEdit_outputFile.text() - - if audioFile and outputPath and self.core.selectedComponents: - if not os.path.dirname(outputPath): - outputPath = os.path.join( - os.path.expanduser("~"), outputPath) - if outputPath and os.path.isdir(outputPath): - self.showMessage( - msg='Chosen filename matches a directory, which ' - 'cannot be overwritten. Please choose a different ' - 'filename or move the directory.', - icon='Warning', - ) - return - else: - if not audioFile or not outputPath: - self.showMessage( - msg="You must select an audio file and output filename." - ) - elif not self.core.selectedComponents: - self.showMessage( - msg="Not enough components." - ) - return - - self.canceled = False - self.progressBarUpdated(-1) - self.videoWorker = self.core.newVideoWorker( - self, audioFile, outputPath - ) - self.videoWorker.progressBarUpdate.connect(self.progressBarUpdated) - self.videoWorker.progressBarSetText.connect( - self.progressBarSetText) - self.videoWorker.imageCreated.connect(self.showPreviewImage) - self.videoWorker.encoding.connect(self.changeEncodingStatus) - self.createVideo.emit() - - @QtCore.pyqtSlot(str, str) - def videoThreadError(self, msg, detail): - try: - self.stopVideo() - except AttributeError as e: - if 'videoWorker' not in str(e): - raise - self.showMessage( - msg=msg, - detail=detail, - icon='Critical', - ) - - def changeEncodingStatus(self, status): - self.encoding = status - if status: - self.window.pushButton_createVideo.setEnabled(False) - self.window.pushButton_Cancel.setEnabled(True) - self.window.comboBox_resolution.setEnabled(False) - self.window.stackedWidget.setEnabled(False) - self.window.tab_encoderSettings.setEnabled(False) - self.window.label_audioFile.setEnabled(False) - self.window.toolButton_selectAudioFile.setEnabled(False) - self.window.label_outputFile.setEnabled(False) - self.window.toolButton_selectOutputFile.setEnabled(False) - self.window.lineEdit_audioFile.setEnabled(False) - self.window.lineEdit_outputFile.setEnabled(False) - self.window.pushButton_addComponent.setEnabled(False) - self.window.pushButton_removeComponent.setEnabled(False) - self.window.pushButton_listMoveDown.setEnabled(False) - self.window.pushButton_listMoveUp.setEnabled(False) - self.window.menuButton_newProject.setEnabled(False) - self.window.menuButton_openProject.setEnabled(False) - if sys.platform == 'darwin': - self.window.progressLabel.setHidden(False) - else: - self.window.listWidget_componentList.setEnabled(False) - else: - self.window.pushButton_createVideo.setEnabled(True) - self.window.pushButton_Cancel.setEnabled(False) - self.window.comboBox_resolution.setEnabled(True) - self.window.stackedWidget.setEnabled(True) - self.window.tab_encoderSettings.setEnabled(True) - self.window.label_audioFile.setEnabled(True) - self.window.toolButton_selectAudioFile.setEnabled(True) - self.window.lineEdit_audioFile.setEnabled(True) - self.window.label_outputFile.setEnabled(True) - self.window.toolButton_selectOutputFile.setEnabled(True) - self.window.lineEdit_outputFile.setEnabled(True) - self.window.pushButton_addComponent.setEnabled(True) - self.window.pushButton_removeComponent.setEnabled(True) - self.window.pushButton_listMoveDown.setEnabled(True) - self.window.pushButton_listMoveUp.setEnabled(True) - self.window.menuButton_newProject.setEnabled(True) - self.window.menuButton_openProject.setEnabled(True) - self.window.listWidget_componentList.setEnabled(True) - self.window.progressLabel.setHidden(True) - self.drawPreview(True) - - @QtCore.pyqtSlot(int) - def progressBarUpdated(self, value): - self.window.progressBar_createVideo.setValue(value) - - @QtCore.pyqtSlot(str) - def progressBarSetText(self, value): - if sys.platform == 'darwin': - self.window.progressLabel.setText(value) - else: - self.window.progressBar_createVideo.setFormat(value) - - def updateResolution(self): - resIndex = int(self.window.comboBox_resolution.currentIndex()) - res = Core.resolutions[resIndex].split('x') - changed = res[0] != self.settings.value("outputWidth") - self.settings.setValue('outputWidth', res[0]) - self.settings.setValue('outputHeight', res[1]) - if changed: - for i in range(len(self.core.selectedComponents)): - self.core.updateComponent(i) - - def drawPreview(self, force=False, **kwargs): - '''Use autosave keyword arg to force saving or not saving if needed''' - self.newTask.emit(self.core.selectedComponents) - # self.processTask.emit() - if force or 'autosave' in kwargs: - if force or kwargs['autosave']: - self.autosave(True) - else: - self.autosave() - self.updateWindowTitle() - - @QtCore.pyqtSlot(QtGui.QImage) - def showPreviewImage(self, image): - self.previewWindow.changePixmap(image) - - def showFfmpegCommand(self): - from textwrap import wrap - from toolkit.ffmpeg import createFfmpegCommand - command = createFfmpegCommand( - self.window.lineEdit_audioFile.text(), - self.window.lineEdit_outputFile.text(), - self.core.selectedComponents - ) - lines = wrap(" ".join(command), 49) - self.showMessage( - msg="Current FFmpeg command:\n\n %s" % " ".join(lines) - ) - - def insertComponent(self, index): - componentList = self.window.listWidget_componentList - stackedWidget = self.window.stackedWidget - - componentList.insertItem( - index, - self.core.selectedComponents[index].name) - componentList.setCurrentRow(index) - - # connect to signal that adds an asterisk when modified - self.core.selectedComponents[index].modified.connect( - self.updateComponentTitle) - - self.pages.insert(index, self.core.selectedComponents[index].page) - stackedWidget.insertWidget(index, self.pages[index]) - stackedWidget.setCurrentIndex(index) - - return index - - def removeComponent(self): - componentList = self.window.listWidget_componentList - - 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() - self.drawPreview() - - @disableWhenEncoding - def moveComponent(self, change): - '''Moves a component relatively from its current position''' - componentList = self.window.listWidget_componentList - if change == 'top': - change = -componentList.currentRow() - elif change == 'bottom': - change = len(componentList)-componentList.currentRow()-1 - stackedWidget = self.window.stackedWidget - - 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) - - def getComponentListMousePos(self, position): - ''' - Given a QPos, returns the component index under the mouse cursor - or -1 if no component is there. - ''' - componentList = self.window.listWidget_componentList - - modelIndexes = [ - componentList.model().index(i) - for i in range(componentList.count()) - ] - rects = [ - componentList.visualRect(modelIndex) - for modelIndex in modelIndexes - ] - mousePos = [rect.contains(position) for rect in rects] - if not any(mousePos): - # Not clicking a component - mousePos = -1 - else: - mousePos = mousePos.index(True) - log.debug('Click component list row %s' % mousePos) - return mousePos - - @disableWhenEncoding - def dragComponent(self, event): - '''Used as Qt drop event for the component listwidget''' - componentList = self.window.listWidget_componentList - mousePos = self.getComponentListMousePos(event.pos()) - if mousePos > -1: - change = (componentList.currentRow() - mousePos) * -1 - else: - change = (componentList.count() - componentList.currentRow() - 1) - self.moveComponent(change) - - def changeComponentWidget(self): - selected = self.window.listWidget_componentList.selectedItems() - if selected: - index = self.window.listWidget_componentList.row(selected[0]) - self.window.stackedWidget.setCurrentIndex(index) - - def openPresetManager(self): - '''Preset manager for importing, exporting, renaming, deleting''' - self.presetManager.show() - - def clear(self): - '''Get a blank slate''' - self.core.clearComponents() - self.window.listWidget_componentList.clear() - for widget in self.pages: - self.window.stackedWidget.removeWidget(widget) - self.pages = [] - for field in ( - self.window.lineEdit_audioFile, - self.window.lineEdit_outputFile - ): - field.blockSignals(True) - field.setText('') - field.blockSignals(False) - self.progressBarUpdated(0) - self.progressBarSetText('') - - @disableWhenEncoding - def createNewProject(self, prompt=True): - if prompt: - self.openSaveChangesDialog('starting a new project') - - self.clear() - self.currentProject = None - self.settings.setValue("currentProject", None) - self.drawPreview(True) - - def saveCurrentProject(self): - if self.currentProject: - self.core.createProjectFile(self.currentProject, self.window) - try: - os.remove(self.autosavePath) - except FileNotFoundError: - pass - self.updateWindowTitle() - else: - self.openSaveProjectDialog() - - def openSaveChangesDialog(self, phrase): - success = 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) - if ch: - success = self.saveProjectChanges() - - if success and os.path.exists(self.autosavePath): - os.remove(self.autosavePath) - - def openSaveProjectDialog(self): - filename, _ = QtWidgets.QFileDialog.getSaveFileName( - self.window, "Create Project File", - self.settings.value("projectDir"), - "Project Files (*.avp)") - if not filename: - return - if not filename.endswith(".avp"): - filename += '.avp' - self.settings.setValue("projectDir", os.path.dirname(filename)) - self.settings.setValue("currentProject", filename) - self.currentProject = filename - self.core.createProjectFile(filename, self.window) - self.updateWindowTitle() - - @disableWhenEncoding - def openOpenProjectDialog(self): - filename, _ = QtWidgets.QFileDialog.getOpenFileName( - self.window, "Open Project File", - self.settings.value("projectDir"), - "Project Files (*.avp)") - self.openProject(filename) - - def openProject(self, filepath, prompt=True): - if not filepath or not os.path.exists(filepath) \ - or not filepath.endswith('.avp'): - return - - self.clear() - # ask to save any changes that are about to get deleted - if prompt: - self.openSaveChangesDialog('opening another project') - - self.currentProject = filepath - self.settings.setValue("currentProject", filepath) - self.settings.setValue("projectDir", os.path.dirname(filepath)) - # actually load the project using core method - self.core.openProject(self, filepath) - self.drawPreview(autosave=False) - self.updateWindowTitle() - - def showMessage(self, **kwargs): - parent = kwargs['parent'] if 'parent' in kwargs else self.window - msg = QtWidgets.QMessageBox(parent) - msg.setModal(True) - msg.setText(kwargs['msg']) - msg.setIcon( - eval('QtWidgets.QMessageBox.%s' % kwargs['icon']) - if 'icon' in kwargs else QtWidgets.QMessageBox.Information - ) - msg.setDetailedText(kwargs['detail'] if 'detail' in kwargs else None) - if 'showCancel'in kwargs and kwargs['showCancel']: - msg.setStandardButtons( - QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) - else: - msg.setStandardButtons(QtWidgets.QMessageBox.Ok) - ch = msg.exec_() - if ch == 1024: - return True - return False - - @disableWhenEncoding - def componentContextMenu(self, QPos): - '''Appears when right-clicking the component list''' - componentList = self.window.listWidget_componentList - self.menu = QMenu() - parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0)) - - index = self.getComponentListMousePos(QPos) - if index > -1: - # Show preset menu if clicking a component - self.presetManager.findPresets() - menuItem = self.menu.addAction("Save Preset") - menuItem.triggered.connect( - self.presetManager.openSavePresetDialog - ) - - # submenu for opening presets - try: - presets = self.presetManager.presets[ - str(self.core.selectedComponents[index]) - ] - self.presetSubmenu = QMenu("Open Preset") - self.menu.addMenu(self.presetSubmenu) - - for version, presetName in presets: - menuItem = self.presetSubmenu.addAction(presetName) - menuItem.triggered.connect( - lambda _, presetName=presetName: - self.presetManager.openPreset(presetName) - ) - except KeyError: - pass - - if self.core.selectedComponents[index].currentPreset: - menuItem = self.menu.addAction("Clear Preset") - menuItem.triggered.connect( - self.presetManager.clearPreset - ) - self.menu.addSeparator() - - # "Add Component" submenu - self.submenu = QMenu("Add") - self.menu.addMenu(self.submenu) - insertCompAtTop = self.settings.value("pref_insertCompAtTop") - 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 - ) - ) - - 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 diff --git a/src/mainwindow.ui b/src/mainwindow.ui deleted file mode 100644 index b43d375..0000000 --- a/src/mainwindow.ui +++ /dev/null @@ -1,828 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 1008 - 575 - - - - - 0 - 0 - - - - - 0 - 0 - - - - Qt::StrongFocus - - - MainWindow - - - - - 0 - 0 - - - - false - - - - 9 - - - 0 - - - - - - - Qt::Vertical - - - QSizePolicy::MinimumExpanding - - - - 0 - 360 - - - - - - - - QLayout::SetDefaultConstraint - - - 0 - - - - - Qt::Horizontal - - - QSizePolicy::MinimumExpanding - - - - 420 - 0 - - - - - - - - - - QLayout::SetMinimumSize - - - 3 - - - - - QLayout::SetMinimumSize - - - 3 - - - - - QLayout::SetMinimumSize - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 140 - 20 - - - - - - - - Projects - - - - - - - Presets - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 20 - 2 - - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - true - - - QFrame::StyledPanel - - - QFrame::Sunken - - - 1 - - - true - - - true - - - false - - - QAbstractItemView::InternalMove - - - Qt::MoveAction - - - - - - - - - Add - - - - - - - Remove - - - - - - - Up - - - - - - - Down - - - - - - - - - - - 4 - - - 2 - - - - - - - - - - - QLayout::SetFixedSize - - - 4 - - - 0 - - - - - - 0 - 0 - - - - - 500 - 0 - - - - - 16777215 - 180 - - - - QTabWidget::North - - - QTabWidget::Rounded - - - 0 - - - - Export Video - - - - 10 - - - - - 0 - - - - - - 0 - 0 - - - - - 85 - 0 - - - - - 80 - 16777215 - - - - - 80 - 0 - - - - Audio File - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - - 0 - 0 - - - - - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - ... - - - - - - - - - - - - - - 0 - 0 - - - - - 85 - 0 - - - - - 0 - 0 - - - - Output File - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - ... - - - - - - - - - - - 0 - - - - - - 0 - 0 - - - - 24 - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 10 - 20 - - - - - - - - Create Video - - - - - - - false - - - Cancel - - - - - - - - - - - - true - - - Qt::AlignCenter - - - -1 - - - - - - - progressLabel - - - - Encoder Settings - - - - 10 - - - - - - - - 0 - 0 - - - - - 85 - 0 - - - - Container - - - - - - - - 150 - 0 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 5 - 5 - - - - - - - - - 0 - 0 - - - - Resolution - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - - - - - - - - - 0 - 0 - - - - - 85 - 0 - - - - Video Codec - - - - - - - - 150 - 0 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 5 - - - - - - - - - 0 - 0 - - - - Video Bitrate (Kbps) - - - - - - - 99999 - - - - - - - - - - - - 0 - 0 - - - - - 85 - 0 - - - - Audio Codec - - - - - - - - 150 - 0 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 10 - - - - - - - - - 0 - 0 - - - - Audio Bitrate (Kbps) - - - - - - - 9999 - - - - - - - - - - - - - QLayout::SetDefaultConstraint - - - - - Qt::Horizontal - - - QSizePolicy::MinimumExpanding - - - - 500 - 0 - - - - - - - - - 0 - 0 - - - - - 0 - 180 - - - - - 16777215 - 180 - - - - -1 - - - - - - - - - - - - - diff --git a/src/presetmanager.py b/src/presetmanager.py deleted file mode 100644 index b1eeb34..0000000 --- a/src/presetmanager.py +++ /dev/null @@ -1,358 +0,0 @@ -''' - Preset manager object handles all interactions with presets, including - the context menu accessed from MainWindow. -''' -from PyQt5 import QtCore, QtWidgets -import string -import os - -from toolkit import badName -from core import Core - - -class PresetManager(QtWidgets.QDialog): - def __init__(self, window, parent): - super().__init__(parent.window) - self.parent = parent - self.core = parent.core - self.settings = parent.settings - self.presetDir = parent.presetDir - if not self.settings.value('presetDir'): - self.settings.setValue( - "presetDir", - os.path.join(parent.dataDir, 'projects')) - - self.findPresets() - - # window - self.lastFilter = '*' - self.presetRows = [] # list of (comp, vers, name) tuples - self.window = window - self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) - - # connect button signals - self.window.pushButton_delete.clicked.connect( - self.openDeletePresetDialog - ) - self.window.pushButton_rename.clicked.connect( - self.openRenamePresetDialog - ) - self.window.pushButton_import.clicked.connect( - self.openImportDialog - ) - self.window.pushButton_export.clicked.connect( - self.openExportDialog - ) - self.window.pushButton_close.clicked.connect( - self.window.close - ) - - # create filter box and preset list - self.drawFilterList() - self.window.comboBox_filter.currentIndexChanged.connect( - lambda: self.drawPresetList( - self.window.comboBox_filter.currentText(), - self.window.lineEdit_search.text() - ) - ) - - # make auto-completion for search bar - self.autocomplete = QtCore.QStringListModel() - completer = QtWidgets.QCompleter() - completer.setModel(self.autocomplete) - self.window.lineEdit_search.setCompleter(completer) - self.window.lineEdit_search.textChanged.connect( - lambda: self.drawPresetList( - self.window.comboBox_filter.currentText(), - self.window.lineEdit_search.text() - ) - ) - self.drawPresetList('*') - - def show(self): - '''Open a new preset manager window from the mainwindow''' - self.findPresets() - self.drawFilterList() - self.drawPresetList('*') - self.window.show() - - def findPresets(self): - parseList = [] - for dirpath, dirnames, filenames in os.walk(self.presetDir): - # anything without a subdirectory must be a preset folder - if dirnames: - continue - for preset in filenames: - compName = os.path.basename(os.path.dirname(dirpath)) - if compName not in self.core.compNames: - continue - compVers = os.path.basename(dirpath) - try: - parseList.append((compName, int(compVers), preset)) - except ValueError: - continue - self.presets = { - compName: [ - (vers, preset) - for name, vers, preset in parseList - if name == compName - ] - for compName, _, __ in parseList - } - - def drawPresetList(self, compFilter=None, presetFilter=''): - self.window.listWidget_presets.clear() - if compFilter: - self.lastFilter = str(compFilter) - else: - compFilter = str(self.lastFilter) - self.presetRows = [] - presetNames = [] - for component, presets in self.presets.items(): - if compFilter != '*' and component != compFilter: - continue - for vers, preset in presets: - if not presetFilter or presetFilter in preset: - self.window.listWidget_presets.addItem( - '%s: %s' % (component, preset) - ) - self.presetRows.append((component, vers, preset)) - if preset not in presetNames: - presetNames.append(preset) - self.autocomplete.setStringList(presetNames) - - def drawFilterList(self): - self.window.comboBox_filter.clear() - self.window.comboBox_filter.addItem('*') - for component in self.presets: - self.window.comboBox_filter.addItem(component) - - 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) - - def openSavePresetDialog(self): - '''Functions on mainwindow level from the context menu''' - window = self.parent.window - selectedComponents = self.core.selectedComponents - componentList = self.parent.window.listWidget_componentList - - if componentList.currentRow() == -1: - return - while True: - index = componentList.currentRow() - currentPreset = selectedComponents[index].currentPreset - newName, OK = QtWidgets.QInputDialog.getText( - self.parent.window, - 'Audio Visualizer', - 'New Preset Name:', - QtWidgets.QLineEdit.Normal, - currentPreset - ) - if OK: - if badName(newName): - self.warnMessage(self.parent.window) - continue - if newName: - if index != -1: - selectedComponents[index].currentPreset = newName - saveValueStore = \ - selectedComponents[index].savePreset() - saveValueStore['preset'] = newName - componentName = str(selectedComponents[index]).strip() - vers = selectedComponents[index].version - self.createNewPreset( - componentName, vers, newName, - saveValueStore, window=self.parent.window) - self.findPresets() - self.drawPresetList() - self.openPreset(newName, index) - break - - def createNewPreset( - self, compName, vers, filename, saveValueStore, **kwargs): - path = os.path.join(self.presetDir, compName, str(vers), filename) - if self.presetExists(path, **kwargs): - return - self.core.createPresetFile(compName, vers, filename, saveValueStore) - - def presetExists(self, path, **kwargs): - if os.path.exists(path): - window = self.window \ - if 'window' not in kwargs else kwargs['window'] - ch = self.parent.showMessage( - msg="%s already exists! Overwrite it?" % - os.path.basename(path), - showCancel=True, - icon='Warning', - parent=window) - if not ch: - # user clicked cancel - return True - - return False - - 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() - version = selectedComponents[index].version - dirname = os.path.join(self.presetDir, componentName, str(version)) - filepath = os.path.join(dirname, presetName) - self.core.openPreset(filepath, index, presetName) - - self.parent.updateComponentTitle(index) - self.parent.drawPreview() - - def openDeletePresetDialog(self): - row = self.getPresetRow() - if row == -1: - return - comp, vers, name = self.presetRows[row] - ch = self.parent.showMessage( - msg='Really delete %s?' % name, - showCancel=True, - icon='Warning', - parent=self.window - ) - 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) - - def warnMessage(self, window=None): - self.parent.showMessage( - msg='Preset names must contain only letters, ' - 'numbers, and spaces.', - parent=window if window else self.window) - - def getPresetRow(self): - row = self.window.listWidget_presets.currentRow() - if row > -1: - return row - - # check if component selected in MainWindow has preset loaded - componentList = self.parent.window.listWidget_componentList - compIndex = componentList.currentRow() - if compIndex == -1: - return compIndex - - preset = self.core.selectedComponents[compIndex].currentPreset - if preset is None: - return -1 - else: - rowTuple = ( - self.core.selectedComponents[compIndex].name, - self.core.selectedComponents[compIndex].version, - preset - ) - for i, tup in enumerate(self.presetRows): - if rowTuple == tup: - index = i - break - else: - return -1 - 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: - return - - while True: - newName, OK = QtWidgets.QInputDialog.getText( - self.window, - 'Preset Manager', - 'Rename Preset:', - QtWidgets.QLineEdit.Normal, - self.presetRows[index][2] - ) - if OK: - if badName(newName): - self.warnMessage() - continue - if newName: - comp, vers, oldName = self.presetRows[index] - 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() - break - - def openImportDialog(self): - filename, _ = QtWidgets.QFileDialog.getOpenFileName( - self.window, "Import Preset File", - self.settings.value("presetDir"), - "Preset Files (*.avl)") - if filename: - # get installed path & ask user to overwrite if needed - path = '' - while True: - if path: - if self.presetExists(path): - break - else: - if os.path.exists(path): - os.remove(path) - success, path = self.core.importPreset(filename) - if success: - break - - self.findPresets() - self.drawPresetList() - self.settings.setValue("presetDir", os.path.dirname(filename)) - - def openExportDialog(self): - index = self.getPresetRow() - if index == -1: - return - filename, _ = QtWidgets.QFileDialog.getSaveFileName( - self.window, "Export Preset", - self.settings.value("presetDir"), - "Preset Files (*.avl)") - if filename: - comp, vers, name = self.presetRows[index] - if not self.core.exportPreset(filename, comp, vers, name): - self.parent.showMessage( - msg='Couldn\'t export %s.' % filename, - parent=self.window - ) - self.settings.setValue("presetDir", os.path.dirname(filename)) - - 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)) diff --git a/src/presetmanager.ui b/src/presetmanager.ui deleted file mode 100644 index 5257b1c..0000000 --- a/src/presetmanager.ui +++ /dev/null @@ -1,150 +0,0 @@ - - - presetmanager - - - Qt::NonModal - - - true - - - - 0 - 0 - 497 - 377 - - - - Preset Manager - - - - - - - - - - - Filter by name - - - - - - - - 200 - 0 - - - - - - - - - - - - - 0 - 0 - - - - true - - - - - - - - - QLayout::SetMinimumSize - - - - - Import - - - - - - - Export - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - true - - - Rename - - - - - - - Delete - - - - - - - - - - - <html><head/><body><p><span style=" font-size:10pt; font-style:italic;">Right-click components in the main window to create presets</span></p></body></html> - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Close - - - - - - - - - - diff --git a/src/preview_thread.py b/src/preview_thread.py deleted file mode 100644 index 9615884..0000000 --- a/src/preview_thread.py +++ /dev/null @@ -1,90 +0,0 @@ -''' - Thread that runs to create QImages for MainWindow's preview label. - Processes a queue of component lists. -''' -from PyQt5 import QtCore, QtGui, uic -from PyQt5.QtCore import pyqtSignal, pyqtSlot -from PIL import Image -from PIL.ImageQt import ImageQt -from queue import Queue, Empty -import os -import logging - -from toolkit.frame import Checkerboard -from toolkit import disableWhenOpeningProject - - -log = logging.getLogger("AVP.PreviewThread") - - -class Worker(QtCore.QObject): - - imageCreated = pyqtSignal(QtGui.QImage) - error = pyqtSignal(str) - - def __init__(self, parent=None, queue=None): - QtCore.QObject.__init__(self) - parent.newTask.connect(self.createPreviewImage) - parent.processTask.connect(self.process) - self.parent = parent - self.core = parent.core - self.settings = parent.settings - self.queue = queue - - width = int(self.settings.value('outputWidth')) - height = int(self.settings.value('outputHeight')) - self.background = Checkerboard(width, height) - - @disableWhenOpeningProject - @pyqtSlot(list) - def createPreviewImage(self, components): - dic = { - "components": components, - } - self.queue.put(dic) - - @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: - try: - self.queue.get(block=False) - except Empty: - continue - 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') - components = nextPreviewInformation["components"] - for component in reversed(components): - try: - component.lockSize(width, height) - newFrame = component.previewRender() - component.unlockSize() - frame = Image.alpha_composite( - frame, newFrame - ) - - except ValueError as e: - errMsg = "Bad frame returned by %s's preview renderer. " \ - "%s. New frame size was %s*%s; should be %s*%s." % ( - str(component), str(e).capitalize(), - newFrame.width, newFrame.height, - width, height - ) - log.critical(errMsg) - self.error.emit(errMsg) - break - except RuntimeError as e: - log.error(str(e)) - else: - self.frame = ImageQt(frame) - self.imageCreated.emit(QtGui.QImage(self.frame)) - - except Empty: - True diff --git a/src/preview_win.py b/src/preview_win.py deleted file mode 100644 index 40c19c6..0000000 --- a/src/preview_win.py +++ /dev/null @@ -1,62 +0,0 @@ -from PyQt5 import QtCore, QtGui, QtWidgets -import logging - - -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 - self.setFrameStyle(QtWidgets.QFrame.StyledPanel) - self.pixmap = QtGui.QPixmap(img) - - def paintEvent(self, event): - size = self.size() - painter = QtGui.QPainter(self) - point = QtCore.QPoint(0, 0) - scaledPix = self.pixmap.scaled( - size, - QtCore.Qt.KeepAspectRatio, - transformMode=QtCore.Qt.SmoothTransformation) - - # start painting the label from left upper corner - point.setX((size.width() - scaledPix.width())/2) - point.setY((size.height() - scaledPix.height())/2) - painter.drawPixmap(point, scaledPix) - - def changePixmap(self, img): - self.pixmap = QtGui.QPixmap(img) - self.repaint() - - def mousePressEvent(self, event): - if self.parent.encoding: - return - - i = self.parent.window.listWidget_componentList.currentRow() - 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' % ( - i, pos, butt)) - component.previewClickEvent( - pos, size, butt - ) - self.parent.core.updateComponent(i) - - @QtCore.pyqtSlot(str) - def threadError(self, msg): - self.parent.showMessage( - msg=msg, - icon='Critical', - parent=self - ) -- cgit v1.2.3 From 733c005eeaf5d3ff15e0f60d320f5c03472bad60 Mon Sep 17 00:00:00 2001 From: tassaron Date: Mon, 14 Aug 2017 18:41:45 -0400 Subject: undoable removeComponent action --- src/command.py | 1 + src/component.py | 3 +-- src/core.py | 36 ++++++++++++++++++++++++------------ src/gui/actions.py | 37 +++++++++++++++++++++++++++++++++++++ src/gui/mainwindow.py | 28 +++++++++++++++------------- src/gui/presetmanager.py | 7 +------ src/main.py | 4 ++-- 7 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 src/gui/actions.py (limited to 'src') diff --git a/src/command.py b/src/command.py index 18f7408..4116c5a 100644 --- a/src/command.py +++ b/src/command.py @@ -19,6 +19,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 diff --git a/src/component.py b/src/component.py index cf3085c..0e5144c 100644 --- a/src/component.py +++ b/src/component.py @@ -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) diff --git a/src/core.py b/src/core.py index 4dfb210..20b9c1d 100644 --- a/src/core.py +++ b/src/core.py @@ -64,31 +64,39 @@ 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 + if type(component) is int: + # create component using module index in self.modules + moduleIndex = int(component) + log.debug('Creating new component from module #%s' % moduleIndex) + component = self.modules[moduleIndex].Component( + moduleIndex, compPos, self + ) + # init component's widget for loading/saving presets + 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 moduleIndex > -1: + self.updateComponent(compPos) if hasattr(loader, 'insertComponent'): loader.insertComponent(compPos) @@ -156,6 +164,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, str(comp), 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. diff --git a/src/gui/actions.py b/src/gui/actions.py new file mode 100644 index 0000000..5cf64e1 --- /dev/null +++ b/src/gui/actions.py @@ -0,0 +1,37 @@ +''' + QCommand classes for every undoable user action performed in the MainWindow +''' +from PyQt5.QtWidgets import QUndoCommand + + +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): + stackedWidget = self.parent.window.stackedWidget + componentList = self.parent.window.listWidget_componentList + for index in self.selectedRows: + stackedWidget.removeWidget(self.parent.pages[index]) + componentList.takeItem(index) + self.parent.core.removeComponent(index) + self.parent.pages.pop(index) + self.parent.changeComponentWidget() + self.parent.drawPreview() + + 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() + diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index af6e190..2edb750 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -16,9 +16,10 @@ import time import logging from core import Core -import preview_thread -from preview_win import PreviewWindow -from presetmanager import PresetManager +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 @@ -43,9 +44,12 @@ class MainWindow(QtWidgets.QMainWindow): QtWidgets.QMainWindow.__init__(self) self.window = window self.core = Core() + Core.mode = 'GUI' log.debug( 'Main thread id: {}'.format(int(QtCore.QThread.currentThreadId()))) + self.undoStack = QtWidgets.QUndoStack(self) + # widgets of component settings self.pages = [] self.lastAutosave = time.time() @@ -62,7 +66,7 @@ class MainWindow(QtWidgets.QMainWindow): 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') @@ -298,6 +302,9 @@ 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) # Hotkeys for component list for inskey in ("Ctrl+T", QtCore.Qt.Key_Insert): @@ -685,15 +692,10 @@ class MainWindow(QtWidgets.QMainWindow): def removeComponent(self): componentList = self.window.listWidget_componentList - - 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() - self.drawPreview() + selected = componentList.selectedItems() + if selected: + action = RemoveComponent(self, selected) + self.undoStack.push(action) @disableWhenEncoding def moveComponent(self, change): diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index b1eeb34..1cc0887 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -302,7 +302,7 @@ class PresetManager(QtWidgets.QDialog): self.findPresets() self.drawPresetList() for i, comp in enumerate(self.core.selectedComponents): - if getPresetDir(comp) == path \ + if self.core.getPresetDir(comp) == path \ and comp.currentPreset == oldName: self.core.openPreset(newPath, i, newName) self.parent.updateComponentTitle(i, False) @@ -351,8 +351,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)) diff --git a/src/main.py b/src/main.py index 3a6fbe7..c1278da 100644 --- a/src/main.py +++ b/src/main.py @@ -35,11 +35,11 @@ def main(): log.debug("Finished creating command object") elif mode == 'GUI': - from mainwindow import MainWindow + from gui.mainwindow import MainWindow import atexit import signal - window = uic.loadUi(os.path.join(wd, "mainwindow.ui")) + window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui")) # window.adjustSize() desc = QtWidgets.QDesktopWidget() dpi = desc.physicalDpiX() -- cgit v1.2.3 From a1d7cbb984f2a6c2ea976daa8914a2c9845ee21c Mon Sep 17 00:00:00 2001 From: tassaron Date: Tue, 15 Aug 2017 22:20:25 -0400 Subject: undoable edits for normal component settings; TODO: merge small edits --- src/background.png | Bin 45367 -> 0 bytes src/component.py | 77 +++++++++++++++++++++++++++++++++++++++++------- src/components/color.py | 3 -- src/components/color.ui | 6 ++++ src/components/text.py | 4 --- src/components/text.ui | 6 ++++ src/core.py | 20 ++++++++----- src/gui/background.png | Bin 0 -> 45367 bytes src/gui/mainwindow.py | 34 +++++++++++++++------ src/toolkit/common.py | 12 ++++++++ src/toolkit/frame.py | 2 +- 11 files changed, 130 insertions(+), 34 deletions(-) delete mode 100644 src/background.png create mode 100644 src/gui/background.png (limited to 'src') diff --git a/src/background.png b/src/background.png deleted file mode 100644 index fb58593..0000000 Binary files a/src/background.png and /dev/null differ 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/gui/background.png b/src/gui/background.png new file mode 100644 index 0000000..fb58593 Binary files /dev/null and b/src/gui/background.png differ 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)) -- cgit v1.2.3 From f65ced2853a07b312516bcb729cc28509f524077 Mon Sep 17 00:00:00 2001 From: tassaron Date: Wed, 16 Aug 2017 20:44:37 -0400 Subject: merge consecutive actions on the same widget type --- src/component.py | 54 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/component.py b/src/component.py index dcba082..488b92a 100644 --- a/src/component.py +++ b/src/component.py @@ -317,10 +317,14 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): 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) + 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 _update(self): @@ -662,25 +666,55 @@ class ComponentError(RuntimeError): class ComponentUpdate(QtWidgets.QUndoCommand): '''Command object for making a component action undoable''' - def __init__(self, parent, oldWidgetVals, newWidgetVals): + def __init__(self, parent, oldWidgetVals, modifiedVals): super().__init__( 'Changed %s component #%s' % ( parent.name, parent.compPos ) ) self.parent = parent - self.oldWidgetVals = oldWidgetVals - self.newWidgetVals = newWidgetVals + self.oldWidgetVals = { + attr: val + for attr, val in oldWidgetVals.items() + if attr in modifiedVals + } + self.modifiedVals = modifiedVals + + # Determine if this update is mergeable + self.id_ = -1 + if len(self.modifiedVals) == 1: + attr, val = self.modifiedVals.popitem() + widget = self.parent._trackedWidgets[attr] + if type(widget) is QtWidgets.QLineEdit: + self.id_ = 10 + elif type(widget) is QtWidgets.QSpinBox \ + or type(widget) is QtWidgets.QDoubleSpinBox: + self.id_ = 20 + 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 redo(self): - self.parent.setAttrs(self.newWidgetVals) + self.parent.setAttrs(self.modifiedVals) 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] + for attr, val in self.oldWidgetVals.items(): + widget = self.parent._trackedWidgets[attr] if attr in self.parent._colorWidgets: val = '%s,%s,%s' % val setWidgetValue(widget, val) -- cgit v1.2.3 From ddb04f3a2fe6454a9c98bba39d07a12bd6a91b45 Mon Sep 17 00:00:00 2001 From: tassaron Date: Wed, 16 Aug 2017 21:02:53 -0400 Subject: undo merge IDs given per attr instead of widget type --- src/component.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'src') diff --git a/src/component.py b/src/component.py index 488b92a..b883627 100644 --- a/src/component.py +++ b/src/component.py @@ -684,12 +684,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand): self.id_ = -1 if len(self.modifiedVals) == 1: attr, val = self.modifiedVals.popitem() - widget = self.parent._trackedWidgets[attr] - if type(widget) is QtWidgets.QLineEdit: - self.id_ = 10 - elif type(widget) is QtWidgets.QSpinBox \ - or type(widget) is QtWidgets.QDoubleSpinBox: - self.id_ = 20 + self.id_ = sum([ord(letter) for letter in attr[:14]]) self.modifiedVals[attr] = val else: log.warning( -- cgit v1.2.3 From f66ec40ba6e9c4062d1e41894e0a88f713add96d Mon Sep 17 00:00:00 2001 From: tassaron Date: Wed, 16 Aug 2017 22:17:12 -0400 Subject: undoable component movement --- src/core.py | 1 + src/gui/actions.py | 39 +++++++++++++++++++++++++++++++++++++++ src/gui/mainwindow.py | 18 +++++------------- 3 files changed, 45 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/core.py b/src/core.py index cee0f56..14517b0 100644 --- a/src/core.py +++ b/src/core.py @@ -73,6 +73,7 @@ class Core: compPos = len(self.selectedComponents) if len(self.selectedComponents) > 50: return None + if type(component) is int: # create component using module index in self.modules moduleIndex = int(component) diff --git a/src/gui/actions.py b/src/gui/actions.py index 5cf64e1..5a0869d 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -35,3 +35,42 @@ class RemoveComponent(QUndoCommand): ) 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) diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 47111a0..26464a9 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -716,27 +716,19 @@ class MainWindow(QtWidgets.QMainWindow): 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): ''' -- cgit v1.2.3 From c06ca5cdcb603f1855cb0122fc2ab6d2473f3c24 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 17 Aug 2017 10:42:15 -0400 Subject: undoable add-comp & clear-preset actions --- src/component.py | 35 +++++++++++++++++++++++++++++++---- src/gui/actions.py | 45 ++++++++++++++++++++++++++++++++++++--------- src/gui/mainwindow.py | 31 ++++++++++++++++++++++++------- src/gui/presetmanager.py | 5 +++-- 4 files changed, 94 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/component.py b/src/component.py index b883627..f0a8c6b 100644 --- a/src/component.py +++ b/src/component.py @@ -99,6 +99,23 @@ class ComponentMetaclass(type(QtCore.QObject)): return func(self) return errorWrapper + def presetWrapper(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): + return func(self, *args) + return presetWrapper + def __new__(cls, name, parents, attrs): if 'ui' not in attrs: # Use module name as ui filename by default @@ -111,7 +128,7 @@ class ComponentMetaclass(type(QtCore.QObject)): 'names', # Class methods 'error', 'audio', 'properties', # Properties 'preFrameRender', 'previewRender', - 'frameRender', 'command', + 'frameRender', 'command', 'loadPreset' ) # Auto-decorate methods @@ -140,6 +157,9 @@ class ComponentMetaclass(type(QtCore.QObject)): if key == 'error': attrs[key] = cls.errorWrapper(attrs[key]) + if key == 'loadPreset': + attrs[key] = cls.presetWrapper(attrs[key]) + # Turn version string into a number try: if 'version' not in attrs: @@ -180,6 +200,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self.compPos = compPos self.core = core self.currentPreset = None + self.openingPreset = False self._trackedWidgets = {} self._presetNames = {} @@ -207,7 +228,10 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): preset = self.savePreset() except Exception as e: preset = '%s occurred while saving preset' % str(e) - return '%s\n%s\n%s' % ( + + return 'Component(%s, %s, Core)\n' \ + 'Name: %s v%s\n Preset: %s' % ( + self.moduleIndex, self.compPos, self.__class__.name, str(self.__class__.version), preset ) @@ -308,6 +332,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): A component update triggered by the user changing a widget value Call super() at the END when subclassing this. ''' + if self.openingPreset or not hasattr(self.parent, 'undoStack'): + return self._update() + oldWidgetVals = { attr: getattr(self, attr) for attr in self._trackedWidgets @@ -328,7 +355,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self.parent.undoStack.push(action) def _update(self): - '''An internal component update that is not undoable''' + '''A component update that is not undoable''' newWidgetVals = { attr: getWidgetValue(widget) @@ -684,7 +711,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand): self.id_ = -1 if len(self.modifiedVals) == 1: attr, val = self.modifiedVals.popitem() - self.id_ = sum([ord(letter) for letter in attr[:14]]) + self.id_ = sum([ord(letter) for letter in attr[-14:]]) self.modifiedVals[attr] = val else: log.warning( diff --git a/src/gui/actions.py b/src/gui/actions.py index 5a0869d..cdd3dfa 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -4,6 +4,23 @@ from PyQt5.QtWidgets import QUndoCommand +class AddComponent(QUndoCommand): + def __init__(self, parent, compI, moduleI): + super().__init__( + "New %s component" % + parent.core.modules[moduleI].Component.name + ) + self.parent = parent + self.moduleI = moduleI + self.compI = compI + + def redo(self): + self.parent.core.insertComponent(self.compI, self.moduleI, self.parent) + + def undo(self): + self.parent._removeComponent(self.compI) + + class RemoveComponent(QUndoCommand): def __init__(self, parent, selectedRows): super().__init__('Remove component') @@ -17,15 +34,7 @@ class RemoveComponent(QUndoCommand): ] def redo(self): - stackedWidget = self.parent.window.stackedWidget - componentList = self.parent.window.listWidget_componentList - for index in self.selectedRows: - stackedWidget.removeWidget(self.parent.pages[index]) - componentList.takeItem(index) - self.parent.core.removeComponent(index) - self.parent.pages.pop(index) - self.parent.changeComponentWidget() - self.parent.drawPreview() + self.parent._removeComponent(self.selectedRows[0]) def undo(self): componentList = self.parent.window.listWidget_componentList @@ -74,3 +83,21 @@ class MoveComponent(QUndoCommand): def undo(self): self.do(self.newRow, self.row) + + +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) diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 26464a9..8000b3b 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -20,7 +20,9 @@ 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 +from toolkit import ( + disableWhenEncoding, disableWhenOpeningProject, checkOutput, blockSignals +) log = logging.getLogger('AVP.MainWindow') @@ -165,7 +167,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) @@ -686,7 +688,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 @@ -712,6 +720,16 @@ class MainWindow(QtWidgets.QMainWindow): action = RemoveComponent(self, selected) self.undoStack.push(action) + 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''' @@ -786,9 +804,8 @@ 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() @@ -938,8 +955,8 @@ 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 ) ) diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index 1cc0887..79ec539 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -8,6 +8,7 @@ import os from toolkit import badName from core import Core +from gui.actions import * class PresetManager(QtWidgets.QDialog): @@ -130,8 +131,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''' -- cgit v1.2.3 From 43ea3bfd733f63e5b22d2f1eb7ef7c8ad2cc97c9 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 17 Aug 2017 15:12:22 -0400 Subject: component updateWrapper and more obvious method names --- src/component.py | 208 +++++++++++++++++++++++++++++++------------------------ src/core.py | 7 +- 2 files changed, 122 insertions(+), 93 deletions(-) (limited to 'src') diff --git a/src/component.py b/src/component.py index f0a8c6b..1fe9237 100644 --- a/src/component.py +++ b/src/component.py @@ -99,7 +99,7 @@ class ComponentMetaclass(type(QtCore.QObject)): return func(self) return errorWrapper - def presetWrapper(func): + def loadPresetWrapper(func): '''Wraps loadPreset to handle the self.openingPreset boolean''' class openingPreset: def __init__(self, comp): @@ -116,6 +116,36 @@ class ComponentMetaclass(type(QtCore.QObject)): return func(self, *args) return presetWrapper + def updateWrapper(func): + ''' + For undoable updates triggered by the user, + call _userUpdate() after the subclass's update() method. + For non-user updates, call _autoUpdate() + ''' + class wrap: + def __init__(self, comp, auto): + self.comp = comp + self.auto = auto + + def __enter__(self): + pass + + def __exit__(self, *args): + if self.auto or self.comp.openingPreset \ + or not hasattr(self.comp.parent, 'undoStack'): + self.comp._autoUpdate() + else: + self.comp._userUpdate() + + def updateWrapper(self, **kwargs): + auto = False + if 'auto' in kwargs: + auto = kwargs['auto'] + + with wrap(self, auto): + return func(self) + return updateWrapper + def __new__(cls, name, parents, attrs): if 'ui' not in attrs: # Use module name as ui filename by default @@ -128,37 +158,32 @@ class ComponentMetaclass(type(QtCore.QObject)): 'names', # Class methods 'error', 'audio', 'properties', # Properties 'preFrameRender', 'previewRender', - 'frameRender', 'command', 'loadPreset' + 'frameRender', 'command', + 'loadPreset', 'update' ) # 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 in ('previewRender', 'frameRender'): 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]) - - if key == 'loadPreset': - attrs[key] = cls.presetWrapper(attrs[key]) + elif key == 'loadPreset': + attrs[key] = cls.loadPresetWrapper(attrs[key]) + elif key == 'update': + attrs[key] = cls.updateWrapper(attrs[key]) # Turn version string into a number try: @@ -229,10 +254,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): except Exception as e: preset = '%s occurred while saving preset' % str(e) - return 'Component(%s, %s, Core)\n' \ - 'Name: %s v%s\n Preset: %s' % ( - self.moduleIndex, self.compPos, - self.__class__.name, str(self.__class__.version), preset + return ( + 'Component(%s, %s, Core)\n' + 'Name: %s v%s\n Preset: %s' % ( + self.moduleIndex, self.compPos, + self.__class__.name, str(self.__class__.version), preset + ) ) # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ @@ -329,74 +356,10 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): def update(self): ''' - A component update triggered by the user changing a widget value - Call super() at the END when subclassing 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. ''' - if self.openingPreset or not hasattr(self.parent, 'undoStack'): - return self._update() - - 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() - } - 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 _update(self): - '''A 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 - 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) - - elif attr in self._relativeWidgets: - # Relative widgets: number scales to fit export resolution - self.updateRelativeWidget(attr) - setattr(self, attr, val) - - else: - # Normal tracked widget - setattr(self, attr, val) - - def 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): ''' @@ -464,6 +427,69 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # "Private" Methods # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ + def _userUpdate(self): + '''An undoable component update triggered by the user''' + 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() + } + 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): + '''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 + 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) + + elif attr in self._relativeWidgets: + # Relative widgets: number scales to fit export resolution + self.updateRelativeWidget(attr) + setattr(self, attr, val) + + else: + # Normal tracked widget + setattr(self, attr, val) + + def _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): ''' @@ -730,7 +756,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand): def redo(self): self.parent.setAttrs(self.modifiedVals) - self.parent.sendUpdateSignal() + self.parent._sendUpdateSignal() def undo(self): self.parent.setAttrs(self.oldWidgetVals) @@ -740,4 +766,4 @@ class ComponentUpdate(QtWidgets.QUndoCommand): if attr in self.parent._colorWidgets: val = '%s,%s,%s' % val setWidgetValue(widget, val) - self.parent.sendUpdateSignal() + self.parent._sendUpdateSignal() diff --git a/src/core.py b/src/core.py index 14517b0..7609698 100644 --- a/src/core.py +++ b/src/core.py @@ -83,6 +83,8 @@ class Core: ) # init component's widget for loading/saving presets component.widget(loader) + # use autoUpdate() method before update() this 1 time to set attrs + component._autoUpdate() else: moduleIndex = -1 log.debug( @@ -118,8 +120,9 @@ 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: -- cgit v1.2.3 From 87e762a8aa3fa97a3d43a18c59098b287bb95506 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 17 Aug 2017 20:12:46 -0400 Subject: undoable preset open, rename, and delete' --- src/core.py | 4 +-- src/gui/actions.py | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ src/gui/presetmanager.py | 49 ++++++++++++++++-------------- 3 files changed, 107 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/core.py b/src/core.py index 7609698..d9499f7 100644 --- a/src/core.py +++ b/src/core.py @@ -83,7 +83,7 @@ class Core: ) # init component's widget for loading/saving presets component.widget(loader) - # use autoUpdate() method before update() this 1 time to set attrs + # use autoUpdate() method before update() this 1 time to set attrs component._autoUpdate() else: moduleIndex = -1 @@ -169,7 +169,7 @@ class Core: def getPresetDir(self, comp): '''Get the preset subdir for a particular version of a component''' - return os.path.join(Core.presetDir, str(comp), str(comp.version)) + 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 diff --git a/src/gui/actions.py b/src/gui/actions.py index cdd3dfa..0fe97f2 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -2,7 +2,14 @@ QCommand classes for every undoable user action performed in the MainWindow ''' from PyQt5.QtWidgets import QUndoCommand +import os +from core import Core + + +# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ +# COMPONENT ACTIONS +# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ class AddComponent(QUndoCommand): def __init__(self, parent, compI, moduleI): @@ -85,6 +92,10 @@ class MoveComponent(QUndoCommand): self.do(self.newRow, self.row) +# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ +# PRESET ACTIONS +# =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ + class ClearPreset(QUndoCommand): def __init__(self, parent, compI): super().__init__("Clear preset") @@ -101,3 +112,71 @@ class ClearPreset(QUndoCommand): 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'] = str(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() diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index 79ec539..dce5333 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -197,11 +197,15 @@ 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 + action = OpenPreset(self, presetName, index) + self.parent.undoStack.push(action) + + def _openPreset(self, presetName, index): + selectedComponents = self.core.selectedComponents + componentName = str(selectedComponents[index]).strip() version = selectedComponents[index].version dirname = os.path.join(self.presetDir, componentName, str(version)) @@ -225,16 +229,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( @@ -271,7 +269,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: @@ -294,22 +291,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 self.core.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", -- cgit v1.2.3 From c07f2426ceeada205fdacbfba66329179a74a1dc Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 19 Aug 2017 18:32:12 -0400 Subject: fixed issues with undoing relative widgets --- src/component.py | 198 +++++++++++++++++++++++++++++++++------------ src/components/color.py | 2 - src/components/image.py | 2 - src/components/life.py | 1 - src/components/sound.py | 1 - src/components/spectrum.py | 4 +- src/components/text.py | 1 - src/components/video.py | 2 - src/components/waveform.py | 2 +- src/core.py | 11 +-- src/gui/actions.py | 11 ++- src/gui/mainwindow.py | 4 +- src/gui/presetmanager.py | 4 + src/gui/preview_thread.py | 2 +- src/gui/preview_win.py | 2 +- src/main.py | 2 +- src/toolkit/common.py | 47 +++++++++-- 17 files changed, 215 insertions(+), 81 deletions(-) (limited to 'src') diff --git a/src/component.py b/src/component.py index 1fe9237..ba86422 100644 --- a/src/component.py +++ b/src/component.py @@ -9,6 +9,7 @@ import sys import math import time import logging +from copy import copy from toolkit.frame import BlankFrame from toolkit import ( @@ -113,14 +114,20 @@ class ComponentMetaclass(type(QtCore.QObject)): def presetWrapper(self, *args): with openingPreset(self): - return func(self, *args) + try: + return func(self, *args) + except Exception: + try: + raise ComponentError(self, 'preset loader') + except ComponentError: + return return presetWrapper def updateWrapper(func): ''' - For undoable updates triggered by the user, - call _userUpdate() after the subclass's update() method. - For non-user updates, call _autoUpdate() + Calls _preUpdate before every subclass update(). + Afterwards, for non-user updates, calls _autoUpdate(). + For undoable updates triggered by the user, calls _userUpdate() ''' class wrap: def __init__(self, comp, auto): @@ -128,24 +135,57 @@ class ComponentMetaclass(type(QtCore.QObject)): self.auto = auto def __enter__(self): - pass + self.comp._preUpdate() def __exit__(self, *args): if self.auto or self.comp.openingPreset \ or not hasattr(self.comp.parent, 'undoStack'): + log.verbose('Automatic update') self.comp._autoUpdate() else: + log.verbose('User update') self.comp._userUpdate() def updateWrapper(self, **kwargs): - auto = False - if 'auto' in kwargs: - auto = kwargs['auto'] - + auto = kwargs['auto'] if 'auto' in kwargs else False with wrap(self, auto): - return func(self) + try: + return func(self) + except Exception: + try: + raise ComponentError(self, 'update method') + except ComponentError: + return return updateWrapper + def widgetWrapper(func): + '''Connects all widgets to update method after the subclass's method''' + class wrap: + def __init__(self, comp): + self.comp = comp + + def __enter__(self): + pass + + def __exit__(self, *args): + for widgetList in self.comp._allWidgets.values(): + for widget in widgetList: + log.verbose('Connecting %s' % str( + widget.__class__.__name__)) + connectWidget(widget, self.comp.update) + + def widgetWrapper(self, *args, **kwargs): + auto = kwargs['auto'] if 'auto' in kwargs else False + with wrap(self): + try: + return func(self, *args, **kwargs) + except Exception: + try: + raise ComponentError(self, 'widget creation') + except ComponentError: + return + return widgetWrapper + def __new__(cls, name, parents, attrs): if 'ui' not in attrs: # Use module name as ui filename by default @@ -153,13 +193,12 @@ class ComponentMetaclass(type(QtCore.QObject)): attrs['__module__'].split('.')[-1] )[0] - # if parents[0] == QtCore.QObject: else: decorate = ( 'names', # Class methods 'error', 'audio', 'properties', # Properties 'preFrameRender', 'previewRender', 'frameRender', 'command', - 'loadPreset', 'update' + 'loadPreset', 'update', 'widget', ) # Auto-decorate methods @@ -184,6 +223,8 @@ class ComponentMetaclass(type(QtCore.QObject)): attrs[key] = cls.loadPresetWrapper(attrs[key]) elif key == 'update': attrs[key] = cls.updateWrapper(attrs[key]) + elif key == 'widget' and parents[0] != QtCore.QObject: + attrs[key] = cls.widgetWrapper(attrs[key]) # Turn version string into a number try: @@ -224,23 +265,28 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self.moduleIndex = moduleIndex self.compPos = compPos self.core = core - self.currentPreset = None - self.openingPreset = False + # STATUS VARIABLES + self.currentPreset = None + self._allWidgets = {} self._trackedWidgets = {} self._presetNames = {} self._commandArgs = {} self._colorWidgets = {} self._colorFuncs = {} self._relativeWidgets = {} - # pixel values stored as floats + # Pixel values stored as floats self._relativeValues = {} - # maximum values of spinBoxes at 1080p (Core.resolutions[0]) + # Maximum values of spinBoxes at 1080p (Core.resolutions[0]) self._relativeMaximums = {} + # LOCKING VARIABLES + self.openingPreset = False self._lockedProperties = None self._lockedError = None self._lockedSize = None + # If set to a dict, values are used as basis to update relative widgets + self.oldAttrs = None # Stop lengthy processes in response to this variable self.canceled = False @@ -338,21 +384,21 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' self.parent = parent self.settings = parent.settings + log.verbose('Creating UI for %s #%s\'s widget' % ( + self.name, self.compPos + )) self.page = self.loadUi(self.__class__.ui) - # Connect widget signals - widgets = { + # Find all normal widgets which will be connected after subclass method + self._allWidgets = { 'lineEdit': self.page.findChildren(QtWidgets.QLineEdit), 'checkBox': self.page.findChildren(QtWidgets.QCheckBox), 'spinBox': self.page.findChildren(QtWidgets.QSpinBox), 'comboBox': self.page.findChildren(QtWidgets.QComboBox), } - widgets['spinBox'].extend( + self._allWidgets['spinBox'].extend( self.page.findChildren(QtWidgets.QDoubleSpinBox) ) - for widgetList in widgets.values(): - for widget in widgetList: - connectWidget(widget, self.update) def update(self): ''' @@ -427,10 +473,15 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ # "Private" Methods # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ + def _preUpdate(self): + '''Happens before subclass update()''' + for attr in self._relativeWidgets: + self.updateRelativeWidget(attr) + def _userUpdate(self): - '''An undoable component update triggered by the user''' + '''Happens after subclass update() for an undoable update by user.''' oldWidgetVals = { - attr: getattr(self, attr) + attr: copy(getattr(self, attr)) for attr in self._trackedWidgets } newWidgetVals = { @@ -443,13 +494,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): for attr, val in newWidgetVals.items() if val != oldWidgetVals[attr] } - if modifiedWidgets: action = ComponentUpdate(self, oldWidgetVals, modifiedWidgets) self.parent.undoStack.push(action) def _autoUpdate(self): - '''An internal component update that is not undoable''' + '''Happens after subclass update() for an internal component update.''' newWidgetVals = { attr: getWidgetValue(widget) for attr, widget in self._trackedWidgets.items() @@ -459,12 +509,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): def setAttrs(self, attrDict): ''' - Sets attrs (linked to trackedWidgets) in this preset to + Sets attrs (linked to trackedWidgets) in this component to the values in the attrDict. Mutates certain widget values if needed ''' for attr, val in attrDict.items(): if attr in self._colorWidgets: - # Color Widgets: text stored as tuple & update the button color + # Color Widgets must have a tuple & have a button to update if type(val) is tuple: rgbTuple = val else: @@ -475,15 +525,25 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self._colorWidgets[attr].setStyleSheet(btnStyle) setattr(self, attr, rgbTuple) - elif attr in self._relativeWidgets: - # Relative widgets: number scales to fit export resolution - self.updateRelativeWidget(attr) - setattr(self, attr, val) - else: # Normal tracked widget setattr(self, attr, val) + def setWidgetValues(self, attrDict): + ''' + Sets widgets defined by keys in trackedWidgets in this preset to + the values in the attrDict. + ''' + affectedWidgets = [ + self._trackedWidgets[attr] for attr in attrDict + ] + with blockSignals(affectedWidgets): + for attr, val in attrDict.items(): + widget = self._trackedWidgets[attr] + if attr in self._colorWidgets: + val = '%s,%s,%s' % val + setWidgetValue(widget, val) + def _sendUpdateSignal(self): if not self.core.openingProject: self.parent.drawPreview() @@ -499,6 +559,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): Optional args: 'presetNames': preset variable names to replace attr names 'commandArgs': arg keywords that differ from attr names + 'colorWidgets': identify attr as RGB tuple & update button CSS + 'relativeWidgets': change value proportionally to resolution NOTE: Any kwarg key set to None will selectively disable tracking. ''' @@ -542,6 +604,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self._relativeMaximums[attr] = \ self._trackedWidgets[attr].maximum() self.updateRelativeWidgetMaximum(attr) + self._preUpdate() + self._autoUpdate() def pickColor(self, textWidget, button): '''Use color picker to get color input from the user.''' @@ -627,12 +691,28 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): def setRelativeWidget(self, attr, floatVal): '''Set a relative widget using a float''' pixelVal = self.pixelValForAttr(attr, floatVal) - self._trackedWidgets[attr].setValue(pixelVal) + with blockSignals(self._allWidgets): + self._trackedWidgets[attr].setValue(pixelVal) + self.update(auto=True) + + def getOldAttr(self, attr): + ''' + Returns previous state of this attr. Used to determine whether + a relative widget must be updated. Required because undoing/redoing + can make determining the 'previous' value tricky. + ''' + if self.oldAttrs is not None: + log.verbose('Using nonstandard oldAttr for %s' % attr) + return self.oldAttrs[attr] + else: + return getattr(self, attr) def updateRelativeWidget(self, attr): + '''Called by _preUpdate() for each relativeWidget before each update''' try: - oldUserValue = getattr(self, attr) - except AttributeError: + oldUserValue = self.getOldAttr(attr) + except (AttributeError, KeyError): + log.info('Using visible values as basis for relative widgets') oldUserValue = self._trackedWidgets[attr].value() newUserValue = self._trackedWidgets[attr].value() newRelativeVal = self.floatValForAttr(attr, newUserValue) @@ -645,11 +725,10 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): # means the pixel value needs to be updated log.debug('Updating %s #%s\'s relative widget: %s' % ( self.name, self.compPos, attr)) - self._trackedWidgets[attr].blockSignals(True) - self.updateRelativeWidgetMaximum(attr) - pixelVal = self.pixelValForAttr(attr, oldRelativeVal) - self._trackedWidgets[attr].setValue(pixelVal) - self._trackedWidgets[attr].blockSignals(False) + with blockSignals(self._trackedWidgets[attr]): + self.updateRelativeWidgetMaximum(attr) + pixelVal = self.pixelValForAttr(attr, oldRelativeVal) + self._trackedWidgets[attr].setValue(pixelVal) if attr not in self._relativeValues \ or oldUserValue != newUserValue: @@ -725,14 +804,22 @@ class ComponentUpdate(QtWidgets.QUndoCommand): parent.name, parent.compPos ) ) + self.undone = False self.parent = parent self.oldWidgetVals = { - attr: val + attr: copy(val) for attr, val in oldWidgetVals.items() if attr in modifiedVals } self.modifiedVals = modifiedVals + # Because relative widgets change themselves every update based on + # their previous value, we must store ALL their values in case of undo + self.redoRelativeWidgetVals = { + attr: copy(getattr(self.parent, attr)) + for attr in self.parent._relativeWidgets + } + # Determine if this update is mergeable self.id_ = -1 if len(self.modifiedVals) == 1: @@ -755,15 +842,26 @@ class ComponentUpdate(QtWidgets.QUndoCommand): return True def redo(self): + if self.undone: + log.debug('Redoing component update') + self.parent.setWidgetValues(self.modifiedVals) self.parent.setAttrs(self.modifiedVals) - self.parent._sendUpdateSignal() + if self.undone: + self.parent.oldAttrs = self.redoRelativeWidgetVals + self.parent.update(auto=True) + self.parent.oldAttrs = None + else: + self.undoRelativeWidgetVals = { + attr: copy(getattr(self.parent, attr)) + for attr in self.parent._relativeWidgets + } + self.parent._sendUpdateSignal() def undo(self): + log.debug('Undoing component update') + self.undone = True + self.parent.oldAttrs = self.undoRelativeWidgetVals + self.parent.setWidgetValues(self.oldWidgetVals) self.parent.setAttrs(self.oldWidgetVals) - with blockSignals(self.parent): - for attr, val in self.oldWidgetVals.items(): - widget = self.parent._trackedWidgets[attr] - if attr in self.parent._colorWidgets: - val = '%s,%s,%s' % val - setWidgetValue(widget, val) - self.parent._sendUpdateSignal() + self.parent.update(auto=True) + self.parent.oldAttrs = None diff --git a/src/components/color.py b/src/components/color.py index d09cee8..a55aa10 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -82,8 +82,6 @@ class Component(Component): self.page.pushButton_color2.setEnabled(False) self.page.fillWidget.setCurrentIndex(fillType) - super().update() - def previewRender(self): return self.drawFrame(self.width, self.height) diff --git a/src/components/image.py b/src/components/image.py index 63bee1a..c57b69c 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -84,7 +84,6 @@ class Component(Component): if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) - self.update() def command(self, arg): if '=' in arg: @@ -123,4 +122,3 @@ class Component(Component): else: scaleBox.setVisible(True) stretchScaleBox.setVisible(False) - super().update() diff --git a/src/components/life.py b/src/components/life.py index 2383d30..76d2c5f 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -53,7 +53,6 @@ class Component(Component): if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) - self.update() def shiftGrid(self, d): def newGrid(Xchange, Ychange): diff --git a/src/components/sound.py b/src/components/sound.py index 26ecf93..b86f40c 100644 --- a/src/components/sound.py +++ b/src/components/sound.py @@ -53,7 +53,6 @@ class Component(Component): if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_sound.setText(filename) - self.update() def commandHelp(self): print('Path to audio file:\n path=/filepath/to/sound.ogg') diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 89130a2..2b98dc2 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -76,8 +76,6 @@ class Component(Component): else: self.page.checkBox_mono.setEnabled(True) - super().update() - def previewRender(self): changedSize = self.updateChunksize() if not changedSize \ @@ -138,7 +136,7 @@ class Component(Component): '-r', self.settings.value("outputFrameRate"), '-ss', "{0:.3f}".format(startPt), '-i', - os.path.join(self.core.wd, 'background.png') + self.core.junkStream if genericPreview else inputFile, '-f', 'image2pipe', '-pix_fmt', 'rgba', diff --git a/src/components/text.py b/src/components/text.py index d3afd5c..92f0599 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -68,7 +68,6 @@ class Component(Component): self.page.spinBox_shadY.setHidden(True) self.page.label_shadBlur.setHidden(True) self.page.spinBox_shadBlur.setHidden(True) - super().update() def centerXY(self): self.setRelativeWidget('xPosition', 0.5) diff --git a/src/components/video.py b/src/components/video.py index a189f60..9c0d608 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -52,7 +52,6 @@ class Component(Component): else: self.page.label_volume.setEnabled(False) self.page.spinBox_volume.setEnabled(False) - super().update() def previewRender(self): self.updateChunksize() @@ -119,7 +118,6 @@ class Component(Component): if filename: self.settings.setValue("componentDir", os.path.dirname(filename)) self.page.lineEdit_video.setText(filename) - self.update() def getPreviewFrame(self, width, height): if not self.videoPath or not os.path.exists(self.videoPath): diff --git a/src/components/waveform.py b/src/components/waveform.py index 0743e55..5c02bbf 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -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', diff --git a/src/core.py b/src/core.py index d9499f7..169716c 100644 --- a/src/core.py +++ b/src/core.py @@ -13,7 +13,7 @@ import toolkit log = logging.getLogger('AVP.Core') -STDOUT_LOGLVL = logging.WARNING +STDOUT_LOGLVL = logging.VERBOSE FILE_LOGLVL = logging.DEBUG @@ -81,10 +81,7 @@ class Core: component = self.modules[moduleIndex].Component( moduleIndex, compPos, self ) - # init component's widget for loading/saving presets component.widget(loader) - # use autoUpdate() method before update() this 1 time to set attrs - component._autoUpdate() else: moduleIndex = -1 log.debug( @@ -186,9 +183,8 @@ class Core: if hasattr(loader, 'window'): for widget, value in data['WindowFields']: widget = eval('loader.window.%s' % widget) - widget.blockSignals(True) - toolkit.setWidgetValue(widget, value) - widget.blockSignals(False) + with toolkit.blockSignals(widget): + toolkit.setWidgetValue(widget, value) for key, value in data['Settings']: Core.settings.setValue(key, value) @@ -474,6 +470,7 @@ class Core: 'logDir': os.path.join(dataDir, 'log'), 'presetDir': os.path.join(dataDir, 'presets'), 'componentsPath': os.path.join(wd, 'components'), + 'junkStream': os.path.join(wd, 'gui', 'background.png'), 'encoderOptions': encoderOptions, 'resolutions': [ '1920x1080', diff --git a/src/gui/actions.py b/src/gui/actions.py index 0fe97f2..1444569 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -20,11 +20,20 @@ class AddComponent(QUndoCommand): self.parent = parent self.moduleI = moduleI self.compI = compI + self.comp = None def redo(self): - self.parent.core.insertComponent(self.compI, self.moduleI, self.parent) + if self.comp is None: + self.parent.core.insertComponent( + self.compI, self.moduleI, self.parent) + else: + # inserting previously-created component + self.parent.core.insertComponent( + self.compI, self.comp, self.parent) + def undo(self): + self.comp = self.parent.core.selectedComponents[self.compI] self.parent._removeComponent(self.compI) diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 8000b3b..76c53af 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -25,7 +25,7 @@ from toolkit import ( ) -log = logging.getLogger('AVP.MainWindow') +log = logging.getLogger('AVP.Gui.MainWindow') class MainWindow(QtWidgets.QMainWindow): @@ -76,7 +76,7 @@ class MainWindow(QtWidgets.QMainWindow): # Create the preview window and its thread, queues, and timers log.debug('Creating preview window') self.previewWindow = PreviewWindow(self, os.path.join( - Core.wd, "background.png")) + Core.wd, 'gui', "background.png")) window.verticalLayout_previewWrapper.addWidget(self.previewWindow) log.debug('Starting preview thread') diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index dce5333..befa7cd 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -5,12 +5,16 @@ from PyQt5 import QtCore, QtWidgets import string import os +import logging from toolkit import badName from core import Core from gui.actions import * +log = logging.getLogger('AVP.Gui.PresetManager') + + class PresetManager(QtWidgets.QDialog): def __init__(self, window, parent): super().__init__(parent.window) diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py index 9615884..33a9e7a 100644 --- a/src/gui/preview_thread.py +++ b/src/gui/preview_thread.py @@ -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): diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py index 40c19c6..c6b9a32 100644 --- a/src/gui/preview_win.py +++ b/src/gui/preview_win.py @@ -7,7 +7,7 @@ class PreviewWindow(QtWidgets.QLabel): Paints the preview QLabel in MainWindow and maintains the aspect ratio when the window is resized. ''' - log = logging.getLogger('AVP.PreviewWindow') + log = logging.getLogger('AVP.Gui.PreviewWindow') def __init__(self, parent, img): super(PreviewWindow, self).__init__() diff --git a/src/main.py b/src/main.py index c1278da..6d18af3 100644 --- a/src/main.py +++ b/src/main.py @@ -6,7 +6,7 @@ import logging from __init__ import wd -log = logging.getLogger('AVP.Entrypoint') +log = logging.getLogger('AVP.Main') def main(): diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 51ad023..74143e8 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -6,19 +6,53 @@ import string import os import sys import subprocess +import logging +from copy import copy from collections import OrderedDict +log = logging.getLogger('AVP.Toolkit.Common') + + class blockSignals: - '''A context manager to temporarily block a Qt widget from updating''' - def __init__(self, widget): - self.widget = widget + ''' + Context manager to temporarily block list of QtWidgets from updating, + and guarantee restoring the previous state afterwards. + ''' + def __init__(self, widgets): + if type(widgets) is dict: + self.widgets = concatDictVals(widgets) + else: + self.widgets = ( + widgets if hasattr(widgets, '__iter__') + else [widgets] + ) def __enter__(self): - self.widget.blockSignals(True) + log.verbose('Blocking signals for %s' % ", ".join([ + str(w.__class__.__name__) for w in self.widgets + ])) + self.oldStates = [w.signalsBlocked() for w in self.widgets] + for w in self.widgets: + w.blockSignals(True) def __exit__(self, *args): - self.widget.blockSignals(False) + log.verbose('Resetting blockSignals to %s' % sum(self.oldStates)) + for w, state in zip(self.widgets, self.oldStates): + w.blockSignals(state) + + +def concatDictVals(d): + '''Concatenates all values in given dict into one list.''' + key, value = d.popitem() + d[key] = value + final = copy(value) + if type(final) is not list: + final = [final] + final.extend([val for val in d.values()]) + else: + value.extend([item for val in d.values() for item in val]) + return final def badName(name): @@ -119,12 +153,14 @@ def connectWidget(widget, func): elif type(widget) == QtWidgets.QComboBox: widget.currentIndexChanged.connect(func) else: + log.warning('Failed to connect %s ' % str(widget.__class__.__name__)) return False return True def setWidgetValue(widget, val): '''Generic setValue method for use with any typical QtWidget''' + log.verbose('Setting %s to %s' % (str(widget.__class__.__name__), val)) if type(widget) == QtWidgets.QLineEdit: widget.setText(val) elif type(widget) == QtWidgets.QSpinBox \ @@ -135,6 +171,7 @@ def setWidgetValue(widget, val): elif type(widget) == QtWidgets.QComboBox: widget.setCurrentIndex(val) else: + log.warning('Failed to set %s ' % str(widget.__class__.__name__)) return False return True -- cgit v1.2.3 From d4b63e4d4612db262424fe10c83f8eaa4f741f24 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 19 Aug 2017 20:45:44 -0400 Subject: remove % from log calls --- src/component.py | 32 +++++++++++++++++--------------- src/core.py | 19 ++++++++++--------- src/gui/actions.py | 3 ++- src/gui/mainwindow.py | 26 +++++++++++++++++++++----- src/gui/presetmanager.py | 2 +- src/toolkit/common.py | 16 ++++++++++------ src/toolkit/ffmpeg.py | 2 +- src/video_thread.py | 7 ++++--- 8 files changed, 66 insertions(+), 41 deletions(-) (limited to 'src') diff --git a/src/component.py b/src/component.py index ba86422..992a82e 100644 --- a/src/component.py +++ b/src/component.py @@ -40,11 +40,11 @@ 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%s frame %s###', 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: @@ -170,7 +170,7 @@ class ComponentMetaclass(type(QtCore.QObject)): def __exit__(self, *args): for widgetList in self.comp._allWidgets.values(): for widget in widgetList: - log.verbose('Connecting %s' % str( + log.verbose('Connecting %s', str( widget.__class__.__name__)) connectWidget(widget, self.comp.update) @@ -230,16 +230,18 @@ class ComponentMetaclass(type(QtCore.QObject)): 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) @@ -384,9 +386,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' self.parent = parent self.settings = parent.settings - log.verbose('Creating UI for %s #%s\'s widget' % ( + log.verbose('Creating UI for %s #%s\'s widget', self.name, self.compPos - )) + ) self.page = self.loadUi(self.__class__.ui) # Find all normal widgets which will be connected after subclass method @@ -702,7 +704,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): can make determining the 'previous' value tricky. ''' if self.oldAttrs is not None: - log.verbose('Using nonstandard oldAttr for %s' % attr) + log.verbose('Using nonstandard oldAttr for %s', attr) return self.oldAttrs[attr] else: return getattr(self, attr) @@ -723,8 +725,9 @@ 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)) + log.debug( + 'Updating %s #%s\'s relative widget: %s', + self.name, self.compPos, attr) with blockSignals(self._trackedWidgets[attr]): self.updateRelativeWidgetMaximum(attr) pixelVal = self.pixelValForAttr(attr, oldRelativeVal) @@ -828,9 +831,8 @@ class ComponentUpdate(QtWidgets.QUndoCommand): self.modifiedVals[attr] = val else: log.warning( - '%s component settings changed at once. (%s)' % ( - len(self.modifiedVals), repr(self.modifiedVals) - ) + '%s component settings changed at once. (%s)', + len(self.modifiedVals), repr(self.modifiedVals) ) def id(self): diff --git a/src/core.py b/src/core.py index 169716c..bfb8272 100644 --- a/src/core.py +++ b/src/core.py @@ -77,7 +77,8 @@ class Core: if type(component) is int: # create component using module index in self.modules moduleIndex = int(component) - log.debug('Creating new component from module #%s' % moduleIndex) + log.debug( + 'Creating new component from module #%s', str(moduleIndex)) component = self.modules[moduleIndex].Component( moduleIndex, compPos, self ) @@ -85,7 +86,7 @@ class Core: else: moduleIndex = -1 log.debug( - 'Inserting previously-created %s component' % component.name) + 'Inserting previously-created %s component', component.name) component._error.connect( loader.videoThreadError @@ -117,8 +118,9 @@ class Core: self.componentListChanged() def updateComponent(self, i): - log.debug('Auto-updating %s #%s' % ( - self.selectedComponents[i], str(i))) + log.debug( + 'Auto-updating %s #%s', + self.selectedComponents[i], str(i)) self.selectedComponents[i].update(auto=True) def moduleIndexFor(self, compName): @@ -146,9 +148,8 @@ class Core: ) except KeyError as e: log.warning( - '%s #%s\'s preset is missing value: %s' % ( - comp.name, str(compIndex), str(e) - ) + '%s #%s\'s preset is missing value: %s', + comp.name, str(compIndex), str(e) ) self.savedPresets[presetName] = dict(saveValueStore) @@ -266,7 +267,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', @@ -385,7 +386,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', diff --git a/src/gui/actions.py b/src/gui/actions.py index 1444569..f101bd7 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -3,6 +3,7 @@ ''' from PyQt5.QtWidgets import QUndoCommand import os +from copy import copy from core import Core @@ -132,7 +133,7 @@ class OpenPreset(QUndoCommand): comp = self.parent.core.selectedComponents[compI] self.store = comp.savePreset() - self.store['preset'] = str(comp.currentPreset) + self.store['preset'] = copy(comp.currentPreset) def redo(self): self.parent._openPreset(self.presetName, self.compI) diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 76c53af..833d2d1 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -387,30 +387,46 @@ class MainWindow(QtWidgets.QMainWindow): @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]) + if modified: + log.verbose( + 'Differing values between presets: %s', + ", ".join([ + '%s: %s' % item for item in presetStore.items() + if val != self.core.savedPresets[name][key] + ]) + ) else: 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): diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index befa7cd..2445760 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -210,7 +210,7 @@ class PresetManager(QtWidgets.QDialog): def _openPreset(self, presetName, index): selectedComponents = self.core.selectedComponents - componentName = str(selectedComponents[index]).strip() + componentName = selectedComponents[index].name.strip() version = selectedComponents[index].version dirname = os.path.join(self.presetDir, componentName, str(version)) filepath = os.path.join(dirname, presetName) diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 74143e8..95aeab3 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -29,15 +29,19 @@ class blockSignals: ) def __enter__(self): - log.verbose('Blocking signals for %s' % ", ".join([ - str(w.__class__.__name__) for w in self.widgets - ])) + 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' % sum(self.oldStates)) + log.verbose( + 'Resetting blockSignals to %s', str(bool(sum(self.oldStates)))) for w, state in zip(self.widgets, self.oldStates): w.blockSignals(state) @@ -153,7 +157,7 @@ def connectWidget(widget, func): elif type(widget) == QtWidgets.QComboBox: widget.currentIndexChanged.connect(func) else: - log.warning('Failed to connect %s ' % str(widget.__class__.__name__)) + log.warning('Failed to connect %s ', str(widget.__class__.__name__)) return False return True @@ -171,7 +175,7 @@ def setWidgetValue(widget, val): elif type(widget) == QtWidgets.QComboBox: widget.setCurrentIndex(val) else: - log.warning('Failed to set %s ' % str(widget.__class__.__name__)) + log.warning('Failed to set %s ', str(widget.__class__.__name__)) return False return True diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index 8fe9148..f007f90 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -93,7 +93,7 @@ class FfmpegVideo: 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) + 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: diff --git a/src/video_thread.py b/src/video_thread.py index 87fb9bd..823ac73 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -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() -- cgit v1.2.3 From be9eb9077b2234e6d91c78d70bb8e1d8347b03aa Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 20 Aug 2017 17:47:00 -0400 Subject: relative widgets scale properly when undoing at different resolutions --- src/component.py | 81 +++++++++++++++++++++++++++++++++-------------- src/components/life.py | 2 +- src/gui/mainwindow.py | 7 ++-- src/gui/preview_thread.py | 6 ++-- src/toolkit/common.py | 1 + 5 files changed, 68 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/component.py b/src/component.py index 992a82e..0ff2fbd 100644 --- a/src/component.py +++ b/src/component.py @@ -40,7 +40,8 @@ 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%s frame %s###', self.__class__.name, str(self.compPos), '' if args else ' a preview', '' if not args else '%s ' % args[0], @@ -289,7 +290,6 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): 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 @@ -386,7 +386,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): ''' self.parent = parent self.settings = parent.settings - log.verbose('Creating UI for %s #%s\'s widget', + log.verbose( + 'Creating UI for %s #%s\'s widget', self.name, self.compPos ) self.page = self.loadUi(self.__class__.ui) @@ -530,6 +531,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): else: # Normal tracked widget setattr(self, attr, val) + log.verbose('Setting %s self.%s to %s' % (self.name, attr, val)) def setWidgetValues(self, attrDict): ''' @@ -669,12 +671,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 @@ -682,7 +694,12 @@ 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) + 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): @@ -693,7 +710,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): def setRelativeWidget(self, attr, floatVal): '''Set a relative widget using a float''' pixelVal = self.pixelValForAttr(attr, floatVal) - with blockSignals(self._allWidgets): + with blockSignals(self._trackedWidgets[attr]): self._trackedWidgets[attr].setValue(pixelVal) self.update(auto=True) @@ -707,15 +724,15 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): log.verbose('Using nonstandard oldAttr for %s', attr) return self.oldAttrs[attr] else: - return getattr(self, attr) + try: + return getattr(self, attr) + except AttributeError: + log.info('Using visible values instead of attrs') + return self._trackedWidgets[attr].value() def updateRelativeWidget(self, attr): '''Called by _preUpdate() for each relativeWidget before each update''' - try: - oldUserValue = self.getOldAttr(attr) - except (AttributeError, KeyError): - log.info('Using visible values as basis for relative widgets') - oldUserValue = self._trackedWidgets[attr].value() + oldUserValue = self.getOldAttr(attr) newUserValue = self._trackedWidgets[attr].value() newRelativeVal = self.floatValForAttr(attr, newUserValue) @@ -808,17 +825,25 @@ class ComponentUpdate(QtWidgets.QUndoCommand): ) ) 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 = 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.redoRelativeWidgetVals = { + self.relativeWidgetValsAfterUndo = { attr: copy(getattr(self.parent, attr)) for attr in self.parent._relativeWidgets } @@ -843,17 +868,28 @@ class ComponentUpdate(QtWidgets.QUndoCommand): 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.debug('Redoing component update') - self.parent.setWidgetValues(self.modifiedVals) - self.parent.setAttrs(self.modifiedVals) - if self.undone: - self.parent.oldAttrs = self.redoRelativeWidgetVals + self.parent.oldAttrs = self.relativeWidgetValsAfterUndo + self.setWidgetValues(self.modifiedVals) self.parent.update(auto=True) self.parent.oldAttrs = None else: - self.undoRelativeWidgetVals = { + self.parent.setAttrs(self.modifiedVals) + self.relativeWidgetValsAfterRedo = { attr: copy(getattr(self.parent, attr)) for attr in self.parent._relativeWidgets } @@ -862,8 +898,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand): def undo(self): log.debug('Undoing component update') self.undone = True - self.parent.oldAttrs = self.undoRelativeWidgetVals - self.parent.setWidgetValues(self.oldWidgetVals) - self.parent.setAttrs(self.oldWidgetVals) + self.parent.oldAttrs = self.relativeWidgetValsAfterRedo + self.setWidgetValues(self.oldWidgetVals) self.parent.update(auto=True) self.parent.oldAttrs = None diff --git a/src/components/life.py b/src/components/life.py index 76d2c5f..5d00987 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -70,7 +70,7 @@ class Component(Component): elif d == 3: newGrid = newGrid(1, 0) self.startingGrid = newGrid - self.sendUpdateSignal() + self._sendUpdateSignal() def update(self): self.updateGridSize() diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 833d2d1..2841896 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -88,10 +88,13 @@ class MainWindow(QtWidgets.QMainWindow): self.previewWorker.imageCreated.connect(self.showPreviewImage) self.previewThread.start() - 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) diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py index 33a9e7a..d3e0581 100644 --- a/src/gui/preview_thread.py +++ b/src/gui/preview_thread.py @@ -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: diff --git a/src/toolkit/common.py b/src/toolkit/common.py index 95aeab3..2e800eb 100644 --- a/src/toolkit/common.py +++ b/src/toolkit/common.py @@ -84,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): -- cgit v1.2.3 From 6bf8a553d6170e0ca6e7d2002e46ae327a6e5e81 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 20 Aug 2017 18:36:43 -0400 Subject: don't merge undos when setting text with a button plus changes to life.py for pep8 compliance --- src/component.py | 5 ++++- src/components/image.py | 2 ++ src/components/life.py | 46 +++++++++++++++++++++++++++------------------- src/components/sound.py | 2 ++ src/components/video.py | 2 ++ src/gui/actions.py | 1 - 6 files changed, 37 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/component.py b/src/component.py index 0ff2fbd..1f55a19 100644 --- a/src/component.py +++ b/src/component.py @@ -285,6 +285,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): # LOCKING VARIABLES self.openingPreset = False + self.mergeUndo = True self._lockedProperties = None self._lockedError = None self._lockedSize = None @@ -587,10 +588,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] @@ -850,7 +853,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand): # Determine if this update is mergeable self.id_ = -1 - if len(self.modifiedVals) == 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 diff --git a/src/components/image.py b/src/components/image.py index c57b69c..dd363bf 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -83,7 +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.mergeUndo = True def command(self, arg): if '=' in arg: diff --git a/src/components/life.py b/src/components/life.py index 5d00987..d4a455d 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -35,6 +35,7 @@ class Component(Component): self.page.toolButton_left, self.page.toolButton_right, ) + def shiftFunc(i): def shift(): self.shiftGrid(i) @@ -52,7 +53,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.mergeUndo = True def shiftGrid(self, d): def newGrid(Xchange, Ychange): @@ -197,7 +200,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': @@ -207,9 +210,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': @@ -245,19 +248,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) @@ -287,20 +290,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, @@ -337,13 +345,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, ) diff --git a/src/components/sound.py b/src/components/sound.py index b86f40c..18d2a65 100644 --- a/src/components/sound.py +++ b/src/components/sound.py @@ -52,7 +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.mergeUndo = True def commandHelp(self): print('Path to audio file:\n path=/filepath/to/sound.ogg') diff --git a/src/components/video.py b/src/components/video.py index 9c0d608..e6486ea 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -117,7 +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.mergeUndo = True def getPreviewFrame(self, width, height): if not self.videoPath or not os.path.exists(self.videoPath): diff --git a/src/gui/actions.py b/src/gui/actions.py index f101bd7..ebd9702 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -32,7 +32,6 @@ class AddComponent(QUndoCommand): 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) -- cgit v1.2.3 From 9d9c4076ac1dfccdd1a753d137d87bcf5f179e3b Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 20 Aug 2017 22:04:57 -0400 Subject: added undo button to GUI with icons that theoretically should look ok cross-platform --- src/component.py | 2 +- src/gui/actions.py | 14 +++++++------- src/gui/mainwindow.py | 36 ++++++++++++++++++++++++++++++++++++ src/gui/mainwindow.ui | 7 +++++++ 4 files changed, 51 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/component.py b/src/component.py index 1f55a19..35fc717 100644 --- a/src/component.py +++ b/src/component.py @@ -823,7 +823,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand): '''Command object for making a component action undoable''' def __init__(self, parent, oldWidgetVals, modifiedVals): super().__init__( - 'Changed %s component #%s' % ( + 'change %s component #%s' % ( parent.name, parent.compPos ) ) diff --git a/src/gui/actions.py b/src/gui/actions.py index ebd9702..8e867b9 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -15,7 +15,7 @@ from core import Core class AddComponent(QUndoCommand): def __init__(self, parent, compI, moduleI): super().__init__( - "New %s component" % + "create new %s component" % parent.core.modules[moduleI].Component.name ) self.parent = parent @@ -39,7 +39,7 @@ class AddComponent(QUndoCommand): class RemoveComponent(QUndoCommand): def __init__(self, parent, selectedRows): - super().__init__('Remove component') + super().__init__('remove component') self.parent = parent componentList = self.parent.window.listWidget_componentList self.selectedRows = [ @@ -63,7 +63,7 @@ class RemoveComponent(QUndoCommand): class MoveComponent(QUndoCommand): def __init__(self, parent, row, newRow, tag): - super().__init__("Move component %s" % tag) + super().__init__("move component %s" % tag) self.parent = parent self.row = row self.newRow = newRow @@ -107,7 +107,7 @@ class MoveComponent(QUndoCommand): class ClearPreset(QUndoCommand): def __init__(self, parent, compI): - super().__init__("Clear preset") + super().__init__("clear preset") self.parent = parent self.compI = compI self.component = self.parent.core.selectedComponents[compI] @@ -125,7 +125,7 @@ class ClearPreset(QUndoCommand): class OpenPreset(QUndoCommand): def __init__(self, parent, presetName, compI): - super().__init__("Open %s preset" % presetName) + super().__init__("open %s preset" % presetName) self.parent = parent self.presetName = presetName self.compI = compI @@ -145,7 +145,7 @@ class OpenPreset(QUndoCommand): class RenamePreset(QUndoCommand): def __init__(self, parent, path, oldName, newName): - super().__init__('Rename preset') + super().__init__('rename preset') self.parent = parent self.path = path self.oldName = oldName @@ -167,7 +167,7 @@ class DeletePreset(QUndoCommand): ) self.store = self.parent.core.getPreset(self.path) self.presetName = self.store['preset'] - super().__init__('Delete %s preset (%s)' % (self.presetName, compName)) + 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) diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 2841896..3b204b7 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -100,6 +100,42 @@ class MainWindow(QtWidgets.QMainWindow): 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') diff --git a/src/gui/mainwindow.ui b/src/gui/mainwindow.ui index b43d375..cd8454d 100644 --- a/src/gui/mainwindow.ui +++ b/src/gui/mainwindow.ui @@ -110,6 +110,13 @@ QLayout::SetMinimumSize + + + + Undo + + + -- cgit v1.2.3 From 62e2ef18a3a31c15f88a96f07b2bc587808f5ad5 Mon Sep 17 00:00:00 2001 From: tassaron Date: Mon, 21 Aug 2017 07:06:12 -0400 Subject: potential dataDir paths in comments for future reference --- src/core.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/core.py b/src/core.py index bfb8272..784f3b8 100644 --- a/src/core.py +++ b/src/core.py @@ -14,7 +14,7 @@ import toolkit log = logging.getLogger('AVP.Core') STDOUT_LOGLVL = logging.VERBOSE -FILE_LOGLVL = logging.DEBUG +FILE_LOGLVL = logging.VERBOSE class Core: @@ -460,6 +460,9 @@ class Core: dataDir = QtCore.QStandardPaths.writableLocation( QtCore.QStandardPaths.AppConfigLocation ) + # Windows: C:/Users//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) -- cgit v1.2.3 From 85d3b779d07ad92b0f540ea52185777c3c3f5e48 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sat, 26 Aug 2017 21:23:44 -0400 Subject: fixed too-large Color sizes, fixed a redoing bug, rm pointless things and now Ctrl+Alt+Shift+A gives a bunch of debug info --- src/component.py | 30 +++++++++++------------ src/components/color.py | 2 +- src/components/color.ui | 4 ++-- src/components/text.py | 13 ++++++---- src/core.py | 8 +++++-- src/gui/mainwindow.py | 63 +++++++++++++++++++++++++++++-------------------- src/gui/preview_win.py | 1 + src/main.py | 5 ---- src/toolkit/ffmpeg.py | 2 +- src/toolkit/frame.py | 3 --- 10 files changed, 72 insertions(+), 59 deletions(-) (limited to 'src') diff --git a/src/component.py b/src/component.py index 35fc717..de4b6a7 100644 --- a/src/component.py +++ b/src/component.py @@ -41,10 +41,8 @@ class ComponentMetaclass(type(QtCore.QObject)): def renderWrapper(self, *args, **kwargs): try: log.verbose( - '### %s #%s renders%s frame %s###', + '### %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: @@ -198,8 +196,8 @@ class ComponentMetaclass(type(QtCore.QObject)): 'names', # Class methods 'error', 'audio', 'properties', # Properties 'preFrameRender', 'previewRender', - 'frameRender', 'command', - 'loadPreset', 'update', 'widget', + 'loadPreset', 'command', + 'update', 'widget', ) # Auto-decorate methods @@ -212,7 +210,7 @@ class ComponentMetaclass(type(QtCore.QObject)): attrs[key] = property(attrs[key]) elif key == 'command': attrs[key] = cls.commandWrapper(attrs[key]) - elif key in ('previewRender', 'frameRender'): + elif key == 'previewRender': attrs[key] = cls.renderWrapper(attrs[key]) elif key == 'preFrameRender': attrs[key] = cls.initializationWrapper(attrs[key]) @@ -298,16 +296,19 @@ 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 ( - 'Component(%s, %s, Core)\n' - 'Name: %s v%s\n Preset: %s' % ( + 'Component(module %s, pos %s) (%s)\n' + 'Name: %s v%s\nPreset: %s' % ( self.moduleIndex, self.compPos, - self.__class__.name, str(self.__class__.version), preset + object.__repr__(self), + self.__class__.name, str(self.__class__.version), + pprint.pformat(preset) ) ) @@ -886,12 +887,11 @@ class ComponentUpdate(QtWidgets.QUndoCommand): def redo(self): if self.undone: log.debug('Redoing component update') - self.parent.oldAttrs = self.relativeWidgetValsAfterUndo - self.setWidgetValues(self.modifiedVals) - self.parent.update(auto=True) - self.parent.oldAttrs = None - else: - self.parent.setAttrs(self.modifiedVals) + 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 diff --git a/src/components/color.py b/src/components/color.py index a55aa10..7d4f86d 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -102,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 diff --git a/src/components/color.ui b/src/components/color.ui index 1865e60..c1713fb 100644 --- a/src/components/color.ui +++ b/src/components/color.ui @@ -204,7 +204,7 @@ 0 - 999999999 + 19200 0 @@ -239,7 +239,7 @@ - 999999999 + 10800 diff --git a/src/components/text.py b/src/components/text.py index 92f0599..32a108e 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -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' @@ -76,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): @@ -137,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) diff --git a/src/core.py b/src/core.py index 784f3b8..b9e2335 100644 --- a/src/core.py +++ b/src/core.py @@ -14,7 +14,7 @@ import toolkit log = logging.getLogger('AVP.Core') STDOUT_LOGLVL = logging.VERBOSE -FILE_LOGLVL = logging.VERBOSE +FILE_LOGLVL = logging.DEBUG 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): @@ -482,7 +487,6 @@ class Core: '854x480', ], 'FFMPEG_BIN': findFfmpeg(), - 'windowHasFocus': False, 'canceled': False, } diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 3b204b7..d7fde5c 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -11,6 +11,7 @@ from queue import Queue import sys import os import signal +import atexit import filecmp import time import logging @@ -49,6 +50,13 @@ class MainWindow(QtWidgets.QMainWindow): self.window = window self.core = Core() Core.mode = 'GUI' + # widgets of component settings + self.pages = [] + self.lastAutosave = time.time() + # list of previous five autosave times, used to reduce update spam + self.autosaveTimes = [] + self.autosaveCooldown = 0.2 + self.encoding = False # Find settings created by Core object self.dataDir = Core.dataDir @@ -56,19 +64,16 @@ 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) - # widgets of component settings - self.pages = [] - self.lastAutosave = time.time() - # list of previous five autosave times, used to reduce update spam - self.autosaveTimes = [] - self.autosaveCooldown = 0.2 - self.encoding = False - + # Create Preset Manager self.presetManager = PresetManager( uic.loadUi( os.path.join(Core.wd, 'gui', 'presetmanager.ui')), self) @@ -97,7 +102,6 @@ class MainWindow(QtWidgets.QMainWindow): 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() @@ -391,24 +395,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 ) - QtWidgets.QShortcut( - "Ctrl+Alt+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' @@ -542,7 +563,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 @@ -639,6 +660,7 @@ class MainWindow(QtWidgets.QMainWindow): detail=detail, icon='Critical', ) + log.info('%s', repr(self)) def changeEncodingStatus(self, status): self.encoding = status @@ -1017,12 +1039,3 @@ class MainWindow(QtWidgets.QMainWindow): 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 diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py index c6b9a32..49a22eb 100644 --- a/src/gui/preview_win.py +++ b/src/gui/preview_win.py @@ -60,3 +60,4 @@ class PreviewWindow(QtWidgets.QLabel): icon='Critical', parent=self ) + log.info('%', repr(self.parent)) diff --git a/src/main.py b/src/main.py index 6d18af3..f767de1 100644 --- a/src/main.py +++ b/src/main.py @@ -36,8 +36,6 @@ def main(): elif mode == 'GUI': from gui.mainwindow import MainWindow - import atexit - import signal window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui")) # window.adjustSize() @@ -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__": diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index f007f90..a77831e 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -157,7 +157,7 @@ def findFfmpeg(): ['ffmpeg', '-version'], stderr=f ) return "ffmpeg" - except subprocess.CalledProcessError: + except (subprocess.CalledProcessError, FileNotFoundError): return "avconv" diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 2104978..aefb55f 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -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) @@ -78,8 +77,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) -- cgit v1.2.3 From e8a7b18293768497df272bb4cb64b678d57f58da Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 27 Aug 2017 09:53:18 -0400 Subject: disallow suspiciously enormous floats this stops a bad project file from crashing my computer... --- src/component.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/component.py b/src/component.py index de4b6a7..01c1d06 100644 --- a/src/component.py +++ b/src/component.py @@ -390,7 +390,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): self.settings = parent.settings log.verbose( 'Creating UI for %s #%s\'s widget', - self.name, self.compPos + self.__class__.name, self.compPos ) self.page = self.loadUi(self.__class__.ui) @@ -533,7 +533,8 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): else: # Normal tracked widget setattr(self, attr, val) - log.verbose('Setting %s self.%s to %s' % (self.name, attr, val)) + log.verbose('Setting %s self.%s to %s' % ( + self.__class__.name, attr, val)) def setWidgetValues(self, attrDict): ''' @@ -698,6 +699,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): def pixelValForAttr(self, attr, val=None, **kwargs): if val is None: val = self._relativeValues[attr] + 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', @@ -748,7 +755,7 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): # means the pixel value needs to be updated log.debug( 'Updating %s #%s\'s relative widget: %s', - self.name, self.compPos, attr) + self.__class__.name, self.compPos, attr) with blockSignals(self._trackedWidgets[attr]): self.updateRelativeWidgetMaximum(attr) pixelVal = self.pixelValForAttr(attr, oldRelativeVal) -- cgit v1.2.3 From 4a310ffb2870babf6774da843cad271f8a477bcc Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 27 Aug 2017 12:10:21 -0400 Subject: file logging can be turned completely off and various changes to log levels and messages everywhere --- src/component.py | 22 ++++++++---- src/components/spectrum.py | 21 ++++++++---- src/components/video.py | 21 ++++++++---- src/components/waveform.py | 20 +++++++---- src/core.py | 85 ++++++++++++++++++++++------------------------ src/gui/mainwindow.py | 18 ++++------ src/toolkit/ffmpeg.py | 22 ++++++++---- 7 files changed, 119 insertions(+), 90 deletions(-) (limited to 'src') diff --git a/src/component.py b/src/component.py index 01c1d06..f3ee188 100644 --- a/src/component.py +++ b/src/component.py @@ -423,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) @@ -580,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.') @@ -613,6 +620,10 @@ 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() @@ -732,13 +743,12 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): can make determining the 'previous' value tricky. ''' if self.oldAttrs is not None: - log.verbose('Using nonstandard oldAttr for %s', attr) return self.oldAttrs[attr] else: try: return getattr(self, attr) except AttributeError: - log.info('Using visible values instead of attrs') + log.error('Using visible values instead of oldAttrs') return self._trackedWidgets[attr].value() def updateRelativeWidget(self, attr): @@ -893,7 +903,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand): def redo(self): if self.undone: - log.debug('Redoing component update') + log.info('Redoing component update') self.parent.oldAttrs = self.relativeWidgetValsAfterUndo self.setWidgetValues(self.modifiedVals) self.parent.update(auto=True) @@ -906,7 +916,7 @@ class ComponentUpdate(QtWidgets.QUndoCommand): self.parent._sendUpdateSignal() def undo(self): - log.debug('Undoing component update') + log.info('Undoing component update') self.undone = True self.parent.oldAttrs = self.relativeWidgetValsAfterRedo self.setWidgetValues(self.oldWidgetVals) diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 2b98dc2..77cb086 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -148,15 +148,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) diff --git a/src/components/video.py b/src/components/video.py index e6486ea..8ad21b5 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -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) diff --git a/src/components/waveform.py b/src/components/waveform.py index 5c02bbf..cbfc47f 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -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) diff --git a/src/core.py b/src/core.py index b9e2335..1a90296 100644 --- a/src/core.py +++ b/src/core.py @@ -13,8 +13,8 @@ import toolkit log = logging.getLogger('AVP.Core') -STDOUT_LOGLVL = logging.VERBOSE -FILE_LOGLVL = logging.DEBUG +STDOUT_LOGLVL = logging.INFO +FILE_LOGLVL = logging.VERBOSE class Core: @@ -145,17 +145,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 @@ -472,11 +466,12 @@ class Core: 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'), @@ -486,8 +481,8 @@ class Core: '1280x720', '854x480', ], - 'FFMPEG_BIN': findFfmpeg(), - 'canceled': False, + 'logDir': os.path.join(dataDir, 'log'), + 'logEnabled': False, } settings['videoFormats'] = toolkit.appendUppercase([ @@ -572,42 +567,42 @@ class Core: @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() diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index d7fde5c..81c5d7c 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -92,6 +92,10 @@ 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.') + ) timeout = 500 log.debug( @@ -442,7 +446,7 @@ 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) @@ -459,16 +463,8 @@ class MainWindow(QtWidgets.QMainWindow): modified = False else: modified = (presetStore != self.core.savedPresets[name]) - if modified: - log.verbose( - 'Differing values between presets: %s', - ", ".join([ - '%s: %s' % item for item in presetStore.items() - if val != self.core.savedPresets[name][key] - ]) - ) - else: - modified = bool(presetStore) + + modified = bool(presetStore) if pos < 0: pos = len(self.core.selectedComponents)-1 name = self.core.selectedComponents[pos].name diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index a77831e..d78d803 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -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 -- cgit v1.2.3 From ad6dd9f5329f3e23e75c181c21ca8701028b538f Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 27 Aug 2017 19:59:51 -0400 Subject: undoable Life component grid actions --- src/components/life.py | 132 ++++++++++++++++++++++++++++++++++++------------- src/gui/preview_win.py | 8 ++- 2 files changed, 102 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/src/components/life.py b/src/components/life.py index d4a455d..7a610eb 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -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 @@ -58,22 +59,8 @@ class Component(Component): 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() @@ -98,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') @@ -223,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: @@ -363,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 } @@ -374,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: @@ -397,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 + + +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 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 + 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() diff --git a/src/gui/preview_win.py b/src/gui/preview_win.py index 49a22eb..3db420c 100644 --- a/src/gui/preview_win.py +++ b/src/gui/preview_win.py @@ -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.Gui.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): -- cgit v1.2.3 From 8411857030d92e448d5c64682f396e677161afbe Mon Sep 17 00:00:00 2001 From: tassaron Date: Mon, 28 Aug 2017 18:54:54 -0400 Subject: ctrl-c ends commandline mode properly --- setup.py | 2 +- src/command.py | 9 +++++++++ src/components/spectrum.py | 3 ++- src/core.py | 10 ++++------ src/toolkit/frame.py | 1 + src/video_thread.py | 11 ++++++++--- 6 files changed, 25 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/setup.py b/setup.py index dd546e2..cdf4c4a 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup import os -__version__ = '2.0.0.rc4' +__version__ = '2.0.0rc5' def package_files(directory): diff --git a/src/command.py b/src/command.py index 4116c5a..cd3c6c3 100644 --- a/src/command.py +++ b/src/command.py @@ -8,6 +8,7 @@ import argparse import os import sys import time +import signal from core import Core @@ -91,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']: @@ -124,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: diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 77cb086..6675f5b 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -98,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( diff --git a/src/core.py b/src/core.py index 1a90296..d7445c9 100644 --- a/src/core.py +++ b/src/core.py @@ -13,8 +13,8 @@ import toolkit log = logging.getLogger('AVP.Core') -STDOUT_LOGLVL = logging.INFO -FILE_LOGLVL = logging.VERBOSE +STDOUT_LOGLVL = logging.WARNING +FILE_LOGLVL = None class Core: @@ -77,8 +77,7 @@ class Core: if compPos < 0 or compPos > len(self.selectedComponents): compPos = len(self.selectedComponents) if len(self.selectedComponents) > 50: - return None - + return -1 if type(component) is int: # create component using module index in self.modules moduleIndex = int(component) @@ -188,7 +187,6 @@ class Core: for key, value in data['Settings']: Core.settings.setValue(key, value) - for tup in data['Components']: name, vers, preset = tup clearThis = False @@ -213,7 +211,7 @@ class Core: self.moduleIndexFor(name), loader ) - if i is None: + if i == -1: loader.showMessage(msg="Too many components!") break diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index aefb55f..0e200b5 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -32,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 diff --git a/src/video_thread.py b/src/video_thread.py index 823ac73..91ebe93 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -252,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 -- cgit v1.2.3