From 8b253717f7c0dd3fe73b1f3474fea2176e8f19ba Mon Sep 17 00:00:00 2001 From: tassaron Date: Wed, 9 Aug 2017 16:46:59 -0400 Subject: Conway's Game of Life component --- src/components/life.py | 155 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/components/life.py (limited to 'src/components/life.py') diff --git a/src/components/life.py b/src/components/life.py new file mode 100644 index 0000000..1e72620 --- /dev/null +++ b/src/components/life.py @@ -0,0 +1,155 @@ +from PyQt5 import QtGui, QtCore, QtWidgets +from PIL import ImageDraw, ImageEnhance, ImageChops, ImageFilter +import os +import math + +from component import Component +from toolkit.frame import BlankFrame, FramePainter + + +class Component(Component): + name = 'Conway\'s Game of Life' + version = '1.0.0a' + + def widget(self, *args): + super().widget(*args) + self.scale = 32 + self.updateGridSize() + self.startingGrid = {} + self.trackWidgets({ + 'tickRate': self.page.spinBox_tickRate, + 'scale': self.page.spinBox_scale, + 'color': self.page.lineEdit_color, + 'shapeType': self.page.comboBox_shapeType, + 'shadow': self.page.checkBox_shadow, + }, colorWidgets={ + 'color': self.page.pushButton_color, + }) + self.page.spinBox_scale.setValue(self.scale) + + def update(self): + self.updateGridSize() + 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[pos] = True + elif button == 2 and pos in self.startingGrid: + self.startingGrid.pop(pos) + + def updateGridSize(self): + w, h = self.core.resolutions[-1].split('x') + self.gridWidth = int(int(w) / self.scale) + self.gridHeight = int(int(h) / self.scale) + self.pxWidth = math.ceil(self.width / self.gridWidth) + self.pxHeight = math.ceil(self.height / self.gridHeight) + + def previewRender(self): + return self.drawGrid(self.startingGrid) + + def preFrameRender(self, *args, **kwargs): + super().preFrameRender(*args, **kwargs) + self.progressBarSetText.emit("Computing evolution...") + self.tickGrids = {0: self.startingGrid} + tick = 0 + for frameNo in range( + self.tickRate, len(self.completeAudioArray), self.sampleSize + ): + if frameNo % self.tickRate == 0: + tick += 1 + self.tickGrids[tick] = self.gridForTick(tick) + + # update progress bar + progress = int(100*(frameNo/len(self.completeAudioArray))) + if progress >= 100: + progress = 100 + pStr = "Computing evolution: "+str(progress)+'%' + self.progressBarSetText.emit(pStr) + self.progressBarUpdate.emit(int(progress)) + + def frameRender(self, frameNo): + tick = math.floor(frameNo / self.tickRate) + grid = self.tickGrids[tick] + return self.drawGrid(grid) + + def drawGrid(self, grid): + frame = BlankFrame(self.width, self.height) + drawer = ImageDraw.Draw(frame) + + for x, y in grid: + drawPtX = x * self.pxWidth + drawPtY = y * self.pxHeight + rect = ( + (drawPtX, drawPtY), + (drawPtX + self.pxWidth, drawPtY + self.pxHeight) + ) + if self.shapeType == 0: + drawer.rectangle(rect, fill=self.color) + elif self.shapeType == 1: + drawer.ellipse(rect, fill=self.color) + elif self.shapeType == 2: + drawer.pieslice(rect, 290, 250, fill=self.color) + elif self.shapeType == 3: + drawer.pieslice(rect, 20, 340, fill=self.color) + + if self.shadow: + shadImg = ImageEnhance.Contrast(frame).enhance(0.0) + shadImg = shadImg.filter(ImageFilter.GaussianBlur(5.00)) + shadImg = ImageChops.offset(shadImg, -2, 2) + shadImg.paste(frame, box=(0, 0), mask=frame) + frame = shadImg + return frame + + def gridForTick(self, tick): + '''Given a tick number over 0, returns a new grid dict of tuples''' + lastGrid = self.tickGrids[tick - 1] + + 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 neighbours(x, y): + nearbyCells = [ + lastGrid.get(cell) for cell in nearbyCoords(x, y) + ] + return [ + nearbyCell for nearbyCell in nearbyCells + if nearbyCell is not None + ] + + newGrid = {} + for x, y in lastGrid: + surrounding = len(neighbours(x, y)) + if surrounding == 2 or surrounding == 3: + newGrid[(x, y)] = True + potentialNewCells = set([ + coordTup for origin in lastGrid + for coordTup in list(nearbyCoords(*origin)) + ]) + for x, y in potentialNewCells: + if (x, y) in newGrid: + continue + surrounding = len(neighbours(x, y)) + if surrounding == 3: + newGrid[(x, y)] = True + + return newGrid + + def savePreset(self): + pr = super().savePreset() + pr['GRID'] = self.startingGrid + return pr + + def loadPreset(self, pr, *args): + super().loadPreset(pr, *args) + self.startingGrid = pr['GRID'] -- cgit v1.2.3 From cacab464c7655a1c0cedcfe95b63609f55d78322 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 10 Aug 2017 00:46:31 -0400 Subject: more shapes and custom image option for Life --- src/.goutputstream-67IS4Y | 976 ++++++++++++++++++++++++++++++++++++++++++++++ src/components/life.py | 167 +++++++- src/components/life.ui | 57 ++- 3 files changed, 1186 insertions(+), 14 deletions(-) create mode 100644 src/.goutputstream-67IS4Y (limited to 'src/components/life.py') diff --git a/src/.goutputstream-67IS4Y b/src/.goutputstream-67IS4Y new file mode 100644 index 0000000..789a6e7 --- /dev/null +++ b/src/.goutputstream-67IS4Y @@ -0,0 +1,976 @@ +''' + 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 + +from core import Core +import preview_thread +from presetmanager import PresetManager +from toolkit import disableWhenEncoding, disableWhenOpeningProject, checkOutput + + +class PreviewWindow(QtWidgets.QLabel): + ''' + Paints the preview QLabel and maintains the aspect ratio when the + window is resized. + ''' + + 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'): + return + pos = (event.x(), event.y()) + size = (self.width(), self.height()) + component.previewClickEvent( + pos, size, event.button() + ) + self.parent.core.updateComponent(i) + + @QtCore.pyqtSlot(str) + def threadError(self, msg): + self.parent.showMessage( + msg=msg, + icon='Critical', + parent=self + ) + + +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) + # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) + self.window = window + self.core = Core() + + # 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 data directory, load/create settings + 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) + + if not os.path.exists(self.dataDir): + os.makedirs(self.dataDir) + for neededDirectory in ( + self.presetDir, self.settings.value("projectDir")): + if not os.path.exists(neededDirectory): + os.mkdir(neededDirectory) + + # Create the preview window and its thread, queues, and timers + self.previewWindow = PreviewWindow(self, os.path.join( + Core.wd, "background.png")) + window.verticalLayout_previewWrapper.addWidget(self.previewWindow) + + 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() + + 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': + 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() + 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): + 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 + self.window.setWindowTitle(appName) + + @QtCore.pyqtSlot(int, dict) + def updateComponentTitle(self, pos, presetStore=False): + if type(presetStore) == 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 + title = str(self.core.selectedComponents[pos]) + if self.core.selectedComponents[pos].currentPreset: + title += ' - %s' % self.core.selectedComponents[pos].currentPreset + if modified: + 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) + + 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: + return True + except FileNotFoundError: + print('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): + print('stop') + 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) + 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/components/life.py b/src/components/life.py index 1e72620..89a4c5c 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -1,10 +1,10 @@ from PyQt5 import QtGui, QtCore, QtWidgets -from PIL import ImageDraw, ImageEnhance, ImageChops, ImageFilter +from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter import os import math from component import Component -from toolkit.frame import BlankFrame, FramePainter +from toolkit.frame import BlankFrame, scale class Component(Component): @@ -16,19 +16,51 @@ class Component(Component): self.scale = 32 self.updateGridSize() self.startingGrid = {} + self.page.pushButton_pickImage.clicked.connect(self.pickImage) self.trackWidgets({ 'tickRate': self.page.spinBox_tickRate, 'scale': self.page.spinBox_scale, 'color': self.page.lineEdit_color, 'shapeType': self.page.comboBox_shapeType, 'shadow': self.page.checkBox_shadow, + 'customImg': self.page.checkBox_customImg, + 'image': self.page.lineEdit_image, }, colorWidgets={ 'color': self.page.pushButton_color, }) self.page.spinBox_scale.setValue(self.scale) + self.page.spinBox_scale.valueChanged.connect(self.updateGridSize) + + def pickImage(self): + imgDir = self.settings.value("componentDir", os.path.expanduser("~")) + filename, _ = QtWidgets.QFileDialog.getOpenFileName( + self.page, "Choose Image", imgDir, + "Image Files (%s)" % " ".join(self.core.imageFormats)) + if filename: + self.settings.setValue("componentDir", os.path.dirname(filename)) + self.page.lineEdit_image.setText(filename) + self.update() def update(self): self.updateGridSize() + if self.page.checkBox_customImg.isChecked(): + self.page.label_color.setVisible(False) + self.page.lineEdit_color.setVisible(False) + self.page.pushButton_color.setVisible(False) + self.page.label_shape.setVisible(False) + self.page.comboBox_shapeType.setVisible(False) + self.page.label_image.setVisible(True) + self.page.lineEdit_image.setVisible(True) + self.page.pushButton_pickImage.setVisible(True) + else: + self.page.label_color.setVisible(True) + self.page.lineEdit_color.setVisible(True) + self.page.pushButton_color.setVisible(True) + self.page.label_shape.setVisible(True) + self.page.comboBox_shapeType.setVisible(True) + self.page.label_image.setVisible(False) + self.page.lineEdit_image.setVisible(False) + self.page.pushButton_pickImage.setVisible(False) super().update() def previewClickEvent(self, pos, size, button): @@ -59,6 +91,8 @@ class Component(Component): for frameNo in range( self.tickRate, len(self.completeAudioArray), self.sampleSize ): + if self.parent.canceled: + break if frameNo % self.tickRate == 0: tick += 1 self.tickGrids[tick] = self.gridForTick(tick) @@ -71,6 +105,16 @@ class Component(Component): self.progressBarSetText.emit(pStr) self.progressBarUpdate.emit(int(progress)) + def properties(self): + if self.customImg and ( + not self.image or not os.path.exists(self.image) + ): + return ['error'] + return [] + + def error(self): + return "No image selected to represent life." + def frameRender(self, frameNo): tick = math.floor(frameNo / self.tickRate) grid = self.tickGrids[tick] @@ -78,23 +122,124 @@ class Component(Component): def drawGrid(self, grid): frame = BlankFrame(self.width, self.height) - drawer = ImageDraw.Draw(frame) + + def drawCustomImg(): + try: + img = Image.open(self.image) + except Exception: + return + img = img.resize((self.pxWidth, self.pxHeight), Image.ANTIALIAS) + frame.paste(img, box=(drawPtX, drawPtY)) + + def drawShape(): + drawer = ImageDraw.Draw(frame) + + # Rectangle + if self.shapeType == 0: + drawer.rectangle(rect, fill=self.color) + + # Ellipse + elif self.shapeType == 1: + drawer.ellipse(rect, fill=self.color) + + tenthX, tenthY = scale(10, self.pxWidth, self.pxHeight, int) + smallerShape = ( + (drawPtX + tenthX + int(tenthX / 4), + drawPtY + tenthY + int(tenthY / 2)), + (drawPtX + self.pxWidth - tenthX - int(tenthX / 4), + drawPtY + self.pxHeight - (tenthY + int(tenthY / 2))) + ) + outlineShape = ( + (drawPtX + int(tenthX / 4), + drawPtY + int(tenthY / 2)), + (drawPtX + self.pxWidth - int(tenthX / 4), + drawPtY + self.pxHeight - int(tenthY / 2)) + ) + + # Circle + if self.shapeType == 2: + drawer.ellipse(outlineShape, fill=self.color) + drawer.ellipse(smallerShape, fill=(0,0,0,0)) + + # Lilypad + elif self.shapeType == 3: + drawer.pieslice(smallerShape, 290, 250, fill=self.color) + + # Pac-Man + elif self.shapeType == 4: + 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 + + # Duck + if self.shapeType == 5: + duckHead = ( + (drawPtX + qX, drawPtY + qY), + (drawPtX + int(qX * 3), drawPtY + int(tY * 2)) + ) + duckBeak = ( + (drawPtX + hX, drawPtY + qY), + (drawPtX + self.pxWidth + qX, + drawPtY + int(qY * 3)) + ) + duckWing = ( + (drawPtX, drawPtY + hY), + rect[1] + ) + duckBody = ( + (drawPtX + int(qX / 4), drawPtY + int(qY * 3)), + (drawPtX + int(tX * 2), drawPtY + self.pxHeight) + ) + drawer.ellipse(duckBody, fill=self.color) + drawer.ellipse(duckHead, fill=self.color) + drawer.pieslice(duckWing, 130, 200, fill=self.color) + drawer.pieslice(duckBeak, 145, 200, fill=self.color) + + # Peace + elif self.shapeType == 6: + line = ( + (drawPtX + hX - int(tenthX / 2), drawPtY + int(tenthY / 2)), + (drawPtX + hX + int(tenthX / 2), + drawPtY + self.pxHeight - int(tenthY / 2)) + ) + drawer.ellipse(outlineShape, fill=self.color) + 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)), + ) + drawer.line( + slantLine(qX), + fill=self.color, + width=tenthX + ) + drawer.line( + slantLine(self.pxWidth - qX), + fill=self.color, + width=tenthX + ) for x, y in grid: drawPtX = x * self.pxWidth + if drawPtX > self.width: + continue drawPtY = y * self.pxHeight + if drawPtY > self.height: + continue rect = ( (drawPtX, drawPtY), (drawPtX + self.pxWidth, drawPtY + self.pxHeight) ) - if self.shapeType == 0: - drawer.rectangle(rect, fill=self.color) - elif self.shapeType == 1: - drawer.ellipse(rect, fill=self.color) - elif self.shapeType == 2: - drawer.pieslice(rect, 290, 250, fill=self.color) - elif self.shapeType == 3: - drawer.pieslice(rect, 20, 340, fill=self.color) + + if self.customImg: + drawCustomImg() + else: + drawShape() if self.shadow: shadImg = ImageEnhance.Contrast(frame).enhance(0.0) diff --git a/src/components/life.ui b/src/components/life.ui index 88f8eca..2341c19 100644 --- a/src/components/life.ui +++ b/src/components/life.ui @@ -39,7 +39,7 @@ 30 - 15 + 5 @@ -93,6 +93,13 @@ + + + + Custom Image + + + @@ -111,7 +118,36 @@ - + + + Image + + + + + + + + + + + 0 + 0 + + + + + 32 + 32 + + + + ... + + + + + Color @@ -169,7 +205,7 @@ - + Shape @@ -182,6 +218,11 @@ Rectangle + + + Ellipse + + Circle @@ -197,6 +238,16 @@ Pac-Man + + + Duck + + + + + Peace + + -- cgit v1.2.3 From 8baa24e87847a0c7c530cbb55196103ce9cc511c Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 10 Aug 2017 09:12:48 -0400 Subject: added connected path shape to Life --- src/components/life.py | 104 ++++++++++++++++++++++++++++++++++++------------- src/components/life.ui | 7 +++- 2 files changed, 82 insertions(+), 29 deletions(-) (limited to 'src/components/life.py') diff --git a/src/components/life.py b/src/components/life.py index 89a4c5c..08360a2 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -133,13 +133,18 @@ class Component(Component): def drawShape(): drawer = ImageDraw.Draw(frame) + rect = ( + (drawPtX, drawPtY), + (drawPtX + self.pxWidth, drawPtY + self.pxHeight) + ) + shape = self.page.comboBox_shapeType.currentText().lower() # Rectangle - if self.shapeType == 0: + if shape == 'rectangle': drawer.rectangle(rect, fill=self.color) - # Ellipse - elif self.shapeType == 1: + # Elliptical + elif shape == 'elliptical': drawer.ellipse(rect, fill=self.color) tenthX, tenthY = scale(10, self.pxWidth, self.pxHeight, int) @@ -155,26 +160,75 @@ class Component(Component): (drawPtX + self.pxWidth - int(tenthX / 4), drawPtY + self.pxHeight - int(tenthY / 2)) ) - # Circle - if self.shapeType == 2: + if shape == 'circle': drawer.ellipse(outlineShape, fill=self.color) drawer.ellipse(smallerShape, fill=(0,0,0,0)) # Lilypad - elif self.shapeType == 3: + elif shape == 'lilypad': drawer.pieslice(smallerShape, 290, 250, fill=self.color) # Pac-Man - elif self.shapeType == 4: + 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 + # Path + if shape == 'path': + drawer.ellipse(rect, fill=self.color) + rects = { + direction: False + for direction in ( + 'up', 'down', 'left', 'right', + ) + } + for cell in nearbyCoords(x, y): + if grid.get(cell) is None: + continue + if cell[0] == x: + if cell[1] < y: + rects['up'] = True + if cell[1] > y: + rects['down'] = True + if cell[1] == y: + if cell[0] < x: + rects['left'] = True + if cell[0] > x: + rects['right'] = True + + for direction, rect in rects.items(): + if rect: + if direction == 'up': + sect = ( + (drawPtX, drawPtY), + (drawPtX + self.pxWidth, drawPtY + hY) + ) + elif direction == 'down': + sect = ( + (drawPtX, drawPtY + hY), + (drawPtX + self.pxWidth, + drawPtY + self.pxHeight) + ) + elif direction == 'left': + sect = ( + (drawPtX, drawPtY), + (drawPtX + hX, + drawPtY + self.pxHeight) + ) + elif direction == 'right': + sect = ( + (drawPtX + hX, drawPtY), + (drawPtX + self.pxWidth, + drawPtY + self.pxHeight) + ) + drawer.rectangle(sect, fill=self.color) + # Duck - if self.shapeType == 5: + elif shape == 'duck': duckHead = ( (drawPtX + qX, drawPtY + qY), (drawPtX + int(qX * 3), drawPtY + int(tY * 2)) @@ -198,7 +252,7 @@ class Component(Component): drawer.pieslice(duckBeak, 145, 200, fill=self.color) # Peace - elif self.shapeType == 6: + elif shape == 'peace': line = ( (drawPtX + hX - int(tenthX / 2), drawPtY + int(tenthY / 2)), (drawPtX + hX + int(tenthX / 2), @@ -231,10 +285,6 @@ class Component(Component): drawPtY = y * self.pxHeight if drawPtY > self.height: continue - rect = ( - (drawPtX, drawPtY), - (drawPtX + self.pxWidth, drawPtY + self.pxHeight) - ) if self.customImg: drawCustomImg() @@ -253,23 +303,10 @@ class Component(Component): '''Given a tick number over 0, returns a new grid dict of tuples''' lastGrid = self.tickGrids[tick - 1] - 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 neighbours(x, y): - nearbyCells = [ - lastGrid.get(cell) for cell in nearbyCoords(x, y) - ] return [ - nearbyCell for nearbyCell in nearbyCells - if nearbyCell is not None + cell for cell in nearbyCoords(x, y) + if lastGrid.get(cell) is not None ] newGrid = {} @@ -298,3 +335,14 @@ class Component(Component): def loadPreset(self, pr, *args): super().loadPreset(pr, *args) self.startingGrid = pr['GRID'] + + +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 diff --git a/src/components/life.ui b/src/components/life.ui index 2341c19..3b393dd 100644 --- a/src/components/life.ui +++ b/src/components/life.ui @@ -213,6 +213,11 @@ + + + Path + + Rectangle @@ -220,7 +225,7 @@ - Ellipse + Elliptical -- cgit v1.2.3 From bdb006f25d2237ad69ee88d7f054cefaa0c5a3d8 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 10 Aug 2017 17:27:59 -0400 Subject: fixed relative image scale bug & Life preset bug dicts must be alphabetized in AV files --- setup.py | 2 +- src/components/image.py | 32 ++++++++++++++++++++++++++++---- src/components/image.ui | 16 ++++++++++++++++ src/components/life.py | 7 ++++--- 4 files changed, 49 insertions(+), 8 deletions(-) (limited to 'src/components/life.py') diff --git a/setup.py b/setup.py index 4a4511f..dd546e2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup import os -__version__ = '2.0.0.rc3' +__version__ = '2.0.0.rc4' def package_files(directory): diff --git a/src/components/image.py b/src/components/image.py index 1555541..63bee1a 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -8,7 +8,7 @@ from toolkit.frame import BlankFrame class Component(Component): name = 'Image' - version = '1.0.0' + version = '1.0.1' def widget(self, *args): super().widget(*args) @@ -16,6 +16,7 @@ class Component(Component): self.trackWidgets({ 'imagePath': self.page.lineEdit_image, 'scale': self.page.spinBox_scale, + 'stretchScale': self.page.spinBox_scale_stretch, 'rotate': self.page.spinBox_rotate, 'color': self.page.spinBox_color, 'xPosition': self.page.spinBox_x, @@ -51,6 +52,7 @@ class Component(Component): def drawFrame(self, width, height): frame = BlankFrame(width, height) if self.imagePath and os.path.exists(self.imagePath): + scale = self.scale if not self.stretched else self.stretchScale image = Image.open(self.imagePath) # Modify image's appearance @@ -62,9 +64,9 @@ class Component(Component): image = image.transpose(Image.FLIP_LEFT_RIGHT) if self.stretched and image.size != (width, height): image = image.resize((width, height), Image.ANTIALIAS) - if self.scale != 100: - newHeight = int((image.height / 100) * self.scale) - newWidth = int((image.width / 100) * self.scale) + if scale != 100: + newHeight = int((image.height / 100) * scale) + newWidth = int((image.width / 100) * scale) image = image.resize((newWidth, newHeight), Image.ANTIALIAS) # Paste image at correct position @@ -100,3 +102,25 @@ class Component(Component): def commandHelp(self): print('Load an image:\n path=/filepath/to/image.png') + + def savePreset(self): + # Maintain the illusion that the scale spinbox is one widget + scaleBox = self.page.spinBox_scale + stretchScaleBox = self.page.spinBox_scale_stretch + if self.page.checkBox_stretch.isChecked(): + scaleBox.setValue(stretchScaleBox.value()) + else: + stretchScaleBox.setValue(scaleBox.value()) + return super().savePreset() + + def update(self): + # Maintain the illusion that the scale spinbox is one widget + scaleBox = self.page.spinBox_scale + stretchScaleBox = self.page.spinBox_scale_stretch + if self.page.checkBox_stretch.isChecked(): + scaleBox.setVisible(False) + stretchScaleBox.setVisible(True) + else: + scaleBox.setVisible(True) + stretchScaleBox.setVisible(False) + super().update() diff --git a/src/components/image.ui b/src/components/image.ui index 1837b64..2dad127 100644 --- a/src/components/image.ui +++ b/src/components/image.ui @@ -293,6 +293,22 @@ + + + + % + + + 10 + + + 400 + + + 100 + + + diff --git a/src/components/life.py b/src/components/life.py index 08360a2..147d4d5 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -4,12 +4,13 @@ import os import math from component import Component +from toolkit import alphabetizeDict from toolkit.frame import BlankFrame, scale class Component(Component): name = 'Conway\'s Game of Life' - version = '1.0.0a' + version = '1.0.0' def widget(self, *args): super().widget(*args) @@ -329,12 +330,12 @@ class Component(Component): def savePreset(self): pr = super().savePreset() - pr['GRID'] = self.startingGrid + pr['GRID'] = alphabetizeDict(self.startingGrid) return pr def loadPreset(self, pr, *args): super().loadPreset(pr, *args) - self.startingGrid = pr['GRID'] + self.startingGrid = dict(pr['GRID']) def nearbyCoords(x, y): -- cgit v1.2.3 From c3f128806b45c427058448e6f2ff799de16da418 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 10 Aug 2017 21:57:06 -0400 Subject: Life comp shift buttons and Show Grid option --- src/component.py | 2 ++ src/components/life.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++++- src/components/life.ui | 49 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 2 deletions(-) (limited to 'src/components/life.py') diff --git a/src/component.py b/src/component.py index a1e24db..d011f1e 100644 --- a/src/component.py +++ b/src/component.py @@ -323,7 +323,9 @@ class Component(QtCore.QObject, metaclass=ComponentMetaclass): else: # Normal tracked widget setattr(self, attr, getWidgetValue(widget)) + self.sendUpdateSignal() + def sendUpdateSignal(self): if not self.core.openingProject: self.parent.drawPreview() saveValueStore = self.savePreset() diff --git a/src/components/life.py b/src/components/life.py index 147d4d5..9254126 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -25,10 +25,24 @@ class Component(Component): 'shapeType': self.page.comboBox_shapeType, 'shadow': self.page.checkBox_shadow, 'customImg': self.page.checkBox_customImg, + 'showGrid': self.page.checkBox_showGrid, 'image': self.page.lineEdit_image, }, colorWidgets={ 'color': self.page.pushButton_color, }) + self.shiftButtons = ( + self.page.toolButton_up, + self.page.toolButton_down, + self.page.toolButton_left, + self.page.toolButton_right, + ) + def shiftFunc(i): + def shift(): + self.shiftGrid(i) + return shift + shiftFuncs = [shiftFunc(i) for i in range(len(self.shiftButtons))] + for i, widget in enumerate(self.shiftButtons): + widget.clicked.connect(shiftFuncs[i]) self.page.spinBox_scale.setValue(self.scale) self.page.spinBox_scale.valueChanged.connect(self.updateGridSize) @@ -42,6 +56,24 @@ class Component(Component): self.page.lineEdit_image.setText(filename) self.update() + def shiftGrid(self, d): + def newGrid(Xchange, Ychange): + return { + (x + Xchange, y + Ychange): True + for x, y in self.startingGrid + } + + if d == 0: + newGrid = newGrid(0, -1) + elif d == 1: + newGrid = newGrid(0, 1) + elif d == 2: + newGrid = newGrid(-1, 0) + elif d == 3: + newGrid = newGrid(1, 0) + self.startingGrid = newGrid + self.sendUpdateSignal() + def update(self): self.updateGridSize() if self.page.checkBox_customImg.isChecked(): @@ -62,6 +94,9 @@ class Component(Component): self.page.label_image.setVisible(False) self.page.lineEdit_image.setVisible(False) self.page.pushButton_pickImage.setVisible(False) + enabled = (len(self.startingGrid) > 0) + for widget in self.shiftButtons: + widget.setEnabled(enabled) super().update() def previewClickEvent(self, pos, size, button): @@ -298,6 +333,22 @@ class Component(Component): shadImg = ImageChops.offset(shadImg, -2, 2) shadImg.paste(frame, box=(0, 0), mask=frame) frame = shadImg + if self.showGrid: + drawer = ImageDraw.Draw(frame) + w, h = scale(0.05, self.width, self.height, int) + for x in range(self.pxWidth, self.width, self.pxWidth): + drawer.rectangle( + ((x, 0), + (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)), + fill=self.color, + ) + return frame def gridForTick(self, tick): @@ -334,8 +385,11 @@ class Component(Component): return pr def loadPreset(self, pr, *args): - super().loadPreset(pr, *args) self.startingGrid = dict(pr['GRID']) + if self.startingGrid: + for widget in self.shiftButtons: + widget.setEnabled(True) + super().loadPreset(pr, *args) def nearbyCoords(x, y): diff --git a/src/components/life.ui b/src/components/life.ui index 3b393dd..85b2926 100644 --- a/src/components/life.ui +++ b/src/components/life.ui @@ -83,7 +83,7 @@ - 24 + 22 128 @@ -279,6 +279,13 @@ + + + + Show Grid + + + @@ -296,6 +303,46 @@ + + + + Up + + + Qt::UpArrow + + + + + + + Down + + + Qt::DownArrow + + + + + + + Left + + + Qt::LeftArrow + + + + + + + Right + + + Qt::RightArrow + + + -- cgit v1.2.3 From 64da6f14cea6eb0bf8fdffcc8277027fb0e96e54 Mon Sep 17 00:00:00 2001 From: tassaron Date: Fri, 11 Aug 2017 19:03:10 -0400 Subject: why did I use a dict here? --- src/components/life.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) (limited to 'src/components/life.py') diff --git a/src/components/life.py b/src/components/life.py index 9254126..02dd76b 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -4,7 +4,6 @@ import os import math from component import Component -from toolkit import alphabetizeDict from toolkit.frame import BlankFrame, scale @@ -16,7 +15,7 @@ class Component(Component): super().widget(*args) self.scale = 32 self.updateGridSize() - self.startingGrid = {} + self.startingGrid = set() self.page.pushButton_pickImage.clicked.connect(self.pickImage) self.trackWidgets({ 'tickRate': self.page.spinBox_tickRate, @@ -59,7 +58,7 @@ class Component(Component): def shiftGrid(self, d): def newGrid(Xchange, Ychange): return { - (x + Xchange, y + Ychange): True + (x + Xchange, y + Ychange) for x, y in self.startingGrid } @@ -105,9 +104,9 @@ class Component(Component): math.ceil((pos[1] / size[1]) * self.gridHeight) - 1 ) if button == 1: - self.startingGrid[pos] = True - elif button == 2 and pos in self.startingGrid: - self.startingGrid.pop(pos) + self.startingGrid.add(pos) + elif button == 2: + self.startingGrid.discard(pos) def updateGridSize(self): w, h = self.core.resolutions[-1].split('x') @@ -223,7 +222,7 @@ class Component(Component): ) } for cell in nearbyCoords(x, y): - if grid.get(cell) is None: + if cell not in grid: continue if cell[0] == x: if cell[1] < y: @@ -352,40 +351,40 @@ class Component(Component): return frame def gridForTick(self, tick): - '''Given a tick number over 0, returns a new grid dict of tuples''' + '''Given a tick number over 0, returns a new grid set of tuples''' lastGrid = self.tickGrids[tick - 1] def neighbours(x, y): - return [ + return { cell for cell in nearbyCoords(x, y) - if lastGrid.get(cell) is not None - ] + if cell in lastGrid + } - newGrid = {} + newGrid = set() for x, y in lastGrid: surrounding = len(neighbours(x, y)) if surrounding == 2 or surrounding == 3: - newGrid[(x, y)] = True - potentialNewCells = set([ + newGrid.add((x, y)) + potentialNewCells = { coordTup for origin in lastGrid for coordTup in list(nearbyCoords(*origin)) - ]) + } for x, y in potentialNewCells: if (x, y) in newGrid: continue surrounding = len(neighbours(x, y)) if surrounding == 3: - newGrid[(x, y)] = True + newGrid.add((x, y)) return newGrid def savePreset(self): pr = super().savePreset() - pr['GRID'] = alphabetizeDict(self.startingGrid) + pr['GRID'] = sorted(self.startingGrid) return pr def loadPreset(self, pr, *args): - self.startingGrid = dict(pr['GRID']) + self.startingGrid = set(pr['GRID']) if self.startingGrid: for widget in self.shiftButtons: widget.setEnabled(True) -- cgit v1.2.3 From a233d36ce29be459cc9cc041e77f96b9f40d0ed0 Mon Sep 17 00:00:00 2001 From: tassaron Date: Sun, 13 Aug 2017 15:43:42 -0400 Subject: graceful renderNode crash, code clean-up --- src/components/life.py | 4 +-- src/video_thread.py | 88 +++++++++++++++++++++++++++++--------------------- 2 files changed, 54 insertions(+), 38 deletions(-) (limited to 'src/components/life.py') diff --git a/src/components/life.py b/src/components/life.py index 02dd76b..2383d30 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -124,7 +124,7 @@ class Component(Component): self.tickGrids = {0: self.startingGrid} tick = 0 for frameNo in range( - self.tickRate, len(self.completeAudioArray), self.sampleSize + self.tickRate, self.audioArrayLen, self.sampleSize ): if self.parent.canceled: break @@ -133,7 +133,7 @@ class Component(Component): self.tickGrids[tick] = self.gridForTick(tick) # update progress bar - progress = int(100*(frameNo/len(self.completeAudioArray))) + progress = int(100*(frameNo/self.audioArrayLen)) if progress >= 100: progress = 100 pStr = "Computing evolution: "+str(progress)+'%' diff --git a/src/video_thread.py b/src/video_thread.py index e7e4136..63d06ee 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -32,7 +32,7 @@ log = logging.getLogger("AVP.VideoThread") class Worker(QtCore.QObject): - imageCreated = pyqtSignal(['QImage']) + imageCreated = pyqtSignal('QImage') videoCreated = pyqtSignal() progressBarUpdate = pyqtSignal(int) progressBarSetText = pyqtSignal(str) @@ -50,6 +50,7 @@ class Worker(QtCore.QObject): self.outputFile = outputFile self.inputFile = inputFile + self.hertz = 44100 self.sampleSize = 1470 # 44100 / 30 = 1470 self.canceled = False self.error = False @@ -62,30 +63,40 @@ class Worker(QtCore.QObject): to create subframes & composite them into the final frame. The resulting frames are collected in the renderQueue ''' + def err(): + self.closePipe() + self.cancelExport() + self.error = True + comp._error.emit('A render node failed critically.', str(e)) + while not self.stopped: audioI = self.compositeQueue.get() bgI = int(audioI / self.sampleSize) frame = None for layerNo, comp in enumerate(reversed((self.components))): - if layerNo in self.staticComponents: - if self.staticComponents[layerNo] is None: - # this layer was merged into a following layer - continue - # static component - if frame is None: # bottom-most layer - frame = self.staticComponents[layerNo] - else: - frame = Image.alpha_composite( - frame, self.staticComponents[layerNo] - ) - else: - # animated component - if frame is None: # bottom-most layer - frame = comp.frameRender(bgI) + try: + if layerNo in self.staticComponents: + if self.staticComponents[layerNo] is None: + # this layer was merged into a following layer + continue + # static component + if frame is None: # bottom-most layer + frame = self.staticComponents[layerNo] + else: + frame = Image.alpha_composite( + frame, self.staticComponents[layerNo] + ) + else: - frame = Image.alpha_composite( - frame, comp.frameRender(bgI) - ) + # animated component + if frame is None: # bottom-most layer + frame = comp.frameRender(bgI) + else: + frame = Image.alpha_composite( + frame, comp.frameRender(bgI) + ) + except Exception as e: + err() self.renderQueue.put([audioI, frame]) self.compositeQueue.task_done() @@ -98,7 +109,7 @@ class Worker(QtCore.QObject): ''' log.debug('Dispatching Frames for Compositing...') - for audioI in range(0, len(self.completeAudioArray), self.sampleSize): + for audioI in range(0, self.audioArrayLen, self.sampleSize): self.compositeQueue.put(audioI) def previewDispatch(self): @@ -150,17 +161,18 @@ class Worker(QtCore.QObject): self.cancelExport() return self.completeAudioArray, duration = audioFileTraits + self.audioArrayLen = len(self.completeAudioArray) else: duration = getAudioDuration(self.inputFile) - class FakeList: - def __len__(self): - return int((duration * 44100) + 44100) - 1470 - self.completeAudioArray = FakeList() + self.completeAudioArray = [] + self.audioArrayLen = int( + ((duration * self.hertz) + + self.hertz) - self.sampleSize) self.progressBarUpdate.emit(0) self.progressBarSetText.emit("Starting components...") canceledByComponent = False - initText = ", ".join([ + initText = ", ".join([ "%s) %s" % (num, str(component)) for num, component in enumerate(reversed(self.components)) ]) @@ -172,6 +184,7 @@ class Worker(QtCore.QObject): comp.preFrameRender( audioFile=self.inputFile, completeAudioArray=self.completeAudioArray, + audioArrayLen=self.audioArrayLen, sampleSize=self.sampleSize, progressBarUpdate=self.progressBarUpdate, progressBarSetText=self.progressBarSetText @@ -276,7 +289,7 @@ class Worker(QtCore.QObject): self.progressBarSetText.emit("Exporting video...") if not self.canceled: for audioI in range( - 0, len(self.completeAudioArray), self.sampleSize): + 0, self.audioArrayLen, self.sampleSize): while True: if audioI in frameBuffer or self.canceled: # if frame's in buffer, pipe it to ffmpeg @@ -295,7 +308,7 @@ class Worker(QtCore.QObject): break # increase progress bar value - completion = (audioI / len(self.completeAudioArray)) * 100 + completion = (audioI / self.audioArrayLen) * 100 if progressBarValue + 1 <= completion: progressBarValue = numpy.floor(completion) self.progressBarUpdate.emit(progressBarValue) @@ -305,15 +318,7 @@ class Worker(QtCore.QObject): numpy.seterr(all='print') - try: - self.out_pipe.stdin.close() - except BrokenPipeError: - log.error('Broken pipe to ffmpeg!') - if self.out_pipe.stderr is not None: - log.error(self.out_pipe.stderr.read()) - self.out_pipe.stderr.close() - self.error = True - self.out_pipe.wait() + self.closePipe() for comp in reversed(self.components): comp.postFrameRender() @@ -342,6 +347,17 @@ class Worker(QtCore.QObject): self.encoding.emit(False) self.videoCreated.emit() + def closePipe(self): + try: + self.out_pipe.stdin.close() + except BrokenPipeError: + log.error('Broken pipe to ffmpeg!') + if self.out_pipe.stderr is not None: + log.error(self.out_pipe.stderr.read()) + self.out_pipe.stderr.close() + self.error = True + self.out_pipe.wait() + def cancelExport(self): self.progressBarUpdate.emit(0) self.progressBarSetText.emit('Export Canceled') -- 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/components/life.py') 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 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/components/life.py') 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/components/life.py') 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 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/components/life.py') 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 05d2ebc3c69f5a876d602004f69202c5ba8b09f7 Mon Sep 17 00:00:00 2001 From: tassaron Date: Fri, 22 Apr 2022 17:09:50 -0400 Subject: make pip-installable as a package --- MANIFEST.in | 7 ++++++ setup.py | 61 ++++++++++++++++++++++++++-------------------- src/__init__.py | 6 ++--- src/__main__.py | 4 +-- src/component.py | 4 +-- src/components/color.py | 4 +-- src/components/image.py | 4 +-- src/components/life.py | 4 +-- src/components/original.py | 4 +-- src/components/sound.py | 4 +-- src/components/spectrum.py | 8 +++--- src/components/text.py | 4 +-- src/components/video.py | 8 +++--- src/components/waveform.py | 8 +++--- src/core.py | 12 ++++----- src/gui/actions.py | 2 +- src/gui/mainwindow.py | 13 +++++----- src/gui/presetmanager.py | 6 ++--- src/gui/preview_thread.py | 4 +-- src/main.py | 11 ++++----- src/toolkit/__init__.py | 2 +- src/toolkit/ffmpeg.py | 8 +++--- src/toolkit/frame.py | 2 +- src/video_thread.py | 8 +++--- 24 files changed, 106 insertions(+), 92 deletions(-) create mode 100644 MANIFEST.in (limited to 'src/components/life.py') diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2b2d794 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +recursive-include src/tests +include src/components/*.ui +include src/gui/*.ui +include src/gui/background.png +include src/encoder-options.json +global-exclude src/components/__template__.ui +global-exclude *.py[cod] diff --git a/setup.py b/setup.py index cdf4c4a..5e01229 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,39 @@ -from setuptools import setup -import os +from setuptools import setup, find_packages +from importlib import import_module +from os import path +import re -__version__ = '2.0.0rc5' +def getTextFromFile(filename, fallback): + try: + with open( + path.join(path.abspath(path.dirname(__file__)), filename), encoding="utf-8" + ) as f: + output = f.read() + except Exception: + output = fallback + return output -def package_files(directory): - paths = [] - for (path, directories, filenames) in os.walk(directory): - for filename in filenames: - paths.append(os.path.join('..', path, filename)) - return paths +PACKAGE_NAME = 'avp' +SOURCE_DIRECTORY = 'src' +SOURCE_PACKAGE_REGEX = re.compile(rf'^{SOURCE_DIRECTORY}') +PACKAGE_DESCRIPTION = 'Create audio visualization videos from a GUI or commandline' + + +avp = import_module(SOURCE_DIRECTORY) +source_packages = find_packages(include=[SOURCE_DIRECTORY, f'{SOURCE_DIRECTORY}.*']) +proj_packages = [SOURCE_PACKAGE_REGEX.sub(PACKAGE_NAME, name) for name in source_packages] setup( name='audio_visualizer_python', - version=__version__, + version=avp.__version__, url='https://github.com/djfun/audio-visualizer-python/tree/feature-newgui', license='MIT', - description='Create audio visualization videos from a GUI or commandline', - long_description="Create customized audio visualization videos and save " - "them as Projects to continue editing later. Different components can " - "be added and layered to add visualizers, images, videos, gradients, " - "text, etc. Use Projects created in the GUI with commandline mode to " - "automate your video production workflow without any complex syntax.", + description=PACKAGE_DESCRIPTION, + author=getTextFromFile('AUTHORS', 'djfun, tassaron'), + long_description=getTextFromFile('README.md', PACKAGE_DESCRIPTION), classifiers=[ 'Development Status :: 4 - Beta', 'License :: OSI Approved :: MIT License', @@ -35,19 +45,18 @@ setup( 'visualizer', 'visualization', 'commandline video', 'video editor', 'ffmpeg', 'podcast' ], - packages=[ - 'avpython', - 'avpython.toolkit', - 'avpython.components' + packages=proj_packages, + package_dir={PACKAGE_NAME: SOURCE_DIRECTORY}, + include_package_data=True, + install_requires=[ + 'Pillow-SIMD', + 'PyQt5', + 'numpy', + 'pytest' ], - package_dir={'avpython': 'src'}, - package_data={ - 'avpython': package_files('src'), - }, - install_requires=['Pillow-SIMD', 'PyQt5', 'numpy'], entry_points={ 'gui_scripts': [ - 'avp = avpython.main:main' + f'avp = {PACKAGE_NAME}.main:main' ], } ) diff --git a/src/__init__.py b/src/__init__.py index 73f174a..08131ce 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -3,6 +3,9 @@ import os import logging +__version__ = '2.0.0rc6' + + class Logger(logging.getLoggerClass()): ''' Custom Logger class to handle custom VERBOSE log level. @@ -31,6 +34,3 @@ if getattr(sys, 'frozen', False): else: # unfrozen wd = os.path.dirname(os.path.realpath(__file__)) - -# make relative imports work when using /src as a package -sys.path.insert(0, wd) diff --git a/src/__main__.py b/src/__main__.py index 3babeae..3206bc8 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,5 +1,5 @@ -# Allows for launching with python3 -m avpython +# Allows for launching with python3 -m avp -from avpython.main import main +from .main import main main() diff --git a/src/component.py b/src/component.py index f3ee188..33c7657 100644 --- a/src/component.py +++ b/src/component.py @@ -11,8 +11,8 @@ import time import logging from copy import copy -from toolkit.frame import BlankFrame -from toolkit import ( +from .toolkit.frame import BlankFrame +from .toolkit import ( getWidgetValue, setWidgetValue, connectWidget, rgbFromString, blockSignals ) diff --git a/src/components/color.py b/src/components/color.py index 7d4f86d..6336194 100644 --- a/src/components/color.py +++ b/src/components/color.py @@ -4,8 +4,8 @@ from PyQt5.QtGui import QColor from PIL.ImageQt import ImageQt import os -from component import Component -from toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor +from ..component import Component +from ..toolkit.frame import BlankFrame, FloodFrame, FramePainter, PaintColor class Component(Component): diff --git a/src/components/image.py b/src/components/image.py index dd363bf..42f9564 100644 --- a/src/components/image.py +++ b/src/components/image.py @@ -2,8 +2,8 @@ from PIL import Image, ImageDraw, ImageEnhance from PyQt5 import QtGui, QtCore, QtWidgets import os -from component import Component -from toolkit.frame import BlankFrame +from ..component import Component +from ..toolkit.frame import BlankFrame class Component(Component): diff --git a/src/components/life.py b/src/components/life.py index 7a610eb..94704bc 100644 --- a/src/components/life.py +++ b/src/components/life.py @@ -4,8 +4,8 @@ from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter import os import math -from component import Component -from toolkit.frame import BlankFrame, scale +from ..component import Component +from ..toolkit.frame import BlankFrame, scale class Component(Component): diff --git a/src/components/original.py b/src/components/original.py index f886374..80228fe 100644 --- a/src/components/original.py +++ b/src/components/original.py @@ -6,8 +6,8 @@ import os import time from copy import copy -from component import Component -from toolkit.frame import BlankFrame +from ..component import Component +from ..toolkit.frame import BlankFrame class Component(Component): diff --git a/src/components/sound.py b/src/components/sound.py index 18d2a65..118ea23 100644 --- a/src/components/sound.py +++ b/src/components/sound.py @@ -1,8 +1,8 @@ from PyQt5 import QtGui, QtCore, QtWidgets import os -from component import Component -from toolkit.frame import BlankFrame +from ..component import Component +from ..toolkit.frame import BlankFrame class Component(Component): diff --git a/src/components/spectrum.py b/src/components/spectrum.py index 6675f5b..d1f8fb6 100644 --- a/src/components/spectrum.py +++ b/src/components/spectrum.py @@ -6,10 +6,10 @@ import subprocess import time import logging -from component import Component -from toolkit.frame import BlankFrame, scale -from toolkit import checkOutput, connectWidget -from toolkit.ffmpeg import ( +from ..component import Component +from ..toolkit.frame import BlankFrame, scale +from ..toolkit import checkOutput, connectWidget +from ..toolkit.ffmpeg import ( openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound ) diff --git a/src/components/text.py b/src/components/text.py index 32a108e..e8c5a9c 100644 --- a/src/components/text.py +++ b/src/components/text.py @@ -4,8 +4,8 @@ from PyQt5 import QtGui, QtCore, QtWidgets import os import logging -from component import Component -from toolkit.frame import FramePainter, PaintColor +from ..component import Component +from ..toolkit.frame import FramePainter, PaintColor log = logging.getLogger('AVP.Components.Text') diff --git a/src/components/video.py b/src/components/video.py index 8ad21b5..070940d 100644 --- a/src/components/video.py +++ b/src/components/video.py @@ -5,10 +5,10 @@ import math import subprocess import logging -from component import Component -from toolkit.frame import BlankFrame, scale -from toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo -from toolkit import checkOutput +from ..component import Component +from ..toolkit.frame import BlankFrame, scale +from ..toolkit.ffmpeg import openPipe, closePipe, testAudioStream, FfmpegVideo +from ..toolkit import checkOutput log = logging.getLogger('AVP.Components.Video') diff --git a/src/components/waveform.py b/src/components/waveform.py index cbfc47f..1a6035f 100644 --- a/src/components/waveform.py +++ b/src/components/waveform.py @@ -6,10 +6,10 @@ import math import subprocess import logging -from component import Component -from toolkit.frame import BlankFrame, scale -from toolkit import checkOutput -from toolkit.ffmpeg import ( +from ..component import Component +from ..toolkit.frame import BlankFrame, scale +from ..toolkit import checkOutput +from ..toolkit.ffmpeg import ( openPipe, closePipe, getAudioDuration, FfmpegVideo, exampleSound ) diff --git a/src/core.py b/src/core.py index d7445c9..bc6f9b4 100644 --- a/src/core.py +++ b/src/core.py @@ -9,12 +9,12 @@ import json from importlib import import_module import logging -import toolkit +from . import toolkit log = logging.getLogger('AVP.Core') STDOUT_LOGLVL = logging.WARNING -FILE_LOGLVL = None +FILE_LOGLVL = logging.ERROR class Core: @@ -47,7 +47,7 @@ class Core: yield name log.debug('Importing component modules') self.modules = [ - import_module('components.%s' % name) + import_module('.components.%s' % name, __package__) for name in findComponents() ] # store canonical module names and indexes @@ -426,7 +426,7 @@ class Core: def newVideoWorker(self, loader, audioFile, outputPath): '''loader is MainWindow or Command object which must own the thread''' - import video_thread + from . import video_thread self.videoThread = QtCore.QThread(loader) videoWorker = video_thread.Worker( loader, audioFile, outputPath, self.selectedComponents @@ -450,8 +450,8 @@ class Core: @classmethod def storeSettings(cls): '''Store settings/paths to directories as class variables''' - from __init__ import wd - from toolkit.ffmpeg import findFfmpeg + from .__init__ import wd + from .toolkit.ffmpeg import findFfmpeg cls.wd = wd dataDir = QtCore.QStandardPaths.writableLocation( diff --git a/src/gui/actions.py b/src/gui/actions.py index 8e867b9..eb7b953 100644 --- a/src/gui/actions.py +++ b/src/gui/actions.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QUndoCommand import os from copy import copy -from core import Core +from ..core import Core # =~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~==~=~=~=~=~=~=~=~=~=~=~=~=~=~ diff --git a/src/gui/mainwindow.py b/src/gui/mainwindow.py index 75534c2..da8370d 100644 --- a/src/gui/mainwindow.py +++ b/src/gui/mainwindow.py @@ -16,12 +16,12 @@ import filecmp import time import logging -from core import Core -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 ( +from ..core import Core +from . import preview_thread +from .preview_win import PreviewWindow +from .presetmanager import PresetManager +from .actions import * +from ..toolkit import ( disableWhenEncoding, disableWhenOpeningProject, checkOutput, blockSignals ) @@ -65,7 +65,6 @@ class MainWindow(QtWidgets.QMainWindow): self.settings = Core.settings # Register clean-up functions - signal.signal(signal.SIGINT, self.terminate) atexit.register(self.cleanUp) # Create stack of undoable user actions diff --git a/src/gui/presetmanager.py b/src/gui/presetmanager.py index 2445760..1e47a7f 100644 --- a/src/gui/presetmanager.py +++ b/src/gui/presetmanager.py @@ -7,9 +7,9 @@ import string import os import logging -from toolkit import badName -from core import Core -from gui.actions import * +from ..toolkit import badName +from ..core import Core +from .actions import * log = logging.getLogger('AVP.Gui.PresetManager') diff --git a/src/gui/preview_thread.py b/src/gui/preview_thread.py index d3e0581..7829476 100644 --- a/src/gui/preview_thread.py +++ b/src/gui/preview_thread.py @@ -10,8 +10,8 @@ from queue import Queue, Empty import os import logging -from toolkit.frame import Checkerboard -from toolkit import disableWhenOpeningProject +from ..toolkit.frame import Checkerboard +from ..toolkit import disableWhenOpeningProject log = logging.getLogger("AVP.Gui.PreviewThread") diff --git a/src/main.py b/src/main.py index 126e4a8..5fabda3 100644 --- a/src/main.py +++ b/src/main.py @@ -3,7 +3,7 @@ import sys import os import logging -from __init__ import wd +from .__init__ import wd log = logging.getLogger('AVP.Main') @@ -12,6 +12,7 @@ log = logging.getLogger('AVP.Main') def main(): app = QtWidgets.QApplication(sys.argv) app.setApplicationName("audio-visualizer") + proj = None # Determine mode mode = 'GUI' @@ -23,19 +24,17 @@ def main(): else: # opening a project file with gui proj = sys.argv[1] - else: - # normal gui launch - proj = None # Launch program if mode == 'commandline': - from command import Command + from .command import Command main = Command() + main.parseArgs() log.debug("Finished creating command object") elif mode == 'GUI': - from gui.mainwindow import MainWindow + from .gui.mainwindow import MainWindow window = uic.loadUi(os.path.join(wd, "gui", "mainwindow.ui")) # window.adjustSize() diff --git a/src/toolkit/__init__.py b/src/toolkit/__init__.py index 3fca275..55e5f84 100644 --- a/src/toolkit/__init__.py +++ b/src/toolkit/__init__.py @@ -1 +1 @@ -from toolkit.common import * +from .common import * diff --git a/src/toolkit/ffmpeg.py b/src/toolkit/ffmpeg.py index 419d491..3298c04 100644 --- a/src/toolkit/ffmpeg.py +++ b/src/toolkit/ffmpeg.py @@ -10,8 +10,8 @@ import signal from queue import PriorityQueue import logging -import core -from toolkit.common import checkOutput, pipeWrapper +from .. import core +from .common import checkOutput, pipeWrapper log = logging.getLogger('AVP.Toolkit.Ffmpeg') @@ -90,7 +90,7 @@ class FfmpegVideo: self.frameBuffer.task_done() def fillBuffer(self): - from component import ComponentError + from ..component import ComponentError if core.Core.logEnabled: logFilename = os.path.join( core.Core.logDir, 'render_%s.log' % str(self.component.compPos) @@ -144,7 +144,7 @@ def openPipe(commandList, **kwargs): def closePipe(pipe): pipe.stdout.close() - pipe.send_signal(signal.SIGINT) + pipe.send_signal(signal.SIGTERM) def findFfmpeg(): diff --git a/src/toolkit/frame.py b/src/toolkit/frame.py index 0e200b5..f2511fe 100644 --- a/src/toolkit/frame.py +++ b/src/toolkit/frame.py @@ -9,7 +9,7 @@ import os import math import logging -import core +from .. import core log = logging.getLogger('AVP.Toolkit.Frame') diff --git a/src/video_thread.py b/src/video_thread.py index 0a39f28..31331a3 100644 --- a/src/video_thread.py +++ b/src/video_thread.py @@ -19,9 +19,9 @@ import time import signal import logging -from component import ComponentError -from toolkit.frame import Checkerboard -from toolkit.ffmpeg import ( +from .component import ComponentError +from .toolkit.frame import Checkerboard +from .toolkit.ffmpeg import ( openPipe, readAudioFile, getAudioDuration, createFfmpegCommand ) @@ -400,7 +400,7 @@ class Worker(QtCore.QObject): comp.cancel() try: - self.out_pipe.send_signal(signal.SIGINT) + self.out_pipe.send_signal(signal.SIGTERM) except Exception: pass -- cgit v1.2.3