Exemple #1
0
    def __init__(self):
        super(App, self).__init__(gSettings)
        self.setAnimated(False)
        
        if datetime.datetime.month == '12':
            self.setWindowIcon(icons.get('Candy Cane'))
        else:
            self.setWindowIcon(icons.get('SqrMelon'))
        self.setWindowTitle('SqrMelon')
        self.setDockNestingEnabled(True)

        self.__menuBar = QMenuBar()
        self.setMenuBar(self.__menuBar)

        self.__dockWidgetMenu = QMenu('Components')

        self.__statusBar = QStatusBar()
        self.setStatusBar(self.__statusBar)

        self._timer = Timer()
        self.__shotsManager = ShotManager()
        self.__shotsManager.viewShotAction.connect(self.__onViewShot)
        self.__graphEditor = CurveEditor(self._timer)
        self.__shotsManager.currentChanged.connect(self.__graphEditor.setShot)
        self.__overlays = Overlays()
        self.__sceneView = SceneView(self.__shotsManager, self._timer, self.__overlays)
        self.__overlays.changed.connect(self.__sceneView.repaint)
        self._timer.timeChanged.connect(self.__setCurrentShot)
        self.__shotsManager.shotPinned.connect(self.__setCurrentShot)
        self.__shotsManager.shotsEnabled.connect(self.__setCurrentShot)
        self.__shotsManager.shotsDisabled.connect(self.__setCurrentShot)

        self.setCentralWidget(None)
        cameraView = Camera(self.__shotsManager, self.__graphEditor, self._timer)
        cameraView.cameraChanged.connect(self.__sceneView.repaint)
        self.__graphEditor.requestPositionKey.connect(cameraView.forwardPositionKey)
        self.__graphEditor.requestRotationKey.connect(cameraView.forwardRotationKey)
        self.__sceneView.setCamera(cameraView)
        self.__projectMenu = self.__menuBar.addMenu('&Project')
        self.__projectMenu.addAction('&New').triggered.connect(self.__onNewProject)
        self.__projectMenu.addAction('&Open').triggered.connect(self.__onOpenProject)
        save = self.__projectMenu.addAction('&Save')
        save.setShortcut(QKeySequence.Save)
        save.setShortcutContext(Qt.ApplicationShortcut)
        save.triggered.connect(self.__onCtrlS)
        self.__sceneList = SceneList()
        self.__shotsManager.findSceneRequest.connect(self.__sceneList.selectSceneWithName)
        self.__sceneList.requestCreateShot.connect(self.__shotsManager.createShot)
        self.__sceneList.setEnabled(False)
        self.__sceneList.setShotsManager(self.__shotsManager)

        self.__profiler = Profiler()

        self.timeSlider = TimeSlider(self._timer, self.__shotsManager)
        self.__shotsManager.shotChanged.connect(self.timeSlider.repaint)

        self._addDockWidget(self.__sceneList, where=Qt.TopDockWidgetArea)
        self._addDockWidget(self.__shotsManager, where=Qt.TopDockWidgetArea)
        viewDock = self._addDockWidget(self.__sceneView, '3D View', where=Qt.TopDockWidgetArea)
        logDock = self._addDockWidget(PyDebugLog.create(), 'Python log', where=Qt.TopDockWidgetArea)
        self.tabifyDockWidget(logDock, viewDock)

        self._addDockWidget(self.timeSlider, where=Qt.LeftDockWidgetArea)
        cameraDock = self._addDockWidget(cameraView, where=Qt.LeftDockWidgetArea)
        overlayDock = self._addDockWidget(self.__overlays, 'Overlays', Qt.LeftDockWidgetArea)
        self.tabifyDockWidget(overlayDock, cameraDock)

        self._addDockWidget(self.__graphEditor, where=Qt.BottomDockWidgetArea)
        self._addDockWidget(self.__profiler, where=Qt.BottomDockWidgetArea, direction=Qt.Vertical)

        self.__initializeProject()

        undoStack, cameraUndoStack = self.__graphEditor.undoStacks()
        undo = undoStack.createUndoAction(self, '&Undo')
        undo.setShortcut(QKeySequence.Undo)
        undo.setShortcutContext(Qt.ApplicationShortcut)

        redo = undoStack.createRedoAction(self, '&Redo')
        redo.setShortcuts(QKeySequence.Redo)
        redo.setShortcutContext(Qt.ApplicationShortcut)

        camUndo = cameraUndoStack.createUndoAction(self, 'Undo')
        camUndo.setShortcuts(QKeySequence('['))
        camUndo.setShortcutContext(Qt.ApplicationShortcut)

        camRedo = cameraUndoStack.createRedoAction(self, 'Redo')
        camRedo.setShortcuts(QKeySequence(']'))
        camRedo.setShortcutContext(Qt.ApplicationShortcut)

        camKey = QAction('&Key camera', self)
        camKey.setShortcuts(QKeySequence(Qt.Key_K))
        camKey.setShortcutContext(Qt.ApplicationShortcut)
        camKey.triggered.connect(cameraView.insertKey)

        camToggle = QAction('&Toggle camera control', self)
        camToggle.setShortcuts(QKeySequence(Qt.Key_T))
        camToggle.setShortcutContext(Qt.ApplicationShortcut)
        camToggle.triggered.connect(cameraView.toggle)

        camCopAnim = QAction('Snap came&ra to animation', self)
        camCopAnim.setShortcuts(QKeySequence(Qt.Key_R))
        camCopAnim.setShortcutContext(Qt.ApplicationShortcut)
        camCopAnim.triggered.connect(cameraView.copyAnim)

        self.__editMenu = self.__menuBar.addMenu('Edit')
        self.__editMenu.addAction(undo)
        self.__editMenu.addAction(redo)
        self.__editMenu.addAction(camUndo)
        self.__editMenu.addAction(camRedo)
        self.__editMenu.addSeparator()
        self.__editMenu.addAction(camKey)
        self.__editMenu.addAction(camToggle)
        self.__editMenu.addAction(camCopAnim)

        toolsMenu = self.__menuBar.addMenu('Tools')
        toolsMenu.addAction('Color Picker').triggered.connect(self.__colorPicker)

        lock = toolsMenu.addAction('Lock UI')
        lock.setCheckable(True)
        lock.toggled.connect(self.__toggleUILock)

        fs = toolsMenu.addAction('Full screen viewport')
        fs.setShortcut(Qt.Key_F11)
        fs.setShortcutContext(Qt.ApplicationShortcut)
        fs.triggered.connect(self.__fullScreenViewport)

        self.__previewMenu = toolsMenu.addMenu('Preview resolution')
        previewRadioGroup = QActionGroup(self)
        # add action & connect it to the setPreviewRes with right parameters
        hd = self.__previewMenu.addAction('1080p (HD)')
        hd.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, 1920, 1080, 1.0))
        hd.setCheckable(True)
        hd.setActionGroup(previewRadioGroup)
        hdready = self.__previewMenu.addAction('720p')
        hdready.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, 1280, 720, 1.0))
        hdready.setCheckable(True)
        hdready.setActionGroup(previewRadioGroup)
        sdready = self.__previewMenu.addAction('480p')
        sdready.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, 854, 480, 1.0))
        sdready.setCheckable(True)
        sdready.setActionGroup(previewRadioGroup)

        viewport = self.__previewMenu.addAction('Viewport')
        viewport.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, None, None, 1.0))
        viewport.setCheckable(True)
        viewport.setActionGroup(previewRadioGroup)
        half = self.__previewMenu.addAction('1/2 view')
        half.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, None, None, 0.5))
        half.setCheckable(True)
        half.setActionGroup(previewRadioGroup)
        quart = self.__previewMenu.addAction('1/4 view')
        quart.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, None, None, 0.25))
        quart.setCheckable(True)
        quart.setActionGroup(previewRadioGroup)
        eight = self.__previewMenu.addAction('1/8 view')
        eight.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, None, None, 0.125))
        eight.setCheckable(True)
        eight.setActionGroup(previewRadioGroup)

        toolsMenu.addAction('Record').triggered.connect(self.__record)

        option = viewport
        if gSettings.contains('GLViewScale'):
            option = {1.0: viewport, 0.5: half, 0.25: quart, 0.125: eight}[float(gSettings.value('GLViewScale'))]
        option.setChecked(True)

        self.__menuBar.addMenu(self.__dockWidgetMenu)
        self.__menuBar.addAction('About').triggered.connect(self.__aboutDialog)
        self.__restoreUiLock(lock)
class StatusHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hi there")
        self.finish()


class TMSHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("")
        self.set_header("Access-Control-Allow-Origin", "*")
        self.finish()


application = tornado.web.Application([
    (r"/index.html", StatusHandler),
    #(r"/tiles/tilemapresource.xml", TMSHandler),
    (r"/tiles/([0-9]+)/([0-9]+)/([0-9]+).(.+)", TileHandler),
])

if __name__ == "__main__":
    scene_filename = "scene_list.csv"
    scene_list = SceneList(scene_filename)
    print "Loaded %s scenes" % len(scene_list.scenes)
    port = 8000
    if len(sys.argv) > 1:
        port = int(sys.argv[1])
    print "Listening on port %s" % port
    application.listen(port)
    tornado.ioloop.IOLoop.instance().start()
Exemple #3
0
class App(QMainWindowState):
    def __init__(self):
        super(App, self).__init__(gSettings)
        self.setAnimated(False)
        
        if datetime.datetime.month == '12':
            self.setWindowIcon(icons.get('Candy Cane'))
        else:
            self.setWindowIcon(icons.get('SqrMelon'))
        self.setWindowTitle('SqrMelon')
        self.setDockNestingEnabled(True)

        self.__menuBar = QMenuBar()
        self.setMenuBar(self.__menuBar)

        self.__dockWidgetMenu = QMenu('Components')

        self.__statusBar = QStatusBar()
        self.setStatusBar(self.__statusBar)

        self._timer = Timer()
        self.__shotsManager = ShotManager()
        self.__shotsManager.viewShotAction.connect(self.__onViewShot)
        self.__graphEditor = CurveEditor(self._timer)
        self.__shotsManager.currentChanged.connect(self.__graphEditor.setShot)
        self.__overlays = Overlays()
        self.__sceneView = SceneView(self.__shotsManager, self._timer, self.__overlays)
        self.__overlays.changed.connect(self.__sceneView.repaint)
        self._timer.timeChanged.connect(self.__setCurrentShot)
        self.__shotsManager.shotPinned.connect(self.__setCurrentShot)
        self.__shotsManager.shotsEnabled.connect(self.__setCurrentShot)
        self.__shotsManager.shotsDisabled.connect(self.__setCurrentShot)

        self.setCentralWidget(None)
        cameraView = Camera(self.__shotsManager, self.__graphEditor, self._timer)
        cameraView.cameraChanged.connect(self.__sceneView.repaint)
        self.__graphEditor.requestPositionKey.connect(cameraView.forwardPositionKey)
        self.__graphEditor.requestRotationKey.connect(cameraView.forwardRotationKey)
        self.__sceneView.setCamera(cameraView)
        self.__projectMenu = self.__menuBar.addMenu('&Project')
        self.__projectMenu.addAction('&New').triggered.connect(self.__onNewProject)
        self.__projectMenu.addAction('&Open').triggered.connect(self.__onOpenProject)
        save = self.__projectMenu.addAction('&Save')
        save.setShortcut(QKeySequence.Save)
        save.setShortcutContext(Qt.ApplicationShortcut)
        save.triggered.connect(self.__onCtrlS)
        self.__sceneList = SceneList()
        self.__shotsManager.findSceneRequest.connect(self.__sceneList.selectSceneWithName)
        self.__sceneList.requestCreateShot.connect(self.__shotsManager.createShot)
        self.__sceneList.setEnabled(False)
        self.__sceneList.setShotsManager(self.__shotsManager)

        self.__profiler = Profiler()

        self.timeSlider = TimeSlider(self._timer, self.__shotsManager)
        self.__shotsManager.shotChanged.connect(self.timeSlider.repaint)

        self._addDockWidget(self.__sceneList, where=Qt.TopDockWidgetArea)
        self._addDockWidget(self.__shotsManager, where=Qt.TopDockWidgetArea)
        viewDock = self._addDockWidget(self.__sceneView, '3D View', where=Qt.TopDockWidgetArea)
        logDock = self._addDockWidget(PyDebugLog.create(), 'Python log', where=Qt.TopDockWidgetArea)
        self.tabifyDockWidget(logDock, viewDock)

        self._addDockWidget(self.timeSlider, where=Qt.LeftDockWidgetArea)
        cameraDock = self._addDockWidget(cameraView, where=Qt.LeftDockWidgetArea)
        overlayDock = self._addDockWidget(self.__overlays, 'Overlays', Qt.LeftDockWidgetArea)
        self.tabifyDockWidget(overlayDock, cameraDock)

        self._addDockWidget(self.__graphEditor, where=Qt.BottomDockWidgetArea)
        self._addDockWidget(self.__profiler, where=Qt.BottomDockWidgetArea, direction=Qt.Vertical)

        self.__initializeProject()

        undoStack, cameraUndoStack = self.__graphEditor.undoStacks()
        undo = undoStack.createUndoAction(self, '&Undo')
        undo.setShortcut(QKeySequence.Undo)
        undo.setShortcutContext(Qt.ApplicationShortcut)

        redo = undoStack.createRedoAction(self, '&Redo')
        redo.setShortcuts(QKeySequence.Redo)
        redo.setShortcutContext(Qt.ApplicationShortcut)

        camUndo = cameraUndoStack.createUndoAction(self, 'Undo')
        camUndo.setShortcuts(QKeySequence('['))
        camUndo.setShortcutContext(Qt.ApplicationShortcut)

        camRedo = cameraUndoStack.createRedoAction(self, 'Redo')
        camRedo.setShortcuts(QKeySequence(']'))
        camRedo.setShortcutContext(Qt.ApplicationShortcut)

        camKey = QAction('&Key camera', self)
        camKey.setShortcuts(QKeySequence(Qt.Key_K))
        camKey.setShortcutContext(Qt.ApplicationShortcut)
        camKey.triggered.connect(cameraView.insertKey)

        camToggle = QAction('&Toggle camera control', self)
        camToggle.setShortcuts(QKeySequence(Qt.Key_T))
        camToggle.setShortcutContext(Qt.ApplicationShortcut)
        camToggle.triggered.connect(cameraView.toggle)

        camCopAnim = QAction('Snap came&ra to animation', self)
        camCopAnim.setShortcuts(QKeySequence(Qt.Key_R))
        camCopAnim.setShortcutContext(Qt.ApplicationShortcut)
        camCopAnim.triggered.connect(cameraView.copyAnim)

        self.__editMenu = self.__menuBar.addMenu('Edit')
        self.__editMenu.addAction(undo)
        self.__editMenu.addAction(redo)
        self.__editMenu.addAction(camUndo)
        self.__editMenu.addAction(camRedo)
        self.__editMenu.addSeparator()
        self.__editMenu.addAction(camKey)
        self.__editMenu.addAction(camToggle)
        self.__editMenu.addAction(camCopAnim)

        toolsMenu = self.__menuBar.addMenu('Tools')
        toolsMenu.addAction('Color Picker').triggered.connect(self.__colorPicker)

        lock = toolsMenu.addAction('Lock UI')
        lock.setCheckable(True)
        lock.toggled.connect(self.__toggleUILock)

        fs = toolsMenu.addAction('Full screen viewport')
        fs.setShortcut(Qt.Key_F11)
        fs.setShortcutContext(Qt.ApplicationShortcut)
        fs.triggered.connect(self.__fullScreenViewport)

        self.__previewMenu = toolsMenu.addMenu('Preview resolution')
        previewRadioGroup = QActionGroup(self)
        # add action & connect it to the setPreviewRes with right parameters
        hd = self.__previewMenu.addAction('1080p (HD)')
        hd.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, 1920, 1080, 1.0))
        hd.setCheckable(True)
        hd.setActionGroup(previewRadioGroup)
        hdready = self.__previewMenu.addAction('720p')
        hdready.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, 1280, 720, 1.0))
        hdready.setCheckable(True)
        hdready.setActionGroup(previewRadioGroup)
        sdready = self.__previewMenu.addAction('480p')
        sdready.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, 854, 480, 1.0))
        sdready.setCheckable(True)
        sdready.setActionGroup(previewRadioGroup)

        viewport = self.__previewMenu.addAction('Viewport')
        viewport.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, None, None, 1.0))
        viewport.setCheckable(True)
        viewport.setActionGroup(previewRadioGroup)
        half = self.__previewMenu.addAction('1/2 view')
        half.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, None, None, 0.5))
        half.setCheckable(True)
        half.setActionGroup(previewRadioGroup)
        quart = self.__previewMenu.addAction('1/4 view')
        quart.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, None, None, 0.25))
        quart.setCheckable(True)
        quart.setActionGroup(previewRadioGroup)
        eight = self.__previewMenu.addAction('1/8 view')
        eight.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, None, None, 0.125))
        eight.setCheckable(True)
        eight.setActionGroup(previewRadioGroup)

        toolsMenu.addAction('Record').triggered.connect(self.__record)

        option = viewport
        if gSettings.contains('GLViewScale'):
            option = {1.0: viewport, 0.5: half, 0.25: quart, 0.125: eight}[float(gSettings.value('GLViewScale'))]
        option.setChecked(True)

        self.__menuBar.addMenu(self.__dockWidgetMenu)
        self.__menuBar.addAction('About').triggered.connect(self.__aboutDialog)
        self.__restoreUiLock(lock)

    def _addDockWidget(self, widget, name=None, where=Qt.RightDockWidgetArea, direction=Qt.Horizontal):
        dockWidget = super(App, self)._addDockWidget(widget, name, where, direction)
        self.__dockWidgetMenu.addAction(dockWidget.toggleViewAction())
        return dockWidget

    def __record(self):
        diag = QDialog()
        fId = gSettings.value('RecordFPS', 2)
        rId = gSettings.value('RecordResolution', 3)
        layout = QGridLayout()
        diag.setLayout(layout)
        layout.addWidget(QLabel('FPS: '), 0, 0)
        fps = QComboBox()
        fps.addItems(['12', '24', '30', '48', '60', '120'])
        fps.setCurrentIndex(fId)
        layout.addWidget(fps, 0, 1)
        layout.addWidget(QLabel('Vertical resolution: '), 1, 0)
        resolution = QComboBox()
        resolution.addItems(['144', '288', '360', '720', '1080', '2160'])
        resolution.setCurrentIndex(rId)
        layout.addWidget(resolution, 1, 1)
        ok = QPushButton('Ok')
        ok.clicked.connect(diag.accept)
        cancel = QPushButton('Cancel')
        cancel.clicked.connect(diag.reject)
        layout.addWidget(ok, 2, 0)
        layout.addWidget(cancel, 2, 1)
        diag.exec_()
        if diag.result() != QDialog.Accepted:
            return
        gSettings.setValue('RecordFPS', fps.currentIndex())
        gSettings.setValue('RecordResolution', resolution.currentIndex())

        FPS = int(fps.currentText())
        HEIGHT = int(resolution.currentText())
        WIDTH = (HEIGHT * 16) / 9
        FMT = 'jpg'

        data = (ctypes.c_ubyte * (WIDTH * HEIGHT * 3))()  # alloc buffer once
        flooredStart = self._timer.secondsToBeats(int(self._timer.beatsToSeconds(self._timer.start) * FPS) / float(FPS))
        duration = self._timer.beatsToSeconds(self._timer.end - flooredStart)
        if not fileutil.exists('capture'):
            os.makedirs('capture')
        progress = QProgressDialog(self)
        progress.setMaximum(int(duration * FPS))
        prevFrame = 0
        for frame in xrange(int(duration * FPS)):
            deltaTime = (frame - prevFrame) / float(FPS)
            prevFrame = frame
            progress.setValue(frame)
            QApplication.processEvents()
            if progress.wasCanceled():
                break
            beats = flooredStart + self._timer.secondsToBeats(frame / float(FPS))

            shot = self.__shotsManager.shotAtTime(beats)
            if shot is None:
                continue
            sceneFile = os.path.join(ScenesPath(), shot.sceneName + SCENE_EXT)
            scene = Scene.getScene(sceneFile)
            scene.setSize(WIDTH, HEIGHT)

            uniforms = self.__shotsManager.evaluate(beats)
            textureUniforms = self.__shotsManager.additionalTextures(beats)
            self.__sceneView._cameraInput.setData(*(uniforms['uOrigin'] + uniforms['uAngles']))  # feed animation into camera so animationprocessor can read it again
            cameraData = self.__sceneView._cameraInput.data()

            modifier = os.path.join(ProjectDir(), 'animationprocessor.py')
            if fileutil.exists(modifier):
                execfile(modifier, globals(), locals())

            for name in self.__sceneView._textures:
                uniforms[name] = self.__sceneView._textures[name]._id

            scene.drawToScreen(self._timer.beatsToSeconds(beats), beats, uniforms, (0, 0, WIDTH, HEIGHT), textureUniforms)
            scene.colorBuffers[-1][0].use()

            from OpenGL.GL import glGetTexImage, GL_TEXTURE_2D, GL_RGB, GL_UNSIGNED_BYTE
            glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, data)

            QImage(data, WIDTH, HEIGHT, QImage.Format_RGB888).mirrored(False, True).save('capture/dump_%s_%05d.%s' % (FPS, int(self._timer.beatsToSeconds(self._timer.start) * FPS) + frame, FMT))
        progress.close()

        if not fileutil.exists('convertcapture'):
            os.makedirs('convertcapture')
        with fileutil.edit('convertcapture/convert.bat') as fh:
            start = ''
            start2 = ''
            if int(self._timer.start * FPS) > 0:
                start = '-start_number {} '.format(int(self._timer.beatsToSeconds(self._timer.start) * FPS))
                start2 = '-vframes {} '.format(int(self._timer.beatsToSeconds(self._timer.end - self._timer.start) * FPS))
            fh.write('cd "../capture"\n"../convertcapture/ffmpeg.exe" -framerate {} {}-i dump_{}_%%05d.{} {}-c:v libx264 -r {} -pix_fmt yuv420p "../convertcapture/output.mp4"'.format(FPS, start, FPS, FMT, start2, FPS))

        with fileutil.edit('convertcapture/convertGif.bat') as fh:
            start = ''
            start2 = ''
            if int(self._timer.start * FPS) > 0:
                start = '-start_number {} '.format(int(self._timer.beatsToSeconds(self._timer.start) * FPS))
                start2 = '-vframes {} '.format(int(self._timer.beatsToSeconds(self._timer.end - self._timer.start) * FPS))
            fh.write('cd "../capture"\n"../convertcapture/ffmpeg.exe" -framerate {} {}-i dump_{}_%%05d.{} {}-r {} "../convertcapture/output.gif"'.format(FPS, start, FPS, FMT, start2, FPS))

        sound = self.timeSlider.soundtrackPath()
        if not sound:
            return
        with fileutil.edit('convertcapture/merge.bat') as fh:
            startSeconds = self._timer.beatsToSeconds(self._timer.start)
            fh.write('ffmpeg -i output.mp4 -itsoffset {} -i "{}" -vcodec copy -shortest merged.mp4'.format(-startSeconds, sound))

    def __restoreUiLock(self, action):
        state = True if gSettings.value('lockui', '0') == '1' else False
        action.setChecked(state)
        features = QDockWidget.NoDockWidgetFeatures if state else QDockWidget.AllDockWidgetFeatures
        for dockWidget in self.findChildren(QDockWidget):
            dockWidget.setFeatures(features)

    def __fullScreenViewport(self, *args):
        # force floating
        dockWidget = self.__sceneView.parent()
        if not dockWidget.isFloating():
            dockWidget.setFloating(True)
        if dockWidget.isFullScreen():
            dockWidget.showNormal()
            dockWidget.resize(self.__restoreFullScreenSize)
        else:
            self.__restoreFullScreenSize = dockWidget.size()
            dockWidget.showFullScreen()

    def __toggleUILock(self, state):
        gSettings.setValue('lockui', '1' if state else '0')
        features = QDockWidget.NoDockWidgetFeatures if state else QDockWidget.AllDockWidgetFeatures
        for dockWidget in self.findChildren(QDockWidget):
            # only affect docked widgets
            if not dockWidget.isFloating():
                dockWidget.setFeatures(features)

    def __onViewShot(self, start, end):
        self._timer.start = start
        self._timer.end = end - 0.01

    def __aboutDialog(self):
        QMessageBox.about(self, 'About SqrMelon',
                          r"""<p>SqrMelon is a tool to manage a versions (scenes) of a graph of fragment shaders (templates) & drive uniforms with animation curves (shots).</p>
                          <p>Download or find documentation on <a href="https://github.com/trevorvanhoof/sqrmelon/">GitHub/</a>!</p>
                          <p>Icons from <a href="https://icons8.com/">icons8.com/</a></p>""")

    def __colorPicker(self):
        color = QColorDialog.getColor()
        color = "vec3(" + str(round(color.red() / 255.0, 2)) + ", " + str(round(color.green() / 255.0, 2)) + ", " + str(round(color.blue() / 255.0, 2)) + ")"
        cb = QApplication.clipboard()
        cb.setText(color, mode=cb.Clipboard)

    def __onCtrlS(self):
        if qApp.focusWidget() != self.__sceneView:
            self.saveProject()
        else:
            self.__sceneView.keyPressEvent(QKeyEvent(QEvent.KeyPress, Qt.Key_S, Qt.ControlModifier))

    def saveProject(self):
        self.__sceneView.saveCameraData()
        self.__shotsManager.saveAllShots()
        self._timer.saveState()
        QMessageBox.information(self, 'Save succesful!', 'Animation, shot & timing changes have been saved.')

    def closeEvent(self, event):
        res = QMessageBox.question(self, 'Save before exit?', 'Do you want to save?', QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
        if res == QMessageBox.Cancel:
            event.ignore()
            return
        if res == QMessageBox.Yes:
            self.saveProject()
        super(App, self).hideEvent(event)

    def __setCurrentShot(self, *args):
        shot = self.__shotsManager.shotAtTime(self._timer.time)
        if shot is None:
            self.__sceneView.setScene(None)
            self.__profiler.setScene(None)
            return
        sceneFile = os.path.join(ScenesPath(), shot.sceneName + SCENE_EXT)
        sc = Scene.getScene(sceneFile)
        self.__sceneView.setScene(sc)
        self.__profiler.setScene(sc)

    def __openProject(self, path):
        gSettings.setValue('currentproject', path)
        self.__sceneList.projectOpened()
        self.__shotsManager.projectOpened()
        self._timer.projectOpened()

    def __initializeProject(self):
        if gSettings.contains('currentproject'):
            project = str(gSettings.value('currentproject'))
            if fileutil.exists(project):
                self.__openProject(project)
                return
        project = [x for x in list(os.listdir(os.getcwd())) if x.endswith(PROJ_EXT)]
        if project:
            self.__openProject(os.path.join(os.getcwd(), project[0]))
            return

    def __onNewProject(self):
        if gSettings.contains('currentproject') and QMessageBox.warning(self, 'Creating new project',
                                                                        'Any unsaved changes will be lost. Continue?',
                                                                        QMessageBox.Yes | QMessageBox.No) == QMessageBox.No:
            return

        currentPath = os.getcwd()
        if gSettings.contains('currentproject'):
            project = str(gSettings.value('currentproject'))
            if fileutil.exists(project):
                currentPath = os.path.dirname(project)

        res = QFileDialog.getSaveFileName(self, 'Create new project', currentPath, 'Project folder')
        if not res:
            return
        shutil.copytree(DEFAULT_PROJECT, res, ignore=lambda p, f: [] if os.path.basename(p).lower() == 'Scenes' else [n for n in f if os.path.splitext(n)[-1].lower() in IGNORED_EXTENSIONS])
        projectFile = os.path.join(res, os.path.basename(res) + PROJ_EXT)
        fileutil.create(projectFile)
        self.__openProject(projectFile)

    def __onOpenProject(self):
        if gSettings.contains('currentproject') and QMessageBox.warning(self, 'Changing project',
                                                                        'Any unsaved changes will be lost. Continue?',
                                                                        QMessageBox.Yes | QMessageBox.No) == QMessageBox.No:
            return

        currentPath = os.getcwd()
        if gSettings.contains('currentproject'):
            project = str(gSettings.value('currentproject'))
            if fileutil.exists(project):
                currentPath = os.path.dirname(project)

        res = QFileDialog.getOpenFileName(self, 'Open project', currentPath, 'Project files (*%s)' % PROJ_EXT)
        if not res:
            return
        self.__openProject(res)
Exemple #4
0
class App(QMainWindowState):
    def __init__(self):
        super(App, self).__init__(gSettings)
        self.setAnimated(False)

        # We found that not setting a version in Ubunto didn't work
        glFormat = QGLFormat()
        glFormat.setVersion(4, 1)
        glFormat.setProfile(QGLFormat.CoreProfile)
        glFormat.setDefaultFormat(glFormat)

        if datetime.datetime.month == '12':
            self.setWindowIcon(icons.get('Candy Cane-48'))
        else:
            self.setWindowIcon(icons.get('SqrMelon'))
        self.setWindowTitle('SqrMelon')
        self.setDockNestingEnabled(True)

        self.__menuBar = QMenuBar()
        self.setMenuBar(self.__menuBar)

        self.__dockWidgetMenu = QMenu('Windows')

        self.__statusBar = QStatusBar()
        self.setStatusBar(self.__statusBar)

        self._timer = Timer()
        self.__shotsManager = ShotManager()
        self.__shotsManager.viewShotAction.connect(self.__onViewShot)
        self.__graphEditor = CurveEditor(self._timer)
        self.__shotsManager.currentChanged.connect(self.__graphEditor.setShot)
        self.__overlays = Overlays()
        self.__sceneView = SceneView(self.__shotsManager, self._timer, self.__overlays)
        self.__overlays.changed.connect(self.__sceneView.repaint)
        self._timer.timeChanged.connect(self.__setCurrentShot)
        self.__shotsManager.shotPinned.connect(self.__setCurrentShot)
        self.__shotsManager.shotsEnabled.connect(self.__setCurrentShot)
        self.__shotsManager.shotsDisabled.connect(self.__setCurrentShot)

        self.setCentralWidget(None)
        cameraView = Camera(self.__shotsManager, self.__graphEditor, self._timer)
        cameraView.cameraChanged.connect(self.__sceneView.repaint)
        self.__graphEditor.requestPositionKey.connect(cameraView.forwardPositionKey)
        self.__graphEditor.requestRotationKey.connect(cameraView.forwardRotationKey)
        self.__sceneView.setCamera(cameraView)
        self.__projectMenu = self.__menuBar.addMenu('&Project')
        newAction = self.__projectMenu.addAction('&New')
        newAction.setIcon(icons.get('icons8-new-document-50'))
        newAction.triggered.connect(self.__onNewProject)
        openAction = self.__projectMenu.addAction('&Open')
        openAction.setIcon(icons.get('icons8-opened-folder-48'))
        openAction.triggered.connect(self.__onOpenProject)
        self.__projectMenu.addSeparator()
        save = self.__projectMenu.addAction('&Save')
        save.setShortcut(QKeySequence.Save)
        save.setShortcutContext(Qt.ApplicationShortcut)
        save.setIcon(icons.get('icons8-save-50'))
        save.triggered.connect(self.__onCtrlS)
        self.__projectMenu.addSeparator()
        exitAction = self.__projectMenu.addAction('E&xit')
        exitAction.setIcon(icons.get('icons8-exit-50'))
        exitAction.triggered.connect(self.close)

        self.__sceneList = SceneList()
        self.__shotsManager.findSceneRequest.connect(self.__sceneList.selectSceneWithName)
        self.__sceneList.requestCreateShot.connect(self.__shotsManager.createShot)
        self.__sceneList.setEnabled(False)
        self.__sceneList.setShotsManager(self.__shotsManager)

        self.__profiler = Profiler()

        self.__models = Models()
        self.__modeler = modeler.Modeler(self.__models)
        self.__modelsOutliner = ModelsOutliner(self.__models)
        self.__modelsOutliner.selectedModelNodesChanged.connect(self.__modeler.viewport.setCurrentModelAndNodes)
        self.__models.modelChanged.connect(self.__modeler.viewport.onModelChanged)
        self.__models.modelChanged.connect(self._scheduleModelExport)
        self.__modeler.viewport.selectedModelNodesChanged.connect(self.__modelsOutliner.selectModelNodes)
        self.__modeler.viewport.cameraChanged.connect(cameraView.setModelerCameraTransform)

        self.timeSlider = TimeSlider(self._timer, self.__shotsManager)
        self.__shotsManager.shotChanged.connect(self.timeSlider.repaint)

        self._addDockWidget(self.__sceneList, where=Qt.TopDockWidgetArea)
        shotsDock = self._addDockWidget(self.__shotsManager, where=Qt.TopDockWidgetArea)
        modelsOutlinerDock = self._addDockWidget(self.__modelsOutliner, 'Models Outliner', where=Qt.TopDockWidgetArea)
        self.tabifyDockWidget(shotsDock, modelsOutlinerDock)

        viewDock = self._addDockWidget(self.__sceneView, '3D View', where=Qt.TopDockWidgetArea)
        logDock = self._addDockWidget(PyDebugLog.create(), 'Python log', where=Qt.TopDockWidgetArea)
        modelerDock = self._addDockWidget(self.__modeler, 'Modeler View', where=Qt.TopDockWidgetArea)
        self.tabifyDockWidget(logDock, viewDock)
        self.tabifyDockWidget(logDock, modelerDock)

        self._addDockWidget(self.timeSlider, where=Qt.LeftDockWidgetArea)
        cameraDock = self._addDockWidget(cameraView, where=Qt.LeftDockWidgetArea)
        overlayDock = self._addDockWidget(self.__overlays, 'Overlays', Qt.LeftDockWidgetArea)
        self.tabifyDockWidget(overlayDock, cameraDock)

        self._addDockWidget(self.__graphEditor, where=Qt.BottomDockWidgetArea)
        self._addDockWidget(self.__profiler, where=Qt.BottomDockWidgetArea, direction=Qt.Vertical)

        self.__initializeProject()

        undoStack, cameraUndoStack = self.__graphEditor.undoStacks()
        undo = undoStack.createUndoAction(self, '&Undo')
        undo.setShortcut(QKeySequence.Undo)
        undo.setShortcutContext(Qt.ApplicationShortcut)

        redo = undoStack.createRedoAction(self, '&Redo')
        redo.setShortcut(QKeySequence.Redo)
        redo.setShortcutContext(Qt.ApplicationShortcut)

        camUndo = cameraUndoStack.createUndoAction(self, 'Undo')
        camUndo.setShortcut(QKeySequence('['))
        camUndo.setShortcutContext(Qt.ApplicationShortcut)

        camRedo = cameraUndoStack.createRedoAction(self, 'Redo')
        camRedo.setShortcut(QKeySequence(']'))
        camRedo.setShortcutContext(Qt.ApplicationShortcut)

        camKey = QAction('&Key camera', self)
        camKey.setShortcut(QKeySequence(Qt.Key_K))
        camKey.setShortcutContext(Qt.ApplicationShortcut)
        camKey.triggered.connect(cameraView.insertKey)

        camToggle = QAction('&Toggle camera control', self)
        camToggle.setShortcut(QKeySequence(Qt.Key_T))
        camToggle.setShortcutContext(Qt.ApplicationShortcut)
        camToggle.triggered.connect(cameraView.toggleBetweenFreeAndAnimation)

        camCopAnim = QAction('Snap came&ra to animation', self)
        camCopAnim.setShortcut(QKeySequence(Qt.Key_R))
        camCopAnim.setShortcutContext(Qt.ApplicationShortcut)
        camCopAnim.triggered.connect(cameraView.copyAnim)

        self.__editMenu = self.__menuBar.addMenu('Edit')
        self.__editMenu.addAction(undo)
        self.__editMenu.addAction(redo)
        self.__editMenu.addAction(camUndo)
        self.__editMenu.addAction(camRedo)
        self.__editMenu.addSeparator()
        self.__editMenu.addAction(camKey)
        self.__editMenu.addAction(camToggle)
        self.__editMenu.addAction(camCopAnim)

        toolsMenu = self.__menuBar.addMenu('Tools')
        toolsMenu.addAction('Color Picker').triggered.connect(self.__colorPicker)

        lock = toolsMenu.addAction('Lock UI')
        lock.setCheckable(True)
        lock.toggled.connect(self.__toggleUILock)

        fs = toolsMenu.addAction('Full screen viewport')
        fs.setShortcut(QKeySequence(Qt.Key_F11))
        fs.setShortcutContext(Qt.ApplicationShortcut)
        fs.triggered.connect(self.__fullScreenViewport)

        self.__previewMenu = toolsMenu.addMenu('Preview resolution')
        previewRadioGroup = QActionGroup(self)
        # add action & connect it to the setPreviewRes with right parameters
        hd = self.__previewMenu.addAction('1080p (HD)')
        hd.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, 1920, 1080, 1.0))
        hd.setCheckable(True)
        hd.setActionGroup(previewRadioGroup)
        hdready = self.__previewMenu.addAction('720p')
        hdready.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, 1280, 720, 1.0))
        hdready.setCheckable(True)
        hdready.setActionGroup(previewRadioGroup)
        sdready = self.__previewMenu.addAction('480p')
        sdready.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, 854, 480, 1.0))
        sdready.setCheckable(True)
        sdready.setActionGroup(previewRadioGroup)

        viewport = self.__previewMenu.addAction('Viewport')
        viewport.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, None, None, 1.0))
        viewport.setCheckable(True)
        viewport.setActionGroup(previewRadioGroup)
        half = self.__previewMenu.addAction('1/2 view')
        half.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, None, None, 0.5))
        half.setCheckable(True)
        half.setActionGroup(previewRadioGroup)
        quart = self.__previewMenu.addAction('1/4 view')
        quart.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, None, None, 0.25))
        quart.setCheckable(True)
        quart.setActionGroup(previewRadioGroup)
        eight = self.__previewMenu.addAction('1/8 view')
        eight.triggered.connect(functools.partial(self.__sceneView.setPreviewRes, None, None, 0.125))
        eight.setCheckable(True)
        eight.setActionGroup(previewRadioGroup)

        toolsMenu.addAction('Record').triggered.connect(self.__record)

        option = viewport
        if gSettings.contains('GLViewScale'):
            option = {1.0: viewport, 0.5: half, 0.25: quart, 0.125: eight}[float(gSettings.value('GLViewScale'))]
        option.setChecked(True)

        self.__menuBar.addMenu(self.__dockWidgetMenu)

        self.__helpMenu = self.__menuBar.addMenu('&Help')
        aboutAction = self.__helpMenu.addAction('About')
        aboutAction.setIcon(icons.get('icons8-info-48'))
        aboutAction.triggered.connect(self.__aboutDialog)

        self.__restoreUiLock(lock)

        self._exportModelTimer = None

    def _addDockWidget(self, widget, name=None, where=Qt.RightDockWidgetArea, direction=Qt.Horizontal):
        dockWidget = super(App, self)._addDockWidget(widget, name, where, direction)
        self.__dockWidgetMenu.addAction(dockWidget.toggleViewAction())
        return dockWidget

    def __record(self):
        diag = QDialog()
        fId = gSettings.value('RecordFPS', 2)
        rId = gSettings.value('RecordResolution', 3)
        layout = QGridLayout()
        diag.setLayout(layout)
        layout.addWidget(QLabel('FPS: '), 0, 0)
        fps = QComboBox()
        fps.addItems(['12', '24', '30', '48', '60', '120'])
        fps.setCurrentIndex(fId)
        layout.addWidget(fps, 0, 1)
        layout.addWidget(QLabel('Vertical resolution: '), 1, 0)
        resolution = QComboBox()
        resolution.addItems(['144', '288', '360', '720', '1080', '2160'])
        resolution.setCurrentIndex(rId)
        layout.addWidget(resolution, 1, 1)
        ok = QPushButton('Ok')
        ok.clicked.connect(diag.accept)
        cancel = QPushButton('Cancel')
        cancel.clicked.connect(diag.reject)
        layout.addWidget(ok, 2, 0)
        layout.addWidget(cancel, 2, 1)
        diag.exec_()
        if diag.result() != QDialog.Accepted:
            return
        gSettings.setValue('RecordFPS', fps.currentIndex())
        gSettings.setValue('RecordResolution', resolution.currentIndex())

        FPS = int(fps.currentText())
        HEIGHT = int(resolution.currentText())
        WIDTH = (HEIGHT * 16) / 9
        FMT = 'jpg'

        data = (ctypes.c_ubyte * (WIDTH * HEIGHT * 3))()  # alloc buffer once
        flooredStart = self._timer.secondsToBeats(int(self._timer.beatsToSeconds(self._timer.start) * FPS) / float(FPS))
        duration = self._timer.beatsToSeconds(self._timer.end - flooredStart)

        captureDir = currentProjectDirectory().join('capture')
        captureDir.ensureExists(isFolder=True)

        progress = QProgressDialog(self)
        progress.setMaximum(int(duration * FPS))
        prevFrame = 0
        for frame in range(int(duration * FPS)):
            deltaTime = (frame - prevFrame) / float(FPS)
            prevFrame = frame
            progress.setValue(frame)
            QApplication.processEvents()
            if progress.wasCanceled():
                break
            beats = flooredStart + self._timer.secondsToBeats(frame / float(FPS))

            shot = self.__shotsManager.shotAtTime(beats)
            if shot is None:
                continue
            sceneFile = currentScenesDirectory().join(shot.sceneName).ensureExt(SCENE_EXT)
            scene = Scene.getScene(sceneFile, self.__models)
            scene.setSize(WIDTH, HEIGHT)

            uniforms = self.__shotsManager.evaluate(beats)
            textureUniforms = self.__shotsManager.additionalTextures(beats)
            self.__sceneView._cameraInput.setData(*(uniforms['uOrigin'] + uniforms['uAngles']))  # feed animation into camera so animationprocessor can read it again
            cameraData = self.__sceneView._cameraInput.data()

            modifier = currentProjectDirectory().join('animationprocessor.py')
            if modifier.exists():
                execfile(str(modifier), globals(), locals())

            for name in self.__sceneView._textures:
                uniforms[name] = self.__sceneView._textures[name]._id

            scene.drawToScreen(self._timer.beatsToSeconds(beats), beats, uniforms, (0, 0, WIDTH, HEIGHT), textureUniforms)
            scene.colorBuffers[-1][0].use()

            from OpenGL.GL import glGetTexImage, GL_TEXTURE_2D, GL_RGB, GL_UNSIGNED_BYTE
            glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, data)

            captureDir = currentProjectDirectory().join('capture')
            QImage(data, WIDTH, HEIGHT, QImage.Format_RGB888).mirrored(False, True).save(captureDir.join('dump_%s_%05d.%s' % (FPS, int(self._timer.beatsToSeconds(self._timer.start) * FPS) + frame, FMT)))
        progress.close()

        convertCaptureDir = currentProjectDirectory().join('convertcapture')
        convertCaptureDir.ensureExists(isFolder=True)

        with convertCaptureDir.join('convert.bat').edit() as fh:
            start = ''
            start2 = ''
            if int(self._timer.start * FPS) > 0:
                start = '-start_number {} '.format(int(self._timer.beatsToSeconds(self._timer.start) * FPS))
                start2 = '-vframes {} '.format(int(self._timer.beatsToSeconds(self._timer.end - self._timer.start) * FPS))

            fh.write('cd "../capture"\n"{}" -framerate {} {}-i dump_{}_%%05d.{} {}-c:v libx264 -r {} -pix_fmt yuv420p "../convertcapture/output.mp4"'.format(FFMPEG_PATH, FPS, start, FPS, FMT,
                                                                                                                                                                                  start2, FPS))
        with convertCaptureDir.join('convertGif.bat').edit() as fh:
            start = ''
            start2 = ''
            iln = ''
            if int(self._timer.start * FPS) > 0:
                start = '-start_number {} '.format(int(self._timer.beatsToSeconds(self._timer.start) * FPS))
                start2 = '-vframes {} '.format(int(self._timer.beatsToSeconds(self._timer.end - self._timer.start) * FPS))
                iln = '-t {:03f} '.format(self._timer.beatsToSeconds(self._timer.end - self._timer.start))
            fh.write('REM File format is actually %5d but in a .bat file we need to escape % or something, so you can\'t copy paste this into a cmd prompt without fixing up the %%05d to be %5d.\n')
            fh.write('cd "../capture"\n"{}" -framerate {} {}{}-i dump_{}_%%05d.{} -vf "fps={},scale={}:-1:flags=lanczos,palettegen" palette.png\n'.format(FFMPEG_PATH, FPS, start, iln, FPS, FMT, FPS, HEIGHT))
            fh.write('"{}" -framerate {} {}-i dump_{}_%%05d.{} -i "palette.png" -filter_complex "fps=12,scale=360:-1:flags=lanczos[x];[x][1:v]paletteuse" {}-r {} "../convertcapture/output.gif"'.format(FFMPEG_PATH, FPS, start, FPS, FMT, start2, FPS))

        sound = self.timeSlider.soundtrackPath()
        if not sound:
            return

        with convertCaptureDir.join('merge.bat').edit() as fh:
            startSeconds = self._timer.beatsToSeconds(self._timer.start)
            fh.write('{} -i output.mp4 -itsoffset {} -i "{}" -vcodec copy -shortest merged.mp4'.format(FFMPEG_PATH, -startSeconds, sound))

    def __restoreUiLock(self, action):
        state = True if gSettings.value('lockui', '0') == '1' else False
        action.setChecked(state)
        features = QDockWidget.NoDockWidgetFeatures if state else QDockWidget.AllDockWidgetFeatures
        for dockWidget in self.findChildren(QDockWidget):
            dockWidget.setFeatures(features)

    def __fullScreenViewport(self, *args):
        # force floating
        dockWidget = self.__sceneView.parent()
        if not dockWidget.isFloating():
            dockWidget.setFloating(True)
        if dockWidget.isFullScreen():
            dockWidget.showNormal()
            dockWidget.resize(self.__restoreFullScreenSize)
        else:
            self.__restoreFullScreenSize = dockWidget.size()
            dockWidget.showFullScreen()

    def __toggleUILock(self, state):
        gSettings.setValue('lockui', '1' if state else '0')
        features = QDockWidget.NoDockWidgetFeatures if state else QDockWidget.AllDockWidgetFeatures
        for dockWidget in self.findChildren(QDockWidget):
            # only affect docked widgets
            if not dockWidget.isFloating():
                dockWidget.setFeatures(features)

    def __onViewShot(self, start, end):
        self._timer.start = start
        self._timer.end = end - 0.01

    def __aboutDialog(self):
        QMessageBox.about(self, 'About SqrMelon',
                          r"""<p>SqrMelon is a tool for keyframe animation & fragment shader management for 64k executables.</p>
                          <p>Source code and documentation is available on <a href="https://github.com/thijskruithof/sqrmelon/">GitHub</a>.</p><br/>
                          <p>Note: this is a customized fork of the original <a href="https://github.com/trevorvanhoof/sqrmelon/">SqrMelon project</a>.</p>
                          <p>Icons from <a href="https://icons8.com/">icons8.com</a></p>""")

    def __colorPicker(self):
        color = QColorDialog.getColor()
        color = "vec3(" + str(round(color.red() / 255.0, 2)) + ", " + str(round(color.green() / 255.0, 2)) + ", " + str(round(color.blue() / 255.0, 2)) + ")"
        cb = QApplication.clipboard()
        cb.setText(color, mode=cb.Clipboard)

    def __onCtrlS(self):
        if qApp.focusWidget() != self.__sceneView:
            self.saveProject()
        else:
            self.__sceneView.keyPressEvent(QKeyEvent(QEvent.KeyPress, Qt.Key_S, Qt.ControlModifier))

    def saveProject(self):
        self.__sceneView.saveCameraData()
        self.__shotsManager.saveAllShots()
        self._timer.saveState()
        self.__models.saveToProject()
        self.__modeler.viewport.saveState()

        QMessageBox.information(self, 'Save succesful!', 'Animation, shot & timing changes have been saved.')

    def closeEvent(self, event):
        res = QMessageBox.question(self, 'Save before exit?', 'Do you want to save?', QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
        if res == QMessageBox.Cancel:
            event.ignore()
            return
        if res == QMessageBox.Yes:
            self.saveProject()
        super(App, self).hideEvent(event)

    def __setCurrentShot(self, *args):
        shot = self.__shotsManager.shotAtTime(self._timer.time)
        if shot is None:
            self.__sceneView.setScene(None)
            self.__profiler.setScene(None)
            return
        sceneFile = currentScenesDirectory().join(shot.sceneName + SCENE_EXT)
        sc = Scene.getScene(sceneFile, self.__models)
        self.__sceneView.setScene(sc)
        self.__profiler.setScene(sc)

    def __openProject(self, path):
        setCurrentProjectFilePath(FilePath(path))
        self.__sceneList.projectOpened()
        self.__shotsManager.projectOpened()
        self._timer.projectOpened()
        self.__models.loadFromProject()
        self.__modelsOutliner.reset()
        self.__modeler.viewport.loadState()

    def __initializeProject(self):
        project = currentProjectFilePath()
        if project is not None:
            if project.exists():
                self.__openProject(project)
                return
        project = [x for x in list(os.listdir(os.getcwd())) if x.endswith(PROJ_EXT)]
        if project:
            self.__openProject(os.path.join(os.getcwd(), project[0]))
            return

    def __changeProjectHelper(self, title):
        """
        Utility that shows a dialog if we're changing projects with potentially unsaved changes.
        Returns the current project directory, or the current working directory if no such project.
        """
        currentPath = FilePath(os.getcwd())

        project = currentProjectFilePath()
        if project is not None:
            # propose to save near current project
            dir = project.parent()
            if dir.exists():
                currentPath = dir

            # check if unsaved changes
            if QMessageBox.No == QMessageBox.warning(self, title, 'Any unsaved changes will be lost. Continue?', QMessageBox.Yes | QMessageBox.No):
                return

        return currentPath

    def __onNewProject(self):
        currentPath = self.__changeProjectHelper('Creating new project')
        if currentPath is None:
            return
        res = FileDialog.getSaveFileName(self, 'Create new project', currentPath, 'Project folder')
        if not res:
            return
        shutil.copytree(DEFAULT_PROJECT, res, ignore=lambda p, f: [] if os.path.basename(p).lower() == 'Scenes' else [n for n in f if os.path.splitext(n)[-1].lower() in IGNORED_EXTENSIONS])
        projectFile = FilePath(res).join(os.path.basename(res) + PROJ_EXT)
        projectFile.ensureExists()
        self.__openProject(projectFile)

    def __onOpenProject(self):
        currentPath = self.__changeProjectHelper('Changing project')
        if currentPath is None:
            return
        res = FileDialog.getOpenFileName(self, 'Open project', currentPath, 'Project files (*%s)' % PROJ_EXT)
        if not res:
            return
        self.__openProject(res)

    def _scheduleModelExport(self, model):
        if model is None:
            return

        if not self._exportModelTimer is None:
            self._exportModelTimer.cancel()

        self._exportModelTimer = threading.Timer(0.25, self._doModelExport)
        self._exportedModel = model
        self._exportModelTimer.start()

    def _doModelExport(self):
        self._exportModelTimer = None
        if not self._exportedModel is None:
            self._exportedModel.export(currentModelsDirectory())