Cleanup
[carla.git] / source / frontend / xycontroller-ui
blob06848b45767beb85b6468cd76e2807f3dcef88a8
1 #!/usr/bin/env python3
2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ------------------------------------------------------------------------------------------------------------
6 # Imports (Global)
8 from qt_compat import qt_config
10 if qt_config == 5:
11     from PyQt5.QtCore import pyqtSignal, pyqtSlot, QT_VERSION, Qt, QPointF, QRectF, QSize, QTimer
12     from PyQt5.QtGui import QColor, QPainter, QPen
13     from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow
14 elif qt_config == 6:
15     from PyQt6.QtCore import pyqtSignal, pyqtSlot, QT_VERSION, Qt, QPointF, QRectF, QSize, QTimer
16     from PyQt6.QtGui import QColor, QPainter, QPen
17     from PyQt6.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow
19 # -----------------------------------------------------------------------
20 # Imports (Custom)
22 from carla_shared import *
23 from carla_utils import *
25 import ui_xycontroller
27 # -----------------------------------------------------------------------
28 # Imports (ExternalUI)
30 from carla_app import CarlaApplication
31 from externalui import ExternalUI
32 from widgets.paramspinbox import ParamSpinBox
34 # ------------------------------------------------------------------------------------------------------------
36 XYCONTROLLER_PARAMETER_X = 0
37 XYCONTROLLER_PARAMETER_Y = 1
39 # ------------------------------------------------------------------------------------------------------------
41 class XYGraphicsScene(QGraphicsScene):
42     # signals
43     cursorMoved = pyqtSignal(float,float)
45     def __init__(self, parent):
46         QGraphicsScene.__init__(self, parent)
48         self.cc_x = 1
49         self.cc_y = 2
50         self.rparent = parent
52         self.m_channels  = []
53         self.m_mouseLock = False
54         self.m_smooth    = False
55         self.m_smooth_x  = 0.0
56         self.m_smooth_y  = 0.0
58         self.setBackgroundBrush(Qt.black)
60         cursorPen = QPen(QColor(255, 255, 255), 2)
61         cursorBrush = QColor(255, 255, 255, 50)
62         self.m_cursor = self.addEllipse(QRectF(-10, -10, 20, 20), cursorPen, cursorBrush)
64         linePen = QPen(QColor(200, 200, 200, 100), 1, Qt.DashLine)
65         self.m_lineH = self.addLine(-9999, 0, 9999, 0, linePen)
66         self.m_lineV = self.addLine(0, -9999, 0, 9999, linePen)
68         self.p_size = QRectF(-100, -100, 100, 100)
70     # -------------------------------------------------------------------
72     def setControlX(self, x: int):
73         self.cc_x = x
75     def setControlY(self, y: int):
76         self.cc_y = y
78     def setChannels(self, channels):
79         self.m_channels = channels
81     def setPosX(self, x: float, forward: bool = True):
82         if self.m_mouseLock:
83             return
85         posX = x * (self.p_size.x() + self.p_size.width())
86         self.m_cursor.setPos(posX, self.m_cursor.y())
87         self.m_lineV.setX(posX)
89         if forward:
90             value = posX / (self.p_size.x() + self.p_size.width());
91             self.sendMIDI(value, None)
92         else:
93             self.m_smooth_x = posX;
95     def setPosY(self, y: float, forward: bool = True):
96         if self.m_mouseLock:
97             return;
99         posY = y * (self.p_size.y() + self.p_size.height())
100         self.m_cursor.setPos(self.m_cursor.x(), posY)
101         self.m_lineH.setY(posY)
103         if forward:
104             value = posY / (self.p_size.y() + self.p_size.height())
105             self.sendMIDI(None, value)
106         else:
107             self.m_smooth_y = posY
109     def setSmooth(self, smooth: bool):
110         self.m_smooth = smooth
112     def setSmoothValues(self, x: float, y: float):
113         self.m_smooth_x = x * (self.p_size.x() + self.p_size.width());
114         self.m_smooth_y = y * (self.p_size.y() + self.p_size.height());
116     # -------------------------------------------------------------------
118     def updateSize(self, size: QSize):
119         self.p_size.setRect(-(float(size.width())/2),
120                             -(float(size.height())/2),
121                             size.width(),
122                             size.height());
124     def updateSmooth(self):
125         if not self.m_smooth:
126             return
128         if self.m_cursor.x() == self.m_smooth_x and self.m_cursor.y() == self.m_smooth_y:
129             return
131         same = 0
132         if abs(self.m_cursor.x() - self.m_smooth_x) <= 0.0005:
133             self.m_smooth_x = self.m_cursor.x()
134             same += 1
136         if abs(self.m_cursor.y() - self.m_smooth_y) <= 0.0005:
137             self.m_smooth_y = self.m_cursor.y()
138             same += 1
140         if same == 2:
141             return
143         newX = float(self.m_smooth_x + self.m_cursor.x()*7) / 8
144         newY = float(self.m_smooth_y + self.m_cursor.y()*7) / 8
145         pos  = QPointF(newX, newY)
147         self.m_cursor.setPos(pos)
148         self.m_lineH.setY(pos.y())
149         self.m_lineV.setX(pos.x())
151         xp = pos.x() / (self.p_size.x() + self.p_size.width())
152         yp = pos.y() / (self.p_size.y() + self.p_size.height())
154         self.sendMIDI(xp, yp)
155         self.cursorMoved.emit(xp, yp)
157     # -------------------------------------------------------------------
159     def handleMousePos(self, pos: QPointF):
160         if not self.p_size.contains(pos):
161             if pos.x() < self.p_size.x():
162                 pos.setX(self.p_size.x())
163             elif pos.x() > (self.p_size.x() + self.p_size.width()):
164                 pos.setX(self.p_size.x() + self.p_size.width());
166             if pos.y() < self.p_size.y():
167                 pos.setY(self.p_size.y())
168             elif pos.y() > (self.p_size.y() + self.p_size.height()):
169                 pos.setY(self.p_size.y() + self.p_size.height())
171         self.m_smooth_x = pos.x()
172         self.m_smooth_y = pos.y()
174         if not self.m_smooth:
175             self.m_cursor.setPos(pos)
176             self.m_lineH.setY(pos.y())
177             self.m_lineV.setX(pos.x())
179             xp = pos.x() / (self.p_size.x() + self.p_size.width());
180             yp = pos.y() / (self.p_size.y() + self.p_size.height());
182             self.sendMIDI(xp, yp)
183             self.cursorMoved.emit(xp, yp)
185     def sendMIDI(self, xp, yp):
186         rate = float(0xff) / 4
187         msgd = ["cc2" if xp is not None and yp is not None else "cc"]
189         if xp is not None:
190             msgd.append(self.cc_x)
191             msgd.append(int(xp * rate + rate))
193         if yp is not None:
194             msgd.append(self.cc_y)
195             msgd.append(int(yp * rate + rate))
197         self.rparent.send(msgd)
199     # -------------------------------------------------------------------
201     def keyPressEvent(self, event): # QKeyEvent
202         event.accept()
204     def wheelEvent(self, event): # QGraphicsSceneWheelEvent
205         event.accept()
207     def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
208         self.m_mouseLock = True
209         self.handleMousePos(event.scenePos())
210         self.rparent.setCursor(Qt.CrossCursor)
211         QGraphicsScene.mousePressEvent(self, event);
213     def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):
214         self.handleMousePos(event.scenePos())
215         QGraphicsScene.mouseMoveEvent(self, event);
217     def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent):
218         self.m_mouseLock = False
219         self.rparent.setCursor(Qt.ArrowCursor)
220         QGraphicsScene.mouseReleaseEvent(self, event)
222 # -----------------------------------------------------------------------
223 # External UI
225 class XYControllerUI(ExternalUI, QMainWindow):
226     def __init__(self):
227         ExternalUI.__init__(self)
228         QMainWindow.__init__(self)
229         self.ui = ui_xycontroller.Ui_XYControllerW()
230         self.ui.setupUi(self)
232         self.fSaveSizeNowChecker = -1
234         # ---------------------------------------------------------------
235         # Set-up GUI stuff
237         self.scene = XYGraphicsScene(self)
239         self.ui.dial_x.setImage(2)
240         self.ui.dial_y.setImage(2)
241         self.ui.dial_x.setLabel("X")
242         self.ui.dial_y.setLabel("Y")
243         self.ui.keyboard.setOctaves(10)
245         self.ui.graphicsView.setScene(self.scene)
246         self.ui.graphicsView.setRenderHints(QPainter.Antialiasing)
248         for MIDI_CC in MIDI_CC_LIST:
249             self.ui.cb_control_x.addItem(MIDI_CC)
250             self.ui.cb_control_y.addItem(MIDI_CC)
252         # ---------------------------------------------------------------
253         # Initial state
255         self.m_channels = [1]
257         self.ui.act_ch_01.setChecked(True)
258         self.ui.act_show_keyboard.setChecked(True)
259         self.ui.cb_control_y.setCurrentIndex(1)
261         # ---------------------------------------------------------------
262         # Connect actions to functions
264         self.scene.cursorMoved.connect(self.slot_sceneCursorMoved)
266         self.ui.keyboard.noteOn.connect(self.slot_noteOn)
267         self.ui.keyboard.noteOff.connect(self.slot_noteOff)
269         self.ui.cb_smooth.clicked.connect(self.slot_setSmooth)
271         self.ui.dial_x.realValueChanged.connect(self.slot_knobValueChangedX)
272         self.ui.dial_y.realValueChanged.connect(self.slot_knobValueChangedY)
274         if QT_VERSION >= 0x60000:
275             self.ui.cb_control_x.currentTextChanged.connect(self.slot_checkCC_X)
276             self.ui.cb_control_y.currentTextChanged.connect(self.slot_checkCC_Y)
277         else:
278             self.ui.cb_control_x.currentIndexChanged[str].connect(self.slot_checkCC_X)
279             self.ui.cb_control_y.currentIndexChanged[str].connect(self.slot_checkCC_Y)
281         self.ui.act_ch_01.triggered.connect(self.slot_checkChannel)
282         self.ui.act_ch_02.triggered.connect(self.slot_checkChannel)
283         self.ui.act_ch_03.triggered.connect(self.slot_checkChannel)
284         self.ui.act_ch_04.triggered.connect(self.slot_checkChannel)
285         self.ui.act_ch_05.triggered.connect(self.slot_checkChannel)
286         self.ui.act_ch_06.triggered.connect(self.slot_checkChannel)
287         self.ui.act_ch_07.triggered.connect(self.slot_checkChannel)
288         self.ui.act_ch_08.triggered.connect(self.slot_checkChannel)
289         self.ui.act_ch_09.triggered.connect(self.slot_checkChannel)
290         self.ui.act_ch_10.triggered.connect(self.slot_checkChannel)
291         self.ui.act_ch_11.triggered.connect(self.slot_checkChannel)
292         self.ui.act_ch_12.triggered.connect(self.slot_checkChannel)
293         self.ui.act_ch_13.triggered.connect(self.slot_checkChannel)
294         self.ui.act_ch_14.triggered.connect(self.slot_checkChannel)
295         self.ui.act_ch_15.triggered.connect(self.slot_checkChannel)
296         self.ui.act_ch_16.triggered.connect(self.slot_checkChannel)
297         self.ui.act_ch_all.triggered.connect(self.slot_checkChannel_all)
298         self.ui.act_ch_none.triggered.connect(self.slot_checkChannel_none)
300         self.ui.act_show_keyboard.triggered.connect(self.slot_showKeyboard)
302         # ---------------------------------------------------------------
303         # Final stuff
305         self.fIdleTimer = self.startTimer(60)
306         self.setWindowTitle(self.fUiName)
307         self.ready()
309     # -------------------------------------------------------------------
311     @pyqtSlot()
312     def slot_updateScreen(self):
313         self.ui.graphicsView.centerOn(0, 0)
314         self.scene.updateSize(self.ui.graphicsView.size())
316         dial_x = self.ui.dial_x.rvalue()
317         dial_y = self.ui.dial_y.rvalue()
318         self.scene.setPosX(dial_x / 100, False)
319         self.scene.setPosY(dial_y / 100, False)
320         self.scene.setSmoothValues(dial_x / 100, dial_y / 100)
322     @pyqtSlot(int)
323     def slot_noteOn(self, note):
324         self.send(["note", True, note])
326     @pyqtSlot(int)
327     def slot_noteOff(self, note):
328         self.send(["note", False, note])
330     @pyqtSlot(float)
331     def slot_knobValueChangedX(self, x: float):
332         self.sendControl(XYCONTROLLER_PARAMETER_X, x)
333         self.scene.setPosX(x / 100, True)
334         self.scene.setSmoothValues(x / 100, self.ui.dial_y.rvalue() / 100)
336     @pyqtSlot(float)
337     def slot_knobValueChangedY(self, y: float):
338         self.sendControl(XYCONTROLLER_PARAMETER_Y, y)
339         self.scene.setPosY(y / 100, True)
340         self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, y / 100)
342     @pyqtSlot(str)
343     def slot_checkCC_X(self, text: str):
344         if not text:
345             return
347         cc_x = int(text.split(" ",1)[0])
349         self.scene.setControlX(cc_x)
350         self.sendConfigure("cc_x", str(cc_x))
352     @pyqtSlot(str)
353     def slot_checkCC_Y(self, text: str):
354         if not text:
355             return
357         cc_y = int(text.split(" ",1)[0])
359         self.scene.setControlY(cc_y)
360         self.sendConfigure("cc_y", str(cc_y))
362     @pyqtSlot(bool)
363     def slot_checkChannel(self, clicked):
364         if not self.sender():
365             return
367         channel = int(self.sender().text())
369         if clicked and channel not in self.m_channels:
370             self.m_channels.append(channel)
371         elif not clicked and channel in self.m_channels:
372             self.m_channels.remove(channel)
374         self.scene.setChannels(self.m_channels)
375         self.sendConfigure("channels", ",".join(str(c) for c in self.m_channels))
377     @pyqtSlot()
378     def slot_checkChannel_all(self):
379         self.ui.act_ch_01.setChecked(True)
380         self.ui.act_ch_02.setChecked(True)
381         self.ui.act_ch_03.setChecked(True)
382         self.ui.act_ch_04.setChecked(True)
383         self.ui.act_ch_05.setChecked(True)
384         self.ui.act_ch_06.setChecked(True)
385         self.ui.act_ch_07.setChecked(True)
386         self.ui.act_ch_08.setChecked(True)
387         self.ui.act_ch_09.setChecked(True)
388         self.ui.act_ch_10.setChecked(True)
389         self.ui.act_ch_11.setChecked(True)
390         self.ui.act_ch_12.setChecked(True)
391         self.ui.act_ch_13.setChecked(True)
392         self.ui.act_ch_14.setChecked(True)
393         self.ui.act_ch_15.setChecked(True)
394         self.ui.act_ch_16.setChecked(True)
396         self.m_channels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
397         self.scene.setChannels(self.m_channels)
398         self.sendConfigure("channels", ",".join(str(c) for c in self.m_channels))
400     @pyqtSlot()
401     def slot_checkChannel_none(self):
402         self.ui.act_ch_01.setChecked(False)
403         self.ui.act_ch_02.setChecked(False)
404         self.ui.act_ch_03.setChecked(False)
405         self.ui.act_ch_04.setChecked(False)
406         self.ui.act_ch_05.setChecked(False)
407         self.ui.act_ch_06.setChecked(False)
408         self.ui.act_ch_07.setChecked(False)
409         self.ui.act_ch_08.setChecked(False)
410         self.ui.act_ch_09.setChecked(False)
411         self.ui.act_ch_10.setChecked(False)
412         self.ui.act_ch_11.setChecked(False)
413         self.ui.act_ch_12.setChecked(False)
414         self.ui.act_ch_13.setChecked(False)
415         self.ui.act_ch_14.setChecked(False)
416         self.ui.act_ch_15.setChecked(False)
417         self.ui.act_ch_16.setChecked(False)
419         self.m_channels = []
420         self.scene.setChannels(self.m_channels)
421         self.sendConfigure("channels", "")
423     @pyqtSlot(bool)
424     def slot_setSmooth(self, smooth):
425         self.scene.setSmooth(smooth)
426         self.sendConfigure("smooth", "yes" if smooth else "no")
428         if smooth:
429             dial_x = self.ui.dial_x.rvalue()
430             dial_y = self.ui.dial_y.rvalue()
431             self.scene.setSmoothValues(dial_x / 100, dial_y / 100)
433     @pyqtSlot(float, float)
434     def slot_sceneCursorMoved(self, xp: float, yp: float):
435         self.ui.dial_x.setValue(xp * 100, False)
436         self.ui.dial_y.setValue(yp * 100, False)
437         self.sendControl(XYCONTROLLER_PARAMETER_X, xp * 100)
438         self.sendControl(XYCONTROLLER_PARAMETER_Y, yp * 100)
440     @pyqtSlot(bool)
441     def slot_showKeyboard(self, yesno):
442         self.ui.scrollArea.setVisible(yesno)
443         self.sendConfigure("show-midi-keyboard", "yes" if yesno else "no")
444         QTimer.singleShot(0, self.slot_updateScreen)
446     # -------------------------------------------------------------------
447     # DSP Callbacks
449     def dspParameterChanged(self, index: int, value: float):
450         if index == XYCONTROLLER_PARAMETER_X:
451             self.ui.dial_x.setValue(value, False)
452             self.scene.setPosX(value / 100, False)
453         elif index == XYCONTROLLER_PARAMETER_Y:
454             self.ui.dial_y.setValue(value, False)
455             self.scene.setPosY(value / 100, False)
456         else:
457             return
459         self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100,
460                                    self.ui.dial_y.rvalue() / 100)
462     def dspStateChanged(self, key: str, value: str):
463         if key == "guiWidth":
464             try:
465                 width = int(value)
466             except:
467                 width = 0
469             if width > 0:
470                 self.resize(width, self.height())
472         elif key == "guiHeight":
473             try:
474                 height = int(value)
475             except:
476                 height = 0
478             if height > 0:
479                 self.resize(self.width(), height)
481         elif key == "smooth":
482             smooth = (value == "yes")
483             self.ui.cb_smooth.blockSignals(True)
484             self.ui.cb_smooth.setChecked(smooth)
485             self.ui.cb_smooth.blockSignals(False)
486             self.scene.setSmooth(smooth)
488             if smooth:
489                 dial_x = self.ui.dial_x.rvalue()
490                 dial_y = self.ui.dial_y.rvalue()
491                 self.scene.setSmoothValues(dial_x / 100, dial_y / 100)
493         elif key == "show-midi-keyboard":
494             show = (value == "yes")
495             self.ui.act_show_keyboard.blockSignals(True)
496             self.ui.act_show_keyboard.setChecked(show)
497             self.ui.act_show_keyboard.blockSignals(False)
498             self.ui.scrollArea.setVisible(show)
500         elif key == "channels":
501             if value:
502                 self.m_channels = [int(c) for c in value.split(",")]
503             else:
504                 self.m_channels = []
505             self.scene.setChannels(self.m_channels)
506             self.ui.act_ch_01.setChecked(bool(1 in self.m_channels))
507             self.ui.act_ch_02.setChecked(bool(2 in self.m_channels))
508             self.ui.act_ch_03.setChecked(bool(3 in self.m_channels))
509             self.ui.act_ch_04.setChecked(bool(4 in self.m_channels))
510             self.ui.act_ch_05.setChecked(bool(5 in self.m_channels))
511             self.ui.act_ch_06.setChecked(bool(6 in self.m_channels))
512             self.ui.act_ch_07.setChecked(bool(7 in self.m_channels))
513             self.ui.act_ch_08.setChecked(bool(8 in self.m_channels))
514             self.ui.act_ch_09.setChecked(bool(9 in self.m_channels))
515             self.ui.act_ch_10.setChecked(bool(10 in self.m_channels))
516             self.ui.act_ch_11.setChecked(bool(11 in self.m_channels))
517             self.ui.act_ch_12.setChecked(bool(12 in self.m_channels))
518             self.ui.act_ch_13.setChecked(bool(13 in self.m_channels))
519             self.ui.act_ch_14.setChecked(bool(14 in self.m_channels))
520             self.ui.act_ch_15.setChecked(bool(15 in self.m_channels))
521             self.ui.act_ch_16.setChecked(bool(16 in self.m_channels))
523         elif key == "cc_x":
524             cc_x = int(value)
525             self.scene.setControlX(cc_x)
527             for cc_index in range(len(MIDI_CC_LIST)):
528                 if cc_x == int(MIDI_CC_LIST[cc_index].split(" ",1)[0]):
529                     self.ui.cb_control_x.blockSignals(True)
530                     self.ui.cb_control_x.setCurrentIndex(cc_index)
531                     self.ui.cb_control_x.blockSignals(False)
532                     break
534         elif key == "cc_y":
535             cc_y = int(value)
536             self.scene.setControlY(cc_y)
538             for cc_index in range(len(MIDI_CC_LIST)):
539                 if cc_y == int(MIDI_CC_LIST[cc_index].split(" ",1)[0]):
540                     self.ui.cb_control_y.blockSignals(True)
541                     self.ui.cb_control_y.setCurrentIndex(cc_index)
542                     self.ui.cb_control_y.blockSignals(False)
543                     break
545     def dspNoteReceived(self, onOff, channel, note, velocity):
546         if channel+1 not in self.m_channels:
547             return
548         if onOff:
549             self.ui.keyboard.sendNoteOn(note, False)
550         else:
551             self.ui.keyboard.sendNoteOff(note, False)
553     # -------------------------------------------------------------------
554     # ExternalUI Callbacks
556     def uiShow(self):
557         self.show()
559     def uiFocus(self):
560         self.setWindowState((self.windowState() & ~Qt.WindowMinimized) | Qt.WindowActive)
561         self.show()
563         self.raise_()
564         self.activateWindow()
566     def uiHide(self):
567         self.hide()
569     def uiQuit(self):
570         self.closeExternalUI()
571         self.close()
572         app.quit()
574     def uiTitleChanged(self, uiTitle):
575         self.setWindowTitle(uiTitle)
577     # -------------------------------------------------------------------
578     # Qt events
580     def showEvent(self, event):
581         self.slot_updateScreen()
582         QMainWindow.showEvent(self, event)
584     def resizeEvent(self, event):
585         self.fSaveSizeNowChecker = 0
586         self.slot_updateScreen()
587         QMainWindow.resizeEvent(self, event)
589     def timerEvent(self, event):
590         if event.timerId() == self.fIdleTimer:
591             self.idleExternalUI()
592             self.scene.updateSmooth()
594             if self.fSaveSizeNowChecker == 11:
595                 self.sendConfigure("guiWidth", str(self.width()))
596                 self.sendConfigure("guiHeight", str(self.height()))
597                 self.fSaveSizeNowChecker = -1
598             elif self.fSaveSizeNowChecker >= 0:
599                 self.fSaveSizeNowChecker += 1
601         QMainWindow.timerEvent(self, event)
603     def closeEvent(self, event):
604         self.closeExternalUI()
605         QMainWindow.closeEvent(self, event)
607         # there might be other qt windows open which will block the UI from quitting
608         app.quit()
610 #--------------- main ------------------
611 if __name__ == '__main__':
612     import resources_rc
614     pathBinaries, _ = getPaths()
615     gCarla.utils = CarlaUtils(os.path.join(pathBinaries, "libcarla_utils." + DLL_EXTENSION))
616     gCarla.utils.set_process_name("XYController")
618     app = CarlaApplication("XYController")
619     gui = XYControllerUI()
620     app.exit_exec()