diff --git a/apps/kmix.cpp b/apps/kmix.cpp index 5b83d007..b9568fec 100644 --- a/apps/kmix.cpp +++ b/apps/kmix.cpp @@ -1,1288 +1,1287 @@ /* * KMix -- KDE's full featured mini mixer * * Copyright 1996-2014 The KMix authors. Maintainer: Christian Esken * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "apps/kmix.h" // include files for Qt #include #include #include #include #include #include #include #include #include #include #include #include // include files for KDE #include #include #include #include #include #include #include #include // KMix #include "gui/guiprofile.h" #include "core/ControlManager.h" #include "core/GlobalConfig.h" #include "core/MasterControl.h" #include "core/MediaController.h" #include "core/mixertoolbox.h" #include "core/kmixdevicemanager.h" #include "gui/kmixerwidget.h" #include "gui/kmixprefdlg.h" #include "gui/kmixdockwidget.h" #include "gui/kmixtoolbox.h" #include "core/version.h" #include "gui/viewdockareapopup.h" #include "gui/dialogaddview.h" #include "gui/dialogselectmaster.h" #include "dbus/dbusmixsetwrapper.h" #include "kmix_debug.h" #include #include #include /* KMixWindow * Constructs a mixer window (KMix main window) */ KMixWindow::KMixWindow(bool invisible, bool reset) : KXmlGuiWindow(0, Qt::WindowFlags( KDE_DEFAULT_WINDOWFLAGS | Qt::WindowContextHelpButtonHint)), m_multiDriverMode(false), // -<- I never-ever want the multi-drivermode to be activated by accident m_autouseMultimediaKeys(true), m_dockWidget(), m_dsm(0), m_dontSetDefaultCardOnStart(false) { setObjectName(QStringLiteral("KMixWindow")); // disable delete-on-close because KMix might just sit in the background waiting for cards to be plugged in setAttribute(Qt::WA_DeleteOnClose, false); initActions(); // init actions first, so we can use them in the loadConfig() already loadAndInitConfig(reset); // Load config before initMixer(), e.g. due to "MultiDriver" keyword initActionsLate(); // init actions that require a loaded config // TODO: Port to KF5 //KGlobal::locale()->insertCatalog(QLatin1String("kmix-controls")); initWidgets(); initPrefDlg(); DBusMixSetWrapper::initialize(this, QStringLiteral("/Mixers")); MixerToolBox::initMixer(m_multiDriverMode, m_backendFilter, true); KMixDeviceManager *theKMixDeviceManager = KMixDeviceManager::instance(); initActionsAfterInitMixer(); // init actions that require initialized mixer backend(s). recreateGUI(false, reset); if (m_wsMixers->count() < 1) { // Something is wrong. Perhaps a hardware or driver or backend change. Let KMix search harder recreateGUI(false, QString(), true, reset); } if (!qApp->isSessionRestored() ) // done by the session manager otherwise setInitialSize(); fixConfigAfterRead(); connect(theKMixDeviceManager, &KMixDeviceManager::plugged, this, &KMixWindow::plugged); connect(theKMixDeviceManager, &KMixDeviceManager::unplugged, this, &KMixWindow::unplugged); theKMixDeviceManager->initHotplug(); if (m_startVisible && !invisible) show(); // Started visible connect(qApp, SIGNAL(aboutToQuit()), SLOT(saveConfig()) ); ControlManager::instance().addListener( QString(), // All mixers (as the Global master Mixer might change) ControlManager::ControlList|ControlManager::MasterChanged, this, "KMixWindow"); // Send an initial volume refresh (otherwise all volumes are 0 until the next change) ControlManager::instance().announce(QString(), ControlManager::Volume, "Startup"); } KMixWindow::~KMixWindow() { ControlManager::instance().removeListener(this); delete m_dsm; // -1- Cleanup Memory: clearMixerWidgets while (m_wsMixers->count() != 0) { QWidget *mw = m_wsMixers->widget(0); m_wsMixers->removeTab(0); delete mw; } // -2- Mixer HW MixerToolBox::deinitMixer(); // -3- Action collection (just to please Valgrind) actionCollection()->clear(); // GUIProfile cache should be cleared very very late, as GUIProfile instances are used in the Views, which // means main window and potentially also in the tray popup (at least we might do so in the future). // This place here could be to early, if we would start to GUIProfile outside KMixWIndow, e.g. in the tray popup. // Until we do so, this is the best place to call clearCache(). Later, e.g. in main() would likely be problematic. GUIProfile::clearCache(); } void KMixWindow::controlsChange(ControlManager::ChangeType changeType) { switch (changeType) { case ControlManager::ControlList: case ControlManager::MasterChanged: updateDocking(); break; default: ControlManager::warnUnexpectedChangeType(changeType, this); break; } } void KMixWindow::initActions() { // file menu KStandardAction::quit(this, SLOT(quit()), actionCollection()); // settings menu _actionShowMenubar = KStandardAction::showMenubar(this, SLOT(toggleMenuBar()), actionCollection()); //actionCollection()->addAction(QStringLiteral( a->objectName()), a ); KStandardAction::preferences(this, SLOT(showSettings()), actionCollection()); KStandardAction::keyBindings(guiFactory(), SLOT(configureShortcuts()), actionCollection()); QAction* action = actionCollection()->addAction(QStringLiteral("launch_kdesoundsetup")); action->setText(i18n("Audio Setup...")); connect(action, SIGNAL(triggered(bool)), SLOT(slotKdeAudioSetupExec())); action = actionCollection()->addAction(QStringLiteral("hide_kmixwindow")); action->setText(i18n("Hide Mixer Window")); connect(action, SIGNAL(triggered(bool)), SLOT(hideOrClose())); actionCollection()->setDefaultShortcut(action, Qt::Key_Escape); action = actionCollection()->addAction(QStringLiteral("toggle_channels_currentview")); action->setText(i18n("Configure &Channels...")); connect(action, SIGNAL(triggered(bool)), SLOT(slotConfigureCurrentView())); action = actionCollection()->addAction(QStringLiteral("select_master")); action->setText(i18n("Select Master Channel...")); connect(action, SIGNAL(triggered(bool)), SLOT(slotSelectMaster())); action = actionCollection()->addAction(QStringLiteral("save_1")); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_1); action->setText(i18n("Save volume profile 1")); connect(action, SIGNAL(triggered(bool)), SLOT(saveVolumes1())); action = actionCollection()->addAction(QStringLiteral("save_2")); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_2); action->setText(i18n("Save volume profile 2")); connect(action, SIGNAL(triggered(bool)), SLOT(saveVolumes2())); action = actionCollection()->addAction(QStringLiteral("save_3")); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_3); action->setText(i18n("Save volume profile 3")); connect(action, SIGNAL(triggered(bool)), SLOT(saveVolumes3())); action = actionCollection()->addAction(QStringLiteral("save_4")); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::SHIFT + Qt::Key_4); action->setText(i18n("Save volume profile 4")); connect(action, SIGNAL(triggered(bool)), SLOT(saveVolumes4())); action = actionCollection()->addAction(QStringLiteral("load_1")); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_1); action->setText(i18n("Load volume profile 1")); connect(action, SIGNAL(triggered(bool)), SLOT(loadVolumes1())); action = actionCollection()->addAction(QStringLiteral("load_2")); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_2); action->setText(i18n("Load volume profile 2")); connect(action, SIGNAL(triggered(bool)), SLOT(loadVolumes2())); action = actionCollection()->addAction(QStringLiteral("load_3")); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_3); action->setText(i18n("Load volume profile 3")); connect(action, SIGNAL(triggered(bool)), SLOT(loadVolumes3())); action = actionCollection()->addAction(QStringLiteral("load_4")); actionCollection()->setDefaultShortcut(action, Qt::CTRL + Qt::Key_4); action->setText(i18n("Load volume profile 4")); connect(action, SIGNAL(triggered(bool)), SLOT(loadVolumes4())); createGUI(QLatin1String("kmixui.rc")); } void KMixWindow::initActionsLate() { if (m_autouseMultimediaKeys) { QAction* globalAction = actionCollection()->addAction(QStringLiteral("increase_volume")); globalAction->setText(i18n("Increase Volume")); KGlobalAccel::setGlobalShortcut(globalAction, Qt::Key_VolumeUp); connect(globalAction, SIGNAL(triggered(bool)), SLOT(slotIncreaseVolume())); globalAction = actionCollection()->addAction(QStringLiteral("decrease_volume")); globalAction->setText(i18n("Decrease Volume")); KGlobalAccel::setGlobalShortcut(globalAction, Qt::Key_VolumeDown); connect(globalAction, SIGNAL(triggered(bool)), SLOT(slotDecreaseVolume())); globalAction = actionCollection()->addAction(QStringLiteral("mute")); globalAction->setText(i18n("Mute")); KGlobalAccel::setGlobalShortcut(globalAction, Qt::Key_VolumeMute); connect(globalAction, SIGNAL(triggered(bool)), SLOT(slotMute())); } } void KMixWindow::initActionsAfterInitMixer() { // Only show the new tab widget if Pulseaudio is not used. Hint: The Pulseaudio backend always // runs with 4 fixed Tabs. if (!Mixer::pulseaudioPresent()) { QPushButton* _cornerLabelNew = new QPushButton(); _cornerLabelNew->setIcon(QIcon::fromTheme("tab-new")); _cornerLabelNew->setToolTip(i18n("Add new view")); //cornerLabelNew->setSizePolicy(QSizePolicy()); m_wsMixers->setCornerWidget(_cornerLabelNew, Qt::TopLeftCorner); connect(_cornerLabelNew, SIGNAL(clicked()), SLOT(newView())); } } void KMixWindow::initPrefDlg() { KMixPrefDlg* prefDlg = KMixPrefDlg::createInstance(this, GlobalConfig::instance()); connect(prefDlg, SIGNAL(kmixConfigHasChanged()), SLOT(applyPrefs())); } void KMixWindow::initWidgets() { m_wsMixers = new QTabWidget(); m_wsMixers->setDocumentMode(true); setCentralWidget(m_wsMixers); m_wsMixers->setTabsClosable(false); connect(m_wsMixers, SIGNAL(tabCloseRequested(int)), SLOT(saveAndCloseView(int))); connect(m_wsMixers, SIGNAL(currentChanged(int)), SLOT(newMixerShown(int))); // show menubar if the actions says so (or if the action does not exist) menuBar()->setVisible((_actionShowMenubar == 0) || _actionShowMenubar->isChecked()); } void KMixWindow::setInitialSize() { KConfigGroup config(KSharedConfig::openConfig(), "Global"); // HACK: QTabWidget will bound its sizeHint to 200x200 unless scrollbuttons // are disabled, so we disable them, get a decent sizehint and enable them // back m_wsMixers->setUsesScrollButtons(false); QSize defSize = sizeHint(); m_wsMixers->setUsesScrollButtons(true); QSize size = config.readEntry("Size", defSize); if (!size.isEmpty()) resize(size); QPoint defPos = pos(); QPoint pos = config.readEntry("Position", defPos); move(pos); } void KMixWindow::removeDock() { if (m_dockWidget) { m_dockWidget->deleteLater(); m_dockWidget = 0; } } /** * Creates or deletes the KMixDockWidget, depending on whether there is a Mixer instance available. * * @returns true, if the docking succeeded. Failure usually means that there * was no suitable mixer control selected. */ bool KMixWindow::updateDocking() { GlobalConfigData& gcd = GlobalConfig::instance().data; if (!gcd.showDockWidget || Mixer::mixers().isEmpty()) { removeDock(); return false; } if (!m_dockWidget) { m_dockWidget = new KMixDockWidget(this); } return true; } void KMixWindow::saveConfig() { saveBaseConfig(); saveViewConfig(); saveVolumes(); #ifdef __GNUC_ #warn We must Sync here, or we will lose configuration data. The reson for that is unknown. #endif // TODO cesken The reason for not writing might be that we have multiple cascaded KConfig objects. I must migrate to KSharedConfig !!! KSharedConfig::openConfig()->sync(); qCDebug(KMIX_LOG) << "Saved config ... sync finished"; } void KMixWindow::saveBaseConfig() { GlobalConfig::instance().save(); KConfigGroup config(KSharedConfig::openConfig(), "Global"); config.writeEntry("Size", size()); config.writeEntry("Position", pos()); // Cannot use isVisible() here, as in the "aboutToQuit()" case this widget is already hidden. // (Please note that the problem was only there when quitting via Systray - esken). // Using it again, as internal behaviour has changed with KDE4 config.writeEntry("Visible", isVisible()); config.writeEntry("Menubar", _actionShowMenubar->isChecked()); config.writeEntry("Soundmenu.Mixers", GlobalConfig::instance().getMixersForSoundmenu().values()); config.writeEntry("DefaultCardOnStart", m_defaultCardOnStart); config.writeEntry("ConfigVersion", KMIX_CONFIG_VERSION); config.writeEntry("AutoUseMultimediaKeys", m_autouseMultimediaKeys); MasterControl& master = Mixer::getGlobalMasterPreferred(false); config.writeEntry("MasterMixer", master.getCard()); config.writeEntry("MasterMixerDevice", master.getControl()); QString mixerIgnoreExpression = MixerToolBox::mixerIgnoreExpression(); config.writeEntry("MixerIgnoreExpression", mixerIgnoreExpression); qCDebug(KMIX_LOG) << "Base configuration saved"; } void KMixWindow::saveViewConfig() { QMap mixerViews; // The following loop is necessary for the case that the user has hidden all views for a Mixer instance. // Otherwise we would not save the Meta information (step -2- below for that mixer. // We also do not save dynamic mixers (e.g. PulseAudio) foreach ( Mixer* mixer, Mixer::mixers() ) { mixerViews[mixer->id()]; // just insert a map entry } // -1- Save the views themselves for (int i = 0; i < m_wsMixers->count(); ++i) { QWidget *w = m_wsMixers->widget(i); KMixerWidget *mw = qobject_cast(w); if (mw!=nullptr) { // Here also Views are saved. even for Mixers that are closed. This is necessary when unplugging cards. // Otherwise the user will be confused afer re-plugging the card (as the config was not saved). mw->saveConfig(KSharedConfig::openConfig().data()); // add the view to the corresponding mixer list, so we can save a views-per-mixer list below // if (!mw->mixer()->isDynamic()) // { QStringList& qsl = mixerViews[mw->mixer()->id()]; qsl.append(mw->getGuiprof()->getId()); // } } } // -2- Save Meta-Information (which views, and in which order). views-per-mixer list KConfigGroup pconfig(KSharedConfig::openConfig(), "Profiles"); QMap::const_iterator itEnd = mixerViews.constEnd(); for (QMap::const_iterator it = mixerViews.constBegin(); it != itEnd; ++it) { const QString& mixerProfileKey = it.key(); // this is actually some mixer->id() const QStringList& qslProfiles = it.value(); pconfig.writeEntry(mixerProfileKey, qslProfiles); qCDebug(KMIX_LOG) << "Save Profile List for " << mixerProfileKey << ", number of views is " << qslProfiles.count(); } qCDebug(KMIX_LOG) << "View configuration saved"; } /** * Stores the volumes of all mixers Can be restored via loadVolumes() or * the kmixctrl application. */ void KMixWindow::saveVolumes() { saveVolumes(QString()); } void KMixWindow::saveVolumes(const QString &postfix) { const QString& kmixctrlRcFilename = getKmixctrlRcFilename(postfix); KConfig *cfg = new KConfig(kmixctrlRcFilename); for (int i = 0; i < Mixer::mixers().count(); ++i) { Mixer *mixer = (Mixer::mixers())[i]; if (mixer->isOpen()) { // protect from unplugged devices (better do *not* save them) mixer->volumeSave(cfg); } } cfg->sync(); delete cfg; qCDebug(KMIX_LOG) << "Volume configuration saved"; } QString KMixWindow::getKmixctrlRcFilename(const QString &postfix) { QString kmixctrlRcFilename("kmixctrlrc"); if (!postfix.isEmpty()) { kmixctrlRcFilename.append(".").append(postfix); } return kmixctrlRcFilename; } void KMixWindow::loadAndInitConfig(bool reset) { if (!reset) { loadBaseConfig(); } //loadViewConfig(); // mw->loadConfig() explicitly called always after creating mw. //loadVolumes(); // not in use // create an initial snapshot, so we have a reference of the state before changes through the preferences dialog configDataSnapshot = GlobalConfig::instance().data; } void KMixWindow::loadBaseConfig() { KConfigGroup config(KSharedConfig::openConfig(), "Global"); GlobalConfigData& gcd = GlobalConfig::instance().data; #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) QList preferredMixersInSoundMenu; preferredMixersInSoundMenu = config.readEntry("Soundmenu.Mixers", preferredMixersInSoundMenu); GlobalConfig::instance().setMixersForSoundmenu(preferredMixersInSoundMenu.toSet()); #else const QStringList preferredMixers = config.readEntry("Soundmenu.Mixers", QStringList()); const QSet preferredMixersSet(preferredMixers.begin(), preferredMixers.end()); GlobalConfig::instance().setMixersForSoundmenu(preferredMixersSet); #endif m_startVisible = config.readEntry("Visible", false); m_multiDriverMode = config.readEntry("MultiDriver", false); m_defaultCardOnStart = config.readEntry("DefaultCardOnStart", ""); m_configVersion = config.readEntry("ConfigVersion", 0); // WARNING Don't overwrite m_configVersion with the "correct" value, before having it // evaluated. Better only write that in saveBaseConfig() m_autouseMultimediaKeys = config.readEntry("AutoUseMultimediaKeys", true); QString mixerMasterCard = config.readEntry("MasterMixer", ""); QString masterDev = config.readEntry("MasterMixerDevice", ""); Mixer::setGlobalMaster(mixerMasterCard, masterDev, true); QString mixerIgnoreExpression = config.readEntry("MixerIgnoreExpression", "Modem"); MixerToolBox::setMixerIgnoreExpression(mixerIgnoreExpression); // --- Advanced options, without GUI: START ------------------------------------- QString volumePercentageStepString = config.readEntry("VolumePercentageStep"); if (!volumePercentageStepString.isNull()) { float volumePercentageStep = volumePercentageStepString.toFloat(); if (volumePercentageStep > 0 && volumePercentageStep <= 100) Volume::VOLUME_STEP_DIVISOR = (100 / volumePercentageStep); } // --- Advanced options, without GUI: END ------------------------------------- // The following log is very helpful in bug reports. Please keep it. m_backendFilter = config.readEntry<>("Backends", QList()); qCDebug(KMIX_LOG) << "Backends: " << m_backendFilter; // show/hide menu bar bool showMenubar = config.readEntry("Menubar", true); if (_actionShowMenubar!=nullptr) _actionShowMenubar->setChecked(showMenubar); } /** * Loads the volumes of all mixers from kmixctrlrc. * In other words: * Restores the default volumes as stored via saveVolumes() or the * execution of "kmixctrl --save" */ void KMixWindow::loadVolumes() { loadVolumes(QString()); } void KMixWindow::loadVolumes(QString postfix) { qCDebug(KMIX_LOG) << "About to load config (Volume)"; const QString& kmixctrlRcFilename = getKmixctrlRcFilename(postfix); KConfig *cfg = new KConfig(kmixctrlRcFilename); for (int i = 0; i < Mixer::mixers().count(); ++i) { Mixer *mixer = (Mixer::mixers())[i]; mixer->volumeLoad(cfg); } delete cfg; } void KMixWindow::recreateGUIwithSavingView() { recreateGUI(true, false); } void KMixWindow::recreateGUI(bool saveConfig, bool reset) { recreateGUI(saveConfig, QString(), false, reset); } /** * Create or recreate the Mixer GUI elements * * @param saveConfig Whether to save all View configurations before recreating * @param forceNewTab To enforce opening a new tab, even when the profileList in the kmixrc is empty. * It should only be set to "true" in case of a Hotplug (because then the user definitely expects a new Tab to show). */ void KMixWindow::recreateGUI(bool saveConfig, const QString& mixerId, bool forceNewTab, bool reset) { // -1- Remember which of the tabs is currently selected for restoration for re-insertion int oldTabPosition = m_wsMixers->currentIndex(); if (!reset && saveConfig) saveViewConfig(); // save the state before recreating // -2- RECREATE THE ALREADY EXISTING TABS ********************************** QHash mixerHasProfile; // -2a- Build a list of all active profiles in the main window (that means: from all tabs) QList activeGuiProfiles; for (int i = 0; i < m_wsMixers->count(); ++i) { KMixerWidget* kmw = dynamic_cast(m_wsMixers->widget(i)); if (kmw) { activeGuiProfiles.append(kmw->getGuiprof()); } } foreach ( GUIProfile* guiprof, activeGuiProfiles) { Mixer *mixer = Mixer::findMixer( guiprof->getMixerId() ); if ( mixer == 0 ) { qCCritical(KMIX_LOG) << "MixerToolBox::find() hasn't found the Mixer for the profile " << guiprof->getId(); continue; } mixerHasProfile[mixer] = true; KMixerWidget* kmw = findKMWforTab(guiprof->getId()); if ( kmw == 0 ) { // does not yet exist => create addMixerWidget(mixer->id(), guiprof->getId(), -1); } else { // did exist => remove and insert new guiprof at old position int indexOfTab = m_wsMixers->indexOf(kmw); if ( indexOfTab != -1 ) m_wsMixers->removeTab(indexOfTab); delete kmw; addMixerWidget(mixer->id(), guiprof->getId(), indexOfTab); } } // Loop over all GUIProfile's // -3- ADD TABS FOR Mixer instances that have no tab yet ********************************** KConfigGroup pconfig(KSharedConfig::openConfig(), "Profiles"); foreach ( Mixer *mixer, Mixer::mixers()) { if ( mixerHasProfile.contains(mixer)) { continue; // OK, this mixer already has a profile => skip it } // ========================================================================================= // No TAB YET => This should mean KMix is just started, or the user has just plugged in a card { GUIProfile* guiprof = 0; if (reset) { guiprof = GUIProfile::find(mixer, QString("default"), false, true); // ### Card unspecific profile ### } if ( guiprof != 0 ) { guiprof->setDirty(); // All fallback => dirty addMixerWidget(mixer->id(), guiprof->getId(), -1); continue; } } // ========================================================================================= // The trivial cases have not added anything => Look at [Profiles] in config file QStringList profileList = pconfig.readEntry( mixer->id(), QStringList() ); bool allProfilesRemovedByUser = pconfig.hasKey(mixer->id()) && profileList.isEmpty(); if (allProfilesRemovedByUser) { continue; // User has explicitly hidden the views => do no further checks } { bool atLeastOneProfileWasAdded = false; foreach ( QString profileId, profileList) { // This handles the profileList form the kmixrc qCDebug(KMIX_LOG) << "Searching for GUI profile" << profileId; GUIProfile* guiprof = GUIProfile::find(mixer, profileId, true, false);// ### Card specific profile ### if (guiprof==nullptr) { qCWarning(KMIX_LOG) << "Cannot load profile" << profileId; if (profileId.startsWith(QLatin1String("MPRIS2."))) { profileId = "MPRIS2.default"; qCDebug(KMIX_LOG) << "For MPRIS2 falling back to" << profileId; guiprof = GUIProfile::find(mixer, profileId, true, false); } } if (guiprof!=nullptr) { addMixerWidget(mixer->id(), guiprof->getId(), -1); atLeastOneProfileWasAdded = true; } } if (atLeastOneProfileWasAdded) { // Continue continue; } } // ========================================================================================= // Neither trivial cases have added something, nor the anything => Look at [Profiles] in config file // The we_need_a_fallback case is a bit tricky. Please ask the author (cesken) before even considering to change the code. bool mixerIdMatch = mixerId.isEmpty() || (mixer->id() == mixerId); bool thisMixerShouldBeForced = forceNewTab && mixerIdMatch; bool we_need_a_fallback = mixerIdMatch && thisMixerShouldBeForced; if ( we_need_a_fallback ) { // The profileList was empty or nothing could be loaded // (Hint: This means the user cannot hide a device completely // Lets try a bunch of fallback strategies: qCDebug(KMIX_LOG) << "Attempting to find a card-specific GUI Profile for the mixer " << mixer->id(); GUIProfile* guiprof = GUIProfile::find(mixer, QString("default"), false, false);// ### Card specific profile ### if ( guiprof == 0 ) { qCDebug(KMIX_LOG) << "Not found. Attempting to find a generic GUI Profile for the mixer " << mixer->id(); guiprof = GUIProfile::find(mixer, QString("default"), false, true); // ### Card unspecific profile ### } if ( guiprof == 0) { qCDebug(KMIX_LOG) << "Using fallback GUI Profile for the mixer " << mixer->id(); // This means there is neither card specific nor card unspecific profile // This is the case for some backends (as they don't ship profiles). guiprof = GUIProfile::fallbackProfile(mixer); } if ( guiprof != 0 ) { guiprof->setDirty(); // All fallback => dirty addMixerWidget(mixer->id(), guiprof->getId(), -1); } else { qCCritical(KMIX_LOG) << "Cannot use ANY profile (including Fallback) for mixer " << mixer->id() << " . This is impossible, and thus this mixer can NOT be used."; } } } mixerHasProfile.clear(); // -4- FINALIZE ********************************** if (m_wsMixers->count() > 0) { if (oldTabPosition >= 0) { m_wsMixers->setCurrentIndex(oldTabPosition); } bool dockingSucceded = updateDocking(); if (!dockingSucceded && !Mixer::mixers().empty()) { show(); // avoid invisible and inaccessible main window } } else { // No soundcard found. Do not complain, but sit in the background, and wait for newly plugged soundcards. updateDocking(); // -<- removes the DockIcon hide(); } } KMixerWidget* KMixWindow::findKMWforTab(const QString& kmwId) { for (int i = 0; i < m_wsMixers->count(); ++i) { KMixerWidget *kmw = qobject_cast(m_wsMixers->widget(i)); if (kmw->getGuiprof()->getId() == kmwId) { return kmw; } } return 0; } void KMixWindow::newView() { if (Mixer::mixers().empty()) { qCCritical(KMIX_LOG) << "Trying to create a View, but no Mixer exists"; return; // should never happen } Mixer *mixer = Mixer::mixers()[0]; QPointer dav = new DialogAddView(this, mixer); int ret = dav->exec(); if (QDialog::Accepted == ret) { QString profileName = dav->getresultViewName(); QString mixerId = dav->getresultMixerId(); mixer = Mixer::findMixer(mixerId); qCDebug(KMIX_LOG) << ">>> mixer = " << mixerId << " -> " << mixer; GUIProfile* guiprof = GUIProfile::find(mixer, profileName, false, false); if (guiprof == nullptr) { guiprof = GUIProfile::find(mixer, profileName, false, true); } if (guiprof == nullptr) { KMessageBox::sorry(this, i18n("Cannot add view - GUIProfile is invalid."), i18n("Error")); } else { bool ret = addMixerWidget(mixer->id(), guiprof->getId(), -1); if (!ret) { KMessageBox::sorry(this, i18n("Cannot add view - View already exists."), i18n("Error")); } } delete dav; } //qCDebug(KMIX_LOG) << "Exit"; } /** * Save the view and close it * * @arg idx The index in the TabWidget */ void KMixWindow::saveAndCloseView(int idx) { qCDebug(KMIX_LOG) << "Enter"; QWidget *w = m_wsMixers->widget(idx); KMixerWidget* kmw = ::qobject_cast(w); if (kmw) { kmw->saveConfig(KSharedConfig::openConfig().data()); // -<- This alone is not enough, as I need to save the META information as well. Thus use saveViewConfig() below m_wsMixers->removeTab(idx); updateTabsClosable(); saveViewConfig(); delete kmw; } qCDebug(KMIX_LOG) << "Exit"; } void KMixWindow::fixConfigAfterRead() { KConfigGroup grp(KSharedConfig::openConfig(), "Global"); unsigned int configVersion = grp.readEntry("ConfigVersion", 0); if (configVersion < 3) { // Fix the "double Base" bug, by deleting all groups starting with "View.Base.Base.". // The group has been copied over by KMixToolBox::loadView() for all soundcards, so // we should be fine now QStringList cfgGroups = KSharedConfig::openConfig()->groupList(); QStringListIterator it(cfgGroups); while (it.hasNext()) { QString groupName = it.next(); if (groupName.indexOf("View.Base.Base") == 0) { qCDebug(KMIX_LOG) << "Fixing group " << groupName; KConfigGroup buggyDevgrpCG(KSharedConfig::openConfig(), groupName); buggyDevgrpCG.deleteGroup(); } // remove buggy group } // for all groups } // if config version < 3 } void KMixWindow::plugged(const char *driverName, const QString &udi, int dev) { qCDebug(KMIX_LOG) << "dev" << dev << "driver" << driverName << "udi" << udi; Mixer *mixer = new Mixer(QString::fromLocal8Bit(driverName), dev); if (mixer!=nullptr) { if (MixerToolBox::possiblyAddMixer(mixer)) { qCDebug(KMIX_LOG) << "adding mixer id" << mixer->id() << "name" << mixer->readableName(); recreateGUI(true, mixer->id(), true, false); } else qCWarning(KMIX_LOG) << "Cannot add mixer to GUI"; } } void KMixWindow::unplugged(const QString &udi) { qCDebug(KMIX_LOG) << "udi" << udi; for (int i = 0; i < Mixer::mixers().count(); ++i) { Mixer *mixer = (Mixer::mixers())[i]; // qCDebug(KMIX_LOG) << "Try Match with:" << mixer->udi(); if (mixer->udi() == udi) { qCDebug(KMIX_LOG) << "Removing mixer"; bool globalMasterMixerDestroyed = (mixer == Mixer::getGlobalMasterMixer()); // Part 1: Remove tab from GUI for (int i = 0; i < m_wsMixers->count(); ++i) { QWidget *w = m_wsMixers->widget(i); KMixerWidget* kmw = ::qobject_cast(w); if (kmw && kmw->mixer() == mixer) { saveAndCloseView(i); i = -1; // Restart loop from scratch (indices are most likely invalidated at removeTab() ) } } // Part 2: Remove mixer from known list MixerToolBox::removeMixer(mixer); // Part 3: Check whether the Global Master disappeared, // and select a new one if necessary shared_ptr md = Mixer::getGlobalMasterMD(); if (globalMasterMixerDestroyed || md.get() == 0) { // We don't know what the global master should be now. // So lets play stupid, and just select the recommended master of the first device if (Mixer::mixers().count() > 0) { shared_ptr master = ((Mixer::mixers())[0])->getLocalMasterMD(); if (master.get() != 0) { QString localMaster = master->id(); Mixer::setGlobalMaster(((Mixer::mixers())[0])->id(), localMaster, false); QString text; text = i18n( "The soundcard containing the master device was unplugged. Changing to control %1 on card %2.", master->readableName(), ((Mixer::mixers())[0])->readableName()); KMixToolBox::notification("MasterFallback", text); } } } if (Mixer::mixers().count() == 0) { QString text; text = i18n("The last soundcard was unplugged."); KMixToolBox::notification("MasterFallback", text); } recreateGUI(true, false); break; } } } /** * */ bool KMixWindow::profileExists(QString guiProfileId) { for (int i = 0; i < m_wsMixers->count(); ++i) { KMixerWidget* kmw = dynamic_cast(m_wsMixers->widget(i)); if (kmw && kmw->getGuiprof()->getId() == guiProfileId) return true; } return false; } bool KMixWindow::addMixerWidget(const QString& mixer_ID, QString guiprofId, int insertPosition) { qCDebug(KMIX_LOG) << "Add " << guiprofId; GUIProfile* guiprof = GUIProfile::find(guiprofId); if (guiprof != 0 && profileExists(guiprof->getId())) // TODO Bad place. Should be checked in the add-tab-dialog return false; // already present => don't add again Mixer *mixer = Mixer::findMixer(mixer_ID); if (mixer == 0) return false; // no such Mixer // qCDebug(KMIX_LOG) << "KMixWindow::addMixerWidget() " << mixer_ID << " is being added"; ViewBase::ViewFlags vflags = ViewBase::HasMenuBar; if ((_actionShowMenubar == 0) || _actionShowMenubar->isChecked()) vflags |= ViewBase::MenuBarVisible; KMixerWidget *kmw = new KMixerWidget(mixer, this, vflags, guiprofId, actionCollection()); /* A newly added mixer will automatically added at the top * and thus the window title is also set appropriately */ /* * Skip the name from the profile for now. I would at least have to do the '&' quoting for the tab label. But I am * also not 100% sure whether the current name from the profile is any good - it does (likely) not even contain the * card ID. This means you cannot distinguish between cards with an identical name. */ // QString tabLabel = guiprof->getName(); // if (tabLabel.isEmpty()) // QString tabLabel = kmw->mixer()->readableName(true); QString tabLabel = kmw->mixer()->readableName(true); m_dontSetDefaultCardOnStart = true; // inhibit implicit setting of m_defaultCardOnStart if (insertPosition == -1) m_wsMixers->addTab(kmw, tabLabel); else m_wsMixers->insertTab(insertPosition, kmw, tabLabel); if (kmw->getGuiprof()->getId() == m_defaultCardOnStart) { m_wsMixers->setCurrentWidget(kmw); } updateTabsClosable(); m_dontSetDefaultCardOnStart = false; kmw->loadConfig(KSharedConfig::openConfig().data()); // Now force to read for new tabs, especially after hotplug. Note: Doing it here is bad design and possibly // obsolete, as the backend should take care of updating itself. kmw->mixer()->readSetFromHWforceUpdate(); return true; } void KMixWindow::updateTabsClosable() { // Pulseaudio runs with 4 fixed tabs - don't allow to close them. // Also do not allow to close the last view m_wsMixers->setTabsClosable(!Mixer::pulseaudioPresent() && m_wsMixers->count() > 1); } bool KMixWindow::queryClose() { GlobalConfigData& gcd = GlobalConfig::instance().data; if (gcd.showDockWidget && !qApp->isSavingSession() ) { // Hide (don't close and destroy), if docking is enabled. Except when session saving (shutdown) is in process. hide(); return false; } else { // Accept the close, if: // The user has disabled docking // or SessionSaving() is running // qCDebug(KMIX_LOG) << "close"; return true; } } void KMixWindow::hideOrClose() { GlobalConfigData& gcd = GlobalConfig::instance().data; if (gcd.showDockWidget && m_dockWidget != 0) { // we can hide if there is a dock widget hide(); } else { // if there is no dock widget, we will quit quit(); } } // internal helper to prevent code duplication in slotIncreaseVolume and slotDecreaseVolume void KMixWindow::increaseOrDecreaseVolume(bool increase) { Mixer* mixer = Mixer::getGlobalMasterMixer(); // only needed for the awkward construct below if (mixer == 0) return; // e.g. when no soundcard is available shared_ptr md = Mixer::getGlobalMasterMD(); if (md.get() == 0) return; // shouldn't happen, but lets play safe Volume::VolumeTypeFlag volumeType = md->playbackVolume().hasVolume() ? Volume::Playback : Volume::Capture; md->increaseOrDecreaseVolume(!increase, volumeType); md->mixer()->commitVolumeChange(md); showVolumeDisplay(); } void KMixWindow::slotIncreaseVolume() { increaseOrDecreaseVolume(true); } void KMixWindow::slotDecreaseVolume() { increaseOrDecreaseVolume(false); } void KMixWindow::showVolumeDisplay() { Mixer* mixer = Mixer::getGlobalMasterMixer(); if (mixer == 0) return; // e.g. when no soundcard is available shared_ptr md = Mixer::getGlobalMasterMD(); if (md.get() == 0) return; // shouldn't happen, but lets play safe if (GlobalConfig::instance().data.showOSD) { QDBusMessage msg = QDBusMessage::createMethodCall( "org.kde.plasmashell", "/org/kde/osdService", "org.kde.osdService", "volumeChanged" ); int currentVolume = 0; if (!md->isMuted()) { currentVolume = md->playbackVolume().getAvgVolumePercent(Volume::MALL); } msg.setArguments(QList() << currentVolume); QDBusConnection::sessionBus().asyncCall(msg); } } /** * Mutes the global master. (SLOT) */ void KMixWindow::slotMute() { Mixer* mixer = Mixer::getGlobalMasterMixer(); if (mixer == 0) return; // e.g. when no soundcard is available shared_ptr md = Mixer::getGlobalMasterMD(); if (md.get() == 0) return; // shouldn't happen, but lets play safe md->toggleMute(); mixer->commitVolumeChange(md); showVolumeDisplay(); } void KMixWindow::quit() { // qCDebug(KMIX_LOG) << "quit"; qApp->quit(); } /** * Shows the configuration dialog, with the "general" tab opened. */ void KMixWindow::showSettings() { KMixPrefDlg::getInstance()->switchToPage(KMixPrefDlg::PrefGeneral); KMixPrefDlg::getInstance()->show(); } void KMixWindow::showHelp() { actionCollection()->action("help_contents")->trigger(); } void KMixWindow::showAbout() { actionCollection()->action("help_about_app")->trigger(); } /** * Apply the Preferences from the preferences dialog. Depending on what has been changed, * the corresponding announcements are made. */ void KMixWindow::applyPrefs() { // -1- Determine what has changed ------------------------------------------------------------------ GlobalConfigData& config = GlobalConfig::instance().data; GlobalConfigData& configBefore = configDataSnapshot; bool labelsHasChanged = config.showLabels ^ configBefore.showLabels; bool ticksHasChanged = config.showTicks ^ configBefore.showTicks; bool dockwidgetHasChanged = config.showDockWidget ^ configBefore.showDockWidget; bool toplevelOrientationHasChanged = config.getToplevelOrientation() != configBefore.getToplevelOrientation(); bool traypopupOrientationHasChanged = config.getTraypopupOrientation() != configBefore.getTraypopupOrientation(); qCDebug(KMIX_LOG) << "toplevelOrientationHasChanged=" << toplevelOrientationHasChanged << ", config=" << config.getToplevelOrientation() << ", configBefore=" << configBefore.getToplevelOrientation(); qCDebug(KMIX_LOG) << "trayOrientationHasChanged=" << traypopupOrientationHasChanged << ", config=" << config.getTraypopupOrientation() << ", configBefore=" << configBefore.getTraypopupOrientation(); // -2- Determine what effect the changes have ------------------------------------------------------------------ if (dockwidgetHasChanged || toplevelOrientationHasChanged || traypopupOrientationHasChanged) { // These might need a complete relayout => announce a ControlList change to rebuild everything ControlManager::instance().announce(QString(), ControlManager::ControlList, QString("Preferences Dialog")); } else if (labelsHasChanged || ticksHasChanged) { ControlManager::instance().announce(QString(), ControlManager::GUI, QString("Preferences Dialog")); } // showOSD does not require any information. It reads on-the-fly from GlobalConfig. // -3- Apply all changes ------------------------------------------------------------------ // this->repaint(); // make KMix look fast (saveConfig() often uses several seconds) qApp->processEvents(); configDataSnapshot = GlobalConfig::instance().data; // create a new snapshot as all current changes are applied now // Remove saveConfig() IF aa changes have been migrated to GlobalConfig. // Currently there is still stuff like "show menu bar". saveConfig(); } void KMixWindow::toggleMenuBar() { menuBar()->setVisible(_actionShowMenubar->isChecked()); } void KMixWindow::slotKdeAudioSetupExec() { forkExec(QStringList() << "kcmshell5" << "kcm_phonon"); } void KMixWindow::forkExec(const QStringList& args) { int pid = KProcess::startDetached(args); if (pid == 0) { KMessageBox::error(this, i18n("The helper application is either not installed or not working.\n\n%1", args.join(QLatin1String(" ")))); } } void KMixWindow::slotConfigureCurrentView() { KMixerWidget *mw = qobject_cast(m_wsMixers->currentWidget()); ViewBase* view = 0; if (mw) view = mw->currentView(); if (view) view->configureView(); } void KMixWindow::slotSelectMasterClose(QObject*) { m_dsm = 0; } void KMixWindow::slotSelectMaster() { const Mixer *mixer = Mixer::getGlobalMasterMixer(); if (mixer!=nullptr) { if (!m_dsm) { m_dsm = new DialogSelectMaster(mixer, this); connect(m_dsm, SIGNAL(destroyed(QObject*)), this, SLOT(slotSelectMasterClose(QObject*))); m_dsm->setAttribute(Qt::WA_DeleteOnClose, true); m_dsm->show(); } m_dsm->raise(); m_dsm->activateWindow(); } else { KMessageBox::error(nullptr, KMixToolBox::noDevicesWarningString()); } } void KMixWindow::newMixerShown(int /*tabIndex*/) { KMixerWidget *kmw = qobject_cast(m_wsMixers->currentWidget()); if (kmw!=nullptr) { // I am using the app name as a PREFIX, as KMix is a single window app, and it is // more helpful to the user to see "KDE Mixer" in a window list than a possibly cryptic // soundcard name like "HDA ATI SB". // Reformatted for KF5 so as to not say "KDE" // and so that there are not two different dashes. setWindowTitle(i18n("Mixer (%1)", kmw->mixer()->readableName())); if (!m_dontSetDefaultCardOnStart) m_defaultCardOnStart = kmw->getGuiprof()->getId(); // As switching the tab does NOT mean switching the master card, we do not need to update dock icon here. // It would lead to unnecesary flickering of the (complete) dock area. // We only show the "Configure Channels..." menu item if the mixer is not dynamic ViewBase* view = kmw->currentView(); QAction* action = actionCollection()->action("toggle_channels_currentview"); if (view && action) action->setVisible(!view->isDynamic()); } } - diff --git a/core/GlobalConfig.cpp b/core/GlobalConfig.cpp index 43719bfc..68892f15 100644 --- a/core/GlobalConfig.cpp +++ b/core/GlobalConfig.cpp @@ -1,112 +1,113 @@ /* KMix -- KDE's full featured mini mixer Copyright (C) 2012 Christian Esken This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "GlobalConfig.h" // instanceObj must be created "late", so we can refer to the correct application config file kmixrc instead of kderc. GlobalConfig* GlobalConfig::instanceObj; GlobalConfig::GlobalConfig() : KConfigSkeleton("kmixrc") { setCurrentGroup("Global"); // General addItemBool("Tickmarks", data.showTicks, true); addItemBool("Labels", data.showLabels, true); addItemBool("VolumeOverdrive", data.volumeOverdrive, false); addItemBool("VolumeFeedback", data.beepOnVolumeChange, true); + addItemString("VolumePercentageStep", data.VolumePercentageStep, "5"); // ItemString* is = addItemString("Orientation", data.orientationMainGUIString, "Vertical"); // qCDebug(KMIX_LOG) << is->name() << is->value(); addItemString("Orientation.TrayPopup", data.orientationTrayPopupString, QLatin1String("Vertical")); // Sound Menu addItemBool("showOSD", data.showOSD, true); addItemBool("AllowDocking", data.showDockWidget, true); // addItemBool("TrayVolumeControl", data.trayVolumePopupEnabled, true); // removed support in KDE4.13. Always active! // Startup addItemBool("AutoStart", data.allowAutostart, true); addItemBool("VolumeFeedback", data.volumeFeedback, true); addItemBool("startkdeRestore", data.startkdeRestore, true); // Debug options: Not in dialog addItemBool("Debug.ControlManager", data.debugControlManager, false); addItemBool("Debug.GUI", data.debugGUI, false); addItemBool("Debug.Volume", data.debugVolume, false); addItemBool("Debug.Config", data.debugConfig, false); load(); } // --- Special READ/WRITE ---------------------------------------------------------------------------------------- void GlobalConfig::usrReadConfig() { // qCDebug(KMIX_LOG) << "or=" << data.orientationMainGUIString; // Convert orientation strings to Qt::Orientation data.convertOrientation(); } //void GlobalConfig::usrWriteConfig() //{ // // TODO: Is this any good? When is usrWriteConfig() called? Hopefully BEFORE actually writing. Otherwise // // I must move this code to #setToplevelOrientation() and #setTraypopupOrientation(). //} Qt::Orientation GlobalConfigData::getToplevelOrientation() { return toplevelOrientation; } Qt::Orientation GlobalConfigData::getTraypopupOrientation() { return traypopupOrientation; } /** * Converts the orientation strings to Qt::Orientation */ void GlobalConfigData::convertOrientation() { toplevelOrientation = stringToOrientation(orientationMainGUIString); traypopupOrientation = stringToOrientation(orientationTrayPopupString); } void GlobalConfigData::setToplevelOrientation(Qt::Orientation orientation) { toplevelOrientation = orientation; orientationMainGUIString = orientationToString(toplevelOrientation); } void GlobalConfigData::setTraypopupOrientation(Qt::Orientation orientation) { traypopupOrientation = orientation; orientationTrayPopupString = orientationToString(traypopupOrientation); } Qt::Orientation GlobalConfigData::stringToOrientation(QString& orientationString) { return orientationString == "Horizontal" ? Qt::Horizontal : Qt::Vertical; } QString GlobalConfigData::orientationToString(Qt::Orientation orientation) { return orientation == Qt::Horizontal ? "Horizontal" : "Vertical"; } diff --git a/core/GlobalConfig.h b/core/GlobalConfig.h index 7a8222e2..7ce97c6b 100644 --- a/core/GlobalConfig.h +++ b/core/GlobalConfig.h @@ -1,141 +1,142 @@ /* KMix -- KDE's full featured mini mixer Copyright (C) 2012 Christian Esken This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #ifndef GLOBALCONFIG_H #define GLOBALCONFIG_H #include #include #include #include "kmixcore_export.h" class KMIXCORE_EXPORT GlobalConfigData { friend class GlobalConfig; public: // Hint: We are using the standard 1-arg constructor as copy constructor bool showTicks; bool showLabels; bool showOSD; bool volumeFeedback; bool volumeOverdrive; // whether more than recommended volume (typically 0dB) is allowed bool beepOnVolumeChange; + QString VolumePercentageStep; // Startup bool allowAutostart; bool showDockWidget; bool startkdeRestore; // Debug options bool debugControlManager; bool debugGUI; bool debugVolume; bool debugConfig; Qt::Orientation getToplevelOrientation(); Qt::Orientation getTraypopupOrientation(); void setToplevelOrientation(Qt::Orientation orientation); void setTraypopupOrientation(Qt::Orientation orientation); private: QString orientationMainGUIString; QString orientationTrayPopupString; // The following two values are only converted/cached date from the former fields. Qt::Orientation toplevelOrientation; Qt::Orientation traypopupOrientation; void convertOrientation(); Qt::Orientation stringToOrientation(QString& orientationString); QString orientationToString(Qt::Orientation orientation); }; class KMIXCORE_EXPORT GlobalConfig : public KConfigSkeleton { private: static GlobalConfig* instanceObj; public: static GlobalConfig& instance() { return *instanceObj; } ; /** * Call this init method when your app core is properly initialized. * It is very important that KGlobal is initialized then. Otherwise * KSharedConfig::openConfig() could return a reference to * the "kderc" config instead of the actual application config "kmixrc" or "kmixctrlrc". * */ static void init() { instanceObj = new GlobalConfig(); } ; /** * Frees all data associated with the static instance. * */ static void shutdown() { delete instanceObj; instanceObj = 0; } ; GlobalConfigData data; void setMixersForSoundmenu(QSet mixersForSoundmenu) { this->mixersForSoundmenu = mixersForSoundmenu; } ; QSet getMixersForSoundmenu() { return mixersForSoundmenu; } ; protected: QSet mixersForSoundmenu; private: GlobalConfig(); /** * @Override */ void usrReadConfig() override; /** * @Override */ // virtual void usrWriteConfig(); }; #endif // GLOBALCONFIG_H diff --git a/gui/kmixprefdlg.cpp b/gui/kmixprefdlg.cpp index 5922dac8..28399057 100644 --- a/gui/kmixprefdlg.cpp +++ b/gui/kmixprefdlg.cpp @@ -1,471 +1,488 @@ /* * KMix -- KDE's full featured mini mixer * * * Copyright (C) 2000 Stefan Schimanski <1Stein@gmx.de> * Copyright (C) 2001 Preston Brown * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "gui/kmixprefdlg.h" #include #include +#include #include #include #include #include #include #include #include "dialogbase.h" #include "dialogstatesaver.h" #include "gui/kmixerwidget.h" #include "core/GlobalConfig.h" KMixPrefDlg* KMixPrefDlg::instance = 0; KMixPrefDlg* KMixPrefDlg::getInstance() { return instance; } KMixPrefDlg* KMixPrefDlg::createInstance(QWidget *parent, GlobalConfig& config) { if (instance == 0) { instance = new KMixPrefDlg(parent, config); } return instance; } KMixPrefDlg::KMixPrefDlg(QWidget *parent, GlobalConfig& config) : KConfigDialog(parent, i18n("Configure"), &config), dialogConfig(config) { setFaceType(KPageDialog::List); //setCaption(i18n("Configure")); dvc = 0; dvcSpacerBelow = 0; // general buttons m_generalTab = new QFrame(this); m_controlsTab = new QFrame(this); m_startupTab = new QFrame(this); createStartupTab(); createGeneralTab(); createControlsTab(); updateWidgets(); // I thought KConfigDialog would call this, but I saw during a gdb session that it does not do so. generalPage = addPage(m_generalTab, i18n("General"), "configure"); startupPage = addPage(m_startupTab, i18n("Startup"), "preferences-system-login"); soundmenuPage = addPage(m_controlsTab, i18n("Volume Control"), "audio-volume-high"); new DialogStateSaver(this); } /** * Switches to a specific page and shows it. * @param page */ void KMixPrefDlg::switchToPage(KMixPrefPage page) { switch (page) { case PrefGeneral: setCurrentPage(generalPage); break; case PrefSoundMenu: setCurrentPage(soundmenuPage); break; case PrefStartup: setCurrentPage(startupPage); break; default: qCWarning(KMIX_LOG) << "Tried to activated unknown preferences page" << page; break; } show(); } // --- TABS -------------------------------------------------------------------------------------------------- void KMixPrefDlg::createStartupTab() { layoutStartupTab = new QVBoxLayout(m_startupTab); layoutStartupTab->setContentsMargins(0, 0, 0, 0); layoutStartupTab->setSpacing(DialogBase::verticalSpacing()); allowAutostart = new QCheckBox(i18n("Start KMix on desktop startup"), m_startupTab); addWidgetToLayout(allowAutostart, layoutStartupTab, 10, i18n("Start KMix automatically when the desktop starts."), "AutoStart"); allowAutostartWarning = new KMessageWidget( i18n("Autostart will not work, because the autostart file kmix_autostart.desktop is missing. Check that KMix is installed correctly."), m_startupTab); allowAutostartWarning->setIcon(QIcon::fromTheme("dialog-error")); allowAutostartWarning->setMessageType(KMessageWidget::Error); allowAutostartWarning->setCloseButtonVisible(false); allowAutostartWarning->setWordWrap(true); allowAutostartWarning->setVisible(false); addWidgetToLayout(allowAutostartWarning, layoutStartupTab, 2, "", ""); layoutStartupTab->addItem(new QSpacerItem(1, 2*DialogBase::verticalSpacing())); m_onLogin = new QCheckBox(i18n("Restore previous volume settings on desktop startup"), m_startupTab); addWidgetToLayout(m_onLogin, layoutStartupTab, 10, i18n("Restore all mixer volume levels and switches to their last used settings when the desktop starts."), "startkdeRestore"); dynamicControlsRestoreWarning = new KMessageWidget( i18n("The volume of PulseAudio or MPRIS2 dynamic controls will not be restored."), m_startupTab); dynamicControlsRestoreWarning->setIcon(QIcon::fromTheme("dialog-warning")); dynamicControlsRestoreWarning->setMessageType(KMessageWidget::Warning); dynamicControlsRestoreWarning->setCloseButtonVisible(false); dynamicControlsRestoreWarning->setWordWrap(true); dynamicControlsRestoreWarning->setVisible(false); addWidgetToLayout(dynamicControlsRestoreWarning, layoutStartupTab, 2, "", ""); layoutStartupTab->addStretch(); } void KMixPrefDlg::createOrientationGroup(const QString& labelSliderOrientation, QGridLayout* orientationLayout, int row, KMixPrefDlgPrefOrientationType prefType) { QButtonGroup* orientationGroup = new QButtonGroup(m_generalTab); orientationGroup->setExclusive(true); QLabel* qlb = new QLabel(labelSliderOrientation, m_generalTab); QRadioButton* qrbHor = new QRadioButton(i18n("&Horizontal"), m_generalTab); QRadioButton* qrbVert = new QRadioButton(i18n("&Vertical"), m_generalTab); if (prefType == TrayOrientation) { _rbTraypopupHorizontal = qrbHor; _rbTraypopupVertical = qrbVert; orientationGroup->setObjectName("Orientation.TrayPopup"); } else { _rbHorizontal = qrbHor; _rbVertical = qrbVert; orientationGroup->setObjectName("Orientation"); } // Add both buttons to button group //qrbHor->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); orientationGroup->addButton(qrbHor); orientationGroup->addButton(qrbVert); // Add both buttons and label to layout orientationLayout->addWidget(qlb, row, 0, Qt::AlignLeft); orientationLayout->addWidget(qrbHor, row, 1, Qt::AlignLeft); orientationLayout->addWidget(qrbVert, row, 2, Qt::AlignLeft); orientationLayout->addItem(new QSpacerItem(1,1,QSizePolicy::Expanding), row, 3); connect(qrbHor, SIGNAL(toggled(bool)), SLOT(updateButtons())); connect(qrbVert, SIGNAL(toggled(bool)), SLOT(updateButtons())); connect(button(QDialogButtonBox::Apply), SIGNAL(clicked()), SLOT(kmixConfigHasChangedEmitter())); connect(button(QDialogButtonBox::Ok), SIGNAL(clicked()), SLOT(kmixConfigHasChangedEmitter())); // connect(qrbHor, SIGNAL(toggled(bool)), SLOT(settingsChangedSlot())); // connect(qrbVert, SIGNAL(toggled(bool)), SLOT(settingsChangedSlot())); } void KMixPrefDlg::createGeneralTab() { QBoxLayout* layout = new QVBoxLayout(m_generalTab); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(DialogBase::verticalSpacing()); // --- Behavior --------------------------------------------------------- QGroupBox *grp = new QGroupBox(i18n("Behavior"), m_generalTab); grp->setFlat(true); layout->addWidget(grp); auto *behaviorLayout = new QVBoxLayout; behaviorLayout->setContentsMargins(0, 0, 0, 0); grp->setLayout(behaviorLayout); // [CONFIG] m_beepOnVolumeChange = new QCheckBox(i18n("Volume feedback"), grp); addWidgetToLayout(m_beepOnVolumeChange, behaviorLayout, 10, "", "VolumeFeedback"); m_volumeOverdrive = new QCheckBox(i18n("Volume overdrive"), grp); addWidgetToLayout(m_volumeOverdrive, behaviorLayout, 10, i18nc("@info:tooltip", "Raise the maximum volume to 150%"), "VolumeOverdrive"); + // Volume Feedback Warning volumeFeedbackWarning = new KMessageWidget( i18n("Volume feedback and volume overdrive are only available for PulseAudio."), grp); volumeFeedbackWarning->setIcon(QIcon::fromTheme("dialog-warning")); volumeFeedbackWarning->setMessageType(KMessageWidget::Warning); volumeFeedbackWarning->setCloseButtonVisible(false); volumeFeedbackWarning->setWordWrap(true); volumeFeedbackWarning->setVisible(false); addWidgetToLayout(volumeFeedbackWarning, behaviorLayout, 2, "", ""); + // Volume Step Grid + QGridLayout* horizontalGrid = new QGridLayout(); + horizontalGrid->setHorizontalSpacing(DialogBase::horizontalSpacing()); + + // Volume Step SpinBox + m_volumeStep = new QSpinBox(grp); + m_volumeStep->setSuffix(" %"); + m_volumeStep->setRange(1, 50); + // Register SpinBox for KConfig + m_volumeStep->setObjectName("kcfg_VolumePercentageStep"); + + horizontalGrid->addWidget(new QLabel(i18n("Volume step:"), m_generalTab), 0, 0, Qt::AlignLeft); + horizontalGrid->addWidget(m_volumeStep, 0, 1, Qt::AlignLeft); + horizontalGrid->addItem(new QSpacerItem(1 ,1 , QSizePolicy::Expanding), 0, 2); + + // Add grid to behavior layout + behaviorLayout->addLayout(horizontalGrid); + + // Volume Step and Overdrive Warning volumeOverdriveWarning = new KMessageWidget( - i18n("KMix must be restarted for the Volume Overdrive setting to take effect."), grp); + i18n("KMix must be restarted for the Volume Step and Overdrive settings to take effect."), grp); volumeOverdriveWarning->setIcon(QIcon::fromTheme("dialog-information")); volumeOverdriveWarning->setMessageType(KMessageWidget::Information); volumeOverdriveWarning->setCloseButtonVisible(false); volumeOverdriveWarning->setWordWrap(true); volumeOverdriveWarning->setVisible(false); addWidgetToLayout(volumeOverdriveWarning, behaviorLayout, 2, "", ""); // --- Visual --------------------------------------------------------- layout->addItem(new QSpacerItem(1, DialogBase::verticalSpacing())); grp = new QGroupBox(i18n("Visual"), m_generalTab); grp->setFlat(true); layout->addWidget(grp); auto *visualLayout = new QVBoxLayout; visualLayout->setContentsMargins(0, 0, 0, 0); grp->setLayout(visualLayout); // [CONFIG] m_showTicks = new QCheckBox(i18n("Show slider &tickmarks"), grp); addWidgetToLayout(m_showTicks, visualLayout, 10, i18n("Enable/disable tickmark scales on the sliders"), "Tickmarks"); m_showLabels = new QCheckBox(i18n("Show control &labels"), grp); addWidgetToLayout(m_showLabels, visualLayout, 10, i18n("Enables/disables description labels above the sliders"), "Labels"); // [CONFIG] m_showOSD = new QCheckBox(i18n("Show On Screen Display (&OSD)"), grp); addWidgetToLayout(m_showOSD, visualLayout, 10, "", "showOSD"); // [CONFIG] Slider orientation (main window) visualLayout->addItem(new QSpacerItem(1, DialogBase::verticalSpacing())); QGridLayout* orientationGrid = new QGridLayout(); orientationGrid->setHorizontalSpacing(DialogBase::horizontalSpacing()); visualLayout->addLayout(orientationGrid); // Slider orientation (main window, and tray popup separately). createOrientationGroup(i18n("Slider orientation (main window): "), orientationGrid, 0, KMixPrefDlg::MainOrientation); createOrientationGroup(i18n("Slider orientation (system tray popup):"), orientationGrid, 1, KMixPrefDlg::TrayOrientation); // Push everything above to the top layout->addStretch(); } void KMixPrefDlg::createControlsTab() { layoutControlsTab = new QVBoxLayout(m_controlsTab); layoutControlsTab->setContentsMargins(0, 0, 0, 0); layoutControlsTab->setSpacing(DialogBase::verticalSpacing()); m_dockingChk = new QCheckBox(i18n("Dock in system tray"), m_controlsTab); addWidgetToLayout(m_dockingChk, layoutControlsTab, 10, i18n("Dock the mixer into the system tray. Click on it to open the popup volume control."), "AllowDocking"); layoutControlsTab->addItem(new QSpacerItem(1, 2*DialogBase::verticalSpacing())); replaceBackendsInTab(); } // --- Helper -------------------------------------------------------------------------------------------------- /** * Register widget with correct name for KConfigDialog, then add it to the given layout * * @param widget * @param layout * @param spacingBefore * @param toopTipText * @param objectName */ void KMixPrefDlg::addWidgetToLayout(QWidget* widget, QBoxLayout* layout, int spacingBefore, const QString &tooltip, const QString &kconfigName) { if (!kconfigName.isEmpty()) { // Widget to be registered for KConfig widget->setObjectName("kcfg_" + kconfigName); } if ( !tooltip.isEmpty() ) { widget->setToolTip(tooltip); } QBoxLayout *l = new QHBoxLayout(); l->addSpacing(spacingBefore); l->addWidget(widget); layout->addItem(l); } // --- KConfigDialog CUSTOM WIDGET management ------------------------------------------------------------------------ /** * Update Widgets from config. *

