From d551938cbd3a7bad3e7859ab7abd1499d7326b3b Mon Sep 17 00:00:00 2001 From: superpaik <68785450+superpaik@users.noreply.github.com> Date: Wed, 10 May 2023 13:14:24 +0200 Subject: [PATCH] Add confirm removal on mixer channels (#6691) * Add confirm removal on mixer channels Add confirm removal popup when the user calls the action "remove channel" on a mixer channel that is in use (receives audio from other channel or track). Set a config variable to keep track if the user don't want to be asked again. Adding a scroll on settings-general tab because there weren't enough space on the area. * Core Mixer function channel in use New core Mixer function to check if a given channel is in use (receives audio) --------- Co-authored-by: Hyunjin Song Co-authored-by: saker --- include/Mixer.h | 4 ++ include/MixerView.h | 1 + include/SetupDialog.h | 2 + src/core/Mixer.cpp | 36 ++++++++++++++ src/gui/MixerView.cpp | 88 ++++++++++++++++++++++------------ src/gui/modals/SetupDialog.cpp | 46 +++++++++++++++--- 6 files changed, 140 insertions(+), 37 deletions(-) diff --git a/include/Mixer.h b/include/Mixer.h index d84a1106328..6589836c49f 100644 --- a/include/Mixer.h +++ b/include/Mixer.h @@ -202,6 +202,10 @@ class LMMS_EXPORT Mixer : public Model, public JournallingObject // rename channels when moving etc. if they still have their original name void validateChannelName( int index, int oldIndex ); + // check if the index channel receives audio from any other channel + // or from any instrument or sample track + bool isChannelInUse(int index); + void toggledSolo(); void activateSolo(); void deactivateSolo(); diff --git a/include/MixerView.h b/include/MixerView.h index 9dc5abeeea2..2bb5ed4170f 100644 --- a/include/MixerView.h +++ b/include/MixerView.h @@ -95,6 +95,7 @@ class LMMS_EXPORT MixerView : public QWidget, public ModelView, // notify the view that a mixer channel was deleted void deleteChannel(int index); + bool confirmRemoval(int index); // delete all unused channels void deleteUnusedChannels(); diff --git a/include/SetupDialog.h b/include/SetupDialog.h index 6a9986d648c..27a4ce4f9f8 100644 --- a/include/SetupDialog.h +++ b/include/SetupDialog.h @@ -82,6 +82,7 @@ private slots: void toggleLetPreviewsFinish(bool enabled); void toggleSoloLegacyBehavior(bool enabled); void toggleTrackDeletionWarning(bool enabled); + void toggleMixerChannelDeletionWarning(bool enabled); void toggleMMPZ(bool enabled); void toggleDisableBackup(bool enabled); void toggleOpenLastProject(bool enabled); @@ -141,6 +142,7 @@ private slots: bool m_letPreviewsFinish; bool m_soloLegacyBehavior; bool m_trackDeletionWarning; + bool m_mixerChannelDeletionWarning; bool m_MMPZ; bool m_disableBackup; bool m_openLastProject; diff --git a/src/core/Mixer.cpp b/src/core/Mixer.cpp index 52ce2732c0a..9354687a8bb 100644 --- a/src/core/Mixer.cpp +++ b/src/core/Mixer.cpp @@ -825,5 +825,41 @@ void Mixer::validateChannelName( int index, int oldIndex ) } } +bool Mixer::isChannelInUse(int index) +{ + // check if the index mixer channel receives audio from any other channel + if (!m_mixerChannels[index]->m_receives.isEmpty()) + { + return true; + } + + // check if the destination mixer channel on any instrument or sample track is the index mixer channel + TrackContainer::TrackList tracks; + tracks += Engine::getSong()->tracks(); + tracks += Engine::patternStore()->tracks(); + + for (const auto t : tracks) + { + if (t->type() == Track::InstrumentTrack) + { + auto inst = dynamic_cast(t); + if (inst->mixerChannelModel()->value() == index) + { + return true; + } + } + else if (t->type() == Track::SampleTrack) + { + auto strack = dynamic_cast(t); + if (strack->mixerChannelModel()->value() == index) + { + return true; + } + } + } + + return false; +} + } // namespace lmms diff --git a/src/gui/MixerView.cpp b/src/gui/MixerView.cpp index a9b7bf99206..a9582b4e24b 100644 --- a/src/gui/MixerView.cpp +++ b/src/gui/MixerView.cpp @@ -23,7 +23,9 @@ */ +#include #include +#include #include #include #include @@ -385,6 +387,12 @@ void MixerView::deleteChannel(int index) // can't delete master if( index == 0 ) return; + // if there is no user confirmation, do nothing + if (!confirmRemoval(index)) + { + return; + } + // remember selected line int selLine = m_currentMixerLine->channelIndex(); @@ -397,7 +405,7 @@ void MixerView::deleteChannel(int index) // delete the view chLayout->removeWidget(m_mixerChannelViews[index]->m_mixerLine); - m_racksLayout->removeWidget( m_mixerChannelViews[index]->m_rackView ); + m_racksLayout->removeWidget(m_mixerChannelViews[index]->m_rackView); delete m_mixerChannelViews[index]->m_fader; delete m_mixerChannelViews[index]->m_muteBtn; delete m_mixerChannelViews[index]->m_soloBtn; @@ -409,56 +417,74 @@ void MixerView::deleteChannel(int index) m_channelAreaWidget->adjustSize(); // make sure every channel knows what index it is - for(int i=index + 1; im_mixerLine->setChannelIndex(i-1); + m_mixerChannelViews[i]->m_mixerLine->setChannelIndex(i - 1); } m_mixerChannelViews.remove(index); // select the next channel - if( selLine >= m_mixerChannelViews.size() ) + if (selLine >= m_mixerChannelViews.size()) { - selLine = m_mixerChannelViews.size()-1; + selLine = m_mixerChannelViews.size() - 1; } setCurrentMixerLine(selLine); updateMaxChannelSelector(); } +bool MixerView::confirmRemoval(int index) +{ + // if config variable is set to false, there is no need for user confirmation + bool needConfirm = ConfigManager::inst()->value("ui", "mixerchanneldeletionwarning", "1").toInt(); + if (!needConfirm) { return true; } + + Mixer* mix = Engine::mixer(); + + if (!mix->isChannelInUse(index)) + { + // is the channel is not in use, there is no need for user confirmation + return true; + } + + QString messageRemoveTrack = tr("This Mixer Channel is being used.\n" + "Are you sure you want to remove this channel?\n\n" + "Warning: This operation can not be undone."); + + QString messageTitleRemoveTrack = tr("Confirm removal"); + QString askAgainText = tr("Don't ask again"); + auto askAgainCheckBox = new QCheckBox(askAgainText, nullptr); + connect(askAgainCheckBox, &QCheckBox::stateChanged, [this](int state) { + // Invert button state, if it's checked we *shouldn't* ask again + ConfigManager::inst()->setValue("ui", "mixerchanneldeletionwarning", state ? "0" : "1"); + }); + + QMessageBox mb(this); + mb.setText(messageRemoveTrack); + mb.setWindowTitle(messageTitleRemoveTrack); + mb.setIcon(QMessageBox::Warning); + mb.addButton(QMessageBox::Cancel); + mb.addButton(QMessageBox::Ok); + mb.setCheckBox(askAgainCheckBox); + mb.setDefaultButton(QMessageBox::Cancel); + + int answer = mb.exec(); + + return answer == QMessageBox::Ok; +} void MixerView::deleteUnusedChannels() { - TrackContainer::TrackList tracks; - tracks += Engine::getSong()->tracks(); - tracks += Engine::patternStore()->tracks(); + Mixer* mix = Engine::mixer(); - std::vector inUse(m_mixerChannelViews.size(), false); - - //Populate inUse by checking the destination channel for every track - for (Track* t: tracks) + // Check all channels except master, delete those with no incoming sends + for (int i = m_mixerChannelViews.size() - 1; i > 0; --i) { - //The channel that this track sends to. Since master channel is always in use, - //setting this to 0 is a safe default (for tracks that don't sent to the mixer). - int channel = 0; - if (t->type() == Track::InstrumentTrack) - { - auto inst = dynamic_cast(t); - channel = inst->mixerChannelModel()->value(); - } - else if (t->type() == Track::SampleTrack) + if (!mix->isChannelInUse(i)) { - auto strack = dynamic_cast(t); - channel = strack->mixerChannelModel()->value(); + deleteChannel(i); } - inUse[channel] = true; - } - - //Check all channels except master, delete those with no incoming sends - for(int i = m_mixerChannelViews.size()-1; i > 0; --i) - { - if (!inUse[i] && Engine::mixer()->mixerChannel(i)->m_receives.isEmpty()) - { deleteChannel(i); } } } diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index 44eaf87b0c8..33505c3997d 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -112,6 +112,8 @@ SetupDialog::SetupDialog(ConfigTabs tab_to_open) : "app", "sololegacybehavior", "0").toInt()), m_trackDeletionWarning(ConfigManager::inst()->value( "ui", "trackdeletionwarning", "1").toInt()), + m_mixerChannelDeletionWarning(ConfigManager::inst()->value( + "ui", "mixerchanneldeletionwarning", "1").toInt()), m_MMPZ(!ConfigManager::inst()->value( "app", "nommpz").toInt()), m_disableBackup(!ConfigManager::inst()->value( @@ -198,6 +200,18 @@ SetupDialog::SetupDialog(ConfigTabs tab_to_open) : general_layout->setContentsMargins(0, 0, 0, 0); labelWidget(general_w, tr("General")); + // General scroll area. + auto generalScroll = new QScrollArea(general_w); + generalScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + generalScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + // General controls widget. + auto generalControls = new QWidget(general_w); + + // Path selectors layout. + auto generalControlsLayout = new QVBoxLayout; + generalControlsLayout->setSpacing(10); + auto addLedCheckBox = [&XDelta, &YDelta, this](const QString& ledText, TabWidget* tw, int& counter, bool initialState, const char* toggledSlot, bool showRestartWarning) { auto checkBox = new LedCheckBox(ledText, tw); @@ -214,7 +228,7 @@ SetupDialog::SetupDialog(ConfigTabs tab_to_open) : int counter = 0; // GUI tab. - auto gui_tw = new TabWidget(tr("Graphical user interface (GUI)"), general_w); + auto gui_tw = new TabWidget(tr("Graphical user interface (GUI)"), generalControls); addLedCheckBox(tr("Display volume as dBFS "), gui_tw, counter, m_displaydBFS, SLOT(toggleDisplaydBFS(bool)), true); @@ -236,14 +250,19 @@ SetupDialog::SetupDialog(ConfigTabs tab_to_open) : m_soloLegacyBehavior, SLOT(toggleSoloLegacyBehavior(bool)), false); addLedCheckBox(tr("Show warning when deleting tracks"), gui_tw, counter, m_trackDeletionWarning, SLOT(toggleTrackDeletionWarning(bool)), false); + addLedCheckBox(tr("Show warning when deleting a mixer channel that is in use"), gui_tw, counter, + m_mixerChannelDeletionWarning, SLOT(toggleMixerChannelDeletionWarning(bool)), false); gui_tw->setFixedHeight(YDelta + YDelta * counter); + generalControlsLayout->addWidget(gui_tw); + generalControlsLayout->addSpacing(10); + counter = 0; // Projects tab. - auto projects_tw = new TabWidget(tr("Projects"), general_w); + auto projects_tw = new TabWidget(tr("Projects"), generalControls); addLedCheckBox(tr("Compress project files by default"), projects_tw, counter, m_MMPZ, SLOT(toggleMMPZ(bool)), true); @@ -254,8 +273,12 @@ SetupDialog::SetupDialog(ConfigTabs tab_to_open) : projects_tw->setFixedHeight(YDelta + YDelta * counter); + generalControlsLayout->addWidget(projects_tw); + generalControlsLayout->addSpacing(10); + + // Language tab. - auto lang_tw = new TabWidget(tr("Language"), general_w); + auto lang_tw = new TabWidget(tr("Language"), generalControls); lang_tw->setFixedHeight(48); auto changeLang = new QComboBox(lang_tw); changeLang->move(XDelta, 20); @@ -310,11 +333,15 @@ SetupDialog::SetupDialog(ConfigTabs tab_to_open) : connect(changeLang, SIGNAL(currentIndexChanged(int)), this, SLOT(showRestartWarning())); + generalControlsLayout->addWidget(lang_tw); + generalControlsLayout->addSpacing(10); // General layout ordering. - general_layout->addWidget(gui_tw); - general_layout->addWidget(projects_tw); - general_layout->addWidget(lang_tw); + generalControlsLayout->addStretch(); + generalControls->setLayout(generalControlsLayout); + generalScroll->setWidget(generalControls); + generalScroll->setWidgetResizable(true); + general_layout->addWidget(generalScroll); general_layout->addStretch(); @@ -896,6 +923,8 @@ void SetupDialog::accept() QString::number(m_soloLegacyBehavior)); ConfigManager::inst()->setValue("ui", "trackdeletionwarning", QString::number(m_trackDeletionWarning)); + ConfigManager::inst()->setValue("ui", "mixerchanneldeletionwarning", + QString::number(m_mixerChannelDeletionWarning)); ConfigManager::inst()->setValue("app", "nommpz", QString::number(!m_MMPZ)); ConfigManager::inst()->setValue("app", "disablebackup", @@ -1017,6 +1046,11 @@ void SetupDialog::toggleTrackDeletionWarning(bool enabled) m_trackDeletionWarning = enabled; } +void SetupDialog::toggleMixerChannelDeletionWarning(bool enabled) +{ + m_mixerChannelDeletionWarning = enabled; +} + void SetupDialog::toggleMMPZ(bool enabled) {