diff --git a/LASSIE/CMakeLists.txt b/LASSIE/CMakeLists.txt index 042e186d..7979b7ae 100644 --- a/LASSIE/CMakeLists.txt +++ b/LASSIE/CMakeLists.txt @@ -20,6 +20,7 @@ qt_add_executable(LASSIE src/core/EnvelopeLibraryEntry.cpp src/core/EnvelopeLibraryEntry.hpp src/core/ProjectXmlWriter.cpp src/core/ProjectXmlWriter.hpp src/dialogs/FunctionGenerator.cpp src/dialogs/FunctionGenerator.hpp src/ui/FunctionGenerator.ui + src/dialogs/GeneralPartialModifierDialog.cpp src/dialogs/GeneralPartialModifierDialog.hpp src/dialogs/PartialModifierDialog.cpp src/dialogs/PartialModifierDialog.hpp src/dialogs/PartialModifierFormat.cpp src/dialogs/PartialModifierFormat.hpp src/dialogs/functions/FunctionWidget.cpp src/dialogs/functions/FunctionWidget.hpp diff --git a/LASSIE/src/dialogs/GeneralPartialModifierDialog.cpp b/LASSIE/src/dialogs/GeneralPartialModifierDialog.cpp new file mode 100644 index 00000000..10019aea --- /dev/null +++ b/LASSIE/src/dialogs/GeneralPartialModifierDialog.cpp @@ -0,0 +1,514 @@ +#include "GeneralPartialModifierDialog.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include "FunctionGenerator.hpp" +#include +#include + +using enum FunctionReturnType; + +GeneralPartialModifierDialog::GeneralPartialModifierDialog( + int modifierType, + int maxPartialCount, + const QString& existingResultString, + QWidget* parent +) + : QDialog(parent), + m_modifierType(modifierType), + m_maxPartialCount(maxPartialCount) +{ + setWindowTitle("Customize Partials"); + resize(950, 500); + + m_mainLayout = new QVBoxLayout(this); + m_rowsLayout = new QVBoxLayout(); + + m_mainLayout->addLayout(m_rowsLayout); + m_mainLayout->addStretch(); + + QLabel* resultLabel = new QLabel("Result String", this); + m_resultPreview = new QPlainTextEdit(this); + m_resultPreview->setReadOnly(true); + m_resultPreview->setMinimumHeight(55); + + m_mainLayout->addWidget(resultLabel); + m_mainLayout->addWidget(m_resultPreview); + + QHBoxLayout* buttonLayout = new QHBoxLayout(); + + m_addNodeButton = new QPushButton("Add Node", this); + m_insertFunctionButton = new QPushButton("Insert Function", this); + m_okButton = new QPushButton("OK", this); + m_cancelButton = new QPushButton("Cancel", this); + + buttonLayout->addStretch(); + buttonLayout->addWidget(m_addNodeButton); + buttonLayout->addSpacing(20); + buttonLayout->addWidget(m_insertFunctionButton); + buttonLayout->addSpacing(20); + buttonLayout->addWidget(m_okButton); + buttonLayout->addWidget(m_cancelButton); + buttonLayout->addStretch(); + + m_mainLayout->addLayout(buttonLayout); + + connect(m_addNodeButton, &QPushButton::clicked, + this, &GeneralPartialModifierDialog::addNode); + + connect(m_insertFunctionButton, &QPushButton::clicked, + this, &GeneralPartialModifierDialog::insertFunction); + + connect(m_okButton, &QPushButton::clicked, + this, &QDialog::accept); + + connect(m_cancelButton, &QPushButton::clicked, + this, &QDialog::reject); + + populateFromResultString(existingResultString); + updateResultPreview(); +} + + +void GeneralPartialModifierDialog::addNode() +{ + if (m_maxPartialCount <= 0) { + QMessageBox::warning( + this, + "No Spectrum", + "This Bottom event does not contain a Spectrum, so partial nodes cannot be added." + ); + return; + } + + if (m_nodeCount >= m_maxPartialCount) { + QMessageBox::warning( + this, + "Partial Limit Reached", + "Cannot add more partial nodes than the maximum number of partials in this Bottom event's spectra." + ); + return; + } + + ++m_nodeCount; + + QWidget* rowContainer = new QWidget(this); + QHBoxLayout* rowLayout = new QHBoxLayout(rowContainer); + rowLayout->setContentsMargins(0, 0, 0, 0); + + QLabel* partialLabel = new QLabel( + "Partial #" + QString::number(m_nodeCount), this); + rowLayout->addWidget(partialLabel); + + PartialRow row; + row.container = rowContainer; + row.partialLabel = partialLabel; + + const EnvelopeEnabled enabled = enabledEnvelopeFields(); + + row.probabilityLabel = new QLabel("Probability:", this); + row.probability = new QLineEdit(this); + row.probability->setText(enabled.probability ? "PROB" : ""); + row.probability->setMinimumWidth(120); + + row.magnitudeLabel = new QLabel("Magnitude:", this); + row.magnitude = new QLineEdit(this); + row.magnitude->setText(enabled.magnitude ? "AMP" : ""); + row.magnitude->setMinimumWidth(120); + + row.widthLabel = new QLabel("Width:", this); + row.width = new QLineEdit(this); + row.width->setText(enabled.width ? "WIDTH" : ""); + row.width->setMinimumWidth(120); + + row.rateLabel = new QLabel("Rate Value:", this); + row.rate = new QLineEdit(this); + row.rate->setText(enabled.rate ? "RATE" : ""); + row.rate->setMinimumWidth(120); + + connect(row.probability, &QLineEdit::cursorPositionChanged, + this, [this, row]() { + trackFocusedEdit(row.probability); + }); + + connect(row.magnitude, &QLineEdit::cursorPositionChanged, + this, [this, row]() { + trackFocusedEdit(row.magnitude); + }); + + connect(row.width, &QLineEdit::cursorPositionChanged, + this, [this, row]() { + trackFocusedEdit(row.width); + }); + + connect(row.rate, &QLineEdit::cursorPositionChanged, + this, [this, row]() { + trackFocusedEdit(row.rate); + }); + connect(row.probability, &QLineEdit::textChanged, + this, &GeneralPartialModifierDialog::updateResultPreview); + + connect(row.magnitude, &QLineEdit::textChanged, + this, &GeneralPartialModifierDialog::updateResultPreview); + + connect(row.width, &QLineEdit::textChanged, + this, &GeneralPartialModifierDialog::updateResultPreview); + + connect(row.rate, &QLineEdit::textChanged, + this, &GeneralPartialModifierDialog::updateResultPreview); + + rowLayout->addWidget(row.probabilityLabel); + rowLayout->addWidget(row.probability); + + rowLayout->addWidget(row.magnitudeLabel); + rowLayout->addWidget(row.magnitude); + + rowLayout->addWidget(row.widthLabel); + rowLayout->addWidget(row.width); + + rowLayout->addWidget(row.rateLabel); + rowLayout->addWidget(row.rate); + + QPushButton* removeButton = new QPushButton("Remove Node", this); + connect(removeButton, &QPushButton::clicked, + this, [this, rowContainer]() { + removeNode(rowContainer); + }); + + rowLayout->addWidget(removeButton); + + applyRowEnabledState(row); + + m_rows.append(row); + m_rowsLayout->addWidget(rowContainer); + updateResultPreview(); +} + + +void GeneralPartialModifierDialog::insertFunction() +{ + if (!m_lastFocusedEdit) { + QMessageBox::warning(this, + "No Field Selected", + "Please select a field before inserting a function."); + return; + } + + FunctionGenerator* gen = new FunctionGenerator( + this, + functionReturnENV, + m_lastFocusedEdit->text() + ); + + if (gen) { + if (gen->exec() == QDialog::Accepted && !gen->getResultString().isEmpty()) { + m_lastFocusedEdit->setText(gen->getResultString()); + } + delete gen; + } +} + +QStringList GeneralPartialModifierDialog::enabledFieldLabels() const +{ + // Field order: + // Probability, Magnitude, Rate, Width, + // Detune Spread, Detune Direction, Detune Velocity + // + // Modifier type order from Modifiers.ui: + // 0 TREMOLO + // 1 VIBRATO + // 2 GLISSANDO + // 3 DETUNE + // 4 PHASE_MOD + // 5 AMPTRANS + // 6 FREQTRANS + // 7 WAVE_TYPE + + static const bool table[8][7] = { + /* TREMOLO */ { true, true, true, false, false, false, false }, + /* VIBRATO */ { true, true, true, false, false, false, false }, + /* GLISSANDO */ { true, true, false, false, false, false, false }, + /* DETUNE */ { true, false, false, false, true, true, true }, + /* PHASE_MOD */ { true, true, true, false, false, false, false }, + /* AMPTRANS */ { true, true, true, true, false, false, false }, + /* FREQTRANS */ { true, true, true, true, false, false, false }, + /* WAVE_TYPE */ { true, true, true, false, false, false, false }, + }; + + const QString labels[7] = { + "Probability Envelope:", + "Magnitude Envelope:", + "Rate Value Envelope:", + "Width Envelope:", + "Detune Spread:", + "Detune Direction:", + "Detune Velocity:" + }; + + QStringList result; + + if (m_modifierType < 0 || m_modifierType >= 8) { + return result; + } + + for (int i = 0; i < 7; ++i) { + if (table[m_modifierType][i]) { + result.append(labels[i]); + } + } + + return result; +} +void GeneralPartialModifierDialog::trackFocusedEdit(QLineEdit* edit) +{ + m_lastFocusedEdit = edit; +} + + + +QString GeneralPartialModifierDialog::envelopeOrNA(QLineEdit* edit) +{ + if (!edit) { + return ""; + } + + return edit->text(); +} + +QString GeneralPartialModifierDialog::buildPartialResultString() const +{ + QString result = "Partials"; + + for (const PartialRow& row : m_rows) { + result += "" + envelopeOrNA(row.probability) + ""; + result += "" + envelopeOrNA(row.magnitude) + ""; + result += "" + envelopeOrNA(row.width) + ""; + result += "" + envelopeOrNA(row.rate) + ""; + } + + result += ""; + return result; +} + +QStringList GeneralPartialModifierDialog::extractEnvelopeValues(const QString& resultString) +{ + QStringList values; + + QRegularExpression re("([\\s\\S]*?)"); + QRegularExpressionMatchIterator it = re.globalMatch(resultString); + + while (it.hasNext()) { + QRegularExpressionMatch match = it.next(); + values.append(match.captured(1)); + } + + return values; +} + +void GeneralPartialModifierDialog::populateFromResultString(const QString& resultString) +{ + if (resultString.trimmed().isEmpty()) { + return; + } + + const QStringList envelopes = extractEnvelopeValues(resultString); + + if (envelopes.isEmpty()) { + return; + } + + // CMOD currently reads 4 envelopes per partial: + // probability, magnitude, width, rate + const int valuesPerPartial = 4; + const int partialCount = envelopes.size() / valuesPerPartial; + + for (int i = 0; i < partialCount; ++i) { + const int oldRowCount = m_rows.size(); + + addNode(); + + if (m_rows.size() == oldRowCount) { + return; + } + + PartialRow& row = m_rows.last(); + + const QString probability = envelopes[i * valuesPerPartial + 0]; + const QString magnitude = envelopes[i * valuesPerPartial + 1]; + const QString width = envelopes[i * valuesPerPartial + 2]; + const QString rate = envelopes[i * valuesPerPartial + 3]; + + if (row.probability) { + row.probability->setText(probability); + } + + if (row.magnitude) { + row.magnitude->setText(magnitude); + } + + if (row.width) { + row.width->setText(width); + } + + if (row.rate) { + row.rate->setText(rate); + } + + applyRowEnabledState(row); + } +} + +GeneralPartialModifierDialog::EnvelopeEnabled +GeneralPartialModifierDialog::enabledEnvelopeFields() const +{ + EnvelopeEnabled enabled; + + switch (m_modifierType) { + case 0: // TREMOLO + enabled.probability = true; + enabled.magnitude = true; + enabled.width = false; + enabled.rate = true; + break; + + case 1: // VIBRATO + enabled.probability = true; + enabled.magnitude = true; + enabled.width = false; + enabled.rate = true; + break; + + case 2: // GLISSANDO + enabled.probability = true; + enabled.magnitude = true; + enabled.width = false; + enabled.rate = false; + break; + + case 3: // DETUNE + // DETUNE should not use PARTIAL mode. + enabled.probability = false; + enabled.magnitude = false; + enabled.width = false; + enabled.rate = false; + break; + + case 4: // PHASE_MOD + // PHASE_MOD should use Diyun's special PartialModifierDialog, + // not GeneralPartialModifierDialog. + enabled.probability = false; + enabled.magnitude = false; + enabled.width = false; + enabled.rate = false; + break; + + case 5: // AMPTRANS + enabled.probability = true; + enabled.magnitude = true; + enabled.width = true; + enabled.rate = true; + break; + + case 6: // FREQTRANS + enabled.probability = true; + enabled.magnitude = true; + enabled.width = true; + enabled.rate = true; + break; + + case 7: // WAVE_TYPE + enabled.probability = true; + enabled.magnitude = true; + enabled.width = false; + enabled.rate = true; + break; + + default: + enabled.probability = false; + enabled.magnitude = false; + enabled.width = false; + enabled.rate = false; + break; + } + + return enabled; +} + +void GeneralPartialModifierDialog::applyRowEnabledState(const PartialRow& row) const +{ + const EnvelopeEnabled enabled = enabledEnvelopeFields(); + + auto apply = [](QLabel* label, QLineEdit* edit, bool isEnabled) { + if (label) { + label->setEnabled(isEnabled); + } + + if (edit) { + edit->setEnabled(isEnabled); + + if (!isEnabled) { + edit->clear(); + } + } + }; + + apply(row.probabilityLabel, row.probability, enabled.probability); + apply(row.magnitudeLabel, row.magnitude, enabled.magnitude); + apply(row.widthLabel, row.width, enabled.width); + apply(row.rateLabel, row.rate, enabled.rate); +} + +void GeneralPartialModifierDialog::removeNode(QWidget* rowContainer) +{ + if (!rowContainer) { + return; + } + + for (int i = 0; i < m_rows.size(); ++i) { + if (m_rows[i].container == rowContainer) { + m_rows.removeAt(i); + break; + } + } + + if (m_lastFocusedEdit && rowContainer->isAncestorOf(m_lastFocusedEdit)) { + m_lastFocusedEdit = nullptr; + } + + m_rowsLayout->removeWidget(rowContainer); + rowContainer->deleteLater(); + + m_nodeCount = m_rows.size(); + renumberRows(); + updateResultPreview(); +} + +void GeneralPartialModifierDialog::renumberRows() +{ + for (int i = 0; i < m_rows.size(); ++i) { + if (m_rows[i].partialLabel) { + m_rows[i].partialLabel->setText( + "Partial #" + QString::number(i + 1) + ); + } + } +} + +void GeneralPartialModifierDialog::updateResultPreview() +{ + if (!m_resultPreview) { + return; + } + + m_resultPreview->setPlainText(buildPartialResultString()); +} + +QString GeneralPartialModifierDialog::getResultString() const +{ + return buildPartialResultString(); +} \ No newline at end of file diff --git a/LASSIE/src/dialogs/GeneralPartialModifierDialog.hpp b/LASSIE/src/dialogs/GeneralPartialModifierDialog.hpp new file mode 100644 index 00000000..d00018dd --- /dev/null +++ b/LASSIE/src/dialogs/GeneralPartialModifierDialog.hpp @@ -0,0 +1,89 @@ +#ifndef GENERALPARTIALMODIFIERDIALOG_HPP +#define GENERALPARTIALMODIFIERDIALOG_HPP + +#include +#include "../dialogs/FunctionGenerator.hpp" +#include +#include + +class QVBoxLayout; +class QPushButton; +class QLineEdit; +class QLabel; +class QWidget; +class QPlainTextEdit; + +class GeneralPartialModifierDialog : public QDialog +{ + Q_OBJECT + +public: + explicit GeneralPartialModifierDialog( + int modifierType, + int maxPartialCount, + const QString& existingResultString = QString(), + QWidget* parent = nullptr +); + QString getResultString() const; + +private: + struct PartialRow { + QWidget* container = nullptr; + QLabel* partialLabel = nullptr; + + QLabel* probabilityLabel = nullptr; + QLabel* magnitudeLabel = nullptr; + QLabel* widthLabel = nullptr; + QLabel* rateLabel = nullptr; + + QLineEdit* probability = nullptr; + QLineEdit* magnitude = nullptr; + QLineEdit* width = nullptr; + QLineEdit* rate = nullptr; + }; + + struct EnvelopeEnabled { + bool probability = true; + bool magnitude = true; + bool width = true; + bool rate = true; + }; + + QVector m_rows; + + EnvelopeEnabled enabledEnvelopeFields() const; + void applyRowEnabledState(const PartialRow& row) const; + + void removeNode(QWidget* rowContainer); + void renumberRows(); + + QString buildPartialResultString() const; + static QString envelopeOrNA(QLineEdit* edit); + + + QVBoxLayout* m_mainLayout = nullptr; + QVBoxLayout* m_rowsLayout = nullptr; + QPushButton* m_addNodeButton = nullptr; + QPushButton* m_insertFunctionButton = nullptr; + QLineEdit* m_lastFocusedEdit = nullptr; + QPushButton* m_okButton = nullptr; + QPushButton* m_cancelButton = nullptr; + + QPlainTextEdit* m_resultPreview = nullptr; + + int m_nodeCount = 0; + int m_modifierType = 0; + int m_maxPartialCount = 0; + + QStringList enabledFieldLabels() const; + void trackFocusedEdit(QLineEdit* edit); + void populateFromResultString(const QString& resultString); + static QStringList extractEnvelopeValues(const QString& resultString); + void updateResultPreview(); + +private slots: + void addNode(); + void insertFunction(); +}; + +#endif // GENERALPARTIALMODIFIERDIALOG_HPP \ No newline at end of file diff --git a/LASSIE/src/widgets/EventAttributesViewController.cpp b/LASSIE/src/widgets/EventAttributesViewController.cpp index ce31f410..9fd0b862 100644 --- a/LASSIE/src/widgets/EventAttributesViewController.cpp +++ b/LASSIE/src/widgets/EventAttributesViewController.cpp @@ -467,9 +467,11 @@ void EventAttributesViewController::saveCurrentShownEventData() { event.filter = ui->filEntry->text(); } - // save modifiers - for (Modifiers* mod : m_modifiers) { - mod->saveModifierToBackend(); + // Modifiers are only editable for Bottom events. + if (type == bottom) { + for (Modifiers* mod : m_modifiers) { + mod->saveModifierToBackend(); + } } // save layer weights @@ -579,21 +581,21 @@ void EventAttributesViewController::showCurrentEventData() { case high: case mid: case low: - case bottom: + case bottom: { ui->stackedWidget->setCurrentWidget(ui->standardPage); - if (type == bottom) { - ui->frequencyContainer->setVisible(true); - ui->loudnessContainer->setVisible(true); - ui->phaseContainer->setVisible(true); - ui->modGroupContainer->setVisible(true); - } else { - ui->frequencyContainer->setVisible(false); - ui->loudnessContainer->setVisible(false); - ui->phaseContainer->setVisible(false); - ui->modGroupContainer->setVisible(false); - } + + const bool showBottomOnlyControls = (type == bottom); + + ui->frequencyContainer->setVisible(showBottomOnlyControls); + ui->loudnessContainer->setVisible(showBottomOnlyControls); + ui->modGroupContainer->setVisible(showBottomOnlyControls); + + ui->addModifierButton->setVisible(showBottomOnlyControls); + ui->modifiersLabel->setVisible(showBottomOnlyControls); + fixStackedWidgetLayout(ui->standardPage); break; + } case sound: ui->stackedWidget->setCurrentWidget(ui->soundPage); break; @@ -639,6 +641,14 @@ void EventAttributesViewController::showCurrentEventData() { // ui->nameEntry->setText(QString::fromStdString(m_currentlyShownEvent->getEventName())); HEvent event; if(type <= bottom){ + // Clear existing modifier widgets whenever switching standard-page events. + // Modifiers should only be shown for Bottom events. + for (Modifiers* mod : m_modifiers) { + ui->modifiersLayout->removeWidget(mod); + mod->deleteLater(); + } + m_modifiers.clear(); + if(type == bottom){ const BottomEvent& bottom_event = pm->bottomevents()[m_curreventindex]; ExtraInfo extra_info = bottom_event.extra_info; @@ -658,18 +668,11 @@ void EventAttributesViewController::showCurrentEventData() { ui->powerOfTwoRadio->setChecked(freq_info.continuum_flag == 1); ui->loudnessEntry->setText(extra_info.loudness); - ui->phaseEntry->setText(extra_info.phase); ui->spaEntry->setText(extra_info.spa); ui->revEntry->setText(extra_info.reverb); ui->filEntry->setText(extra_info.filter); ui->modifierGroupEntry->setText(extra_info.modifier_group); - // clear existing Modifiers widgets - for (Modifiers* mod : m_modifiers) { - ui->modifiersLayout->removeWidget(mod); - mod->deleteLater(); - } - m_modifiers.clear(); // rebuild buttom Modifiers for (int i = 0; i < extra_info.modifiers.size(); ++i) { @@ -760,22 +763,6 @@ void EventAttributesViewController::showCurrentEventData() { ui->durationTypeUnitsRadio->setChecked(dt_flag == 1); ui->durationTypeSecondsRadio->setChecked(dt_flag == 2); - // clear and rebuild hevent modifier widgets - if (type != bottom) { - // clear existing Modifiers widgets - for (Modifiers* mod : m_modifiers) { - ui->modifiersLayout->removeWidget(mod); - mod->deleteLater(); - } - m_modifiers.clear(); - - // rebuild Modifiers - for (int i = 0; i < event.modifiers.size(); ++i) { - addModifiersUI(i); - m_modifiers[i]->setModifierData(event.modifiers[i]); - } - - } // environment if (type != bottom) { ui->spaEntry->setText(event.spa); @@ -839,9 +826,6 @@ void EventAttributesViewController::showCurrentEventData() { ui->noteNameEntry->setText(event.name); ui->noteNameEntry->setEnabled(false); ui->staffNumberEntry->setText(event.note_info.staffs); - NoteModifierSelection::load( - event.note_info.modifiers, - ui->notePage->findChildren()); }else if(type == filter){ const FilterEvent& event = pm->filterevents()[m_curreventindex]; ui->filNameEntry->setText(event.name); diff --git a/LASSIE/src/widgets/ModifierUiPolicy.hpp b/LASSIE/src/widgets/ModifierUiPolicy.hpp index 8590c27e..d2e7577c 100644 --- a/LASSIE/src/widgets/ModifierUiPolicy.hpp +++ b/LASSIE/src/widgets/ModifierUiPolicy.hpp @@ -21,12 +21,13 @@ inline bool fieldEnabled(int modifierType, int field, bool applyByPartial) }; if (modifierType < 0 || modifierType >= 8 || field < 0 || field >= fieldCount) return false; - // In PARTIAL mode CMOD reads these values exclusively from - // PartialResultString. Leaving the top-level PM controls enabled would - // make edits appear effective even though CMOD ignores them. - if (modifierType == 7 && applyByPartial) + // In PARTIAL mode, CMOD reads modifier envelopes from PartialResultString. + // Keep only that field editable so the top-level SOUND fields do not look active. + if (applyByPartial) { return field == 7; - return field == 7 ? applyByPartial : fields[modifierType][field]; + } + // In SOUND mode, PartialResultString is not used. + return field == 7 ? false : fields[modifierType][field]; } } // namespace ModifierUiPolicy diff --git a/LASSIE/src/widgets/Modifiers.cpp b/LASSIE/src/widgets/Modifiers.cpp index 09503b9f..3be31b52 100644 --- a/LASSIE/src/widgets/Modifiers.cpp +++ b/LASSIE/src/widgets/Modifiers.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -18,12 +19,15 @@ #include "../inst.hpp" #include "../dialogs/FunctionGenerator.hpp" #include "../dialogs/PartialModifierDialog.hpp" +#include "../dialogs/GeneralPartialModifierDialog.hpp" #include "ModifierUiPolicy.hpp" + using enum FunctionReturnType; namespace { constexpr int phaseModType = 7; +constexpr int detuneType = 3; // The combo box is ordered for display, while these values must retain the // stable modifier IDs serialized in project files and consumed by CMOD. @@ -57,8 +61,8 @@ Modifiers::Modifiers(Eventtype eventType, unsigned eventIndex, int modifierIndex for (int index = 0; index < modifierTypeCount; ++index) ui->modifierType->setItemData(index, modifierTypesByDisplayOrder[index]); - this->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - this->setMinimumHeight(480); + this->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + this->setMinimumHeight(0); setupUi(); ui->modifierSpreadLabel->adjustSize(); @@ -174,13 +178,154 @@ void Modifiers::setupUi() { connect(ui->modifierType, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int) { saveCurrentModifierType(ui->modifierType, getBackendLayer()); + updateApplyOptionsForModifierType(); + updateFieldsForApplyMode(); updateModState(); }); connect(ui->modifierApply, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { - getBackendLayer().applyhow_flag = index; + getBackendLayer().applyhow_flag = (index == 1); + updateFieldsForApplyMode(); updateModState(); }); + // Reduce spacing between modifier rows. + ui->modifierGroupLayout->setContentsMargins(0, 0, 0, 0); + ui->modifierGroupLayout->setSpacing(0); + + ui->modifierLayout->setContentsMargins(8, 8, 8, 8); + ui->modifierLayout->setSpacing(4); + + ui->modifierRemoveLayout->setContentsMargins(0, 0, 0, 0); + ui->modifierRemoveLayout->setSpacing(6); + + ui->modifierNameLayout->setContentsMargins(0, 0, 0, 0); + ui->modifierNameLayout->setSpacing(6); + + ui->modifierProbLayout->setContentsMargins(0, 0, 0, 0); + ui->modifierProbLayout->setSpacing(6); + + ui->modifierMagLayout->setContentsMargins(0, 0, 0, 0); + ui->modifierMagLayout->setSpacing(6); + + ui->modifierRateLayout->setContentsMargins(0, 0, 0, 0); + ui->modifierRateLayout->setSpacing(6); + + ui->modifierWidthLayout->setContentsMargins(0, 0, 0, 0); + ui->modifierWidthLayout->setSpacing(6); + + ui->modifierDetuneLayout->setContentsMargins(0, 0, 0, 0); + ui->modifierDetuneLayout->setSpacing(6); + + ui->modifierResLayout->setContentsMargins(0, 0, 0, 0); + ui->modifierResLayout->setSpacing(6); +} + + +// 3 Helpers for switching modifier fields between SOUND and PARTIAL modes +// without accidentally overwriting backend values. +void Modifiers::setSoundFieldsFromBackend() +{ + Modifier& mod = getBackendLayer(); + + ui->modifierProbEdit->blockSignals(true); + ui->modifierProbEdit->setText(mod.probability); + ui->modifierProbEdit->blockSignals(false); + + ui->modifierMagEdit->blockSignals(true); + ui->modifierMagEdit->setText(mod.amplitude); + ui->modifierMagEdit->blockSignals(false); + + ui->modifierRateEdit->blockSignals(true); + ui->modifierRateEdit->setText(mod.rate); + ui->modifierRateEdit->blockSignals(false); + + ui->modifierWidthEdit->blockSignals(true); + ui->modifierWidthEdit->setText(mod.width); + ui->modifierWidthEdit->blockSignals(false); + + ui->modifierSpreadEdit->blockSignals(true); + ui->modifierSpreadEdit->setText(mod.detune_spread); + ui->modifierSpreadEdit->blockSignals(false); + + ui->modifierDirEdit->blockSignals(true); + ui->modifierDirEdit->setText(mod.detune_direction); + ui->modifierDirEdit->blockSignals(false); + + ui->modifierVelEdit->blockSignals(true); + ui->modifierVelEdit->setText(mod.detune_velocity); + ui->modifierVelEdit->blockSignals(false); +} + +void Modifiers::clearSoundFieldsForPartialMode() +{ + ui->modifierProbEdit->blockSignals(true); + ui->modifierProbEdit->clear(); + ui->modifierProbEdit->blockSignals(false); + + ui->modifierMagEdit->blockSignals(true); + ui->modifierMagEdit->clear(); + ui->modifierMagEdit->blockSignals(false); + + ui->modifierRateEdit->blockSignals(true); + ui->modifierRateEdit->clear(); + ui->modifierRateEdit->blockSignals(false); + + ui->modifierWidthEdit->blockSignals(true); + ui->modifierWidthEdit->clear(); + ui->modifierWidthEdit->blockSignals(false); + + ui->modifierSpreadEdit->blockSignals(true); + ui->modifierSpreadEdit->clear(); + ui->modifierSpreadEdit->blockSignals(false); + + ui->modifierDirEdit->blockSignals(true); + ui->modifierDirEdit->clear(); + ui->modifierDirEdit->blockSignals(false); + + ui->modifierVelEdit->blockSignals(true); + ui->modifierVelEdit->clear(); + ui->modifierVelEdit->blockSignals(false); +} + +void Modifiers::updateFieldsForApplyMode() +{ + const bool isPartial = (ui->modifierApply->currentIndex() == 1); + Modifier& mod = getBackendLayer(); + + if (isPartial) { + clearSoundFieldsForPartialMode(); + } else { + setSoundFieldsFromBackend(); + } + + ui->modifierResEdit->blockSignals(true); + ui->modifierResEdit->setText(isPartial ? mod.partialresult_string : ""); + ui->modifierResEdit->blockSignals(false); +} + +// Keep the Apply combo box valid for the selected modifier type. +// DETUNE only supports SOUND, while other modifier types may use PARTIAL. +// This also preserves an existing PARTIAL choice when it is still allowed. +void Modifiers::updateApplyOptionsForModifierType() +{ + const int modifierType = currentModifierType(ui->modifierType); + const bool isDetune = (modifierType == detuneType); + + Modifier& mod = getBackendLayer(); + const bool shouldUsePartial = (!isDetune && mod.applyhow_flag); + + ui->modifierApply->blockSignals(true); + ui->modifierApply->clear(); + ui->modifierApply->addItem("SOUND"); + + if (!isDetune) { + ui->modifierApply->addItem("PARTIAL"); + } + + ui->modifierApply->setCurrentIndex(shouldUsePartial ? 1 : 0); + ui->modifierApply->blockSignals(false); + + mod.applyhow_flag = shouldUsePartial; } void Modifiers::updateModState() { @@ -315,38 +460,65 @@ void Modifiers::modFunctionButtonClicked(ModChanged type) { if (!target) return; - // The structured editor is deliberately PHASE_MOD-only. Other modifier - // types retain their legacy FunctionGenerator path and wire semantics. - if (type == modPartialChanged - && currentModifierType(ui->modifierType) == phaseModType) { - ProjectManager* pm = Inst::get_project_manager(); - int spectrumPartialCount = 0; - constexpr int generatedSpectrumPartialCount = 20; - if (pm->get_curr_project()) { - for (const SpectrumEvent& spectrum : pm->spectrumevents()) { - spectrumPartialCount = std::max( - spectrumPartialCount, - static_cast(spectrum.spectrum.partials.size())); - - bool isInteger = false; - const int declaredCount = spectrum.num_partials.toInt(&isInteger); - if (isInteger) - spectrumPartialCount = std::max(spectrumPartialCount, declaredCount); - - // CMOD's generated-spectrum path currently creates 20 partials. - if (!spectrum.generate_spectrum.trimmed().isEmpty()) - spectrumPartialCount = std::max(spectrumPartialCount, - generatedSpectrumPartialCount); +// In PARTIAL mode, open a structured partial editor instead of the +// legacy FunctionGenerator path. PHASE_MOD uses its specialized editor; +// other non-DETUNE modifiers use the general partial editor. +const int modifierType = currentModifierType(ui->modifierType); +const bool applyByPartial = (ui->modifierApply->currentIndex() == 1); + +if (type == modPartialChanged && applyByPartial) { + ProjectManager* pm = Inst::get_project_manager(); + int spectrumPartialCount = 0; + constexpr int generatedSpectrumPartialCount = 20; + + if (pm->get_curr_project()) { + for (const SpectrumEvent& spectrum : pm->spectrumevents()) { + spectrumPartialCount = std::max( + spectrumPartialCount, + static_cast(spectrum.spectrum.partials.size())); + + bool isInteger = false; + const int declaredCount = spectrum.num_partials.toInt(&isInteger); + if (isInteger) { + spectrumPartialCount = std::max(spectrumPartialCount, declaredCount); + } + + // CMOD's generated-spectrum path currently creates 20 partials. + if (!spectrum.generate_spectrum.trimmed().isEmpty()) { + spectrumPartialCount = std::max(spectrumPartialCount, + generatedSpectrumPartialCount); } } + } + if (modifierType == phaseModType) { PartialModifierDialog dialog(this, std::max(1, spectrumPartialCount), target->text()); - if (dialog.exec() == QDialog::Accepted) + + if (dialog.exec() == QDialog::Accepted) { target->setText(dialog.resultString()); + } + + return; + } + + if (modifierType != 3) { // DETUNE does not support PARTIAL mode. + GeneralPartialModifierDialog dialog(modifierType, + spectrumPartialCount, + target->text(), + this); + + if (dialog.exec() == QDialog::Accepted) { + const QString result = dialog.getResultString(); + if (!result.isEmpty()) { + target->setText(result); + } + } + return; } +} gen = new FunctionGenerator(nullptr, functionReturnENV, target->text()); if (gen) { @@ -356,19 +528,27 @@ void Modifiers::modFunctionButtonClicked(ModChanged type) { } } -void Modifiers::saveModifierToBackend() { - saveCurrentModifierType(ui->modifierType, getBackendLayer()); - getBackendLayer().applyhow_flag = ui->modifierApply->currentIndex(); +// Save modifier data according to the selected apply mode. +// This prevents PARTIAL mode from overwriting SOUND fields with visually cleared values. +void Modifiers::saveModifierToBackend(){ + Modifier& mod = getBackendLayer(); + saveCurrentModifierType(ui->modifierType, mod); + mod.applyhow_flag = (ui->modifierApply->currentIndex() == 1); modTextChanged(modNameChanged); - modTextChanged(modProbabilityChanged); - modTextChanged(modMagnitudeChanged); - modTextChanged(modRateChanged); - modTextChanged(modWidthChanged); - modTextChanged(modPartialChanged); - modTextChanged(modSpreadChanged); - modTextChanged(modDirChanged); - modTextChanged(modVelChanged); + if (mod.applyhow_flag) { + // PARTIAL mode: only PartialResultString is used. + modTextChanged(modPartialChanged); + } else { + // SOUND mode: only top-level sound fields are used. + modTextChanged(modProbabilityChanged); + modTextChanged(modMagnitudeChanged); + modTextChanged(modRateChanged); + modTextChanged(modWidthChanged); + modTextChanged(modSpreadChanged); + modTextChanged(modDirChanged); + modTextChanged(modVelChanged); + } } void Modifiers::setModifierData(Modifier& modData) { @@ -417,6 +597,8 @@ void Modifiers::setModifierData(Modifier& modData) { ui->modifierResEdit->setText(modData.partialresult_string); ui->modifierResEdit->blockSignals(false); + updateApplyOptionsForModifierType(); + updateFieldsForApplyMode(); updateModState(); } diff --git a/LASSIE/src/widgets/Modifiers.hpp b/LASSIE/src/widgets/Modifiers.hpp index 6fc2d5b2..c14c418f 100644 --- a/LASSIE/src/widgets/Modifiers.hpp +++ b/LASSIE/src/widgets/Modifiers.hpp @@ -53,6 +53,11 @@ private slots: void setupUi(); void updateModState(); + void updateFieldsForApplyMode(); + void setSoundFieldsFromBackend(); + void clearSoundFieldsForPartialMode(); + void updateApplyOptionsForModifierType(); + Modifier& getBackendLayer(); Eventtype m_eventType;