diff --git a/components/__base__.py b/components/__base__.py index 4fdf31f..88f22d4 100644 --- a/components/__base__.py +++ b/components/__base__.py @@ -1,7 +1,18 @@ -from PyQt4 import QtGui +from PyQt4 import QtGui, QtCore +from PIL import Image -class Component: +class Component(QtCore.QObject): + '''A base class for components to inherit from''' + + # modified = QtCore.pyqtSignal(int, bool) + + def __init__(self, moduleIndex, compPos): + super().__init__() + self.currentPreset = None + self.moduleIndex = moduleIndex + self.compPos = compPos + def __str__(self): return self.__doc__ @@ -10,18 +21,38 @@ class Component: return 1 def cancel(self): - # make sure your component responds to these variables in frameRender() + # please stop any lengthy process in response to this variable self.canceled = True def reset(self): self.canceled = False + def update(self): + self.modified.emit(self.compPos, self.savePreset()) + # read your widget values, then call super().update() + + def loadPreset(self, presetDict, presetName): + '''Children should take (presetDict, presetName=None) as args''' + + # Use super().loadPreset(presetDict, presetName) + # Then update your widgets using the preset dict + self.currentPreset = presetName \ + if presetName != None else presetDict['preset'] + ''' + def savePreset(self): + return {} + ''' def preFrameRender(self, **kwargs): for var, value in kwargs.items(): exec('self.%s = value' % var) + def blankFrame(self, width, height): + return Image.new("RGBA", (width, height), (0, 0, 0, 0)) + def pickColor(self): - color = QtGui.QColorDialog.getColor() + dialog = QtGui.QColorDialog() + dialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True) + color = dialog.getColor() if color.isValid(): RGBstring = '%s,%s,%s' % ( str(color.red()), str(color.green()), str(color.blue())) @@ -57,7 +88,7 @@ class Component: return page def update(self): - # read widget values + super().update() self.parent.drawPreview() def previewRender(self, previewWorker): @@ -72,12 +103,6 @@ class Component: image = Image.new("RGBA", (width, height), (0,0,0,0)) return image - def loadPreset(self, presetDict): - # update widgets using a preset dict - - def savePreset(self): - return {} - def cancel(self): self.canceled = True diff --git a/components/color.py b/components/color.py index b050fbd..36f3906 100644 --- a/components/color.py +++ b/components/color.py @@ -7,6 +7,9 @@ from . import __base__ class Component(__base__.Component): '''Color''' + + modified = QtCore.pyqtSignal(int, dict) + def widget(self, parent): self.parent = parent page = uic.loadUi(os.path.join( @@ -20,14 +23,14 @@ class Component(__base__.Component): page.lineEdit_color1.setText('%s,%s,%s' % self.color1) page.lineEdit_color2.setText('%s,%s,%s' % self.color2) - btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*self.color1).name() - btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + btnStyle2 = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*self.color2).name() - page.pushButton_color1.setStyleSheet(btnStyle) - page.pushButton_color2.setStyleSheet(btnStyle) + page.pushButton_color1.setStyleSheet(btnStyle1) + page.pushButton_color2.setStyleSheet(btnStyle2) page.pushButton_color1.clicked.connect(lambda: self.pickColor(1)) page.pushButton_color2.clicked.connect(lambda: self.pickColor(2)) @@ -50,6 +53,7 @@ class Component(__base__.Component): self.x = self.page.spinBox_x.value() self.y = self.page.spinBox_y.value() self.parent.drawPreview() + super().update() def previewRender(self, previewWorker): width = int(previewWorker.core.settings.value('outputWidth')) @@ -67,23 +71,26 @@ class Component(__base__.Component): def drawFrame(self, width, height): r, g, b = self.color1 - return Image.new("RGBA", (width, height), (r, g, b, 255)) + return self.blankFrame(width, height) + + def loadPreset(self, pr, presetName=None): + super().loadPreset(pr, presetName) - def loadPreset(self, pr): self.page.lineEdit_color1.setText('%s,%s,%s' % pr['color1']) self.page.lineEdit_color2.setText('%s,%s,%s' % pr['color2']) - btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + btnStyle1 = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*pr['color1']).name() - btnStyle = "QPushButton { background-color : %s; outline: none; }" \ + btnStyle2 = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*pr['color2']).name() - self.page.pushButton_color1.setStyleSheet(btnStyle) - self.page.pushButton_color2.setStyleSheet(btnStyle) + self.page.pushButton_color1.setStyleSheet(btnStyle1) + self.page.pushButton_color2.setStyleSheet(btnStyle2) def savePreset(self): return { + 'preset': self.currentPreset, 'color1': self.color1, 'color2': self.color2, } diff --git a/components/image.py b/components/image.py index f9a92ca..b6aa29b 100644 --- a/components/image.py +++ b/components/image.py @@ -6,6 +6,9 @@ from . import __base__ class Component(__base__.Component): '''Image''' + + modified = QtCore.pyqtSignal(int, dict) + def widget(self, parent): self.parent = parent self.settings = parent.settings @@ -17,15 +20,25 @@ class Component(__base__.Component): page.lineEdit_image.textChanged.connect(self.update) page.pushButton_image.clicked.connect(self.pickImage) + page.spinBox_scale.valueChanged.connect(self.update) + page.checkBox_stretch.stateChanged.connect(self.update) + page.spinBox_x.valueChanged.connect(self.update) + page.spinBox_y.valueChanged.connect(self.update) self.page = page return page def update(self): self.imagePath = self.page.lineEdit_image.text() + self.scale = self.page.spinBox_scale.value() + self.xPosition = self.page.spinBox_x.value() + self.yPosition = self.page.spinBox_y.value() + self.stretched = self.page.checkBox_stretch.isChecked() self.parent.drawPreview() + super().update() def previewRender(self, previewWorker): + self.imageFormats = previewWorker.core.imageFormats width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) return self.drawFrame(width, height) @@ -40,27 +53,42 @@ class Component(__base__.Component): return self.drawFrame(width, height) def drawFrame(self, width, height): - frame = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + frame = self.blankFrame(width, height) if self.imagePath and os.path.exists(self.imagePath): image = Image.open(self.imagePath) - if image.size != (width, height): + if self.stretched and image.size != (width, height): image = image.resize((width, height), Image.ANTIALIAS) - frame.paste(image) + if self.scale != 100: + newHeight = int((image.height / 100) * self.scale) + newWidth = int((image.width / 100) * self.scale) + image = image.resize((newWidth, newHeight), Image.ANTIALIAS) + frame.paste(image, box=(self.xPosition, self.yPosition)) return frame - def loadPreset(self, pr): + def loadPreset(self, pr, presetName=None): + super().loadPreset(pr, presetName) self.page.lineEdit_image.setText(pr['image']) + self.page.spinBox_scale.setValue(pr['scale']) + self.page.spinBox_x.setValue(pr['x']) + self.page.spinBox_y.setValue(pr['y']) + self.page.checkBox_stretch.setChecked(pr['stretched']) def savePreset(self): return { + 'preset': self.currentPreset, 'image': self.imagePath, + 'scale': self.scale, + 'stretched': self.stretched, + 'x': self.xPosition, + 'y': self.yPosition, } def pickImage(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) filename = QtGui.QFileDialog.getOpenFileName( - self.page, "Choose Image", imgDir, "Image Files (*.jpg *.png)") - if filename: + self.page, "Choose Image", imgDir, + "Image Files (%s)" % " ".join(self.imageFormats)) + if filename: self.settings.setValue("backgroundDir", os.path.dirname(filename)) self.page.lineEdit_image.setText(filename) self.update() diff --git a/components/image.ui b/components/image.ui index 3cd5b1b..6df03a5 100644 --- a/components/image.ui +++ b/components/image.ui @@ -124,8 +124,11 @@ 16777215 + + -10000 + - 999999999 + 10000 @@ -163,10 +166,10 @@ - 0 + -1000 - 999999999 + 1000 0 @@ -177,6 +180,65 @@ + + + + + + Stretch + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + Scale + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QAbstractSpinBox::UpDownArrows + + + % + + + 10 + + + 400 + + + 100 + + + + + diff --git a/components/original.py b/components/original.py index 4d0e83b..5e2f9d4 100644 --- a/components/original.py +++ b/components/original.py @@ -1,9 +1,8 @@ import numpy from PIL import Image, ImageDraw -from PyQt4 import uic, QtGui +from PyQt4 import uic, QtGui, QtCore from PyQt4.QtGui import QColor import os -import random from . import __base__ import time from copy import copy @@ -11,6 +10,9 @@ from copy import copy class Component(__base__.Component): '''Original Audio Visualization''' + + modified = QtCore.pyqtSignal(int, dict) + def widget(self, parent): self.parent = parent self.visColor = (255, 255, 255) @@ -36,8 +38,11 @@ class Component(__base__.Component): self.layout = self.page.comboBox_visLayout.currentIndex() self.visColor = self.RGBFromString(self.page.lineEdit_visColor.text()) self.parent.drawPreview() + super().update() + + def loadPreset(self, pr, presetName=None): + super().loadPreset(pr, presetName) - def loadPreset(self, pr): self.page.lineEdit_visColor.setText('%s,%s,%s' % pr['visColor']) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*pr['visColor']).name() @@ -46,6 +51,7 @@ class Component(__base__.Component): def savePreset(self): return { + 'preset': self.currentPreset, 'layout': self.layout, 'visColor': self.visColor, } @@ -139,7 +145,7 @@ class Component(__base__.Component): bF = width / 64 bH = bF / 2 bQ = bF / 4 - imTop = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + imTop = self.blankFrame(width, height) draw = ImageDraw.Draw(imTop) r, g, b = color color2 = (r, g, b, 125) @@ -157,7 +163,7 @@ class Component(__base__.Component): imBottom = imTop.transpose(Image.FLIP_TOP_BOTTOM) - im = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + im = self.blankFrame(width, height) if layout == 0: y = 0 - int(height/100*43) diff --git a/components/text.py b/components/text.py index 6cdc0dd..f8ef7b3 100644 --- a/components/text.py +++ b/components/text.py @@ -9,8 +9,11 @@ from . import __base__ class Component(__base__.Component): '''Title Text''' - def __init__(self): - super().__init__() + + modified = QtCore.pyqtSignal(int, dict) + + def __init__(self, *args): + super().__init__(*args) self.titleFont = QFont() def widget(self, parent): @@ -31,7 +34,7 @@ class Component(__base__.Component): page.comboBox_textAlign.addItem("Right") page.lineEdit_textColor.setText('%s,%s,%s' % self.textColor) - page.pushButton_textColor.clicked.connect(lambda: self.pickColor()) + page.pushButton_textColor.clicked.connect(self.pickColor) btnStyle = "QPushButton { background-color : %s; outline: none; }" \ % QColor(*self.textColor).name() page.pushButton_textColor.setStyleSheet(btnStyle) @@ -62,6 +65,7 @@ class Component(__base__.Component): self.textColor = self.RGBFromString( self.page.lineEdit_textColor.text()) self.parent.drawPreview() + super().update() def getXY(self): '''Returns true x, y after considering alignment settings''' @@ -78,7 +82,9 @@ class Component(__base__.Component): x = self.xPosition - offset return x, self.yPosition - def loadPreset(self, pr): + def loadPreset(self, pr, presetName=None): + super().loadPreset(pr, presetName) + self.page.lineEdit_title.setText(pr['title']) font = QFont() font.fromString(pr['titleFont']) @@ -94,6 +100,7 @@ class Component(__base__.Component): def savePreset(self): return { + 'preset': self.currentPreset, 'title': self.title, 'titleFont': self.titleFont.toString(), 'alignment': self.alignment, @@ -119,7 +126,7 @@ class Component(__base__.Component): def addText(self, width, height): x, y = self.getXY() - im = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + im = self.blankFrame(width, height) image = ImageQt(im) painter = QPainter(image) diff --git a/components/video.py b/components/video.py index b28b81e..3d43a18 100644 --- a/components/video.py +++ b/components/video.py @@ -5,12 +5,22 @@ import subprocess import threading from queue import PriorityQueue from . import __base__ - + + class Video: '''Video Component Frame-Fetcher''' def __init__(self, **kwargs): - mandatoryArgs = ['ffmpeg', 'videoPath', 'width', 'height', - 'frameRate', 'chunkSize', 'parent'] + mandatoryArgs = [ + 'ffmpeg', # path to ffmpeg, usually core.FFMPEG_BIN + 'videoPath', + 'width', + 'height', + 'scale', # percentage scale + 'frameRate', # frames per second + 'chunkSize', # number of bytes in one frame + 'parent', # mainwindow object + 'component', # component object + ] for arg in mandatoryArgs: try: exec('self.%s = kwargs[arg]' % arg) @@ -31,7 +41,8 @@ class Video: '-i', self.videoPath, '-f', 'image2pipe', '-pix_fmt', 'rgba', - '-filter:v', 'scale='+str(self.width)+':'+str(self.height), + '-filter:v', 'scale=%s:%s' % + scale(self.scale, self.width, self.height, str), '-vcodec', 'rawvideo', '-', ] @@ -50,7 +61,9 @@ class Video: while True: if num in self.finishedFrames: image = self.finishedFrames.pop(num) - return Image.frombytes('RGBA', (self.width, self.height), image) + return finalizeFrame( + self.component, image, self.width, self.height) + i, image = self.frameBuffer.get() self.finishedFrames[i] = image self.frameBuffer.task_done() @@ -78,6 +91,9 @@ class Video: class Component(__base__.Component): '''Video''' + + modified = QtCore.pyqtSignal(int, dict) + def widget(self, parent): self.parent = parent self.settings = parent.settings @@ -93,6 +109,10 @@ class Component(__base__.Component): page.lineEdit_video.textChanged.connect(self.update) page.pushButton_video.clicked.connect(self.pickVideo) page.checkBox_loop.stateChanged.connect(self.update) + page.checkBox_distort.stateChanged.connect(self.update) + page.spinBox_scale.valueChanged.connect(self.update) + page.spinBox_x.valueChanged.connect(self.update) + page.spinBox_y.valueChanged.connect(self.update) self.page = page return page @@ -100,15 +120,21 @@ class Component(__base__.Component): def update(self): self.videoPath = self.page.lineEdit_video.text() self.loopVideo = self.page.checkBox_loop.isChecked() + self.distort = self.page.checkBox_distort.isChecked() + self.scale = self.page.spinBox_scale.value() + self.xPosition = self.page.spinBox_x.value() + self.yPosition = self.page.spinBox_y.value() self.parent.drawPreview() + super().update() def previewRender(self, previewWorker): + self.videoFormats = previewWorker.core.videoFormats width = int(previewWorker.core.settings.value('outputWidth')) height = int(previewWorker.core.settings.value('outputHeight')) - self.chunkSize = 4*width*height + self.updateChunksize(width, height) frame = self.getPreviewFrame(width, height) if not frame: - return Image.new("RGBA", (width, height), (0, 0, 0, 0)) + return self.blankFrame(width, height) else: return frame @@ -116,32 +142,49 @@ class Component(__base__.Component): super().preFrameRender(**kwargs) width = int(self.worker.core.settings.value('outputWidth')) height = int(self.worker.core.settings.value('outputHeight')) - self.chunkSize = 4*width*height + self.blankFrame_ = self.blankFrame(width, height) + self.updateChunksize(width, height) self.video = Video( ffmpeg=self.parent.core.FFMPEG_BIN, videoPath=self.videoPath, width=width, height=height, chunkSize=self.chunkSize, frameRate=int(self.settings.value("outputFrameRate")), - parent=self.parent, loopVideo=self.loopVideo - ) + parent=self.parent, loopVideo=self.loopVideo, + component=self, scale=self.scale + ) if os.path.exists(self.videoPath) else None def frameRender(self, moduleNo, arrayNo, frameNo): - return self.video.frame(frameNo) + if self.video: + return self.video.frame(frameNo) + else: + return self.blankFrame_ - def loadPreset(self, pr): + def loadPreset(self, pr, presetName=None): + super().loadPreset(pr, presetName) self.page.lineEdit_video.setText(pr['video']) + self.page.checkBox_loop.setChecked(pr['loop']) + self.page.checkBox_distort.setChecked(pr['distort']) + self.page.spinBox_scale.setValue(pr['scale']) + self.page.spinBox_x.setValue(pr['x']) + self.page.spinBox_y.setValue(pr['y']) def savePreset(self): return { + 'preset': self.currentPreset, 'video': self.videoPath, + 'loop': self.loopVideo, + 'distort': self.distort, + 'scale': self.scale, + 'x': self.xPosition, + 'y': self.yPosition, } def pickVideo(self): imgDir = self.settings.value("backgroundDir", os.path.expanduser("~")) filename = QtGui.QFileDialog.getOpenFileName( self.page, "Choose Video", - imgDir, "Video Files (*.mp4 *.mov)" + imgDir, "Video Files (%s)" % " ".join(self.videoFormats) ) - if filename: + if filename: self.settings.setValue("backgroundDir", os.path.dirname(filename)) self.page.lineEdit_video.setText(filename) self.update() @@ -149,13 +192,15 @@ class Component(__base__.Component): def getPreviewFrame(self, width, height): if not self.videoPath or not os.path.exists(self.videoPath): return + command = [ self.parent.core.FFMPEG_BIN, '-thread_queue_size', '512', '-i', self.videoPath, '-f', 'image2pipe', '-pix_fmt', 'rgba', - '-filter:v', 'scale='+str(width)+':'+str(height), + '-filter:v', 'scale=%s:%s' % + scale(self.scale, width, height, str), '-vcodec', 'rawvideo', '-', '-ss', '90', '-vframes', '1', @@ -165,7 +210,47 @@ class Component(__base__.Component): stderr=subprocess.DEVNULL, bufsize=10**8 ) byteFrame = pipe.stdout.read(self.chunkSize) - image = Image.frombytes('RGBA', (width, height), byteFrame) + frame = finalizeFrame(self, byteFrame, width, height) pipe.stdout.close() pipe.kill() - return image + + return frame + + def updateChunksize(self, width, height): + if self.scale != 100 and not self.distort: + width, height = scale(self.scale, width, height, int) + self.chunkSize = 4*width*height + +def scale(scale, width, height, returntype=None): + width = (float(width) / 100.0) * float(scale) + height = (float(height) / 100.0) * float(scale) + if returntype == str: + return (str(int(width)), str(int(height))) + elif returntype == int: + return (int(width), int(height)) + else: + return (width, height) + +def finalizeFrame(self, imageData, width, height): + if self.distort: + try: + image = Image.frombytes( + 'RGBA', + (width, height), + imageData) + except ValueError: + print('#### ignored invalid data caused by distortion ####') + image = self.blankFrame(width, height) + else: + image = Image.frombytes( + 'RGBA', + scale(self.scale, width, height, int), + imageData) + + if self.scale != 100 \ + or self.xPosition != 0 or self.yPosition != 0: + frame = self.blankFrame(width, height) + frame.paste(image, box=(self.xPosition, self.yPosition)) + else: + frame = image + return frame diff --git a/components/video.ui b/components/video.ui index 6a01368..f05e8a5 100644 --- a/components/video.ui +++ b/components/video.ui @@ -111,7 +111,7 @@ - + 0 @@ -124,8 +124,11 @@ 16777215 + + -10000 + - 999999999 + 10000 @@ -163,10 +166,10 @@ - 0 + -10000 - 999999999 + 10000 0 @@ -202,6 +205,42 @@ + + + + Distort by scale + + + + + + + Scale + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + QAbstractSpinBox::UpDownArrows + + + % + + + 10 + + + 400 + + + 100 + + + @@ -217,6 +256,9 @@ + + + diff --git a/core.py b/core.py index 7b3c69a..dcea783 100644 --- a/core.py +++ b/core.py @@ -6,25 +6,330 @@ from os.path import expanduser import subprocess as sp import numpy from PIL import Image -import tempfile from shutil import rmtree -import atexit import time from collections import OrderedDict import json +from importlib import import_module +from PyQt4.QtGui import QDesktopServices +import string class Core(): def __init__(self): self.FFMPEG_BIN = self.findFfmpeg() - self.tempDir = os.path.join( - tempfile.gettempdir(), 'audio-visualizer-python-data') - if not os.path.exists(self.tempDir): - os.makedirs(self.tempDir) - atexit.register(self.deleteTempDir) + self.dataDir = QDesktopServices.storageLocation( + QDesktopServices.DataLocation) + self.presetDir = os.path.join(self.dataDir, 'presets') self.wd = os.path.dirname(os.path.realpath(__file__)) self.loadEncoderOptions() + self.videoFormats = Core.appendUppercase([ + '*.mp4', + '*.mov', + '*.mkv', + '*.avi', + '*.webm', + '*.flv', + ]) + self.audioFormats = Core.appendUppercase([ + '*.mp3', + '*.wav', + '*.ogg', + '*.fla', + '*.aac', + ]) + self.imageFormats = Core.appendUppercase([ + '*.png', + '*.jpg', + '*.tif', + '*.tiff', + '*.gif', + '*.bmp', + '*.ico', + '*.xbm', + '*.xpm', + ]) + + self.findComponents() + self.selectedComponents = [] + # copies of named presets to detect modification + self.savedPresets = {} + + def findComponents(self): + def findComponents(): + srcPath = os.path.join(self.wd, 'components') + if os.path.exists(srcPath): + for f in sorted(os.listdir(srcPath)): + name, ext = os.path.splitext(f) + if name.startswith("__"): + continue + elif ext == '.py': + yield name + self.modules = [ + import_module('components.%s' % name) + for name in findComponents() + ] + self.moduleIndexes = [i for i in range(len(self.modules))] + + def componentListChanged(self): + for i, component in enumerate(self.selectedComponents): + component.compPos = i + + def insertComponent(self, compPos, moduleIndex): + if compPos < 0: + compPos = len(self.selectedComponents) -1 + + component = self.modules[moduleIndex].Component( + moduleIndex, compPos) + self.selectedComponents.insert( + compPos, + component) + + self.componentListChanged() + return compPos + + def moveComponent(self, startI, endI): + comp = self.selectedComponents.pop(startI) + self.selectedComponents.insert(endI, comp) + + self.componentListChanged() + return endI + + def removeComponent(self, i): + self.selectedComponents.pop(i) + self.componentListChanged() + + def updateComponent(self, i): + # print('updating %s' % self.selectedComponents[i]) + self.selectedComponents[i].update() + + def moduleIndexFor(self, compName): + compNames = [mod.Component.__doc__ for mod in self.modules] + index = compNames.index(compName) + return self.moduleIndexes[index] + + def clearPreset(self, compIndex, loader=None): + '''Clears a preset from a component''' + self.selectedComponents[compIndex].currentPreset = None + if loader: + loader.updateComponentTitle(compIndex) + + def openPreset(self, filepath, compIndex, presetName): + '''Applies a preset to a specific component''' + saveValueStore = self.getPreset(filepath) + if not saveValueStore: + return False + try: + self.selectedComponents[compIndex].loadPreset( + saveValueStore, + presetName + ) + except KeyError as e: + print('preset missing value: %s' % e) + + self.savedPresets[presetName] = dict(saveValueStore) + return True + + def getPreset(self, filepath): + '''Returns the preset dict stored at this filepath''' + if not os.path.exists(filepath): + return False + with open(filepath, 'r') as f: + for line in f: + saveValueStore = Core.presetFromString(line.strip()) + break + return saveValueStore + + def openProject(self, loader, filepath): + '''loader is the object calling this method (mainwindow/command) + which implements an insertComponent method''' + errcode, data = self.parseAvFile(filepath) + if errcode == 0: + try: + for i, tup in enumerate(data['Components']): + name, vers, preset = tup + clearThis = False + + # add loaded named presets to savedPresets dict + if 'preset' in preset and preset['preset'] != None: + nam = preset['preset'] + filepath2 = os.path.join( + self.presetDir, name, str(vers), nam) + origSaveValueStore = self.getPreset(filepath2) + if origSaveValueStore: + self.savedPresets[nam] = dict(origSaveValueStore) + else: + # saved preset was renamed or deleted + clearThis = True + + # insert component into the loader + loader.insertComponent( + self.moduleIndexFor(name), -1) + try: + if 'preset' in preset and preset['preset'] != None: + self.selectedComponents[-1].loadPreset( + preset + ) + else: + self.selectedComponents[-1].loadPreset( + preset, + preset['preset'] + ) + except KeyError as e: + print('%s missing value %s' % + (self.selectedComponents[-1], e)) + + if clearThis: + self.clearPreset(-1, loader) + except: + errcode = 1 + data = sys.exc_info() + + + if errcode == 1: + typ, value, _ = data + if typ.__name__ == KeyError: + # probably just an old version, still loadable + print('file missing value: %s' % value) + return + loader.createNewProject() + msg = '%s: %s' % (typ.__name__, value) + loader.showMessage( + msg="Project file '%s' is corrupted." % filepath, + showCancel=False, + icon=QtGui.QMessageBox.Warning, + detail=msg) + + def parseAvFile(self, filepath): + '''Parses an avp (project) or avl (preset package) file. + Returns data usable by another method.''' + data = {} + try: + with open(filepath, 'r') as f: + def parseLine(line): + '''Decides if a given avp or avl line is a section header''' + validSections = ('Components') + line = line.strip() + newSection = '' + + if line.startswith('[') and line.endswith(']') \ + and line[1:-1] in validSections: + newSection = line[1:-1] + + return line, newSection + + section = '' + i = 0 + for line in f: + line, newSection = parseLine(line) + if newSection: + section = str(newSection) + data[section] = [] + continue + if line and section == 'Components': + if i == 0: + lastCompName = str(line) + i += 1 + elif i == 1: + lastCompVers = str(line) + i += 1 + elif i == 2: + lastCompPreset = Core.presetFromString(line) + data[section].append( + (lastCompName, + lastCompVers, + lastCompPreset) + ) + i = 0 + return 0, data + except: + return 1, sys.exc_info() + + def importPreset(self, filepath): + errcode, data = self.parseAvFile(filepath) + returnList = [] + if errcode == 0: + name, vers, preset = data['Components'][0] + presetName = preset['preset'] \ + if preset['preset'] else os.path.basename(filepath)[:-4] + newPath = os.path.join( + self.presetDir, + name, + vers, + presetName + ) + if os.path.exists(newPath): + return False, newPath + preset['preset'] = presetName + self.createPresetFile( + name, vers, presetName, preset + ) + return True, presetName + elif errcode == 1: + # TODO: an error message + return False, '' + + def exportPreset(self, exportPath, compName, vers, origName): + internalPath = os.path.join(self.presetDir, compName, str(vers), origName) + if not os.path.exists(internalPath): + return + if os.path.exists(exportPath): + os.remove(exportPath) + with open(internalPath, 'r') as f: + internalData = [line for line in f] + try: + saveValueStore = Core.presetFromString(internalData[0].strip()) + self.createPresetFile( + compName, vers, + origName, saveValueStore, + exportPath + ) + return True + except: + return False + + def createPresetFile( + self, compName, vers, presetName, saveValueStore, filepath=''): + '''Create a preset file (.avl) at filepath using args. + Or if filepath is empty, create an internal preset using + the args for the filepath.''' + if not filepath: + dirname = os.path.join(self.presetDir, compName, str(vers)) + if not os.path.exists(dirname): + os.makedirs(dirname) + filepath = os.path.join(dirname, presetName) + internal = True + else: + if not filepath.endswith('.avl'): + filepath += '.avl' + internal = False + + with open(filepath, 'w') as f: + if not internal: + f.write('[Components]\n') + f.write('%s\n' % compName) + f.write('%s\n' % str(vers)) + f.write(Core.presetToString(saveValueStore)) + + def createProjectFile(self, filepath): + '''Create a project file (.avp) using the current program state''' + try: + if not filepath.endswith(".avp"): + filepath += '.avp' + if os.path.exists(filepath): + os.remove(filepath) + with open(filepath, 'w') as f: + print('creating %s' % filepath) + f.write('[Components]\n') + for comp in self.selectedComponents: + saveValueStore = comp.savePreset() + f.write('%s\n' % str(comp)) + f.write('%s\n' % str(comp.version())) + f.write('%s\n' % Core.presetToString(saveValueStore)) + return True + except: + return False def loadEncoderOptions(self): file_path = os.path.join(self.wd, 'encoder-options.json') @@ -107,12 +412,6 @@ class Core(): return completeAudioArray - def deleteTempDir(self): - try: - rmtree(self.tempDir) - except FileNotFoundError: - pass - def cancel(self): self.canceled = True @@ -120,6 +419,22 @@ class Core(): self.canceled = False @staticmethod - def stringOrderedDict(dictionary): - sorted_ = OrderedDict(sorted(dictionary.items(), key=lambda t: t[0])) - return repr(sorted_) + def badName(name): + '''Returns whether a name contains non-alphanumeric chars''' + return any([letter in string.punctuation for letter in name]) + + @staticmethod + def presetToString(dictionary): + '''Alphabetizes a dict into OrderedDict & returns string repr''' + return repr(OrderedDict(sorted(dictionary.items(), key=lambda t: t[0]))) + + @staticmethod + def presetFromString(string): + '''Turns a string repr of OrderedDict into a regular dict''' + return dict(eval(string)) + + @staticmethod + def appendUppercase(lst): + for form, i in zip(lst, range(len(lst))): + lst.append(form.upper()) + return lst diff --git a/main.py b/main.py index c771aca..7c0727b 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,4 @@ from importlib import import_module -from collections import OrderedDict from PyQt4 import QtGui, uic from PyQt4.QtCore import Qt import sys diff --git a/mainwindow.py b/mainwindow.py index 78809be..fb9ebfd 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -1,14 +1,10 @@ from os.path import expanduser from queue import Queue -from importlib import import_module -from collections import OrderedDict -from PyQt4 import QtCore, QtGui +from PyQt4 import QtCore, QtGui, uic from PyQt4.QtCore import QSettings, Qt -from PyQt4.QtGui import QDesktopServices, QMenu +from PyQt4.QtGui import QMenu import sys -import io import os -import string import signal import filecmp import time @@ -16,6 +12,7 @@ import time import core import preview_thread import video_thread +from presetmanager import PresetManager from main import LoadDefaultSettings @@ -55,26 +52,30 @@ class MainWindow(QtCore.QObject): # print('main thread id: {}'.format(QtCore.QThread.currentThreadId())) self.window = window self.core = core.Core() - self.pages = [] - self.selectedComponents = [] + + self.pages = [] # widgets of component settings self.lastAutosave = time.time() - # create data directory, load/create settings - self.dataDir = QDesktopServices.storageLocation( - QDesktopServices.DataLocation) + # Create data directory, load/create settings + self.dataDir = self.core.dataDir self.autosavePath = os.path.join(self.dataDir, 'autosave.avp') - self.presetDir = os.path.join(self.dataDir, 'presets') self.settings = QSettings( os.path.join(self.dataDir, 'settings.ini'), QSettings.IniFormat) LoadDefaultSettings(self) + self.presetManager = PresetManager( + uic.loadUi( + os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'presetmanager.ui')), + self) + if not os.path.exists(self.dataDir): os.makedirs(self.dataDir) for neededDirectory in ( - self.presetDir, self.settings.value("projectDir")): + self.core.presetDir, self.settings.value("projectDir")): if not os.path.exists(neededDirectory): os.mkdir(neededDirectory) - # + # Make queues/timers for the preview thread self.previewQueue = Queue() self.previewThread = QtCore.QThread(self) self.previewWorker = preview_thread.Worker(self, self.previewQueue) @@ -86,7 +87,9 @@ class MainWindow(QtCore.QObject): self.timer.timeout.connect(self.processTask.emit) self.timer.start(500) - # begin decorating the window and connecting events + # Begin decorating the window and connecting events + componentList = self.window.listWidget_componentList + window.toolButton_selectAudioFile.clicked.connect( self.openInputFileDialog) @@ -117,7 +120,7 @@ class MainWindow(QtCore.QObject): codec = window.comboBox_videoCodec.itemText(i) if codec == self.settings.value('outputVideoCodec'): window.comboBox_videoCodec.setCurrentIndex(i) - print(codec) + #print(codec) for i in range(window.comboBox_audioCodec.count()): codec = window.comboBox_audioCodec.itemText(i) @@ -141,25 +144,33 @@ class MainWindow(QtCore.QObject): window.spinBox_vBitrate.valueChanged.connect(self.updateCodecSettings) window.spinBox_aBitrate.valueChanged.connect(self.updateCodecSettings) - self.previewWindow = PreviewWindow(self, os.path.join( os.path.dirname(os.path.realpath(__file__)), "background.png")) window.verticalLayout_previewWrapper.addWidget(self.previewWindow) - self.modules = self.findComponents() + # Make component buttons self.compMenu = QMenu() - for i, comp in enumerate(self.modules): + for i, comp in enumerate(self.core.modules): action = self.compMenu.addAction(comp.Component.__doc__) action.triggered[()].connect( lambda item=i: self.insertComponent(item)) self.window.pushButton_addComponent.setMenu(self.compMenu) - window.listWidget_componentList.clicked.connect( - lambda _: self.changeComponentWidget()) + + componentList.dropEvent = self.dragComponent + componentList.itemSelectionChanged.connect( + self.changeComponentWidget) self.window.pushButton_removeComponent.clicked.connect( lambda _: self.removeComponent()) + componentList.setContextMenuPolicy( + QtCore.Qt.CustomContextMenu) + componentList.connect( + componentList, + QtCore.SIGNAL("customContextMenuRequested(QPoint)"), + self.componentContextMenu) + currentRes = str(self.settings.value('outputWidth'))+'x' + \ str(self.settings.value('outputHeight')) for i, res in enumerate(self.resolutions): @@ -169,36 +180,47 @@ class MainWindow(QtCore.QObject): window.comboBox_resolution.setCurrentIndex(currentRes) window.comboBox_resolution.currentIndexChanged.connect( self.updateResolution) - - self.window.pushButton_listMoveUp.clicked.connect( - self.moveComponentUp) + lambda: self.moveComponent(-1) + ) self.window.pushButton_listMoveDown.clicked.connect( - self.moveComponentDown) - self.window.pushButton_savePreset.clicked.connect( - self.openSavePresetDialog) - self.window.comboBox_openPreset.currentIndexChanged.connect( - self.openPreset) - self.window.pushButton_saveAs.clicked.connect( - self.openSaveProjectDialog) - self.window.pushButton_saveProject.clicked.connect( - self.saveCurrentProject) - self.window.pushButton_openProject.clicked.connect( - self.openOpenProjectDialog) + lambda: self.moveComponent(1) + ) - # show the window and load current project + # Configure the Projects Menu + self.projectMenu = QMenu() + self.ui_newProject = self.projectMenu.addAction("New Project") + self.ui_newProject.triggered[()].connect(self.createNewProject) + + self.ui_openProject = self.projectMenu.addAction("Open Project") + self.ui_openProject.triggered[()].connect(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 + ) + + # Show the window and load current project window.show() self.currentProject = self.settings.value("currentProject") - if self.currentProject and os.path.exists(self.autosavePath) \ - and filecmp.cmp(self.autosavePath, self.currentProject): + if self.autosaveExists(): # delete autosave if it's identical to the project os.remove(self.autosavePath) if self.currentProject and os.path.exists(self.autosavePath): ch = self.showMessage( - "Restore unsaved changes in project '%s'?" - % os.path.basename(self.currentProject)[:-4], True) + msg="Restore unsaved changes in project '%s'?" + % os.path.basename(self.currentProject)[:-4], + showCancel=True) if ch: os.remove(self.currentProject) os.rename(self.autosavePath, self.currentProject) @@ -214,6 +236,26 @@ class MainWindow(QtCore.QObject): self.previewThread.wait() self.autosave() + @QtCore.pyqtSlot(int, dict) + def updateComponentTitle(self, pos, presetStore=False): + if type(presetStore) == dict: + name = presetStore['preset'] + if name == None or name not in self.core.savedPresets: + modified = False + else: + modified = (presetStore != self.core.savedPresets[name]) + else: + print(pos, presetStore) + 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 @@ -249,18 +291,26 @@ class MainWindow(QtCore.QObject): self.settings.setValue('outputAudioBitrate', currentAudioBitrate) def autosave(self): - if time.time() - self.lastAutosave >= 1.0: + if not self.currentProject: if os.path.exists(self.autosavePath): os.remove(self.autosavePath) - self.createProjectFile(self.autosavePath) + elif time.time() - self.lastAutosave >= 2.0: + self.core.createProjectFile(self.autosavePath) self.lastAutosave = time.time() + def autosaveExists(self): + if self.currentProject and os.path.exists(self.autosavePath) \ + and filecmp.cmp(self.autosavePath, self.currentProject): + return True + else: + return False + def openInputFileDialog(self): inputDir = self.settings.value("inputDir", expanduser("~")) fileName = QtGui.QFileDialog.getOpenFileName( self.window, "Open Music File", - inputDir, "Music Files (*.mp3 *.wav *.ogg *.fla *.aac)") + inputDir, "Music Files (%s)" % " ".join(self.core.audioFormats)) if not fileName == "": self.settings.setValue("inputDir", os.path.dirname(fileName)) @@ -271,7 +321,8 @@ class MainWindow(QtCore.QObject): fileName = QtGui.QFileDialog.getSaveFileName( self.window, "Set Output Video File", - outputDir, "Video Files (*.mp4 *.mov *.mkv *.avi *.webm *.flv)") + outputDir, + "Video Files (%s);; All Files (*)" % " ".join(self.core.videoFormats)) if not fileName == "": self.settings.setValue("outputDir", os.path.dirname(fileName)) @@ -302,13 +353,10 @@ class MainWindow(QtCore.QObject): self.videoTask.emit( self.window.lineEdit_audioFile.text(), self.window.lineEdit_outputFile.text(), - self.selectedComponents) + self.core.selectedComponents) else: self.showMessage( - "You must select an audio file and output filename.") - - def progressBarUpdated(self, value): - self.window.progressBar_createVideo.setValue(value) + msg="You must select an audio file and output filename.") def changeEncodingStatus(self, status): if status: @@ -327,11 +375,9 @@ class MainWindow(QtCore.QObject): self.window.pushButton_removeComponent.setEnabled(False) self.window.pushButton_listMoveDown.setEnabled(False) self.window.pushButton_listMoveUp.setEnabled(False) - self.window.comboBox_openPreset.setEnabled(False) - self.window.pushButton_removePreset.setEnabled(False) - self.window.pushButton_savePreset.setEnabled(False) - self.window.pushButton_openProject.setEnabled(False) self.window.listWidget_componentList.setEnabled(False) + self.ui_newProject.setEnabled(False) + self.ui_openProject.setEnabled(False) else: self.window.pushButton_createVideo.setEnabled(True) self.window.pushButton_Cancel.setEnabled(False) @@ -348,11 +394,12 @@ class MainWindow(QtCore.QObject): self.window.pushButton_removeComponent.setEnabled(True) self.window.pushButton_listMoveDown.setEnabled(True) self.window.pushButton_listMoveUp.setEnabled(True) - self.window.comboBox_openPreset.setEnabled(True) - self.window.pushButton_removePreset.setEnabled(True) - self.window.pushButton_savePreset.setEnabled(True) - self.window.pushButton_openProject.setEnabled(True) self.window.listWidget_componentList.setEnabled(True) + self.ui_newProject.setEnabled(True) + self.ui_openProject.setEnabled(True) + + def progressBarUpdated(self, value): + self.window.progressBar_createVideo.setValue(value) def progressBarSetText(self, value): self.window.progressBar_createVideo.setFormat(value) @@ -369,187 +416,126 @@ class MainWindow(QtCore.QObject): self.drawPreview() def drawPreview(self): - self.newTask.emit(self.selectedComponents) + self.newTask.emit(self.core.selectedComponents) # self.processTask.emit() self.autosave() def showPreviewImage(self, image): self.previewWindow.changePixmap(image) - def findComponents(self): - def findComponents(): - srcPath = os.path.join( - os.path.dirname(os.path.realpath(__file__)), 'components') - if os.path.exists(srcPath): - for f in sorted(os.listdir(srcPath)): - name, ext = os.path.splitext(f) - if name.startswith("__"): - continue - elif ext == '.py': - yield name - return [ - import_module('components.%s' % name) - for name in findComponents()] + def insertComponent(self, moduleIndex, compPos=0): + componentList = self.window.listWidget_componentList + stackedWidget = self.window.stackedWidget + if compPos < 0: + compPos = componentList.count() - def addComponent(self, moduleIndex): - index = len(self.pages) - self.selectedComponents.append(self.modules[moduleIndex].Component()) - self.window.listWidget_componentList.addItem( - self.selectedComponents[-1].__doc__) - self.pages.append(self.selectedComponents[-1].widget(self)) - self.window.listWidget_componentList.setCurrentRow(index) - self.window.stackedWidget.addWidget(self.pages[-1]) - self.window.stackedWidget.setCurrentIndex(index) - self.selectedComponents[-1].update() - self.updateOpenPresetComboBox(self.selectedComponents[-1]) + index = self.core.insertComponent( + compPos, moduleIndex) + row = componentList.insertItem( + index, + self.core.selectedComponents[index].__doc__) + componentList.setCurrentRow(index) - def insertComponent(self, moduleIndex): - self.selectedComponents.insert( - 0, self.modules[moduleIndex].Component()) - self.window.listWidget_componentList.insertItem( - 0, self.selectedComponents[0].__doc__) - self.pages.insert(0, self.selectedComponents[0].widget(self)) - self.window.listWidget_componentList.setCurrentRow(0) - self.window.stackedWidget.insertWidget(0, self.pages[0]) - self.window.stackedWidget.setCurrentIndex(0) - self.selectedComponents[0].update() - self.updateOpenPresetComboBox(self.selectedComponents[0]) + # 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].widget(self)) + stackedWidget.insertWidget(index, self.pages[index]) + stackedWidget.setCurrentIndex(index) + + self.core.updateComponent(index) def removeComponent(self): - for selected in self.window.listWidget_componentList.selectedItems(): - index = self.window.listWidget_componentList.row(selected) + componentList = self.window.listWidget_componentList + + for selected in componentList.selectedItems(): + index = componentList.row(selected) self.window.stackedWidget.removeWidget(self.pages[index]) - self.window.listWidget_componentList.takeItem(index) - self.selectedComponents.pop(index) + componentList.takeItem(index) + self.core.removeComponent(index) self.pages.pop(index) self.changeComponentWidget() self.drawPreview() + def moveComponent(self, change): + '''Moves a component relatively from its current position''' + componentList = self.window.listWidget_componentList + 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() + + def dragComponent(self, event): + '''Drop event for the component listwidget''' + componentList = self.window.listWidget_componentList + + modelIndexes = [ \ + componentList.model().index(i) \ + for i in range(componentList.count()) \ + ] + rects = [ \ + componentList.visualRect(modelIndex) \ + for modelIndex in modelIndexes \ + ] + + rowPos = [rect.contains(event.pos()) for rect in rects] + if not any(rowPos): + return + + i = rowPos.index(True) + change = (componentList.currentRow() - i) * -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) - self.updateOpenPresetComboBox(self.selectedComponents[index]) - def moveComponentUp(self): - row = self.window.listWidget_componentList.currentRow() - if row > 0: - module = self.selectedComponents[row] - self.selectedComponents.pop(row) - self.selectedComponents.insert(row - 1, module) - page = self.pages[row] - self.pages.pop(row) - self.pages.insert(row - 1, page) - item = self.window.listWidget_componentList.takeItem(row) - self.window.listWidget_componentList.insertItem(row - 1, item) - widget = self.window.stackedWidget.removeWidget(page) - self.window.stackedWidget.insertWidget(row - 1, page) - self.window.listWidget_componentList.setCurrentRow(row - 1) - self.window.stackedWidget.setCurrentIndex(row - 1) - self.drawPreview() + def openPresetManager(self): + '''Preset manager for importing, exporting, renaming, deleting''' + self.presetManager.show() - def moveComponentDown(self): - row = self.window.listWidget_componentList.currentRow() - if row != -1 and row < len(self.pages)+1: - module = self.selectedComponents[row] - self.selectedComponents.pop(row) - self.selectedComponents.insert(row + 1, module) - page = self.pages[row] - self.pages.pop(row) - self.pages.insert(row + 1, page) - item = self.window.listWidget_componentList.takeItem(row) - self.window.listWidget_componentList.insertItem(row + 1, item) - widget = self.window.stackedWidget.removeWidget(page) - self.window.stackedWidget.insertWidget(row + 1, page) - self.window.listWidget_componentList.setCurrentRow(row + 1) - self.window.stackedWidget.setCurrentIndex(row + 1) - self.drawPreview() + def clear(self): + '''Get a blank slate''' + self.core.selectedComponents = [] + self.window.listWidget_componentList.clear() + for widget in self.pages: + self.window.stackedWidget.removeWidget(widget) + self.pages = [] - def updateOpenPresetComboBox(self, component): - self.window.comboBox_openPreset.clear() - self.window.comboBox_openPreset.addItem("Component Presets") - destination = os.path.join( - self.presetDir, str(component).strip(), str(component.version())) - if not os.path.exists(destination): - os.makedirs(destination) - for f in os.listdir(destination): - self.window.comboBox_openPreset.addItem(f) - - def openSavePresetDialog(self): - if self.window.listWidget_componentList.currentRow() == -1: - return - while True: - newName, OK = QtGui.QInputDialog.getText( - QtGui.QWidget(), 'Audio Visualizer', 'New Preset Name:') - badName = False - for letter in newName: - if letter in string.punctuation: - badName = True - if badName: - # some filesystems don't like bizarre characters - self.showMessage("Preset names must contain only letters, \ - numbers, and spaces.") - continue - if OK and newName: - index = self.window.listWidget_componentList.currentRow() - if index != -1: - saveValueStore = \ - self.selectedComponents[index].savePreset() - componentName = str(self.selectedComponents[index]).strip() - vers = self.selectedComponents[index].version() - self.createPresetFile( - componentName, vers, saveValueStore, newName) - break - - def createPresetFile( - self, componentName, version, saveValueStore, filename): - dirname = os.path.join(self.presetDir, componentName, str(version)) - if not os.path.exists(dirname): - os.makedirs(dirname) - filepath = os.path.join(dirname, filename) - if os.path.exists(filepath): + def createNewProject(self): + if self.autosaveExists(): ch = self.showMessage( - "%s already exists! Overwrite it?" % filename, - True, QtGui.QMessageBox.Warning) - if not ch: - return - # remove old copies of the preset - for i in range(0, self.window.comboBox_openPreset.count()): - if self.window.comboBox_openPreset.itemText(i) == filename: - self.window.comboBox_openPreset.removeItem(i) - with open(filepath, 'w') as f: - f.write(core.Core.stringOrderedDict(saveValueStore)) - self.window.comboBox_openPreset.addItem(filename) - self.window.comboBox_openPreset.setCurrentIndex( - self.window.comboBox_openPreset.count()-1) + msg="You have unsaved changes in project '%s'. " + "Save before starting a new project?" + % os.path.basename(self.currentProject)[:-4], + showCancel=True) + if ch: + self.saveCurrentProject() - def openPreset(self): - if self.window.comboBox_openPreset.currentIndex() < 1: - return - index = self.window.listWidget_componentList.currentRow() - if index == -1: - return - filename = self.window.comboBox_openPreset.itemText( - self.window.comboBox_openPreset.currentIndex()) - componentName = str(self.selectedComponents[index]).strip() - version = self.selectedComponents[index].version() - dirname = os.path.join(self.presetDir, componentName, str(version)) - filepath = os.path.join(dirname, filename) - if not os.path.exists(filepath): - self.window.comboBox_openPreset.removeItem( - self.window.comboBox_openPreset.currentIndex()) - return - with open(filepath, 'r') as f: - for line in f: - saveValueStore = dict(eval(line.strip())) - break - self.selectedComponents[index].loadPreset(saveValueStore) + self.clear() + self.currentProject = None + self.settings.setValue("currentProject", None) self.drawPreview() def saveCurrentProject(self): if self.currentProject: - self.createProjectFile(self.currentProject) + self.core.createProjectFile(self.currentProject) else: self.openSaveProjectDialog() @@ -560,23 +546,13 @@ class MainWindow(QtCore.QObject): "Project Files (*.avp)") if not filename: return - self.createProjectFile(filename) + if not filename.endswith(".avp"): + filename += '.avp' + self.settings.setValue("projectDir", os.path.dirname(filename)) + self.settings.setValue("currentProject", filename) + self.currentProject = filename - def createProjectFile(self, filepath): - if not filepath.endswith(".avp"): - filepath += '.avp' - with open(filepath, 'w') as f: - print('creating %s' % filepath) - f.write('[Components]\n') - for comp in self.selectedComponents: - saveValueStore = comp.savePreset() - f.write('%s\n' % str(comp)) - f.write('%s\n' % str(comp.version())) - f.write('%s\n' % core.Core.stringOrderedDict(saveValueStore)) - if filepath != self.autosavePath: - self.settings.setValue("projectDir", os.path.dirname(filepath)) - self.settings.setValue("currentProject", filepath) - self.currentProject = filepath + self.core.createProjectFile(filename) def openOpenProjectDialog(self): filename = QtGui.QFileDialog.getOpenFileName( @@ -593,58 +569,18 @@ class MainWindow(QtCore.QObject): self.currentProject = filepath self.settings.setValue("currentProject", filepath) self.settings.setValue("projectDir", os.path.dirname(filepath)) - compNames = [mod.Component.__doc__ for mod in self.modules] - try: - with open(filepath, 'r') as f: - validSections = ('Components') - section = '' + # actually load the project using core method + self.core.openProject(self, filepath) - def parseLine(line): - line = line.strip() - newSection = '' - - if line.startswith('[') and line.endswith(']') \ - and line[1:-1] in validSections: - newSection = line[1:-1] - - return line, newSection - - i = 0 - for line in f: - line, newSection = parseLine(line) - if newSection: - section = str(newSection) - continue - if line and section == 'Components': - if i == 0: - compIndex = compNames.index(line) - self.addComponent(compIndex) - i += 1 - elif i == 1: - # version, not used yet - i += 1 - elif i == 2: - saveValueStore = dict(eval(line)) - self.selectedComponents[-1].loadPreset( - saveValueStore) - i = 0 - except (IndexError, ValueError, KeyError, NameError, - SyntaxError, AttributeError, TypeError) as e: - self.clear() - typ, value, _ = sys.exc_info() - msg = '%s: %s' % (typ.__name__, value) - self.showMessage( - "Project file '%s' is corrupted." % filepath, False, - QtGui.QMessageBox.Warning, msg) - - def showMessage( - self, string, showCancel=False, - icon=QtGui.QMessageBox.Information, detail=None): - msg = QtGui.QMessageBox() - msg.setIcon(icon) - msg.setText(string) - msg.setDetailedText(detail) - if showCancel: + def showMessage(self, **kwargs): + parent = kwargs['parent'] if 'parent' in kwargs else self.window + msg = QtGui.QMessageBox(parent) + msg.setModal(True) + msg.setText(kwargs['msg']) + msg.setIcon( + kwargs['icon'] if 'icon' in kwargs else QtGui.QMessageBox.Information) + msg.setDetailedText(kwargs['detail'] if 'detail' in kwargs else None) + if 'showCancel'in kwargs and kwargs['showCancel']: msg.setStandardButtons( QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel) else: @@ -654,10 +590,46 @@ class MainWindow(QtCore.QObject): return True return False - def clear(self): - ''' empty out all components and fields, get a blank slate ''' - self.selectedComponents = [] - self.window.listWidget_componentList.clear() - for widget in self.pages: - self.window.stackedWidget.removeWidget(widget) - self.pages = [] + def componentContextMenu(self, QPos): + '''Appears when right-clicking a component in the list''' + componentList = self.window.listWidget_componentList + if not componentList.selectedItems(): + return + + # don't show menu if clicking empty space + parentPosition = componentList.mapToGlobal(QtCore.QPoint(0, 0)) + index = componentList.currentRow() + modelIndex = componentList.model().index(index) + if not componentList.visualRect(modelIndex).contains(QPos): + return + + self.presetManager.findPresets() + self.menu = QtGui.QMenu() + 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.submenu = QtGui.QMenu("Open Preset") + self.menu.addMenu(self.submenu) + + for version, presetName in presets: + menuItem = self.submenu.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.move(parentPosition + QPos) + self.menu.show() diff --git a/mainwindow.ui b/mainwindow.ui index c010caf..e892959 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -6,8 +6,8 @@ 0 0 - 1008 - 575 + 1028 + 592 @@ -108,23 +108,32 @@ QLayout::SetMinimumSize - + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 140 + 20 + + + + + + - Open Project + Projects - + - Save Project - - - - - - - Save As + Presets @@ -141,11 +150,60 @@ 20 - 15 + 2 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + true + + + QFrame::StyledPanel + + + QFrame::Sunken + + + 1 + + + true + + + true + + + false + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + @@ -188,97 +246,6 @@ 2 - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - QFrame::StyledPanel - - - QFrame::Sunken - - - 1 - - - false - - - false - - - false - - - QAbstractItemView::NoDragDrop - - - - - - - - - 2 - - - - - - 0 - 0 - - - - - 180 - 0 - - - - - Component Presets - - - - - - - - - 0 - 0 - - - - Save - - - - - - - Remove - - - diff --git a/presetmanager.py b/presetmanager.py new file mode 100644 index 0000000..3b02714 --- /dev/null +++ b/presetmanager.py @@ -0,0 +1,290 @@ +from PyQt4 import QtGui, QtCore +import string +import os + +import core + + +class PresetManager(QtGui.QDialog): + def __init__(self, window, parent): + super().__init__(parent.window) + self.parent = parent + self.core = parent.core + self.settings = parent.settings + self.presetDir = self.core.presetDir + if not self.settings.value('presetDir'): + self.settings.setValue( + "presetDir", + os.path.join(self.core.dataDir, 'projects')) + + self.findPresets() + + # window + self.lastFilter = '*' + self.presetRows = [] # list of (comp, vers, name) tuples + self.window = window + self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + + # connect button signals + self.window.pushButton_delete.clicked.connect(self.openDeletePresetDialog) + self.window.pushButton_rename.clicked.connect(self.openRenamePresetDialog) + self.window.pushButton_import.clicked.connect(self.openImportDialog) + self.window.pushButton_export.clicked.connect(self.openExportDialog) + self.window.pushButton_close.clicked.connect(self.window.close) + + # create filter box and preset list + self.drawFilterList() + self.window.comboBox_filter.currentIndexChanged.connect( + lambda: self.drawPresetList( + self.window.comboBox_filter.currentText(), self.window.lineEdit_search.text() + ) + ) + + # make auto-completion for search bar + self.autocomplete = QtGui.QStringListModel() + completer = QtGui.QCompleter() + completer.setModel(self.autocomplete) + self.window.lineEdit_search.setCompleter(completer) + self.window.lineEdit_search.textChanged.connect( + lambda: self.drawPresetList( + self.window.comboBox_filter.currentText(), self.window.lineEdit_search.text() + ) + ) + self.drawPresetList('*') + + def show(self): + '''Open a new preset manager window from the mainwindow''' + self.findPresets() + self.drawFilterList() + self.drawPresetList('*') + self.window.show() + + def findPresets(self): + parseList = [] + for dirpath, dirnames, filenames in os.walk(self.presetDir): + # anything without a subdirectory must be a preset folder + if dirnames: + continue + for preset in filenames: + compName = os.path.basename(os.path.dirname(dirpath)) + compVers = os.path.basename(dirpath) + try: + parseList.append((compName, int(compVers), preset)) + except ValueError: + continue + self.presets =\ + { + compName : \ + [ + (vers, preset) \ + for name, vers, preset in parseList \ + if name == compName \ + ] \ + for compName, _, __ in parseList \ + } + + def drawPresetList(self, compFilter=None, presetFilter=''): + self.window.listWidget_presets.clear() + if compFilter: + self.lastFilter = str(compFilter) + else: + compFilter = str(self.lastFilter) + self.presetRows = [] + presetNames = [] + for component, presets in self.presets.items(): + if compFilter != '*' and component != compFilter: + continue + for vers, preset in presets: + if not presetFilter or presetFilter in preset: + self.window.listWidget_presets.addItem('%s: %s' % (component, preset)) + self.presetRows.append((component, vers, preset)) + if preset not in presetNames: + presetNames.append(preset) + self.autocomplete.setStringList(presetNames) + + def drawFilterList(self): + self.window.comboBox_filter.clear() + self.window.comboBox_filter.addItem('*') + for component in self.presets: + self.window.comboBox_filter.addItem(component) + + def clearPreset(self, compI=None): + '''Functions on mainwindow level from the context menu''' + compI = self.parent.window.listWidget_componentList.currentRow() + self.core.clearPreset(compI, self.parent) + + def openSavePresetDialog(self): + '''Functions on mainwindow level from the context menu''' + window = self.parent.window + selectedComponents = self.core.selectedComponents + componentList = self.parent.window.listWidget_componentList + + if componentList.currentRow() == -1: + return + while True: + index = componentList.currentRow() + currentPreset = selectedComponents[index].currentPreset + newName, OK = QtGui.QInputDialog.getText( + self.parent.window, + 'Audio Visualizer', + 'New Preset Name:', + QtGui.QLineEdit.Normal, + currentPreset + ) + if OK: + if core.Core.badName(newName): + self.warnMessage(self.parent.window) + continue + if newName: + if index != -1: + selectedComponents[index].currentPreset = newName + saveValueStore = \ + selectedComponents[index].savePreset() + componentName = str(selectedComponents[index]).strip() + vers = selectedComponents[index].version() + self.createNewPreset( + componentName, vers, newName, + saveValueStore, window=self.parent.window) + self.openPreset(newName) + break + + def createNewPreset( + self, compName, vers, filename, saveValueStore, **kwargs): + path = os.path.join(self.presetDir, compName, str(vers), filename) + if self.presetExists(path, **kwargs): + return + self.core.createPresetFile(compName, vers, filename, saveValueStore) + + def presetExists(self, path, **kwargs): + if os.path.exists(path): + window = self.window \ + if 'window' not in kwargs else kwargs['window'] + ch = self.parent.showMessage( + msg="%s already exists! Overwrite it?" % + os.path.basename(path), + showCancel=True, + icon=QtGui.QMessageBox.Warning, + parent=window) + if not ch: + # user clicked cancel + return True + + return False + + def openPreset(self, presetName): + componentList = self.parent.window.listWidget_componentList + selectedComponents = self.parent.core.selectedComponents + + index = componentList.currentRow() + if index == -1: + return + componentName = str(selectedComponents[index]).strip() + version = selectedComponents[index].version() + dirname = os.path.join(self.presetDir, componentName, str(version)) + filepath = os.path.join(dirname, presetName) + self.core.openPreset(filepath, index, presetName) + + self.parent.updateComponentTitle(index) + self.parent.drawPreview() + + def openDeletePresetDialog(self): + selected = self.window.listWidget_presets.selectedItems() + if not selected: + return + row = self.window.listWidget_presets.row(selected[0]) + comp, vers, name = self.presetRows[row] + ch = self.parent.showMessage( + msg='Really delete %s?' % name, + showCancel=True, + icon=QtGui.QMessageBox.Warning, + parent=self.window + ) + if not ch: + return + self.deletePreset(comp, vers, name) + self.findPresets() + self.drawPresetList() + + def deletePreset(self, comp, vers, name): + filepath = os.path.join(self.presetDir, comp, str(vers), name) + os.remove(filepath) + + def warnMessage(self, window=None): + print(window) + self.parent.showMessage( + msg='Preset names must contain only letters, ' + 'numbers, and spaces.', + parent=window if window else self.window) + + def openRenamePresetDialog(self): + presetList = self.window.listWidget_presets + if presetList.currentRow() == -1: + return + + while True: + index = presetList.currentRow() + newName, OK = QtGui.QInputDialog.getText( + self.window, + 'Preset Manager', + 'Rename Preset:', + QtGui.QLineEdit.Normal, + self.presetRows[index][2] + ) + if OK: + if core.Core.badName(newName): + self.warnMessage() + continue + if newName: + comp, vers, oldName = self.presetRows[index] + path = os.path.join( + self.presetDir, comp, str(vers)) + newPath = os.path.join(path, newName) + oldPath = os.path.join(path, oldName) + if self.presetExists(newPath): + return + if os.path.exists(newPath): + os.remove(newPath) + os.rename(oldPath, newPath) + self.findPresets() + self.drawPresetList() + break + + def openImportDialog(self): + filename = QtGui.QFileDialog.getOpenFileName( + self.window, "Import Preset File", + self.settings.value("presetDir"), + "Preset Files (*.avl)") + if filename: + # get installed path & ask user to overwrite if needed + path = '' + while True: + if path: + if self.presetExists(path): + break + else: + if os.path.exists(path): + os.remove(path) + success, path = self.core.importPreset(filename) + if success: + break + + self.findPresets() + self.drawPresetList() + self.settings.setValue("presetDir", os.path.dirname(filename)) + + def openExportDialog(self): + if not self.window.listWidget_presets.selectedItems(): + return + filename = QtGui.QFileDialog.getSaveFileName( + self.window, "Export Preset", + self.settings.value("presetDir"), + "Preset Files (*.avl)") + if filename: + index = self.window.listWidget_presets.currentRow() + comp, vers, name = self.presetRows[index] + if not self.core.exportPreset(filename, comp, vers, name): + self.parent.showMessage( + msg='Couldn\'t export %s.' % filename, + parent=self.window + ) + self.settings.setValue("presetDir", os.path.dirname(filename)) diff --git a/presetmanager.ui b/presetmanager.ui new file mode 100644 index 0000000..5257b1c --- /dev/null +++ b/presetmanager.ui @@ -0,0 +1,150 @@ + + + presetmanager + + + Qt::NonModal + + + true + + + + 0 + 0 + 497 + 377 + + + + Preset Manager + + + + + + + + + + + Filter by name + + + + + + + + 200 + 0 + + + + + + + + + + + + + 0 + 0 + + + + true + + + + + + + + + QLayout::SetMinimumSize + + + + + Import + + + + + + + Export + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + true + + + Rename + + + + + + + Delete + + + + + + + + + + + <html><head/><body><p><span style=" font-size:10pt; font-style:italic;">Right-click components in the main window to create presets</span></p></body></html> + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + + + + + + diff --git a/preview_thread.py b/preview_thread.py index d54dba5..e3e8279 100644 --- a/preview_thread.py +++ b/preview_thread.py @@ -1,11 +1,9 @@ from PyQt4 import QtCore, QtGui, uic from PyQt4.QtCore import pyqtSignal, pyqtSlot -from PIL import Image, ImageDraw, ImageFont +from PIL import Image from PIL.ImageQt import ImageQt import core -import time from queue import Queue, Empty -import numpy import os from copy import copy @@ -18,6 +16,7 @@ class Worker(QtCore.QObject): QtCore.QObject.__init__(self) parent.newTask.connect(self.createPreviewImage) parent.processTask.connect(self.process) + self.parent = parent self.core = core.Core() self.queue = queue self.core.settings = parent.settings diff --git a/video_thread.py b/video_thread.py index f5354be..fc877bd 100644 --- a/video_thread.py +++ b/video_thread.py @@ -26,7 +26,7 @@ class Worker(QtCore.QObject): QtCore.QObject.__init__(self) self.core = core.Core() self.core.settings = parent.settings - self.modules = parent.modules + self.modules = parent.core.modules self.stackedWidget = parent.window.stackedWidget self.parent = parent parent.videoTask.connect(self.createVideo)