diff --git a/data/themes/classic/edit_unlink.svg b/data/themes/classic/edit_unlink.svg new file mode 100644 index 00000000000..15526dea44a --- /dev/null +++ b/data/themes/classic/edit_unlink.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + diff --git a/data/themes/default/edit_unlink.svg b/data/themes/default/edit_unlink.svg new file mode 100644 index 00000000000..15526dea44a --- /dev/null +++ b/data/themes/default/edit_unlink.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + diff --git a/include/AutomatableModel.h b/include/AutomatableModel.h index 0f05ea343f5..f7d9b470e80 100644 --- a/include/AutomatableModel.h +++ b/include/AutomatableModel.h @@ -77,8 +77,6 @@ class LMMS_EXPORT AutomatableModel : public Model, public JournallingObject { Q_OBJECT public: - using AutoModelVector = std::vector; - enum class ScaleType { Linear, @@ -150,22 +148,26 @@ class LMMS_EXPORT AutomatableModel : public Model, public JournallingObject template inline T value( int frameOffset = 0 ) const { - if (m_controllerConnection) + // TODO + // The `m_value` should only be updated whenever the Controller value changes, + // instead of the Model calling `controller->currentValue()` every time. + // This becomes even worse in the case of linked Models, where it has to + // loop through the list of all links. + + if (m_useControllerValue) { - if (!m_useControllerValue) + if (m_controllerConnection) { - return castValue(m_value); + return castValue(controllerValue(frameOffset)); } - else + for (auto next = m_nextLink; next != this; next = next->m_nextLink) { - return castValue(controllerValue(frameOffset)); + if (next->controllerConnection() && next->useControllerValue()) + { + return castValue(fittedValue(next->controllerValue(frameOffset))); + } } } - else if (hasLinkedModels()) - { - return castValue( controllerValue( frameOffset ) ); - } - return castValue( m_value ); } @@ -211,8 +213,7 @@ class LMMS_EXPORT AutomatableModel : public Model, public JournallingObject void setInitValue( const float value ); - void setAutomatedValue( const float value ); - void setValue( const float value ); + void setValue(const float value, const bool isAutomated = false); void incValue( int steps ) { @@ -249,11 +250,10 @@ class LMMS_EXPORT AutomatableModel : public Model, public JournallingObject m_centerValue = centerVal; } - //! link @p m1 and @p m2, let @p m1 take the values of @p m2 - static void linkModels( AutomatableModel* m1, AutomatableModel* m2 ); - static void unlinkModels( AutomatableModel* m1, AutomatableModel* m2 ); - - void unlinkAllModels(); + //! link this to @p model, copying the value from @p model + void linkToModel(AutomatableModel* model); + //! @return number of other models linked to this + size_t countLinks() const; /** * @brief Saves settings (value, automation links and controller connections) of AutomatableModel into @@ -276,9 +276,9 @@ class LMMS_EXPORT AutomatableModel : public Model, public JournallingObject virtual QString displayValue( const float val ) const = 0; - bool hasLinkedModels() const + bool isLinked() const { - return !m_linkedModels.empty(); + return m_nextLink != this; } // a way to track changed values in the model and avoid using signals/slots - useful for speed-critical code. @@ -311,13 +311,14 @@ class LMMS_EXPORT AutomatableModel : public Model, public JournallingObject s_periodCounter = 0; } - bool useControllerValue() + bool useControllerValue() const { return m_useControllerValue; } public slots: virtual void reset(); + void unlink(); void unlinkControllerConnection(); void setUseControllerValue(bool b = true); @@ -367,9 +368,15 @@ public slots: loadSettings( element, "value" ); } - void linkModel( AutomatableModel* model ); - void unlinkModel( AutomatableModel* model ); + void setValueInternal(const float value); + //! linking is stored in a linked list ring + //! @return the model whose `m_nextLink` is `this`, + //! or `this` if there are no linked models + AutomatableModel* getLastLinkedModel() const; + //! @return true if the `model` is in the linked list + bool isLinkedToModel(AutomatableModel* model) const; + //! @brief Scales @value from linear to logarithmic. //! Value should be within [0,1] template T logToLinearScale( T value ) const; @@ -389,16 +396,15 @@ public slots: float m_centerValue; bool m_valueChanged; - - // currently unused? - float m_oldValue; - int m_setValueDepth; + float m_oldValue; //!< used by valueBuffer for interpolation // used to determine if step size should be applied strictly (ie. always) // or only when value set from gui (default) bool m_hasStrictStepSize; - AutoModelVector m_linkedModels; + //! an `AutomatableModel` can be linked together with others in a linked list + //! the list has no end, the last model is connected to the first forming a ring + AutomatableModel* m_nextLink; //! NULL if not appended to controller, otherwise connection info diff --git a/include/AutomatableModelView.h b/include/AutomatableModelView.h index 12b2e4d4919..2d4a88ac085 100644 --- a/include/AutomatableModelView.h +++ b/include/AutomatableModelView.h @@ -98,7 +98,6 @@ public slots: void execConnectionDialog(); void removeConnection(); void editSongGlobalAutomation(); - void unlinkAllModels(); void removeSongGlobalAutomation(); private slots: diff --git a/plugins/Vestige/Vestige.cpp b/plugins/Vestige/Vestige.cpp index 014efbc7a09..53e06f10131 100644 --- a/plugins/Vestige/Vestige.cpp +++ b/plugins/Vestige/Vestige.cpp @@ -1072,8 +1072,8 @@ void ManageVestigeInstrumentView::syncPlugin( void ) std::snprintf(paramStr.data(), paramStr.size(), "param%d", i); s_dumpValues = dump[paramStr.data()].split(":"); float f_value = LocaleHelper::toFloat(s_dumpValues.at(2)); - m_vi->knobFModel[ i ]->setAutomatedValue( f_value ); - m_vi->knobFModel[ i ]->setInitValue( f_value ); + m_vi->knobFModel[i]->setValue(f_value, true); + m_vi->knobFModel[i]->setInitValue(f_value); } } syncParameterText(); diff --git a/plugins/VstEffect/VstEffectControls.cpp b/plugins/VstEffect/VstEffectControls.cpp index 2f31d75eee9..840c1089376 100644 --- a/plugins/VstEffect/VstEffectControls.cpp +++ b/plugins/VstEffect/VstEffectControls.cpp @@ -465,8 +465,8 @@ void ManageVSTEffectView::syncPlugin() std::snprintf(paramStr.data(), paramStr.size(), "param%d", i); s_dumpValues = dump[paramStr.data()].split(":"); float f_value = LocaleHelper::toFloat(s_dumpValues.at(2)); - m_vi2->knobFModel[ i ]->setAutomatedValue( f_value ); - m_vi2->knobFModel[ i ]->setInitValue( f_value ); + m_vi2->knobFModel[i]->setValue(f_value, true); + m_vi2->knobFModel[i]->setInitValue(f_value); } } syncParameterText(); diff --git a/src/core/AutomatableModel.cpp b/src/core/AutomatableModel.cpp index 4006c1d8513..92e536a7a68 100644 --- a/src/core/AutomatableModel.cpp +++ b/src/core/AutomatableModel.cpp @@ -53,8 +53,9 @@ AutomatableModel::AutomatableModel( m_range( max - min ), m_centerValue( m_minValue ), m_valueChanged( false ), - m_setValueDepth( 0 ), + m_oldValue(val), m_hasStrictStepSize( false ), + m_nextLink(this), m_controllerConnection( nullptr ), m_valueBuffer( static_cast( Engine::audioEngine()->framesPerPeriod() ) ), m_lastUpdatedPeriod( -1 ), @@ -71,13 +72,9 @@ AutomatableModel::AutomatableModel( AutomatableModel::~AutomatableModel() { - while( m_linkedModels.empty() == false ) - { - m_linkedModels.back()->unlinkModel(this); - m_linkedModels.erase( m_linkedModels.end() - 1 ); - } + unlink(); - if( m_controllerConnection ) + if (m_controllerConnection) { delete m_controllerConnection; } @@ -294,41 +291,42 @@ void AutomatableModel::loadSettings( const QDomElement& element, const QString& -void AutomatableModel::setValue( const float value ) +void AutomatableModel::setValue(const float value, const bool isAutomated) { - m_oldValue = m_value; - ++m_setValueDepth; - const float old_val = m_value; + if (fittedValue(value) == m_value) + { + // TODO check why we need this signal, is there a better solution? + if (!isAutomated) { emit dataUnchanged(); } + return; + } + + if (!isAutomated) { addJournalCheckPoint(); } - m_value = fittedValue( value ); - if( old_val != m_value ) + // set value for this and the other linked models + setValueInternal(value); + for (auto model = m_nextLink; model != this; model = model->m_nextLink) { - // add changes to history so user can undo it - addJournalCheckPoint(); + model->setValueInternal(value); + } +} - // notify linked models - for (const auto& linkedModel : m_linkedModels) - { - if (linkedModel->m_setValueDepth < 1 && linkedModel->fittedValue(value) != linkedModel->m_value) - { - bool journalling = linkedModel->testAndSetJournalling(isJournalling()); - linkedModel->setValue(value); - linkedModel->setJournalling(journalling); - } - } + + + +void AutomatableModel::setValueInternal(const float value) +{ + m_oldValue = m_value; + m_value = fittedValue(value); + + if (m_oldValue != m_value) + { m_valueChanged = true; emit dataChanged(); } - else - { - emit dataUnchanged(); - } - --m_setValueDepth; } - template T AutomatableModel::logToLinearScale( T value ) const { return castValue( lmms::logToLinearScale( minValue(), maxValue(), static_cast( value ) ) ); @@ -362,34 +360,6 @@ void AutomatableModel::roundAt( T& value, const T& where ) const -void AutomatableModel::setAutomatedValue( const float value ) -{ - setUseControllerValue(false); - - m_oldValue = m_value; - ++m_setValueDepth; - const float oldValue = m_value; - - const float scaled_value = scaledValue( value ); - - m_value = fittedValue( scaled_value ); - - if( oldValue != m_value ) - { - // notify linked models - for (const auto& linkedModel : m_linkedModels) - { - if (!(linkedModel->controllerConnection()) && linkedModel->m_setValueDepth < 1 && - linkedModel->fittedValue(m_value) != linkedModel->m_value) - { - linkedModel->setAutomatedValue(value); - } - } - m_valueChanged = true; - emit dataChanged(); - } - --m_setValueDepth; -} @@ -459,79 +429,68 @@ float AutomatableModel::fittedValue( float value ) const - -void AutomatableModel::linkModel( AutomatableModel* model ) +AutomatableModel* AutomatableModel::getLastLinkedModel() const { - auto containsModel = std::find(m_linkedModels.begin(), m_linkedModels.end(), model) != m_linkedModels.end(); - if (!containsModel && model != this) + for (auto model = m_nextLink; ; model = model->m_nextLink) { - m_linkedModels.push_back( model ); - - if( !model->hasLinkedModels() ) - { - QObject::connect( this, SIGNAL(dataChanged()), - model, SIGNAL(dataChanged()), Qt::DirectConnection ); - } + // The last model in the circular reference links back to this + if (model->m_nextLink == this) { return model; } } } -void AutomatableModel::unlinkModel( AutomatableModel* model ) +bool AutomatableModel::isLinkedToModel(AutomatableModel* model) const { - auto it = std::find(m_linkedModels.begin(), m_linkedModels.end(), model); - if( it != m_linkedModels.end() ) + if (model == this) { return true; } + for (auto next = m_nextLink; next != this; next = next->m_nextLink) { - m_linkedModels.erase( it ); + if (next == model) { return true; } } + return false; } - - - - - -void AutomatableModel::linkModels( AutomatableModel* model1, AutomatableModel* model2 ) +size_t AutomatableModel::countLinks() const { - auto model1ContainsModel2 = std::find(model1->m_linkedModels.begin(), model1->m_linkedModels.end(), model2) != model1->m_linkedModels.end(); - if (!model1ContainsModel2 && model1 != model2) + size_t output = 0; + for (auto model = m_nextLink; model != this; model = model->m_nextLink) { - // copy data - model1->m_value = model2->m_value; - if (model1->valueBuffer() && model2->valueBuffer()) - { - std::copy_n(model2->valueBuffer()->data(), - model1->valueBuffer()->length(), - model1->valueBuffer()->data()); - } - // send dataChanged() before linking (because linking will - // connect the two dataChanged() signals) - emit model1->dataChanged(); - // finally: link the models - model1->linkModel( model2 ); - model2->linkModel( model1 ); + output++; } + return output; } -void AutomatableModel::unlinkModels( AutomatableModel* model1, AutomatableModel* model2 ) +void AutomatableModel::linkToModel(AutomatableModel* other) { - model1->unlinkModel( model2 ); - model2->unlinkModel( model1 ); + if (isLinkedToModel(other)) { return; } + + // copy data from other to this + setValue(other->m_value); + if (valueBuffer() && other->valueBuffer()) + { + std::copy_n(other->valueBuffer()->data(), + valueBuffer()->length(), + valueBuffer()->data()); + emit dataChanged(); + } + // link the models + AutomatableModel* thisEnd = getLastLinkedModel(); + AutomatableModel* otherEnd = other->getLastLinkedModel(); + thisEnd->m_nextLink = other; + otherEnd->m_nextLink = this; } -void AutomatableModel::unlinkAllModels() +void AutomatableModel::unlink() { - for( AutomatableModel* model : m_linkedModels ) - { - unlinkModels( this, model ); - } + getLastLinkedModel()->m_nextLink = m_nextLink; + m_nextLink = this; } @@ -555,11 +514,11 @@ void AutomatableModel::setControllerConnection( ControllerConnection* c ) float AutomatableModel::controllerValue( int frameOffset ) const { - if( m_controllerConnection ) + assert(m_controllerConnection != nullptr); + + float v = 0; + switch (m_scaleType) { - float v = 0; - switch(m_scaleType) - { case ScaleType::Linear: v = minValue() + ( range() * controllerConnection()->currentValue( frameOffset ) ); break; @@ -571,24 +530,14 @@ float AutomatableModel::controllerValue( int frameOffset ) const qFatal("AutomatableModel::controllerValue(int)" "lacks implementation for a scale type"); break; - } - if (approximatelyEqual(m_step, 1) && m_hasStrictStepSize) - { - return std::round(v); - } - return v; } - - AutomatableModel* lm = m_linkedModels.front(); - if (lm->controllerConnection() && lm->useControllerValue()) + if (approximatelyEqual(m_step, 1) && m_hasStrictStepSize) { - return fittedValue( lm->controllerValue( frameOffset ) ); + return std::round(v); } - - return fittedValue( lm->m_value ); + return v; } - ValueBuffer * AutomatableModel::valueBuffer() { QMutexLocker m( &m_valueBufferMutex ); @@ -602,6 +551,11 @@ ValueBuffer * AutomatableModel::valueBuffer() float val = m_value; // make sure our m_value doesn't change midway + // TODO + // Let the Controller set the value of connected Models, + // instead of each Model checking the Controller value every time. + + // Get value buffer from our controller if (m_controllerConnection && m_useControllerValue && m_controllerConnection->getController()->isSampleExact()) { auto vb = m_controllerConnection->valueBuffer(); @@ -634,32 +588,36 @@ ValueBuffer * AutomatableModel::valueBuffer() } } - if (!m_controllerConnection) + // Get value buffer from one of the linked models' controller + if (m_useControllerValue) { - AutomatableModel* lm = nullptr; - if (hasLinkedModels()) + for (auto next = m_nextLink; next != this; next = next->m_nextLink) { - lm = m_linkedModels.front(); - } - if (lm && lm->controllerConnection() && lm->useControllerValue() && - lm->controllerConnection()->getController()->isSampleExact()) - { - auto vb = lm->valueBuffer(); - float * values = vb->values(); - float * nvalues = m_valueBuffer.values(); - for (int i = 0; i < vb->length(); i++) - { - nvalues[i] = fittedValue(values[i]); - } - m_lastUpdatedPeriod = s_periodCounter; - m_hasSampleExactData = true; - return &m_valueBuffer; + if (next->controllerConnection() && next->useControllerValue() && + next->controllerConnection()->getController()->isSampleExact()) + { + auto vb = next->valueBuffer(); + float* values = vb->values(); + float* nvalues = m_valueBuffer.values(); + for (int i = 0; i < vb->length(); i++) + { + nvalues[i] = fittedValue(values[i]); + } + m_lastUpdatedPeriod = s_periodCounter; + m_hasSampleExactData = true; + return &m_valueBuffer; + } } } - if( m_oldValue != val ) + // Note: if there are linked models but no controller, `setValue()` will have + // updated `m_value` and `m_oldValue` across all linked models, so the `valueBuffer()` + // will be the same (even if it is calculated separatly for each model). + + // Populate value buffer by interpolatating between the old and new value + if (m_oldValue != val) { - m_valueBuffer.interpolate( m_oldValue, val ); + m_valueBuffer.interpolate(m_oldValue, val); m_oldValue = val; m_lastUpdatedPeriod = s_periodCounter; m_hasSampleExactData = true; diff --git a/src/core/LadspaControl.cpp b/src/core/LadspaControl.cpp index 3282a0c7a0e..4227f7c4fb2 100644 --- a/src/core/LadspaControl.cpp +++ b/src/core/LadspaControl.cpp @@ -291,16 +291,15 @@ void LadspaControl::linkControls( LadspaControl * _control ) switch( m_port->data_type ) { case BufferDataType::Toggled: - BoolModel::linkModels( &m_toggledModel, _control->toggledModel() ); + _control->toggledModel()->linkToModel(&m_toggledModel); break; case BufferDataType::Integer: case BufferDataType::Enum: case BufferDataType::Floating: - FloatModel::linkModels( &m_knobModel, _control->knobModel() ); + _control->knobModel()->linkToModel(&m_knobModel); break; case BufferDataType::Time: - TempoSyncKnobModel::linkModels( &m_tempoSyncKnobModel, - _control->tempoSyncKnobModel() ); + _control->tempoSyncKnobModel()->linkToModel(&m_tempoSyncKnobModel); break; default: break; @@ -342,16 +341,15 @@ void LadspaControl::unlinkControls( LadspaControl * _control ) switch( m_port->data_type ) { case BufferDataType::Toggled: - BoolModel::unlinkModels( &m_toggledModel, _control->toggledModel() ); + _control->toggledModel()->unlink(); break; case BufferDataType::Integer: case BufferDataType::Enum: case BufferDataType::Floating: - FloatModel::unlinkModels( &m_knobModel, _control->knobModel() ); + _control->knobModel()->unlink(); break; case BufferDataType::Time: - TempoSyncKnobModel::unlinkModels( &m_tempoSyncKnobModel, - _control->tempoSyncKnobModel() ); + _control->tempoSyncKnobModel()->unlink(); break; default: break; diff --git a/src/core/LinkedModelGroups.cpp b/src/core/LinkedModelGroups.cpp index c52bce43310..3778d4d80da 100644 --- a/src/core/LinkedModelGroups.cpp +++ b/src/core/LinkedModelGroups.cpp @@ -46,7 +46,7 @@ void LinkedModelGroup::linkControls(LinkedModelGroup *other) { auto itr2 = other->m_models.find(id); Q_ASSERT(itr2 != other->m_models.end()); - AutomatableModel::linkModels(inf.m_model, itr2->second.m_model); + inf.m_model->linkToModel(itr2->second.m_model); }); } diff --git a/src/core/Song.cpp b/src/core/Song.cpp index 441780d414a..e49d99c7cb1 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -413,7 +413,7 @@ void Song::processAutomations(const TrackList &tracklist, TimePos timeStart, fpp for (auto it = m_oldAutomatedValues.begin(); it != m_oldAutomatedValues.end(); it++) { AutomatableModel * am = it.key(); - if (am->controllerConnection() && !values.contains(am)) + if (!values.contains(am)) { am->setUseControllerValue(true); } @@ -423,13 +423,18 @@ void Song::processAutomations(const TrackList &tracklist, TimePos timeStart, fpp // Apply values for (auto it = values.begin(); it != values.end(); it++) { - if (! recordedModels.contains(it.key())) - { - it.key()->setAutomatedValue(it.value()); - } - else if (!it.key()->useControllerValue()) + AutomatableModel* model = it.key(); + bool isRecording = recordedModels.contains(model); + model->setUseControllerValue(isRecording); + + if (!isRecording) { - it.key()->setUseControllerValue(true); + /* TODO + * Remove scaleValue() from here when automation editor's + * Y axis can be set to logarithmic, and automation clips store + * the actual values, and not the invertedScaledValue. + */ + model->setValue(model->scaledValue(it.value()), true); } } } diff --git a/src/gui/AutomatableModelView.cpp b/src/gui/AutomatableModelView.cpp index 2faf74064a5..ef40da8158b 100644 --- a/src/gui/AutomatableModelView.cpp +++ b/src/gui/AutomatableModelView.cpp @@ -96,11 +96,11 @@ void AutomatableModelView::addDefaultActions( QMenu* menu ) menu->addSeparator(); - if( model->hasLinkedModels() ) + if (model->isLinked()) { - menu->addAction( embed::getIconPixmap( "edit-delete" ), - AutomatableModel::tr( "Remove all linked controls" ), - amvSlots, SLOT(unlinkAllModels())); + menu->addAction(embed::getIconPixmap("edit_unlink"), + AutomatableModel::tr("Remove all linked controls"), + model, SLOT(unlink())); menu->addSeparator(); } @@ -276,11 +276,6 @@ void AutomatableModelViewSlots::removeSongGlobalAutomation() } -void AutomatableModelViewSlots::unlinkAllModels() -{ - m_amv->modelUntyped()->unlinkAllModels(); -} - void AutomatableModelViewSlots::copyToClipboard() { // For copyString() and MimeType enum class diff --git a/src/gui/widgets/FloatModelEditorBase.cpp b/src/gui/widgets/FloatModelEditorBase.cpp index ed09fa261a4..a492bad0d1b 100644 --- a/src/gui/widgets/FloatModelEditorBase.cpp +++ b/src/gui/widgets/FloatModelEditorBase.cpp @@ -146,8 +146,7 @@ void FloatModelEditorBase::dropEvent(QDropEvent * de) auto mod = dynamic_cast(Engine::projectJournal()->journallingObject(val.toInt())); if (mod != nullptr) { - AutomatableModel::linkModels(model(), mod); - mod->setValue(model()->value()); + model()->linkToModel(mod); } } } @@ -418,12 +417,16 @@ void FloatModelEditorBase::enterValue() void FloatModelEditorBase::friendlyUpdate() { - if (model() && (model()->controllerConnection() == nullptr || - model()->controllerConnection()->getController()->frequentUpdates() == false || - Controller::runningFrames() % (256*4) == 0)) - { - update(); - } + if (model() == nullptr) { return; } + + // If the controller changes constantly, only repaint every 1024th frame + if (model()->useControllerValue() + && model()->controllerConnection() + && model()->controllerConnection()->getController()->frequentUpdates() + && Controller::runningFrames() % (256 * 4) != 0) + { return; } + + update(); } diff --git a/tests/src/core/AutomatableModelTest.cpp b/tests/src/core/AutomatableModelTest.cpp index 9bc5fd6ace2..6fa86d20c1e 100644 --- a/tests/src/core/AutomatableModelTest.cpp +++ b/tests/src/core/AutomatableModelTest.cpp @@ -78,7 +78,7 @@ private slots: // tests { using namespace lmms; - BoolModel m1(false), m2(false); + BoolModel m1(true), m2(false); QObject::connect(&m1, SIGNAL(dataChanged()), this, SLOT(onM1Changed())); @@ -86,17 +86,19 @@ private slots: // tests this, SLOT(onM2Changed())); resetChanged(); - AutomatableModel::linkModels(&m1, &m1); + m1.linkToModel(&m1); QVERIFY(!m1Changed); // cannot link to itself QVERIFY(!m2Changed); + QVERIFY(m1.countLinks() == 0); resetChanged(); - AutomatableModel::linkModels(&m1, &m2); - QVERIFY(m1Changed); // since m1 takes the value of m2 + m1.linkToModel(&m2); + QVERIFY(m1.value() == m2.value()); // since m1 takes the value of m2 QVERIFY(!m2Changed); // the second model is the source + QVERIFY(m1.countLinks() == 1); resetChanged(); - AutomatableModel::linkModels(&m1, &m2); + m1.linkToModel(&m2); QVERIFY(!m1Changed); // it's already linked QVERIFY(!m2Changed); @@ -104,15 +106,15 @@ private slots: // tests BoolModel m3(false); m1.setValue(1.f); m2.setValue(1.f); - AutomatableModel::linkModels(&m1, &m2); + m1.linkToModel(&m2); QVERIFY(m1.value()); QVERIFY(m2.value()); QVERIFY(!m3.value()); - AutomatableModel::linkModels(&m2, &m3); // drag m3, drop on m2 + m2.linkToModel(&m3); // drag m3, drop on m2 // m2 should take m3's (0) value - // due to a bug(?), this does not happen - QVERIFY(m2.value()); + QVERIFY(m2.value() == m3.value()); QVERIFY(!m3.value()); + QVERIFY(m1.countLinks() == 2); } };