#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho # SPDX-License-Identifier: GPL-2.0-or-later # ------------------------------------------------------------------------------------------------------------ # Imports (Global) from qt_compat import qt_config if qt_config == 5: from PyQt5.QtCore import pyqtSignal, pyqtSlot, QT_VERSION, Qt, QPointF, QRectF, QSize, QTimer from PyQt5.QtGui import QColor, QPainter, QPen from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow, QApplication elif qt_config == 6: from PyQt6.QtCore import pyqtSignal, pyqtSlot, QT_VERSION, Qt, QPointF, QRectF, QSize, QTimer from PyQt6.QtGui import QColor, QPainter, QPen from PyQt6.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow, QApplication import ctypes from time import sleep # ----------------------------------------------------------------------- # Imports (Custom) from carla_shared import * from carla_utils import * from widgets.scalabledial import ScalableDial import ui_xycontroller # ----------------------------------------------------------------------- # Imports (ExternalUI) from carla_app import CarlaApplication from externalui import ExternalUI from widgets.paramspinbox import ParamSpinBox # ------------------------------------------------------------------------------------------------------------ XYCONTROLLER_PARAMETER_SMOOTH = 0 XYCONTROLLER_PARAMETER_LINEAR = 1 XYCONTROLLER_PARAMETER_SPEED = 2 XYCONTROLLER_PARAMETER_REVERSEY = 3 XYCONTROLLER_PARAMETER_X = 4 XYCONTROLLER_PARAMETER_Y = 5 XYCONTROLLER_PARAMETER_OUT_X = 6 XYCONTROLLER_PARAMETER_OUT_Y = 7 # ------------------------------------------------------------------------------------------------------------ class XYGraphicsScene(QGraphicsScene): # signals cursorMoved = pyqtSignal(float,float) knobsUpdate = pyqtSignal(float,float) def __init__(self, parent): QGraphicsScene.__init__(self, parent) self.cc_x = 1 self.cc_y = 2 self.rparent = parent self.m_channels = [] self.m_mouseLock = False self.m_smooth = False self.m_linear = False self.m_speed = 8.0 # 1.0 to 100.0 self.m_rSmooth = False # Using right button self.m_smooth_x = 0.0 self.m_smooth_y = 0.0 self.reverseY = 1.0 # -1.0 when reversed self.xpPrev = 0.0 self.ypPrev = 0.0 self.time: ctypes.c_uint64 = 0 # I sure just int is quite enough here, but... self.prevTime: ctypes.c_uint64 = 0 self.setBackgroundBrush(Qt.black) cursorPen = QPen(QColor(255, 255, 255), 2) cursorBrush = QColor(255, 255, 255, 50) self.m_cursor = self.addEllipse(QRectF(-10, -10, 20, 20), cursorPen, cursorBrush) linePen = QPen(QColor(200, 200, 200, 100), 1, Qt.DashLine) self.m_lineH = self.addLine(-9999, 0, 9999, 0, linePen) self.m_lineV = self.addLine(0, -9999, 0, 9999, linePen) self.p_size = QRectF(-100, -100, 100, 100) # ------------------------------------------------------------------- def setControlX(self, x: int): self.cc_x = x def setControlY(self, y: int): self.cc_y = y def setChannels(self, channels): self.m_channels = channels def setPosX(self, x: float, forward: bool = True): if self.m_mouseLock: return posX = x * (self.p_size.x() + self.p_size.width()) self.m_cursor.setPos(posX, self.m_cursor.y()) self.m_lineV.setX(posX) if forward: value = posX / (self.p_size.x() + self.p_size.width()) self.sendMIDI(value, None) else: self.m_smooth_x = posX def setPosY(self, y: float, forward: bool = True): if self.m_mouseLock: return posY = y * (self.p_size.y() + self.p_size.height()) * self.reverseY self.m_cursor.setPos(self.m_cursor.x(), posY) self.m_lineH.setY(posY) if forward: value = posY / (self.p_size.y() + self.p_size.height()) * self.reverseY self.sendMIDI(None, value) else: self.m_smooth_y = posY def setSmoothValues(self, x: float, y: float): self.m_smooth_x = x * (self.p_size.x() + self.p_size.width()) self.m_smooth_y = y * (self.p_size.y() + self.p_size.height()) * self.reverseY def setReverseY(self, rev: bool): self.reverseY = 1 - (int(rev) * 2) # 1.0 or -1.0 # ------------------------------------------------------------------- def updateSize(self, size: QSize): self.p_size.setRect(-(float(size.width())/2), -(float(size.height())/2), size.width(), size.height()) def updatePos(self, pos: QPointF, filterSame: bool = False, knobsOnly: bool = False): xp = pos.x() / (self.p_size.x() + self.p_size.width()) yp = pos.y() / (self.p_size.y() + self.p_size.height()) * self.reverseY if knobsOnly: self.knobsUpdate.emit(xp * 100, yp * 100) return self.m_cursor.setPos(pos) self.m_lineH.setY(pos.y()) self.m_lineV.setX(pos.x()) # Set 0.05% precision, yet exact final value settling, esp. zero xp = round(xp * 1000) / 1000 yp = round(yp * 1000) / 1000 self.sendMIDI(xp, yp, filterSame) self.cursorMoved.emit(xp, yp) def updateSmooth(self, time): if not (self.m_smooth or self.m_rSmooth): return dx = self.m_smooth_x - self.m_cursor.x() dy = self.m_smooth_y - self.m_cursor.y() if dx == dy == 0: return same = 0 if abs(dx) <= 0.0005: self.m_smooth_x = self.m_cursor.x() same += 1 if abs(dy) <= 0.0005: self.m_smooth_y = self.m_cursor.y() same += 1 if same == 2: return speed = self.m_speed mod = QApplication.keyboardModifiers() if (mod & Qt.ControlModifier): speed /= 2 elif (mod & Qt.ShiftModifier): speed *= 2 if self.m_linear: newX = self.m_cursor.x() + max(min(dx / speed, 1), -1) * speed newY = self.m_cursor.y() + max(min(dy / speed, 1), -1) * speed else: precision = 64 / speed newX = float(self.m_smooth_x + self.m_cursor.x()*(precision-1)) / precision newY = float(self.m_smooth_y + self.m_cursor.y()*(precision-1)) / precision pos = QPointF(newX, newY) self.updatePos(pos, ((time - self.prevTime) == 1)) # Continuous calls or Not self.prevTime = time # ------------------------------------------------------------------- def handleMousePos(self, event): if (event.buttons() & Qt.MiddleButton): pos = QPointF(0, 0) else: pos = QPointF(event.scenePos()) if not self.p_size.contains(pos): if pos.x() < self.p_size.x(): pos.setX(self.p_size.x()) elif pos.x() > (self.p_size.x() + self.p_size.width()): pos.setX(self.p_size.x() + self.p_size.width()) if pos.y() < self.p_size.y(): pos.setY(self.p_size.y()) elif pos.y() > (self.p_size.y() + self.p_size.height()): pos.setY(self.p_size.y() + self.p_size.height()) self.updatePos(pos, knobsOnly=True) self.m_smooth_x = pos.x() self.m_smooth_y = pos.y() self.m_rSmooth = event.buttons() & Qt.RightButton # When not smooth, update each time; # (commented-out part) When smooth, update (re-send same) if click position is same (when smoothing not sends anything). (To match non-smoothed behaviour) if (not (self.m_smooth or self.m_rSmooth)): # or (abs(self.m_cursor.x() - self.m_smooth_x) <= 0.0005 and abs(self.m_cursor.y() - self.m_smooth_y) <= 0.0005): self.updatePos(pos) def sendMIDI(self, xp, yp, filterSame = False): rate = float(0xff) / 4 prefix = [] msgd = [] if xp is not None: value = int(xp * rate + rate) if not (filterSame and (value == self.xpPrev)): prefix = ["cc"] msgd.append(self.cc_x) msgd.append(value) self.xpPrev = value if yp is not None: value = int(yp * rate + rate) if not (filterSame and (value == self.ypPrev)): if prefix == []: prefix = ["cc"] else: prefix = ["cc2"] msgd.append(self.cc_y) msgd.append(value) self.ypPrev = value if not (prefix == []): self.rparent.send(prefix + msgd) # ------------------------------------------------------------------- def keyPressEvent(self, event): # QKeyEvent event.accept() def wheelEvent(self, event): # QGraphicsSceneWheelEvent event.accept() def mousePressEvent(self, event: QGraphicsSceneMouseEvent): self.m_mouseLock = True self.handleMousePos(event) self.rparent.setCursor(Qt.CrossCursor) QGraphicsScene.mousePressEvent(self, event) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): self.handleMousePos(event) QGraphicsScene.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): self.m_mouseLock = False self.rparent.setCursor(Qt.ArrowCursor) QGraphicsScene.mouseReleaseEvent(self, event) # ----------------------------------------------------------------------- # External UI class XYControllerUI(ExternalUI, QMainWindow): def __init__(self): ExternalUI.__init__(self) QMainWindow.__init__(self) self.ui = ui_xycontroller.Ui_XYControllerW() self.ui.setupUi(self) self.fSaveSizeNowChecker = -1 self.isXActual = False self.isYActual = False # --------------------------------------------------------------- # Set-up GUI stuff self.scene = XYGraphicsScene(self) # Now knobs are QWidgets, not QDials. self.ui.dial_x = ScalableDial(self.ui.dial_x, 0, 400, 0, -100, 100, "X", 64, -1, "%", "tube", 1, {}) self.ui.dial_y = ScalableDial(self.ui.dial_y, 1, 400, 0, -100, 100, "Y", 64, -1, "%", "tube", 1, {}) # These are outputs (7-seg displays). self.ui.dial_out_x = ScalableDial(self.ui.dial_out_x, 2, 400, 0, -100, 100, "Out X", 64, -1, "%", "tube", 1, {'Auto7segWidth':1, }, isOutput=True) self.ui.dial_out_y = ScalableDial(self.ui.dial_out_y, 3, 400, 0, -100, 100, "Out Y", 64, -1, "%", "tube", 1, {'Auto7segWidth':1, }, isOutput=True) self.ui.keyboard.setOctaves(10) self.ui.graphicsView.setScene(self.scene) self.ui.graphicsView.setRenderHints(QPainter.Antialiasing) for MIDI_CC in MIDI_CC_LIST: self.ui.cb_control_x.addItem(MIDI_CC) self.ui.cb_control_y.addItem(MIDI_CC) # --------------------------------------------------------------- # Initial state self.m_channels = [1] self.ui.act_ch_01.setChecked(True) self.ui.act_show_keyboard.setChecked(True) self.ui.cb_control_y.setCurrentIndex(1) # --------------------------------------------------------------- # Connect actions to functions self.scene.cursorMoved.connect(self.slot_sceneCursorMoved) self.scene.knobsUpdate.connect(self.slot_setKnobs) self.ui.keyboard.noteOn.connect(self.slot_noteOn) self.ui.keyboard.noteOff.connect(self.slot_noteOff) self.ui.cb_smooth.clicked.connect(self.slot_setSmooth) self.ui.dial_x.realValueChanged.connect(self.slot_knobValueChangedX) self.ui.dial_y.realValueChanged.connect(self.slot_knobValueChangedY) if QT_VERSION >= 0x60000: self.ui.cb_control_x.currentTextChanged.connect(self.slot_checkCC_X) self.ui.cb_control_y.currentTextChanged.connect(self.slot_checkCC_Y) else: self.ui.cb_control_x.currentIndexChanged[str].connect(self.slot_checkCC_X) self.ui.cb_control_y.currentIndexChanged[str].connect(self.slot_checkCC_Y) self.ui.act_ch_01.triggered.connect(self.slot_checkChannel) self.ui.act_ch_02.triggered.connect(self.slot_checkChannel) self.ui.act_ch_03.triggered.connect(self.slot_checkChannel) self.ui.act_ch_04.triggered.connect(self.slot_checkChannel) self.ui.act_ch_05.triggered.connect(self.slot_checkChannel) self.ui.act_ch_06.triggered.connect(self.slot_checkChannel) self.ui.act_ch_07.triggered.connect(self.slot_checkChannel) self.ui.act_ch_08.triggered.connect(self.slot_checkChannel) self.ui.act_ch_09.triggered.connect(self.slot_checkChannel) self.ui.act_ch_10.triggered.connect(self.slot_checkChannel) self.ui.act_ch_11.triggered.connect(self.slot_checkChannel) self.ui.act_ch_12.triggered.connect(self.slot_checkChannel) self.ui.act_ch_13.triggered.connect(self.slot_checkChannel) self.ui.act_ch_14.triggered.connect(self.slot_checkChannel) self.ui.act_ch_15.triggered.connect(self.slot_checkChannel) self.ui.act_ch_16.triggered.connect(self.slot_checkChannel) self.ui.act_ch_all.triggered.connect(self.slot_checkChannel_all) self.ui.act_ch_none.triggered.connect(self.slot_checkChannel_none) self.ui.act_show_keyboard.triggered.connect(self.slot_showKeyboard) # --------------------------------------------------------------- # Final stuff self.fIdleTimer = self.startTimer(60) self.setWindowTitle(self.fUiName) self.ready() # ------------------------------------------------------------------- def setSmooth(self, smooth: bool): self.scene.m_smooth = smooth x = self.ui.dial_x.rvalue() / 100 y = self.ui.dial_y.rvalue() / 100 if smooth: self.scene.setSmoothValues(x, y) else: self.scene.setPosX(x, True) self.scene.setPosY(y, True) self.slot_sceneCursorMoved(x, y) @pyqtSlot() def slot_updateScreen(self): self.ui.graphicsView.centerOn(0, 0) self.scene.updateSize(self.ui.graphicsView.size()) @pyqtSlot(int) def slot_noteOn(self, note): self.send(["note", True, note]) @pyqtSlot(int) def slot_noteOff(self, note): self.send(["note", False, note]) @pyqtSlot(float) def slot_knobValueChangedX(self, x:float, external:bool=False, firstRun:bool=False): if not external: self.sendControl(XYCONTROLLER_PARAMETER_X, x) else: self.ui.dial_x.setValue(x, False) if (not self.scene.m_smooth) or firstRun: self.sendControl(XYCONTROLLER_PARAMETER_OUT_X, x) self.ui.dial_out_x.setValue(x, False) self.scene.setPosX(x / 100, True) else: self.scene.setSmoothValues(x / 100, self.ui.dial_y.rvalue() / 100) @pyqtSlot(float) def slot_knobValueChangedY(self, y:float, external:bool=False, firstRun:bool=False): if not external: self.sendControl(XYCONTROLLER_PARAMETER_Y, y) else: self.ui.dial_y.setValue(y, False) if (not self.scene.m_smooth) or firstRun: self.sendControl(XYCONTROLLER_PARAMETER_OUT_Y, y) self.ui.dial_out_y.setValue(y, False) self.scene.setPosY(y / 100, True) else: self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, y / 100) @pyqtSlot(str) def slot_checkCC_X(self, text: str): if not text: return cc_x = int(text.split(" ",1)[0]) self.scene.setControlX(cc_x) self.sendConfigure("cc_x", str(cc_x)) @pyqtSlot(str) def slot_checkCC_Y(self, text: str): if not text: return cc_y = int(text.split(" ",1)[0]) self.scene.setControlY(cc_y) self.sendConfigure("cc_y", str(cc_y)) @pyqtSlot(bool) def slot_checkChannel(self, clicked): if not self.sender(): return channel = int(self.sender().text()) if clicked and channel not in self.m_channels: self.m_channels.append(channel) elif not clicked and channel in self.m_channels: self.m_channels.remove(channel) self.scene.setChannels(self.m_channels) self.sendConfigure("channels", ",".join(str(c) for c in self.m_channels)) @pyqtSlot() def slot_checkChannel_all(self): self.ui.act_ch_01.setChecked(True) self.ui.act_ch_02.setChecked(True) self.ui.act_ch_03.setChecked(True) self.ui.act_ch_04.setChecked(True) self.ui.act_ch_05.setChecked(True) self.ui.act_ch_06.setChecked(True) self.ui.act_ch_07.setChecked(True) self.ui.act_ch_08.setChecked(True) self.ui.act_ch_09.setChecked(True) self.ui.act_ch_10.setChecked(True) self.ui.act_ch_11.setChecked(True) self.ui.act_ch_12.setChecked(True) self.ui.act_ch_13.setChecked(True) self.ui.act_ch_14.setChecked(True) self.ui.act_ch_15.setChecked(True) self.ui.act_ch_16.setChecked(True) self.m_channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] self.scene.setChannels(self.m_channels) self.sendConfigure("channels", ",".join(str(c) for c in self.m_channels)) @pyqtSlot() def slot_checkChannel_none(self): self.ui.act_ch_01.setChecked(False) self.ui.act_ch_02.setChecked(False) self.ui.act_ch_03.setChecked(False) self.ui.act_ch_04.setChecked(False) self.ui.act_ch_05.setChecked(False) self.ui.act_ch_06.setChecked(False) self.ui.act_ch_07.setChecked(False) self.ui.act_ch_08.setChecked(False) self.ui.act_ch_09.setChecked(False) self.ui.act_ch_10.setChecked(False) self.ui.act_ch_11.setChecked(False) self.ui.act_ch_12.setChecked(False) self.ui.act_ch_13.setChecked(False) self.ui.act_ch_14.setChecked(False) self.ui.act_ch_15.setChecked(False) self.ui.act_ch_16.setChecked(False) self.m_channels = [] self.scene.setChannels(self.m_channels) self.sendConfigure("channels", "") @pyqtSlot(bool) def slot_setSmooth(self, smooth): self.setSmooth(smooth) self.sendConfigure("smooth", "yes" if smooth else "no") self.sendControl(XYCONTROLLER_PARAMETER_SMOOTH, int(smooth)) @pyqtSlot(float, float) def slot_sceneCursorMoved(self, xp: float, yp: float): self.ui.dial_out_x.setValue(xp * 100, False) self.ui.dial_out_y.setValue(yp * 100, False) self.sendControl(XYCONTROLLER_PARAMETER_OUT_X, xp * 100) self.sendControl(XYCONTROLLER_PARAMETER_OUT_Y, yp * 100) @pyqtSlot(bool) def slot_showKeyboard(self, yesno): self.ui.scrollArea.setVisible(yesno) self.sendConfigure("show-midi-keyboard", "yes" if yesno else "no") QTimer.singleShot(0, self.slot_updateScreen) @pyqtSlot(float, float) def slot_setKnobs(self, x, y): self.ui.dial_x.setValue(x, False) self.ui.dial_y.setValue(y, False) self.sendControl(XYCONTROLLER_PARAMETER_X, x) self.sendControl(XYCONTROLLER_PARAMETER_Y, y) # ------------------------------------------------------------------- # DSP Callbacks # NOTE It called continuously with params 6, 7 (outs, not used here), is this good? def dspParameterChanged(self, index: int, value: float): if index == XYCONTROLLER_PARAMETER_SMOOTH: self.ui.cb_smooth.blockSignals(True) self.ui.cb_smooth.setChecked(bool(value)) self.ui.cb_smooth.blockSignals(False) self.setSmooth(bool(value)) elif index == XYCONTROLLER_PARAMETER_LINEAR: self.scene.m_linear = bool(value) elif index == XYCONTROLLER_PARAMETER_SPEED: self.scene.m_speed = value elif index == XYCONTROLLER_PARAMETER_REVERSEY: self.scene.setReverseY(bool(value)) elif index == XYCONTROLLER_PARAMETER_X: self.slot_knobValueChangedX(value, True, not self.isXActual) self.isXActual = True elif index == XYCONTROLLER_PARAMETER_Y: self.slot_knobValueChangedY(value, True, not self.isYActual) if self.isXActual and not self.isYActual: # Run it once, BUT when both X & Y are received (they can be shuffled). self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, self.ui.dial_y.rvalue() / 100) self.isYActual = True else: return def dspStateChanged(self, key: str, value: str): if key == "guiWidth": try: width = int(value) except: width = 0 if width > 0: self.resize(width, self.height()) elif key == "guiHeight": try: height = int(value) except: height = 0 if height > 0: self.resize(self.width(), height) elif key == "smooth": smooth = (value == "yes") self.setSmooth(bool(smooth)) elif key == "show-midi-keyboard": show = (value == "yes") self.ui.act_show_keyboard.blockSignals(True) self.ui.act_show_keyboard.setChecked(show) self.ui.act_show_keyboard.blockSignals(False) self.ui.scrollArea.setVisible(show) elif key == "channels": if value: self.m_channels = [int(c) for c in value.split(",")] else: self.m_channels = [] self.scene.setChannels(self.m_channels) self.ui.act_ch_01.setChecked(bool(1 in self.m_channels)) self.ui.act_ch_02.setChecked(bool(2 in self.m_channels)) self.ui.act_ch_03.setChecked(bool(3 in self.m_channels)) self.ui.act_ch_04.setChecked(bool(4 in self.m_channels)) self.ui.act_ch_05.setChecked(bool(5 in self.m_channels)) self.ui.act_ch_06.setChecked(bool(6 in self.m_channels)) self.ui.act_ch_07.setChecked(bool(7 in self.m_channels)) self.ui.act_ch_08.setChecked(bool(8 in self.m_channels)) self.ui.act_ch_09.setChecked(bool(9 in self.m_channels)) self.ui.act_ch_10.setChecked(bool(10 in self.m_channels)) self.ui.act_ch_11.setChecked(bool(11 in self.m_channels)) self.ui.act_ch_12.setChecked(bool(12 in self.m_channels)) self.ui.act_ch_13.setChecked(bool(13 in self.m_channels)) self.ui.act_ch_14.setChecked(bool(14 in self.m_channels)) self.ui.act_ch_15.setChecked(bool(15 in self.m_channels)) self.ui.act_ch_16.setChecked(bool(16 in self.m_channels)) elif key == "cc_x": cc_x = int(value) self.scene.setControlX(cc_x) for cc_index in range(len(MIDI_CC_LIST)): if cc_x == int(MIDI_CC_LIST[cc_index].split(" ",1)[0]): self.ui.cb_control_x.blockSignals(True) self.ui.cb_control_x.setCurrentIndex(cc_index) self.ui.cb_control_x.blockSignals(False) break elif key == "cc_y": cc_y = int(value) self.scene.setControlY(cc_y) for cc_index in range(len(MIDI_CC_LIST)): if cc_y == int(MIDI_CC_LIST[cc_index].split(" ",1)[0]): self.ui.cb_control_y.blockSignals(True) self.ui.cb_control_y.setCurrentIndex(cc_index) self.ui.cb_control_y.blockSignals(False) break def dspNoteReceived(self, onOff, channel, note, velocity): if channel+1 not in self.m_channels: return if onOff: self.ui.keyboard.sendNoteOn(note, False) else: self.ui.keyboard.sendNoteOff(note, False) # ------------------------------------------------------------------- # ExternalUI Callbacks def uiShow(self): self.show() def uiFocus(self): 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() app.quit() def uiTitleChanged(self, uiTitle): self.setWindowTitle(uiTitle) # ------------------------------------------------------------------- # Qt events def showEvent(self, event): self.slot_updateScreen() QMainWindow.showEvent(self, event) def resizeEvent(self, event): self.fSaveSizeNowChecker = 0 self.slot_updateScreen() QMainWindow.resizeEvent(self, event) def timerEvent(self, event): self.scene.time += 1 if event.timerId() == self.fIdleTimer: self.idleExternalUI() self.scene.updateSmooth(self.scene.time) if self.fSaveSizeNowChecker == 11: self.sendConfigure("guiWidth", str(self.width())) self.sendConfigure("guiHeight", str(self.height())) self.fSaveSizeNowChecker = -1 elif self.fSaveSizeNowChecker >= 0: self.fSaveSizeNowChecker += 1 QMainWindow.timerEvent(self, event) def closeEvent(self, event): self.closeExternalUI() QMainWindow.closeEvent(self, event) # there might be other qt windows open which will block the UI from quitting app.quit() #--------------- main ------------------ if __name__ == '__main__': import resources_rc pathBinaries, _ = getPaths() gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION)) gCarla.utils.set_process_name("XYController") app = CarlaApplication("XYController") gui = XYControllerUI() app.exit_exec()