diff --git a/resources/16x16/add-from-favorites.svgz b/resources/16x16/add-from-favorites.svgz new file mode 100644 index 000000000..602fee8f3 Binary files /dev/null and b/resources/16x16/add-from-favorites.svgz differ diff --git a/resources/16x16/add-jack-alt.svgz b/resources/16x16/add-jack-alt.svgz new file mode 100644 index 000000000..3ece5fdac Binary files /dev/null and b/resources/16x16/add-jack-alt.svgz differ diff --git a/resources/16x16/add-jack.svgz b/resources/16x16/add-jack.svgz new file mode 100644 index 000000000..4d821803b Binary files /dev/null and b/resources/16x16/add-jack.svgz differ diff --git a/resources/16x16/audio-volume-medium.svgz b/resources/16x16/audio-volume-medium.svgz new file mode 100644 index 000000000..5c92b11d2 Binary files /dev/null and b/resources/16x16/audio-volume-medium.svgz differ diff --git a/resources/16x16/audio-volume-muted-orig.svgz b/resources/16x16/audio-volume-muted-orig.svgz new file mode 100644 index 000000000..5580e561a Binary files /dev/null and b/resources/16x16/audio-volume-muted-orig.svgz differ diff --git a/resources/16x16/audio-volume-muted.svgz b/resources/16x16/audio-volume-muted.svgz new file mode 100644 index 000000000..29547052f Binary files /dev/null and b/resources/16x16/audio-volume-muted.svgz differ diff --git a/resources/16x16/balance-alt.svgz b/resources/16x16/balance-alt.svgz new file mode 100644 index 000000000..dadf30332 Binary files /dev/null and b/resources/16x16/balance-alt.svgz differ diff --git a/resources/16x16/balance-alt2.svgz b/resources/16x16/balance-alt2.svgz new file mode 100644 index 000000000..e0600643e Binary files /dev/null and b/resources/16x16/balance-alt2.svgz differ diff --git a/resources/16x16/balance.svgz b/resources/16x16/balance.svgz new file mode 100644 index 000000000..73dfdc7ad Binary files /dev/null and b/resources/16x16/balance.svgz differ diff --git a/resources/16x16/compact-alt.svgz b/resources/16x16/compact-alt.svgz new file mode 100644 index 000000000..e935723c5 Binary files /dev/null and b/resources/16x16/compact-alt.svgz differ diff --git a/resources/16x16/compact.svgz b/resources/16x16/compact.svgz new file mode 100644 index 000000000..8f14c322e Binary files /dev/null and b/resources/16x16/compact.svgz differ diff --git a/resources/16x16/dry.svgz b/resources/16x16/dry.svgz new file mode 100644 index 000000000..57cd45092 Binary files /dev/null and b/resources/16x16/dry.svgz differ diff --git a/resources/16x16/emblem-favorite.svgz b/resources/16x16/emblem-favorite.svgz new file mode 100644 index 000000000..9fdb44fc9 Binary files /dev/null and b/resources/16x16/emblem-favorite.svgz differ diff --git a/resources/16x16/restore-alt.svgz b/resources/16x16/restore-alt.svgz new file mode 100644 index 000000000..aebd0b633 Binary files /dev/null and b/resources/16x16/restore-alt.svgz differ diff --git a/resources/16x16/restore.svgz b/resources/16x16/restore.svgz new file mode 100644 index 000000000..e5913fc3a Binary files /dev/null and b/resources/16x16/restore.svgz differ diff --git a/resources/16x16/style-alt.svgz b/resources/16x16/style-alt.svgz new file mode 100644 index 000000000..6576e0d33 Binary files /dev/null and b/resources/16x16/style-alt.svgz differ diff --git a/resources/16x16/style.svgz b/resources/16x16/style.svgz new file mode 100644 index 000000000..7105399b5 Binary files /dev/null and b/resources/16x16/style.svgz differ diff --git a/resources/16x16/system-shutdown.svgz b/resources/16x16/system-shutdown.svgz new file mode 100644 index 000000000..b4abcc25b Binary files /dev/null and b/resources/16x16/system-shutdown.svgz differ diff --git a/resources/16x16/system-turnon.svgz b/resources/16x16/system-turnon.svgz new file mode 100644 index 000000000..9f0168119 Binary files /dev/null and b/resources/16x16/system-turnon.svgz differ diff --git a/resources/16x16/view-refresh-purple.svgz b/resources/16x16/view-refresh-purple.svgz new file mode 100644 index 000000000..b5340f858 Binary files /dev/null and b/resources/16x16/view-refresh-purple.svgz differ diff --git a/resources/16x16/wet.svgz b/resources/16x16/wet.svgz new file mode 100644 index 000000000..ca89c0ad7 Binary files /dev/null and b/resources/16x16/wet.svgz differ diff --git a/resources/resources.qrc b/resources/resources.qrc index 74ace3905..2cc9a3a98 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -3,9 +3,15 @@ 16x16/carla.png 16x16/carla-control.png + 16x16/add-from-favorites.svgz + 16x16/add-jack.svgz 16x16/application-exit.svgz 16x16/arrow-right.svgz + 16x16/audio-volume-medium.svgz + 16x16/audio-volume-muted.svgz + 16x16/balance.svgz 16x16/bookmarks.svgz + 16x16/compact.svgz 16x16/configure.svgz 16x16/dialog-cancel.svgz 16x16/dialog-error.svgz @@ -16,9 +22,11 @@ 16x16/document-open.svgz 16x16/document-save.svgz 16x16/document-save-as.svgz + 16x16/dry.svgz 16x16/edit-clear.svgz 16x16/edit-delete.svgz 16x16/edit-rename.svgz + 16x16/emblem-favorite.svgz 16x16/list-add.svgz 16x16/list-remove.svgz 16x16/media-playback-pause.svgz @@ -27,8 +35,14 @@ 16x16/media-seek-backward.svgz 16x16/media-seek-forward.svgz 16x16/network-connect.svgz + 16x16/restore.svgz + 16x16/style.svgz + 16x16/system-shutdown.svgz + 16x16/system-turnon.svgz 16x16/view-refresh.svgz + 16x16/view-refresh-purple.svgz 16x16/view-sort-ascending.svgz + 16x16/wet.svgz 16x16/window-close.svgz 16x16/zoom-fit-best.svgz 16x16/zoom-in.svgz diff --git a/resources/ui/carla_edit.ui b/resources/ui/carla_edit.ui index 321142a5e..8750c4dcb 100644 --- a/resources/ui/carla_edit.ui +++ b/resources/ui/carla_edit.ui @@ -108,17 +108,17 @@ - + - 34 - 34 + 32 + 48 - 34 - 34 + 32 + 48 @@ -130,17 +130,17 @@ - + - 34 - 34 + 32 + 48 - 34 - 34 + 32 + 48 @@ -152,148 +152,92 @@ - + + + + 26 + 40 + + - 16777215 - 42 + 26 + 40 - - 0 + + Qt::CustomContextMenu - - 0 + + Balance Left (0%) - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 26 - 26 - - - - - 26 - 26 - - - - Qt::CustomContextMenu - - - Balance Left (0%) - - - - - - - - 26 - 26 - - - - - 26 - 26 - - - - Qt::CustomContextMenu - - - Balance Right (0%) - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 26 - 26 - - - - - 26 - 26 - - - - Qt::CustomContextMenu - - - Balance Right (0%) - - - - - - - - 0 + + + + 26 + 40 + + + + + 26 + 40 + - - - - Use Balance - - - true - - - - - - - Use Panning - - - - + + Qt::CustomContextMenu + + + Balance Right (0%) + + + + + + + + 32 + 48 + + + + + 32 + 48 + + + + Qt::CustomContextMenu + + + Left-Right (0%) + + + + + + + + 32 + 48 + + + + + 32 + 48 + + + + Qt::CustomContextMenu + + + Front-Rear (0%) + + @@ -310,6 +254,21 @@ + + + + + true + + + + ⚠ L, R are special mixing type. + + + Qt::AlignCenter + + + @@ -876,13 +835,6 @@ Plugin Name - - - ScalableDial - QDial -
widgets/scalabledial.h
-
-
diff --git a/resources/ui/carla_host.ui b/resources/ui/carla_host.ui index 4956f3e85..9fefae396 100644 --- a/resources/ui/carla_host.ui +++ b/resources/ui/carla_host.ui @@ -474,6 +474,7 @@ + @@ -508,6 +509,7 @@ + @@ -548,6 +550,7 @@ &Settings + @@ -589,6 +592,7 @@ + @@ -600,6 +604,17 @@ + + + + + + + + + + + @@ -1111,6 +1126,30 @@ QAction::NoRole + + + + :/16x16/view-refresh-purple.svgz:/16x16/view-refresh-purple.svgz + + + &Reload (!) + + + Reload (!) + + + Reload file. CAUTION, non-saved changes will be LOST! + + + Ctrl+Shift+R + + + false + + + QAction::NoRole + + @@ -1220,6 +1259,10 @@ + + + :/16x16/system-turnon.svgz:/16x16/system-turnon.svgz + Enable @@ -1228,6 +1271,10 @@ + + + :/16x16/system-shutdown.svgz:/16x16/system-shutdown.svgz + Disable @@ -1236,6 +1283,10 @@ + + + :/16x16/dry.svgz:/16x16/dry.svgz + 0% Wet (Bypass) @@ -1244,6 +1295,10 @@ + + + :/16x16/wet.svgz:/16x16/wet.svgz + 100% Wet @@ -1252,6 +1307,10 @@ + + + :/16x16/audio-volume-muted.svgz:/16x16/audio-volume-muted.svgz + 0% Volume (Mute) @@ -1260,6 +1319,10 @@ + + + :/16x16/audio-volume-medium.svgz:/16x16/audio-volume-medium.svgz + 100% Volume @@ -1268,6 +1331,10 @@ + + + :/16x16/balance.svgz:/16x16/balance.svgz + Center Balance @@ -1435,6 +1502,17 @@ QAction::NoRole + + + true + + + Show Toolbar Text + + + QAction::NoRole + + @@ -1553,7 +1631,23 @@ QAction::NoRole + + + + :/16x16/style.svgz:/16x16/style.svgz + + + Change &Skin... + + + QAction::NoRole + + + + + :/16x16/compact.svgz:/16x16/compact.svgz + Compact Slots @@ -1562,6 +1656,10 @@ + + + :/16x16/restore.svgz:/16x16/restore.svgz + Expand Slots @@ -1597,7 +1695,7 @@ - :/16x16/list-add.svgz:/16x16/list-add.svgz + :/16x16/add-jack.svgz:/16x16/add-jack.svgz Add &JACK Application... diff --git a/resources/ui/carla_settings.ui b/resources/ui/carla_settings.ui index 89c3c77a7..b14dc2d43 100644 --- a/resources/ui/carla_settings.ui +++ b/resources/ui/carla_settings.ui @@ -511,6 +511,27 @@ + + + + + + Example: 'Tweak' is for all skins; extra 'skinnameTweak' overrides it for that skin only. + + + Skin tweaks: + + + + + + + Example: 'Tweak' is for all skins; extra 'skinnameTweak' overrides it for that skin only. + + + + + diff --git a/resources/ui/xycontroller.ui b/resources/ui/xycontroller.ui index 83c16a4b2..675d3bd1c 100644 --- a/resources/ui/xycontroller.ui +++ b/resources/ui/xycontroller.ui @@ -30,41 +30,117 @@ - - - -100 + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 48 + 0 + - - 100 + + + + + + + 48 + 58 + + + + + 48 + 58 + - + + + + 48 + 58 + + + + + 48 + 58 + + + + + + Qt::Vertical QSizePolicy::Fixed - + - 20 - 30 + 48 + 0 - - - -100 + + + + 48 + 58 + + + + + 48 + 58 + - - 100 + + + + + + + 48 + 58 + + + + + 48 + 58 + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 48 + 0 + + + + diff --git a/source/backend/plugin/CarlaPluginFluidSynth.cpp b/source/backend/plugin/CarlaPluginFluidSynth.cpp index 2c8278bc0..31124eec3 100644 --- a/source/backend/plugin/CarlaPluginFluidSynth.cpp +++ b/source/backend/plugin/CarlaPluginFluidSynth.cpp @@ -947,7 +947,7 @@ public: pData->hints |= PLUGIN_USES_MULTI_PROGS; if (! kUse16Outs) - pData->hints |= PLUGIN_CAN_BALANCE; + pData->hints |= PLUGIN_CAN_BALANCE | PLUGIN_CAN_PANNING; // extra plugin hints pData->extraHints = 0x0; @@ -1521,7 +1521,7 @@ public: { // note - balance not possible with kUse16Outs, so we can safely skip fAudioOutBuffers - const bool doVolume = (pData->hints & PLUGIN_CAN_VOLUME) != 0 && carla_isNotEqual(pData->postProc.volume, 1.0f); + const bool doVolume = (pData->hints & PLUGIN_CAN_VOLUME) != 0 && (carla_isNotEqual(pData->postProc.volume, 1.0f) || carla_isNotEqual(pData->postProc.panning, 0.0f)); const bool doBalance = (pData->hints & PLUGIN_CAN_BALANCE) != 0 && ! (carla_isEqual(pData->postProc.balanceLeft, -1.0f) && carla_isEqual(pData->postProc.balanceRight, 1.0f)); float* const oldBufLeft = pData->postProc.extraBuffer; @@ -1554,16 +1554,40 @@ public: } } + // Panning + // Only decrease of levels, but never increase, unlike 'L, R'. + // Note: no any pan processing for Mono. + + uint32_t q = pData->audioOut.count; + float pan = pData->postProc.panning; + float vol = pData->postProc.volume; + + // Pan: Stereo only. + if ((pan != 0.0) && (q == 2)) + { + // left channel(s) reduce when pan to right + if ((pan > 0) && (i == 0)) + { + vol = vol * (1.0 - pan); + } + + // right channel(s) reduce when pan to left + else if ((pan < 0) && (i == 1)) + { + vol = vol * (1.0 + pan); + } + } + // Volume if (kUse16Outs) { for (uint32_t k=0; k < frames; ++k) - outBuffer[i][k+timeOffset] = fAudio16Buffers[i][k] * pData->postProc.volume; + outBuffer[i][k+timeOffset] = fAudio16Buffers[i][k] * vol; } else if (doVolume) { for (uint32_t k=0; k < frames; ++k) - outBuffer[i][k+timeOffset] *= pData->postProc.volume; + outBuffer[i][k+timeOffset] *= vol; } } diff --git a/source/backend/plugin/CarlaPluginLADSPADSSI.cpp b/source/backend/plugin/CarlaPluginLADSPADSSI.cpp index e4b0b3657..26cd148c1 100644 --- a/source/backend/plugin/CarlaPluginLADSPADSSI.cpp +++ b/source/backend/plugin/CarlaPluginLADSPADSSI.cpp @@ -1271,6 +1271,9 @@ public: if (aOuts >= 2 && aOuts % 2 == 0) pData->hints |= PLUGIN_CAN_BALANCE; + + if (aOuts >= 2) + pData->hints |= PLUGIN_CAN_PANNING; #endif // extra plugin hints @@ -2083,7 +2086,11 @@ public: fAudioOutBuffers[i][k] = (fAudioOutBuffers[i][k] * pData->postProc.dryWet) + (bufValue * (1.0f - pData->postProc.dryWet)); } } + } + // Do not join this loop with loop above. + for (uint32_t i=0; i < pData->audioOut.count; ++i) + { // Balance if (doBalance) { @@ -2115,10 +2122,34 @@ public: } } + // Panning + // Only decrease of levels, but never increase, unlike 'L, R'. + // Note: no pan processing for Mono. + + uint32_t q = pData->audioOut.count; + float pan = pData->postProc.panning; + float vol = pData->postProc.volume; + + // Pan: Stereo, 3 ch (extra rear/bass), or Quadro. + if ((pan != 0.0) && ((q == 2) || (q == 3) || (q == 4))) + { + // left channel(s) reduce when pan to right + if ((pan > 0) && ((i == 0) || ((i == 2) && (q == 4)))) + { + vol = vol * (1.0 - pan); + } + + // right channel(s) reduce when pan to left + else if ((pan < 0) && ((i == 1) || (i == 3))) + { + vol = vol * (1.0 + pan); + } + } + // Volume (and buffer copy) { for (uint32_t k=0; k < frames; ++k) - audioOut[i][k+timeOffset] = fAudioOutBuffers[i][k] * pData->postProc.volume; + audioOut[i][k+timeOffset] = fAudioOutBuffers[i][k] * vol; } } diff --git a/source/backend/plugin/CarlaPluginLV2.cpp b/source/backend/plugin/CarlaPluginLV2.cpp index 001abc252..294199add 100644 --- a/source/backend/plugin/CarlaPluginLV2.cpp +++ b/source/backend/plugin/CarlaPluginLV2.cpp @@ -3349,6 +3349,9 @@ public: if (aOuts >= 2 && aOuts % 2 == 0) pData->hints |= PLUGIN_CAN_BALANCE; + if (aOuts >= 2) + pData->hints |= PLUGIN_CAN_PANNING; + // extra plugin hints pData->extraHints = 0x0; @@ -4684,7 +4687,11 @@ public: fAudioOutBuffers[i][k] = (fAudioOutBuffers[i][k] * pData->postProc.dryWet) + (bufValue * (1.0f - pData->postProc.dryWet)); } } + } + // Do not join this loop with loop above. + for (uint32_t i=0; i < pData->audioOut.count; ++i) + { // Balance if (doBalance) { @@ -4716,10 +4723,34 @@ public: } } + // Panning + // Only decrease of levels, but never increase, unlike 'L, R'. + // Note: no pan processing for Mono. + + uint32_t q = pData->audioOut.count; + float pan = pData->postProc.panning; + float vol = pData->postProc.volume; + + // Pan: Stereo, 3 ch (extra rear/bass), or Quadro. + if ((pan != 0.0) && ((q == 2) || (q == 3) || (q == 4))) + { + // left channel(s) reduce when pan to right + if ((pan > 0) && ((i == 0) || ((i == 2) && (q == 4)))) + { + vol = vol * (1.0 - pan); + } + + // right channel(s) reduce when pan to left + else if ((pan < 0) && ((i == 1) || (i == 3))) + { + vol = vol * (1.0 + pan); + } + } + // Volume (and buffer copy) { for (uint32_t k=0; k < frames; ++k) - audioOut[i][k+timeOffset] = fAudioOutBuffers[i][k] * pData->postProc.volume; + audioOut[i][k+timeOffset] = fAudioOutBuffers[i][k] * vol; } } } // End of Post-processing diff --git a/source/backend/plugin/CarlaPluginNative.cpp b/source/backend/plugin/CarlaPluginNative.cpp index 7b8a56e2c..dc4550e27 100644 --- a/source/backend/plugin/CarlaPluginNative.cpp +++ b/source/backend/plugin/CarlaPluginNative.cpp @@ -1405,6 +1405,9 @@ public: if (aOuts >= 2 && aOuts % 2 == 0) pData->hints |= PLUGIN_CAN_BALANCE; + if (aOuts >= 2) + pData->hints |= PLUGIN_CAN_PANNING; + // native plugin hints if (fDescriptor->hints & NATIVE_PLUGIN_IS_RTSAFE) pData->hints |= PLUGIN_IS_RTSAFE; @@ -2369,7 +2372,7 @@ public: float bufValue; float* const oldBufLeft = pData->postProc.extraBuffer; - for (; i < pData->audioOut.count; ++i) + for (uint32_t i=0; i < pData->audioOut.count; ++i) { // Dry/Wet if (doDryWet) @@ -2380,7 +2383,11 @@ public: fAudioAndCvOutBuffers[i][k] = (fAudioAndCvOutBuffers[i][k] * pData->postProc.dryWet) + (bufValue * (1.0f - pData->postProc.dryWet)); } } + } + // Do not join this loop with loop above. + for (uint32_t i=0; i < pData->audioOut.count; ++i) + { // Balance if (doBalance) { @@ -2412,10 +2419,34 @@ public: } } + // Panning + // Only decrease of levels, but never increase, unlike 'L, R'. + // Note: no pan processing for Mono. + + uint32_t q = pData->audioOut.count; + float pan = pData->postProc.panning; + float vol = pData->postProc.volume; + + // Pan: Stereo, 3 ch (extra rear/bass), or Quadro. + if ((pan != 0.0) && ((q == 2) || (q == 3) || (q == 4))) + { + // left channel(s) reduce when pan to right + if ((pan > 0) && ((i == 0) || ((i == 2) && (q == 4)))) + { + vol = vol * (1.0 - pan); + } + + // right channel(s) reduce when pan to left + else if ((pan < 0) && ((i == 1) || (i == 3))) + { + vol = vol * (1.0 + pan); + } + } + // Volume (and buffer copy) { for (uint32_t k=0; k < frames; ++k) - audioOut[i][k+timeOffset] = fAudioAndCvOutBuffers[i][k] * pData->postProc.volume; + audioOut[i][k+timeOffset] = fAudioAndCvOutBuffers[i][k] * vol; } } diff --git a/source/frontend/carla_backend.py b/source/frontend/carla_backend.py index 15b93a158..72eea5a96 100644 --- a/source/frontend/carla_backend.py +++ b/source/frontend/carla_backend.py @@ -310,6 +310,16 @@ PARAMETER_CAN_BE_CV_CONTROLLED = 0x800 # @note only valid for parameter inputs. PARAMETER_IS_NOT_SAVED = 0x1000 +# Human readable labels for 24 decoded bits (currently for XRay tab of Edit dialog). +# Are some hints can exceed 2^24 ? +parameterHintsText = ( + "IS_BOOLEAN", "IS_INTEGER", "IS_LOGARITHMIC", "n/a", + "IS_ENABLED", "IS_AUTOMATABLE", "IS_READ_ONLY", "n/a", + "USES_SAMPLERATE", "USES_SCALEPOINTS", "USES_CUSTOM_TEXT", "CAN_BE_CV_CONTROLLED", + "IS_NOT_SAVED", "n/a", "n/a", "n/a", + "n/a", "n/a", "n/a", "n/a", + "n/a", "n/a", "n/a", "n/a", ) + # --------------------------------------------------------------------------------------------------------------------- # Mapped Parameter Flags # Various flags for parameter mappings. diff --git a/source/frontend/carla_host.py b/source/frontend/carla_host.py index dc73ea4f5..2b15f29f3 100644 --- a/source/frontend/carla_host.py +++ b/source/frontend/carla_host.py @@ -54,6 +54,7 @@ if qt_config == 5: QListWidgetItem, QGraphicsView, QMainWindow, + QToolButton, ) elif qt_config == 6: @@ -86,6 +87,7 @@ elif qt_config == 6: QListWidgetItem, QGraphicsView, QMainWindow, + QToolButton, ) # ------------------------------------------------------------------------------------------------------------ @@ -101,6 +103,7 @@ from carla_shared import * from carla_settings import * from carla_utils import * from carla_widgets import * +from carla_skin import * from patchcanvas import patchcanvas from widgets.digitalpeakmeter import DigitalPeakMeter @@ -221,6 +224,8 @@ class HostWindow(QMainWindow): self.fOscAddressTCP = "" self.fOscAddressUDP = "" + self.slowTimer = 0 + if CARLA_OS_MAC: self.fMacClosingHelper = True @@ -270,6 +275,8 @@ class HostWindow(QMainWindow): self.fWithCanvas = withCanvas + self.fTweaks = {} + # ---------------------------------------------------------------------------------------------------- # Internal stuff (logs) @@ -293,6 +300,7 @@ class HostWindow(QMainWindow): if self.host.isControl: self.ui.act_file_new.setVisible(False) self.ui.act_file_open.setVisible(False) + self.ui.act_file_reload.setVisible(False) self.ui.act_file_save_as.setVisible(False) self.ui.tabUtils.removeTab(0) else: @@ -319,10 +327,12 @@ class HostWindow(QMainWindow): self.ui.act_file_new.setEnabled(False) self.ui.act_file_open.setEnabled(False) + self.ui.act_file_reload.setEnabled(False) self.ui.act_file_save.setEnabled(False) self.ui.act_file_save_as.setEnabled(False) self.ui.act_engine_stop.setEnabled(False) - self.ui.act_plugin_remove_all.setEnabled(False) + # self.ui.act_plugin_remove_all.setEnabled(False) + self.setMenuMacrosEnabled(False) self.ui.act_canvas_show_internal.setChecked(False) self.ui.act_canvas_show_internal.setVisible(False) @@ -503,6 +513,7 @@ class HostWindow(QMainWindow): self.ui.act_file_refresh.setIcon(getIcon('view-refresh', 16, 'svgz')) self.ui.act_file_new.setIcon(getIcon('document-new', 16, 'svgz')) self.ui.act_file_open.setIcon(getIcon('document-open', 16, 'svgz')) + self.ui.act_file_reload.setIcon(getIcon('view-refresh-purple', 16, 'svgz')) self.ui.act_file_save.setIcon(getIcon('document-save', 16, 'svgz')) self.ui.act_file_save_as.setIcon(getIcon('document-save-as', 16, 'svgz')) self.ui.act_file_quit.setIcon(getIcon('application-exit', 16, 'svgz')) @@ -511,8 +522,18 @@ class HostWindow(QMainWindow): self.ui.act_engine_panic.setIcon(getIcon('dialog-warning', 16, 'svgz')) self.ui.act_engine_config.setIcon(getIcon('configure', 16, 'svgz')) self.ui.act_plugin_add.setIcon(getIcon('list-add', 16, 'svgz')) - self.ui.act_plugin_add_jack.setIcon(getIcon('list-add', 16, 'svgz')) + self.ui.act_plugin_add_jack.setIcon(getIcon('add-jack', 16, 'svgz')) self.ui.act_plugin_remove_all.setIcon(getIcon('edit-delete', 16, 'svgz')) + self.ui.act_plugins_enable.setIcon(getIcon('system-turnon', 16, 'svgz')) + self.ui.act_plugins_disable.setIcon(getIcon('system-shutdown', 16, 'svgz')) + self.ui.act_plugins_bypass.setIcon(getIcon('dry', 16, 'svgz')) + self.ui.act_plugins_wet100.setIcon(getIcon('wet', 16, 'svgz')) + self.ui.act_plugins_mute.setIcon(getIcon('audio-volume-muted', 16, 'svgz')) + self.ui.act_plugins_volume100.setIcon(getIcon('audio-volume-medium', 16, 'svgz')) + self.ui.act_plugins_center.setIcon(getIcon('balance', 16, 'svgz')) + self.ui.act_plugins_change_skin.setIcon(getIcon('skin', 16, 'svgz')) + self.ui.act_plugins_compact.setIcon(getIcon('compact', 16, 'svgz')) + self.ui.act_plugins_expand.setIcon(getIcon('restore', 16, 'svgz')) self.ui.act_canvas_arrange.setIcon(getIcon('view-sort-ascending', 16, 'svgz')) self.ui.act_canvas_refresh.setIcon(getIcon('view-refresh', 16, 'svgz')) self.ui.act_canvas_zoom_fit.setIcon(getIcon('zoom-fit-best', 16, 'svgz')) @@ -534,6 +555,7 @@ class HostWindow(QMainWindow): self.ui.act_file_new.triggered.connect(self.slot_fileNew) self.ui.act_file_open.triggered.connect(self.slot_fileOpen) + self.ui.act_file_reload.triggered.connect(self.slot_fileReload) self.ui.act_file_save.triggered.connect(self.slot_fileSave) self.ui.act_file_save_as.triggered.connect(self.slot_fileSaveAs) @@ -553,10 +575,12 @@ class HostWindow(QMainWindow): self.ui.act_plugins_wet100.triggered.connect(self.slot_pluginsWet100) self.ui.act_plugins_bypass.triggered.connect(self.slot_pluginsBypass) self.ui.act_plugins_center.triggered.connect(self.slot_pluginsCenter) + self.ui.act_plugins_change_skin.triggered.connect(self.slot_pluginsChangeSkin) self.ui.act_plugins_compact.triggered.connect(self.slot_pluginsCompact) self.ui.act_plugins_expand.triggered.connect(self.slot_pluginsExpand) self.ui.act_settings_show_toolbar.toggled.connect(self.slot_showToolbar) + self.ui.act_settings_show_toolbar_text.toggled.connect(self.slot_showToolbarText) self.ui.act_settings_show_meters.toggled.connect(self.slot_showCanvasMeters) self.ui.act_settings_show_keyboard.toggled.connect(self.slot_showCanvasKeyboard) self.ui.act_settings_show_side_panel.toggled.connect(self.slot_showSidePanel) @@ -738,7 +762,7 @@ class HostWindow(QMainWindow): self.host.set_custom_data(pluginId, CUSTOM_DATA_TYPE_PROPERTY, "CarlaColor", colorStr) pitem.recreateWidget(newColor = color) - def changePluginSkin(self, pluginId, skin): + def changePluginSkin(self, pluginId, skin, color = None): if pluginId > self.fPluginCount: return @@ -748,7 +772,9 @@ class HostWindow(QMainWindow): return self.host.set_custom_data(pluginId, CUSTOM_DATA_TYPE_PROPERTY, "CarlaSkin", skin) - if skin not in ("default","rncbc","presets","mpresets"): + if color is not None: + pitem.recreateWidget(newSkin = skin, newColor = color) + elif skin not in ("default","rncbc","presets","mpresets"): pitem.recreateWidget(newSkin = skin, newColor = (255,255,255)) else: pitem.recreateWidget(newSkin = skin) @@ -935,6 +961,16 @@ class HostWindow(QMainWindow): self.loadProjectNow() self.fProjectFilename = filenameOld + if self.fTweaks.get('ShowReload', 0): + self.ui.act_file_reload.setEnabled(True) + self.ui.act_file_reload.setVisible(True) + + @pyqtSlot() + def slot_fileReload(self): + if not (self.fProjectFilename == ""): + self.pluginRemoveAll() + self.loadProjectNow() + @pyqtSlot() def slot_fileSave(self, saveAs=False): if self.fProjectFilename and not saveAs: @@ -1177,6 +1213,7 @@ class HostWindow(QMainWindow): if self.host.isPlugin or not self.fSessionManagerName: self.ui.act_file_open.setEnabled(False) + self.ui.act_file_reload.setEnabled(False) self.ui.act_file_save_as.setEnabled(False) @pyqtSlot(int, str) @@ -1226,7 +1263,8 @@ class HostWindow(QMainWindow): # Plugins def removeAllPlugins(self): - self.ui.act_plugin_remove_all.setEnabled(False) + # self.ui.act_plugin_remove_all.setEnabled(False) + self.setMenuMacrosEnabled(False) patchcanvas.handleAllPluginsRemoved() while self.ui.listWidget.takeItem(0): @@ -1343,6 +1381,12 @@ class HostWindow(QMainWindow): act = fmenu.addAction(p['name']) act.setData(p) act.triggered.connect(self.slot_favoritePluginAdd) + + if self.fSavedSettings[CARLA_KEY_MAIN_SYSTEM_ICONS]: + fmenu.setIcon(getIcon('add-from-favorites', 16, 'svgz')) + else: + fmenu.setIcon(QIcon(":/16x16/add-from-favorites.svgz")) + menu.addMenu(fmenu) menu.addAction(self.ui.act_plugin_remove_all) @@ -1359,6 +1403,7 @@ class HostWindow(QMainWindow): menu.addSeparator() menu.addAction(self.ui.act_plugins_center) menu.addSeparator() + menu.addAction(self.ui.act_plugins_change_skin) menu.addAction(self.ui.act_plugins_compact) menu.addAction(self.ui.act_plugins_expand) @@ -1454,7 +1499,7 @@ class HostWindow(QMainWindow): if pitem is None: break - pitem.getWidget().setInternalParameter(PLUGIN_CAN_VOLUME, 1.0) + pitem.getWidget().setInternalParameter(PARAMETER_VOLUME, 1.0) @pyqtSlot() def slot_pluginsMute(self): @@ -1465,7 +1510,7 @@ class HostWindow(QMainWindow): if pitem is None: break - pitem.getWidget().setInternalParameter(PLUGIN_CAN_VOLUME, 0.0) + pitem.getWidget().setInternalParameter(PARAMETER_VOLUME, 0.0) @pyqtSlot() def slot_pluginsWet100(self): @@ -1476,7 +1521,7 @@ class HostWindow(QMainWindow): if pitem is None: break - pitem.getWidget().setInternalParameter(PLUGIN_CAN_DRYWET, 1.0) + pitem.getWidget().setInternalParameter(PARAMETER_DRYWET, 1.0) @pyqtSlot() def slot_pluginsBypass(self): @@ -1487,7 +1532,7 @@ class HostWindow(QMainWindow): if pitem is None: break - pitem.getWidget().setInternalParameter(PLUGIN_CAN_DRYWET, 0.0) + pitem.getWidget().setInternalParameter(PARAMETER_DRYWET, 0.0) @pyqtSlot() def slot_pluginsCenter(self): @@ -1502,6 +1547,38 @@ class HostWindow(QMainWindow): pitem.getWidget().setInternalParameter(PARAMETER_BALANCE_RIGHT, 1.0) pitem.getWidget().setInternalParameter(PARAMETER_PANNING, 0.0) + @pyqtSlot() + def slot_pluginsChangeSkin(self): + skin = QInputDialog.getItem(self, self.tr("Change Skin"), self.tr("Change Skin to:"), skinList, 0, False) + if not all(skin): + return + + if skin[0][:4] in ("calf", "clas", "zynf"): # These are non-colorable + for pluginId in range(self.fPluginCount): + gCarla.gui.changePluginSkin(pluginId, skin[0]) + return + + reColor = QInputDialog.getItem(self, self.tr("Change Color"), self.tr("Change Color mode:"), ['Follow Rules / As Is', 'Random'], 0, False) + if not all(reColor): + return + + color = None + if skin[0].startswith("3ba"): # These are dark tinted, need enlight. + color = (254,255,255) # If not random + luma = 0.5 + sat = 0.5 + elif skin[0].startswith("ope"): # These are dark tinted, need enlight. + luma = 0.5 + sat = 1.0 + else: + luma = 0.125 + sat = 0.25 + + for pluginId in range(self.fPluginCount): + if reColor[0][:2] == 'Ra': + color = QColor.fromHslF(random.random(), sat, luma, 1).getRgb()[0:3] + gCarla.gui.changePluginSkin(pluginId, skin[0], color) + @pyqtSlot() def slot_pluginsCompact(self): for pitem in self.fPluginList: @@ -1531,11 +1608,24 @@ class HostWindow(QMainWindow): self.fPluginList.append(pitem) self.fPluginCount += 1 - self.ui.act_plugin_remove_all.setEnabled(self.fPluginCount > 0) + self.setMenuMacrosEnabled(self.fPluginCount > 0) if pluginType == PLUGIN_LV2: self.fHasLoadedLv2Plugins = True + def setMenuMacrosEnabled(self, enabled): + self.ui.act_plugin_remove_all.setEnabled(enabled) + self.ui.act_plugins_enable.setEnabled(enabled) + self.ui.act_plugins_disable.setEnabled(enabled) + self.ui.act_plugins_volume100.setEnabled(enabled) + self.ui.act_plugins_mute.setEnabled(enabled) + self.ui.act_plugins_wet100.setEnabled(enabled) + self.ui.act_plugins_bypass.setEnabled(enabled) + self.ui.act_plugins_center.setEnabled(enabled) + self.ui.act_plugins_change_skin.setEnabled(enabled) + self.ui.act_plugins_compact.setEnabled(enabled) + self.ui.act_plugins_expand.setEnabled(enabled) + @pyqtSlot(int) def slot_handlePluginRemovedCallback(self, pluginId): if self.fWithCanvas: @@ -1558,7 +1648,8 @@ class HostWindow(QMainWindow): del pitem if self.fPluginCount == 0: - self.ui.act_plugin_remove_all.setEnabled(False) + # self.ui.act_plugin_remove_all.setEnabled(False) + self.setMenuMacrosEnabled(False) if self.fCurrentlyRemovingAllPlugins: self.fCurrentlyRemovingAllPlugins = False self.projectLoadingFinished(False) @@ -1569,7 +1660,9 @@ class HostWindow(QMainWindow): pitem = self.fPluginList[i] pitem.setPluginId(i) - self.ui.act_plugin_remove_all.setEnabled(True) + # self.ui.act_plugin_remove_all.setEnabled(True) + self.setMenuMacrosEnabled(True) + # -------------------------------------------------------------------------------------------------------- # Canvas @@ -1975,6 +2068,7 @@ class HostWindow(QMainWindow): settings.setValue("Geometry", self.saveGeometry()) settings.setValue("ShowToolbar", self.ui.toolBar.isEnabled()) + settings.setValue("ShowToolbarText", self.ui.act_settings_show_toolbar_text.isChecked()) settings.setValue("ShowSidePanel", self.ui.dockWidget.isEnabled()) diskFolders = [] @@ -2009,11 +2103,24 @@ class HostWindow(QMainWindow): showToolbar = settings.value("ShowToolbar", True, bool) self.ui.act_settings_show_toolbar.setChecked(showToolbar) + self.ui.act_settings_show_toolbar_text.setEnabled(showToolbar) self.ui.toolBar.blockSignals(True) self.ui.toolBar.setEnabled(showToolbar) self.ui.toolBar.setVisible(showToolbar) self.ui.toolBar.blockSignals(False) + showToolbarText = settings.value("ShowToolbarText", True, bool) + self.ui.act_settings_show_toolbar_text.setChecked(showToolbarText) + self.ui.toolBar.blockSignals(True) + + for btn in self.ui.toolBar.findChildren(QToolButton): + if showToolbarText: + btn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + else: + btn.setToolButtonStyle(Qt.ToolButtonIconOnly) + + self.ui.toolBar.blockSignals(False) + #if settings.contains("SplitterState"): #self.ui.splitter.restoreState(settings.value("SplitterState", b"")) #else: @@ -2066,6 +2173,7 @@ class HostWindow(QMainWindow): CARLA_KEY_MAIN_REFRESH_INTERVAL: settings.value(CARLA_KEY_MAIN_REFRESH_INTERVAL, CARLA_DEFAULT_MAIN_REFRESH_INTERVAL, int), CARLA_KEY_MAIN_SYSTEM_ICONS: settings.value(CARLA_KEY_MAIN_SYSTEM_ICONS, CARLA_DEFAULT_MAIN_SYSTEM_ICONS, bool), CARLA_KEY_MAIN_EXPERIMENTAL: settings.value(CARLA_KEY_MAIN_EXPERIMENTAL, CARLA_DEFAULT_MAIN_EXPERIMENTAL, bool), + CARLA_KEY_MAIN_SKIN_TWEAKS: settings.value(CARLA_KEY_MAIN_SKIN_TWEAKS, CARLA_DEFAULT_MAIN_SKIN_TWEAKS, str), CARLA_KEY_CANVAS_THEME: settings.value(CARLA_KEY_CANVAS_THEME, CARLA_DEFAULT_CANVAS_THEME, str), CARLA_KEY_CANVAS_SIZE: settings.value(CARLA_KEY_CANVAS_SIZE, CARLA_DEFAULT_CANVAS_SIZE, str), CARLA_KEY_CANVAS_AUTO_HIDE_GROUPS: settings.value(CARLA_KEY_CANVAS_AUTO_HIDE_GROUPS, CARLA_DEFAULT_CANVAS_AUTO_HIDE_GROUPS, bool), @@ -2182,6 +2290,19 @@ class HostWindow(QMainWindow): self.ui.toolBar.blockSignals(True) self.ui.toolBar.setEnabled(yesNo) self.ui.toolBar.setVisible(yesNo) + self.ui.act_settings_show_toolbar_text.setEnabled(yesNo) + self.ui.toolBar.blockSignals(False) + + @pyqtSlot(bool) + def slot_showToolbarText(self, yesNo): + self.ui.toolBar.blockSignals(True) + + for btn in self.ui.toolBar.findChildren(QToolButton): + if yesNo: + btn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + else: + btn.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.ui.toolBar.blockSignals(False) @pyqtSlot(bool) @@ -2623,6 +2744,8 @@ class HostWindow(QMainWindow): self.ui.toolBar.setEnabled(visible) self.ui.toolBar.blockSignals(False) self.ui.act_settings_show_toolbar.setChecked(visible) + self.ui.act_settings_show_toolbar_text.setEnabled(visible) + @pyqtSlot(int) def slot_tabChanged(self, index): @@ -2889,7 +3012,6 @@ class HostWindow(QMainWindow): def idleFast(self): self.host.engine_idle() - self.refreshTransport() if self.fPluginCount == 0 or self.fCurrentlyRemovingAllPlugins: return @@ -2922,6 +3044,10 @@ class HostWindow(QMainWindow): def idleSlow(self): self.getAndRefreshRuntimeInfo() + self.slowTimer = (self.slowTimer + 1) % 4 + if (self.slowTimer == 0): + self.refreshTransport() # This one is CPU hungry. Ticket #1934 + if self.fPluginCount == 0 or self.fCurrentlyRemovingAllPlugins: return @@ -2949,6 +3075,13 @@ class HostWindow(QMainWindow): QMainWindow.changeEvent(self, event) def updateStyle(self): + loadTweaks(self) + + if self.fTweaks.get('MoreSpace', 0): + self.ui.pad_left.hide() + self.ui.pad_right.hide() + return + # Rack padding images setup rack_imgL = QImage(":/bitmaps/rack_padding_left.png") rack_imgR = QImage(":/bitmaps/rack_padding_right.png") diff --git a/source/frontend/carla_settings.py b/source/frontend/carla_settings.py index 80fff3340..5905f7196 100755 --- a/source/frontend/carla_settings.py +++ b/source/frontend/carla_settings.py @@ -47,6 +47,7 @@ from carla_backend import ( from carla_shared import ( CARLA_KEY_MAIN_PROJECT_FOLDER, CARLA_KEY_MAIN_USE_PRO_THEME, + CARLA_KEY_MAIN_SKIN_TWEAKS, CARLA_KEY_MAIN_PRO_THEME_COLOR, CARLA_KEY_MAIN_REFRESH_INTERVAL, CARLA_KEY_MAIN_CONFIRM_EXIT, @@ -115,6 +116,7 @@ from carla_shared import ( CARLA_DEFAULT_MAIN_CLASSIC_SKIN, CARLA_DEFAULT_MAIN_SHOW_LOGS, CARLA_DEFAULT_MAIN_SYSTEM_ICONS, + CARLA_DEFAULT_MAIN_SKIN_TWEAKS, #CARLA_DEFAULT_MAIN_EXPERIMENTAL, CARLA_DEFAULT_CANVAS_THEME, CARLA_DEFAULT_CANVAS_SIZE, @@ -705,6 +707,9 @@ class CarlaSettingsW(QDialog): self.ui.cb_main_theme_color.findText(settings.value(CARLA_KEY_MAIN_PRO_THEME_COLOR, CARLA_DEFAULT_MAIN_PRO_THEME_COLOR, str))) + self.ui.le_main_skin_tweaks.setText( + settings.value(CARLA_KEY_MAIN_SKIN_TWEAKS, CARLA_DEFAULT_MAIN_SKIN_TWEAKS, str)) + self.ui.sb_main_refresh_interval.setValue( settings.value(CARLA_KEY_MAIN_REFRESH_INTERVAL, CARLA_DEFAULT_MAIN_REFRESH_INTERVAL, int)) @@ -995,6 +1000,7 @@ class CarlaSettingsW(QDialog): settings.setValue(CARLA_KEY_MAIN_CONFIRM_EXIT, self.ui.ch_main_confirm_exit.isChecked()) settings.setValue(CARLA_KEY_MAIN_CLASSIC_SKIN, self.ui.cb_main_classic_skin_default.isChecked()) settings.setValue(CARLA_KEY_MAIN_USE_PRO_THEME, self.ui.ch_main_theme_pro.isChecked()) + settings.setValue(CARLA_KEY_MAIN_SKIN_TWEAKS, self.ui.le_main_skin_tweaks.text()) settings.setValue(CARLA_KEY_MAIN_PRO_THEME_COLOR, self.ui.cb_main_theme_color.currentText()) settings.setValue(CARLA_KEY_MAIN_REFRESH_INTERVAL, self.ui.sb_main_refresh_interval.value()) settings.setValue(CARLA_KEY_MAIN_SYSTEM_ICONS, self.ui.ch_main_system_icons.isChecked()) @@ -1193,6 +1199,7 @@ class CarlaSettingsW(QDialog): self.ui.group_main_theme.isEnabled()) self.ui.cb_main_theme_color.setCurrentIndex( self.ui.cb_main_theme_color.findText(CARLA_DEFAULT_MAIN_PRO_THEME_COLOR)) + self.ui.le_main_skin_tweaks.setText(CARLA_DEFAULT_MAIN_SKIN_TWEAKS) self.ui.sb_main_refresh_interval.setValue(CARLA_DEFAULT_MAIN_REFRESH_INTERVAL) self.ui.ch_main_confirm_exit.setChecked(CARLA_DEFAULT_MAIN_CONFIRM_EXIT) self.ui.cb_main_classic_skin_default(CARLA_DEFAULT_MAIN_CLASSIC_SKIN) diff --git a/source/frontend/carla_shared.py b/source/frontend/carla_shared.py index aae07a0d0..e93925b92 100644 --- a/source/frontend/carla_shared.py +++ b/source/frontend/carla_shared.py @@ -8,7 +8,7 @@ import os import sys -from math import fmod +from math import fmod, log10 # ------------------------------------------------------------------------------------------------------------ # Imports (Signal) @@ -63,6 +63,9 @@ from carla_backend import ( ENGINE_TRANSPORT_MODE_JACK, ) +from utils import QSafeSettings +import ast + # ------------------------------------------------------------------------------------------------------------ # Config @@ -184,6 +187,7 @@ CANVAS_EYECANDY_SMALL = 1 CARLA_KEY_MAIN_PROJECT_FOLDER = "Main/ProjectFolder" # str CARLA_KEY_MAIN_USE_PRO_THEME = "Main/UseProTheme" # bool CARLA_KEY_MAIN_PRO_THEME_COLOR = "Main/ProThemeColor" # str +CARLA_KEY_MAIN_SKIN_TWEAKS = "Main/SkinTweaks" # str CARLA_KEY_MAIN_REFRESH_INTERVAL = "Main/RefreshInterval" # int CARLA_KEY_MAIN_CONFIRM_EXIT = "Main/ConfirmExit" # bool CARLA_KEY_MAIN_CLASSIC_SKIN = "Main/ClassicSkin" # bool @@ -273,6 +277,7 @@ CARLA_DEFAULT_MAIN_CLASSIC_SKIN = False CARLA_DEFAULT_MAIN_SHOW_LOGS = bool(not CARLA_OS_WIN) CARLA_DEFAULT_MAIN_SYSTEM_ICONS = False CARLA_DEFAULT_MAIN_EXPERIMENTAL = False +CARLA_DEFAULT_MAIN_SKIN_TWEAKS = "'ShowPan':0, 'ShowForth':0, 'WetVolPush':0, 'WetVolPushLed':1, 'Tooltips':0, 'MoreSpace':0, 'WetVolOnCompact':0, 'SymmetricArc':1, 'GapAuto':0, 'ColorFollow':0, 'ShortenLabels':1, 'ButtonHaveLed':1, 'ColoredNeon':1, 'HighContrast':0, 'ShowDisabled':0, 'ShowOutputs':1, 'ShowButtons':1, 'Button3Pos':1, 'TwoLineLabels':0, 'GapMin':0, 'GapMax':100, 'ColorFrom':-0.1, 'ColorSpan':0.4, 'Auto7segSize':0, 'Auto7segWidth':1, 'ShowReload':0, 'ShowPrograms':0, 'ShowMidiPrograms':0, 'ShowProgramsOnCompact':0, 'ShowMidiProgramsOnCompact':0, " # Canvas CARLA_DEFAULT_CANVAS_THEME = "Modern Dark" @@ -647,18 +652,22 @@ else: # Find decimal points for a parameter, using step and stepSmall def countDecimalPoints(step, stepSmall): - if stepSmall >= 1.0: + if (stepSmall >= 1.0) or (step <= 0) or (stepSmall <= 0): return 0 if step >= 1.0: return 2 - count = 0 - value = fmod(abs(step), 1) - while 0.0001 < value < 0.999 and count < 6: - value = fmod(value*10, 1) - count += 1 +# OLD + # count = 0 + # value = fmod(abs(step), 1) + # while 0.0001 < value < 0.999 and count < 6: + # value = fmod(value*10, 1) + # count += 1 + # + # return count - return count +# NEW: Looks like better handling of small values. + return -int(log10(stepSmall)) + 2 # ------------------------------------------------------------------------------------------------------------ # Check if a value is a number (float support) @@ -927,5 +936,50 @@ def CustomMessageBox(parent, icon, title, text, # pylint: enable=no-value-for-parameter # pylint: enable=too-many-arguments +# ------------------------------------------------------------------------------------------------------------ +# Tweaks, in form of 'Parameter':Value or 'skinnameParameter':Value, are holds both per-rack and per-plugin fine-tuning values (tweaks). + +def loadTweaks(self): + settings = QSafeSettings("falkTX", "Carla2") + skinTweaks = settings.value(CARLA_KEY_MAIN_SKIN_TWEAKS, CARLA_DEFAULT_MAIN_SKIN_TWEAKS, str) + try: + self.fTweaks = ast.literal_eval('{' + skinTweaks + '}') + except ValueError as e: + print("ERROR while parse `" + skinTweaks + "` :", e) + +# ------------------------------------------------------------------------------------------------------------ + +def getPrefixSuffix(unit): + prefix = "" + suffix = unit.strip() + + if suffix == "(coef)": + prefix = "* " + suffix = "" + else: + suffix = " " + suffix + + return prefix, suffix + +# ------------------------------------------------------------------------------------------------------------ + +def strLim(value, digits = 5): + # np.format_float_positional(value, trim='-', fractional=False, precision=digits) + result = "%.5f" % value + if "." in result: + result = result.strip("0") + if result[-1] == ".": + result = result.removesuffix(".") + + if len(result) > 9: + return '{:.3e}'.format(value) + else: + return result + +# ------------------------------------------------------------------------------------------------------------ +# Geometry + +RACK_KNOB_GAP = 5 + # ------------------------------------------------------------------------------------------------------------ # pylint: enable=possibly-used-before-assignment diff --git a/source/frontend/carla_skin.py b/source/frontend/carla_skin.py index 2eaba1ccd..bc4a79a14 100644 --- a/source/frontend/carla_skin.py +++ b/source/frontend/carla_skin.py @@ -6,15 +6,19 @@ # Imports (Global) from qt_compat import qt_config +import math +import random +import operator +from operator import itemgetter if qt_config == 5: from PyQt5.QtCore import Qt, QRectF, QLineF, QTimer from PyQt5.QtGui import QColor, QFont, QFontDatabase, QPainter, QPainterPath, QPen - from PyQt5.QtWidgets import QColorDialog, QFrame, QLineEdit, QPushButton + from PyQt5.QtWidgets import QColorDialog, QFrame, QLineEdit, QPushButton, QComboBox, QSizePolicy elif qt_config == 6: from PyQt6.QtCore import Qt, QRectF, QLineF, QTimer from PyQt6.QtGui import QColor, QFont, QFontDatabase, QPainter, QPainterPath, QPen - from PyQt6.QtWidgets import QColorDialog, QFrame, QLineEdit, QPushButton + from PyQt6.QtWidgets import QColorDialog, QFrame, QLineEdit, QPushButton, QComboBox, QSizePolicy # ------------------------------------------------------------------------------------------------------------ # Imports (Custom) @@ -29,7 +33,6 @@ from carla_backend import * from carla_shared import * from carla_widgets import * from widgets.digitalpeakmeter import DigitalPeakMeter -from widgets.paramspinbox import CustomInputDialog from widgets.scalabledial import ScalableDial # ------------------------------------------------------------------------------------------------------------ @@ -126,7 +129,7 @@ def getParameterShortName(paramName): # Get RGB colors for a plugin category def getColorFromCategory(category): - r = 40 + r = 39 g = 40 b = 40 @@ -152,45 +155,35 @@ def getColorFromCategory(category): return (r, g, b) # ------------------------------------------------------------------------------------------------------------ -# - -def setScalableDialStyle(widget, parameterId, parameterCount, whiteLabels, skinStyle): - if skinStyle.startswith("calf"): - widget.setCustomPaintMode(ScalableDial.CUSTOM_PAINT_MODE_NO_GRADIENT) - widget.setImage(7) - - elif skinStyle.startswith("openav"): - widget.setCustomPaintMode(ScalableDial.CUSTOM_PAINT_MODE_NO_GRADIENT) - if parameterId == PARAMETER_DRYWET: - widget.setImage(13) - elif parameterId == PARAMETER_VOLUME: - widget.setImage(12) - else: - widget.setImage(11) - - else: - if parameterId == PARAMETER_DRYWET: - widget.setCustomPaintMode(ScalableDial.CUSTOM_PAINT_MODE_CARLA_WET) - - elif parameterId == PARAMETER_VOLUME: - widget.setCustomPaintMode(ScalableDial.CUSTOM_PAINT_MODE_CARLA_VOL) - - else: - _r = 255 - int((float(parameterId)/float(parameterCount))*200.0) - _g = 55 + int((float(parameterId)/float(parameterCount))*200.0) - _b = 0 #(r-40)*4 - widget.setCustomPaintColor(QColor(_r, _g, _b)) - widget.setCustomPaintMode(ScalableDial.CUSTOM_PAINT_MODE_COLOR) - - if whiteLabels: - colorEnabled = QColor("#BBB") - colorDisabled = QColor("#555") - else: - colorEnabled = QColor("#111") - colorDisabled = QColor("#AAA") - widget.setLabelColor(colorEnabled, colorDisabled) - widget.setImage(3) +skinList = [ + "default", + "3bandeq", + "rncbc", + "calf_black", + "calf_blue", + "classic", + "openav-old", + "openav", + "zynfx", + "presets", + "mpresets", + "tube", + ] + +skinListTweakable = [ + "default", + "calf", + "openav", + "zynfx", + "tube", + ] + +def arrayIndex(array, value): + for index, item in enumerate(array): + if item.startswith(value): + return index + return 0 # ------------------------------------------------------------------------------------------------------------ # Abstract plugin slot @@ -235,6 +228,16 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): self.fAdjustViewableKnobCountScheduled = False + # load fresh skin tweaks + self.fTweaks = {} + loadTweaks(self) + + # take panel color hue & sat to make "follow panel" paint + color = QColor(skinColor[0], skinColor[1], skinColor[2]) + hue = color.hueF() % 1.0 + sat = color.saturationF() + self.fColorHint = int(hue * 100) + int(sat * 100) / 100.0 # 50.80: 50% hue, 80% sat + # used during testing self.fIdleTimerId = 0 @@ -269,6 +272,8 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): self.w_knobs_right = None self.spacer_knobs = None + self.slowTimer = 0 + # ------------------------------------------------------------- # Set-up connections @@ -348,8 +353,31 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): if self.fEditDialog is not None and self.fPluginId == pluginId: self.customUiStateChanged(state) + # @pyqtSlot(int, int, int) + # def slot_handleParameterKnobVisible(self, pluginId, index, value): + # if self.fEditDialog is not None and self.fPluginId == pluginId: + # self.setKnobVisible(index, value) + + # @pyqtSlot(bool) + # def slot_knobVisible(self, value): + # self.host.set_drywet(self.fPluginId, value) + # self.setParameterValue(PARAMETER_DRYWET, value, True) + + @pyqtSlot(float) + def slot_dryWetChanged(self, value): + self.host.set_drywet(self.fPluginId, value) + self.setParameterValue(PARAMETER_DRYWET, value, True) + + @pyqtSlot(float) + def slot_volumeChanged(self, value): + self.host.set_volume(self.fPluginId, value) + self.setParameterValue(PARAMETER_VOLUME, value, True) + # ------------------------------------------------------------------ + def tweak(self, skinName, tweakName, default): + return self.fTweaks.get(skinName + tweakName, self.fTweaks.get(tweakName, default)) + def ready(self): self.fIsActive = bool(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_ACTIVE) >= 0.5) @@ -481,6 +509,9 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): self.peak_in.setMeterStyle(DigitalPeakMeter.STYLE_RNCBC) elif self.fSkinStyle.startswith("openav") or self.fSkinStyle == "zynfx": self.peak_in.setMeterStyle(DigitalPeakMeter.STYLE_OPENAV) + elif self.fSkinStyle == "tube": + self.peak_in.setMeterStyle(DigitalPeakMeter.STYLE_TUBE) + self.peak_in.setMeterLinesEnabled(False) if self.fPeaksInputCount == 0 and not isinstance(self, PluginSlot_Classic): self.peak_in.hide() @@ -496,6 +527,9 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): self.peak_out.setMeterStyle(DigitalPeakMeter.STYLE_RNCBC) elif self.fSkinStyle.startswith("openav") or self.fSkinStyle == "zynfx": self.peak_out.setMeterStyle(DigitalPeakMeter.STYLE_OPENAV) + elif self.fSkinStyle == "tube": + self.peak_out.setMeterStyle(DigitalPeakMeter.STYLE_TUBE) + self.peak_out.setMeterLinesEnabled(False) if self.fPeaksOutputCount == 0 and not isinstance(self, PluginSlot_Classic): self.peak_out.hide() @@ -530,7 +564,8 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): styleSheet2 = "background-image: url(:/bitmaps/background_%s.png);" % self.fSkinStyle else: styleSheet2 = "background-color: rgb(200, 200, 200);" - styleSheet2 += "background-image: url(:/bitmaps/background_noise1.png);" + if self.fSkinStyle not in ("classic"): + styleSheet2 += "background-image: url(:/bitmaps/background_noise1.png);" if not self.fDarkStyle: colorEnabled = "#111" @@ -551,12 +586,63 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): styleSheet += """ QComboBox#cb_presets, + QComboBox#cb_presets0, + QComboBox#cb_presets1, QLabel#label_audio_in, QLabel#label_audio_out, QLabel#label_midi { font-size: 10px; } """ self.setStyleSheet(styleSheet) + # ------------------------------------------------------------- + # Wet and Vol knobs on compacted slot + + # If "long" style not in "shorts" list, it will be matched to 0 ("default"). + skinNum = arrayIndex(skinListTweakable, self.fSkinStyle[: 3]) + skinName = skinListTweakable [skinNum] + + wetVolOnCompact = self.tweak(skinName, 'WetVolOnCompact', 0) + showDisabled = self.tweak(skinName, 'ShowDisabled', 0) + showOutputs = self.tweak(skinName, 'ShowOutputs', 0) + shortenLabels = self.tweak(skinName, 'ShortenLabels', 1) + btn3state = self.tweak(skinName, 'Button3Pos', 1) + + if isinstance(self, PluginSlot_Compact): + if self.fPluginInfo['hints'] & PLUGIN_CAN_DRYWET or showDisabled: + + + self.dial0 = ScalableDial(self, PARAMETER_DRYWET, 100, 1.0, 0.0, 1.0, "Dry/Wet", ScalableDial.CUSTOM_PAINT_MODE_CARLA_WET_MINI, -1, "%", self.fSkinStyle, whiteLabels, self.fTweaks) + self.dial0.setObjectName("dial0") + self.ui.horizontalLayout_2.insertWidget(6, self.dial0) + + if wetVolOnCompact: + self.dial0.setEnabled(bool(self.fPluginInfo['hints'] & PLUGIN_CAN_DRYWET)) + self.dial0.dragStateChanged.connect(self.slot_parameterDragStateChanged) + self.dial0.realValueChanged.connect(self.slot_dryWetChanged) + self.dial0.customContextMenuRequested.connect(self.slot_knobCustomMenu) + self.dial0.blockSignals(True) + self.dial0.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_DRYWET)) + self.dial0.blockSignals(False) + else: + self.dial0.hide() + + if self.fPluginInfo['hints'] & PLUGIN_CAN_VOLUME or showDisabled: + # self.dial1 = ScalableDial(self.ui.dial1, PARAMETER_VOLUME, 254, 1.0, 0.0, 1.27, "Volume", ScalableDial.CUSTOM_PAINT_MODE_CARLA_VOL_MINI, 0, "%", self.fSkinStyle, whiteLabels, self.fTweaks) + self.dial1 = ScalableDial(self, PARAMETER_VOLUME, 127, 1.0, 0.0, 1.27, "Volume", ScalableDial.CUSTOM_PAINT_MODE_CARLA_VOL_MINI, -1, "%", self.fSkinStyle, whiteLabels, self.fTweaks) + self.dial1.setObjectName("dial1") + self.ui.horizontalLayout_2.insertWidget(6, self.dial1) + + if wetVolOnCompact: + self.dial1.setEnabled(bool(self.fPluginInfo['hints'] & PLUGIN_CAN_VOLUME)) + self.dial1.dragStateChanged.connect(self.slot_parameterDragStateChanged) + self.dial1.realValueChanged.connect(self.slot_volumeChanged) + self.dial1.customContextMenuRequested.connect(self.slot_knobCustomMenu) + self.dial1.blockSignals(True) + self.dial1.setValue(self.host.get_internal_parameter_value(self.fPluginId, PARAMETER_VOLUME)) + self.dial1.blockSignals(False) + else: + self.dial1.hide() + # ------------------------------------------------------------- # Set-up parameters @@ -565,6 +651,11 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): index = 0 layout = self.w_knobs_left.layout() + + # Rainbow paint, default is deep red -> green. Span can be negative. + hueFrom = self.tweak(skinName, 'ColorFrom', -0.03) + hueSpan = self.tweak(skinName, 'ColorSpan', 0.4) + for i in range(parameterCount): # 50 should be enough for everybody, right? if index >= 50: @@ -573,64 +664,145 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): paramInfo = self.host.get_parameter_info(self.fPluginId, i) paramData = self.host.get_parameter_data(self.fPluginId, i) paramRanges = self.host.get_parameter_ranges(self.fPluginId, i) - isInteger = (paramData['hints'] & PARAMETER_IS_INTEGER) != 0 + default = self.host.get_default_parameter_value(self.fPluginId, i) + minimum = paramRanges['min'] + maximum = paramRanges['max'] + isEnabled = (paramData['hints'] & PARAMETER_IS_ENABLED) != 0 + isOutput = (paramData['type'] != PARAMETER_INPUT) + isBoolean = (paramData['hints'] & PARAMETER_IS_BOOLEAN) != 0 + isInteger = ((paramData['hints'] & PARAMETER_IS_INTEGER) != 0) or isBoolean - if paramData['type'] != PARAMETER_INPUT: - continue - if paramData['hints'] & PARAMETER_IS_BOOLEAN: - continue - if (paramData['hints'] & PARAMETER_IS_ENABLED) == 0: - continue - if (paramData['hints'] & PARAMETER_USES_SCALEPOINTS) != 0 and not isInteger: - # NOTE: we assume integer scalepoints are continuous - continue - if isInteger and paramRanges['max']-paramRanges['min'] <= 3: - continue if paramInfo['name'].startswith("unused"): + print("Carla: INFO: Parameter "+str(i)+" is Unused, so skipped.") continue - paramName = getParameterShortName(paramInfo['name']) + if not isEnabled: + print("Carla: INFO: Parameter "+str(i)+" is Disabled.") + if not showDisabled: + continue + + delta = maximum - minimum + if delta <= 0: + print("Carla: ERROR: Parameter "+str(i)+": Min, Max are same or wrong.") + return + + # NOTE: Booleans are mimic as isInteger with range [0 or 1]. + if btn3state: + isButton = (isInteger and (minimum == 0) and (maximum in (1, 2))) + else: + isButton = (isInteger and (minimum == 0) and (maximum == 1)) + + vuMeter = 0 + precision = 1 + if isOutput: + if not showOutputs: + continue + vuMeter = ((minimum == 0) and ((maximum == 1) or (maximum == 100)))\ + or (minimum == -maximum) # from -N to N, is it good to use VU ? + else: + # Integers have somewhat more coarse step + if isInteger: + while delta > 50: + delta = int(math.ceil(delta / 2)) + precision = delta + + # Floats are finer-step smoothed + else: + # Pretty steps for most common values, like 1-2-5-10 scales, + # still not in its final form. + while delta > 200: + # Mantissa is near 2.5 + is25 = int(abs((log10(delta) % 1) - log10(2.5)) < 0.001) + delta = delta / (2.0 + is25 * 0.5) + + while delta < 100: + # Mantissa is near 2.0 + is25 = int(abs((log10(delta) % 1) - log10(2.0)) < 0.001) + delta = delta * (2.0 + is25 * 0.5) + + precision = math.ceil(delta) + + if precision <= 0: # suddenly... + print("Carla: ERROR: Parameter "+str(i)+": Precision "+str(precision)+" is wrong!") + return + + if shortenLabels: + label = getParameterShortName(paramInfo['name']) + else: + label = paramInfo['name'] + + widget = ScalableDial(self, i, + precision, + default, + minimum, + maximum, + label, + skinNum * 16, + self.fColorHint, + paramInfo['unit'], + self.fSkinStyle, + whiteLabels, + self.fTweaks, + isInteger, + isButton, + isOutput, + vuMeter, + 1 ) # isVisible Experiment (index % 2) + + widget.setEnabled(isEnabled) - widget = ScalableDial(self, i) - widget.setLabel(paramName) - widget.setMinimum(paramRanges['min']) - widget.setMaximum(paramRanges['max']) widget.hide() - if isInteger: - widget.setPrecision(paramRanges['max']-paramRanges['min'], True) + scalePoints = [] + prefix = "" + suffix = "" + # NOTE: Issue #1983 + # if ((paramData['hints'] & PARAMETER_USES_SCALEPOINTS) != 0): + count = paramInfo['scalePointCount'] + if count: + for j in range(count): + scalePoints.append(self.host.get_parameter_scalepoint_info(self.fPluginId, i, j)) - setScalableDialStyle(widget, i, parameterCount, whiteLabels, self.fSkinStyle) + prefix, suffix = getPrefixSuffix(paramInfo['unit']) + widget.setScalePPS(sorted(scalePoints, key=operator.itemgetter("value")), prefix, suffix) index += 1 self.fParameterList.append([i, widget]) layout.addWidget(widget) - if self.w_knobs_right is not None and (self.fPluginInfo['hints'] & PLUGIN_CAN_DRYWET) != 0: - widget = ScalableDial(self, PARAMETER_DRYWET) - widget.setLabel("Dry/Wet") - widget.setMinimum(0.0) - widget.setMaximum(1.0) - setScalableDialStyle(widget, PARAMETER_DRYWET, 0, whiteLabels, self.fSkinStyle) + for i in range(index): + widget = layout.itemAt(i).widget() + if widget is not None: + coef = i/(index-1) if index > 1 else 0.5 # 0.5 = Midrange + hue = (hueFrom + coef * hueSpan) % 1.0 + widget.setCustomPaintColor(QColor.fromHslF(hue, 1, 0.5, 1)) - self.fParameterList.append([PARAMETER_DRYWET, widget]) - self.w_knobs_right.layout().addWidget(widget) - if self.w_knobs_right is not None and (self.fPluginInfo['hints'] & PLUGIN_CAN_VOLUME) != 0: - widget = ScalableDial(self, PARAMETER_VOLUME) - widget.setLabel("Volume") - widget.setMinimum(0.0) - widget.setMaximum(1.27) - setScalableDialStyle(widget, PARAMETER_VOLUME, 0, whiteLabels, self.fSkinStyle) + if self.w_knobs_right is not None: + if (self.fPluginInfo['hints'] & PLUGIN_CAN_DRYWET) != 0: + widget = ScalableDial(self, PARAMETER_DRYWET, 100, 1.0, 0.0, 1.0, "Dry/Wet", skinNum * 16 + ScalableDial.CUSTOM_PAINT_MODE_CARLA_WET, -1, "%", self.fSkinStyle, whiteLabels, self.fTweaks) - self.fParameterList.append([PARAMETER_VOLUME, widget]) - self.w_knobs_right.layout().addWidget(widget) + self.fParameterList.append([PARAMETER_DRYWET, widget]) + self.w_knobs_right.layout().addWidget(widget) + + if (self.fPluginInfo['hints'] & PLUGIN_CAN_VOLUME) != 0: + widget = ScalableDial(self, PARAMETER_VOLUME, 127, 1.0, 0.0, 1.27, "Volume", skinNum * 16 + ScalableDial.CUSTOM_PAINT_MODE_CARLA_VOL, -1, "%", self.fSkinStyle, whiteLabels, self.fTweaks) + + self.fParameterList.append([PARAMETER_VOLUME, widget]) + self.w_knobs_right.layout().addWidget(widget) + + if (self.fPluginInfo['hints'] & PLUGIN_CAN_PANNING) != 0: + if widget.getTweak('ShowPan', 0): + widget = ScalableDial(self, PARAMETER_PANNING, 100, 0.0, -1.0, 1.0, "Pan", skinNum * 16 + ScalableDial.CUSTOM_PAINT_MODE_CARLA_PAN, -1, "%", self.fSkinStyle, whiteLabels, self.fTweaks) + + self.fParameterList.append([PARAMETER_PANNING, widget]) + self.w_knobs_right.layout().addWidget(widget) for paramIndex, paramWidget in self.fParameterList: - paramWidget.setContextMenuPolicy(Qt.CustomContextMenu) - paramWidget.customContextMenuRequested.connect(self.slot_knobCustomMenu) - paramWidget.dragStateChanged.connect(self.slot_parameterDragStateChanged) - paramWidget.realValueChanged.connect(self.slot_parameterValueChanged) + if not paramWidget.fIsOutput: + paramWidget.customContextMenuRequested.connect(self.slot_knobCustomMenu) + paramWidget.dragStateChanged.connect(self.slot_parameterDragStateChanged) + paramWidget.realValueChanged.connect(self.slot_parameterValueChanged) paramWidget.blockSignals(True) paramWidget.setValue(self.host.get_internal_parameter_value(self.fPluginId, paramIndex)) paramWidget.blockSignals(False) @@ -724,6 +896,7 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): elif parameterId == PARAMETER_CTRL_CHANNEL: self.host.set_ctrl_channel(self.fPluginId, value) + self.setParameterValue(parameterId, value, True) self.fEditDialog.setParameterValue(parameterId, value) # ----------------------------------------------------------------- @@ -891,13 +1064,18 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): paramWidget.setVisible(hints & PLUGIN_CAN_DRYWET) elif paramIndex == PARAMETER_VOLUME: paramWidget.setVisible(hints & PLUGIN_CAN_VOLUME) + # jpka: FIXME i add it, but can't trigger it for test, so disable to prevent possible crashes. Maybe it don't needed. + # self.dial0.setVisible(hints & PLUGIN_CAN_DRYWET) + # self.dial1.setVisible(hints & PLUGIN_CAN_VOLUME) + # print("self.dial0.setVisible(hints & PLUGIN_CAN_DRYWET)") if self.b_gui is not None: self.b_gui.setEnabled(bool(hints & PLUGIN_HAS_CUSTOM_UI)) + # NOTE: self.fParameterList is empty when compacted. def editDialogParameterValueChanged(self, pluginId, parameterId, value): for paramIndex, paramWidget in self.fParameterList: - if paramIndex != parameterId: + if (paramIndex != parameterId) or paramWidget.fIsOutput: continue paramWidget.blockSignals(True) @@ -905,6 +1083,16 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): paramWidget.blockSignals(False) break + if isinstance(self, PluginSlot_Compact): + if (parameterId == PARAMETER_DRYWET) and (self.fPluginInfo['hints'] & PLUGIN_CAN_DRYWET): + self.dial0.blockSignals(True) + self.dial0.setValue(value) + self.dial0.blockSignals(False) + if (parameterId == PARAMETER_VOLUME) and (self.fPluginInfo['hints'] & PLUGIN_CAN_VOLUME): + self.dial1.blockSignals(True) + self.dial1.setValue(value) + self.dial1.blockSignals(False) + def editDialogProgramChanged(self, pluginId, index): if self.cb_presets is None: return @@ -997,6 +1185,23 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): self.fEditDialog.idleSlow() + # 7-seg displays are pretty effective, but added frame skip will make it even better. + self.slowTimer = (self.slowTimer + 1) % 2 # Half the FPS, win some CPU. + + # NOTE: self.fParameterList is empty when compacted. + for paramIndex, paramWidget in self.fParameterList: + if not paramWidget.fIsOutput: + continue + # VU displays are CPU effective, make it run faster than 7-seg displays. + # if (self.slowTimer > 0) and (not paramWidget.fIsVuOutput): + if (self.slowTimer > 0): + continue + + paramWidget.blockSignals(True) # TODO Is it required for output? + value = self.host.get_current_parameter_value(self.fPluginId, paramIndex) + paramWidget.setValue(value, False) + paramWidget.blockSignals(False) + # ----------------------------------------------------------------- def drawOutline(self, painter): @@ -1023,7 +1228,9 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): def updateParameterValues(self): for paramIndex, paramWidget in self.fParameterList: - if paramIndex < 0: + if paramIndex < 0: # DryWet and Volume + continue + if paramWidget.fIsOutput: continue paramWidget.blockSignals(True) @@ -1044,8 +1251,9 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): # Expand/Minimize and Tweaks actCompact = menu.addAction(self.tr("Expand") if isinstance(self, PluginSlot_Compact) else self.tr("Minimize")) - actColor = menu.addAction(self.tr("Change Color...")) - actSkin = menu.addAction(self.tr("Change Skin...")) + actColor = menu.addAction(self.tr("Change Color...")) + actColorRandom = menu.addAction(self.tr("Random Color")) + actSkin = menu.addAction(self.tr("Change Skin...")) menu.addSeparator() # ------------------------------------------------------------- @@ -1157,28 +1365,17 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): colorStr = "%i;%i;%i" % color gCarla.gui.changePluginColor(self.fPluginId, color, colorStr) - elif actSel == actSkin: - skinList = [ - "default", - "3bandeq", - "rncbc", - "calf_black", - "calf_blue", - "classic", - "openav-old", - "openav", - "zynfx", - "presets", - "mpresets", - ] - try: - index = skinList.index(self.fSkinStyle) - except: - index = 0 + elif actSel == actColorRandom: + hue = QColor(self.fSkinColor[0], self.fSkinColor[1], self.fSkinColor[2]).hueF() + color = QColor.fromHslF((hue + random.random()*0.5 + 0.25) % 1.0, 0.25, 0.125, 1).getRgb()[0:3] + colorStr = "%i;%i;%i" % color + gCarla.gui.changePluginColor(self.fPluginId, color, colorStr) + elif actSel == actSkin: skin = QInputDialog.getItem(self, self.tr("Change Skin"), self.tr("Change Skin to:"), - skinList, index, False) + skinList, arrayIndex(skinList, self.fSkinStyle), False) + if not all(skin): return gCarla.gui.changePluginSkin(self.fPluginId, skin[0]) @@ -1287,97 +1484,7 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): @pyqtSlot() def slot_knobCustomMenu(self): - sender = self.sender() - index = sender.fIndex - minimum = sender.fMinimum - maximum = sender.fMaximum - current = sender.fRealValue - label = sender.fLabel - - if index in (PARAMETER_NULL, PARAMETER_CTRL_CHANNEL) or index <= PARAMETER_MAX: - return - elif index in (PARAMETER_DRYWET, PARAMETER_VOLUME): - default = 1.0 - elif index == PARAMETER_BALANCE_LEFT: - default = -1.0 - elif index == PARAMETER_BALANCE_RIGHT: - default = 1.0 - elif index == PARAMETER_PANNING: - default = 0.0 - else: - default = self.host.get_default_parameter_value(self.fPluginId, index) - - if index < PARAMETER_NULL: - # show in integer percentage - textReset = self.tr("Reset (%i%%)" % round(default*100.0)) - textMinim = self.tr("Set to Minimum (%i%%)" % round(minimum*100.0)) - textMaxim = self.tr("Set to Maximum (%i%%)" % round(maximum*100.0)) - else: - # show in full float value - textReset = self.tr("Reset (%f)" % default) - textMinim = self.tr("Set to Minimum (%f)" % minimum) - textMaxim = self.tr("Set to Maximum (%f)" % maximum) - - menu = QMenu(self) - actReset = menu.addAction(textReset) - menu.addSeparator() - actMinimum = menu.addAction(textMinim) - actCenter = menu.addAction(self.tr("Set to Center")) - actMaximum = menu.addAction(textMaxim) - menu.addSeparator() - actSet = menu.addAction(self.tr("Set value...")) - - if index > PARAMETER_NULL or index not in (PARAMETER_BALANCE_LEFT, PARAMETER_BALANCE_RIGHT, PARAMETER_PANNING): - menu.removeAction(actCenter) - - actSelected = menu.exec_(QCursor.pos()) - - if actSelected == actSet: - if index < PARAMETER_NULL: - value, ok = QInputDialog.getInt(self, self.tr("Set value"), label, round(current*100), round(minimum*100), round(maximum*100), 1) - - if not ok: - return - - value = float(value)/100.0 - - else: - paramInfo = self.host.get_parameter_info(self.fPluginId, index) - paramRanges = self.host.get_parameter_ranges(self.fPluginId, index) - scalePoints = [] - - for i in range(paramInfo['scalePointCount']): - scalePoints.append(self.host.get_parameter_scalepoint_info(self.fPluginId, index, i)) - - prefix = "" - suffix = paramInfo['unit'].strip() - - if suffix == "(coef)": - prefix = "* " - suffix = "" - else: - suffix = " " + suffix - - dialog = CustomInputDialog(self, label, current, minimum, maximum, - paramRanges['step'], paramRanges['stepSmall'], scalePoints, prefix, suffix) - - if not dialog.exec_(): - return - - value = dialog.returnValue() - - elif actSelected == actMinimum: - value = minimum - elif actSelected == actMaximum: - value = maximum - elif actSelected == actReset: - value = default - elif actSelected == actCenter: - value = 0.0 - else: - return - - sender.setValue(value, True) + PluginEdit.slot_knobCustomMenu(self) # ----------------------------------------------------------------- @@ -1439,9 +1546,23 @@ class AbstractPluginSlot(QFrame, PluginEditParentMeta): if index < 0: break - curWidth += widget.width() + 4 + if not widget.getIsVisible(): + continue + + curWidth += widget.width() + RACK_KNOB_GAP - if curWidth + widget.width() * 2 + 8 < maxWidth: + if self.fTweaks.get('MoreSpace', 0): + if self.w_knobs_right is None: # calf + limit = curWidth + else: + if QT_VERSION < 0x60000: + limit = curWidth + self.w_knobs_right.getContentsMargins()[0] + 8 + else: + limit = curWidth + 4 + 8 + else: + limit = curWidth + 56 + 8 + + if limit < maxWidth: #if not widget.isVisible(): widget.show() continue @@ -1719,6 +1840,12 @@ class PluginSlot_Compact(AbstractPluginSlot): self.peak_in = self.ui.peak_in self.peak_out = self.ui.peak_out + if self.fTweaks.get('ShowProgramsOnCompact', 0): + insertProgramList(self, self.ui.layout_peaks, 0) + + if self.fTweaks.get('ShowMidiProgramsOnCompact', 0): + insertMidiProgramList(self, self.ui.layout_peaks, 0) + self.ready() # ----------------------------------------------------------------- @@ -1756,11 +1883,24 @@ class PluginSlot_Default(AbstractPluginSlot): self.w_knobs_right = self.ui.w_knobs_right self.spacer_knobs = self.ui.layout_bottom.itemAt(1).spacerItem() + if self.fTweaks.get('MoreSpace', 0): + self.ui.layout_bottom.setContentsMargins(0, 4, 0, 0) + + if self.fTweaks.get('ShowPrograms', 0): + # insertProgramList(self, self.ui.layout_top, 6) + insertProgramList(self, self.ui.layout_peaks, 0) + + if self.fTweaks.get('ShowMidiPrograms', 0): + # insertMidiProgramList(self, self.ui.layout_top, 6) + insertMidiProgramList(self, self.ui.layout_peaks, 0) + self.ready() # ----------------------------------------------------------------- def getFixedHeight(self): + if self.fSkinStyle == "tube": + return 98 return 80 # ----------------------------------------------------------------- @@ -1839,13 +1979,17 @@ class PluginSlot_Presets(AbstractPluginSlot): self.peak_in = self.ui.peak_in self.peak_out = self.ui.peak_out - if skinStyle == "zynfx": + # if skinStyle == "zynfx": # TODO jpka: TEST ing zynfx as normal tweakable skin + if False: self.setupZynFxParams() else: self.w_knobs_left = self.ui.w_knobs_left self.w_knobs_right = self.ui.w_knobs_right self.spacer_knobs = self.ui.layout_bottom.itemAt(1).spacerItem() + if self.fTweaks.get('MoreSpace', 0): + self.ui.layout_bottom.setContentsMargins(0, 4, 0, 0) + self.ready() if usingMidiPrograms: @@ -1855,6 +1999,8 @@ class PluginSlot_Presets(AbstractPluginSlot): # ------------------------------------------------------------- + # it works only for internal zyn builds, which are disabled by default + # (?) not for just manual "zynfx" skin selection def setupZynFxParams(self): parameterCount = min(self.host.get_parameter_count(self.fPluginId), 8) @@ -2001,6 +2147,46 @@ class PluginSlot_Presets(AbstractPluginSlot): # ------------------------------------------------------------------------------------------------------------ +def insertProgramList(self, layout, index): + count = self.host.get_program_count(self.fPluginId) + if count: + cb = QComboBox(None) + cb.setObjectName("cb_presets0") # use this stylesheet + + for i in range(count): + string = self.host.get_program_name(self.fPluginId, i) + + if len(string) == 0: + print("Carla: WARNING: Program List have zero length item.") + return + + cb.addItem(string) + + layout.insertWidget(index, cb) + cb.setCurrentIndex(self.host.get_current_program_index(self.fPluginId)) + cb.currentIndexChanged.connect(self.slot_programChanged) + +def insertMidiProgramList(self, layout, index): + count = self.host.get_midi_program_count(self.fPluginId) + if count: + cb = QComboBox(None) + cb.setObjectName("cb_presets1") # use this stylesheet + + for i in range(count): + string = self.host.get_midi_program_data(self.fPluginId, i)['name'] + + if len(string) == 0: + print("Carla: WARNING: MIDI Program List have zero length item.") + return + + cb.addItem(string) + + layout.insertWidget(index, cb) + cb.setCurrentIndex(self.host.get_current_midi_program_index(self.fPluginId)) + cb.currentIndexChanged.connect(self.slot_midiProgramChanged) + +# ------------------------------------------------------------------------------------------------------------ + def getColorAndSkinStyle(host, pluginId): pluginInfo = host.get_plugin_info(pluginId) pluginName = host.get_real_plugin_name(pluginId) @@ -2018,9 +2204,9 @@ def getColorAndSkinStyle(host, pluginId): # Samplers if pluginInfo['type'] == PLUGIN_SF2: - return (colorCategory, "sf2") + return (colorCategory, "mpresets") if pluginInfo['type'] == PLUGIN_SFZ: - return (colorCategory, "sfz") + return (colorCategory, "mpresets") # Calf if pluginName.split(" ", 1)[0].lower() == "calf": @@ -2032,6 +2218,10 @@ def getColorAndSkinStyle(host, pluginId): if pluginMaker == "OpenAV": return (colorNone, "openav") + # Tube + if "tube" in pluginLabel: + return (colorCategory, "tube") + # ZynFX if pluginInfo['type'] == PLUGIN_INTERNAL: if pluginLabel.startswith("zyn") and pluginInfo['category'] != PLUGIN_CATEGORY_SYNTH: @@ -2042,7 +2232,8 @@ def getColorAndSkinStyle(host, pluginId): return (colorNone, "zynfx") if pluginInfo['type'] == PLUGIN_LV2: - if pluginLabel.startswith("http://kxstudio.sf.net/carla/plugins/zyn") and pluginName != "ZynAddSubFX": + # if pluginLabel.startswith("http://kxstudio.sf.net/carla/plugins/zyn") and pluginName != "ZynAddSubFX": + if pluginLabel.startswith("http://kxstudio.sf.net/carla/plugins/zyn") and pluginName != "ZynAddSubFX" or "zyn" in pluginLabel: # jpka: TEST ing zynfx as normal tweakable skin return (colorNone, "zynfx") # Presets diff --git a/source/frontend/carla_widgets.py b/source/frontend/carla_widgets.py index 0a90cdd12..3a8e2e1ee 100755 --- a/source/frontend/carla_widgets.py +++ b/source/frontend/carla_widgets.py @@ -14,7 +14,7 @@ from qt_compat import qt_config if qt_config == 5: from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QByteArray - from PyQt5.QtGui import QCursor, QIcon, QPalette, QPixmap + from PyQt5.QtGui import QCursor, QIcon, QPalette, QPixmap, QFont from PyQt5.QtWidgets import ( QDialog, QFileDialog, @@ -24,10 +24,18 @@ if qt_config == 5: QScrollArea, QVBoxLayout, QWidget, + QGraphicsScene, + QGraphicsTextItem, + QGraphicsView, + QTableWidget, + QTableWidgetItem, + QHeaderView, + QLabel, + QSizePolicy, ) elif qt_config == 6: from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QByteArray - from PyQt6.QtGui import QCursor, QIcon, QPalette, QPixmap + from PyQt6.QtGui import QCursor, QIcon, QPalette, QPixmap, QFont from PyQt6.QtWidgets import ( QDialog, QFileDialog, @@ -37,6 +45,14 @@ elif qt_config == 6: QScrollArea, QVBoxLayout, QWidget, + QGraphicsScene, + QGraphicsTextItem, + QGraphicsView, + QTableWidget, + QTableWidgetItem, + QHeaderView, + QLabel, + QSizePolicy, ) # ------------------------------------------------------------------------------------------------------------ @@ -72,6 +88,7 @@ from carla_backend import ( PLUGIN_OPTION_SEND_ALL_SOUND_OFF, PLUGIN_OPTION_SEND_PROGRAM_CHANGES, PLUGIN_OPTION_SKIP_SENDING_NOTES, + PARAMETER_NULL, PARAMETER_DRYWET, PARAMETER_VOLUME, PARAMETER_BALANCE_LEFT, @@ -84,6 +101,7 @@ from carla_backend import ( PARAMETER_USES_SCALEPOINTS, PARAMETER_USES_CUSTOM_TEXT, PARAMETER_CAN_BE_CV_CONTROLLED, + parameterHintsText, PARAMETER_INPUT, PARAMETER_OUTPUT, CONTROL_INDEX_NONE, CONTROL_INDEX_MIDI_PITCHBEND, @@ -94,6 +112,7 @@ from carla_backend import ( from carla_shared import ( MIDI_CC_LIST, MAX_MIDI_CC_LIST_ITEM, countDecimalPoints, + strLim, fontMetricsHorizontalAdvance, setUpSignals, gCarla @@ -103,6 +122,8 @@ from carla_utils import getPluginTypeAsString from widgets.collapsablewidget import CollapsibleBox from widgets.pixmapkeyboard import PixmapKeyboardHArea +from widgets.paramspinbox import CustomInputDialog, ParamSpinBox +from widgets.scalabledial import ScalableDial # ------------------------------------------------------------------------------------------------------------ # Carla GUI defines @@ -119,6 +140,7 @@ class PluginParameter(QWidget): mappedControlChanged = pyqtSignal(int, int) mappedRangeChanged = pyqtSignal(int, float, float) midiChannelChanged = pyqtSignal(int, int) + knobVisibilityChanged = pyqtSignal(int, int) valueChanged = pyqtSignal(int, float) def __init__(self, parent, host, pInfo, pluginId, tabIndex): @@ -141,6 +163,7 @@ class PluginParameter(QWidget): self.fParameterId = pInfo['index'] self.fPluginId = pluginId self.fTabIndex = tabIndex + self.fKnobVisible = True # ------------------------------------------------------------- # Set-up GUI @@ -157,6 +180,7 @@ class PluginParameter(QWidget): self.ui.widget.setStep(pInfo['step']) self.ui.widget.setStepSmall(pInfo['stepSmall']) self.ui.widget.setStepLarge(pInfo['stepLarge']) + # NOTE: Issue #1983 self.ui.widget.setScalePoints(pInfo['scalePoints'], bool(pHints & PARAMETER_USES_SCALEPOINTS)) if pInfo['comment']: @@ -536,39 +560,21 @@ class PluginEdit(QDialog): labelPluginFont.setWeight(75) self.ui.label_plugin.setFont(labelPluginFont) - self.ui.dial_drywet.setCustomPaintMode(self.ui.dial_drywet.CUSTOM_PAINT_MODE_CARLA_WET) - self.ui.dial_drywet.setImage(3) - self.ui.dial_drywet.setLabel("Dry/Wet") - self.ui.dial_drywet.setMinimum(0.0) - self.ui.dial_drywet.setMaximum(1.0) + pluginHints = self.host.get_plugin_info(self.fPluginId)['hints'] + + self.ui.dial_drywet = ScalableDial(self.ui.dial_drywet, PARAMETER_DRYWET, 100, 1.0, 0.0, 1.0, "Dry/Wet", ScalableDial.CUSTOM_PAINT_MODE_CARLA_WET) self.ui.dial_drywet.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_DRYWET)) - self.ui.dial_vol.setCustomPaintMode(self.ui.dial_vol.CUSTOM_PAINT_MODE_CARLA_VOL) - self.ui.dial_vol.setImage(3) - self.ui.dial_vol.setLabel("Volume") - self.ui.dial_vol.setMinimum(0.0) - self.ui.dial_vol.setMaximum(1.27) + self.ui.dial_vol = ScalableDial(self.ui.dial_vol, PARAMETER_VOLUME, 127, 1.0, 0.0, 1.27, "Volume", ScalableDial.CUSTOM_PAINT_MODE_CARLA_VOL) self.ui.dial_vol.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_VOLUME)) - self.ui.dial_b_left.setCustomPaintMode(self.ui.dial_b_left.CUSTOM_PAINT_MODE_CARLA_L) - self.ui.dial_b_left.setImage(4) - self.ui.dial_b_left.setLabel("L") - self.ui.dial_b_left.setMinimum(-1.0) - self.ui.dial_b_left.setMaximum(1.0) + self.ui.dial_b_left = ScalableDial(self.ui.dial_b_left, PARAMETER_BALANCE_LEFT, 100, -1.0, -1.0, 1.0, "L", ScalableDial.CUSTOM_PAINT_MODE_CARLA_L) self.ui.dial_b_left.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_BALANCE_LEFT)) - self.ui.dial_b_right.setCustomPaintMode(self.ui.dial_b_right.CUSTOM_PAINT_MODE_CARLA_R) - self.ui.dial_b_right.setImage(4) - self.ui.dial_b_right.setLabel("R") - self.ui.dial_b_right.setMinimum(-1.0) - self.ui.dial_b_right.setMaximum(1.0) + self.ui.dial_b_right = ScalableDial(self.ui.dial_b_right, PARAMETER_BALANCE_RIGHT, 100, 1.0, -1.0, 1.0, "R", ScalableDial.CUSTOM_PAINT_MODE_CARLA_R) self.ui.dial_b_right.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_BALANCE_RIGHT)) - self.ui.dial_pan.setCustomPaintMode(self.ui.dial_b_right.CUSTOM_PAINT_MODE_CARLA_PAN) - self.ui.dial_pan.setImage(4) - self.ui.dial_pan.setLabel("Pan") - self.ui.dial_pan.setMinimum(-1.0) - self.ui.dial_pan.setMaximum(1.0) + self.ui.dial_pan = ScalableDial(self.ui.dial_pan, PARAMETER_PANNING, 100, 0, -1.0, 1.0, "Pan", ScalableDial.CUSTOM_PAINT_MODE_CARLA_PAN) self.ui.dial_pan.setValue(host.get_internal_parameter_value(pluginId, PARAMETER_PANNING)) self.ui.sb_ctrl_channel.setValue(self.fControlChannel+1) @@ -582,10 +588,10 @@ class PluginEdit(QDialog): self.ui.scrollArea.setVisible(False) # todo - self.ui.rb_balance.setEnabled(False) - self.ui.rb_balance.setVisible(False) - self.ui.rb_pan.setEnabled(False) - self.ui.rb_pan.setVisible(False) + # self.ui.rb_balance.setEnabled(False) + # self.ui.rb_balance.setVisible(False) + # self.ui.rb_pan.setEnabled(False) + # self.ui.rb_pan.setVisible(False) flags = self.windowFlags() flags &= ~Qt.WindowContextHelpButtonHint @@ -901,7 +907,6 @@ class PluginEdit(QDialog): break paramData = self.host.get_parameter_data(self.fPluginId, i) - if paramData['type'] not in (PARAMETER_INPUT, PARAMETER_OUTPUT): unusedParameters += 1 continue @@ -974,6 +979,12 @@ class PluginEdit(QDialog): self._createParameterWidgets(PARAMETER_INPUT, paramInputListFull, self.tr("Parameters")) self._createParameterWidgets(PARAMETER_OUTPUT, paramOutputListFull, self.tr("Outputs")) + # Create full parameter list table tab + self._createParameterXrayTab(self.tr("XRay")) + + # Create experimental description tab WORKINPROGRESS NOTE discussions/1967, pull/1961 + self._createDescriptionTab(self.tr("Description")) + # Restore tab state if tabIndex < self.ui.tabWidget.count(): self.ui.tabWidget.setCurrentIndex(tabIndex) @@ -1474,69 +1485,67 @@ class PluginEdit(QDialog): @pyqtSlot() def slot_knobCustomMenu(self): - sender = self.sender() - knobName = sender.objectName() - - if knobName == "dial_drywet": - minimum = 0.0 - maximum = 1.0 - default = 1.0 - label = "Dry/Wet" - elif knobName == "dial_vol": - minimum = 0.0 - maximum = 1.27 - default = 1.0 - label = "Volume" - elif knobName == "dial_b_left": - minimum = -1.0 - maximum = 1.0 - default = -1.0 - label = "Balance-Left" - elif knobName == "dial_b_right": - minimum = -1.0 - maximum = 1.0 - default = 1.0 - label = "Balance-Right" - elif knobName == "dial_pan": - minimum = -1.0 - maximum = 1.0 - default = 0.0 - label = "Panning" + # jpka: NOTE now Edit knobs are also know their constraints, so it's worth to set values as normal ones. + sender = self.sender() + index = sender.fIndex + minimum = sender.fMinimum + maximum = sender.fMaximum + current = sender.fRealValue + label = sender.fLabel + default = sender.fDefault + unit = sender.fUnit + step = stepSmall = 1.0 + + if index < PARAMETER_NULL: + percent = 100.0 else: - minimum = 0.0 - maximum = 1.0 - default = 0.5 - label = "Unknown" + percent = 1 + + textReset = self.tr("Reset (" + strLim(default * percent) + unit + ")\tR, Middle click") + textMinim = self.tr("Set to Minimum (" + strLim(minimum * percent) + unit + ")\t0") + textMaxim = self.tr("Set to Maximum (" + strLim(maximum * percent) + unit + ")\tEnd") + + if sender.fIsButton: + editHotKey = "E" + else: + editHotKey = "Enter, Double click" menu = QMenu(self) - actReset = menu.addAction(self.tr("Reset (%i%%)" % (default*100))) + actReset = menu.addAction(textReset) menu.addSeparator() - actMinimum = menu.addAction(self.tr("Set to Minimum (%i%%)" % (minimum*100))) - actCenter = menu.addAction(self.tr("Set to Center")) - actMaximum = menu.addAction(self.tr("Set to Maximum (%i%%)" % (maximum*100))) + actMinimum = menu.addAction(textMinim) + actCenter = menu.addAction(self.tr("Set to Center\t5")) + actMaximum = menu.addAction(textMaxim) menu.addSeparator() - actSet = menu.addAction(self.tr("Set value...")) + actSet = menu.addAction(self.tr("Set value...\t" + editHotKey)) - if label not in ("Balance-Left", "Balance-Right", "Panning"): + if index > PARAMETER_NULL or index not in (PARAMETER_BALANCE_LEFT, PARAMETER_BALANCE_RIGHT, PARAMETER_PANNING): menu.removeAction(actCenter) actSelected = menu.exec_(QCursor.pos()) if actSelected == actSet: - current = minimum + (maximum-minimum)*(float(sender.value())/10000) - value, ok = QInputDialog.getInt(self, - self.tr("Set value"), - label, - round(current*100.0), - round(minimum*100.0), - round(maximum*100.0), - 1) - if ok: - value = float(value)/100.0 + paramInfo = self.host.get_parameter_info(self.fPluginId, index) + paramRanges = self.host.get_parameter_ranges(self.fPluginId, index) + scalePoints = [] - if not ok: + for i in range(paramInfo['scalePointCount']): + scalePoints.append(self.host.get_parameter_scalepoint_info(self.fPluginId, index, i)) + + if sender.fIsInteger: + step = max(1, int((maximum - minimum)/100)) + stepSmall = max(1, int(step/10)) + else: + step = paramRanges['step'] * percent + stepSmall = paramRanges['stepSmall'] * percent + + dialog = CustomInputDialog(self, label, current * percent, minimum * percent, maximum * percent, step, stepSmall, scalePoints, "", "", unit) + + if not dialog.exec_(): return + value = dialog.returnValue() / percent + elif actSelected == actMinimum: value = minimum elif actSelected == actMaximum: @@ -1605,13 +1614,15 @@ class PluginEdit(QDialog): scrollAreaLayout = QVBoxLayout(scrollAreaWidget) scrollAreaLayout.setSpacing(3) + expandBox = (len(paramList) < 50) + for paramInfo in paramList: groupName = paramInfo['groupName'] if groupName: groupSymbol, groupName = groupName.split(":",1) groupLayout, groupWidget = groupWidgets.get(groupSymbol, (None, None)) if groupLayout is None: - groupWidget = CollapsibleBox(groupName, scrollAreaWidget) + groupWidget = CollapsibleBox(groupName, scrollAreaWidget, expandBox) groupLayout = groupWidget.getContentLayout() groupWidget.setPalette(palette2) scrollAreaLayout.addWidget(groupWidget) @@ -1678,6 +1689,140 @@ class PluginEdit(QDialog): #------------------------------------------------------------------ + # NOTE To speed things up, displayed data is not realtime. Reopen project to expose last changes. + def _createParameterXrayTab(self, tabName): + # How simple would be fit a value into cell? Yet to be as fast as we can. + def strFit(value): + if isinstance(value, str): + return value + # For 'carla-control', but anyway scalePoints are not work. #1984 + elif isinstance(value, list): + return str(value) # It's [] + elif abs(value) >= 1E8: + return '{:.3e}'.format(value) + elif value == int(value): # Zero falls here + return str(int(value)) + else: + return strLim(value) + + def strLineWrap(string, cut): + result = '' + while len(string) > cut: # FIXME Optimize me! + result += string[:cut] + '\n' + string = string[cut:] + result += string + return result + + def addCell(section, name, string, toolTip = ''): + if x == table.columnCount(): + table.insertColumn(x) + # jpka: FIXME Here we need vertical text. But impossible, no working examples. + # Only untested https://stackoverflow.com/questions/52162125/ + nameWrapped = section + '\n' + strLineWrap(name, 6) + table.setHorizontalHeaderItem(x, QTableWidgetItem(nameWrapped)) + table.horizontalHeader().setSectionResizeMode(x, QHeaderView.ResizeToContents) + + if y == table.rowCount(): + table.insertRow(y) + table.verticalHeader().setSectionResizeMode(y, QHeaderView.ResizeToContents) + + item = QTableWidgetItem(string) + if toolTip: + item.setToolTip(toolTip) + table.setItem(y, x, item) + return + + table = QTableWidget(self) + table.setObjectName("table") + table.setRowCount(1) + # table.setToolTipDuration(2000) + + parameterCount = self.host.get_parameter_count(self.fPluginId) + if parameterCount <= 0: + return + + y = 0 + for i in range(parameterCount): + x = 0 + param = self.host.get_parameter_data(self.fPluginId, i) + for name in param: + value = param[name] + if (name == 'type') and (value in (1, 2,)): + addCell('Data', name, str(value) + (' in',' out')[value - 1]) + elif (name == 'hints'): + # toolTip = '' + bin(value)[2:] + hints = '' + for bit in range(len(parameterHintsText)): + if (value & int(2**(bit-1))): + hint = parameterHintsText[bit-1] + # toolTip += '
' + hint + hints += ', ' + hint + addCell('Data', name, str(value)) # , toolTip + '') + x += 1 + addCell('Hints', '', hints[2:]) + else: + addCell('Data', name, strFit(value)) + x += 1 + + param = self.host.get_parameter_ranges(self.fPluginId, i) + for name in param: + addCell('Ranges', name, strFit(param[name])) + x += 1 + + param = self.host.get_parameter_info(self.fPluginId, i) + for name in param: + addCell('Info', name, strFit(param[name])) + x += 1 + + strScalePoints = '' + for j in range(param['scalePointCount']): + scalePointInfo = self.host.get_parameter_scalepoint_info(self.fPluginId, i, j) + strScalePoints += (strFit(scalePointInfo['value']) + ':' + scalePointInfo['label'] + ',') + + if strScalePoints: + addCell('Scalepoint_info', 'Scale Points', strLineWrap(strScalePoints[:len(strScalePoints)-1], 80)) + x += 1 + + y += 1 + + self.ui.tabWidget.addTab(table, tabName) + # self.ui.tabWidget.setToolTipDuration(2000) + + #------------------------------------------------------------------ + + def _createDescriptionTab(self, tabPageName): +# jpka: To be filled from 'rdfs:comment' + strDescr = "To be filled from rdfs:comment" + + realPluginName = self.host.get_real_plugin_name(self.fPluginId) + labelURI = self.fPluginInfo['label'] + + strLoadState = "" + programCount = self.host.get_program_count(self.fPluginId) + if programCount > 0: + strLoadState = '

