@@ -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 | |||
@@ -0,0 +1,6 @@ | |||
#!/bin/bash | |||
ASPATH=$(readlink -f $0) | |||
BINDIR=$(dirname $ASPATH) | |||
exec python3 $BINDIR/../source/frontend/carla_modgui.py "$@" |
@@ -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 "$@" |
@@ -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; | |||
@@ -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; | |||
{ | |||
@@ -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 \ | |||
@@ -0,0 +1,459 @@ | |||
#!/usr/bin/env python3 | |||
# -*- coding: utf-8 -*- | |||
# Carla bridge for LV2 modguis | |||
# Copyright (C) 2015-2019 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 (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) |
@@ -0,0 +1,210 @@ | |||
#!/usr/bin/env python3 | |||
# -*- coding: utf-8 -*- | |||
# Carla bridge for LV2 modguis | |||
# Copyright (C) 2015-2019 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 (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) |
@@ -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 | |||
@@ -99,6 +99,7 @@ typedef std::map<double,const LilvScalePoint*> 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; | |||
} | |||