From d0610840bd1b4395271cdf34dab13d0a9a873b2a Mon Sep 17 00:00:00 2001 From: falkTX Date: Tue, 19 Mar 2019 23:40:32 +0100 Subject: [PATCH] Add back modgui support Signed-off-by: falkTX --- Makefile | 19 + bin/carla-bridge-lv2-modgui | 6 + data/carla-bridge-lv2-modgui | 11 + source/backend/plugin/CarlaPluginLV2.cpp | 30 +- source/backend/utils/CachedPlugins.cpp | 2 +- source/frontend/Makefile | 1 + source/frontend/modgui/__init__.py | 0 source/frontend/modgui/host.py | 459 +++++++++++++++++++++++ source/frontend/modgui/webserver.py | 210 +++++++++++ source/includes/lv2_rdf.hpp | 2 + source/utils/CarlaLv2Utils.hpp | 30 +- 11 files changed, 762 insertions(+), 8 deletions(-) create mode 100755 bin/carla-bridge-lv2-modgui create mode 100644 data/carla-bridge-lv2-modgui create mode 100644 source/frontend/modgui/__init__.py create mode 100644 source/frontend/modgui/host.py create mode 100644 source/frontend/modgui/webserver.py diff --git a/Makefile b/Makefile index f12ba2d21..88b1b5468 100644 --- a/Makefile +++ b/Makefile @@ -522,6 +522,14 @@ ifeq ($(HAVE_LIBLO),true) $(DESTDIR)$(BINDIR)/carla-control endif + # Install the real modgui bridge + install -m 755 \ + data/carla-bridge-lv2-modgui \ + $(DESTDIR)$(LIBDIR)/carla + + sed $(SED_ARGS) 's?X-PREFIX-X?$(PREFIX)?' \ + $(DESTDIR)$(LIBDIR)/carla/carla-bridge-lv2-modgui + # Install frontend install -m 644 \ source/frontend/carla \ @@ -592,6 +600,7 @@ endif install -m 644 resources/scalable/carla-control.svg $(DESTDIR)$(DATADIR)/icons/hicolor/scalable/apps # Install resources (re-use python files) + $(LINK) ../modgui $(DESTDIR)$(DATADIR)/carla/resources $(LINK) ../patchcanvas $(DESTDIR)$(DATADIR)/carla/resources $(LINK) ../widgets $(DESTDIR)$(DATADIR)/carla/resources $(LINK) ../carla_app.py $(DESTDIR)$(DATADIR)/carla/resources @@ -690,6 +699,16 @@ ifeq ($(HAVE_PYQT),true) endif endif + # ------------------------------------------------------------------------------------------------------------- + +ifneq ($(HAVE_PYQT),true) + # Remove gui files for non-gui build + rm $(DESTDIR)$(LIBDIR)/carla/carla-bridge-lv2-modgui + rm $(DESTDIR)$(LIBDIR)/lv2/carla.lv2/carla-bridge-lv2-modgui +endif + +# --------------------------------------------------------------------------------------------------------------------- + ifneq ($(EXTERNAL_PLUGINS),true) install_external_plugins: endif diff --git a/bin/carla-bridge-lv2-modgui b/bin/carla-bridge-lv2-modgui new file mode 100755 index 000000000..323d86863 --- /dev/null +++ b/bin/carla-bridge-lv2-modgui @@ -0,0 +1,6 @@ +#!/bin/bash + +ASPATH=$(readlink -f $0) +BINDIR=$(dirname $ASPATH) + +exec python3 $BINDIR/../source/frontend/carla_modgui.py "$@" diff --git a/data/carla-bridge-lv2-modgui b/data/carla-bridge-lv2-modgui new file mode 100644 index 000000000..dba0c9661 --- /dev/null +++ b/data/carla-bridge-lv2-modgui @@ -0,0 +1,11 @@ +#!/bin/bash + +PYTHON=$(which python3 2>/dev/null) + +if [ ! -f ${PYTHON} ]; then + PYTHON=python +fi + +INSTALL_PREFIX="X-PREFIX-X" +export CARLA_LIB_PREFIX="$INSTALL_PREFIX" +exec $PYTHON "$INSTALL_PREFIX"/share/carla/carla_modgui.py "$@" diff --git a/source/backend/plugin/CarlaPluginLV2.cpp b/source/backend/plugin/CarlaPluginLV2.cpp index 250d3569c..418ab0023 100644 --- a/source/backend/plugin/CarlaPluginLV2.cpp +++ b/source/backend/plugin/CarlaPluginLV2.cpp @@ -1512,6 +1512,11 @@ public: fPipeServer.flushMessages(); } + +#ifndef BUILD_BRIDGE + if (fUI.rdfDescriptor->Type == LV2_UI_MOD) + pData->tryTransient(); +#endif } else { @@ -4640,6 +4645,9 @@ public: case LV2_UI_X11: bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-lv2-x11"; break; + case LV2_UI_MOD: + bridgeBinary += CARLA_OS_SEP_STR "carla-bridge-lv2-modgui"; + break; #if 0 case LV2_UI_EXTERNAL: case LV2_UI_OLD_EXTERNAL: @@ -5691,8 +5699,8 @@ public: // --------------------------------------------------------------- // find the most appropriate ui - int eQt4, eQt5, eGtk2, eGtk3, eCocoa, eWindows, eX11, iCocoa, iWindows, iX11, iExt, iFinal; - eQt4 = eQt5 = eGtk2 = eGtk3 = eCocoa = eWindows = eX11 = iCocoa = iWindows = iX11 = iExt = iFinal = -1; + int eQt4, eQt5, eGtk2, eGtk3, eCocoa, eWindows, eX11, eMod, iCocoa, iWindows, iX11, iExt, iFinal; + eQt4 = eQt5 = eGtk2 = eGtk3 = eCocoa = eWindows = eX11 = eMod = iCocoa = iWindows = iX11 = iExt = iFinal = -1; #if defined(LV2_UIS_ONLY_BRIDGES) const bool preferUiBridges = true; @@ -5750,6 +5758,9 @@ public: case LV2_UI_OLD_EXTERNAL: iExt = ii; break; + case LV2_UI_MOD: + eMod = ii; + break; default: break; } @@ -5816,8 +5827,14 @@ public: if (iFinal < 0) { - carla_stderr("Failed to find an appropriate LV2 UI for this plugin"); - return; + if (eMod < 0) + { + carla_stderr("Failed to find an appropriate LV2 UI for this plugin"); + return; + } + + // use MODGUI as last resort + iFinal = eMod; } } @@ -5867,7 +5884,8 @@ public: iFinal == eGtk3 || iFinal == eCocoa || iFinal == eWindows || - iFinal == eX11) + iFinal == eX11 || + iFinal == eMod) #ifdef BUILD_BRIDGE && ! hasShowInterface #endif @@ -5891,7 +5909,7 @@ public: return; } - if (iFinal == eQt4 || iFinal == eQt5 || iFinal == eGtk2 || iFinal == eGtk3) + if (iFinal == eQt4 || iFinal == eQt5 || iFinal == eGtk2 || iFinal == eGtk3 || iFinal == eMod) { carla_stderr2("Failed to find UI bridge binary for '%s', cannot use UI", pData->name); fUI.rdfDescriptor = nullptr; diff --git a/source/backend/utils/CachedPlugins.cpp b/source/backend/utils/CachedPlugins.cpp index c2c9d43fd..869e72c0e 100644 --- a/source/backend/utils/CachedPlugins.cpp +++ b/source/backend/utils/CachedPlugins.cpp @@ -187,7 +187,7 @@ static const CarlaCachedPluginInfo* get_cached_plugin_lv2(Lv2WorldClass& lv2Worl info.hints = 0x0; - if (lilvPlugin.get_uis().size() > 0) + if (lilvPlugin.get_uis().size() > 0 || lilvPlugin.get_modgui_resources_directory().as_uri() != nullptr) info.hints |= CB::PLUGIN_HAS_CUSTOM_UI; { diff --git a/source/frontend/Makefile b/source/frontend/Makefile index 5f7208a64..2c98adf57 100644 --- a/source/frontend/Makefile +++ b/source/frontend/Makefile @@ -56,6 +56,7 @@ RES = \ $(BINDIR)/resources/carla_control.py \ $(BINDIR)/resources/carla_database.py \ $(BINDIR)/resources/carla_host.py \ + $(BINDIR)/resources/carla_modgui.py \ $(BINDIR)/resources/carla_settings.py \ $(BINDIR)/resources/carla_skin.py \ $(BINDIR)/resources/carla_shared.py \ diff --git a/source/frontend/modgui/__init__.py b/source/frontend/modgui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/source/frontend/modgui/host.py b/source/frontend/modgui/host.py new file mode 100644 index 000000000..e4ed56eb4 --- /dev/null +++ b/source/frontend/modgui/host.py @@ -0,0 +1,459 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Carla bridge for LV2 modguis +# Copyright (C) 2015-2019 Filipe Coelho +# +# 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 (Global) + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QSize, QUrl +from PyQt5.QtGui import QImage, QPainter, QPalette +from PyQt5.QtWidgets import QApplication, QMainWindow +from PyQt5.QtWebKit import QWebSettings +from PyQt5.QtWebKitWidgets import QWebView + +import sys + +# ------------------------------------------------------------------------------------------------------------ +# Imports (Custom) + +from carla_host import charPtrToString, gCarla +from .webserver import WebServerThread, PORT + +# ------------------------------------------------------------------------------------------------------------ +# Imports (MOD) + +from mod.utils import get_plugin_info, init as lv2_init + +# ------------------------------------------------------------------------------------------------------------ +# 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 + + lv2_init() + + 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() + QApplication.instance().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 + QApplication.instance().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) diff --git a/source/frontend/modgui/webserver.py b/source/frontend/modgui/webserver.py new file mode 100644 index 000000000..9339f222c --- /dev/null +++ b/source/frontend/modgui/webserver.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Carla bridge for LV2 modguis +# Copyright (C) 2015-2019 Filipe Coelho +# +# 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 (Global) + +import os + +from PyQt5.QtCore import pyqtSignal, QThread + +# ------------------------------------------------------------------------------------------------------------ +# Generate a random port number between 9000 and 18000 + +from random import random + +PORTn = 8998 + int(random()*9000) + +# ------------------------------------------------------------------------------------------------------------ +# 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 + +# ------------------------------------------------------------------------------------------------------------ +# Set up environment for the webserver + +PORT = str(PORTn) +ROOT = "/usr/share/mod" +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) diff --git a/source/includes/lv2_rdf.hpp b/source/includes/lv2_rdf.hpp index fa7582f2a..3c0a503b9 100644 --- a/source/includes/lv2_rdf.hpp +++ b/source/includes/lv2_rdf.hpp @@ -234,6 +234,7 @@ typedef uint32_t LV2_Property; #define LV2_UI_X11 7 #define LV2_UI_EXTERNAL 8 #define LV2_UI_OLD_EXTERNAL 9 +#define LV2_UI_MOD 10 #define LV2_IS_UI_GTK2(x) ((x) == LV2_UI_GTK2) #define LV2_IS_UI_GTK3(x) ((x) == LV2_UI_GTK3) @@ -244,6 +245,7 @@ typedef uint32_t LV2_Property; #define LV2_IS_UI_X11(x) ((x) == LV2_UI_X11) #define LV2_IS_UI_EXTERNAL(x) ((x) == LV2_UI_EXTERNAL) #define LV2_IS_UI_OLD_EXTERNAL(x) ((x) == LV2_UI_OLD_EXTERNAL) +#define LV2_IS_UI_MOD(x) ((x) == LV2_UI_MOD) // Plugin Types #define LV2_PLUGIN_DELAY 0x000001 diff --git a/source/utils/CarlaLv2Utils.hpp b/source/utils/CarlaLv2Utils.hpp index 4a37ce9af..77544a537 100644 --- a/source/utils/CarlaLv2Utils.hpp +++ b/source/utils/CarlaLv2Utils.hpp @@ -99,6 +99,7 @@ typedef std::map LilvScalePointMap; #define NS_rdfs "http://www.w3.org/2000/01/rdf-schema#" #define NS_llmm "http://ll-plugins.nongnu.org/lv2/ext/midimap#" #define NS_devp "http://lv2plug.in/ns/dev/extportinfo#" +#define NS_mod "http://moddevices.com/ns/modgui#" #define LV2_MIDI_Map__CC "http://ll-plugins.nongnu.org/lv2/namespace#CC" #define LV2_MIDI_Map__NRPN "http://ll-plugins.nongnu.org/lv2/namespace#NRPN" @@ -2624,9 +2625,13 @@ const LV2_RDF_Descriptor* lv2_rdf_new(const LV2_URI uri, const bool loadPresets) // ---------------------------------------------------------------------------------------------------------------- // Set Plugin UIs { + const bool hasMODGui(lilvPlugin.get_modgui_resources_directory().as_uri() != nullptr); + Lilv::UIs lilvUIs(lilvPlugin.get_uis()); - if (const uint numUIs = lilvUIs.size()) + const uint numUIs = lilvUIs.size() + (hasMODGui ? 1 : 0); + + if (numUIs > 0) { rdfDescriptor->UIs = new LV2_RDF_UI[numUIs]; @@ -2821,6 +2826,29 @@ const LV2_RDF_Descriptor* lv2_rdf_new(const LV2_URI uri, const bool loadPresets) } } + for (; hasMODGui;) + { + CARLA_SAFE_ASSERT_BREAK(numUsed == numUIs-1); + + LV2_RDF_UI* const rdfUI(&rdfDescriptor->UIs[numUsed++]); + + // ------------------------------------------------------- + // Set UI Type + + rdfUI->Type = LV2_UI_MOD; + + // ------------------------------------------------------- + // Set UI Information + + if (const char* const resDir = lilvPlugin.get_modgui_resources_directory().as_uri()) + rdfUI->URI = carla_strdup_free(lilv_file_uri_parse(resDir, nullptr)); + + if (rdfDescriptor->Bundle != nullptr) + rdfUI->Bundle = carla_strdup(rdfDescriptor->Bundle); + + break; + } + rdfDescriptor->UICount = numUsed; }