* Hint: this get internally called by KConfigdialog on initialization and reset. */ void KMixPrefDlg::updateWidgets() { if (dialogConfig.data.debugConfig) qCDebug(KMIX_LOG) << ""; bool toplevelHorizontal = dialogConfig.data.getToplevelOrientation() == Qt::Horizontal; _rbHorizontal->setChecked(toplevelHorizontal); _rbVertical->setChecked(!toplevelHorizontal); bool trayHorizontal = dialogConfig.data.getTraypopupOrientation() == Qt::Horizontal; _rbTraypopupHorizontal->setChecked(trayHorizontal); _rbTraypopupVertical->setChecked(!trayHorizontal); } /** * Updates config from the widgets. And emits the signal kmixConfigHasChanged(). *

* Hint: this get internally called by KConfigDialog after pressing the OK or Apply button. */ void KMixPrefDlg::updateSettings() { Qt::Orientation toplevelOrientation = _rbHorizontal->isChecked() ? Qt::Horizontal : Qt::Vertical; if (dialogConfig.data.debugConfig) qCDebug(KMIX_LOG) << "toplevelOrientation" << toplevelOrientation << ", _rbHorizontal->isChecked()" << _rbHorizontal->isChecked(); dialogConfig.data.setToplevelOrientation(toplevelOrientation); Qt::Orientation trayOrientation = _rbTraypopupHorizontal->isChecked() ? Qt::Horizontal : Qt::Vertical; if (dialogConfig.data.debugConfig) qCDebug(KMIX_LOG) << "trayOrientation" << trayOrientation << ", _rbTraypopupHorizontal->isChecked()" << _rbTraypopupHorizontal->isChecked(); dialogConfig.data.setTraypopupOrientation(trayOrientation); // Announcing MasterChanged, as the sound menu (aka ViewDockAreaPopup) primarily shows master volume(s). // In any case, ViewDockAreaPopup treats MasterChanged and ControlList the same, so it is better to announce // the "smaller" change. bool modified = dvc->getAndResetModifyFlag(); if (modified) { GlobalConfig::instance().setMixersForSoundmenu(dvc->getChosenBackends()); ControlManager::instance().announce(QString(), ControlManager::MasterChanged, QString("Select Backends Dialog")); } } void KMixPrefDlg::kmixConfigHasChangedEmitter() { emit(kmixConfigHasChanged()); } /** * Returns whether the custom widgets (orientation checkboxes) has changed. *

* Hint: this get internally called by KConfigDialog from updateButtons(). * @return */ bool KMixPrefDlg::hasChanged() { bool orientationFromConfigIsHor = dialogConfig.data.getToplevelOrientation() == Qt::Horizontal; bool orientationFromWidgetIsHor = _rbHorizontal->isChecked(); if (dialogConfig.data.debugConfig) qCDebug(KMIX_LOG) << "Orientation MAIN fromConfig=" << (orientationFromConfigIsHor ? "Hor" : "Vert") << ", fromWidget=" << (orientationFromWidgetIsHor ? "Hor" : "Vert"); bool changed = orientationFromConfigIsHor ^ orientationFromWidgetIsHor; if (!changed) { bool orientationFromConfigIsHor = dialogConfig.data.getTraypopupOrientation() == Qt::Horizontal; orientationFromWidgetIsHor = _rbTraypopupHorizontal->isChecked(); if (dialogConfig.data.debugConfig) qCDebug(KMIX_LOG) << "Orientation TRAY fromConfig=" << (orientationFromConfigIsHor ? "Hor" : "Vert") << ", fromWidget=" << (orientationFromWidgetIsHor ? "Hor" : "Vert"); changed = orientationFromConfigIsHor ^ orientationFromWidgetIsHor; } if (!changed) { changed = dvc->getModifyFlag(); } if (dialogConfig.data.debugConfig) qCDebug(KMIX_LOG) << "hasChanged=" << changed; return changed; } void KMixPrefDlg::showEvent(QShowEvent * event) { // -1- Replace widgets ------------------------------------------------------------ // Hotplug can change mixers or backends => recreate tab replaceBackendsInTab(); KConfigDialog::showEvent(event); // -2- Change visibility and enable status (of the new widgets) ---------------------- // As GUI can change, the warning will only been shown on demand dynamicControlsRestoreWarning->setVisible(Mixer::dynamicBackendsPresent()); QString autostartConfigFilename = QStandardPaths::locate(QStandardPaths::GenericConfigLocation, "/autostart/kmix_autostart.desktop"); if (dialogConfig.data.debugConfig) qCDebug(KMIX_LOG) << "autostartConfigFilename = " << autostartConfigFilename; bool autostartFileExists = !autostartConfigFilename.isEmpty(); allowAutostartWarning->setVisible(!autostartFileExists); allowAutostart->setEnabled(autostartFileExists); // Only PulseAudio supports volume feedback and volume overdrive. // Disable those configuration options for other backends, and // show a warning message. const bool pulseAudioAvailable = Mixer::pulseaudioPresent(); if (!pulseAudioAvailable) { m_beepOnVolumeChange->setChecked(false); m_beepOnVolumeChange->setEnabled(false); m_volumeOverdrive->setChecked(false); m_volumeOverdrive->setEnabled(false); volumeFeedbackWarning->setVisible(true); volumeOverdriveWarning->setVisible(false); } else { volumeFeedbackWarning->setVisible(false); volumeOverdriveWarning->setVisible(true); } } void KMixPrefDlg::replaceBackendsInTab() { if (dvc != 0) { layoutControlsTab->removeWidget(dvc); delete dvc; layoutControlsTab->removeItem(dvcSpacerBelow); delete dvcSpacerBelow; } QSet backendsFromConfig = GlobalConfig::instance().getMixersForSoundmenu(); dvc = new DialogChooseBackends(0, backendsFromConfig); connect(dvc, SIGNAL(backendsModified()), SLOT(updateButtons())); layoutControlsTab->addWidget(dvc); dvc->show(); // Push everything above to the top // layoutControlsTab->addStretch(); dvcSpacerBelow = new QSpacerItem(1,1); layoutControlsTab->addItem(dvcSpacerBelow); } - - - - diff --git a/gui/kmixprefdlg.h b/gui/kmixprefdlg.h index 5f10d97e..23932ad1 100644 --- a/gui/kmixprefdlg.h +++ b/gui/kmixprefdlg.h @@ -1,131 +1,133 @@ /* * KMix -- KDE's full featured mini mixer * * * Copyright (C) 2000 Stefan Schimanski <1Stein@gmx.de> * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KMIXPREFDLG_H #define KMIXPREFDLG_H #include class KMixPrefWidget; class QBoxLayout; class QCheckBox; +class QSpinBox; class QFrame; class QGridLayout; class QRadioButton; class QShowEvent; class QWidget; class QSpacerItem; class KMessageWidget; #include "core/GlobalConfig.h" #include "gui/dialogchoosebackends.h" class KMixPrefDlg: public KConfigDialog { Q_OBJECT public: enum KMixPrefPage { PrefGeneral, PrefSoundMenu, PrefStartup }; static KMixPrefDlg* createInstance(QWidget *parent, GlobalConfig& config); static KMixPrefDlg* getInstance(); void switchToPage(KMixPrefPage page); signals: void kmixConfigHasChanged(); private slots: void kmixConfigHasChangedEmitter(); protected: void showEvent(QShowEvent * event) override; /** * Orientation is not supported by default => implement manually * @Override */ void updateWidgets() override; /** * Orientation is not supported by default => implement manually * @Override */ void updateSettings() override; bool hasChanged() override; private: static KMixPrefDlg* instance; KMixPrefDlg(QWidget *parent, GlobalConfig& config); virtual ~KMixPrefDlg() = default; enum KMixPrefDlgPrefOrientationType { MainOrientation, TrayOrientation }; GlobalConfig& dialogConfig; void addWidgetToLayout(QWidget* widget, QBoxLayout* layout, int spacingBefore, const QString &tooltip, const QString &kconfigName); void createStartupTab(); void replaceBackendsInTab(); void createGeneralTab(); void createControlsTab(); void createOrientationGroup(const QString& labelSliderOrientation, QGridLayout* orientationLayout, int row, KMixPrefDlgPrefOrientationType type); QFrame *m_generalTab; QFrame *m_startupTab; QFrame *m_controlsTab; QCheckBox *m_dockingChk; KMessageWidget *dynamicControlsRestoreWarning; QCheckBox *m_showTicks; QCheckBox *m_showLabels; QCheckBox* m_showOSD; QCheckBox *m_onLogin; QCheckBox *allowAutostart; KMessageWidget *allowAutostartWarning; QCheckBox *m_beepOnVolumeChange; QCheckBox *m_volumeOverdrive; + QSpinBox *m_volumeStep; KMessageWidget *volumeFeedbackWarning; KMessageWidget *volumeOverdriveWarning; QBoxLayout *layoutControlsTab; QBoxLayout *layoutStartupTab; DialogChooseBackends* dvc; QSpacerItem *dvcSpacerBelow; QRadioButton *_rbVertical; QRadioButton *_rbHorizontal; QRadioButton *_rbTraypopupVertical; QRadioButton *_rbTraypopupHorizontal; KPageWidgetItem* generalPage; KPageWidgetItem* soundmenuPage; KPageWidgetItem* startupPage; }; #endif // KMIXPREFDLG_H