2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ------------------------------------------------------------------------------------------------------------
8 from qt_compat import qt_config
11 from PyQt5.QtGui import QKeySequence, QMouseEvent
12 from PyQt5.QtWidgets import QFrame, QSplitter
14 from PyQt6.QtGui import QKeySequence, QMouseEvent
15 from PyQt6.QtWidgets import QFrame, QSplitter
17 # ------------------------------------------------------------------------------------------------------------
18 # Imports (Custom Stuff)
20 from carla_backend_qt import CarlaHostQtPlugin
21 from carla_host import *
22 from externalui import ExternalUI
24 # ------------------------------------------------------------------------------------------------------------
27 class PluginHost(CarlaHostQtPlugin):
29 CarlaHostQtPlugin.__init__(self)
32 # kdevelop likes this :)
33 self.fExternalUI = ExternalUI()
35 # ---------------------------------------------------------------
37 self.fExternalUI = None
39 # -------------------------------------------------------------------
41 def setExternalUI(self, extUI):
42 self.fExternalUI = extUI
44 def sendMsg(self, lines):
45 if self.fExternalUI is None:
48 return self.fExternalUI.send(lines)
50 # -------------------------------------------------------------------
52 def engine_init(self, driverName, clientName):
55 def engine_close(self):
58 def engine_idle(self):
59 self.fExternalUI.idleExternalUI()
61 def is_engine_running(self):
62 if self.fExternalUI is None:
65 return self.fExternalUI.isRunning()
67 def set_engine_about_to_close(self):
70 def get_host_osc_url_tcp(self):
71 return self.tr("(OSC TCP port not provided in Plugin version)")
73 # ------------------------------------------------------------------------------------------------------------
76 class CarlaMiniW(ExternalUI, HostWindow):
77 def __init__(self, host, isPatchbay, parent=None):
78 ExternalUI.__init__(self)
79 HostWindow.__init__(self, host, isPatchbay, parent)
82 # kdevelop likes this :)
87 host.setExternalUI(self)
89 self.fFirstInit = True
91 self.setWindowTitle(self.fUiName)
94 # Override this as it can be called from several places.
95 # We really need to close all UIs as events are driven by host idle which is only available when UI is visible
96 def closeExternalUI(self):
97 for i in reversed(range(self.fPluginCount)):
98 self.host.show_custom_ui(i, False)
100 ExternalUI.closeExternalUI(self)
102 # -------------------------------------------------------------------
103 # ExternalUI Callbacks
106 if self.parent() is not None:
111 if self.parent() is not None:
114 self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
118 self.activateWindow()
121 if self.parent() is not None:
126 self.closeExternalUI()
132 # there might be other qt windows open which will block carla-plugin from quitting
135 def uiTitleChanged(self, uiTitle):
136 self.setWindowTitle(uiTitle)
138 # -------------------------------------------------------------------
141 def closeEvent(self, event):
142 self.closeExternalUI()
143 HostWindow.closeEvent(self, event)
145 # there might be other qt windows open which will block carla-plugin from quitting
148 # -------------------------------------------------------------------
151 def msgCallback(self, msg):
153 self.msgCallback2(msg)
154 except Exception as e:
155 print("msgCallback error, skipped for", msg, "error was:\n", e)
157 def msgCallback2(self, msg):
158 msg = charPtrToString(msg)
163 if msg == "runtime-info":
164 values = self.readlineblock().split(":")
165 load = float(values[0])
166 xruns = int(values[1])
167 self.host._set_runtime_info(load, xruns)
169 elif msg == "project-folder":
170 self.fProjectFilename = self.readlineblock()
172 elif msg == "transport":
173 playing = self.readlineblock_bool()
174 frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")]
175 bpm = self.readlineblock_float()
176 self.host._set_transport(playing, frame, bar, beat, tick, bpm)
178 elif msg.startswith("PEAKS_"):
179 pluginId = int(msg.replace("PEAKS_", ""))
180 in1, in2, out1, out2 = [float(i) for i in self.readlineblock().split(":")]
181 self.host._set_peaks(pluginId, in1, in2, out1, out2)
183 elif msg.startswith("PARAMVAL_"):
184 pluginId, paramId = [int(i) for i in msg.replace("PARAMVAL_", "").split(":")]
185 paramValue = self.readlineblock_float()
187 self.host._set_internalValue(pluginId, paramId, paramValue)
189 self.host._set_parameterValue(pluginId, paramId, paramValue)
191 elif msg.startswith("ENGINE_CALLBACK_"):
192 action = int(msg.replace("ENGINE_CALLBACK_", ""))
193 pluginId = self.readlineblock_int()
194 value1 = self.readlineblock_int()
195 value2 = self.readlineblock_int()
196 value3 = self.readlineblock_int()
197 valuef = self.readlineblock_float()
198 valueStr = self.readlineblock()
200 self.host._setViaCallback(action, pluginId, value1, value2, value3, valuef, valueStr)
201 engineCallback(self.host, action, pluginId, value1, value2, value3, valuef, valueStr)
203 elif msg.startswith("ENGINE_OPTION_"):
204 option = int(msg.replace("ENGINE_OPTION_", ""))
205 forced = self.readlineblock_bool()
206 value = self.readlineblock()
208 if self.fFirstInit and not forced:
211 if option == ENGINE_OPTION_PROCESS_MODE:
212 self.host.processMode = int(value)
213 elif option == ENGINE_OPTION_TRANSPORT_MODE:
214 self.host.transportMode = int(value)
215 elif option == ENGINE_OPTION_FORCE_STEREO:
216 self.host.forceStereo = bool(value == "true")
217 elif option == ENGINE_OPTION_PREFER_PLUGIN_BRIDGES:
218 self.host.preferPluginBridges = bool(value == "true")
219 elif option == ENGINE_OPTION_PREFER_UI_BRIDGES:
220 self.host.preferUIBridges = bool(value == "true")
221 elif option == ENGINE_OPTION_UIS_ALWAYS_ON_TOP:
222 self.host.uisAlwaysOnTop = bool(value == "true")
223 elif option == ENGINE_OPTION_MAX_PARAMETERS:
224 self.host.maxParameters = int(value)
225 elif option == ENGINE_OPTION_UI_BRIDGES_TIMEOUT:
226 self.host.uiBridgesTimeout = int(value)
227 elif option == ENGINE_OPTION_PATH_BINARIES:
228 self.host.pathBinaries = value
229 elif option == ENGINE_OPTION_PATH_RESOURCES:
230 self.host.pathResources = value
232 elif msg.startswith("PLUGIN_INFO_"):
233 pluginId = int(msg.replace("PLUGIN_INFO_", ""))
234 self.host._add(pluginId)
236 type_, category, hints, uniqueId, optsAvail, optsEnabled = [int(i) for i in self.readlineblock().split(":")]
237 filename = self.readlineblock()
238 name = self.readlineblock()
239 iconName = self.readlineblock()
240 realName = self.readlineblock()
241 label = self.readlineblock()
242 maker = self.readlineblock()
243 copyright = self.readlineblock()
247 'category': category,
249 'optionsAvailable': optsAvail,
250 'optionsEnabled': optsEnabled,
251 'filename': filename,
255 'copyright': copyright,
256 'iconName': iconName,
257 'patchbayClientId': 0,
260 self.host._set_pluginInfo(pluginId, pinfo)
261 self.host._set_pluginRealName(pluginId, realName)
263 elif msg.startswith("AUDIO_COUNT_"):
264 pluginId, ins, outs = [int(i) for i in msg.replace("AUDIO_COUNT_", "").split(":")]
265 self.host._set_audioCountInfo(pluginId, {'ins': ins, 'outs': outs})
267 elif msg.startswith("MIDI_COUNT_"):
268 pluginId, ins, outs = [int(i) for i in msg.replace("MIDI_COUNT_", "").split(":")]
269 self.host._set_midiCountInfo(pluginId, {'ins': ins, 'outs': outs})
271 elif msg.startswith("PARAMETER_COUNT_"):
272 pluginId, ins, outs, count = [int(i) for i in msg.replace("PARAMETER_COUNT_", "").split(":")]
273 self.host._set_parameterCountInfo(pluginId, count, {'ins': ins, 'outs': outs})
275 elif msg.startswith("PARAMETER_DATA_"):
276 pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_DATA_", "").split(":")]
277 paramType, paramHints, mappedControlIndex, midiChannel = [int(i) for i in self.readlineblock().split(":")]
278 mappedMinimum, mappedMaximum = [float(i) for i in self.readlineblock().split(":")]
279 paramName = self.readlineblock()
280 paramUnit = self.readlineblock()
281 paramComment = self.readlineblock()
282 paramGroupName = self.readlineblock()
288 'comment': paramComment,
289 'groupName': paramGroupName,
290 'scalePointCount': 0,
292 self.host._set_parameterInfo(pluginId, paramId, paramInfo)
299 'midiChannel': midiChannel,
300 'mappedControlIndex': mappedControlIndex,
301 'mappedMinimum': mappedMinimum,
302 'mappedMaximum': mappedMaximum,
304 self.host._set_parameterData(pluginId, paramId, paramData)
306 elif msg.startswith("PARAMETER_RANGES_"):
307 pluginId, paramId = [int(i) for i in msg.replace("PARAMETER_RANGES_", "").split(":")]
308 def_, min_, max_, step, stepSmall, stepLarge = [float(i) for i in self.readlineblock().split(":")]
315 'stepSmall': stepSmall,
316 'stepLarge': stepLarge
318 self.host._set_parameterRanges(pluginId, paramId, paramRanges)
320 elif msg.startswith("PROGRAM_COUNT_"):
321 pluginId, count, current = [int(i) for i in msg.replace("PROGRAM_COUNT_", "").split(":")]
322 self.host._set_programCount(pluginId, count)
323 self.host._set_currentProgram(pluginId, current)
325 elif msg.startswith("PROGRAM_NAME_"):
326 pluginId, progId = [int(i) for i in msg.replace("PROGRAM_NAME_", "").split(":")]
327 progName = self.readlineblock()
328 self.host._set_programName(pluginId, progId, progName)
330 elif msg.startswith("MIDI_PROGRAM_COUNT_"):
331 pluginId, count, current = [int(i) for i in msg.replace("MIDI_PROGRAM_COUNT_", "").split(":")]
332 self.host._set_midiProgramCount(pluginId, count)
333 self.host._set_currentMidiProgram(pluginId, current)
335 elif msg.startswith("MIDI_PROGRAM_DATA_"):
336 pluginId, midiProgId = [int(i) for i in msg.replace("MIDI_PROGRAM_DATA_", "").split(":")]
337 bank, program = [int(i) for i in self.readlineblock().split(":")]
338 name = self.readlineblock()
339 self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
341 elif msg.startswith("CUSTOM_DATA_COUNT_"):
342 pluginId, count = [int(i) for i in msg.replace("CUSTOM_DATA_COUNT_", "").split(":")]
343 self.host._set_customDataCount(pluginId, count)
345 elif msg.startswith("CUSTOM_DATA_"):
346 pluginId, customDataId = [int(i) for i in msg.replace("CUSTOM_DATA_", "").split(":")]
348 type_ = self.readlineblock()
349 key = self.readlineblock()
350 value = self.readlineblock()
351 self.host._set_customData(pluginId, customDataId, {'type': type_, 'key': key, 'value': value})
353 elif msg == "osc-urls":
354 tcp = self.readlineblock()
355 udp = self.readlineblock()
356 self.host.fOscTCP = tcp
357 self.host.fOscUDP = udp
359 elif msg == "max-plugin-number":
360 maxnum = self.readlineblock_int()
361 self.host.fMaxPluginNumber = maxnum
363 elif msg == "buffer-size":
364 bufsize = self.readlineblock_int()
365 self.host.fBufferSize = bufsize
367 elif msg == "sample-rate":
368 srate = self.readlineblock_float()
369 self.host.fSampleRate = srate
372 error = self.readlineblock()
373 engineCallback(self.host, ENGINE_CALLBACK_ERROR, 0, 0, 0, 0, 0.0, error)
376 self.fFirstInit = False
386 self.fQuitReceived = True
389 elif msg == "uiTitle":
390 uiTitle = self.readlineblock()
391 self.uiTitleChanged(uiTitle)
394 print("unknown message: \"" + msg + "\"")
396 # ------------------------------------------------------------------------------------------------------------
399 class QEmbedWidget(QWidget):
400 def __init__(self, winId):
401 QWidget.__init__(self)
402 self.setAttribute(Qt.WA_LayoutUsesWidgetRect)
408 def finalSetup(self, gui, winId):
409 self.fWinId = int(self.winId())
410 gui.ui.centralwidget.installEventFilter(self)
411 gui.ui.menubar.installEventFilter(self)
412 gCarla.utils.x11_reparent_window(self.fWinId, winId)
415 def fixPosition(self):
416 pos = gCarla.utils.x11_get_window_pos(self.fWinId)
420 self.move(pos[0], pos[1])
421 gCarla.utils.x11_move_window(self.fWinId, pos[2], pos[3])
423 def eventFilter(self, obj, ev):
424 if isinstance(ev, QMouseEvent):
428 def enterEvent(self, ev):
430 QWidget.enterEvent(self, ev)
432 # ------------------------------------------------------------------------------------------------------------
435 class CarlaEmbedW(QEmbedWidget):
436 def __init__(self, host, winId, isPatchbay):
437 QEmbedWidget.__init__(self, winId)
440 host = CarlaHostPlugin()
444 self.setFixedSize(1024, 712)
446 self.fLayout = QVBoxLayout(self)
447 self.fLayout.setContentsMargins(0, 0, 0, 0)
448 self.fLayout.setSpacing(0)
449 self.setLayout(self.fLayout)
451 self.gui = CarlaMiniW(host, isPatchbay, self)
454 self.gui.ui.act_file_quit.setEnabled(False)
455 self.gui.ui.act_file_quit.setVisible(False)
457 self.fShortcutActions = []
458 self.addShortcutActions(self.gui.ui.menu_File.actions())
459 self.addShortcutActions(self.gui.ui.menu_Plugin.actions())
460 self.addShortcutActions(self.gui.ui.menu_PluginMacros.actions())
461 self.addShortcutActions(self.gui.ui.menu_Settings.actions())
462 self.addShortcutActions(self.gui.ui.menu_Help.actions())
464 if self.host.processMode == ENGINE_PROCESS_MODE_PATCHBAY:
465 self.addShortcutActions(self.gui.ui.menu_Canvas.actions())
466 self.addShortcutActions(self.gui.ui.menu_Canvas_Zoom.actions())
468 self.addWidget(self.gui.ui.menubar)
470 self.addWidget(self.gui.ui.toolBar)
472 if self.host.processMode == ENGINE_PROCESS_MODE_PATCHBAY:
475 self.fCentralSplitter = QSplitter(self)
476 policy = self.fCentralSplitter.sizePolicy()
477 policy.setVerticalStretch(1)
478 self.fCentralSplitter.setSizePolicy(policy)
480 self.addCentralWidget(self.gui.ui.dockWidget)
481 self.addCentralWidget(self.gui.centralWidget())
482 self.fLayout.addWidget(self.fCentralSplitter)
484 self.finalSetup(self.gui, winId)
486 def addShortcutActions(self, actions):
487 for action in actions:
488 if not action.shortcut().isEmpty():
489 self.fShortcutActions.append(action)
491 def addWidget(self, widget):
492 widget.setParent(self)
493 self.fLayout.addWidget(widget)
495 def addCentralWidget(self, widget):
496 widget.setParent(self)
497 self.fCentralSplitter.addWidget(widget)
501 line.setFrameShadow(QFrame.Sunken)
502 line.setFrameShape(QFrame.HLine)
504 line.setMidLineWidth(1)
505 self.fLayout.addWidget(line)
507 def keyPressEvent(self, event):
508 modifiers = event.modifiers()
511 if modifiers & Qt.ShiftModifier:
512 modifiersStr += "Shift+"
513 if modifiers & Qt.ControlModifier:
514 modifiersStr += "Ctrl+"
515 if modifiers & Qt.AltModifier:
516 modifiersStr += "Alt+"
517 if modifiers & Qt.MetaModifier:
518 modifiersStr += "Meta+"
520 keyStr = QKeySequence(event.key()).toString()
521 keySeq = QKeySequence(modifiersStr + keyStr)
523 for action in self.fShortcutActions:
524 if not action.isEnabled():
526 if keySeq.matches(action.shortcut()) != QKeySequence.ExactMatch:
532 QEmbedWidget.keyPressEvent(self, event)
534 def showEvent(self, event):
535 QEmbedWidget.showEvent(self, event)
537 if QT_VERSION >= 0x50600:
538 self.host.set_engine_option(ENGINE_OPTION_FRONTEND_UI_SCALE, int(self.devicePixelRatioF() * 1000), "")
539 print("Plugin UI pixel ratio is", self.devicePixelRatioF(),
540 "with %ix%i" % (self.width(), self.height()), "in size")
542 # set our gui as parent for all plugins UIs
543 if self.host.manageUIs:
545 nsViewPtr = int(self.fWinId)
546 winIdStr = "%x" % gCarla.utils.cocoa_get_window(nsViewPtr)
548 winIdStr = "%x" % int(self.fWinId)
549 self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, winIdStr)
551 def hideEvent(self, event):
553 self.host.set_engine_option(ENGINE_OPTION_FRONTEND_WIN_ID, 0, "0")
555 QEmbedWidget.hideEvent(self, event)
557 def closeEvent(self, event):
559 self.gui.closeExternalUI()
560 QEmbedWidget.closeEvent(self, event)
562 # there might be other qt windows open which will block carla-plugin from quitting
565 def setLoadRDFsNeeded(self):
566 self.gui.setLoadRDFsNeeded()
568 # ------------------------------------------------------------------------------------------------------------
571 if __name__ == '__main__':
574 # -------------------------------------------------------------
575 # Get details regarding target usage
578 winId = int(os.getenv("CARLA_PLUGIN_EMBED_WINID"))
582 usingEmbed = bool(LINUX and winId != 0)
584 # -------------------------------------------------------------
585 # Init host backend (part 1)
587 isPatchbay = sys.argv[0].rsplit(os.path.sep)[-1].lower().replace(".exe","") == "carla-plugin-patchbay"
589 host = initHost("Carla-Plugin", None, False, True, True, PluginHost)
590 host.processMode = ENGINE_PROCESS_MODE_PATCHBAY if isPatchbay else ENGINE_PROCESS_MODE_CONTINUOUS_RACK
591 host.processModeForced = True
592 host.nextProcessMode = host.processMode
594 # -------------------------------------------------------------
597 gCarla.utils.setenv("CARLA_PLUGIN_EMBED_WINID", "0")
600 gCarla.utils.setenv("QT_QPA_PLATFORM", "xcb")
602 # -------------------------------------------------------------
605 app = CarlaApplication("Carla2-Plugin")
607 # -------------------------------------------------------------
608 # Set-up custom signal handling
612 # -------------------------------------------------------------
613 # Init host backend (part 2)
615 loadHostSettings(host)
617 # -------------------------------------------------------------
621 gui = CarlaEmbedW(host, winId, isPatchbay)
623 gui = CarlaMiniW(host, isPatchbay)
625 # -------------------------------------------------------------