|  | #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Carla bridge for LV2 modguis
# Copyright (C) 2015-2016 Filipe Coelho <falktx@falktx.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of
# the License, or any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# For a full copy of the GNU General Public License see the doc/GPL.txt file.
# ------------------------------------------------------------------------------------------------------------
# Imports (Config)
from carla_config import *
# ------------------------------------------------------------------------------------------------------------
# Imports (Global)
import json
if config_UseQt5:
    from PyQt5.QtCore import pyqtSlot, QPoint, QThread, QSize, QUrl
    from PyQt5.QtGui import QImage, QPainter, QPalette
    from PyQt5.QtWidgets import QMainWindow
    from PyQt5.QtWebKit import QWebElement, QWebSettings
    from PyQt5.QtWebKitWidgets import QWebView
else:
    from PyQt4.QtCore import pyqtSlot, QPoint, QThread, QSize, QUrl
    from PyQt4.QtGui import QImage, QPainter, QPalette
    from PyQt4.QtGui import QMainWindow
    from PyQt4.QtWebKit import QWebElement, QWebSettings, QWebView
# ------------------------------------------------------------------------------------------------------------
# Imports (tornado)
from tornado.log import enable_pretty_logging
from tornado.ioloop import IOLoop
from tornado.util import unicode_type
from tornado.web import HTTPError
from tornado.web import Application, RequestHandler, StaticFileHandler
# ------------------------------------------------------------------------------------------------------------
# Imports (Custom)
from carla_app import *
from carla_utils import *
# ------------------------------------------------------------------------------------------------------------
# Generate a random port number between 9000 and 18000
from random import random
PORTn = 8998 + int(random()*9000)
# ------------------------------------------------------------------------------------------------------------
# Set up environment for the webserver
PORT     = str(PORTn)
ROOT     = "/usr/share/mod"
#ROOT     = "/home/falktx/FOSS/GIT-mine/MOD/mod-app/source/modules/mod-ui"
DATA_DIR = os.path.expanduser("~/.local/share/mod-data/")
HTML_DIR = os.path.join(ROOT, "html")
os.environ['MOD_DEV_HOST'] = "1"
os.environ['MOD_DEV_HMI']  = "1"
os.environ['MOD_DESKTOP']  = "1"
os.environ['MOD_DATA_DIR']           = DATA_DIR
os.environ['MOD_HTML_DIR']           = HTML_DIR
os.environ['MOD_KEY_PATH']           = os.path.join(DATA_DIR, "keys")
os.environ['MOD_CLOUD_PUB']          = os.path.join(ROOT, "keys", "cloud_key.pub")
os.environ['MOD_PLUGIN_LIBRARY_DIR'] = os.path.join(DATA_DIR, "lib")
os.environ['MOD_PHANTOM_BINARY']        = "/usr/bin/phantomjs"
os.environ['MOD_SCREENSHOT_JS']         = os.path.join(ROOT, "screenshot.js")
os.environ['MOD_DEVICE_WEBSERVER_PORT'] = PORT
# ------------------------------------------------------------------------------------------------------------
# Imports (MOD)
from mod.utils import get_plugin_info, get_plugin_gui, get_plugin_gui_mini, init as lv2_init
# ------------------------------------------------------------------------------------------------------------
# MOD related classes
class JsonRequestHandler(RequestHandler):
    def write(self, data):
        if isinstance(data, (bytes, unicode_type, dict)):
            RequestHandler.write(self, data)
            self.finish()
            return
        elif data is True:
            data = "true"
            self.set_header("Content-Type", "application/json; charset=UTF-8")
        elif data is False:
            data = "false"
            self.set_header("Content-Type", "application/json; charset=UTF-8")
        else:
            data = json.dumps(data)
            self.set_header("Content-Type", "application/json; charset=UTF-8")
        RequestHandler.write(self, data)
        self.finish()