'\ + 'Note: This plugin collected some presets for you.
'\ + 'Use Edit tab, then Load State button.
' + + scene = QGraphicsScene(self) + text = QGraphicsTextItem("",None) + text.setTextInteractionFlags(Qt.TextSelectableByMouse) + text.setTextWidth(600); +# text.setFont(QFont("Arial, 16")) # NOTE: All Qt sizes are in Pt; real px~=4/3Pt. + text.setHtml('\ +

' + realPluginName + '


'\ + '' + labelURI + '

'\ + '
' + strDescr + '
' +\ + strLoadState +\ + ''); + + scene.addItem(text) + view = QGraphicsView(scene, self) + + self.ui.tabWidget.addTab(view, tabPageName) + + #------------------------------------------------------------------ + def testTimer(self): self.fIdleTimerId = self.startTimer(50) diff --git a/source/frontend/widgets/collapsablewidget.py b/source/frontend/widgets/collapsablewidget.py index 75e86b25a..fa42f4132 100644 --- a/source/frontend/widgets/collapsablewidget.py +++ b/source/frontend/widgets/collapsablewidget.py @@ -33,7 +33,7 @@ class QToolButtonWithMouseTracking(QToolButton): QToolButton.leaveEvent(self, event) class CollapsibleBox(QFrame): - def __init__(self, title, parent): + def __init__(self, title, parent, startsExpanded = True): QFrame.__init__(self, parent) self.setFrameShape(QFrame.StyledPanel) @@ -42,10 +42,10 @@ class CollapsibleBox(QFrame): self.toggle_button = QToolButtonWithMouseTracking(self) self.toggle_button.setText(title) self.toggle_button.setCheckable(True) - self.toggle_button.setChecked(True) + self.toggle_button.setChecked(startsExpanded) self.toggle_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) - self.toggle_button.setArrowType(Qt.DownArrow) + # self.toggle_button.setArrowType(Qt.DownArrow) # Not deleted, just moved self.toggle_button.toggled.connect(self.toolButtonPressed) self.content_area = QWidget(self) @@ -59,6 +59,8 @@ class CollapsibleBox(QFrame): lay.addWidget(self.toggle_button) lay.addWidget(self.content_area) + self.toolButtonPressed(startsExpanded) # Set initial state + @pyqtSlot(bool) def toolButtonPressed(self, toggled): self.content_area.setVisible(toggled) diff --git a/source/frontend/widgets/commondial.py b/source/frontend/widgets/commondial.py index a615cdeae..d24e6dc92 100644 --- a/source/frontend/widgets/commondial.py +++ b/source/frontend/widgets/commondial.py @@ -1,22 +1,26 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: 2011-2024 Filipe Coelho +# SPDX-FileCopyrightText: 2011-2025 Filipe Coelho # SPDX-License-Identifier: GPL-2.0-or-later # --------------------------------------------------------------------------------------------------------------------- # Imports (Global) -from math import isnan +from math import isnan, log10 from qt_compat import qt_config if qt_config == 5: - from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPointF, QRectF + from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QRectF, QEvent, QTimer from PyQt5.QtGui import QColor, QFont, QLinearGradient, QPainter - from PyQt5.QtWidgets import QDial + from PyQt5.QtWidgets import QWidget, QToolTip, QInputDialog elif qt_config == 6: - from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPointF, QRectF + from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QRectF, QEvent, QTimer from PyQt6.QtGui import QColor, QFont, QLinearGradient, QPainter - from PyQt6.QtWidgets import QDial + from PyQt6.QtWidgets import QWidget, QToolTip, QInputDialog + +from carla_shared import strLim +from widgets.paramspinbox import CustomInputDialog +from carla_backend import PARAMETER_NULL, PARAMETER_DRYWET, PARAMETER_VOLUME, PARAMETER_BALANCE_LEFT, PARAMETER_BALANCE_RIGHT, PARAMETER_PANNING # --------------------------------------------------------------------------------------------------------------------- # Widget Class @@ -25,7 +29,7 @@ elif qt_config == 6: #def updateSizes(self): #def paintDial(self, painter): -class CommonDial(QDial): +class CommonDial(QWidget): # enum CustomPaintMode CUSTOM_PAINT_MODE_NULL = 0 # default (NOTE: only this mode has label gradient) CUSTOM_PAINT_MODE_CARLA_WET = 1 # color blue-green gradient (reserved #3) @@ -33,9 +37,11 @@ class CommonDial(QDial): CUSTOM_PAINT_MODE_CARLA_L = 3 # color yellow (reserved #4) CUSTOM_PAINT_MODE_CARLA_R = 4 # color yellow (reserved #4) CUSTOM_PAINT_MODE_CARLA_PAN = 5 # color yellow (reserved #3) - CUSTOM_PAINT_MODE_COLOR = 6 # color, selectable (reserved #3) - CUSTOM_PAINT_MODE_ZITA = 7 # custom zita knob (reserved #6) + CUSTOM_PAINT_MODE_CARLA_FORTH = 6 # Experimental + CUSTOM_PAINT_MODE_COLOR = 7 # May be deprecated (unless zynfx internal mode) CUSTOM_PAINT_MODE_NO_GRADIENT = 8 # skip label gradient + CUSTOM_PAINT_MODE_CARLA_WET_MINI = 9 # for compacted slot + CUSTOM_PAINT_MODE_CARLA_VOL_MINI = 10 # for compacted slot # enum Orientation HORIZONTAL = 0 @@ -51,16 +57,37 @@ class CommonDial(QDial): dragStateChanged = pyqtSignal(bool) realValueChanged = pyqtSignal(float) - def __init__(self, parent, index): - QDial.__init__(self, parent) + def __init__(self, parent, index, precision, default, minimum, maximum, label, paintMode, colorHint, unit, skinStyle, whiteLabels, tweaks, isInteger, isButton, isOutput, isVuOutput, isVisible): + QWidget.__init__(self, parent) + + self.fIndex = index + self.fPrecision = precision + self.fDefault = default + self.fMinimum = minimum + self.fMaximum = maximum + self.fCustomPaintMode = paintMode + self.fColorHint = colorHint + self.fUnit = unit + self.fSkinStyle = skinStyle + self.fWhiteLabels = whiteLabels + self.fTweaks = tweaks + self.fIsInteger = isInteger + self.fIsButton = isButton + self.fIsOutput = isOutput + self.fIsVuOutput = isVuOutput + self.fIsVisible = isVisible self.fDialMode = self.MODE_LINEAR - self.fMinimum = 0.0 - self.fMaximum = 1.0 + self.fLabel = label + self.fLastLabel = "" self.fRealValue = 0.0 - self.fPrecision = 10000 - self.fIsInteger = False + self.fLastValue = self.fDefault + + self.fScalePoints = [] + self.fNumScalePoints = 0 + self.fScalePointsPrefix = "" + self.fScalePointsSuffix = "" self.fIsHovered = False self.fIsPressed = False @@ -69,9 +96,6 @@ class CommonDial(QDial): self.fLastDragPos = None self.fLastDragValue = 0.0 - self.fIndex = index - - self.fLabel = "" self.fLabelPos = QPointF(0.0, 0.0) self.fLabelFont = QFont(self.font()) self.fLabelFont.setPixelSize(8) @@ -97,75 +121,79 @@ class CommonDial(QDial): self.fLabelGradientRect = QRectF(0.0, 0.0, 0.0, 0.0) - self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_NULL self.fCustomPaintColor = QColor(0xff, 0xff, 0xff) - # Fake internal value, custom precision - QDial.setMinimum(self, 0) - QDial.setMaximum(self, self.fPrecision) - QDial.setValue(self, 0) + self.addContrast = int(bool(self.getTweak('HighContrast', 0))) + self.colorFollow = bool(self.getTweak('ColorFollow', 0)) + self.knobPusheable = bool(self.getTweak('WetVolPush', 0)) + self.displayTooltip = bool(self.getTweak('Tooltips', 1)) - self.valueChanged.connect(self.slot_valueChanged) + # We have two group of knobs, non-repaintable (like in Edit dialog) and normal. + # For non-repaintable, we init sizes/color once here; + # for normals, it should be (re)inited separately: we do not init it here + # to save CPU, some parameters are not known yet, repaint need anyway. + if self.fColorHint == -1: + self.updateSizes() + + self.update() + +# self.valueChanged.connect(self.slot_valueChanged) # FIXME def forceWhiteLabelGradientText(self): self.fLabelGradientColor1 = QColor(0, 0, 0, 255) self.fLabelGradientColor2 = QColor(0, 0, 0, 0) self.fLabelGradientColorT = [Qt.white, Qt.darkGray] - def setLabelColor(self, enabled, disabled): - self.fLabelGradientColor1 = QColor(0, 0, 0, 255) - self.fLabelGradientColor2 = QColor(0, 0, 0, 0) - self.fLabelGradientColorT = [enabled, disabled] + # def setLabelColor(self, enabled, disabled): + # self.fLabelGradientColor1 = QColor(0, 0, 0, 255) + # self.fLabelGradientColor2 = QColor(0, 0, 0, 0) + # self.fLabelGradientColorT = [enabled, disabled] def getIndex(self): return self.fIndex - def setIndex(self, index): - self.fIndex = index - - def setPrecision(self, value, isInteger): - self.fPrecision = value - self.fIsInteger = isInteger - QDial.setMaximum(self, int(value)) - - def setMinimum(self, value): - self.fMinimum = value - - def setMaximum(self, value): - self.fMaximum = value - def rvalue(self): return self.fRealValue + def pushLabel(self, label): + if self.fLastLabel == "": + self.fLastLabel = self.fLabel + self.fLabel = label + self.updateSizes() + self.update() + + def popLabel(self): + if not (self.fLastLabel == ""): + self.fLabel = self.fLastLabel + self.fLastLabel = "" + self.updateSizes() + self.update() + + def setScalePPS(self, scalePoints, prefix, suffix): + self.fScalePoints = scalePoints + self.fNumScalePoints = len(self.fScalePoints) + self.fScalePointsPrefix = prefix + self.fScalePointsSuffix = suffix + def setValue(self, value, emitSignal=False): if self.fRealValue == value or isnan(value): return - if value <= self.fMinimum: - qtValue = 0 + if (not self.fIsOutput) and value <= self.fMinimum: self.fRealValue = self.fMinimum - elif value >= self.fMaximum: - qtValue = int(self.fPrecision) + elif (not self.fIsOutput) and value >= self.fMaximum: self.fRealValue = self.fMaximum + elif self.fIsInteger or (abs(value - int(value)) < 1e-8): # tiny "notch" + self.fRealValue = round(value) + else: - qtValue = round(float(value - self.fMinimum) / float(self.fMaximum - self.fMinimum) * self.fPrecision) self.fRealValue = value - # Block change signal, we'll handle it ourselves - self.blockSignals(True) - QDial.setValue(self, qtValue) - self.blockSignals(False) - if emitSignal: self.realValueChanged.emit(self.fRealValue) - def setCustomPaintMode(self, paintMode): - if self.fCustomPaintMode == paintMode: - return - - self.fCustomPaintMode = paintMode self.update() def setCustomPaintColor(self, color): @@ -173,76 +201,262 @@ class CommonDial(QDial): return self.fCustomPaintColor = color + self.updateSizes() self.update() - def setLabel(self, label): - if self.fLabel == label: - return + def getTweak(self, tweakName, default): + return self.fTweaks.get(self.fSkinStyle + tweakName, self.fTweaks.get(tweakName, default)) - self.fLabel = label - self.updateSizes() - self.update() + def getIsVisible(self): + # print (self.fIsVisible) + return self.fIsVisible @pyqtSlot(int) def slot_valueChanged(self, value): self.fRealValue = float(value)/self.fPrecision * (self.fMaximum - self.fMinimum) + self.fMinimum self.realValueChanged.emit(self.fRealValue) + # jpka: TODO should be replaced by common dialog, but + # PluginEdit.slot_knobCustomMenu(...) - not found, import not work. + # So this is copy w/o access to 'step's. + def knobCustomInputDialog(self): + if self.fIndex < PARAMETER_NULL: + percent = 100.0 + else: + percent = 1 + + if self.fIsInteger: + step = max(1, int((self.fMaximum - self.fMinimum)/100)) + stepSmall = max(1, int(step/10)) + else: + step = 10 ** (round(log10((self.fMaximum - self.fMinimum) * percent))-2) + stepSmall = step / 100 + + dialog = CustomInputDialog(self, self.fLabel, self.fRealValue * percent, self.fMinimum * percent, self.fMaximum * percent, step, stepSmall, self.fScalePoints, "", "", self.fUnit) + if not dialog.exec_(): + return + + self.setValue(dialog.returnValue() / percent, True) + + def enterEvent(self, event): + self.setFocus() self.fIsHovered = True if self.fHoverStep == self.HOVER_MIN: self.fHoverStep = self.HOVER_MIN + 1 - QDial.enterEvent(self, event) + self.update() + def leaveEvent(self, event): self.fIsHovered = False if self.fHoverStep == self.HOVER_MAX: self.fHoverStep = self.HOVER_MAX - 1 - QDial.leaveEvent(self, event) + self.update() + + + def nextScalePoint(self): + for i in range(self.fNumScalePoints): + value = self.fScalePoints[i]['value'] + if value > self.fRealValue: + self.setValue(value, True) + return + self.setValue(self.fScalePoints[0]['value'], True) + def mousePressEvent(self, event): - if self.fDialMode == self.MODE_DEFAULT: - QDial.mousePressEvent(self, event) + if self.fDialMode == self.MODE_DEFAULT or self.fIsOutput: return if event.button() == Qt.LeftButton: - self.fIsPressed = True - self.fLastDragPos = event.pos() - self.fLastDragValue = self.fRealValue - self.dragStateChanged.emit(True) + # if self.fNumScalePoints: + # self.nextScalePoint() + # + if self.fIsButton: + value = int(self.fRealValue) + 1; + if (value > self.fMaximum): + value = 0 + self.setValue(value, True) + else: + self.fIsPressed = True + self.fLastDragPos = event.pos() + self.fLastDragValue = self.fRealValue + self.dragStateChanged.emit(True) + + elif event.button() == Qt.MiddleButton: + if self.fIsOutput: + return + self.setValue(self.fDefault, True) + + + def mouseDoubleClickEvent(self, event): + if self.knobPusheable and self.fIndex in (PARAMETER_DRYWET, PARAMETER_VOLUME, PARAMETER_PANNING): # -3, -4, -7 + return # Mutex with special Single Click + + if event.button() == Qt.LeftButton: + if self.fIsButton: + value = int(self.fRealValue) + 1; + if (value > self.fMaximum): + value = 0 + self.setValue(value, True) + else: + if self.fIsOutput: + return + + self.knobCustomInputDialog() + def mouseMoveEvent(self, event): - if self.fDialMode == self.MODE_DEFAULT: - QDial.mouseMoveEvent(self, event) + if self.fDialMode == self.MODE_DEFAULT or self.fIsOutput: return if not self.fIsPressed: return - diff = (self.fMaximum - self.fMinimum) / 4.0 - pos = event.pos() - dx = diff * float(pos.x() - self.fLastDragPos.x()) / self.width() - dy = diff * float(pos.y() - self.fLastDragPos.y()) / self.height() - value = self.fLastDragValue + dx - dy + pos = event.pos() + delta = (float(pos.x() - self.fLastDragPos.x()) - float(pos.y() - self.fLastDragPos.y())) / 10 - if value < self.fMinimum: - value = self.fMinimum - elif value > self.fMaximum: - value = self.fMaximum - elif self.fIsInteger: - value = float(round(value)) + mod = event.modifiers() + self.applyDelta(mod, delta, True) - self.setValue(value, True) def mouseReleaseEvent(self, event): - if self.fDialMode == self.MODE_DEFAULT: - QDial.mouseReleaseEvent(self, event) + if self.fDialMode == self.MODE_DEFAULT or self.fIsOutput: return if self.fIsPressed: self.fIsPressed = False self.dragStateChanged.emit(False) + if event.button() == Qt.LeftButton: + if event.pos() == self.fLastDragPos: + if self.fNumScalePoints: + self.nextScalePoint() + else: + self.knobPush() + + # NOTE: fLastLabel state managed @ scalabledial + def knobPush(self): + if self.knobPusheable and self.fIndex in (PARAMETER_DRYWET, PARAMETER_VOLUME, PARAMETER_PANNING): # -3, -4, -7 + + if self.fLastLabel == "": # push value + self.fLastValue = self.fRealValue + self.setValue(0, True) # Thru or Mute + else: # pop value + self.setValue(self.fLastValue, True) + + + def applyDelta(self, mod, delta, anchor = False): + if self.fIsOutput: + return + + if self.fIsButton: + self.setValue(self.fRealValue + delta, True) + return + + if self.fIsInteger: # 4 to 50 ticks per revolution + if (mod & Qt.ShiftModifier): + delta = delta * 5 + elif (mod & Qt.ControlModifier): + delta = delta / min(int((self.fMaximum-self.fMinimum)/self.fPrecision), 5) + else: # Floats are 250 to 500 ticks per revolution +# jpka: 1. Should i use these steps? +# 2. And what do i do when i TODO add MODE_LOG along with MODE_LINEAR? +# 3. And they're too small for large ints like in TAP Reverb, and strange for scalepoints. +# paramRanges = self.host.get_parameter_ranges(self.fPluginId, i) +# paramRanges['step'], paramRanges['stepSmall'], paramRanges['stepLarge'] + if (mod & Qt.ControlModifier) and (mod & Qt.ShiftModifier): + delta = delta * 2/5 + elif (mod & Qt.ControlModifier): + delta = delta * 2 + elif (mod & Qt.ShiftModifier): + delta = delta * 50 + else: + delta = delta * 10 + + difference = float(self.fMaximum-self.fMinimum) * float(delta) / float(self.fPrecision) + + if anchor: + self.setValue((self.fLastDragValue + difference), True) + else: + self.setValue((self.fRealValue + difference), True) + + return + + + def wheelEvent(self, event): + if self.fIsOutput: + return + + direction = event.angleDelta().y() + if direction < 0: + delta = -1.0 + elif direction > 0: + delta = 1.0 + else: + return + + mod = event.modifiers() + self.applyDelta(mod, delta) + return + + + def keyPressEvent(self, event): + if self.fIsOutput: + return + + key = event.key() + mod = event.modifiers() + modsNone = not ((mod & Qt.ShiftModifier) | (mod & Qt.ControlModifier) | (mod & Qt.AltModifier)) + + if modsNone: + match key: + case Qt.Key_Space | Qt.Key_Enter | Qt.Key_Return : + if self.fIsButton: + value = int(self.fRealValue) + 1 + if (value > self.fMaximum): + value = 0 + self.setValue(value, True) + + elif not key == Qt.Key_Space: + self.knobCustomInputDialog() + else: + if self.fNumScalePoints: + self.nextScalePoint() + else: + self.knobPush() + + case Qt.Key_E: + self.knobCustomInputDialog() + + case key if Qt.Key_0 <= key <= Qt.Key_9: + if self.fIsInteger and (self.fMinimum == 0) and (self.fMaximum <= 10): + self.setValue(key-Qt.Key_0, True) + + else: + self.setValue(self.fMinimum + float(self.fMaximum-self.fMinimum)/10.0*(key-Qt.Key_0), True) + + case Qt.Key_Home: # NOTE: interferes with Canvas control hotkey + self.setValue(self.fMinimum, True) + + case Qt.Key_End: + self.setValue(self.fMaximum, True) + + case Qt.Key_D: + self.setValue(self.fDefault, True) + + case Qt.Key_R: + self.setValue(self.fDefault, True) + + match key: + case Qt.Key_PageDown: + self.applyDelta(mod, -1) + + case Qt.Key_PageUp: + self.applyDelta(mod, 1) + + return + + def paintEvent(self, event): painter = QPainter(self) event.accept() @@ -250,22 +464,118 @@ class CommonDial(QDial): painter.save() painter.setRenderHint(QPainter.Antialiasing, True) + enabled = int(bool(self.isEnabled())) + if enabled: + self.setContextMenuPolicy(Qt.CustomContextMenu) + else: + self.setContextMenuPolicy(Qt.NoContextMenu) + if self.fLabel: - if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL: - painter.setPen(self.fLabelGradientColor2) - painter.setBrush(self.fLabelGradient) - painter.drawRect(self.fLabelGradientRect) + # if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL: + # painter.setPen(self.fLabelGradientColor2) + # painter.setBrush(self.fLabelGradient) + # # painter.drawRect(self.fLabelGradientRect) FIXME restore gradients. + + luma = int(bool(self.fWhiteLabels)) - 0.5 + if enabled: + L = (luma * (1.6 + self.addContrast * 0.4)) / 2 + 0.5 + else: + L = (luma * (0.2 + self.addContrast * 0.2)) / 2 + 0.5 painter.setFont(self.fLabelFont) - painter.setPen(self.fLabelGradientColorT[0 if self.isEnabled() else 1]) + # painter.setPen(self.fLabelGradientColorT[0 if self.fIsEnabled() else 1]) + painter.setPen(QColor.fromHslF(0, 0, L, 1)) painter.drawText(self.fLabelPos, self.fLabel) - self.paintDial(painter) + X = self.fWidth / 2 + Y = self.fHeight / 2 + + S = enabled * 0.9 # saturation + + E = enabled * self.fHoverStep / 40 # enlight + L = 0.6 + E + if self.addContrast: + L = min(L + 0.3, 1) # luma + + normValue = float(self.fRealValue - self.fMinimum) / float(self.fMaximum - self.fMinimum) + # Work In Progress FIXME + H=0 + if self.fIsOutput: + if self.fIsButton: + # self.paintLed (painter, X, Y, H, S, L, E, normValue) + self.paintDisplay(painter, X, Y, H, S, L, E, normValue, enabled) + else: + self.paintDisplay(painter, X, Y, H, S, L, E, normValue, enabled) + else: + if self.fIsButton: + self.paintButton (painter, X, Y, H, S, L, E, normValue, enabled) + else: + self.paintDial (painter, X, Y, H, S, L, E, normValue, enabled) + + # Display tooltip, above the knob (OS-independent, unlike of mouse tooltip). + # Note, update/redraw Qt's tooltip eats much more CPU than expected, + # so we have tweak for turn it off. See also #1934. + if self.fHoverStep == self.HOVER_MAX and self.displayTooltip: + # First, we need to find exact or nearest match (index from value). + # It is also tests if we have scale points at all. + num = -1 + for i in range(self.fNumScalePoints): + scaleValue = self.fScalePoints[i]['value'] + if i == 0: + finalValue = scaleValue + num = 0 + else: + srange2 = abs(self.fRealValue - finalValue) + srange1 = abs(self.fRealValue - scaleValue) + if srange2 > srange1: + finalValue = scaleValue + num = i + if (srange1 == 0): # Exact match, save some CPU. + break + + tip = "" + if (num >= 0): # Scalepoints are used + tip = str(self.fScalePoints[num]['label']) + if not self.fIsButton: + tip = self.fScalePointsPrefix + \ + strLim(self.fScalePoints[num]['value']) + \ + self.fScalePointsSuffix + ": " + tip + # ? We most probably not need tooltip for button, if it is not scalepoint. + # elif not self.fIsButton: + else: + if self.fRealValue == 0 and self.fIndex == PARAMETER_DRYWET: #-3,-4,-7,-9 + tip = "THRU" + elif self.fRealValue == 0 and self.fIndex == PARAMETER_VOLUME: + tip = "MUTE" + elif self.fRealValue == 0 and self.fIndex == PARAMETER_PANNING: + tip = "Center" + else: + if self.fIndex < PARAMETER_NULL: + percent = 100.0 + else: + percent = 1 + + tip = (strLim(self.fRealValue * percent) + " " + self.fUnit).strip() + if self.fIsOutput: + tip = tip + " [" + strLim(self.fMinimum * percent) + "..." + \ + strLim(self.fMaximum * percent) + "]" + + # Wrong vert. position for Calf: + # QToolTip.showText(self.mapToGlobal(QPoint(0, 0-self.geometry().height())), tip) + # FIXME Still wrong vert. position for QT_SCALE_FACTOR=2. + QToolTip.showText(self.mapToGlobal(QPoint(0, 0-45)), tip) + else: + QToolTip.hideText() + + if enabled: + if self.HOVER_MIN < self.fHoverStep < self.HOVER_MAX: + self.fHoverStep += 1 if self.fIsHovered else -1 + QTimer.singleShot(20, self.update) painter.restore() - def resizeEvent(self, event): - QDial.resizeEvent(self, event) - self.updateSizes() + # def resizeEvent(self, event): + # QWidget.resizeEvent(self, event) + # self.updateSizes() # --------------------------------------------------------------------------------------------------------------------- diff --git a/source/frontend/widgets/digitalpeakmeter.py b/source/frontend/widgets/digitalpeakmeter.py index 3707b296c..b85b4762d 100644 --- a/source/frontend/widgets/digitalpeakmeter.py +++ b/source/frontend/widgets/digitalpeakmeter.py @@ -35,6 +35,7 @@ class DigitalPeakMeter(QWidget): STYLE_OPENAV = 2 STYLE_RNCBC = 3 STYLE_CALF = 4 + STYLE_TUBE = 5 # ----------------------------------------------------------------------------------------------------------------- @@ -153,7 +154,7 @@ class DigitalPeakMeter(QWidget): if self.fMeterStyle == style: return - if style not in (self.STYLE_DEFAULT, self.STYLE_OPENAV, self.STYLE_RNCBC, self.STYLE_CALF): + if style not in (self.STYLE_DEFAULT, self.STYLE_OPENAV, self.STYLE_RNCBC, self.STYLE_CALF, self.STYLE_TUBE): qCritical(f"DigitalPeakMeter::setMeterStyle({style}) - invalid style") return @@ -163,7 +164,7 @@ class DigitalPeakMeter(QWidget): self.fMeterBackground = QColor("#1A1A1A") elif style == self.STYLE_RNCBC: self.fMeterBackground = QColor("#070707") - elif style == self.STYLE_CALF: + elif style in (self.STYLE_CALF, self.STYLE_TUBE): self.fMeterBackground = QColor("#000") if style == self.STYLE_CALF: @@ -215,23 +216,36 @@ class DigitalPeakMeter(QWidget): i = meter - 1 + if level < 0.001: + level = 0.0 + elif level > 0.999: + level = 1.0 + if self.fSmoothMultiplier > 0 and not forced: level = ( (self.fLastChannelData[i] * float(self.fSmoothMultiplier) + level) / float(self.fSmoothMultiplier + 1) ) - if level < 0.001: - level = 0.0 - elif level > 0.999: - level = 1.0 + self.fLastChannelData[i] = level + + # Discretize scale: for 10 points, first will lit at 5%, + # then 15%, and last at 95% of normalized value. + # We also win some CPU when not redraw at small changes. + if (self.fMeterStyle == self.STYLE_TUBE) and (level > 0.0) and (level < 1.0): + points = 20 + + # Transform to Sq Root domain: our meters have Sqrt dynamic compression; + # Discretize: + level = int(sqrt(level) * points + 0.5) / points + + # Transform back from Square Root domain: + level = level * level if self.fChannelData[i] != level: self.fChannelData[i] = level self.update() - self.fLastChannelData[i] = level - # ----------------------------------------------------------------------------------------------------------------- def updateGrandient(self): @@ -284,6 +298,14 @@ class DigitalPeakMeter(QWidget): self.fMeterGradient.setColorAt(0.0, self.fMeterColorBase) self.fMeterGradient.setColorAt(1.0, self.fMeterColorBase) + elif self.fMeterStyle == self.STYLE_TUBE: + color = QColor.fromHslF(0.9, 1, 0.6, 1) # Tuneon filled w/ neon + agron + points = 20 + for i in range(points + 1): + self.fMeterGradient.setColorAt(((i-0.3)/points % 1.0), color) + self.fMeterGradient.setColorAt(( i /points ), Qt.black) + self.fMeterGradient.setColorAt(((i+0.3)/points % 1.0), color) + self.updateGrandientFinalStop() def updateGrandientFinalStop(self): @@ -360,6 +382,13 @@ class DigitalPeakMeter(QWidget): meterPad += 2 meterSize -= 2 + elif self.fMeterStyle == self.STYLE_TUBE: + painter.setPen(QPen(Qt.NoPen)) + painter.setBrush(self.fMeterGradient) + meterPos += 3 + meterPad += 6 + meterSize -= 6 + else: painter.setPen(QPen(self.fMeterBackground, 0)) painter.setBrush(self.fMeterGradient) diff --git a/source/frontend/widgets/paramspinbox.py b/source/frontend/widgets/paramspinbox.py index 4a85a1669..eb7e937e3 100644 --- a/source/frontend/widgets/paramspinbox.py +++ b/source/frontend/widgets/paramspinbox.py @@ -25,7 +25,7 @@ elif qt_config == 6: import ui_inputdialog_value from carla_backend import CARLA_OS_MAC -from carla_shared import countDecimalPoints +from carla_shared import countDecimalPoints, getPrefixSuffix, strLim # ------------------------------------------------------------------------------------------------------------ # Get a fixed value within min/max bounds @@ -46,13 +46,21 @@ def geFixedValue(name, value, minimum, maximum): # Custom InputDialog with Scale Points support class CustomInputDialog(QDialog): - def __init__(self, parent, label, current, minimum, maximum, step, stepSmall, scalePoints, prefix, suffix): + def __init__(self, parent, label, current, minimum, maximum, step, stepSmall, scalePoints, prefix, suffix, unit=""): QDialog.__init__(self, parent) self.ui = ui_inputdialog_value.Ui_Dialog() self.ui.setupUi(self) - decimals = countDecimalPoints(step, stepSmall) - self.ui.label.setText(label) + if not (unit == ""): + prefix, suffix = getPrefixSuffix(unit) + + if unit == "%": + decimals = 1 + else: + decimals = countDecimalPoints(step, stepSmall) + + # self.ui.label.setText(label + " [" + strRound(self, minimum, decimals) + "..." + strRound(self, maximum, decimals) + "]") + self.ui.label.setText(label + " [" + strLim(minimum) + "..." + strLim(maximum) + "]") self.ui.doubleSpinBox.setDecimals(decimals) self.ui.doubleSpinBox.setRange(minimum, maximum) self.ui.doubleSpinBox.setSingleStep(step) @@ -361,14 +369,7 @@ class ParamSpinBox(QAbstractSpinBox): self.fStepLarge = value def setLabel(self, label): - prefix = "" - suffix = label.strip() - - if suffix == "(coef)": - prefix = "* " - suffix = "" - else: - suffix = " " + suffix + prefix, suffix = getPrefixSuffix(label) self.fLabelPrefix = prefix self.fLabelSuffix = suffix @@ -533,16 +534,16 @@ class ParamSpinBox(QAbstractSpinBox): pass menu = QMenu(self) - actReset = menu.addAction(self.tr("Reset (%f)" % self.fDefault)) + actReset = menu.addAction(self.tr("Reset (" + strLim(self.fDefault) + ")")) actRandom = menu.addAction(self.tr("Random")) menu.addSeparator() - actCopy = menu.addAction(self.tr("Copy (%f)" % self.fValue)) + actCopy = menu.addAction(self.tr("Copy (" + strLim(self.fValue) + ")")) if pasteValue is None: actPaste = menu.addAction(self.tr("Paste")) actPaste.setEnabled(False) else: - actPaste = menu.addAction(self.tr("Paste (%f)" % pasteValue)) + actPaste = menu.addAction(self.tr("Paste (" + strLim(pasteValue) + ")")) menu.addSeparator() diff --git a/source/frontend/widgets/racklistwidget.py b/source/frontend/widgets/racklistwidget.py index d64c5761e..30beb6839 100644 --- a/source/frontend/widgets/racklistwidget.py +++ b/source/frontend/widgets/racklistwidget.py @@ -13,11 +13,11 @@ import os from qt_compat import qt_config if qt_config == 5: - from PyQt5.QtCore import Qt, QSize, QRect, QEvent + from PyQt5.QtCore import QT_VERSION, Qt, QSize, QRect, QEvent from PyQt5.QtGui import QColor, QPainter, QPixmap from PyQt5.QtWidgets import QAbstractItemView, QListWidget, QListWidgetItem, QMessageBox elif qt_config == 6: - from PyQt6.QtCore import Qt, QSize, QRect, QEvent + from PyQt6.QtCore import QT_VERSION, Qt, QSize, QRect, QEvent, QPoint # QPoint is for Qt6 only. from PyQt6.QtGui import QColor, QPainter, QPixmap from PyQt6.QtWidgets import QAbstractItemView, QListWidget, QListWidgetItem, QMessageBox @@ -291,7 +291,10 @@ class RackListWidget(QListWidget): event.acceptProposedAction() - tryItem = self.itemAt(event.pos()) + if QT_VERSION < 0x60000: + tryItem = self.itemAt(event.pos()) + else: + tryItem = self.itemAt(QPoint(int(event.position().x()), int(event.position().y()))) if tryItem is not None: self.setCurrentRow(tryItem.getPluginId()) @@ -318,7 +321,10 @@ class RackListWidget(QListWidget): if not urls: return - tryItem = self.itemAt(event.pos()) + if QT_VERSION < 0x60000: + tryItem = self.itemAt(event.pos()) + else: + tryItem = self.itemAt(QPoint(int(event.position().x()), int(event.position().y()))) if tryItem is not None: pluginId = tryItem.getPluginId() diff --git a/source/frontend/widgets/scalabledial.py b/source/frontend/widgets/scalabledial.py index 0d842c738..33f1c83ae 100644 --- a/source/frontend/widgets/scalabledial.py +++ b/source/frontend/widgets/scalabledial.py @@ -6,287 +6,902 @@ # Imports (Global) from math import cos, floor, pi, sin +import ast from qt_compat import qt_config if qt_config == 5: - from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPointF, QRectF, QTimer, QSize - from PyQt5.QtGui import QColor, QConicalGradient, QFontMetrics, QPainterPath, QPen, QPixmap - from PyQt5.QtSvg import QSvgWidget + from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QPointF, QRectF, QTimer, QSize + from PyQt5.QtGui import QColor, QLinearGradient, QRadialGradient, QConicalGradient, QFontMetrics, QPen, QPolygonF + from PyQt5.QtWidgets import QToolTip elif qt_config == 6: - from PyQt6.QtCore import pyqtSlot, Qt, QEvent, QPointF, QRectF, QTimer, QSize - from PyQt6.QtGui import QColor, QConicalGradient, QFontMetrics, QPainterPath, QPen, QPixmap - from PyQt6.QtSvgWidgets import QSvgWidget + from PyQt6.QtCore import pyqtSlot, Qt, QEvent, QPoint, QPointF, QRectF, QTimer, QSize + from PyQt6.QtGui import QColor, QLinearGradient, QRadialGradient, QConicalGradient, QFontMetrics, QPen, QPolygonF + from PyQt6.QtWidgets import QToolTip from .commondial import CommonDial -from carla_shared import fontMetricsHorizontalAdvance +from carla_shared import fontMetricsHorizontalAdvance, RACK_KNOB_GAP +from carla_backend import ( + PARAMETER_NULL, + PARAMETER_DRYWET, + PARAMETER_VOLUME, + PARAMETER_BALANCE_LEFT, + PARAMETER_BALANCE_RIGHT, + PARAMETER_PANNING, + PARAMETER_MAX ) # --------------------------------------------------------------------------------------------------------------------- # Widget Class class ScalableDial(CommonDial): - def __init__(self, parent, index=0): - CommonDial.__init__(self, parent, index) - - self.fImage = QSvgWidget(":/scalable/dial_03.svg") - self.fImageNum = "01" + def __init__(self, parent, index, + precision, + default, + minimum, + maximum, + label, + paintMode, + colorHint = -1, # Hue & Sat, -1 = NotColorable + unit = "%", # Measurement Unit + skinStyle = "default", # Full name (from full list) + whiteLabels = 1, # Is light/white theme? + tweaks = {}, + isInteger = 0, # Input is Integer + isButton = 0, # Integer i/o is Button or LED + isOutput = 0, + isVuOutput = 0, # Output is analog VU meter + isVisible = 1 ): + + # self.fWidth = self.fHeight = 32 # aka fImageBaseSize, not includes label. + CommonDial.__init__(self, parent, index, precision, default, minimum, maximum, label, paintMode, colorHint, unit, skinStyle, whiteLabels, tweaks, isInteger, isButton, isOutput, isVuOutput, isVisible) + + # FIXME not every repaint need to re-calculate geometry? + def updateSizes(self): + knownModes = [ + # default + self.CUSTOM_PAINT_MODE_NULL , # 0 + self.CUSTOM_PAINT_MODE_CARLA_WET , # 1 + self.CUSTOM_PAINT_MODE_CARLA_VOL , # 2 + self.CUSTOM_PAINT_MODE_CARLA_L , # 3 + self.CUSTOM_PAINT_MODE_CARLA_R , # 4 + self.CUSTOM_PAINT_MODE_CARLA_PAN , # 5 + self.CUSTOM_PAINT_MODE_CARLA_FORTH , # 6 + self.CUSTOM_PAINT_MODE_CARLA_WET_MINI, # 9 + self.CUSTOM_PAINT_MODE_CARLA_VOL_MINI, # 10 + # calf + 16, + # openav + 32, 33, 34, 37, 38, + # zynfx + 48, 49, 50, 53, 54, + # tube + 64, 65, 66, 69, 70, + ] + + index = -1 + for i in range(len(knownModes)): + if knownModes[i] == self.fCustomPaintMode: + index = i + break + + if (index == -1): + print("Unknown paint mode "+ str(self.fCustomPaintMode)) + return - if self.fImage.sizeHint().width() > self.fImage.sizeHint().height(): - self.fImageOrientation = self.HORIZONTAL + self.skin = int(self.fCustomPaintMode / 16) + self.subSkin = int(self.fCustomPaintMode % 16) + + width, hueA, hueB, travel, radius, size, point, labelLift = [ + # default Aqua + [ 32, 0.50, 0.50, 260, 10, 10, 3 , 1/2, ], + [ 32, 0.3 , 0.50, 260, 10, 10, 3 , 1/2, ], # WET + [ 32, 0.50, 0.50, 260, 10, 10, 3 , 1/2, ], # VOL + [ 26, 0.21, 0.21, 260, 8, 10, 2.5, 1/2, ], # L + [ 26, 0.21, 0.21, 260, 8, 10, 2.5, 1/2, ], # R + [ 32, 0.50, 0.50, 260, 10, 10, 3 , 1/2, ], # PAN + [ 32, 0.50, 0.50, 260, 10, 10, 3 , 1/2, ], # FORTH + [ 28, 0.3 , 0.50, 260, 9, 10, 2.5, 1/2, ], # WET_MINI + [ 28, 0.50, 0.50, 260, 9, 10, 2.5, 1/2, ], # VOL_MINI + # calf Blue + [ 40, 0.53, 0.53, 290, 12, 12, 4 , 1 , ], # calf absent any wet/vol knobs + # openav Orange + [ 32, 0.05, 0.05, 270, 12, 12, 2.5, 2/3, ], + [ 32, 0.30, 0.5, 270, 12, 12, 2.5, 2/3, ], # WET + [ 32, 0.5, 0.5, 270, 12, 12, 2.5, 2/3, ], # VOL + [ 32, 0.5, 0.5, 270, 12, 12, 2.5, 2/3, ], + [ 32, 0.5, 0.5, 270, 12, 12, 2.5, 2/3, ], + # zynfx Teal + [ 38, 0.55, 0.55, 264, 12, 12, 4 , 1/4, ], + [ 38, 0.30, 0.5, 264, 12, 12, 4 , 1/4, ], # WET + [ 38, 0.5, 0.5, 264, 12, 12, 4 , 1/4, ], # VOL + [ 38, 0.5, 0.5, 264, 12, 12, 4 , 1/4, ], + [ 38, 0.5, 0.5, 264, 12, 12, 4 , 1/4, ], + # tube VFD + [ 50, 0.45, 0.45, 258, 12, 12, 4 , 1/2, ], + [ 50, 0.45, 0.45, 258, 12, 12, 4 , 1/2, ], # WET + [ 50, 0.45, 0.45, 258, 12, 12, 4 , 1/2, ], # VOL + [ 50, 0.45, 0.45, 258, 12, 12, 4 , 1/2, ], + [ 50, 0.45, 0.45, 258, 12, 12, 4 , 1/2, ], + ] [index] + + # Geometry & Color of controls & displays, some are tweakable: + # 1. Try to get value from per-skin tweak; + # 2. Then try to get value from common tweak; + # 3. Then use default value from array. + self.fWidth = self.fHeight = width + + # Angle span (travel) + # calf must be 360/36*29=290 + # tube must be 360/14*10=257.14 or 360/12*10=300 + self.fTravel = int(self.getTweak('KnobTravel', travel)) + + # Radius of some notable element of Knob (not exactly the largest) + self.fRadius = int(self.getTweak('KnobRadius', radius)) + + # Size of Button (half of it, similar to "raduis") + self.fSize = int(self.getTweak('ButtonSize', size)) + + # Point, line or other accent on knob + self.fPointSize = point + + # Colouring, either only one or both values can be used for skin. + if (self.subSkin > 0) or (self.skin in (1, 3, 4,)) : + self.fHueA = hueA + self.fHueB = hueB + # default and openav can be re-colored + elif self.colorFollow: + self.fHueA = self.fHueB = int(self.fColorHint) / 100.0 # we use hue only yet else: - self.fImageOrientation = self.VERTICAL + # NOTE: here all incoming color data, except hue, is lost. + self.fHueA = self.fHueB = self.fCustomPaintColor.hueF() - self.updateSizes() - def getBaseSize(self): - return self.fImageBaseSize + metrics = QFontMetrics(self.fLabelFont) - def updateSizes(self): - if isinstance(self.fImage, QPixmap): - self.fImageWidth = self.fImage.width() - self.fImageHeight = self.fImage.height() + if not self.fLabel: + self.fLabelWidth = 0 else: - self.fImageWidth = self.fImage.sizeHint().width() - self.fImageHeight = self.fImage.sizeHint().height() + self.fLabelWidth = fontMetricsHorizontalAdvance(metrics, self.fLabel) - if self.fImageWidth < 1: - self.fImageWidth = 1 + extraWidthAuto = max((self.fLabelWidth - self.fWidth), 0) - if self.fImageHeight < 1: - self.fImageHeight = 1 + self.fLabelHeight = metrics.height() - if self.fImageOrientation == self.HORIZONTAL: - self.fImageBaseSize = self.fImageHeight - self.fImageLayersCount = self.fImageWidth / self.fImageHeight - else: - self.fImageBaseSize = self.fImageWidth - self.fImageLayersCount = self.fImageHeight / self.fImageWidth + if (self.fCustomPaintMode % 16) == 0: # exclude: DryWet, Volume, etc. + extraWidth = int(self.getTweak('GapMin', 0)) + extraWidthLimit = int(self.getTweak('GapMax', 0)) - self.setMinimumSize(self.fImageBaseSize, self.fImageBaseSize + self.fLabelHeight + 5) - self.setMaximumSize(self.fImageBaseSize, self.fImageBaseSize + self.fLabelHeight + 5) + if self.getTweak('GapAuto', 0): + extraWidth = max(extraWidth, extraWidthAuto) + + extraWidth = min(extraWidth, extraWidthLimit) + + self.fWidth = self.fWidth + extraWidth + + self.setMinimumSize(self.fWidth, self.fHeight + self.fLabelHeight + RACK_KNOB_GAP) + self.setMaximumSize(self.fWidth, self.fHeight + self.fLabelHeight + RACK_KNOB_GAP) if not self.fLabel: self.fLabelHeight = 0 - self.fLabelWidth = 0 + # self.fLabelWidth = 0 return - metrics = QFontMetrics(self.fLabelFont) - self.fLabelWidth = fontMetricsHorizontalAdvance(metrics, self.fLabel) - self.fLabelHeight = metrics.height() - - self.fLabelPos.setX(float(self.fImageBaseSize)/2.0 - float(self.fLabelWidth)/2.0) + self.fLabelPos.setX(float(self.fWidth)/2.0 - float(self.fLabelWidth)/2.0) - if self.fImageNum in ("01", "02", "07", "08", "09", "10"): - self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight) - elif self.fImageNum in ("11",): - self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight*2/3) - else: - self.fLabelPos.setY(self.fImageBaseSize + self.fLabelHeight/2) + # labelLift = (1/2, 1, 2/3, 1/4, 1/2, 1, 1, 1)[skin % 8] + self.fLabelPos.setY(self.fHeight + self.fLabelHeight * labelLift) - self.fLabelGradient.setStart(0, float(self.fImageBaseSize)/2.0) - self.fLabelGradient.setFinalStop(0, self.fImageBaseSize + self.fLabelHeight + 5) + # jpka: TODO Can't see how gradients work, looks like it's never triggered. + self.fLabelGradient.setStart(0, float(self.fHeight)/2.0) + self.fLabelGradient.setFinalStop(0, self.fHeight + self.fLabelHeight + 5) - self.fLabelGradientRect = QRectF(float(self.fImageBaseSize)/8.0, float(self.fImageBaseSize)/2.0, - float(self.fImageBaseSize*3)/4.0, self.fImageBaseSize+self.fLabelHeight+5) + self.fLabelGradientRect = QRectF(float(self.fHeight)/8.0, float(self.fHeight)/2.0, + float(self.fHeight*3)/4.0, self.fHeight+self.fLabelHeight+5) def setImage(self, imageId): - self.fImageNum = "%02i" % imageId - if imageId in (2,6,7,8,9,10,11,12,13): - img = ":/bitmaps/dial_%s%s.png" % (self.fImageNum, "" if self.isEnabled() else "d") - else: - img = ":/scalable/dial_%s%s.svg" % (self.fImageNum, "" if self.isEnabled() else "d") + print("Loopback for self.setupZynFxParams(), FIXME!") + return + + def minimumSizeHint(self): + return QSize(self.fWidth, self.fHeight) - if img.endswith(".png"): - if not isinstance(self.fImage, QPixmap): - self.fImage = QPixmap() + def sizeHint(self): + return QSize(self.fWidth, self.fHeight) + + # def changeEvent(self, event): + # CommonDial.changeEvent(self, event) + # + # # Force svg update if enabled state changes + # if event.type() == QEvent.EnabledChange: + # self.slot_updateImage() + + def drawMark(self, painter, X, Y, r1, r2, angle, width, color): + A = angle * pi/180 + x = X + r1 * cos(A) + y = Y - r1 * sin(A) + painter.setPen(QPen(color, width, cap=Qt.RoundCap)) + if not (r1 == r2): # line + x1 = X + r2 * cos(A) + y1 = Y - r2 * sin(A) + painter.drawLine(QPointF(x, y), QPointF(x1, y1)) + else: # ball + painter.drawEllipse(QRectF(x-width/2, y-width/2, width, width)) + + gradMachined = {5.9, 10.7, 15.7, 20.8, 25.8, 30.6, 40.6, 45.9, + 55.9, 60.7, 65.7, 70.8, 75.8, 80.6, 90.6, 95.9} + + def grayGrad(self, painter, X, Y, a, b, gradPairs, alpha = 1.0): + if b == -1: + grad = QConicalGradient(X, Y, a) + elif b == -2: + grad = QRadialGradient (X, Y, a) else: - if not isinstance(self.fImage, QSvgWidget): - self.fImage = QSvgWidget() + grad = QLinearGradient (X, Y, a, b) - self.fImage.load(img) + for i in gradPairs: + grad.setColorAt(int(i)/100.0, QColor.fromHslF(0, 0, (i % 1.0), alpha)) - if self.fImage.width() > self.fImage.height(): - self.fImageOrientation = self.HORIZONTAL - else: - self.fImageOrientation = self.VERTICAL + return grad - # special svgs - if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_NULL: - # reserved for carla-wet, carla-vol, carla-pan and color - if self.fImageNum == "03": - self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_COLOR + # Pen is always full opacity (alpha = 1) + def grayGradPen(self, painter, X, Y, a, b, gradPairs = {0.10, 50.30, 100.10}, width = 1.0): + painter.setPen(QPen(self.grayGrad(painter, X, Y, a, b, gradPairs, 1), width, Qt.SolidLine, Qt.FlatCap)) - # reserved for carla-L and carla-R - elif self.fImageNum == "04": - self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_CARLA_L + def grayGradBrush(self, painter, X, Y, a, b, gradPairs, alpha = 1.0): + painter.setBrush(self.grayGrad(painter, X, Y, a, b, gradPairs, alpha)) - # reserved for zita - elif self.fImageNum == "06": - self.fCustomPaintMode = self.CUSTOM_PAINT_MODE_ZITA - self.updateSizes() - self.update() + # Replace Qt draw over substrate bitmap or svg to + # all-in-one widget generated from stratch using Qt only, + # make it highly tuneable, and uniformly look like + # using HSL color model to make same brightness of colored things. + # We can also easily have color tinted (themed) knobs. + # Some things were simplified a little, to gain more speed. + # R: knob nib (cap) radius + def paintDial(self, painter, X, Y, H, S, L, E, normValue, enabled): + R = self.fRadius + barWidth = self.fPointSize + angleSpan = self.fTravel - @pyqtSlot() - def slot_updateImage(self): - self.setImage(int(self.fImageNum)) + hueA = self.fHueA + hueB = self.fHueB - def minimumSizeHint(self): - return QSize(self.fImageBaseSize, self.fImageBaseSize) + color0 = QColor.fromHslF(hueA, S, L, 1) + color0a = QColor.fromHslF(hueA, S, L/2-0.25, 1) + color1 = QColor.fromHslF(hueB, S, L, 1) - def sizeHint(self): - return QSize(self.fImageBaseSize, self.fImageBaseSize) + skin = self.skin - def changeEvent(self, event): - CommonDial.changeEvent(self, event) + def ang(value): + return angleSpan * (0.5 - value) + 90 + + def drawArcV(rect, valFrom, valTo, ticks = 0): + # discretize scale: for 10 points, first will lit at 5%, + # then 15%, and last at 95% of normalized value, + # i.e. treshold is: center of point exactly matches knob mark angle + if ticks: + valTo = int(valTo * (ticks * angleSpan / 360) + 0.5) / (ticks * angleSpan / 360) + + painter.drawArc(rect, int(ang(valFrom) * 16), int((ang(valTo) - ang(valFrom)) * 16)) + + def squareBorder(w): + return QRectF(X-R-w, Y-R-w, (R+w)*2, (R+w)*2) + + def gray(luma): + return QColor.fromHslF(0, 0, luma, 1) + + + # Knob light arc "base" (starting) value/angle. + if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_L: + refValue = 0 + elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_R: + refValue = 1 + elif (self.fMinimum == -self.fMaximum) and (skin == 0): + refValue = 0.5 + else: + refValue = 0 - # Force svg update if enabled state changes - if event.type() == QEvent.EnabledChange: - self.slot_updateImage() + knobMuted = (self.knobPusheable and (normValue == refValue)) - def paintDial(self, painter): - if self.isEnabled(): - normValue = float(self.fRealValue - self.fMinimum) / float(self.fMaximum - self.fMinimum) - curLayer = int((self.fImageLayersCount - 1) * normValue) + haveLed = self.getTweak('WetVolPushLed', 1) and self.fCustomPaintMode in (1, 2, 5, 6, 9, 10,) - if self.fImageOrientation == self.HORIZONTAL: - xpos = self.fImageBaseSize * curLayer - ypos = 0.0 + if self.fIndex in (PARAMETER_DRYWET, PARAMETER_VOLUME, PARAMETER_PANNING): # -3, -4, -7 + if knobMuted: + if self.fIndex == PARAMETER_DRYWET: + self.pushLabel("Thru") + elif self.fIndex == PARAMETER_VOLUME: + self.pushLabel("Mute") + elif self.fIndex == PARAMETER_PANNING: + self.pushLabel("Center") + else: + self.pushLabel("Midway") + else: + self.popLabel() + + if skin == 0: # mimic svg dial + # if not knobMuted: + if not (knobMuted and haveLed): + # light arc substrate: near black, 0.5 px exposed + painter.setPen(QPen(gray(0.10), barWidth+1, cap=Qt.FlatCap)) + drawArcV(squareBorder(barWidth), 0, 1) + + # light arc: gray bar + # should be combined with light (value) arc to be a bit faster ? + self.grayGradPen(painter, X, Y, 270, -1, {0.20, 100.15}, barWidth) + drawArcV(squareBorder(barWidth), 0, 1) + + # cap + self.grayGradBrush(painter, X-R, Y-R, R*2, -2, {0.45+E, 100.15+E}) + painter.setPen(QPen(gray(0.10), 0.5)) + painter.drawEllipse(squareBorder(1)) + + elif skin == 1: # calf + # outer chamfer & leds substrate + self.grayGradPen(painter, X, Y, 135, -1, {0.15, 50.50, 100.15}, 1.5) + painter.setBrush(color0a) + painter.drawEllipse(squareBorder(barWidth*2-1)) + + # machined shiny cap with chamfer + self.grayGradPen(painter, X, Y, -45, -1, {0.15, 50.50, 100.15}) + self.grayGradBrush(painter, X, Y, 0, -1, self.gradMachined) + painter.drawEllipse(squareBorder(1)) + + elif skin == 2: # openav + # light arc substrate + painter.setPen(QPen(gray(0.20+E), barWidth)) + drawArcV(squareBorder(barWidth), 0, 1) + + elif skin == 3: # zynfx + # light arc substrate + painter.setPen(QPen(QColor.fromHslF(0.57, 0.8, 0.25, 1), barWidth+2, cap=Qt.FlatCap)) + drawArcV(squareBorder(barWidth), 0, 1) + + # cap + painter.setPen(QPen(gray(0.0), 1)) + painter.setBrush(gray(0.3 + E)) + painter.drawEllipse(squareBorder(-2)) + + # These knobs are different for integers and for floats. + elif skin == 4: # tube / bakelite + chamfer = 1.5 # It is best when 1.5 at normal zoom, and 1.0 for >2x HiDpi + # base + self.grayGradPen(painter, X, Y, -45, -1, width=chamfer) + self.grayGradBrush(painter, X-5-ang(normValue)/36, -20, 83, -2, {0.2, 50.2, 51.00, 100.00}) + if self.fIsInteger: # chickenhead knob: small base + painter.drawEllipse(squareBorder(1)) + else: # round knob: larger base + painter.drawEllipse(squareBorder(R*0.7)) + + polygon = QPolygonF() + # "chickenhead" pointer + if self.fIsInteger: + for i in range(17): + A = ((0.01, 0.02, 0.03, 0.06, 0.2, 0.3, 0.44, 0.455, -0.455, -0.44, -0.3, -0.2, -0.06, -0.03, -0.02, -0.01, 0.01)[i] * 360 - ang(normValue)) * pi/180 + r = (1, 0.97, 0.91, 0.7, 0.38, 0.39, 0.87, 0.9, 0.9, 0.87, 0.39, 0.38, 0.7, 0.91, 0.97, 1, 1)[i] * R + polygon.append(QPointF(X + r * 1.75 * cos(A), Y + r * 1.75 * sin(A))) + # 8-teeth round knob outline else: - xpos = 0.0 - ypos = self.fImageBaseSize * curLayer + for i in range(64): + A = (i / 64 * 360 - ang(normValue)) * pi/180 + r = R * (1, 0.95, 0.91, 0.89, 0.88, 0.89, 0.91, 0.95)[i % 8] + polygon.append(QPointF(X + r * 1.5 * cos(A), Y + r * 1.5 * sin(A))) + + self.grayGradPen(painter, X, Y, -45, -1, {0.10, 50.50, 100.10}, chamfer) + self.grayGradBrush(painter, X-5-ang(normValue)/36, -20, 75, -2, {0.2, 50.2, 51.00, 100.00}) + painter.drawPolygon(polygon) + + # machined shiny penny with chamfer + self.grayGradPen(painter, X, Y, 135, -1, {0.15, 50.50, 100.15}) + self.grayGradBrush(painter, X, Y, -ang(normValue)/36, -1, self.gradMachined, 0.75) + if self.fIsInteger: # chickenhead knob: small circle + painter.drawEllipse(squareBorder(-R*0.65)) + else: # round knob: large one + painter.drawEllipse(squareBorder(-1)) + + # Outer scale marks + for i in range(0, 11): + angle = ((0.5-i/10) * angleSpan + 90) + self.drawMark(painter, X, Y, R*2, R*2, angle, barWidth/12 * (4 + 1 * int((i % 10) == 0)), gray(0.5 + E)) + + # if knobMuted: + if (knobMuted and haveLed): + # if self.getTweak('WetVolPushLed', 1): + self.drawMark(painter, X, Y, 0, 0, 0, barWidth, color0) + return - source = QRectF(xpos, ypos, self.fImageBaseSize, self.fImageBaseSize) + # draw arc: forward, or reverse (for 'R' ch knob) + if (not (normValue == refValue)) and (not (skin == 4)): + + gradient = QConicalGradient(X, Y, 270) + cap=Qt.FlatCap + + if not (skin == 1): # any, except calf + ticks = 0 + gradient.setColorAt(0.75, color0) + gradient.setColorAt(0.25, color1) + if skin == 3: # zynfx + # light arc partial (angled) black substrate + painter.setPen(QPen(gray(0.0), barWidth+2, cap=Qt.FlatCap)) + drawArcV(squareBorder(barWidth), refValue-0.013, normValue+0.013) + elif skin == 2: # openav + cap=Qt.RoundCap + else: # calf + ticks = 36 + for i in range(2, ticks-2, 1): + gradient.setColorAt((i+0.5-0.35)/ticks, color0) + gradient.setColorAt((i+0.5) /ticks, Qt.black) + gradient.setColorAt((i+0.5+0.35)/ticks, color0) + + painter.setPen(QPen(gradient, barWidth, Qt.SolidLine, cap)) + drawArcV(QRectF(squareBorder(barWidth)), refValue, normValue, ticks) + + # do not draw marks on disabled items + if not enabled: + return - if isinstance(self.fImage, QPixmap): - target = QRectF(0.0, 0.0, self.fImageBaseSize, self.fImageBaseSize) - painter.drawPixmap(target, self.fImage, source) + A = ang(normValue) + + match skin: + case 0: # ball + self.drawMark(painter, X, Y, R*0.8, R*0.8, A, barWidth/2+0.5, color0) + case 1: # line for calf + self.drawMark(painter, X, Y, R*0.6, R*0.9, A, barWidth/2, Qt.black) + case 2: # line for openav + self.drawMark(painter, X, Y, 0, R+barWidth, A, barWidth, color0) + case 3: # line for zynfx + self.drawMark(painter, X, Y, 2, R-3, A, barWidth/2+0.5, Qt.white) + case 4: # ball + r = R * (int(self.fIsInteger) * 0.25 + 1.2) + self.drawMark(painter, X, Y, r, r, A, barWidth/2+0.5, Qt.white) + + + def paintButton(self, painter, X, Y, H, S, L, E, normValue, enabled): + # W: button cap half-size ; w: bar width + W = self.fRadius + w = self.fPointSize + + hue = self.fHueA + + skin = int(self.fCustomPaintMode / 16) + + def squareBorder(w, dw=0): + return QRectF(X-W-w-dw, Y-W-w, (W+w+dw)*2, (W+w)*2) + + def gray(luma): + return QColor.fromHslF(0, 0, luma, 1) + + color = QColor.fromHslF(hue, S, L, 1) + + centerLed = self.getTweak('ButtonHaveLed', 0) # LED itself & size increase + coloredNeon = self.getTweak('ColoredNeon', 1) # But worse when HighContrast. + + if skin == 0: # internal + if not centerLed: + # light bar substrate: near black, 0.5 px exposed + painter.setPen(QPen(gray(0.10), w+1)) + painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X+W/2, Y-W-w)) + + # light bar: gray bar + painter.setPen(QPen(gray(0.20), w)) + painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X+W/2, Y-W-w)) + + # cap + self.grayGradBrush(painter, X-W/2, Y-W/2, W*2, -2, {0.13+E, 50.18+E, 100.35+E}) + painter.setPen(QPen(gray(0.05), 1)) + # A bit larger buttons when no top LED, but centered one. + painter.drawRoundedRect(squareBorder(-1+centerLed), 3, 3) + + elif skin == 1: # calf + # outer chamfer & leds substrate + self.grayGradPen(painter, X, Y, 135, -1, {24.25, 26.50, 76.50, 78.25}, 1.5) + painter.setBrush(QColor.fromHslF(hue, S, 0.05+E/2, 1)) + painter.drawRoundedRect(QRectF(X-W-1, Y-W-w-0-1, W*2+2, W*2+w+0+2), 4, 4) + + # machined shiny cap with chamfer + self.grayGradPen(painter, X, Y, -45, -1, {24.25, 26.50, 74.50, 76.25}) + self.grayGradBrush(painter, X, Y, -30, -1, self.gradMachined) + painter.drawRoundedRect(squareBorder(-1), 3, 3) + + elif skin == 2: # openav + # light substrate + pen = QPen(gray(0.20+E), w) + painter.setPen(pen) + painter.drawRoundedRect(squareBorder(0), 3, 3) + + elif skin == 3: # zynfx + if not centerLed: + # light bar substrate: teal, 1 px exposed + painter.setPen(QPen(QColor.fromHslF(hue, 0.8, 0.25, 1), w+2)) + painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X+W/2, Y-W-w)) + + # button + painter.setPen(QPen(gray(0.0), 1)) + painter.setBrush(gray(0.3 + E)) + painter.drawRoundedRect(squareBorder(-2, 4), 3, 3) + + elif skin == 4: # tube + # bakelite cap + self.grayGradPen(painter, X, Y, -45, -1) + self.grayGradBrush(painter, X-10, -40, 120, -2, {0.2, 50.2, 51.00, 100.00}) + painter.drawRoundedRect(squareBorder(W*0.2), 3, 3) + + # neon lamp + if (normValue > 0): + grad = QRadialGradient(X, Y, 10) + for i in ({0.6, 20.6, 70.4, 100.0}): + if coloredNeon: + grad.setColorAt(int(i)/100.0, QColor.fromHslF((0.05 - normValue) % 1.0, S, (i % 1.0), 1)) + else: + grad.setColorAt(int(i)/100.0, QColor.fromHslF(0.05, S, (i % 1.0) * normValue, 1)) + + painter.setPen(QPen(Qt.NoPen)) + painter.setBrush(grad) + painter.drawRoundedRect(squareBorder(-W*0.4), 1.5, 1.5) + + # glass over neon lamp + self.grayGradPen(painter, X, Y, 135, -1) + self.grayGradBrush(painter, X-10, -40, 124, -2, {0.9, 50.9, 51.4, 100.4}, 0.25) + painter.drawRoundedRect(squareBorder(-W*0.4), 1.5, 1.5) + + # draw active lights + if skin == 0: # internal + if not centerLed: + if (normValue > 0): + painter.setPen(QPen(color, w)) + if (normValue < 1): + painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X-w/2, Y-W-w)) + else: + painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X+W/2, Y-W-w)) else: - self.fImage.renderer().render(painter, source) - - # Custom knobs (Dry/Wet and Volume) - if self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_WET, self.CUSTOM_PAINT_MODE_CARLA_VOL): - # knob color - colorGreen = QColor(0x5D, 0xE7, 0x3D).lighter(100 + self.fHoverStep*6) - colorBlue = QColor(0x3E, 0xB8, 0xBE).lighter(100 + self.fHoverStep*6) - - # draw small circle - ballRect = QRectF(8.0, 8.0, 15.0, 15.0) - ballPath = QPainterPath() - ballPath.addEllipse(ballRect) - #painter.drawRect(ballRect) - tmpValue = (0.375 + 0.75*normValue) - ballValue = tmpValue - floor(tmpValue) - ballPoint = ballPath.pointAtPercent(ballValue) - - # draw arc - startAngle = 218*16 - spanAngle = -255*16*normValue - - if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_WET: - painter.setBrush(colorBlue) - painter.setPen(QPen(colorBlue, 0)) - painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2)) - - gradient = QConicalGradient(15.5, 15.5, -45) - gradient.setColorAt(0.0, colorBlue) - gradient.setColorAt(0.125, colorBlue) - gradient.setColorAt(0.625, colorGreen) - gradient.setColorAt(0.75, colorGreen) - gradient.setColorAt(0.76, colorGreen) - gradient.setColorAt(1.0, colorGreen) - painter.setBrush(gradient) - painter.setPen(QPen(gradient, 3)) + painter.setPen(QPen(gray(0.05), 0.5)) + painter.setBrush(color.darker(90 + int(300*(1-normValue)))) + painter.drawRoundedRect(squareBorder(w-W), 1, 1) + + elif skin == 1: # calf + if (normValue > 0): + grad = QLinearGradient(X-W, Y, X+W, Y) + for i in ({20.0, 45.6, 55.6, 80.0} if (normValue < 1) + else {0.0, 30.6, 40.5, 45.7, 55.7, 60.5, 70.6, 100.0}): + grad.setColorAt(int(i)/100.0, QColor.fromHslF(hue, S, (i % 1)+E, 1)) + painter.setPen(QPen(grad, w-0.5, cap=Qt.FlatCap)) + painter.drawLine(QPointF(X-W, Y-W-w/2), QPointF(X+W, Y-W-w/2)) + + elif skin == 2: # openav + painter.setPen(QPen(color, w, cap=Qt.RoundCap)) + if (normValue > 0): + painter.drawRoundedRect(squareBorder(-W * (1 - normValue)), 3, 3) + else: + painter.drawLine(QPointF(X-0.1, Y), QPointF(X+0.1, Y)) + + elif skin == 3: # zynfx + if not centerLed: + if (normValue > 0): + dx = (W - w) if (normValue < 1) else 0 + painter.setPen(QPen(gray(0), w+2)) + painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X+W/2-dx, Y-W-w)) + painter.setPen(QPen(color, w)) + painter.drawLine(QPointF(X-W/2, Y-W-w), QPointF(X+W/2-dx, Y-W-w)) + else: + painter.setPen(QPen(gray(0), 1)) + painter.setBrush(color.darker(90 + int(300*(1-normValue)))) + painter.drawEllipse(squareBorder(w-W+1)) + # do not draw marks on disabled items + if not enabled: + return + + match skin: + case 0: # internal: ball at center + if not centerLed: + self.drawMark(painter, X, Y, 0, 0, 0, w/2+0.5, color) + # case 3: # openav + # painter.setPen(QPen(color, w, cap=Qt.RoundCap)) + # painter.drawLine(QPointF(X-0.1, Y), QPointF(X+0.1, Y)) + case 3: # zynfx: ball at center + if not centerLed: + self.drawMark(painter, X, Y, 0, 0, 0, w/2, gray(1)) + + + # Just a text label not so good for fast updated display, see issue #1934. + # NOTE Work in progress. + def paintDisplay(self, painter, X, Y, H, S, L, E, normValue, enabled): + + # X, Y: Center of label. + def plotStr(self, painter, X, Y, st, fontSize, aspectRatio, skew): + + # Due to CPU/speed gain, we use simplest possible 7-segmented digits. + # Shape to Speed balance: Speed + h = ["KYNKNY ROZUZ RVKVY ROJUJ", # 0 NJ RJ VJ + "KYVJVQ RVSVZ", # 1 NR RR VR + "KYNJVJVRNRNZVZ", # 2 NZ RZ VZ + "KYNJVJVZNZ RVRNR", # 3 P] + "KYNJNRVR RVJVZ", # 4 + "KYVJNJNRVRVZNZ", # 5 + "KYVJNJNZVZVRNR", # 6 + "KYNJVJVZ", # 7 + "KYNJNZVZVJNJ RNRVR", # 8 + "KYNZVZVJNJNRVR", # 9 + "KYNRVR", # - + "OTRZP]", # . + "KYNNNX RVPNTVX", # k + "KYNXNLRRVLVX" ] # M + + def plotHersheyChar(painter, X, Y, c, fontSize, aspectRatio, skew, justGetWidth): + lm = (ord(h[c][0]) - ord('R')) * fontSize * aspectRatio + rm = (ord(h[c][1]) - ord('R')) * fontSize * aspectRatio + if justGetWidth: + return X + rm - lm + + points = [] + X = X - lm + # The speed and CPU load is critical here. + # I try to make it as efficient as possible, but can it be even faster? + for i in range(1, int(len(h[c])/2)): + a = (h[c][i*2]) + b = (h[c][i*2+1]) + if (a == ' ') and (b == 'R'): + painter.drawPolyline(points) + points = [] + else: + y = (ord(b) - ord('R')) * fontSize + x = (ord(a) - ord('R')) * fontSize * aspectRatio + skew * y + points.append(QPointF(X+x, Y+y)) + + painter.drawPolyline(points) + X = X + rm + return X + + def plotDecodedChar(painter, X, Y, st, fontSize, aspectRatio, skew, justGetWidth): + for i in range(len(st)): + digit = "0123456789-.kM".find(st[i]) + if digit < 0: + print("ERROR: Illegal char at " + str(i) + " in " + st) + else: + X = plotHersheyChar(painter, X, Y, digit, fontSize, aspectRatio, skew, justGetWidth) + return X + + widthPx = plotDecodedChar(painter, 0, Y, st, fontSize, aspectRatio, skew, 1) + plotDecodedChar(painter, X-widthPx/2, Y, st, fontSize, aspectRatio, skew, 0) + return + + def strLimDigits(x): + s = str(x) + ret = lambda x: float(x) if '.' in s else int(x) + return str(ret(s[:max(s.find('.'), 4+1)].strip('.'))) +# return str(ret(s[:max(s.find('.'), num+1 + ('-' in s))].strip('.'))) + + def plotNixie(n): + painter.setPen(QPen(QColor.fromHslF(0.05, S, L, 1), 2.5, cap=Qt.RoundCap)) + + # We use true arcs instead of polyline/Bezier. + # Arcs are perfectly matched with original tube. + # x = 0..20, y = 0..32 + digits = [[[ 2,00,18,16, 480,2400],[ 00,-8,48,40,2400,3360], + [ 2,16,18,32,3360,5280],[ -28,-8,20,40,5280,6240]], # 0 + [[10,00,10,32, 0, 0]], # 1 + [[ 1,-0.5,19,17.5,4608,8640],[1,17,30,46,1680,2880], + [1,31.5,19,31.5, 0, 0]], # 2 + [[-2,10,20,32,3500,7300],[ 2,00,19,00, 0, 0], + [19,00, 8,10, 0, 0]], # 3 + [[ 1,22,17,00, 0, 0],[ 17,00,17,32, 0, 0], + [1,22,17,22, 0, 0]], # 4 + [[-1,12,19,32,3500,7920],[ 4,00,18,00, 0, 0], + [4,00, 2,14, 0, 0]], # 5 + [[00,12,20,32, 0,5760],[ 0,-10,64,54,2150,2880]], # 6 + [[ 1,00,19,00, 0, 0],[ 19,00, 8,32, 0, 0]], # 7 + [[ 1,14,19,32, 0,5760],[ 3,00,17,14, 0,5760]], # 8 + [[00,00,20,20, 0,5760],[ 20,42,-44,-22,5030,5760]]] # 9 + + for x0, y0, x1, y1, a0, a1 in digits[n]: + if a0 == a1 == 0: + painter.drawLine(QPointF(x0+X-10, y0+Y-16), QPointF(x1+X-10, y1+Y-16)) else: - painter.setBrush(colorBlue) - painter.setPen(QPen(colorBlue, 0)) - painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2)) - - painter.setBrush(colorBlue) - painter.setPen(QPen(colorBlue, 3)) - - painter.drawArc(QRectF(4.0, 4.0, 26.0, 26.0), int(startAngle), int(spanAngle)) - - # Custom knobs (L and R) - elif self.fCustomPaintMode in (self.CUSTOM_PAINT_MODE_CARLA_L, self.CUSTOM_PAINT_MODE_CARLA_R): - # knob color - color = QColor(0xAD, 0xD5, 0x48).lighter(100 + self.fHoverStep*6) - - # draw small circle - ballRect = QRectF(7.0, 8.0, 11.0, 12.0) - ballPath = QPainterPath() - ballPath.addEllipse(ballRect) - #painter.drawRect(ballRect) - tmpValue = (0.375 + 0.75*normValue) - ballValue = tmpValue - floor(tmpValue) - ballPoint = ballPath.pointAtPercent(ballValue) - - painter.setBrush(color) - painter.setPen(QPen(color, 0)) - painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.0, 2.0)) - - # draw arc - if self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_CARLA_L: - startAngle = 218*16 - spanAngle = -255*16*normValue - else: - startAngle = 322.0*16 - spanAngle = 255.0*16*(1.0-normValue) - - painter.setPen(QPen(color, 2.5)) - painter.drawArc(QRectF(3.5, 3.5, 22.0, 22.0), int(startAngle), int(spanAngle)) - - # Custom knobs (Color) - elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_COLOR: - # knob color - color = self.fCustomPaintColor.lighter(100 + self.fHoverStep*6) - - # draw small circle - ballRect = QRectF(8.0, 8.0, 15.0, 15.0) - ballPath = QPainterPath() - ballPath.addEllipse(ballRect) - tmpValue = (0.375 + 0.75*normValue) - ballValue = tmpValue - floor(tmpValue) - ballPoint = ballPath.pointAtPercent(ballValue) - - # draw arc - startAngle = 218*16 - spanAngle = -255*16*normValue - - painter.setBrush(color) - painter.setPen(QPen(color, 0)) - painter.drawEllipse(QRectF(ballPoint.x(), ballPoint.y(), 2.2, 2.2)) - - painter.setBrush(color) - painter.setPen(QPen(color, 3)) - painter.drawArc(QRectF(4.0, 4.8, 26.0, 26.0), int(startAngle), int(spanAngle)) - - # Custom knobs (Zita) - elif self.fCustomPaintMode == self.CUSTOM_PAINT_MODE_ZITA: - a = normValue * pi * 1.5 - 2.35 - r = 10.0 - x = 10.5 - y = 10.5 - x += r * sin(a) - y -= r * cos(a) - painter.setBrush(Qt.black) - painter.setPen(QPen(Qt.black, 2)) - painter.drawLine(QPointF(11.0, 11.0), QPointF(x, y)) - - # Custom knobs + rect = QRectF(x0+X-10, y0+Y-16, x1-x0, y1-y0) + painter.drawArc(rect, a0, a1-a0) + + def squareBorder(w, dw=0): + return QRectF(X-W-w-dw, Y-W-w, (W+w+dw)*2, (W+w)*2) + + W = Y-1 # "radius" + value = self.fRealValue + hue = self.fHueA + + # if self.fIsButton: # TODO make it separate paintLED + if (self.fIsInteger and (self.fMinimum == 0) and (self.fMaximum == 1)): # TODO + # Neon lamp + if (self.fCustomPaintMode == 64): # tube + # bakelite lamp holder + self.grayGradPen(painter, X, Y, -45, -1) + self.grayGradBrush(painter, X-10, -40, 120, -2, {0.2, 50.2, 51.00, 100.00}) + painter.drawRoundedRect(squareBorder(-W*0.45), 3, 3) + + # neon lamp + if (normValue > 0): + grad = QRadialGradient(X, Y, 13) + for i in ({0.6, 20.6, 70.4, 100.0}): + grad.setColorAt(int(i)/100.0, QColor.fromHslF(0.05, 1.0, (i % 1.0) * normValue, 1)) + painter.setPen(QPen(Qt.NoPen)) + painter.setBrush(grad) + painter.drawRoundedRect(squareBorder(-W*0.6), 1.5, 1.5) + + # glass over neon lamp + self.grayGradPen(painter, X, Y, 135, -1) + self.grayGradBrush(painter, X-10, -40, 124, -2, {0.9, 50.9, 51.4, 100.4}, 0.25) + painter.drawRoundedRect(squareBorder(-W*0.6), 1.5, 1.5) + return + + painter.setPen(QPen(QColor.fromHslF(0, 0, 0.3-0.15*normValue+E, 1), 1.5)) + painter.setBrush(QColor(QColor.fromHslF(hue, S, L*(normValue*0.8+0.1), 1))) + painter.drawRoundedRect(squareBorder(-W/2-2), 1.5, 1.5) + return + + if (self.fCustomPaintMode == 64) and \ + (self.fIsInteger and (self.fMinimum >= 0) and (self.fMaximum < 20)): + #Nixie tube for 0..9, or 1 1/2 tubes for 11..19 + Y = Y - 2 + self.grayGradPen(painter, X, Y, 135, -1) + self.grayGradBrush(painter, X-10, -40, 120, -2, {0.2, 50.2, 51.00, 100.00}) + painter.drawRoundedRect(squareBorder(-2, -W*0.2), 3, 3) + + if (value < 10): + plotNixie(int(value % 10)) else: + if (value == 11): + X = X + 8 + plotNixie(1) + else: + X = X + 4 + plotNixie(int(value % 10)) + X = X - 16 + plotNixie(1) + return + + # Will it be analog display, or digital 7-segment scale? + if not self.fIsVuOutput: + unit = "" + if abs(value) >= 10000.0: + value = value / 1000.0 + unit = "k" + if abs(value) >= 10000.0: + value = value / 1000.0 + unit = "M" + # Remove trailing decimal zero and decimal point also. + if (value % 1.0) == 0: + value = int(value) + + valueStr = strLimDigits(value) + unit + valueLen = len(valueStr) + if valueLen == 0: + print("Zero length string from " + str(self.fRealValue) + " value.") return - if self.HOVER_MIN < self.fHoverStep < self.HOVER_MAX: - self.fHoverStep += 1 if self.fIsHovered else -1 - QTimer.singleShot(20, self.update) + autoFontsize = int(self.getTweak('Auto7segSize', 0)) # Full auto + autoFontwidth = int(self.getTweak('Auto7segWidth', 1)) # Width only + + skew = -0.2 + substrate = 1 + dY = 3 # Work in progress here. NOTE + fntSize = 0.9 + fntAspect = 0.5 + bgLuma = 0.05 + R = 10 # Replace to W + width = 4 + lineWidth = 2.0 + + if self.fCustomPaintMode == 0: # default / internal + fntSize = 0.75 + Y = Y - 2 + + elif self.fCustomPaintMode == 16: # calf + autoFontsize = 1 + skew = 0 + dY = 4 + bgLuma = 0.1 + R = 12 + + elif self.fCustomPaintMode == 32: # openav + autoFontsize = 1 + substrate = 0 + R = 13 + lineWidth = 3.0 + + elif self.fCustomPaintMode == 48: # zynfx + fntSize = 0.99 + Y = Y - 2 + dY = 4 + bgLuma = 0.12 + R = 12 + + elif self.fCustomPaintMode == 64: # tube + autoFontsize = 1 + R = 13 + lineWidth = 3.0 + + else: + print("Unknown paint mode "+ str(self.fCustomPaintMode) + " display.") + return + + if not self.fIsVuOutput: + if autoFontwidth and (valueLen < 4): + if autoFontsize: + fntSize = fntSize * 4/3 + fntAspect = 1.0 - (valueLen-1) * 0.2 + + lineWidth = fntSize + 0.5 # Work in progress here. NOTE + + # substrate + if substrate: + substratePen = QPen(QColor.fromHslF(0, 0, 0.4+E, 1), 0.5) + if (self.fCustomPaintMode == 64): # tube + if not self.fIsVuOutput: + self.grayGradPen(painter, X, Y, 135, -1) + self.grayGradBrush(painter, X-10, -80, 200, -2, {0.2, 50.2, 51.00, 100.00}) + painter.drawRoundedRect(squareBorder(-W*0.4, W*0.3), 3, 3) + + else: + if self.fCustomPaintMode == 16: # calf + if self.fIsVuOutput: + dY = 9 + painter.setPen(substratePen) + painter.setBrush(QColor(QColor.fromHslF(0, 0, bgLuma, 1))) + painter.drawRoundedRect(squareBorder(-dY, dY), 3, 3) + + + color = QColor.fromHslF(hue, S, L, 1) + painter.setPen(QPen(color, lineWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) + if not self.fIsVuOutput: + plotStr(self, painter, X, Y, valueStr, fntSize, fntAspect, skew); + else: + if self.fCustomPaintMode == 16: # calf # Work in progress here. NOTE + for i in range(0, 10+1): + if normValue > ((i+0.5)/11): + painter.drawLine(QPointF(X-15+i*3, Y-R/2), QPointF(X-15+i*3, Y+R/2)) + elif self.fCustomPaintMode == 64: # tube # Work in progress here. NOTE + # Draw Cat eye. + chamfer = 1.5 # It is best when 1.5 at normal zoom, and 1.0 for >2x HiDpi + + # base + self.grayGradPen(painter, X, Y, -45, -1, width=chamfer) + self.grayGradBrush(painter, X-10, -20, 83, -2, {0.2, 50.2, 51.00, 100.00}) + painter.drawEllipse(squareBorder(-W*0.2)) + + # green sectors + rays = 4 # There are 4- or 8-rays (2 or 4 notches) tubes + gradient = QConicalGradient(X, Y, 0) + for i in range(rays): + sign = 1 - (i % 2) * 2 + v = min(normValue, 1) * 1.2 + 0.05 # For output, it can be > max. + a = (i % 2) + v * sign + b = a + 0.02 * sign + if v > 0.99 : + # did you notice overlapping sectors? + gradient.setColorAt((i+a)/rays, color) + gradient.setColorAt((i+b)/rays, color.darker(130)) + else: + b = max(-1, min(1, b)) + gradient.setColorAt((i+a)/rays, color.darker(130)) + gradient.setColorAt((i+b)/rays, color.darker(300)) + + self.grayGradPen(painter, X, Y, 0, -1, width=chamfer) + painter.setBrush(gradient) + painter.drawEllipse(squareBorder(-W*0.4)) + + # cap is black itself, but looks dark green on working tube. + self.drawMark(painter, X, Y, 0, 0, 0, W/4, color.darker(800)) - else: # isEnabled() - target = QRectF(0.0, 0.0, self.fImageBaseSize, self.fImageBaseSize) - if isinstance(self.fImage, QPixmap): - painter.drawPixmap(target, self.fImage, target) else: - self.fImage.renderer().render(painter, target) + # VU scale points + for i in range(0, 5+1): + angle = ((0.5-i/5) * 110 + 90) + self.drawMark(painter, X, Y + R*0.6, R*1.3, R*1.5, angle, lineWidth-0.5, color.darker(150)) + + # VU pointer + angle = ((0.5-normValue) * 110 + 90) + self.drawMark(painter, X, Y + R*0.6, 0, R*1.3, angle, lineWidth, color) + + # Draw "settling screw" of VU meter # Work in progress here. NOTE + if substrate: + painter.setPen(substratePen) + painter.drawEllipse(QRectF(X-width, Y+R*0.6-width, width*2, width*2)) # --------------------------------------------------------------------------------------------------------------------- diff --git a/source/frontend/xycontroller-ui b/source/frontend/xycontroller-ui index 06848b457..67c9803b5 100755 --- a/source/frontend/xycontroller-ui +++ b/source/frontend/xycontroller-ui @@ -10,17 +10,21 @@ from qt_compat import qt_config if qt_config == 5: from PyQt5.QtCore import pyqtSignal, pyqtSlot, QT_VERSION, Qt, QPointF, QRectF, QSize, QTimer from PyQt5.QtGui import QColor, QPainter, QPen - from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow + from PyQt5.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow, QApplication elif qt_config == 6: from PyQt6.QtCore import pyqtSignal, pyqtSlot, QT_VERSION, Qt, QPointF, QRectF, QSize, QTimer from PyQt6.QtGui import QColor, QPainter, QPen - from PyQt6.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow + from PyQt6.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent, QMainWindow, QApplication + +import ctypes +from time import sleep # ----------------------------------------------------------------------- # Imports (Custom) from carla_shared import * from carla_utils import * +from widgets.scalabledial import ScalableDial import ui_xycontroller @@ -32,15 +36,21 @@ from externalui import ExternalUI from widgets.paramspinbox import ParamSpinBox # ------------------------------------------------------------------------------------------------------------ - -XYCONTROLLER_PARAMETER_X = 0 -XYCONTROLLER_PARAMETER_Y = 1 +XYCONTROLLER_PARAMETER_SMOOTH = 0 +XYCONTROLLER_PARAMETER_LINEAR = 1 +XYCONTROLLER_PARAMETER_SPEED = 2 +XYCONTROLLER_PARAMETER_REVERSEY = 3 +XYCONTROLLER_PARAMETER_X = 4 +XYCONTROLLER_PARAMETER_Y = 5 +XYCONTROLLER_PARAMETER_OUT_X = 6 +XYCONTROLLER_PARAMETER_OUT_Y = 7 # ------------------------------------------------------------------------------------------------------------ class XYGraphicsScene(QGraphicsScene): # signals cursorMoved = pyqtSignal(float,float) + knobsUpdate = pyqtSignal(float,float) def __init__(self, parent): QGraphicsScene.__init__(self, parent) @@ -52,9 +62,20 @@ class XYGraphicsScene(QGraphicsScene): self.m_channels = [] self.m_mouseLock = False self.m_smooth = False + self.m_linear = False + self.m_speed = 8.0 # 1.0 to 100.0 + self.m_rSmooth = False # Using right button self.m_smooth_x = 0.0 self.m_smooth_y = 0.0 + self.reverseY = 1.0 # -1.0 when reversed + + self.xpPrev = 0.0 + self.ypPrev = 0.0 + + self.time: ctypes.c_uint64 = 0 # I sure just int is quite enough here, but... + self.prevTime: ctypes.c_uint64 = 0 + self.setBackgroundBrush(Qt.black) cursorPen = QPen(QColor(255, 255, 255), 2) @@ -87,31 +108,31 @@ class XYGraphicsScene(QGraphicsScene): self.m_lineV.setX(posX) if forward: - value = posX / (self.p_size.x() + self.p_size.width()); + value = posX / (self.p_size.x() + self.p_size.width()) self.sendMIDI(value, None) else: - self.m_smooth_x = posX; + self.m_smooth_x = posX def setPosY(self, y: float, forward: bool = True): if self.m_mouseLock: - return; + return - posY = y * (self.p_size.y() + self.p_size.height()) + posY = y * (self.p_size.y() + self.p_size.height()) * self.reverseY self.m_cursor.setPos(self.m_cursor.x(), posY) self.m_lineH.setY(posY) if forward: - value = posY / (self.p_size.y() + self.p_size.height()) + value = posY / (self.p_size.y() + self.p_size.height()) * self.reverseY 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()); + self.m_smooth_x = x * (self.p_size.x() + self.p_size.width()) + self.m_smooth_y = y * (self.p_size.y() + self.p_size.height()) * self.reverseY + + def setReverseY(self, rev: bool): + self.reverseY = 1 - (int(rev) * 2) # 1.0 or -1.0 # ------------------------------------------------------------------- @@ -119,82 +140,132 @@ class XYGraphicsScene(QGraphicsScene): self.p_size.setRect(-(float(size.width())/2), -(float(size.height())/2), size.width(), - size.height()); + size.height()) + + def updatePos(self, pos: QPointF, filterSame: bool = False, knobsOnly: bool = False): + xp = pos.x() / (self.p_size.x() + self.p_size.width()) + yp = pos.y() / (self.p_size.y() + self.p_size.height()) * self.reverseY + if knobsOnly: + self.knobsUpdate.emit(xp * 100, yp * 100) + return + + self.m_cursor.setPos(pos) + self.m_lineH.setY(pos.y()) + self.m_lineV.setX(pos.x()) + + # Set 0.05% precision, yet exact final value settling, esp. zero + xp = round(xp * 1000) / 1000 + yp = round(yp * 1000) / 1000 + + self.sendMIDI(xp, yp, filterSame) + self.cursorMoved.emit(xp, yp) - def updateSmooth(self): - if not self.m_smooth: + def updateSmooth(self, time): + if not (self.m_smooth or self.m_rSmooth): return - if self.m_cursor.x() == self.m_smooth_x and self.m_cursor.y() == self.m_smooth_y: + dx = self.m_smooth_x - self.m_cursor.x() + dy = self.m_smooth_y - self.m_cursor.y() + + if dx == dy == 0: return same = 0 - if abs(self.m_cursor.x() - self.m_smooth_x) <= 0.0005: + if abs(dx) <= 0.0005: self.m_smooth_x = self.m_cursor.x() same += 1 - if abs(self.m_cursor.y() - self.m_smooth_y) <= 0.0005: + if abs(dy) <= 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) + speed = self.m_speed - self.m_cursor.setPos(pos) - self.m_lineH.setY(pos.y()) - self.m_lineV.setX(pos.x()) + mod = QApplication.keyboardModifiers() + if (mod & Qt.ControlModifier): + speed /= 2 + elif (mod & Qt.ShiftModifier): + speed *= 2 - xp = pos.x() / (self.p_size.x() + self.p_size.width()) - yp = pos.y() / (self.p_size.y() + self.p_size.height()) + if self.m_linear: + newX = self.m_cursor.x() + max(min(dx / speed, 1), -1) * speed + newY = self.m_cursor.y() + max(min(dy / speed, 1), -1) * speed + else: + precision = 64 / speed + newX = float(self.m_smooth_x + self.m_cursor.x()*(precision-1)) / precision + newY = float(self.m_smooth_y + self.m_cursor.y()*(precision-1)) / precision - self.sendMIDI(xp, yp) - self.cursorMoved.emit(xp, yp) + pos = QPointF(newX, newY) + + self.updatePos(pos, ((time - self.prevTime) == 1)) # Continuous calls or Not + + self.prevTime = time # ------------------------------------------------------------------- - def handleMousePos(self, pos: QPointF): + def handleMousePos(self, event): + + if (event.buttons() & Qt.MiddleButton): + pos = QPointF(0, 0) + else: + pos = QPointF(event.scenePos()) + if not self.p_size.contains(pos): if pos.x() < self.p_size.x(): pos.setX(self.p_size.x()) elif pos.x() > (self.p_size.x() + self.p_size.width()): - pos.setX(self.p_size.x() + self.p_size.width()); + pos.setX(self.p_size.x() + self.p_size.width()) if pos.y() < self.p_size.y(): pos.setY(self.p_size.y()) elif pos.y() > (self.p_size.y() + self.p_size.height()): pos.setY(self.p_size.y() + self.p_size.height()) + self.updatePos(pos, knobsOnly=True) + self.m_smooth_x = pos.x() self.m_smooth_y = pos.y() - if not self.m_smooth: - self.m_cursor.setPos(pos) - self.m_lineH.setY(pos.y()) - self.m_lineV.setX(pos.x()) + self.m_rSmooth = event.buttons() & Qt.RightButton - xp = pos.x() / (self.p_size.x() + self.p_size.width()); - yp = pos.y() / (self.p_size.y() + self.p_size.height()); + # When not smooth, update each time; + # (commented-out part) When smooth, update (re-send same) if click position is same (when smoothing not sends anything). (To match non-smoothed behaviour) + if (not (self.m_smooth or self.m_rSmooth)): # or (abs(self.m_cursor.x() - self.m_smooth_x) <= 0.0005 and abs(self.m_cursor.y() - self.m_smooth_y) <= 0.0005): + self.updatePos(pos) - self.sendMIDI(xp, yp) - self.cursorMoved.emit(xp, yp) - - def sendMIDI(self, xp, yp): + def sendMIDI(self, xp, yp, filterSame = False): rate = float(0xff) / 4 - msgd = ["cc2" if xp is not None and yp is not None else "cc"] + prefix = [] + msgd = [] if xp is not None: - msgd.append(self.cc_x) - msgd.append(int(xp * rate + rate)) + value = int(xp * rate + rate) + if not (filterSame and (value == self.xpPrev)): + prefix = ["cc"] + + msgd.append(self.cc_x) + msgd.append(value) + + self.xpPrev = value if yp is not None: - msgd.append(self.cc_y) - msgd.append(int(yp * rate + rate)) + value = int(yp * rate + rate) + if not (filterSame and (value == self.ypPrev)): + if prefix == []: + prefix = ["cc"] + else: + prefix = ["cc2"] + + msgd.append(self.cc_y) + msgd.append(value) - self.rparent.send(msgd) + self.ypPrev = value + + if not (prefix == []): + self.rparent.send(prefix + msgd) # ------------------------------------------------------------------- @@ -206,13 +277,13 @@ class XYGraphicsScene(QGraphicsScene): def mousePressEvent(self, event: QGraphicsSceneMouseEvent): self.m_mouseLock = True - self.handleMousePos(event.scenePos()) + self.handleMousePos(event) self.rparent.setCursor(Qt.CrossCursor) - QGraphicsScene.mousePressEvent(self, event); + QGraphicsScene.mousePressEvent(self, event) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): - self.handleMousePos(event.scenePos()) - QGraphicsScene.mouseMoveEvent(self, event); + self.handleMousePos(event) + QGraphicsScene.mouseMoveEvent(self, event) def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): self.m_mouseLock = False @@ -231,15 +302,22 @@ class XYControllerUI(ExternalUI, QMainWindow): self.fSaveSizeNowChecker = -1 + self.isXActual = False + self.isYActual = False + # --------------------------------------------------------------- # 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") + # Now knobs are QWidgets, not QDials. + self.ui.dial_x = ScalableDial(self.ui.dial_x, 0, 400, 0, -100, 100, "X", 64, -1, "%", "tube", 1, {}) + self.ui.dial_y = ScalableDial(self.ui.dial_y, 1, 400, 0, -100, 100, "Y", 64, -1, "%", "tube", 1, {}) + + # These are outputs (7-seg displays). + self.ui.dial_out_x = ScalableDial(self.ui.dial_out_x, 2, 400, 0, -100, 100, "Out X", 64, -1, "%", "tube", 1, {'Auto7segWidth':1, }, isOutput=True) + self.ui.dial_out_y = ScalableDial(self.ui.dial_out_y, 3, 400, 0, -100, 100, "Out Y", 64, -1, "%", "tube", 1, {'Auto7segWidth':1, }, isOutput=True) + self.ui.keyboard.setOctaves(10) self.ui.graphicsView.setScene(self.scene) @@ -262,6 +340,7 @@ class XYControllerUI(ExternalUI, QMainWindow): # Connect actions to functions self.scene.cursorMoved.connect(self.slot_sceneCursorMoved) + self.scene.knobsUpdate.connect(self.slot_setKnobs) self.ui.keyboard.noteOn.connect(self.slot_noteOn) self.ui.keyboard.noteOff.connect(self.slot_noteOff) @@ -271,6 +350,7 @@ class XYControllerUI(ExternalUI, QMainWindow): self.ui.dial_x.realValueChanged.connect(self.slot_knobValueChangedX) self.ui.dial_y.realValueChanged.connect(self.slot_knobValueChangedY) + if QT_VERSION >= 0x60000: self.ui.cb_control_x.currentTextChanged.connect(self.slot_checkCC_X) self.ui.cb_control_y.currentTextChanged.connect(self.slot_checkCC_Y) @@ -308,17 +388,23 @@ class XYControllerUI(ExternalUI, QMainWindow): # ------------------------------------------------------------------- + def setSmooth(self, smooth: bool): + self.scene.m_smooth = smooth + + x = self.ui.dial_x.rvalue() / 100 + y = self.ui.dial_y.rvalue() / 100 + if smooth: + self.scene.setSmoothValues(x, y) + else: + self.scene.setPosX(x, True) + self.scene.setPosY(y, True) + self.slot_sceneCursorMoved(x, y) + @pyqtSlot() def slot_updateScreen(self): self.ui.graphicsView.centerOn(0, 0) self.scene.updateSize(self.ui.graphicsView.size()) - 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): self.send(["note", True, note]) @@ -328,16 +414,34 @@ class XYControllerUI(ExternalUI, QMainWindow): self.send(["note", False, note]) @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) + def slot_knobValueChangedX(self, x:float, external:bool=False, firstRun:bool=False): + if not external: + self.sendControl(XYCONTROLLER_PARAMETER_X, x) + else: + self.ui.dial_x.setValue(x, False) + + if (not self.scene.m_smooth) or firstRun: + self.sendControl(XYCONTROLLER_PARAMETER_OUT_X, x) + self.ui.dial_out_x.setValue(x, False) + self.scene.setPosX(x / 100, True) + + else: + self.scene.setSmoothValues(x / 100, self.ui.dial_y.rvalue() / 100) @pyqtSlot(float) - def slot_knobValueChangedY(self, y: float): - self.sendControl(XYCONTROLLER_PARAMETER_Y, y) - self.scene.setPosY(y / 100, True) - self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, y / 100) + def slot_knobValueChangedY(self, y:float, external:bool=False, firstRun:bool=False): + if not external: + self.sendControl(XYCONTROLLER_PARAMETER_Y, y) + else: + self.ui.dial_y.setValue(y, False) + + if (not self.scene.m_smooth) or firstRun: + self.sendControl(XYCONTROLLER_PARAMETER_OUT_Y, y) + self.ui.dial_out_y.setValue(y, False) + self.scene.setPosY(y / 100, True) + + else: + self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, y / 100) @pyqtSlot(str) def slot_checkCC_X(self, text: str): @@ -422,20 +526,16 @@ class XYControllerUI(ExternalUI, QMainWindow): @pyqtSlot(bool) def slot_setSmooth(self, smooth): - self.scene.setSmooth(smooth) + self.setSmooth(smooth) self.sendConfigure("smooth", "yes" if smooth else "no") - - 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) + self.sendControl(XYCONTROLLER_PARAMETER_SMOOTH, int(smooth)) @pyqtSlot(float, float) def slot_sceneCursorMoved(self, xp: float, yp: float): - self.ui.dial_x.setValue(xp * 100, False) - self.ui.dial_y.setValue(yp * 100, False) - self.sendControl(XYCONTROLLER_PARAMETER_X, xp * 100) - self.sendControl(XYCONTROLLER_PARAMETER_Y, yp * 100) + self.ui.dial_out_x.setValue(xp * 100, False) + self.ui.dial_out_y.setValue(yp * 100, False) + self.sendControl(XYCONTROLLER_PARAMETER_OUT_X, xp * 100) + self.sendControl(XYCONTROLLER_PARAMETER_OUT_Y, yp * 100) @pyqtSlot(bool) def slot_showKeyboard(self, yesno): @@ -443,21 +543,47 @@ class XYControllerUI(ExternalUI, QMainWindow): self.sendConfigure("show-midi-keyboard", "yes" if yesno else "no") QTimer.singleShot(0, self.slot_updateScreen) + @pyqtSlot(float, float) + def slot_setKnobs(self, x, y): + self.ui.dial_x.setValue(x, False) + self.ui.dial_y.setValue(y, False) + self.sendControl(XYCONTROLLER_PARAMETER_X, x) + self.sendControl(XYCONTROLLER_PARAMETER_Y, y) + # ------------------------------------------------------------------- # DSP Callbacks + # NOTE It called continuously with params 6, 7 (outs, not used here), is this good? def dspParameterChanged(self, index: int, value: float): - if index == XYCONTROLLER_PARAMETER_X: - self.ui.dial_x.setValue(value, False) - self.scene.setPosX(value / 100, False) + if index == XYCONTROLLER_PARAMETER_SMOOTH: + self.ui.cb_smooth.blockSignals(True) + self.ui.cb_smooth.setChecked(bool(value)) + self.ui.cb_smooth.blockSignals(False) + self.setSmooth(bool(value)) + + elif index == XYCONTROLLER_PARAMETER_LINEAR: + self.scene.m_linear = bool(value) + + elif index == XYCONTROLLER_PARAMETER_SPEED: + self.scene.m_speed = value + + elif index == XYCONTROLLER_PARAMETER_REVERSEY: + self.scene.setReverseY(bool(value)) + + elif index == XYCONTROLLER_PARAMETER_X: + self.slot_knobValueChangedX(value, True, not self.isXActual) + self.isXActual = True + elif index == XYCONTROLLER_PARAMETER_Y: - self.ui.dial_y.setValue(value, False) - self.scene.setPosY(value / 100, False) + self.slot_knobValueChangedY(value, True, not self.isYActual) + if self.isXActual and not self.isYActual: + # Run it once, BUT when both X & Y are received (they can be shuffled). + self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, self.ui.dial_y.rvalue() / 100) + self.isYActual = True + else: return - self.scene.setSmoothValues(self.ui.dial_x.rvalue() / 100, - self.ui.dial_y.rvalue() / 100) def dspStateChanged(self, key: str, value: str): if key == "guiWidth": @@ -480,15 +606,7 @@ class XYControllerUI(ExternalUI, QMainWindow): 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) + self.setSmooth(bool(smooth)) elif key == "show-midi-keyboard": show = (value == "yes") @@ -587,9 +705,10 @@ class XYControllerUI(ExternalUI, QMainWindow): QMainWindow.resizeEvent(self, event) def timerEvent(self, event): + self.scene.time += 1 if event.timerId() == self.fIdleTimer: self.idleExternalUI() - self.scene.updateSmooth() + self.scene.updateSmooth(self.scene.time) if self.fSaveSizeNowChecker == 11: self.sendConfigure("guiWidth", str(self.width())) diff --git a/source/native-plugins/xycontroller.cpp b/source/native-plugins/xycontroller.cpp index 5b90a9465..27f90b2e7 100644 --- a/source/native-plugins/xycontroller.cpp +++ b/source/native-plugins/xycontroller.cpp @@ -29,6 +29,10 @@ class XYControllerPlugin : public NativePluginAndUiClass { public: enum Parameters { + kParamSmooth, + kParamLinear, + kParamSpeed, + kParamReverseY, kParamInX, kParamInY, kParamOutX, @@ -44,6 +48,7 @@ public: mqueueRT() { carla_zeroStruct(params); + params[kParamSpeed] = 8.0f; carla_zeroStruct(channels); channels[0] = true; } @@ -92,6 +97,56 @@ protected: hints |= NATIVE_PARAMETER_IS_OUTPUT; param.name = "Out Y"; break; + + case kParamSpeed: + param.name = "Speed"; + param.unit = "px"; + param.ranges.def = 8.0f; + param.ranges.min = 1.0f; + break; + } + + if (param.name == nullptr) + { + hints |= NATIVE_PARAMETER_IS_INTEGER | NATIVE_PARAMETER_USES_SCALEPOINTS; + param.unit = nullptr; + param.ranges.min = 0; + param.ranges.max = 1; + param.scalePointCount = 2; + + switch (index) + { + case kParamSmooth: + param.name = "Smooth"; + { + static const NativeParameterScalePoint scalePoints[2] = { + { "Thru", 0 }, + { "Smooth", 1 } + }; + param.scalePoints = scalePoints; + } + break; + case kParamLinear: + param.name = "Linear"; + { + static const NativeParameterScalePoint scalePoints[2] = { + { "Log", 0 }, + { "Linear", 1 } + }; + param.scalePoints = scalePoints; + } + break; + case kParamReverseY: + param.name = "Rev Y"; + { + static const NativeParameterScalePoint scalePoints[2] = { + { "Top to Bottom", 0 }, + { "Bottom to Top", 1 } + }; + param.scalePoints = scalePoints; + } + break; + } } param.hints = static_cast(hints); @@ -113,8 +168,14 @@ protected: { switch (index) { + case kParamSmooth: + case kParamLinear: + case kParamSpeed: + case kParamReverseY: case kParamInX: case kParamInY: + case kParamOutX: + case kParamOutY: params[index] = value; break; } @@ -147,8 +208,8 @@ protected: void process(const float* const*, float**, const uint32_t, const NativeMidiEvent* const midiEvents, const uint32_t midiEventCount) override { - params[kParamOutX] = params[kParamInX]; - params[kParamOutY] = params[kParamInY]; + // params[kParamOutX] = params[kParamInX]; + // params[kParamOutY] = params[kParamInY]; if (mqueue.isNotEmpty() && mqueueRT.tryToCopyDataFrom(mqueue)) { @@ -266,7 +327,7 @@ static const NativePluginDescriptor notesDesc = { /* audioOuts */ 0, /* midiIns */ 1, /* midiOuts */ 1, - /* paramIns */ 2, + /* paramIns */ 6, /* paramOuts */ 2, /* name */ "XY Controller", /* label */ "xycontroller",