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
-
-
-
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",