class EffectGet(JsonRequestHandler):
    def get(self):
        uri = self.get_argument('uri')
        try:
            data = get_plugin_info(uri)
        except:
            print("ERROR: get_plugin_info for '%s' failed" % uri)
            raise HTTPError(404)
        self.write(data)
class EffectFile(StaticFileHandler):
    def initialize(self):
        # return custom type directly. The browser will do the parsing
        self.custom_type = None
        uri = self.get_argument('uri')
        try:
            self.modgui = get_plugin_gui(uri)
        except:
            raise HTTPError(404)
        try:
            root = self.modgui['resourcesDirectory']
        except:
            raise HTTPError(404)
        return StaticFileHandler.initialize(self, root)
    def parse_url_path(self, prop):
        try:
            path = self.modgui[prop]
        except:
            raise HTTPError(404)
        if prop in ("iconTemplate", "settingsTemplate", "stylesheet", "javascript"):
            self.custom_type = "text/plain"
        return path
    def get_content_type(self):
        if self.custom_type is not None:
            return self.custom_type
        return StaticFileHandler.get_content_type(self)
class EffectResource(StaticFileHandler):
    def initialize(self):
        # Overrides StaticFileHandler initialize
        pass
    def get(self, path):
        try:
            uri = self.get_argument('uri')
        except:
            return self.shared_resource(path)
        try:
            modgui = get_plugin_gui_mini(uri)
        except:
            raise HTTPError(404)
        try:
            root = modgui['resourcesDirectory']
        except:
            raise HTTPError(404)
        try:
            super(EffectResource, self).initialize(root)
            return super(EffectResource, self).get(path)
        except HTTPError as e:
            if e.status_code != 404:
                raise e
            return self.shared_resource(path)
        except IOError:
            raise HTTPError(404)
    def shared_resource(self, path):
        super(EffectResource, self).initialize(os.path.join(HTML_DIR, 'resources'))
        return super(EffectResource, self).get(path)
# ------------------------------------------------------------------------------------------------------------
# WebServer Thread
class WebServerThread(QThread):
    # signals
    running = pyqtSignal()
    def __init__(self, parent=None):
        QThread.__init__(self, parent)
        self.fApplication = Application(
            [
                (r"/effect/get/?", EffectGet),
                (r"/effect/file/(.*)", EffectFile),
                (r"/resources/(.*)", EffectResource),
                (r"/(.*)", StaticFileHandler, {"path": HTML_DIR}),
            ],
            debug=True)
        self.fPrepareWasCalled = False
    def run(self):
        if not self.fPrepareWasCalled:
            self.fPrepareWasCalled = True
            self.fApplication.listen(PORT, address="0.0.0.0")
            if int(os.getenv("MOD_LOG", "0")):
                enable_pretty_logging()
        self.running.emit()
        IOLoop.instance().start()
    def stopWait(self):
        IOLoop.instance().stop()
        return self.wait(5000)
