|
|
|
@@ -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) |