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 # ------------------------------------------------------------------------------------------------------------
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
30 # ------------------------------------------------------------------------------------------------------------
33 from carla_host
import charPtrToString
, gCarla
34 from .webserver
import WebServerThread
, PORT
36 # ------------------------------------------------------------------------------------------------------------
39 from modtools
.utils
import get_plugin_info
, init
as lv2_init
41 # ------------------------------------------------------------------------------------------------------------
44 class HostWindow(QMainWindow
):
46 SIGTERM
= pyqtSignal()
47 SIGUSR1
= pyqtSignal()
49 # --------------------------------------------------------------------------------------------------------
52 QMainWindow
.__init
__(self
)
57 # ----------------------------------------------------------------------------------------------------
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
70 self
.fPlugin
= get_plugin_info(URI
)
71 self
.fPorts
= self
.fPlugin
['ports']
72 self
.fPortSymbols
= {}
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:
88 if parameter
['type'] == "http://lv2plug.in/ns/ext/atom#Bool":
90 elif parameter
['type'] == "http://lv2plug.in/ns/ext/atom#Int":
92 elif parameter
['type'] == "http://lv2plug.in/ns/ext/atom#Long":
94 elif parameter
['type'] == "http://lv2plug.in/ns/ext/atom#Float":
96 elif parameter
['type'] == "http://lv2plug.in/ns/ext/atom#Double":
98 elif parameter
['type'] == "http://lv2plug.in/ns/ext/atom#String":
100 elif parameter
['type'] == "http://lv2plug.in/ns/ext/atom#Path":
102 elif parameter
['type'] == "http://lv2plug.in/ns/ext/atom#URI":
106 if paramtype
not in ('s','p','u') and parameter
['ranges']['minimum'] == parameter
['ranges']['maximum']:
108 self
.fParamTypes
[parameter
['uri']] = paramtype
109 self
.fParamValues
[parameter
['uri']] = parameter
['ranges']['default']
111 # ----------------------------------------------------------------------------------------------------
114 if len(sys
.argv
) == 7:
115 self
.fPipeClient
= gCarla
.utils
.pipe_client_new(lambda s
,msg
: self
.msgCallback(msg
))
117 self
.fPipeClient
= None
119 # ----------------------------------------------------------------------------------------------------
122 self
.fWebServerThread
= WebServerThread(self
)
123 self
.fWebServerThread
.start()
125 # ----------------------------------------------------------------------------------------------------
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
)
154 self
.fWebview
.load(QUrl(url
))
156 # ----------------------------------------------------------------------------------------------------
157 # Connect actions to functions
159 self
.SIGTERM
.connect(self
.slot_handleSIGTERM
)
161 # ----------------------------------------------------------------------------------------------------
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:
179 if not self
.fQuitReceived
:
180 self
.send(["exiting"])
182 gCarla
.utils
.pipe_client_destroy(self
.fPipeClient
)
183 self
.fPipeClient
= None
186 if self
.fPipeClient
is not None:
187 gCarla
.utils
.pipe_client_idle(self
.fPipeClient
)
188 self
.checkForRepaintChanges()
192 if self
.fDocElemement
is None or self
.fDocElemement
.isNull():
195 pedal
= self
.fDocElemement
.findFirst(".mod-pedal")
200 size
= pedal
.geometry().size()
202 if size
.width() <= 10 or size
.height() <= 10:
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
)
213 #image.save("/tmp/test.png")
215 # get coordinates and size from image
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:
230 #lastx = max(lastx, w)
232 #if hasNonTransPixels:
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))
243 # TODO that^ needs work
245 self
.setFixedSize(size
)
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
]
254 self
.fCurrentFrame
.evaluateJavaScript("icongui.setOutputPortValue('%s', %f)" % (symbol
, value
))
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')" % (
266 self
.fCanSetValues
= True
267 self
.fSizeSetup
= True
268 self
.fDocElemement
= None
273 def checkForRepaintChanges(self
):
274 if not self
.fWasRepainted
:
277 self
.fWasRepainted
= False
279 if not self
.fCanSetValues
:
282 for index
in self
.fPortValues
.keys():
283 symbol
, isOutput
= self
.fPortSymbols
[index
]
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 # --------------------------------------------------------------------------------------------------------
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 # --------------------------------------------------------------------------------------------------------
320 def msgCallback(self
, msg
):
321 msg
= charPtrToString(msg
)
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
)
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
)
355 index
= self
.readlineblock_int()
356 atomsize
= self
.readlineblock_int()
357 base64size
= self
.readlineblock_int()
358 base64atom
= self
.readlineblock()
362 urid
= self
.readlineblock_int()
363 size
= self
.readlineblock_int()
364 uri
= self
.readlineblock()
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
)
388 self
.fQuitReceived
= True
391 elif msg
== "uiTitle":
392 uiTitle
= self
.readlineblock()
393 self
.uiTitleChanged(uiTitle
)
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
:
406 symbol
, isOutput
= self
.fPortSymbols
[index
]
409 self
.fCurrentFrame
.evaluateJavaScript("icongui.setOutputPortValue('%s', %f)" % (symbol
, value
))
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
:
418 self
.fParamValues
[uri
] = value
420 if self
.fCurrentFrame
is None or not self
.fCanSetValues
:
423 ptype
= self
.fParamTypes
[uri
]
425 self
.fCurrentFrame
.evaluateJavaScript("icongui.setWritableParameterValue('%s', '%c', %f, 'from-carla')" % (
428 def dspProgramChanged(self
, index
):
431 def dspMidiProgramChanged(self
, bank
, program
):
434 def dspStateChanged(self
, key
, value
):
437 def dspNoteReceived(self
, onOff
, channel
, note
, velocity
):
440 # --------------------------------------------------------------------------------------------------------
446 self
.fNeedsShow
= True
449 if not self
.fSizeSetup
:
452 self
.setWindowState((self
.windowState() & ~Qt
.WindowMinimized
) | Qt
.WindowActive
)
456 self
.activateWindow()
462 self
.closeExternalUI()
464 QApplication
.instance().quit()
466 def uiTitleChanged(self
, uiTitle
):
467 self
.setWindowTitle(uiTitle
)
469 # --------------------------------------------------------------------------------------------------------
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
:
483 QMainWindow
.timerEvent(self
, event
)
485 # --------------------------------------------------------------------------------------------------------
488 def slot_handleSIGTERM(self
):
489 print("Got SIGTERM -> Closing now")
492 # --------------------------------------------------------------------------------------------------------
495 def readlineblock(self
):
496 if self
.fPipeClient
is None:
499 return gCarla
.utils
.pipe_client_readlineblock(self
.fPipeClient
, 5000)
501 def readlineblock_bool(self
):
502 if self
.fPipeClient
is None:
505 return gCarla
.utils
.pipe_client_readlineblock_bool(self
.fPipeClient
, 5000)
507 def readlineblock_int(self
):
508 if self
.fPipeClient
is None:
511 return gCarla
.utils
.pipe_client_readlineblock_int(self
.fPipeClient
, 5000)
513 def readlineblock_float(self
):
514 if self
.fPipeClient
is None:
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:
523 gCarla
.utils
.pipe_client_lock(self
.fPipeClient
)
525 # this must never fail, we need to unlock at the end
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):
536 elif isinstance(line
, float):
537 line2
= "%.10f" % line
539 print("unknown data type to send:", type(line
))
542 gCarla
.utils
.pipe_client_write_msg(self
.fPipeClient
, line2
+ "\n")
546 gCarla
.utils
.pipe_client_flush_and_unlock(self
.fPipeClient
)