# ------------------------------------------------------------------------------------------------------------
# Host Window
class HostWindow(QMainWindow):
    # signals
    SIGTERM = pyqtSignal()
    SIGUSR1 = pyqtSignal()
    # --------------------------------------------------------------------------------------------------------
    def __init__(self):
        QMainWindow.__init__(self)
        gCarla.gui = self
        URI = sys.argv[1]
        # ----------------------------------------------------------------------------------------------------
        # Internal stuff
        self.fCurrentFrame = None
        self.fDocElemement = None
        self.fCanSetValues = False
        self.fNeedsShow    = False
        self.fSizeSetup    = False
        self.fQuitReceived = False
        self.fWasRepainted = False
        self.fPlugin      = get_plugin_info(URI)
        self.fPorts       = self.fPlugin['ports']
        self.fPortSymbols = {}
        self.fPortValues  = {}
        for port in self.fPorts['control']['input']:
            self.fPortSymbols[port['index']] = (port['symbol'], False)
            self.fPortValues [port['index']] = port['ranges']['default']
        for port in self.fPorts['control']['output']:
            self.fPortSymbols[port['index']] = (port['symbol'], True)
            self.fPortValues [port['index']] = port['ranges']['default']
        # ----------------------------------------------------------------------------------------------------
        # Init pipe
        if len(sys.argv) == 7:
            self.fPipeClient = gCarla.utils.pipe_client_new(lambda s,msg: self.msgCallback(msg))
        else:
            self.fPipeClient = None
        # ----------------------------------------------------------------------------------------------------
        # Init Web server
        self.fWebServerThread = WebServerThread(self)
        self.fWebServerThread.start()
        # ----------------------------------------------------------------------------------------------------
        # Set up GUI
        self.setContentsMargins(0, 0, 0, 0)
        self.fWebview = QWebView(self)
        #self.fWebview.setAttribute(Qt.WA_OpaquePaintEvent, False)
        #self.fWebview.setAttribute(Qt.WA_TranslucentBackground, True)
        self.setCentralWidget(self.fWebview)
        page = self.fWebview.page()
        page.setViewportSize(QSize(980, 600))
        mainFrame = page.mainFrame()
        mainFrame.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
        mainFrame.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)
        palette = self.fWebview.palette()
        palette.setBrush(QPalette.Base, palette.brush(QPalette.Window))
        page.setPalette(palette)
        self.fWebview.setPalette(palette)
        settings = self.fWebview.settings()
        settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
        self.fWebview.loadFinished.connect(self.slot_webviewLoadFinished)
        url = "http://127.0.0.1:%s/icon.html#%s" % (PORT, URI)
        print("url:", url)
        self.fWebview.load(QUrl(url))
        # ----------------------------------------------------------------------------------------------------
        # Connect actions to functions
        self.SIGTERM.connect(self.slot_handleSIGTERM)
        # ----------------------------------------------------------------------------------------------------
        # Final setup
        self.fIdleTimer = self.startTimer(30)
        if self.fPipeClient is None:
            # testing, show UI only
            self.setWindowTitle("TestUI")
            self.fNeedsShow = True
    # --------------------------------------------------------------------------------------------------------
    def closeExternalUI(self):
        self.fWebServerThread.stopWait()
        if self.fPipeClient is None:
            return
        if not self.fQuitReceived:
            self.send(["exiting"])
        gCarla.utils.pipe_client_destroy(self.fPipeClient)
        self.fPipeClient = None
    def idleStuff(self):
        if self.fPipeClient is not None:
            gCarla.utils.pipe_client_idle(self.fPipeClient)
            self.checkForRepaintChanges()
        if self.fSizeSetup:
            return
        if self.fDocElemement is None or self.fDocElemement.isNull():
            return
        pedal = self.fDocElemement.findFirst(".mod-pedal")
        if pedal.isNull():
            return
        size = pedal.geometry().size()
        if size.width() <= 10 or size.height() <= 10:
            return
        # render web frame to image
        image = QImage(self.fWebview.page().viewportSize(), QImage.Format_ARGB32_Premultiplied)
        image.fill(Qt.transparent)
        painter = QPainter(image)
        self.fCurrentFrame.render(painter)
        painter.end()
        #image.save("/tmp/test.png")
        # get coordinates and size from image
        #x = -1
        #y = -1
        #lastx = -1
        #lasty = -1
        #bgcol = self.fHostColor.rgba()
        #for h in range(0, image.height()):
            #hasNonTransPixels = False
            #for w in range(0, image.width()):
                #if image.pixel(w, h) not in (0, bgcol): # 0xff070707):
                    #hasNonTransPixels = True
                    #if x == -1 or x > w:
                        #x = w
                    #lastx = max(lastx, w)
            #if hasNonTransPixels:
                ##if y == -1:
                    ##y = h
                #lasty = h
        # set size and position accordingly
        #if -1 not in (x, lastx, lasty):
            #self.setFixedSize(lastx-x, lasty)
            #self.fCurrentFrame.setScrollPosition(QPoint(x, 0))
        #else:
        # TODO that^ needs work
        if True:
            self.setFixedSize(size)
        # set initial values
        self.fCurrentFrame.evaluateJavaScript("icongui.setPortValue(':bypass', 0, null)")
        for index in self.fPortValues.keys():
            symbol, isOutput = self.fPortSymbols[index]
            value            = self.fPortValues[index]
            if isOutput:
                self.fCurrentFrame.evaluateJavaScript("icongui.setOutputPortValue('%s', %f)" % (symbol, value))
            else:
                self.fCurrentFrame.evaluateJavaScript("icongui.setPortValue('%s', %f, null)" % (symbol, value))
        # final setup
        self.fCanSetValues = True
        self.fSizeSetup    = True
        self.fDocElemement = None
        if self.fNeedsShow:
            self.show()
    def checkForRepaintChanges(self):
        if not self.fWasRepainted:
            return
        self.fWasRepainted = False
        if not self.fCanSetValues:
            return
        for index in self.fPortValues.keys():
            symbol, isOutput = self.fPortSymbols[index]
            if isOutput:
                continue
            oldValue = self.fPortValues[index]
            newValue = self.fCurrentFrame.evaluateJavaScript("icongui.getPortValue('%s')" % (symbol,))
            if oldValue != newValue:
                self.fPortValues[index] = newValue
                self.send(["control", index, newValue])
    # --------------------------------------------------------------------------------------------------------
    @pyqtSlot(bool)
    def slot_webviewLoadFinished(self, ok):
        page = self.fWebview.page()
        page.repaintRequested.connect(self.slot_repaintRequested)
        self.fCurrentFrame = page.currentFrame()
        self.fDocElemement = self.fCurrentFrame.documentElement()
    def slot_repaintRequested(self):
        if self.fCanSetValues:
            self.fWasRepainted = True
    # --------------------------------------------------------------------------------------------------------
    # Callback
    def msgCallback(self, msg):
        msg = charPtrToString(msg)
        if msg == "control":
            index = int(self.readlineblock())
            value = float(self.readlineblock())
            self.dspParameterChanged(index, value)
        elif msg == "program":
            index = int(self.readlineblock())
            self.dspProgramChanged(index)
        elif msg == "midiprogram":
            bank    = int(self.readlineblock())
            program = float(self.readlineblock())
            self.dspMidiProgramChanged(bank, program)
        elif msg == "configure":
            key   = self.readlineblock()
            value = self.readlineblock()
            self.dspStateChanged(key, value)
        elif msg == "note":
            onOff    = bool(self.readlineblock() == "true")
            channel  = int(self.readlineblock())
            note     = int(self.readlineblock())
            velocity = int(self.readlineblock())
            self.dspNoteReceived(onOff, channel, note, velocity)
        elif msg == "atom":
            index      = int(self.readlineblock())
            size       = int(self.readlineblock())
            base64atom = self.readlineblock()
            # nothing to do yet
        elif msg == "urid":
            urid = int(self.readlineblock())
            uri  = self.readlineblock()
            # nothing to do yet
        elif msg == "uiOptions":
            sampleRate     = float(self.readlineblock())
            useTheme       = bool(self.readlineblock() == "true")
            useThemeColors = bool(self.readlineblock() == "true")
            windowTitle    = self.readlineblock()
            transWindowId  = int(self.readlineblock())
            self.uiTitleChanged(windowTitle)
        elif msg == "show":
            self.uiShow()
        elif msg == "focus":
            self.uiFocus()
        elif msg == "hide":
            self.uiHide()
        elif msg == "quit":
            self.fQuitReceived = True
            self.uiQuit()
        elif msg == "uiTitle":
            uiTitle = self.readlineblock()
            self.uiTitleChanged(uiTitle)
        else:
            print("unknown message: \"" + msg + "\"")
    # --------------------------------------------------------------------------------------------------------
    def dspParameterChanged(self, index, value):
        self.fPortValues[index] = value
        if self.fCurrentFrame is not None and self.fCanSetValues:
            symbol, isOutput = self.fPortSymbols[index]
            if isOutput:
                self.fPortValues[index] = value
                self.fCurrentFrame.evaluateJavaScript("icongui.setOutputPortValue('%s', %f)" % (symbol, value))
            else:
                self.fCurrentFrame.evaluateJavaScript("icongui.setPortValue('%s', %f, null)" % (symbol, value))
    def dspProgramChanged(self, index):
        return
    def dspMidiProgramChanged(self, bank, program):
        return
    def dspStateChanged(self, key, value):
        return
    def dspNoteReceived(self, onOff, channel, note, velocity):
        return
    # --------------------------------------------------------------------------------------------------------
    def uiShow(self):
        if self.fSizeSetup:
            self.show()
        else:
            self.fNeedsShow = True
    def uiFocus(self):
        if not self.fSizeSetup:
            return
        self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
        self.show()
        self.raise_()
        self.activateWindow()
    def uiHide(self):
        self.hide()
    def uiQuit(self):
        self.closeExternalUI()
        self.close()
        app.quit()
    def uiTitleChanged(self, uiTitle):
        self.setWindowTitle(uiTitle)
    # --------------------------------------------------------------------------------------------------------
    # Qt events
    def closeEvent(self, event):
        self.closeExternalUI()
        QMainWindow.closeEvent(self, event)
        # there might be other qt windows open which will block carla-modgui from quitting
        app.quit()
    def timerEvent(self, event):
        if event.timerId() == self.fIdleTimer:
            self.idleStuff()
        QMainWindow.timerEvent(self, event)
    # --------------------------------------------------------------------------------------------------------
    @pyqtSlot()
    def slot_handleSIGTERM(self):
        print("Got SIGTERM -> Closing now")
        self.close()
    # --------------------------------------------------------------------------------------------------------
    # Internal stuff
    def readlineblock(self):
        if self.fPipeClient is None:
            return ""
        return gCarla.utils.pipe_client_readlineblock(self.fPipeClient, 5000)
    def send(self, lines):
        if self.fPipeClient is None or len(lines) == 0:
            return
        gCarla.utils.pipe_client_lock(self.fPipeClient)
        # this must never fail, we need to unlock at the end
        try:
            for line in lines:
                if line is None:
                    line2 = "(null)"
                elif isinstance(line, str):
                    line2 = line.replace("\n", "\r")
                elif isinstance(line, bool):
                    line2 = "true" if line else "false"
                elif isinstance(line, int):
                    line2 = "%i" % line
                elif isinstance(line, float):
                    line2 = "%.10f" % line
                else:
                    print("unknown data type to send:", type(line))
                    return
                gCarla.utils.pipe_client_write_msg(self.fPipeClient, line2 + "\n")
        except:
            pass
        gCarla.utils.pipe_client_flush_and_unlock(self.fPipeClient)
# ------------------------------------------------------------------------------------------------------------
# Main
if __name__ == '__main__':
    # -------------------------------------------------------------
    # Read CLI args
    if len(sys.argv) < 2:
        print("usage: %s <plugin-uri>" % sys.argv[0])
        sys.exit(1)
    libPrefix = os.getenv("CARLA_LIB_PREFIX")
    # -------------------------------------------------------------
    # App initialization
    app = CarlaApplication("Carla2-MODGUI", libPrefix)
    # -------------------------------------------------------------
    # Init utils
    pathBinaries, pathResources = getPaths(libPrefix)
    utilsname = "libcarla_utils.%s" % (DLL_EXTENSION)
    gCarla.utils = CarlaUtils(os.path.join(pathBinaries, utilsname))
    gCarla.utils.set_process_name("carla-bridge-lv2-modgui")
    # -------------------------------------------------------------
    # Set-up custom signal handling
    setUpSignals()
    # -------------------------------------------------------------
    # Init LV2
    lv2_init()
    # -------------------------------------------------------------
    # Create GUI
    gui = HostWindow()
    # --------------------------------------------------------------------------------------------------------
    # App-Loop
    app.exit_exec()
# ------------------------------------------------------------------------------------------------------------
 |