From 8b253717f7c0dd3fe73b1f3474fea2176e8f19ba Mon Sep 17 00:00:00 2001 From: tassaron Date: Wed, 9 Aug 2017 16:46:59 -0400 Subject: [PATCH 1/4] Conway's Game of Life component --- src/components/__template__.ui | 119 +++++++++++++ src/components/life.py | 155 +++++++++++++++++ src/components/life.ui | 302 +++++++++++++++++++++++++++++++++ src/mainwindow.py | 16 ++ 4 files changed, 592 insertions(+) create mode 100644 src/components/__template__.ui create mode 100644 src/components/life.py create mode 100644 src/components/life.ui diff --git a/src/components/__template__.ui b/src/components/__template__.ui new file mode 100644 index 0000000..301a2b7 --- /dev/null +++ b/src/components/__template__.ui @@ -0,0 +1,119 @@ + + + Form + + + + 0 + 0 + 586 + 197 + + + + Form + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 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'] diff --git a/src/components/life.ui b/src/components/life.ui new file mode 100644 index 0000000..88f8eca --- /dev/null +++ b/src/components/life.ui @@ -0,0 +1,302 @@ + + + Form + + + + 0 + 0 + 586 + 197 + + + + Form + + + + + + + + + + + + Simulation Speed + + + + + + + frames per tick + + + 1 + + + 30 + + + 15 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 16777215 + + + + 0,0,0 + + + + + + + + + + + Grid Scale + + + + + + + 24 + + + 128 + + + 32 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Color + + + + + + + + 0 + 16777215 + + + + 0,0,0 + + + + + + + + 0 + 0 + + + + + 32 + 32 + + + + + + + false + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Shape + + + + + + + + Rectangle + + + + + Circle + + + + + Lilypad + + + + + Pac-Man + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Shadow + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:600;">Click the preview window to place a cell. Right-click to remove.</span></p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- A cell with less than 2 neighbours will die from underpopulation</p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- A cell with more than 3 neighbours will die from overpopulation.</p> +<p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- An empty space surrounded by 3 live cells will cause reproduction.</p></body></html> + + + 80 + + + Qt::NoTextInteraction + + + false + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/mainwindow.py b/src/mainwindow.py index 1c8806d..789a6e7 100644 --- a/src/mainwindow.py +++ b/src/mainwindow.py @@ -50,6 +50,22 @@ class PreviewWindow(QtWidgets.QLabel): 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( From cacab464c7655a1c0cedcfe95b63609f55d78322 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 10 Aug 2017 00:46:31 -0400 Subject: [PATCH 2/4] 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 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 + + From 9732f3bdebc7fbeb944d71f313d3c0797b3dbcd4 Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 10 Aug 2017 00:48:07 -0400 Subject: [PATCH 3/4] rm garbage file --- .gitignore | 1 + src/.goutputstream-67IS4Y | 976 -------------------------------------- 2 files changed, 1 insertion(+), 976 deletions(-) delete mode 100644 src/.goutputstream-67IS4Y diff --git a/.gitignore b/.gitignore index 7cec615..916c6c1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ env/* ffmpeg *.bak *~ +*.goutput* \ No newline at end of file diff --git a/src/.goutputstream-67IS4Y b/src/.goutputstream-67IS4Y deleted file mode 100644 index 789a6e7..0000000 --- a/src/.goutputstream-67IS4Y +++ /dev/null @@ -1,976 +0,0 @@ -''' - When using GUI mode, this module's object (the main window) takes - user input to construct a program state (stored in the Core object). - This shows a preview of the video being created and allows for saving - projects and exporting the video at a later time. -''' -from PyQt5 import QtCore, QtGui, uic, QtWidgets -from PyQt5.QtWidgets import QMenu, QShortcut -from PIL import Image -from queue import Queue -import sys -import os -import signal -import filecmp -import time - -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 From 8baa24e87847a0c7c530cbb55196103ce9cc511c Mon Sep 17 00:00:00 2001 From: tassaron Date: Thu, 10 Aug 2017 09:12:48 -0400 Subject: [PATCH 4/4] added connected path shape to Life --- src/components/life.py | 104 ++++++++++++++++++++++++++++++----------- src/components/life.ui | 7 ++- 2 files changed, 82 insertions(+), 29 deletions(-) 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