#!/usr/bin/env python3 # -*- coding: utf-8 -*- # XY Controller UI, taken from Cadence # Copyright (C) 2011-2020 Filipe Coelho # # 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, QPointF, QRectF, QSize, QTimer from PyQt5.QtGui import QColor, QPainter, QPen from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow # ----------------------------------------------------------------------- # Imports (Custom) from carla_shared import * from carla_utils import * import ui_xycontroller # ----------------------------------------------------------------------- # Imports (ExternalUI) from carla_app import CarlaApplication from externalui import ExternalUI from widgets.paramspinbox import ParamSpinBox # ------------------------------------------------------------------------------------------------------------ XYCONTROLLER_PARAMETER_X = 0 XYCONTROLLER_PARAMETER_Y = 1 # ------------------------------------------------------------------------------------------------------------ class XYGraphicsScene(QGraphicsScene): # signals cursorMoved = pyqtSignal(float,float) def __init__(self, parent): QGraphicsScene.__init__(self, parent) self.cc_x = 1 self.cc_y = 2 self.m_channels = [] self.m_mouseLock = False self.m_smooth = False self.m_smooth_x = 0.0 self.m_smooth_y = 0.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.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.sendMIDI(None, value) else: self.m_smooth_y = posY def setSmooth(self, smooth: bool): self.m_smooth = smooth 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()); # ------------------------------------------------------------------- def updateSize(self, size: QSize): self.p_size.setRect(-(float(size.width())/2), -(float(size.height())/2), size.width(), size.height()); def updateSmooth(self): if not self.m_smooth: return if self.m_cursor.x() == self.m_smooth_x and self.m_cursor.y() == self.m_smooth_y: return same = 0 if abs(self.m_cursor.x() - self.m_smooth_x) <= 0.0005: self.m_smooth_x = self.m_cursor.x() same += 1 if abs(self.m_cursor.y() - self.m_smooth_y) <= 0.0005: self.m_smooth_y = self.m_cursor.y() same += 1 if same == 2: return newX = float(self.m_smooth_x + self.m_cursor.x()*7) / 8 newY = float(self.m_smooth_y + self.m_cursor.y()*7) / 8 pos = QPointF(newX, newY) self.m_cursor.setPos(pos) self.m_lineH.setY(pos.y()) self.m_lineV.setX(pos.x()) xp = pos.x() / (self.p_size.x() + self.p_size.width()) yp = pos.y() / (self.p_size.y() + self.p_size.height()) self.sendMIDI(xp, yp) self.cursorMoved.emit(xp, yp) # ------------------------------------------------------------------- def handleMousePos(self, pos: QPointF): 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.m_smooth_x = pos.x() self.m_smooth_y = pos.y() if not self.m_smooth: self.m_cursor.setPos(pos) self.m_lineH.setY(pos.y()) self.m_lineV.setX(pos.x()) xp = pos.x() / (self.p_size.x() + self.p_size.width()); yp = pos.y() / (self.p_size.y() + self.p_size.height()); self.sendMIDI(xp, yp) self.cursorMoved.emit(xp, yp) def sendMIDI(self, xp, yp): rate = float(0xff) / 4; if xp is not None: value = xp * rate + rate #foreach (const int& channel, m_channels) #qMidiOutData.put(0xB0 + channel - 1, cc_x, value); if yp is not None: value = yp * rate + rate #foreach (const int& channel, m_channels) #qMidiOutData.put(0xB0 + channel - 1, cc_y, value); # ------------------------------------------------------------------- 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.scenePos()) self.parent().setCursor(Qt.CrossCursor) QGraphicsScene.mousePressEvent(self, event); def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): self.handleMousePos(event.scenePos()) QGraphicsScene.mouseMoveEvent(self, event); def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): self.m_mouseLock = False self.parent().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 # --------------------------------------------------------------- # Internal stuff self.cc_x = 1 self.cc_y = 2 self.m_channels = [] # --------------------------------------------------------------- # Set-up GUI stuff self.scene = XYGraphicsScene(self) self.ui.dial_x.setImage(2) self.ui.dial_y.setImage(2) self.ui.dial_x.setLabel("X") self.ui.dial_y.setLabel("Y") 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) self.ui.graphicsView.centerOn(0, 0) # --------------------------------------------------------------- # Connect actions to functions self.scene.cursorMoved.connect(self.slot_sceneCursorMoved) 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) 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(50) self.setWindowTitle(self.fUiName) self.ready() QTimer.singleShot(0, self.slot_updateScreen) # ------------------------------------------------------------------- @pyqtSlot() def slot_updateScreen(self): self.scene.updateSize(self.ui.graphicsView.size()) dial_x = self.ui.dial_x.rvalue() dial_y = self.ui.dial_y.rvalue() self.scene.setPosX(dial_x / 100, False) self.scene.setPosY(dial_y / 100, False) self.scene.setSmoothValues(dial_x / 100, dial_y / 100) @pyqtSlot(int) def slot_noteOn(self, note): pass #foreach (const int& channel, m_channels) #qMidiOutData.put(0x90 + channel - 1, note, 100); @pyqtSlot(int) def slot_noteOff(self, note): pass #foreach (const int& channel, m_channels) #qMidiOutData.put(0x80 + channel - 1, note, 0); @pyqtSlot(float) def slot_knobValueChangedX(self, x: float): self.sendControl(XYCONTROLLER_PARAMETER_X, x) self.scene.setPosX(x / 100, True) self.scene.setSmoothValues(x / 100, self.ui.dial_y.rvalue() / 100) @pyqtSlot(float) def slot_knobValueChangedY(self, y: float): self.sendControl(XYCONTROLLER_PARAMETER_Y, y) self.scene.setPosY(y / 100, True) self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, y / 100) @pyqtSlot(str) def slot_checkCC_X(self, text): if not text: return tmp_cc_x = int(text.split(" ",1)[0], 16) self.cc_x = tmp_cc_x; self.scene.setControlX(self.cc_x); @pyqtSlot(str) def slot_checkCC_Y(self, text): if not text: return tmp_cc_y = int(text.split(" ",1)[0], 16) self.cc_y = tmp_cc_y; self.scene.setControlY(self.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); @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) @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) @pyqtSlot(bool) def slot_setSmooth(self, smooth): self.sendConfigure("smooth", "yes" if smooth else "no") self.scene.setSmooth(smooth) if smooth: dial_x = self.ui.dial_x.rvalue() dial_y = self.ui.dial_y.rvalue() self.scene.setSmoothValues(dial_x / 100, dial_y / 100) @pyqtSlot(float, float) def slot_sceneCursorMoved(self, xp, yp): self.sendControl(XYCONTROLLER_PARAMETER_X, xp * 100) self.sendControl(XYCONTROLLER_PARAMETER_Y, yp * 100) self.ui.dial_x.setValue(xp * 100, False) self.ui.dial_y.setValue(yp * 100, False) @pyqtSlot(bool) def slot_showKeyboard(self, yesno): self.ui.scrollArea.setVisible(yesno) QTimer.singleShot(0, self.slot_updateScreen) # ------------------------------------------------------------------- # DSP Callbacks def dspParameterChanged(self, index, value): if index == XYCONTROLLER_PARAMETER_X: self.ui.dial_x.setValue(value, False) self.scene.setPosX(value / 100, False) elif index == XYCONTROLLER_PARAMETER_Y: self.ui.dial_y.setValue(value, False) self.scene.setPosY(value / 100, False) else: return self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, self.ui.dial_y.rvalue() / 100) def dspStateChanged(self, key, value): print("dspStateChanged", key, value) 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.ui.cb_smooth.blockSignals(True) self.ui.cb_smooth.setChecked(smooth) self.ui.cb_smooth.blockSignals(False) self.scene.setSmooth(smooth) if smooth: dial_x = self.ui.dial_x.rvalue() dial_y = self.ui.dial_y.rvalue() self.scene.setSmoothValues(dial_x / 100, dial_y / 100) # ------------------------------------------------------------------- # 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): if event.timerId() == self.fIdleTimer: self.idleExternalUI() self.scene.updateSmooth() 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()