Cleanup
[carla.git] / source / frontend / modgui / host.py
blob8d8e3334f2e950475797bfbbbb5a6a3c43f8741e
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
4 # Carla bridge for LV2 modguis
5 # Copyright (C) 2015-2019 Filipe Coelho <falktx@falktx.com>
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License as
9 # published by the Free Software Foundation; either version 2 of
10 # the License, or any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # For a full copy of the GNU General Public License see the doc/GPL.txt file.
19 # ------------------------------------------------------------------------------------------------------------
20 # Imports (Global)
22 from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QSize, QUrl
23 from PyQt5.QtGui import QImage, QPainter, QPalette
24 from PyQt5.QtWidgets import QApplication, QMainWindow
25 from PyQt5.QtWebKit import QWebSettings
26 from PyQt5.QtWebKitWidgets import QWebView
28 import sys
30 # ------------------------------------------------------------------------------------------------------------
31 # Imports (Custom)
33 from carla_host import charPtrToString, gCarla
34 from .webserver import WebServerThread, PORT
36 # ------------------------------------------------------------------------------------------------------------
37 # Imports (MOD)
39 from modtools.utils import get_plugin_info, init as lv2_init
41 # ------------------------------------------------------------------------------------------------------------
42 # Host Window
44 class HostWindow(QMainWindow):
45 # signals
46 SIGTERM = pyqtSignal()
47 SIGUSR1 = pyqtSignal()
49 # --------------------------------------------------------------------------------------------------------
51 def __init__(self):
52 QMainWindow.__init__(self)
53 gCarla.gui = self
55 URI = sys.argv[1]
57 # ----------------------------------------------------------------------------------------------------
58 # Internal stuff
60 self.fCurrentFrame = None
61 self.fDocElemement = None
62 self.fCanSetValues = False
63 self.fNeedsShow = False
64 self.fSizeSetup = False
65 self.fQuitReceived = False
66 self.fWasRepainted = False
68 lv2_init()
70 self.fPlugin = get_plugin_info(URI)
71 self.fPorts = self.fPlugin['ports']
72 self.fPortSymbols = {}
73 self.fPortValues = {}
74 self.fParamTypes = {}
75 self.fParamValues = {}
77 for port in self.fPorts['control']['input']:
78 self.fPortSymbols[port['index']] = (port['symbol'], False)
79 self.fPortValues [port['index']] = port['ranges']['default']
81 for port in self.fPorts['control']['output']:
82 self.fPortSymbols[port['index']] = (port['symbol'], True)
83 self.fPortValues [port['index']] = port['ranges']['default']
85 for parameter in self.fPlugin['parameters']:
86 if parameter['ranges'] is None:
87 continue
88 if parameter['type'] == "http://lv2plug.in/ns/ext/atom#Bool":
89 paramtype = 'b'
90 elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#Int":
91 paramtype = 'i'
92 elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#Long":
93 paramtype = 'l'
94 elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#Float":
95 paramtype = 'f'
96 elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#Double":
97 paramtype = 'g'
98 elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#String":
99 paramtype = 's'
100 elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#Path":
101 paramtype = 'p'
102 elif parameter['type'] == "http://lv2plug.in/ns/ext/atom#URI":
103 paramtype = 'u'
104 else:
105 continue
106 if paramtype not in ('s','p','u') and parameter['ranges']['minimum'] == parameter['ranges']['maximum']:
107 continue
108 self.fParamTypes [parameter['uri']] = paramtype
109 self.fParamValues[parameter['uri']] = parameter['ranges']['default']
111 # ----------------------------------------------------------------------------------------------------
112 # Init pipe
114 if len(sys.argv) == 7:
115 self.fPipeClient = gCarla.utils.pipe_client_new(lambda s,msg: self.msgCallback(msg))
116 else:
117 self.fPipeClient = None
119 # ----------------------------------------------------------------------------------------------------
120 # Init Web server
122 self.fWebServerThread = WebServerThread(self)
123 self.fWebServerThread.start()
125 # ----------------------------------------------------------------------------------------------------
126 # Set up GUI
128 self.setContentsMargins(0, 0, 0, 0)
130 self.fWebview = QWebView(self)
131 #self.fWebview.setAttribute(Qt.WA_OpaquePaintEvent, False)
132 #self.fWebview.setAttribute(Qt.WA_TranslucentBackground, True)
133 self.setCentralWidget(self.fWebview)
135 page = self.fWebview.page()
136 page.setViewportSize(QSize(980, 600))
138 mainFrame = page.mainFrame()
139 mainFrame.setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
140 mainFrame.setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff)
142 palette = self.fWebview.palette()
143 palette.setBrush(QPalette.Base, palette.brush(QPalette.Window))
144 page.setPalette(palette)
145 self.fWebview.setPalette(palette)
147 settings = self.fWebview.settings()
148 settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
150 self.fWebview.loadFinished.connect(self.slot_webviewLoadFinished)
152 url = "http://127.0.0.1:%s/icon.html#%s" % (PORT, URI)
153 print("url:", url)
154 self.fWebview.load(QUrl(url))
156 # ----------------------------------------------------------------------------------------------------
157 # Connect actions to functions
159 self.SIGTERM.connect(self.slot_handleSIGTERM)
161 # ----------------------------------------------------------------------------------------------------
162 # Final setup
164 self.fIdleTimer = self.startTimer(30)
166 if self.fPipeClient is None:
167 # testing, show UI only
168 self.setWindowTitle("TestUI")
169 self.fNeedsShow = True
171 # --------------------------------------------------------------------------------------------------------
173 def closeExternalUI(self):
174 self.fWebServerThread.stopWait()
176 if self.fPipeClient is None:
177 return
179 if not self.fQuitReceived:
180 self.send(["exiting"])
182 gCarla.utils.pipe_client_destroy(self.fPipeClient)
183 self.fPipeClient = None
185 def idleStuff(self):
186 if self.fPipeClient is not None:
187 gCarla.utils.pipe_client_idle(self.fPipeClient)
188 self.checkForRepaintChanges()
190 if self.fSizeSetup:
191 return
192 if self.fDocElemement is None or self.fDocElemement.isNull():
193 return
195 pedal = self.fDocElemement.findFirst(".mod-pedal")
197 if pedal.isNull():
198 return
200 size = pedal.geometry().size()
202 if size.width() <= 10 or size.height() <= 10:
203 return
205 # render web frame to image
206 image = QImage(self.fWebview.page().viewportSize(), QImage.Format_ARGB32_Premultiplied)
207 image.fill(Qt.transparent)
209 painter = QPainter(image)
210 self.fCurrentFrame.render(painter)
211 painter.end()
213 #image.save("/tmp/test.png")
215 # get coordinates and size from image
216 #x = -1
217 #y = -1
218 #lastx = -1
219 #lasty = -1
220 #bgcol = self.fHostColor.rgba()
222 #for h in range(0, image.height()):
223 #hasNonTransPixels = False
225 #for w in range(0, image.width()):
226 #if image.pixel(w, h) not in (0, bgcol): # 0xff070707):
227 #hasNonTransPixels = True
228 #if x == -1 or x > w:
229 #x = w
230 #lastx = max(lastx, w)
232 #if hasNonTransPixels:
233 ##if y == -1:
234 ##y = h
235 #lasty = h
237 # set size and position accordingly
238 #if -1 not in (x, lastx, lasty):
239 #self.setFixedSize(lastx-x, lasty)
240 #self.fCurrentFrame.setScrollPosition(QPoint(x, 0))
241 #else:
243 # TODO that^ needs work
244 if True:
245 self.setFixedSize(size)
247 # set initial values
248 self.fCurrentFrame.evaluateJavaScript("icongui.setPortWidgetsValue(':bypass', 0, null)")
250 for index in self.fPortValues.keys():
251 symbol, isOutput = self.fPortSymbols[index]
252 value = self.fPortValues[index]
253 if isOutput:
254 self.fCurrentFrame.evaluateJavaScript("icongui.setOutputPortValue('%s', %f)" % (symbol, value))
255 else:
256 self.fCurrentFrame.evaluateJavaScript("icongui.setPortWidgetsValue('%s', %f, null)" % (symbol, value))
258 for uri in self.fParamValues.keys():
259 ptype = self.fParamTypes[uri]
260 value = str(self.fParamValues[uri])
261 print("icongui.setWritableParameterValue('%s', '%c', %s, 'from-carla')" % (uri, ptype, value))
262 self.fCurrentFrame.evaluateJavaScript("icongui.setWritableParameterValue('%s', '%c', %s, 'from-carla')" % (
263 uri, ptype, value))
265 # final setup
266 self.fCanSetValues = True
267 self.fSizeSetup = True
268 self.fDocElemement = None
270 if self.fNeedsShow:
271 self.show()
273 def checkForRepaintChanges(self):
274 if not self.fWasRepainted:
275 return
277 self.fWasRepainted = False
279 if not self.fCanSetValues:
280 return
282 for index in self.fPortValues.keys():
283 symbol, isOutput = self.fPortSymbols[index]
285 if isOutput:
286 continue
288 oldValue = self.fPortValues[index]
289 newValue = self.fCurrentFrame.evaluateJavaScript("icongui.controls['%s'].value" % (symbol,))
291 if oldValue != newValue:
292 self.fPortValues[index] = newValue
293 self.send(["control", index, newValue])
295 for uri in self.fParamValues.keys():
296 oldValue = self.fParamValues[uri]
297 newValue = self.fCurrentFrame.evaluateJavaScript("icongui.parameters['%s'].value" % (uri,))
299 if oldValue != newValue:
300 self.fParamValues[uri] = newValue
301 self.send(["pcontrol", uri, newValue])
303 # --------------------------------------------------------------------------------------------------------
305 @pyqtSlot(bool)
306 def slot_webviewLoadFinished(self, ok):
307 page = self.fWebview.page()
308 page.repaintRequested.connect(self.slot_repaintRequested)
310 self.fCurrentFrame = page.currentFrame()
311 self.fDocElemement = self.fCurrentFrame.documentElement()
313 def slot_repaintRequested(self):
314 if self.fCanSetValues:
315 self.fWasRepainted = True
317 # --------------------------------------------------------------------------------------------------------
318 # Callback
320 def msgCallback(self, msg):
321 msg = charPtrToString(msg)
323 if msg == "control":
324 index = self.readlineblock_int()
325 value = self.readlineblock_float()
326 self.dspControlChanged(index, value)
328 elif msg == "parameter":
329 uri = self.readlineblock()
330 value = self.readlineblock_float()
331 self.dspParameterChanged(uri, value)
333 elif msg == "program":
334 index = self.readlineblock_int()
335 self.dspProgramChanged(index)
337 elif msg == "midiprogram":
338 bank = self.readlineblock_int()
339 program = self.readlineblock_int()
340 self.dspMidiProgramChanged(bank, program)
342 elif msg == "configure":
343 key = self.readlineblock()
344 value = self.readlineblock()
345 self.dspStateChanged(key, value)
347 elif msg == "note":
348 onOff = self.readlineblock_bool()
349 channel = self.readlineblock_int()
350 note = self.readlineblock_int()
351 velocity = self.readlineblock_int()
352 self.dspNoteReceived(onOff, channel, note, velocity)
354 elif msg == "atom":
355 index = self.readlineblock_int()
356 atomsize = self.readlineblock_int()
357 base64size = self.readlineblock_int()
358 base64atom = self.readlineblock()
359 # nothing to do yet
361 elif msg == "urid":
362 urid = self.readlineblock_int()
363 size = self.readlineblock_int()
364 uri = self.readlineblock()
365 # nothing to do yet
367 elif msg == "uiOptions":
368 sampleRate = self.readlineblock_float()
369 bgColor = self.readlineblock_int()
370 fgColor = self.readlineblock_int()
371 uiScale = self.readlineblock_float()
372 useTheme = self.readlineblock_bool()
373 useThemeColors = self.readlineblock_bool()
374 windowTitle = self.readlineblock()
375 transWindowId = self.readlineblock_int()
376 self.uiTitleChanged(windowTitle)
378 elif msg == "show":
379 self.uiShow()
381 elif msg == "focus":
382 self.uiFocus()
384 elif msg == "hide":
385 self.uiHide()
387 elif msg == "quit":
388 self.fQuitReceived = True
389 self.uiQuit()
391 elif msg == "uiTitle":
392 uiTitle = self.readlineblock()
393 self.uiTitleChanged(uiTitle)
395 else:
396 print("unknown message: \"" + msg + "\"")
398 # --------------------------------------------------------------------------------------------------------
400 def dspControlChanged(self, index, value):
401 self.fPortValues[index] = value
403 if self.fCurrentFrame is None or not self.fCanSetValues:
404 return
406 symbol, isOutput = self.fPortSymbols[index]
408 if isOutput:
409 self.fCurrentFrame.evaluateJavaScript("icongui.setOutputPortValue('%s', %f)" % (symbol, value))
410 else:
411 self.fCurrentFrame.evaluateJavaScript("icongui.setPortWidgetsValue('%s', %f, null)" % (symbol, value))
413 def dspParameterChanged(self, uri, value):
414 print("dspParameterChanged", uri, value)
415 if uri not in self.fParamValues:
416 return
418 self.fParamValues[uri] = value
420 if self.fCurrentFrame is None or not self.fCanSetValues:
421 return
423 ptype = self.fParamTypes[uri]
425 self.fCurrentFrame.evaluateJavaScript("icongui.setWritableParameterValue('%s', '%c', %f, 'from-carla')" % (
426 uri, ptype, value))
428 def dspProgramChanged(self, index):
429 return
431 def dspMidiProgramChanged(self, bank, program):
432 return
434 def dspStateChanged(self, key, value):
435 return
437 def dspNoteReceived(self, onOff, channel, note, velocity):
438 return
440 # --------------------------------------------------------------------------------------------------------
442 def uiShow(self):
443 if self.fSizeSetup:
444 self.show()
445 else:
446 self.fNeedsShow = True
448 def uiFocus(self):
449 if not self.fSizeSetup:
450 return
452 self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
453 self.show()
455 self.raise_()
456 self.activateWindow()
458 def uiHide(self):
459 self.hide()
461 def uiQuit(self):
462 self.closeExternalUI()
463 self.close()
464 QApplication.instance().quit()
466 def uiTitleChanged(self, uiTitle):
467 self.setWindowTitle(uiTitle)
469 # --------------------------------------------------------------------------------------------------------
470 # Qt events
472 def closeEvent(self, event):
473 self.closeExternalUI()
474 QMainWindow.closeEvent(self, event)
476 # there might be other qt windows open which will block carla-modgui from quitting
477 QApplication.instance().quit()
479 def timerEvent(self, event):
480 if event.timerId() == self.fIdleTimer:
481 self.idleStuff()
483 QMainWindow.timerEvent(self, event)
485 # --------------------------------------------------------------------------------------------------------
487 @pyqtSlot()
488 def slot_handleSIGTERM(self):
489 print("Got SIGTERM -> Closing now")
490 self.close()
492 # --------------------------------------------------------------------------------------------------------
493 # Internal stuff
495 def readlineblock(self):
496 if self.fPipeClient is None:
497 return ""
499 return gCarla.utils.pipe_client_readlineblock(self.fPipeClient, 5000)
501 def readlineblock_bool(self):
502 if self.fPipeClient is None:
503 return False
505 return gCarla.utils.pipe_client_readlineblock_bool(self.fPipeClient, 5000)
507 def readlineblock_int(self):
508 if self.fPipeClient is None:
509 return 0
511 return gCarla.utils.pipe_client_readlineblock_int(self.fPipeClient, 5000)
513 def readlineblock_float(self):
514 if self.fPipeClient is None:
515 return 0.0
517 return gCarla.utils.pipe_client_readlineblock_float(self.fPipeClient, 5000)
519 def send(self, lines):
520 if self.fPipeClient is None or len(lines) == 0:
521 return
523 gCarla.utils.pipe_client_lock(self.fPipeClient)
525 # this must never fail, we need to unlock at the end
526 try:
527 for line in lines:
528 if line is None:
529 line2 = "(null)"
530 elif isinstance(line, str):
531 line2 = line.replace("\n", "\r")
532 elif isinstance(line, bool):
533 line2 = "true" if line else "false"
534 elif isinstance(line, int):
535 line2 = "%i" % line
536 elif isinstance(line, float):
537 line2 = "%.10f" % line
538 else:
539 print("unknown data type to send:", type(line))
540 return
542 gCarla.utils.pipe_client_write_msg(self.fPipeClient, line2 + "\n")
543 except:
544 pass
546 gCarla.utils.pipe_client_flush_and_unlock(self.fPipeClient)