Use wine paths by default even if non-existent
[carla.git] / source / frontend / carla_widgets.py
blob136268467e2c03bd727a3a54afffbdd7ffced48d
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 abc import abstractmethod
10 # ------------------------------------------------------------------------------------------------------------
11 # Imports (PyQt)
13 from qt_compat import qt_config
15 if qt_config == 5:
16 from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QByteArray
17 from PyQt5.QtGui import QCursor, QIcon, QPalette, QPixmap
18 from PyQt5.QtWidgets import (
19 QDialog,
20 QFileDialog,
21 QInputDialog,
22 QMenu,
23 QMessageBox,
24 QScrollArea,
25 QVBoxLayout,
26 QWidget,
28 elif qt_config == 6:
29 from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QByteArray
30 from PyQt6.QtGui import QCursor, QIcon, QPalette, QPixmap
31 from PyQt6.QtWidgets import (
32 QDialog,
33 QFileDialog,
34 QInputDialog,
35 QMenu,
36 QMessageBox,
37 QScrollArea,
38 QVBoxLayout,
39 QWidget,
42 # ------------------------------------------------------------------------------------------------------------
43 # Imports (Custom)
45 import ui_carla_about
46 import ui_carla_edit
47 import ui_carla_parameter
49 from carla_backend import (
50 MACOS, WINDOWS,
51 BINARY_NATIVE,
52 PLUGIN_INTERNAL,
53 PLUGIN_DSSI,
54 PLUGIN_LV2,
55 PLUGIN_VST2,
56 PLUGIN_SF2,
57 PLUGIN_SFZ,
58 PLUGIN_CAN_DRYWET,
59 PLUGIN_CAN_VOLUME,
60 PLUGIN_CAN_BALANCE,
61 PLUGIN_CAN_PANNING,
62 PLUGIN_CATEGORY_SYNTH,
63 PLUGIN_OPTION_FIXED_BUFFERS,
64 PLUGIN_OPTION_FORCE_STEREO,
65 PLUGIN_OPTION_MAP_PROGRAM_CHANGES,
66 PLUGIN_OPTION_USE_CHUNKS,
67 PLUGIN_OPTION_SEND_CONTROL_CHANGES,
68 PLUGIN_OPTION_SEND_CHANNEL_PRESSURE,
69 PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH,
70 PLUGIN_OPTION_SEND_PITCHBEND,
71 PLUGIN_OPTION_SEND_ALL_SOUND_OFF,
72 PLUGIN_OPTION_SEND_PROGRAM_CHANGES,
73 PLUGIN_OPTION_SKIP_SENDING_NOTES,
74 PARAMETER_DRYWET,
75 PARAMETER_VOLUME,
76 PARAMETER_BALANCE_LEFT,
77 PARAMETER_BALANCE_RIGHT,
78 PARAMETER_PANNING,
79 PARAMETER_CTRL_CHANNEL,
80 PARAMETER_IS_ENABLED,
81 PARAMETER_IS_AUTOMATABLE,
82 PARAMETER_IS_READ_ONLY,
83 PARAMETER_USES_SCALEPOINTS,
84 PARAMETER_USES_CUSTOM_TEXT,
85 PARAMETER_CAN_BE_CV_CONTROLLED,
86 PARAMETER_INPUT, PARAMETER_OUTPUT,
87 CONTROL_INDEX_NONE,
88 CONTROL_INDEX_MIDI_PITCHBEND,
89 CONTROL_INDEX_MIDI_LEARN,
90 CONTROL_INDEX_CV
93 from carla_shared import (
94 MIDI_CC_LIST, MAX_MIDI_CC_LIST_ITEM,
95 VERSION,
96 countDecimalPoints,
97 fontMetricsHorizontalAdvance,
98 setUpSignals,
99 gCarla
102 from carla_utils import getPluginTypeAsString
105 from widgets.collapsablewidget import CollapsibleBox
106 from widgets.pixmapkeyboard import PixmapKeyboardHArea
108 # ------------------------------------------------------------------------------------------------------------
109 # Carla GUI defines
111 ICON_STATE_ON = 3 # turns on, sets as wait
112 ICON_STATE_WAIT = 2 # nothing, sets as off
113 ICON_STATE_OFF = 1 # turns off, sets as null
114 ICON_STATE_NULL = 0 # nothing
116 # ------------------------------------------------------------------------------------------------------------
117 # Carla About dialog
119 class CarlaAboutW(QDialog):
120 def __init__(self, parent, host):
121 QDialog.__init__(self, parent)
122 self.ui = ui_carla_about.Ui_CarlaAboutW()
123 self.ui.setupUi(self)
125 if host.isControl:
126 extraInfo = " - <b>%s</b>" % self.tr("OSC Bridge Version")
127 elif host.isPlugin:
128 extraInfo = " - <b>%s</b>" % self.tr("Plugin Version")
129 else:
130 extraInfo = ""
132 self.ui.l_about.setText(self.tr(""
133 "<br>Version %s"
134 "<br>Carla is a fully-featured audio plugin host%s.<br>"
135 "<br>Copyright (C) 2011-2022 falkTX<br>"
136 "" % (VERSION, extraInfo)))
138 if self.ui.about.palette().color(QPalette.Background).blackF() < 0.5:
139 self.ui.l_icons.setPixmap(QPixmap(":/bitmaps/carla_about_black.png"))
140 self.ui.ico_example_edit.setPixmap(QPixmap(":/bitmaps/button_file-black.png"))
141 self.ui.ico_example_file.setPixmap(QPixmap(":/scalable/button_edit-black.svg"))
142 self.ui.ico_example_gui.setPixmap(QPixmap(":/bitmaps/button_gui-black.png"))
144 if host.isControl:
145 self.ui.l_extended.hide()
146 self.ui.tabWidget.removeTab(3)
147 self.ui.tabWidget.removeTab(2)
149 self.ui.l_extended.setText(gCarla.utils.get_complete_license_text())
151 if host.is_engine_running() and not host.isControl:
152 self.ui.le_osc_url_tcp.setText(host.get_host_osc_url_tcp())
153 self.ui.le_osc_url_udp.setText(host.get_host_osc_url_udp())
154 else:
155 self.ui.le_osc_url_tcp.setText(self.tr("(Engine not running)"))
156 self.ui.le_osc_url_udp.setText(self.tr("(Engine not running)"))
158 # pylint: disable=line-too-long
159 self.ui.l_osc_cmds.setText("<table>"
160 "<tr><td>" "/set_active" "&nbsp;</td><td>&lt;i-value&gt;</td></tr>"
161 "<tr><td>" "/set_drywet" "&nbsp;</td><td>&lt;f-value&gt;</td></tr>"
162 "<tr><td>" "/set_volume" "&nbsp;</td><td>&lt;f-value&gt;</td></tr>"
163 "<tr><td>" "/set_balance_left" "&nbsp;</td><td>&lt;f-value&gt;</td></tr>"
164 "<tr><td>" "/set_balance_right" "&nbsp;</td><td>&lt;f-value&gt;</td></tr>"
165 "<tr><td>" "/set_panning" "&nbsp;</td><td>&lt;f-value&gt;</td></tr>"
166 "<tr><td>" "/set_parameter_value" "&nbsp;</td><td>&lt;i-index&gt; &lt;f-value&gt;</td></tr>"
167 "<tr><td>" "/set_parameter_midi_cc" "&nbsp;</td><td>&lt;i-index&gt; &lt;i-cc&gt;</td></tr>"
168 "<tr><td>" "/set_parameter_midi_channel" "&nbsp;</td><td>&lt;i-index&gt; &lt;i-channel&gt;</td></tr>"
169 "<tr><td>" "/set_program" "&nbsp;</td><td>&lt;i-index&gt;</td></tr>"
170 "<tr><td>" "/set_midi_program" "&nbsp;</td><td>&lt;i-index&gt;</td></tr>"
171 "<tr><td>" "/note_on" "&nbsp;</td><td>&lt;i-channel&gt; &lt;i-note&gt; &lt;i-velo&gt;</td></tr>"
172 "<tr><td>" "/note_off" "&nbsp;</td><td>&lt;i-channel&gt; &lt;i-note</td></tr>"
173 "</table>"
176 self.ui.l_example.setText("/Carla/2/set_parameter_value 5 1.0")
177 self.ui.l_example_help.setText("<i>(as in this example, \"2\" is the plugin number and \"5\" the parameter)</i>")
178 # pylint: enable=line-too-long
180 self.ui.l_ladspa.setText(self.tr("Everything! (Including LRDF)"))
181 self.ui.l_dssi.setText(self.tr("Everything! (Including CustomData/Chunks)"))
182 self.ui.l_lv2.setText(self.tr("About 110&#37; complete (using custom extensions)<br/>"
183 "Implemented Feature/Extensions:"
184 "<ul>"
185 "<li>http://lv2plug.in/ns/ext/atom</li>"
186 "<li>http://lv2plug.in/ns/ext/buf-size</li>"
187 "<li>http://lv2plug.in/ns/ext/data-access</li>"
188 #"<li>http://lv2plug.in/ns/ext/dynmanifest</li>"
189 "<li>http://lv2plug.in/ns/ext/event</li>"
190 "<li>http://lv2plug.in/ns/ext/instance-access</li>"
191 "<li>http://lv2plug.in/ns/ext/log</li>"
192 "<li>http://lv2plug.in/ns/ext/midi</li>"
193 #"<li>http://lv2plug.in/ns/ext/morph</li>"
194 "<li>http://lv2plug.in/ns/ext/options</li>"
195 "<li>http://lv2plug.in/ns/ext/parameters</li>"
196 #"<li>http://lv2plug.in/ns/ext/patch</li>"
197 "<li>http://lv2plug.in/ns/ext/port-props</li>"
198 "<li>http://lv2plug.in/ns/ext/presets</li>"
199 "<li>http://lv2plug.in/ns/ext/resize-port</li>"
200 "<li>http://lv2plug.in/ns/ext/state</li>"
201 "<li>http://lv2plug.in/ns/ext/time</li>"
202 "<li>http://lv2plug.in/ns/ext/uri-map</li>"
203 "<li>http://lv2plug.in/ns/ext/urid</li>"
204 "<li>http://lv2plug.in/ns/ext/worker</li>"
205 "<li>http://lv2plug.in/ns/extensions/ui</li>"
206 "<li>http://lv2plug.in/ns/extensions/units</li>"
207 "<li>http://home.gna.org/lv2dynparam/rtmempool/v1</li>"
208 "<li>http://kxstudio.sf.net/ns/lv2ext/external-ui</li>"
209 "<li>http://kxstudio.sf.net/ns/lv2ext/programs</li>"
210 "<li>http://kxstudio.sf.net/ns/lv2ext/props</li>"
211 "<li>http://kxstudio.sf.net/ns/lv2ext/rtmempool</li>"
212 "<li>http://ll-plugins.nongnu.org/lv2/ext/midimap</li>"
213 "<li>http://ll-plugins.nongnu.org/lv2/ext/miditype</li>"
214 "</ul>"))
216 self.ui.l_vst2.setText(self.tr("About 85&#37; complete (missing vst bank/presets and some minor stuff)"))
217 self.ui.l_vst3.setText(self.tr("About 66&#37; complete"))
219 if MACOS:
220 self.ui.l_au.setText(self.tr("About 20&#37; complete"))
221 else:
222 self.ui.line_vst3.hide()
223 self.ui.l_au.hide()
224 self.ui.lid_au.hide()
226 # 3rd tab is usually longer than the 1st
227 # adjust appropriately
228 self.ui.tabWidget.setCurrentIndex(2)
229 self.adjustSize()
230 self.ui.tabWidget.setCurrentIndex(0)
232 self.setFixedSize(self.size())
234 flags = self.windowFlags()
235 flags &= ~Qt.WindowContextHelpButtonHint
237 if WINDOWS:
238 flags |= Qt.MSWindowsFixedSizeDialogHint
240 self.setWindowFlags(flags)
242 if MACOS:
243 self.setWindowModality(Qt.WindowModal)
245 # ------------------------------------------------------------------------------------------------------------
246 # Plugin Parameter
248 class PluginParameter(QWidget):
249 mappedControlChanged = pyqtSignal(int, int)
250 mappedRangeChanged = pyqtSignal(int, float, float)
251 midiChannelChanged = pyqtSignal(int, int)
252 valueChanged = pyqtSignal(int, float)
254 def __init__(self, parent, host, pInfo, pluginId, tabIndex):
255 QWidget.__init__(self, parent)
256 self.host = host
257 self.ui = ui_carla_parameter.Ui_PluginParameter()
258 self.ui.setupUi(self)
260 # -------------------------------------------------------------
261 # Internal stuff
263 self.fDecimalPoints = max(2, countDecimalPoints(pInfo['step'], pInfo['stepSmall']))
264 self.fCanBeInCV = pInfo['hints'] & PARAMETER_CAN_BE_CV_CONTROLLED
265 self.fMappedCtrl = pInfo['mappedControlIndex']
266 self.fMappedMinimum = pInfo['mappedMinimum']
267 self.fMappedMaximum = pInfo['mappedMaximum']
268 self.fMinimum = pInfo['minimum']
269 self.fMaximum = pInfo['maximum']
270 self.fMidiChannel = pInfo['midiChannel']
271 self.fParameterId = pInfo['index']
272 self.fPluginId = pluginId
273 self.fTabIndex = tabIndex
275 # -------------------------------------------------------------
276 # Set-up GUI
278 pType = pInfo['type']
279 pHints = pInfo['hints']
281 self.ui.l_name.setText(pInfo['name'])
282 self.ui.widget.setName(pInfo['name'])
283 self.ui.widget.setMinimum(pInfo['minimum'])
284 self.ui.widget.setMaximum(pInfo['maximum'])
285 self.ui.widget.setDefault(pInfo['default'])
286 self.ui.widget.setLabel(pInfo['unit'])
287 self.ui.widget.setStep(pInfo['step'])
288 self.ui.widget.setStepSmall(pInfo['stepSmall'])
289 self.ui.widget.setStepLarge(pInfo['stepLarge'])
290 self.ui.widget.setScalePoints(pInfo['scalePoints'], bool(pHints & PARAMETER_USES_SCALEPOINTS))
292 if pInfo['comment']:
293 self.ui.l_name.setToolTip(pInfo['comment'])
294 self.ui.widget.setToolTip(pInfo['comment'])
296 if pType == PARAMETER_INPUT:
297 if not pHints & PARAMETER_IS_ENABLED:
298 self.ui.l_name.setEnabled(False)
299 self.ui.l_status.setEnabled(False)
300 self.ui.widget.setEnabled(False)
301 self.ui.widget.setReadOnly(True)
302 self.ui.tb_options.setEnabled(False)
304 elif not pHints & PARAMETER_IS_AUTOMATABLE:
305 self.ui.l_status.setEnabled(False)
306 self.ui.tb_options.setEnabled(False)
308 if pHints & PARAMETER_IS_READ_ONLY:
309 self.ui.l_status.setEnabled(False)
310 self.ui.widget.setReadOnly(True)
311 self.ui.tb_options.setEnabled(False)
313 elif pType == PARAMETER_OUTPUT:
314 self.ui.widget.setReadOnly(True)
316 else:
317 self.ui.l_status.setVisible(False)
318 self.ui.widget.setVisible(False)
319 self.ui.tb_options.setVisible(False)
321 # Only set value after all hints are handled
322 self.ui.widget.setValue(pInfo['current'])
324 if pHints & PARAMETER_USES_CUSTOM_TEXT and not host.isPlugin:
325 self.ui.widget.setTextCallback(self._textCallBack)
327 self.ui.l_status.setFixedWidth(fontMetricsHorizontalAdvance(self.ui.l_status.fontMetrics(),
328 self.tr("CC%i Ch%i" % (119,16))))
330 self.ui.widget.setValueCallback(self._valueCallBack)
331 self.ui.widget.updateAll()
333 self.setMappedControlIndex(pInfo['mappedControlIndex'])
334 self.setMidiChannel(pInfo['midiChannel'])
335 self.updateStatusLabel()
337 # -------------------------------------------------------------
338 # Set-up connections
340 self.ui.tb_options.clicked.connect(self.slot_optionsCustomMenu)
341 self.ui.widget.dragStateChanged.connect(self.slot_parameterDragStateChanged)
343 # -------------------------------------------------------------
345 def getPluginId(self):
346 return self.fPluginId
348 def getTabIndex(self):
349 return self.fTabIndex
351 def setPluginId(self, pluginId):
352 self.fPluginId = pluginId
354 def setDefault(self, value):
355 self.ui.widget.setDefault(value)
357 def setValue(self, value):
358 self.ui.widget.blockSignals(True)
359 self.ui.widget.setValue(value)
360 self.ui.widget.blockSignals(False)
362 def setMappedControlIndex(self, control):
363 self.fMappedCtrl = control
364 self.updateStatusLabel()
366 def setMappedRange(self, minimum, maximum):
367 self.fMappedMinimum = minimum
368 self.fMappedMaximum = maximum
370 def setMidiChannel(self, channel):
371 self.fMidiChannel = channel
372 self.updateStatusLabel()
374 def setLabelWidth(self, width):
375 self.ui.l_name.setFixedWidth(width)
377 def updateStatusLabel(self):
378 if self.fMappedCtrl == CONTROL_INDEX_NONE:
379 text = self.tr("Unmapped")
380 elif self.fMappedCtrl == CONTROL_INDEX_CV:
381 text = self.tr("CV export")
382 elif self.fMappedCtrl == CONTROL_INDEX_MIDI_PITCHBEND:
383 text = self.tr("PBend Ch%i" % (self.fMidiChannel,))
384 elif self.fMappedCtrl == CONTROL_INDEX_MIDI_LEARN:
385 text = self.tr("MIDI Learn")
386 else:
387 text = self.tr("CC%i Ch%i" % (self.fMappedCtrl, self.fMidiChannel))
389 self.ui.l_status.setText(text)
391 @pyqtSlot()
392 def slot_optionsCustomMenu(self):
393 menu = QMenu(self)
395 if self.fMappedCtrl == CONTROL_INDEX_NONE:
396 title = self.tr("Unmapped")
397 elif self.fMappedCtrl == CONTROL_INDEX_CV:
398 title = self.tr("Exposed as CV port")
399 elif self.fMappedCtrl == CONTROL_INDEX_MIDI_PITCHBEND:
400 title = self.tr("Mapped to MIDI Pitchbend, channel %i" % (self.fMidiChannel,))
401 elif self.fMappedCtrl == CONTROL_INDEX_MIDI_LEARN:
402 title = self.tr("MIDI Learn active")
403 else:
404 title = self.tr("Mapped to MIDI control %i, channel %i" % (self.fMappedCtrl, self.fMidiChannel))
406 if self.fMappedCtrl != CONTROL_INDEX_NONE:
407 title += " (range: %g-%g)" % (self.fMappedMinimum, self.fMappedMaximum)
409 actTitle = menu.addAction(title)
410 actTitle.setEnabled(False)
412 menu.addSeparator()
414 actUnmap = menu.addAction(self.tr("Unmap"))
416 if self.fMappedCtrl == CONTROL_INDEX_NONE:
417 actUnmap.setCheckable(True)
418 actUnmap.setChecked(True)
420 if self.fCanBeInCV:
421 menu.addSection("CV")
422 actCV = menu.addAction(self.tr("Expose as CV port"))
423 if self.fMappedCtrl == CONTROL_INDEX_CV:
424 actCV.setCheckable(True)
425 actCV.setChecked(True)
426 else:
427 actCV = None
429 menu.addSection("MIDI")
431 if not self.ui.widget.isReadOnly():
432 actLearn = menu.addAction(self.tr("MIDI Learn"))
434 if self.fMappedCtrl == CONTROL_INDEX_MIDI_LEARN:
435 actLearn.setCheckable(True)
436 actLearn.setChecked(True)
437 else:
438 actLearn = None
440 menuMIDI = menu.addMenu(self.tr("MIDI Control"))
442 if self.fMappedCtrl not in (CONTROL_INDEX_NONE,
443 CONTROL_INDEX_CV,
444 CONTROL_INDEX_MIDI_PITCHBEND,
445 CONTROL_INDEX_MIDI_LEARN):
446 action = menuMIDI.menuAction()
447 action.setCheckable(True)
448 action.setChecked(True)
450 inlist = False
451 actCCs = []
452 for cc in MIDI_CC_LIST:
453 action = menuMIDI.addAction(cc)
454 actCCs.append(action)
456 if self.fMappedCtrl >= 0 and self.fMappedCtrl <= MAX_MIDI_CC_LIST_ITEM:
457 ccx = int(cc.split(" [", 1)[0], 10)
459 if ccx > self.fMappedCtrl and not inlist:
460 inlist = True
461 action = menuMIDI.addAction(self.tr("%02i [0x%02X] (Custom)" % (self.fMappedCtrl,
462 self.fMappedCtrl)))
463 action.setCheckable(True)
464 action.setChecked(True)
465 actCCs.append(action)
467 elif ccx == self.fMappedCtrl:
468 inlist = True
469 action.setCheckable(True)
470 action.setChecked(True)
472 if self.fMappedCtrl > MAX_MIDI_CC_LIST_ITEM and self.fMappedCtrl <= 0x77:
473 action = menuMIDI.addAction(self.tr("%02i [0x%02X] (Custom)" % (self.fMappedCtrl, self.fMappedCtrl)))
474 action.setCheckable(True)
475 action.setChecked(True)
476 actCCs.append(action)
478 actCustomCC = menuMIDI.addAction(self.tr("Custom..."))
480 # TODO
481 #actPitchbend = menu.addAction(self.tr("MIDI Pitchbend"))
483 #if self.fMappedCtrl == CONTROL_INDEX_MIDI_PITCHBEND:
484 #actPitchbend.setCheckable(True)
485 #actPitchbend.setChecked(True)
487 menuChannel = menu.addMenu(self.tr("MIDI Channel"))
489 actChannels = []
490 for i in range(1, 16+1):
491 action = menuChannel.addAction("%i" % i)
492 actChannels.append(action)
494 if self.fMidiChannel == i:
495 action.setCheckable(True)
496 action.setChecked(True)
498 if self.fMappedCtrl != CONTROL_INDEX_NONE:
499 if self.fMappedCtrl == CONTROL_INDEX_CV:
500 menu.addSection("Range (Scaled CV input)")
501 else:
502 menu.addSection("Range (MIDI bounds)")
503 actRangeMinimum = menu.addAction(self.tr("Set minimum... (%g)" % self.fMappedMinimum))
504 actRangeMaximum = menu.addAction(self.tr("Set maximum... (%g)" % self.fMappedMaximum))
505 else:
506 actRangeMinimum = actRangeMaximum = None
508 actSel = menu.exec_(QCursor.pos())
510 if not actSel:
511 return
513 if actSel in actChannels:
514 channel = int(actSel.text())
515 self.fMidiChannel = channel
516 self.updateStatusLabel()
517 self.midiChannelChanged.emit(self.fParameterId, channel)
518 return
520 if actSel == actRangeMinimum:
521 value, ok = QInputDialog.getDouble(self,
522 self.tr("Custom Minimum"),
523 "Custom minimum value to use:",
524 self.fMappedMinimum,
525 self.fMinimum if self.fMappedCtrl != CONTROL_INDEX_CV else -9e6,
526 self.fMaximum if self.fMappedCtrl != CONTROL_INDEX_CV else 9e6,
527 self.fDecimalPoints)
528 if not ok:
529 return
531 self.fMappedMinimum = value
532 self.mappedRangeChanged.emit(self.fParameterId, self.fMappedMinimum, self.fMappedMaximum)
533 return
535 if actSel == actRangeMaximum:
536 value, ok = QInputDialog.getDouble(self,
537 self.tr("Custom Maximum"),
538 "Custom maximum value to use:",
539 self.fMappedMaximum,
540 self.fMinimum if self.fMappedCtrl != CONTROL_INDEX_CV else -9e6,
541 self.fMaximum if self.fMappedCtrl != CONTROL_INDEX_CV else 9e6,
542 self.fDecimalPoints)
543 if not ok:
544 return
546 self.fMappedMaximum = value
547 self.mappedRangeChanged.emit(self.fParameterId, self.fMappedMinimum, self.fMappedMaximum)
548 return
550 if actSel == actUnmap:
551 ctrl = CONTROL_INDEX_NONE
552 elif actSel == actCV:
553 ctrl = CONTROL_INDEX_CV
554 elif actSel == actCustomCC:
555 value = self.fMappedCtrl if self.fMappedCtrl >= 0x01 and self.fMappedCtrl <= 0x77 else 1
556 ctrl, ok = QInputDialog.getInt(self,
557 self.tr("Custom CC"),
558 "Custom MIDI CC to use:",
559 value,
560 0x01, 0x77, 1)
561 if not ok:
562 return
563 #elif actSel == actPitchbend:
564 #ctrl = CONTROL_INDEX_MIDI_PITCHBEND
565 elif actSel == actLearn:
566 ctrl = CONTROL_INDEX_MIDI_LEARN
567 elif actSel in actCCs:
568 ctrl = int(actSel.text().split(" ", 1)[0].replace("&",""), 10)
569 else:
570 return
572 self.fMappedCtrl = ctrl
573 self.updateStatusLabel()
574 self.mappedControlChanged.emit(self.fParameterId, ctrl)
576 @pyqtSlot(bool)
577 def slot_parameterDragStateChanged(self, touch):
578 self.host.set_parameter_touch(self.fPluginId, self.fParameterId, touch)
580 def _textCallBack(self):
581 return self.host.get_parameter_text(self.fPluginId, self.fParameterId)
583 def _valueCallBack(self, value):
584 self.valueChanged.emit(self.fParameterId, value)
586 # ------------------------------------------------------------------------------------------------------------
587 # Plugin Editor Parent (Meta class)
589 class PluginEditParentMeta():
590 #class PluginEditParentMeta(metaclass=ABCMeta):
591 @abstractmethod
592 def editDialogVisibilityChanged(self, pluginId, visible):
593 raise NotImplementedError
595 @abstractmethod
596 def editDialogPluginHintsChanged(self, pluginId, hints):
597 raise NotImplementedError
599 @abstractmethod
600 def editDialogParameterValueChanged(self, pluginId, parameterId, value):
601 raise NotImplementedError
603 @abstractmethod
604 def editDialogProgramChanged(self, pluginId, index):
605 raise NotImplementedError
607 @abstractmethod
608 def editDialogMidiProgramChanged(self, pluginId, index):
609 raise NotImplementedError
611 @abstractmethod
612 def editDialogNotePressed(self, pluginId, note):
613 raise NotImplementedError
615 @abstractmethod
616 def editDialogNoteReleased(self, pluginId, note):
617 raise NotImplementedError
619 @abstractmethod
620 def editDialogMidiActivityChanged(self, pluginId, onOff):
621 raise NotImplementedError
623 # ------------------------------------------------------------------------------------------------------------
624 # Plugin Editor (Built-in)
626 class PluginEdit(QDialog):
627 # signals
628 SIGTERM = pyqtSignal()
629 SIGUSR1 = pyqtSignal()
631 def __init__(self, parent, host, pluginId):
632 QDialog.__init__(self, parent.window() if parent is not None else None)
633 self.host = host
634 self.ui = ui_carla_edit.Ui_PluginEdit()
635 self.ui.setupUi(self)
637 # -------------------------------------------------------------
638 # Internal stuff
640 self.fGeometry = QByteArray()
641 self.fParent = parent
642 self.fPluginId = pluginId
643 self.fPluginInfo = None
645 self.fCurrentStateFilename = None
646 self.fControlChannel = round(host.get_internal_parameter_value(pluginId, PARAMETER_CTRL_CHANNEL))
647 self.fFirstInit = True
649 self.fParameterList = [] # (type, id, widget)
650 self.fParametersToUpdate = [] # (id, value)
652 self.fPlayingNotes = [] # (channel, note)
654 self.fTabIconOff = QIcon(":/scalable/led_off.svg")
655 self.fTabIconOn = QIcon(":/scalable/led_yellow.svg")
656 self.fTabIconTimers = []
658 # used during testing
659 self.fIdleTimerId = 0
661 # -------------------------------------------------------------
662 # Set-up GUI
664 labelPluginFont = self.ui.label_plugin.font()
665 labelPluginFont.setPixelSize(15)
666 labelPluginFont.setWeight(75)
667 self.ui.label_plugin.setFont(labelPluginFont)
669 self.ui.dial_drywet.setCustomPaintMode(self.ui.dial_drywet.CUSTOM_PAINT_MODE_CARLA_WET)
670 self.ui.dial_drywet.setImage(3)
671 self.ui.dial_drywet.setLabel("Dry/Wet")
672 self.ui.dial_drywet.setMinimum(0.0)
673 self.ui.dial_drywet.setMaximum(1.0)
674 self.ui.dial_drywet.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_DRYWET))
676 self.ui.dial_vol.setCustomPaintMode(self.ui.dial_vol.CUSTOM_PAINT_MODE_CARLA_VOL)
677 self.ui.dial_vol.setImage(3)
678 self.ui.dial_vol.setLabel("Volume")
679 self.ui.dial_vol.setMinimum(0.0)
680 self.ui.dial_vol.setMaximum(1.27)
681 self.ui.dial_vol.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_VOLUME))
683 self.ui.dial_b_left.setCustomPaintMode(self.ui.dial_b_left.CUSTOM_PAINT_MODE_CARLA_L)
684 self.ui.dial_b_left.setImage(4)
685 self.ui.dial_b_left.setLabel("L")
686 self.ui.dial_b_left.setMinimum(-1.0)
687 self.ui.dial_b_left.setMaximum(1.0)
688 self.ui.dial_b_left.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_BALANCE_LEFT))
690 self.ui.dial_b_right.setCustomPaintMode(self.ui.dial_b_right.CUSTOM_PAINT_MODE_CARLA_R)
691 self.ui.dial_b_right.setImage(4)
692 self.ui.dial_b_right.setLabel("R")
693 self.ui.dial_b_right.setMinimum(-1.0)
694 self.ui.dial_b_right.setMaximum(1.0)
695 self.ui.dial_b_right.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_BALANCE_RIGHT))
697 self.ui.dial_pan.setCustomPaintMode(self.ui.dial_b_right.CUSTOM_PAINT_MODE_CARLA_PAN)
698 self.ui.dial_pan.setImage(4)
699 self.ui.dial_pan.setLabel("Pan")
700 self.ui.dial_pan.setMinimum(-1.0)
701 self.ui.dial_pan.setMaximum(1.0)
702 self.ui.dial_pan.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_PANNING))
704 self.ui.sb_ctrl_channel.setValue(self.fControlChannel+1)
706 self.ui.scrollArea = PixmapKeyboardHArea(self)
707 self.ui.keyboard = self.ui.scrollArea.keyboard
708 self.ui.keyboard.setEnabled(self.fControlChannel >= 0)
709 self.layout().addWidget(self.ui.scrollArea)
711 self.ui.scrollArea.setEnabled(False)
712 self.ui.scrollArea.setVisible(False)
714 # todo
715 self.ui.rb_balance.setEnabled(False)
716 self.ui.rb_balance.setVisible(False)
717 self.ui.rb_pan.setEnabled(False)
718 self.ui.rb_pan.setVisible(False)
720 flags = self.windowFlags()
721 flags &= ~Qt.WindowContextHelpButtonHint
722 self.setWindowFlags(flags)
724 self.reloadAll()
726 self.fFirstInit = False
728 # -------------------------------------------------------------
729 # Set-up connections
731 self.finished.connect(self.slot_finished)
733 self.ui.ch_fixed_buffer.clicked.connect(self.slot_optionChanged)
734 self.ui.ch_force_stereo.clicked.connect(self.slot_optionChanged)
735 self.ui.ch_map_program_changes.clicked.connect(self.slot_optionChanged)
736 self.ui.ch_use_chunks.clicked.connect(self.slot_optionChanged)
737 self.ui.ch_send_notes.clicked.connect(self.slot_optionChanged)
738 self.ui.ch_send_program_changes.clicked.connect(self.slot_optionChanged)
739 self.ui.ch_send_control_changes.clicked.connect(self.slot_optionChanged)
740 self.ui.ch_send_channel_pressure.clicked.connect(self.slot_optionChanged)
741 self.ui.ch_send_note_aftertouch.clicked.connect(self.slot_optionChanged)
742 self.ui.ch_send_pitchbend.clicked.connect(self.slot_optionChanged)
743 self.ui.ch_send_all_sound_off.clicked.connect(self.slot_optionChanged)
745 self.ui.dial_drywet.realValueChanged.connect(self.slot_dryWetChanged)
746 self.ui.dial_vol.realValueChanged.connect(self.slot_volumeChanged)
747 self.ui.dial_b_left.realValueChanged.connect(self.slot_balanceLeftChanged)
748 self.ui.dial_b_right.realValueChanged.connect(self.slot_balanceRightChanged)
749 self.ui.dial_pan.realValueChanged.connect(self.slot_panChanged)
750 self.ui.sb_ctrl_channel.valueChanged.connect(self.slot_ctrlChannelChanged)
752 self.ui.dial_drywet.customContextMenuRequested.connect(self.slot_knobCustomMenu)
753 self.ui.dial_vol.customContextMenuRequested.connect(self.slot_knobCustomMenu)
754 self.ui.dial_b_left.customContextMenuRequested.connect(self.slot_knobCustomMenu)
755 self.ui.dial_b_right.customContextMenuRequested.connect(self.slot_knobCustomMenu)
756 self.ui.dial_pan.customContextMenuRequested.connect(self.slot_knobCustomMenu)
757 self.ui.sb_ctrl_channel.customContextMenuRequested.connect(self.slot_channelCustomMenu)
759 self.ui.keyboard.noteOn.connect(self.slot_noteOn)
760 self.ui.keyboard.noteOff.connect(self.slot_noteOff)
762 self.ui.cb_programs.currentIndexChanged.connect(self.slot_programIndexChanged)
763 self.ui.cb_midi_programs.currentIndexChanged.connect(self.slot_midiProgramIndexChanged)
765 self.ui.b_save_state.clicked.connect(self.slot_stateSave)
766 self.ui.b_load_state.clicked.connect(self.slot_stateLoad)
768 host.NoteOnCallback.connect(self.slot_handleNoteOnCallback)
769 host.NoteOffCallback.connect(self.slot_handleNoteOffCallback)
770 host.UpdateCallback.connect(self.slot_handleUpdateCallback)
771 host.ReloadInfoCallback.connect(self.slot_handleReloadInfoCallback)
772 host.ReloadParametersCallback.connect(self.slot_handleReloadParametersCallback)
773 host.ReloadProgramsCallback.connect(self.slot_handleReloadProgramsCallback)
774 host.ReloadAllCallback.connect(self.slot_handleReloadAllCallback)
776 #------------------------------------------------------------------
778 @pyqtSlot(int, int, int, int)
779 def slot_handleNoteOnCallback(self, pluginId, channel, note, velocity):
780 if self.fPluginId != pluginId:
781 return
783 if self.fControlChannel == channel:
784 self.ui.keyboard.sendNoteOn(note, False)
786 playItem = (channel, note)
788 if playItem not in self.fPlayingNotes:
789 self.fPlayingNotes.append(playItem)
791 if len(self.fPlayingNotes) == 1 and self.fParent is not None:
792 self.fParent.editDialogMidiActivityChanged(self.fPluginId, True)
794 @pyqtSlot(int, int, int)
795 def slot_handleNoteOffCallback(self, pluginId, channel, note):
796 if self.fPluginId != pluginId:
797 return
799 if self.fControlChannel == channel:
800 self.ui.keyboard.sendNoteOff(note, False)
802 playItem = (channel, note)
804 if playItem in self.fPlayingNotes:
805 self.fPlayingNotes.remove(playItem)
807 if self.fPlayingNotes and self.fParent is not None:
808 self.fParent.editDialogMidiActivityChanged(self.fPluginId, False)
810 @pyqtSlot(int)
811 def slot_handleUpdateCallback(self, pluginId):
812 if self.fPluginId == pluginId:
813 self.updateInfo()
815 @pyqtSlot(int)
816 def slot_handleReloadInfoCallback(self, pluginId):
817 if self.fPluginId == pluginId:
818 self.reloadInfo()
820 @pyqtSlot(int)
821 def slot_handleReloadParametersCallback(self, pluginId):
822 if self.fPluginId == pluginId:
823 self.reloadParameters()
825 @pyqtSlot(int)
826 def slot_handleReloadProgramsCallback(self, pluginId):
827 if self.fPluginId == pluginId:
828 self.reloadPrograms()
830 @pyqtSlot(int)
831 def slot_handleReloadAllCallback(self, pluginId):
832 if self.fPluginId == pluginId:
833 self.reloadAll()
835 #------------------------------------------------------------------
837 def updateInfo(self):
838 # Update current program text
839 if self.ui.cb_programs.count() > 0:
840 pIndex = self.ui.cb_programs.currentIndex()
841 if pIndex >= 0:
842 pName = self.host.get_program_name(self.fPluginId, pIndex)
843 #pName = pName[:40] + (pName[40:] and "...")
844 self.ui.cb_programs.setItemText(pIndex, pName)
846 # Update current midi program text
847 if self.ui.cb_midi_programs.count() > 0:
848 mpIndex = self.ui.cb_midi_programs.currentIndex()
849 if mpIndex >= 0:
850 mpData = self.host.get_midi_program_data(self.fPluginId, mpIndex)
851 mpBank = mpData['bank']
852 mpProg = mpData['program']
853 mpName = mpData['name']
854 #mpName = mpName[:40] + (mpName[40:] and "...")
855 self.ui.cb_midi_programs.setItemText(mpIndex, "%03i:%03i - %s" % (mpBank+1, mpProg+1, mpName))
857 # Update all parameter values
858 for _, paramId, paramWidget in self.fParameterList:
859 paramWidget.blockSignals(True)
860 paramWidget.setValue(self.host.get_current_parameter_value(self.fPluginId, paramId))
861 paramWidget.blockSignals(False)
863 # and the internal ones too
864 self.ui.dial_drywet.blockSignals(True)
865 self.ui.dial_drywet.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_DRYWET))
866 self.ui.dial_drywet.blockSignals(False)
868 self.ui.dial_vol.blockSignals(True)
869 self.ui.dial_vol.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_VOLUME))
870 self.ui.dial_vol.blockSignals(False)
872 self.ui.dial_b_left.blockSignals(True)
873 self.ui.dial_b_left.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_BALANCE_LEFT))
874 self.ui.dial_b_left.blockSignals(False)
876 self.ui.dial_b_right.blockSignals(True)
877 self.ui.dial_b_right.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_BALANCE_RIGHT))
878 self.ui.dial_b_right.blockSignals(False)
880 self.ui.dial_pan.blockSignals(True)
881 self.ui.dial_pan.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_PANNING))
882 self.ui.dial_pan.blockSignals(False)
884 self.fControlChannel = round(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_CTRL_CHANNEL))
885 self.ui.sb_ctrl_channel.blockSignals(True)
886 self.ui.sb_ctrl_channel.setValue(self.fControlChannel+1)
887 self.ui.sb_ctrl_channel.blockSignals(False)
888 self.ui.keyboard.allNotesOff()
889 self._updateCtrlPrograms()
891 self.fParametersToUpdate = []
893 #------------------------------------------------------------------
895 def reloadAll(self):
896 self.fPluginInfo = self.host.get_plugin_info(self.fPluginId)
898 self.reloadInfo()
899 self.reloadParameters()
900 self.reloadPrograms()
902 if self.fPluginInfo['type'] == PLUGIN_LV2:
903 self.ui.b_save_state.setEnabled(False)
905 if not self.ui.scrollArea.isEnabled():
906 self.resize(self.width(), self.height()-self.ui.scrollArea.height())
908 #------------------------------------------------------------------
910 def reloadInfo(self):
911 realPluginName = self.host.get_real_plugin_name(self.fPluginId)
912 #audioCountInfo = self.host.get_audio_port_count_info(self.fPluginId)
913 midiCountInfo = self.host.get_midi_port_count_info(self.fPluginId)
914 #paramCountInfo = self.host.get_parameter_count_info(self.fPluginId)
916 pluginHints = self.fPluginInfo['hints']
918 self.ui.le_type.setText(getPluginTypeAsString(self.fPluginInfo['type']))
920 self.ui.label_name.setEnabled(bool(realPluginName))
921 self.ui.le_name.setEnabled(bool(realPluginName))
922 self.ui.le_name.setText(realPluginName)
923 self.ui.le_name.setToolTip(realPluginName)
925 self.ui.label_label.setEnabled(bool(self.fPluginInfo['label']))
926 self.ui.le_label.setEnabled(bool(self.fPluginInfo['label']))
927 self.ui.le_label.setText(self.fPluginInfo['label'])
928 self.ui.le_label.setToolTip(self.fPluginInfo['label'])
930 self.ui.label_maker.setEnabled(bool(self.fPluginInfo['maker']))
931 self.ui.le_maker.setEnabled(bool(self.fPluginInfo['maker']))
932 self.ui.le_maker.setText(self.fPluginInfo['maker'])
933 self.ui.le_maker.setToolTip(self.fPluginInfo['maker'])
935 self.ui.label_copyright.setEnabled(bool(self.fPluginInfo['copyright']))
936 self.ui.le_copyright.setEnabled(bool(self.fPluginInfo['copyright']))
937 self.ui.le_copyright.setText(self.fPluginInfo['copyright'])
938 self.ui.le_copyright.setToolTip(self.fPluginInfo['copyright'])
940 self.ui.label_unique_id.setEnabled(bool(self.fPluginInfo['uniqueId']))
941 self.ui.le_unique_id.setEnabled(bool(self.fPluginInfo['uniqueId']))
942 self.ui.le_unique_id.setText(str(self.fPluginInfo['uniqueId']))
943 self.ui.le_unique_id.setToolTip(str(self.fPluginInfo['uniqueId']))
945 self.ui.label_plugin.setText("\n%s\n" % (self.fPluginInfo['name'] or "(none)"))
946 self.setWindowTitle(self.fPluginInfo['name'] or "(none)")
948 self.ui.dial_drywet.setEnabled(pluginHints & PLUGIN_CAN_DRYWET)
949 self.ui.dial_vol.setEnabled(pluginHints & PLUGIN_CAN_VOLUME)
950 self.ui.dial_b_left.setEnabled(pluginHints & PLUGIN_CAN_BALANCE)
951 self.ui.dial_b_right.setEnabled(pluginHints & PLUGIN_CAN_BALANCE)
952 self.ui.dial_pan.setEnabled(pluginHints & PLUGIN_CAN_PANNING)
954 optsAvailable = self.fPluginInfo['optionsAvailable']
955 optsEnabled = self.fPluginInfo['optionsEnabled']
956 self.ui.ch_use_chunks.setEnabled(optsAvailable & PLUGIN_OPTION_USE_CHUNKS)
957 self.ui.ch_use_chunks.setChecked(optsEnabled & PLUGIN_OPTION_USE_CHUNKS)
958 self.ui.ch_fixed_buffer.setEnabled(optsAvailable & PLUGIN_OPTION_FIXED_BUFFERS)
959 self.ui.ch_fixed_buffer.setChecked(optsEnabled & PLUGIN_OPTION_FIXED_BUFFERS)
960 self.ui.ch_force_stereo.setEnabled(optsAvailable & PLUGIN_OPTION_FORCE_STEREO)
961 self.ui.ch_force_stereo.setChecked(optsEnabled & PLUGIN_OPTION_FORCE_STEREO)
962 self.ui.ch_map_program_changes.setEnabled(optsAvailable & PLUGIN_OPTION_MAP_PROGRAM_CHANGES)
963 self.ui.ch_map_program_changes.setChecked(optsEnabled & PLUGIN_OPTION_MAP_PROGRAM_CHANGES)
964 self.ui.ch_send_notes.setEnabled(optsAvailable & PLUGIN_OPTION_SKIP_SENDING_NOTES)
965 self.ui.ch_send_notes.setChecked((self.ui.ch_send_notes.isEnabled() and
966 (optsEnabled & PLUGIN_OPTION_SKIP_SENDING_NOTES) == 0x0))
967 self.ui.ch_send_control_changes.setEnabled(optsAvailable & PLUGIN_OPTION_SEND_CONTROL_CHANGES)
968 self.ui.ch_send_control_changes.setChecked(optsEnabled & PLUGIN_OPTION_SEND_CONTROL_CHANGES)
969 self.ui.ch_send_channel_pressure.setEnabled(optsAvailable & PLUGIN_OPTION_SEND_CHANNEL_PRESSURE)
970 self.ui.ch_send_channel_pressure.setChecked(optsEnabled & PLUGIN_OPTION_SEND_CHANNEL_PRESSURE)
971 self.ui.ch_send_note_aftertouch.setEnabled(optsAvailable & PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH)
972 self.ui.ch_send_note_aftertouch.setChecked(optsEnabled & PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH)
973 self.ui.ch_send_pitchbend.setEnabled(optsAvailable & PLUGIN_OPTION_SEND_PITCHBEND)
974 self.ui.ch_send_pitchbend.setChecked(optsEnabled & PLUGIN_OPTION_SEND_PITCHBEND)
975 self.ui.ch_send_all_sound_off.setEnabled(optsAvailable & PLUGIN_OPTION_SEND_ALL_SOUND_OFF)
976 self.ui.ch_send_all_sound_off.setChecked(optsEnabled & PLUGIN_OPTION_SEND_ALL_SOUND_OFF)
978 canSendPrograms = bool((optsAvailable & PLUGIN_OPTION_SEND_PROGRAM_CHANGES) != 0 and
979 (optsEnabled & PLUGIN_OPTION_MAP_PROGRAM_CHANGES) == 0)
980 self.ui.ch_send_program_changes.setEnabled(canSendPrograms)
981 self.ui.ch_send_program_changes.setChecked(optsEnabled & PLUGIN_OPTION_SEND_PROGRAM_CHANGES)
983 self.ui.sw_programs.setCurrentIndex(0 if self.fPluginInfo['type'] in (PLUGIN_VST2, PLUGIN_SFZ) else 1)
985 # Show/hide keyboard
986 showKeyboard = (self.fPluginInfo['category'] == PLUGIN_CATEGORY_SYNTH or midiCountInfo['ins'] > 0)
987 self.ui.scrollArea.setEnabled(showKeyboard)
988 self.ui.scrollArea.setVisible(showKeyboard)
990 # Force-update parent for new hints
991 if self.fParent is not None and not self.fFirstInit:
992 self.fParent.editDialogPluginHintsChanged(self.fPluginId, pluginHints)
994 def reloadParameters(self):
995 # Reset
996 self.fParameterList = []
997 self.fParametersToUpdate = []
998 self.fTabIconTimers = []
1000 # Save current tab state
1001 tabIndex = self.ui.tabWidget.currentIndex()
1002 tabWidget = self.ui.tabWidget.currentWidget()
1003 scrollVal = tabWidget.verticalScrollBar().value() if isinstance(tabWidget, QScrollArea) else None
1004 del tabWidget
1006 # Remove all previous parameters
1007 for _ in range(self.ui.tabWidget.count()-1):
1008 self.ui.tabWidget.widget(1).deleteLater()
1009 self.ui.tabWidget.removeTab(1)
1011 parameterCount = self.host.get_parameter_count(self.fPluginId)
1013 # -----------------------------------------------------------------
1015 if parameterCount <= 0:
1016 return
1018 # -----------------------------------------------------------------
1020 paramInputList = []
1021 paramOutputList = []
1022 paramInputWidth = 0
1023 paramOutputWidth = 0
1024 unusedParameters = 0
1026 paramInputListFull = [] # ([params], width)
1027 paramOutputListFull = [] # ([params], width)
1029 for i in range(parameterCount):
1030 if i - unusedParameters == self.host.maxParameters:
1031 break
1033 paramData = self.host.get_parameter_data(self.fPluginId, i)
1035 if paramData['type'] not in (PARAMETER_INPUT, PARAMETER_OUTPUT):
1036 unusedParameters += 1
1037 continue
1038 if (paramData['hints'] & PARAMETER_IS_ENABLED) == 0:
1039 unusedParameters += 1
1040 continue
1042 paramInfo = self.host.get_parameter_info(self.fPluginId, i)
1043 paramRanges = self.host.get_parameter_ranges(self.fPluginId, i)
1044 paramValue = self.host.get_current_parameter_value(self.fPluginId, i)
1046 parameter = {
1047 'type': paramData['type'],
1048 'hints': paramData['hints'],
1049 'name': paramInfo['name'],
1050 'unit': paramInfo['unit'],
1051 'scalePoints': [],
1053 'index': paramData['index'],
1054 'default': paramRanges['def'],
1055 'minimum': paramRanges['min'],
1056 'maximum': paramRanges['max'],
1057 'step': paramRanges['step'],
1058 'stepSmall': paramRanges['stepSmall'],
1059 'stepLarge': paramRanges['stepLarge'],
1060 'mappedControlIndex': paramData['mappedControlIndex'],
1061 'mappedMinimum': paramData['mappedMinimum'],
1062 'mappedMaximum': paramData['mappedMaximum'],
1063 'midiChannel': paramData['midiChannel']+1,
1065 'comment': paramInfo['comment'],
1066 'groupName': paramInfo['groupName'],
1068 'current': paramValue
1071 for j in range(paramInfo['scalePointCount']):
1072 scalePointInfo = self.host.get_parameter_scalepoint_info(self.fPluginId, i, j)
1074 parameter['scalePoints'].append({
1075 'value': scalePointInfo['value'],
1076 'label': scalePointInfo['label']
1079 #parameter['name'] = parameter['name'][:30] + (parameter['name'][30:] and "...")
1081 # -----------------------------------------------------------------
1082 # Get width values, in packs of 20
1084 if parameter['type'] == PARAMETER_INPUT:
1085 paramInputWidthTMP = fontMetricsHorizontalAdvance(self.fontMetrics(), parameter['name'])
1087 if paramInputWidthTMP > paramInputWidth:
1088 paramInputWidth = paramInputWidthTMP
1090 paramInputList.append(parameter)
1092 else:
1093 paramOutputWidthTMP = fontMetricsHorizontalAdvance(self.fontMetrics(), parameter['name'])
1095 if paramOutputWidthTMP > paramOutputWidth:
1096 paramOutputWidth = paramOutputWidthTMP
1098 paramOutputList.append(parameter)
1100 paramInputListFull.append((paramInputList, paramInputWidth))
1101 paramOutputListFull.append((paramOutputList, paramOutputWidth))
1103 # Create parameter tabs + widgets
1104 self._createParameterWidgets(PARAMETER_INPUT, paramInputListFull, self.tr("Parameters"))
1105 self._createParameterWidgets(PARAMETER_OUTPUT, paramOutputListFull, self.tr("Outputs"))
1107 # Restore tab state
1108 if tabIndex < self.ui.tabWidget.count():
1109 self.ui.tabWidget.setCurrentIndex(tabIndex)
1110 if scrollVal is not None:
1111 self.ui.tabWidget.currentWidget().verticalScrollBar().setValue(scrollVal)
1113 def reloadPrograms(self):
1114 # Programs
1115 self.ui.cb_programs.blockSignals(True)
1116 self.ui.cb_programs.clear()
1118 programCount = self.host.get_program_count(self.fPluginId)
1120 if programCount > 0:
1121 self.ui.cb_programs.setEnabled(True)
1122 self.ui.label_programs.setEnabled(True)
1124 for i in range(programCount):
1125 pName = self.host.get_program_name(self.fPluginId, i)
1126 #pName = pName[:40] + (pName[40:] and "...")
1127 self.ui.cb_programs.addItem(pName)
1129 self.ui.cb_programs.setCurrentIndex(self.host.get_current_program_index(self.fPluginId))
1131 else:
1132 self.ui.cb_programs.setEnabled(False)
1133 self.ui.label_programs.setEnabled(False)
1135 self.ui.cb_programs.blockSignals(False)
1137 # MIDI Programs
1138 self.ui.cb_midi_programs.blockSignals(True)
1139 self.ui.cb_midi_programs.clear()
1141 midiProgramCount = self.host.get_midi_program_count(self.fPluginId)
1143 if midiProgramCount > 0:
1144 self.ui.cb_midi_programs.setEnabled(True)
1145 self.ui.label_midi_programs.setEnabled(True)
1147 for i in range(midiProgramCount):
1148 mpData = self.host.get_midi_program_data(self.fPluginId, i)
1149 mpBank = mpData['bank']
1150 mpProg = mpData['program']
1151 mpName = mpData['name']
1152 #mpName = mpName[:40] + (mpName[40:] and "...")
1154 self.ui.cb_midi_programs.addItem("%03i:%03i - %s" % (mpBank+1, mpProg+1, mpName))
1156 self.ui.cb_midi_programs.setCurrentIndex(self.host.get_current_midi_program_index(self.fPluginId))
1158 else:
1159 self.ui.cb_midi_programs.setEnabled(False)
1160 self.ui.label_midi_programs.setEnabled(False)
1162 self.ui.cb_midi_programs.blockSignals(False)
1164 self.ui.sw_programs.setEnabled(programCount > 0 or midiProgramCount > 0)
1166 if self.fPluginInfo['type'] == PLUGIN_LV2:
1167 self.ui.b_load_state.setEnabled(programCount > 0)
1169 #------------------------------------------------------------------
1171 def clearNotes(self):
1172 self.fPlayingNotes = []
1173 self.ui.keyboard.allNotesOff()
1175 def noteOn(self, channel, note, velocity):
1176 if self.fControlChannel == channel:
1177 self.ui.keyboard.sendNoteOn(note, False)
1179 def noteOff(self, channel, note):
1180 if self.fControlChannel == channel:
1181 self.ui.keyboard.sendNoteOff(note, False)
1183 #------------------------------------------------------------------
1185 def getHints(self):
1186 return self.fPluginInfo['hints']
1188 def setPluginId(self, idx):
1189 self.fPluginId = idx
1191 def setName(self, name):
1192 self.fPluginInfo['name'] = name
1193 self.ui.label_plugin.setText("\n%s\n" % name)
1194 self.setWindowTitle(name)
1196 #------------------------------------------------------------------
1198 def setParameterValue(self, parameterId, value):
1199 for paramItem in self.fParametersToUpdate:
1200 if paramItem[0] == parameterId:
1201 paramItem[1] = value
1202 break
1203 else:
1204 self.fParametersToUpdate.append([parameterId, value])
1206 def setParameterDefault(self, parameterId, value):
1207 for _, paramId, paramWidget in self.fParameterList:
1208 if paramId == parameterId:
1209 paramWidget.setDefault(value)
1210 break
1212 def setParameterMappedControlIndex(self, parameterId, control):
1213 for _, paramId, paramWidget in self.fParameterList:
1214 if paramId == parameterId:
1215 paramWidget.setMappedControlIndex(control)
1216 break
1218 def setParameterMappedRange(self, parameterId, minimum, maximum):
1219 for _, paramId, paramWidget in self.fParameterList:
1220 if paramId == parameterId:
1221 paramWidget.setMappedRange(minimum, maximum)
1222 break
1224 def setParameterMidiChannel(self, parameterId, channel):
1225 for _, paramId, paramWidget in self.fParameterList:
1226 if paramId == parameterId:
1227 paramWidget.setMidiChannel(channel+1)
1228 break
1230 def setProgram(self, index):
1231 self.ui.cb_programs.blockSignals(True)
1232 self.ui.cb_programs.setCurrentIndex(index)
1233 self.ui.cb_programs.blockSignals(False)
1234 self._updateParameterValues()
1236 def setMidiProgram(self, index):
1237 self.ui.cb_midi_programs.blockSignals(True)
1238 self.ui.cb_midi_programs.setCurrentIndex(index)
1239 self.ui.cb_midi_programs.blockSignals(False)
1240 self._updateParameterValues()
1242 def setOption(self, option, yesNo):
1243 if option == PLUGIN_OPTION_USE_CHUNKS:
1244 widget = self.ui.ch_use_chunks
1245 elif option == PLUGIN_OPTION_FIXED_BUFFERS:
1246 widget = self.ui.ch_fixed_buffer
1247 elif option == PLUGIN_OPTION_FORCE_STEREO:
1248 widget = self.ui.ch_force_stereo
1249 elif option == PLUGIN_OPTION_MAP_PROGRAM_CHANGES:
1250 widget = self.ui.ch_map_program_changes
1251 elif option == PLUGIN_OPTION_SKIP_SENDING_NOTES:
1252 widget = self.ui.ch_send_notes
1253 yesNo = not yesNo
1254 elif option == PLUGIN_OPTION_SEND_PROGRAM_CHANGES:
1255 widget = self.ui.ch_send_program_changes
1256 elif option == PLUGIN_OPTION_SEND_CONTROL_CHANGES:
1257 widget = self.ui.ch_send_control_changes
1258 elif option == PLUGIN_OPTION_SEND_CHANNEL_PRESSURE:
1259 widget = self.ui.ch_send_channel_pressure
1260 elif option == PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH:
1261 widget = self.ui.ch_send_note_aftertouch
1262 elif option == PLUGIN_OPTION_SEND_PITCHBEND:
1263 widget = self.ui.ch_send_pitchbend
1264 elif option == PLUGIN_OPTION_SEND_ALL_SOUND_OFF:
1265 widget = self.ui.ch_send_all_sound_off
1266 else:
1267 return
1269 widget.blockSignals(True)
1270 widget.setChecked(yesNo)
1271 widget.blockSignals(False)
1273 #------------------------------------------------------------------
1275 def setVisible(self, yesNo):
1276 if yesNo:
1277 if not self.fGeometry.isNull():
1278 self.restoreGeometry(self.fGeometry)
1279 else:
1280 self.fGeometry = self.saveGeometry()
1282 QDialog.setVisible(self, yesNo)
1284 if MACOS and yesNo:
1285 parent = self.parent()
1286 if parent is None:
1287 return
1288 gCarla.utils.cocoa_set_transient_window_for(self.windowHandle().winId(), parent.windowHandle().winId())
1290 #------------------------------------------------------------------
1292 def idleSlow(self):
1293 # Check Tab icons
1294 for i in range(len(self.fTabIconTimers)):
1295 if self.fTabIconTimers[i] == ICON_STATE_ON:
1296 self.fTabIconTimers[i] = ICON_STATE_WAIT
1297 elif self.fTabIconTimers[i] == ICON_STATE_WAIT:
1298 self.fTabIconTimers[i] = ICON_STATE_OFF
1299 elif self.fTabIconTimers[i] == ICON_STATE_OFF:
1300 self.fTabIconTimers[i] = ICON_STATE_NULL
1301 self.ui.tabWidget.setTabIcon(i+1, self.fTabIconOff)
1303 # Check parameters needing update
1304 for index, value in self.fParametersToUpdate:
1305 if index == PARAMETER_DRYWET:
1306 self.ui.dial_drywet.blockSignals(True)
1307 self.ui.dial_drywet.setValue(value)
1308 self.ui.dial_drywet.blockSignals(False)
1310 elif index == PARAMETER_VOLUME:
1311 self.ui.dial_vol.blockSignals(True)
1312 self.ui.dial_vol.setValue(value)
1313 self.ui.dial_vol.blockSignals(False)
1315 elif index == PARAMETER_BALANCE_LEFT:
1316 self.ui.dial_b_left.blockSignals(True)
1317 self.ui.dial_b_left.setValue(value)
1318 self.ui.dial_b_left.blockSignals(False)
1320 elif index == PARAMETER_BALANCE_RIGHT:
1321 self.ui.dial_b_right.blockSignals(True)
1322 self.ui.dial_b_right.setValue(value)
1323 self.ui.dial_b_right.blockSignals(False)
1325 elif index == PARAMETER_PANNING:
1326 self.ui.dial_pan.blockSignals(True)
1327 self.ui.dial_pan.setValue(value)
1328 self.ui.dial_pan.blockSignals(False)
1330 elif index == PARAMETER_CTRL_CHANNEL:
1331 self.fControlChannel = round(value)
1332 self.ui.sb_ctrl_channel.blockSignals(True)
1333 self.ui.sb_ctrl_channel.setValue(self.fControlChannel+1)
1334 self.ui.sb_ctrl_channel.blockSignals(False)
1335 self.ui.keyboard.allNotesOff()
1336 self._updateCtrlPrograms()
1338 elif index >= 0:
1339 for paramType, paramId, paramWidget in self.fParameterList:
1340 if paramId != index:
1341 continue
1342 # FIXME see below
1343 if paramType != PARAMETER_INPUT:
1344 continue
1346 paramWidget.blockSignals(True)
1347 paramWidget.setValue(value)
1348 paramWidget.blockSignals(False)
1350 #if paramType == PARAMETER_INPUT:
1351 tabIndex = paramWidget.getTabIndex()
1353 if self.fTabIconTimers[tabIndex-1] == ICON_STATE_NULL:
1354 self.ui.tabWidget.setTabIcon(tabIndex, self.fTabIconOn)
1356 self.fTabIconTimers[tabIndex-1] = ICON_STATE_ON
1357 break
1359 # Clear all parameters
1360 self.fParametersToUpdate = []
1362 # Update parameter outputs | FIXME needed?
1363 for paramType, paramId, paramWidget in self.fParameterList:
1364 if paramType != PARAMETER_OUTPUT:
1365 continue
1367 paramWidget.blockSignals(True)
1368 paramWidget.setValue(self.host.get_current_parameter_value(self.fPluginId, paramId))
1369 paramWidget.blockSignals(False)
1371 #------------------------------------------------------------------
1373 @pyqtSlot()
1374 def slot_stateSave(self):
1375 if self.fPluginInfo['type'] == PLUGIN_LV2:
1376 # TODO
1377 return
1379 if self.fCurrentStateFilename:
1380 askTry = QMessageBox.question(self,
1381 self.tr("Overwrite?"),
1382 self.tr("Overwrite previously created file?"),
1383 QMessageBox.Ok|QMessageBox.Cancel)
1385 if askTry == QMessageBox.Ok:
1386 self.host.save_plugin_state(self.fPluginId, self.fCurrentStateFilename)
1387 return
1389 self.fCurrentStateFilename = None
1391 fileFilter = self.tr("Carla State File (*.carxs)")
1392 filename, _ = QFileDialog.getSaveFileName(self, self.tr("Save Plugin State File"), filter=fileFilter)
1394 # FIXME use ok value, test if it works as expected
1395 if not filename:
1396 return
1398 if not filename.lower().endswith(".carxs"):
1399 filename += ".carxs"
1401 self.fCurrentStateFilename = filename
1402 self.host.save_plugin_state(self.fPluginId, self.fCurrentStateFilename)
1404 @pyqtSlot()
1405 def slot_stateLoad(self):
1406 if self.fPluginInfo['type'] == PLUGIN_LV2:
1407 presetList = []
1409 for i in range(self.host.get_program_count(self.fPluginId)):
1410 presetList.append("%03i - %s" % (i+1, self.host.get_program_name(self.fPluginId, i)))
1412 ret = QInputDialog.getItem(self,
1413 self.tr("Open LV2 Preset"),
1414 self.tr("Select an LV2 Preset:"),
1415 presetList, 0, False)
1417 if ret[1]:
1418 index = int(ret[0].split(" - ", 1)[0])-1
1419 self.host.set_program(self.fPluginId, index)
1420 self.setMidiProgram(-1)
1422 return
1424 fileFilter = self.tr("Carla State File (*.carxs)")
1425 filename, _ = QFileDialog.getOpenFileName(self, self.tr("Open Plugin State File"), filter=fileFilter)
1427 # FIXME use ok value, test if it works as expected
1428 if not filename:
1429 return
1431 self.fCurrentStateFilename = filename
1432 self.host.load_plugin_state(self.fPluginId, self.fCurrentStateFilename)
1434 #------------------------------------------------------------------
1436 @pyqtSlot(bool)
1437 def slot_optionChanged(self, clicked):
1438 sender = self.sender()
1440 if sender == self.ui.ch_use_chunks:
1441 option = PLUGIN_OPTION_USE_CHUNKS
1442 elif sender == self.ui.ch_fixed_buffer:
1443 option = PLUGIN_OPTION_FIXED_BUFFERS
1444 elif sender == self.ui.ch_force_stereo:
1445 option = PLUGIN_OPTION_FORCE_STEREO
1446 elif sender == self.ui.ch_map_program_changes:
1447 option = PLUGIN_OPTION_MAP_PROGRAM_CHANGES
1448 elif sender == self.ui.ch_send_notes:
1449 option = PLUGIN_OPTION_SKIP_SENDING_NOTES
1450 clicked = not clicked
1451 elif sender == self.ui.ch_send_program_changes:
1452 option = PLUGIN_OPTION_SEND_PROGRAM_CHANGES
1453 elif sender == self.ui.ch_send_control_changes:
1454 option = PLUGIN_OPTION_SEND_CONTROL_CHANGES
1455 elif sender == self.ui.ch_send_channel_pressure:
1456 option = PLUGIN_OPTION_SEND_CHANNEL_PRESSURE
1457 elif sender == self.ui.ch_send_note_aftertouch:
1458 option = PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH
1459 elif sender == self.ui.ch_send_pitchbend:
1460 option = PLUGIN_OPTION_SEND_PITCHBEND
1461 elif sender == self.ui.ch_send_all_sound_off:
1462 option = PLUGIN_OPTION_SEND_ALL_SOUND_OFF
1463 else:
1464 return
1466 #--------------------------------------------------------------
1467 # handle map-program-changes and send-program-changes conflict
1469 if option == PLUGIN_OPTION_MAP_PROGRAM_CHANGES and clicked:
1470 self.ui.ch_send_program_changes.setEnabled(False)
1472 # disable send-program-changes if needed
1473 if self.ui.ch_send_program_changes.isChecked():
1474 self.host.set_option(self.fPluginId, PLUGIN_OPTION_SEND_PROGRAM_CHANGES, False)
1476 #--------------------------------------------------------------
1477 # set option
1479 self.host.set_option(self.fPluginId, option, clicked)
1481 #--------------------------------------------------------------
1482 # handle map-program-changes and send-program-changes conflict
1484 if option == PLUGIN_OPTION_MAP_PROGRAM_CHANGES and not clicked:
1485 self.ui.ch_send_program_changes.setEnabled(
1486 self.fPluginInfo['optionsAvailable'] & PLUGIN_OPTION_SEND_PROGRAM_CHANGES)
1488 # restore send-program-changes if needed
1489 if self.ui.ch_send_program_changes.isChecked():
1490 self.host.set_option(self.fPluginId, PLUGIN_OPTION_SEND_PROGRAM_CHANGES, True)
1492 #------------------------------------------------------------------
1494 @pyqtSlot(float)
1495 def slot_dryWetChanged(self, value):
1496 self.host.set_drywet(self.fPluginId, value)
1498 if self.fParent is not None:
1499 self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_DRYWET, value)
1501 @pyqtSlot(float)
1502 def slot_volumeChanged(self, value):
1503 self.host.set_volume(self.fPluginId, value)
1505 if self.fParent is not None:
1506 self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_VOLUME, value)
1508 @pyqtSlot(float)
1509 def slot_balanceLeftChanged(self, value):
1510 self.host.set_balance_left(self.fPluginId, value)
1512 if self.fParent is not None:
1513 self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_BALANCE_LEFT, value)
1515 @pyqtSlot(float)
1516 def slot_balanceRightChanged(self, value):
1517 self.host.set_balance_right(self.fPluginId, value)
1519 if self.fParent is not None:
1520 self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_BALANCE_RIGHT, value)
1522 @pyqtSlot(float)
1523 def slot_panChanged(self, value):
1524 self.host.set_panning(self.fPluginId, value)
1526 if self.fParent is not None:
1527 self.fParent.editDialogParameterValueChanged(self.fPluginId, PARAMETER_PANNING, value)
1529 @pyqtSlot(int)
1530 def slot_ctrlChannelChanged(self, value):
1531 self.fControlChannel = value-1
1532 self.host.set_ctrl_channel(self.fPluginId, self.fControlChannel)
1534 self.ui.keyboard.allNotesOff()
1535 self._updateCtrlPrograms()
1537 #------------------------------------------------------------------
1539 @pyqtSlot(int, float)
1540 def slot_parameterValueChanged(self, parameterId, value):
1541 self.host.set_parameter_value(self.fPluginId, parameterId, value)
1543 if self.fParent is not None:
1544 self.fParent.editDialogParameterValueChanged(self.fPluginId, parameterId, value)
1546 @pyqtSlot(int, int)
1547 def slot_parameterMappedControlChanged(self, parameterId, control):
1548 self.host.set_parameter_mapped_control_index(self.fPluginId, parameterId, control)
1550 @pyqtSlot(int, float, float)
1551 def slot_parameterMappedRangeChanged(self, parameterId, minimum, maximum):
1552 self.host.set_parameter_mapped_range(self.fPluginId, parameterId, minimum, maximum)
1554 @pyqtSlot(int, int)
1555 def slot_parameterMidiChannelChanged(self, parameterId, channel):
1556 self.host.set_parameter_midi_channel(self.fPluginId, parameterId, channel-1)
1558 #------------------------------------------------------------------
1560 @pyqtSlot(int)
1561 def slot_programIndexChanged(self, index):
1562 self.host.set_program(self.fPluginId, index)
1564 if self.fParent is not None:
1565 self.fParent.editDialogProgramChanged(self.fPluginId, index)
1567 self._updateParameterValues()
1569 @pyqtSlot(int)
1570 def slot_midiProgramIndexChanged(self, index):
1571 self.host.set_midi_program(self.fPluginId, index)
1573 if self.fParent is not None:
1574 self.fParent.editDialogMidiProgramChanged(self.fPluginId, index)
1576 self._updateParameterValues()
1578 #------------------------------------------------------------------
1580 @pyqtSlot(int)
1581 def slot_noteOn(self, note):
1582 if self.fControlChannel >= 0:
1583 self.host.send_midi_note(self.fPluginId, self.fControlChannel, note, 100)
1585 if self.fParent is not None:
1586 self.fParent.editDialogNotePressed(self.fPluginId, note)
1588 @pyqtSlot(int)
1589 def slot_noteOff(self, note):
1590 if self.fControlChannel >= 0:
1591 self.host.send_midi_note(self.fPluginId, self.fControlChannel, note, 0)
1593 if self.fParent is not None:
1594 self.fParent.editDialogNoteReleased(self.fPluginId, note)
1596 #------------------------------------------------------------------
1598 @pyqtSlot()
1599 def slot_finished(self):
1600 if self.fParent is not None:
1601 self.fParent.editDialogVisibilityChanged(self.fPluginId, False)
1603 #------------------------------------------------------------------
1605 @pyqtSlot()
1606 def slot_knobCustomMenu(self):
1607 sender = self.sender()
1608 knobName = sender.objectName()
1610 if knobName == "dial_drywet":
1611 minimum = 0.0
1612 maximum = 1.0
1613 default = 1.0
1614 label = "Dry/Wet"
1615 elif knobName == "dial_vol":
1616 minimum = 0.0
1617 maximum = 1.27
1618 default = 1.0
1619 label = "Volume"
1620 elif knobName == "dial_b_left":
1621 minimum = -1.0
1622 maximum = 1.0
1623 default = -1.0
1624 label = "Balance-Left"
1625 elif knobName == "dial_b_right":
1626 minimum = -1.0
1627 maximum = 1.0
1628 default = 1.0
1629 label = "Balance-Right"
1630 elif knobName == "dial_pan":
1631 minimum = -1.0
1632 maximum = 1.0
1633 default = 0.0
1634 label = "Panning"
1635 else:
1636 minimum = 0.0
1637 maximum = 1.0
1638 default = 0.5
1639 label = "Unknown"
1641 menu = QMenu(self)
1642 actReset = menu.addAction(self.tr("Reset (%i%%)" % (default*100)))
1643 menu.addSeparator()
1644 actMinimum = menu.addAction(self.tr("Set to Minimum (%i%%)" % (minimum*100)))
1645 actCenter = menu.addAction(self.tr("Set to Center"))
1646 actMaximum = menu.addAction(self.tr("Set to Maximum (%i%%)" % (maximum*100)))
1647 menu.addSeparator()
1648 actSet = menu.addAction(self.tr("Set value..."))
1650 if label not in ("Balance-Left", "Balance-Right", "Panning"):
1651 menu.removeAction(actCenter)
1653 actSelected = menu.exec_(QCursor.pos())
1655 if actSelected == actSet:
1656 current = minimum + (maximum-minimum)*(float(sender.value())/10000)
1657 value, ok = QInputDialog.getInt(self,
1658 self.tr("Set value"),
1659 label,
1660 round(current*100.0),
1661 round(minimum*100.0),
1662 round(maximum*100.0),
1664 if ok:
1665 value = float(value)/100.0
1667 if not ok:
1668 return
1670 elif actSelected == actMinimum:
1671 value = minimum
1672 elif actSelected == actMaximum:
1673 value = maximum
1674 elif actSelected == actReset:
1675 value = default
1676 elif actSelected == actCenter:
1677 value = 0.0
1678 else:
1679 return
1681 sender.setValue(value, True)
1683 #------------------------------------------------------------------
1685 @pyqtSlot()
1686 def slot_channelCustomMenu(self):
1687 menu = QMenu(self)
1689 actNone = menu.addAction(self.tr("None"))
1691 if self.fControlChannel+1 == 0:
1692 actNone.setCheckable(True)
1693 actNone.setChecked(True)
1695 for i in range(1, 16+1):
1696 action = menu.addAction("%i" % i)
1698 if self.fControlChannel+1 == i:
1699 action.setCheckable(True)
1700 action.setChecked(True)
1702 actSel = menu.exec_(QCursor.pos())
1704 if not actSel:
1705 pass
1706 elif actSel == actNone:
1707 self.ui.sb_ctrl_channel.setValue(0)
1708 elif actSel:
1709 selChannel = int(actSel.text())
1710 self.ui.sb_ctrl_channel.setValue(selChannel)
1712 #------------------------------------------------------------------
1714 def _createParameterWidgets(self, paramType, paramListFull, tabPageName):
1715 groupWidgets = {}
1717 for paramList, width in paramListFull:
1718 if not paramList:
1719 break
1721 tabIndex = self.ui.tabWidget.count()
1723 scrollArea = QScrollArea(self.ui.tabWidget)
1724 scrollArea.setWidgetResizable(True)
1725 scrollArea.setFrameStyle(0)
1727 palette1 = scrollArea.palette()
1728 palette1.setColor(QPalette.Background, Qt.transparent)
1729 scrollArea.setPalette(palette1)
1731 palette2 = scrollArea.palette()
1732 palette2.setColor(QPalette.Background, palette2.color(QPalette.Button))
1734 scrollAreaWidget = QWidget(scrollArea)
1735 scrollAreaLayout = QVBoxLayout(scrollAreaWidget)
1736 scrollAreaLayout.setSpacing(3)
1738 for paramInfo in paramList:
1739 groupName = paramInfo['groupName']
1740 if groupName:
1741 groupSymbol, groupName = groupName.split(":",1)
1742 groupLayout, groupWidget = groupWidgets.get(groupSymbol, (None, None))
1743 if groupLayout is None:
1744 groupWidget = CollapsibleBox(groupName, scrollAreaWidget)
1745 groupLayout = groupWidget.getContentLayout()
1746 groupWidget.setPalette(palette2)
1747 scrollAreaLayout.addWidget(groupWidget)
1748 groupWidgets[groupSymbol] = (groupLayout, groupWidget)
1749 else:
1750 groupLayout = scrollAreaLayout
1751 groupWidget = scrollAreaWidget
1753 paramWidget = PluginParameter(groupWidget, self.host, paramInfo, self.fPluginId, tabIndex)
1754 paramWidget.setLabelWidth(width)
1755 groupLayout.addWidget(paramWidget)
1757 self.fParameterList.append((paramType, paramInfo['index'], paramWidget))
1759 if paramType == PARAMETER_INPUT:
1760 paramWidget.valueChanged.connect(self.slot_parameterValueChanged)
1762 paramWidget.mappedControlChanged.connect(self.slot_parameterMappedControlChanged)
1763 paramWidget.mappedRangeChanged.connect(self.slot_parameterMappedRangeChanged)
1764 paramWidget.midiChannelChanged.connect(self.slot_parameterMidiChannelChanged)
1766 scrollAreaLayout.addStretch()
1768 scrollArea.setWidget(scrollAreaWidget)
1770 self.ui.tabWidget.addTab(scrollArea, tabPageName)
1772 if paramType == PARAMETER_INPUT:
1773 self.ui.tabWidget.setTabIcon(tabIndex, self.fTabIconOff)
1775 self.fTabIconTimers.append(ICON_STATE_NULL)
1777 def _updateCtrlPrograms(self):
1778 self.ui.keyboard.setEnabled(self.fControlChannel >= 0)
1780 if self.fPluginInfo['category'] != PLUGIN_CATEGORY_SYNTH:
1781 return
1782 if self.fPluginInfo['type'] not in (PLUGIN_INTERNAL, PLUGIN_SF2):
1783 return
1785 if self.fControlChannel < 0:
1786 self.ui.cb_programs.setEnabled(False)
1787 self.ui.cb_midi_programs.setEnabled(False)
1788 return
1790 self.ui.cb_programs.setEnabled(True)
1791 self.ui.cb_midi_programs.setEnabled(True)
1793 pIndex = self.host.get_current_program_index(self.fPluginId)
1795 if self.ui.cb_programs.currentIndex() != pIndex:
1796 self.setProgram(pIndex)
1798 mpIndex = self.host.get_current_midi_program_index(self.fPluginId)
1800 if self.ui.cb_midi_programs.currentIndex() != mpIndex:
1801 self.setMidiProgram(mpIndex)
1803 def _updateParameterValues(self):
1804 for _, paramId, paramWidget in self.fParameterList:
1805 paramWidget.blockSignals(True)
1806 paramWidget.setValue(self.host.get_current_parameter_value(self.fPluginId, paramId))
1807 paramWidget.blockSignals(False)
1809 #------------------------------------------------------------------
1811 def testTimer(self):
1812 self.fIdleTimerId = self.startTimer(50)
1814 self.SIGTERM.connect(self.testTimerClose)
1815 gCarla.gui = self
1816 setUpSignals()
1818 def testTimerClose(self):
1819 self.close()
1820 _app.quit()
1822 #------------------------------------------------------------------
1824 def closeEvent(self, event):
1825 if self.fIdleTimerId != 0:
1826 self.killTimer(self.fIdleTimerId)
1827 self.fIdleTimerId = 0
1829 self.host.engine_close()
1831 QDialog.closeEvent(self, event)
1833 def timerEvent(self, event):
1834 if event.timerId() == self.fIdleTimerId:
1835 self.host.engine_idle()
1836 self.idleSlow()
1838 QDialog.timerEvent(self, event)
1840 # ------------------------------------------------------------------------------------------------------------
1841 # Main
1843 if __name__ == '__main__':
1844 from carla_app import CarlaApplication
1845 from carla_host import initHost as _initHost, loadHostSettings as _loadHostSettings
1847 _app = CarlaApplication()
1848 _host = _initHost("Widgets", None, False, False, False)
1849 _loadHostSettings(_host)
1851 _host.engine_init("JACK", "Carla-Widgets")
1852 _host.add_plugin(BINARY_NATIVE, PLUGIN_DSSI, "/usr/lib/dssi/karplong.so", "karplong", "karplong", 0, None, 0x0)
1853 _host.set_active(0, True)
1855 gui1 = CarlaAboutW(None, _host)
1856 gui1.show()
1858 gui2 = PluginEdit(None, _host, 0)
1859 gui2.testTimer()
1860 gui2.show()
1862 _app.exit_exec()