diff --git a/apps/kmix.cpp b/apps/kmix.cpp index 483e17cb..db06efe2 100644 --- a/apps/kmix.cpp +++ b/apps/kmix.cpp @@ -1,1300 +1,1300 @@ /* * 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; QList preferredMixersInSoundMenu; preferredMixersInSoundMenu = config.readEntry("Soundmenu.Mixers", preferredMixersInSoundMenu); GlobalConfig::instance().setMixersForSoundmenu(preferredMixersInSoundMenu.toSet()); 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) _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("MPRIS2.")) + 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; } } } /** * Create a widget with an error message * This widget shows an error message like "no mixers detected. void KMixWindow::setErrorMixerWidget() { QString s = i18n("Please plug in your soundcard. No soundcard found. Probably you have not set it up or are missing soundcard drivers. Please check your operating system manual for installing your soundcard."); // !! better text m_errorLabel = new QLabel( s,this ); m_errorLabel->setAlignment( Qt::AlignCenter ); m_errorLabel->setWordWrap(true); m_errorLabel->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); m_wsMixers->addTab( m_errorLabel, i18n("No soundcard found") ); } */ /** * */ 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() { Mixer *mixer = Mixer::getGlobalMasterMixer(); if (mixer != 0) { if (!m_dsm) { m_dsm = new DialogSelectMaster(Mixer::getGlobalMasterMixer(), 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(0, i18n("No sound card is installed or currently plugged in.")); } } 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/backends/mixer_backend.cpp b/backends/mixer_backend.cpp index 25a5a717..2cfa5d71 100644 --- a/backends/mixer_backend.cpp +++ b/backends/mixer_backend.cpp @@ -1,348 +1,348 @@ /* * KMix -- KDE's full featured mini mixer * * Copyright 2006-2007 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 "mixer_backend.h" #include // for the "ERR_" declarations, #include mixer.h #include "core/mixer.h" #include "core/ControlManager.h" #include #define POLL_RATE_SLOW 1500 #define POLL_RATE_FAST 50 #include "mixer_backend_i18n.cpp" Mixer_Backend::Mixer_Backend(Mixer *mixer, int device) : m_devnum (device) , m_isOpen(false), m_recommendedMaster(), _mixer(mixer), _pollingTimer(0), _cardInstance(1), _cardRegistered(false) { // In all cases create a QTimer. We will use it once as a singleShot(), even if something smart // like ::select() is possible (as in ALSA). And force to do an update. _readSetFromHWforceUpdate = true; _pollingTimer = new QTimer(); // will be started on open() and stopped on close() connect( _pollingTimer, SIGNAL(timeout()), this, SLOT(readSetFromHW()), Qt::QueuedConnection); } void Mixer_Backend::closeCommon() { freeMixDevices(); } int Mixer_Backend::close() { qCDebug(KMIX_LOG) << "Implicit close on " << this << ". Please instead call closeCommon() and close() explicitly (in concrete Backend destructor)"; // ^^^ Background. before the destructor runs, the C++ runtime changes the virtual pointers to point back // to the common base class. So what actually runs is not run Mixer_ALSA::close(), but this method. // - // See http://stackoverflow.com/questions/99552/where-do-pure-virtual-function-call-crashes-come-from?lq=1 + // See https://stackoverflow.com/questions/99552/where-do-pure-virtual-function-call-crashes-come-from?lq=1 // // Comment: IMO this is totally stupid and insane behavior of C++, because you cannot simply cannot call // the overwritten (cleanup) methods in the destructor. return 0; } Mixer_Backend::~Mixer_Backend() { unregisterCard(this->getName()); if (!m_mixDevices.isEmpty()) { qCDebug(KMIX_LOG) << "Implicit close on " << this << ". Please instead call closeCommon() and close() explicitly (in concrete Backend destructor)"; } delete _pollingTimer; } void Mixer_Backend::freeMixDevices() { foreach (shared_ptr md, m_mixDevices) md->close(); m_mixDevices.clear(); } bool Mixer_Backend::openIfValid() { const int ret = open(); if (ret!=0) { //qCWarning(KMIX_LOG) << "open" << getName() << "failed" << ret; return false; // could not open } qCDebug(KMIX_LOG) << "opened" << getName() << "count" << m_mixDevices.count() << "dynamic?" << _mixer->isDynamic() << "needsPolling?" << needsPolling(); if (m_mixDevices.count() > 0 || _mixer->isDynamic()) { if (needsPolling()) { _pollingTimer->start(POLL_RATE_FAST); } else { // The initial state must be read manually QTimer::singleShot( POLL_RATE_FAST, this, SLOT(readSetFromHW())); } return true; // could be opened } else { qCWarning(KMIX_LOG) << "no mix devices and not dynamic"; return false; // could not open } } bool Mixer_Backend::isOpen() { return m_isOpen; } /** * Queries the backend driver whether there are new changes in any of the controls. * If you cannot find out for a backend, return "true" - this is also the default implementation. * @return true, if there are changes. Otherwise false is returned. */ bool Mixer_Backend::hasChangedControls() { return true; } /** * The name of the Mixer this backend represents. * Often it is just a name/id for the kernel. so name and id are usually identical. Virtual/abstracting backends are * different, as they represent some distinct function like "Application streams" or "Capture Devices". Also backends * that do not have names might can to set ID and name different like i18n("SUN Audio") and "SUNAudio". */ QString Mixer_Backend::getName() const { return m_mixerName; } /** * The id of the Mixer this backend represents. The default implementation simply returns the name. * Often it is just a name/id for the kernel. so name and id are usually identical. See also #Mixer_Backend::getName(). * You must override this method if you want to set ID different from name. */ QString Mixer_Backend::getId() const { return m_mixerName; // Backwards compatibility. PulseAudio overrides it. } /** * After calling this, readSetFromHW() will do a complete update. This will * trigger emitting the appropriate signals like controlChanged(). * * This method is useful, if you need to get a "refresh signal" - used at: * 1) Start of KMix - so that we can be sure an initial signal is emitted * 2) When reconstructing any MixerWidget (e.g. DockIcon after applying preferences) */ void Mixer_Backend::readSetFromHWforceUpdate() const { _readSetFromHWforceUpdate = true; } /** * You can call this to retrieve the freshest information from the mixer HW. * This method is also called regularly by the mixer timer. */ void Mixer_Backend::readSetFromHW() { bool updated = hasChangedControls(); if ( (! updated) && (! _readSetFromHWforceUpdate) ) { // Some drivers (ALSA) are smart. We don't need to run the following // time-consuming update loop if there was no change qCDebug(KMIX_LOG) << "Mixer::readSetFromHW(): smart-update-tick"; return; } _readSetFromHWforceUpdate = false; int ret = Mixer::OK_UNCHANGED; foreach (shared_ptr md, m_mixDevices ) { //bool debugMe = (md->id() == "PCM:0" ); bool debugMe = false; if (debugMe) qCDebug(KMIX_LOG) << "Old PCM:0 playback state" << md->isMuted() << ", vol=" << md->playbackVolume().getAvgVolumePercent(Volume::MALL); int retLoop = readVolumeFromHW( md->id(), md ); if (debugMe) qCDebug(KMIX_LOG) << "New PCM:0 playback state" << md->isMuted() << ", vol=" << md->playbackVolume().getAvgVolumePercent(Volume::MALL); if (md->isEnum() ) { /* * This could be reworked: * Plan: Read everything (including enum's) in readVolumeFromHW(). * readVolumeFromHW() should then be renamed to readHW(). */ md->setEnumId( enumIdHW(md->id()) ); } // Transition the outer return value with the value from this loop iteration if ( retLoop == Mixer::OK && ret == Mixer::OK_UNCHANGED ) { // Unchanged => OK (Changed) ret = Mixer::OK; } else if ( retLoop != Mixer::OK && retLoop != Mixer::OK_UNCHANGED ) { // If current ret from loop in not OK, then transition to that: ret (Something) => retLoop (Error) ret = retLoop; } } if ( ret == Mixer::OK ) { // We explicitly exclude Mixer::OK_UNCHANGED and Mixer::ERROR_READ if ( needsPolling() ) { // Upgrade polling frequency temporarily to be more smoooooth _pollingTimer->setInterval(POLL_RATE_FAST); QTime fastPollingEndsAt = QTime::currentTime (); fastPollingEndsAt = fastPollingEndsAt.addSecs(5); _fastPollingEndsAt = fastPollingEndsAt; //_fastPollingEndsAt = fastPollingEndsAt; qCDebug(KMIX_LOG) << "Start fast polling from " << QTime::currentTime() <<"until " << _fastPollingEndsAt; } ControlManager::instance().announce(_mixer->id(), ControlManager::Volume, QString("Mixer.fromHW")); } else { // This code path is entered on Mixer::OK_UNCHANGED and ERROR bool fastPollingEndsNow = (!_fastPollingEndsAt.isNull()) && _fastPollingEndsAt < QTime::currentTime (); if ( fastPollingEndsNow ) { qCDebug(KMIX_LOG) << "End fast polling"; _fastPollingEndsAt = QTime(); // NULL time _pollingTimer->setInterval(POLL_RATE_SLOW); } } } /** * Return the MixDevice, that would qualify best as MasterDevice. The default is to return the * first device in the device list. Backends can override this (i.e. the ALSA Backend does so). * The users preference is NOT returned by this method - see the Mixer class for that. */ shared_ptr Mixer_Backend::recommendedMaster() { if ( m_recommendedMaster ) { // Backend has set a recommended master. Thats fine. Using it. return m_recommendedMaster; } else if ( ! m_mixDevices.isEmpty() ) { // Backend has NOT set a recommended master. Evil backend // => lets help out, using the first device (if exists) return m_mixDevices.at(0); } else { if ( !_mixer->isDynamic()) // This should never ever happen, as KMix does NOT accept soundcards without controls qCCritical(KMIX_LOG) << "Mixer_Backend::recommendedMaster(): returning invalid master. This is a bug in KMix. Please file a bug report stating how you produced this."; } // If we reach this code path, then obviously m_recommendedMaster == 0 (see above) return m_recommendedMaster; } /** * Sets the ID of the currently selected Enum entry. * This is a dummy implementation - if the Mixer backend * wants to support it, it must implement the driver specific * code in its subclass (see Mixer_ALSA.cpp for an example). */ void Mixer_Backend::setEnumIdHW(const QString& , unsigned int) { return; } /** * Return the ID of the currently selected Enum entry. * This is a dummy implementation - if the Mixer backend * wants to support it, it must implement the driver specific * code in its subclass (see Mixer_ALSA.cpp for an example). */ unsigned int Mixer_Backend::enumIdHW(const QString& ) { return 0; } /** * Move the stream to a new destination */ bool Mixer_Backend::moveStream(const QString &id, const QString &destId) { qCDebug(KMIX_LOG) << "called for unsupported" << id; Q_UNUSED(destId); return (false); } /** * Get the current destination device of a stream */ QString Mixer_Backend::currentStreamDevice(const QString &id) const { qCDebug(KMIX_LOG) << "called for unsupported" << id; return (QString()); } QString Mixer_Backend::errorText(int mixer_error) { QString l_s_errmsg; switch (mixer_error) { case Mixer::ERR_PERM: l_s_errmsg = i18n("kmix:You do not have permission to access the mixer device.\n" \ "Please check your operating systems manual to allow the access."); break; case Mixer::ERR_WRITE: l_s_errmsg = i18n("kmix: Could not write to mixer."); break; case Mixer::ERR_READ: l_s_errmsg = i18n("kmix: Could not read from mixer."); break; case Mixer::ERR_OPEN: l_s_errmsg = i18n("kmix: Mixer cannot be found.\n" \ "Please check that the soundcard is installed and that\n" \ "the soundcard driver is loaded.\n"); break; default: l_s_errmsg = i18n("kmix: Unknown error. Please report how you produced this error."); break; } return l_s_errmsg; } diff --git a/backends/mixer_backend.h b/backends/mixer_backend.h index 894afd43..eeb932f4 100644 --- a/backends/mixer_backend.h +++ b/backends/mixer_backend.h @@ -1,245 +1,245 @@ //-*-C++-*- /* * KMix -- KDE's full featured mini mixer * * Copyright 2006-2007 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. */ #ifndef MIXER_BACKEND_H #define MIXER_BACKEND_H #include #include #include #include "core/mixdevice.h" #include "core/mixset.h" #include "kmix_debug.h" class Mixer; class Mixer_Backend : public QObject { Q_OBJECT friend class Mixer; // The Mixer Backend's may only be accessed from the Mixer class. protected: Mixer_Backend(Mixer *mixer, int devnum); virtual ~Mixer_Backend(); /** * Derived classes MUST implement this to open the mixer. * * @return a KMix error code (O=OK). */ virtual int open() = 0; /** * Derived classes MUST implement this to close the mixer. Do not call this directly, but use shutdown() instead. * The method cannot be made pure virtual, as we use close() in the destructor, and C++ does not allow this. - * http://stackoverflow.com/questions/99552/where-do-pure-virtual-function-call-crashes-come-from?lq=1 + * https://stackoverflow.com/questions/99552/where-do-pure-virtual-function-call-crashes-come-from?lq=1 * * @return a KMix error code (O=OK). */ virtual int close(); // Not pure virtual. See comment! /** * Shutdown deinitializes this MixerBackend, freeing resources */ void closeCommon(); /** * Returns the driver name, e.g. "ALSA" or "OSS". This virtual method is for looking up the * driver name on instantiated objects. * * Please note, that there is also a static implementation of the driverName * (Because there is no "virtual static" in C++, I need the method twice). * The static implementation is for the Mixer Factory (who needs it *before* instantiating an object). * While it is not a member function, its implementation can still be found in the corresponding * Backend implementation. For example in mixer_oss.cpp there is a global function called OSS_getDriverName(). */ virtual QString getDriverName() = 0; /** * Opens the mixer, if it constitutes a valid Device. You should return "false", when * the Mixer with the devnum given in the constructor is not supported by the Backend. The two * typical cases are: * (1) No such hardware installed * (2) The hardware exists, but has no mixer support (e.g. external soundcard with only mechanical volume knobs) * The implementation calls open(), checks the return code and whether the number of * supported channels is > 0. The device remains opened if it is valid, otherwise a close() is done. */ bool openIfValid(); /** @return true, if the Mixer is open (and thus can be operated) */ bool isOpen(); virtual bool hasChangedControls(); void readSetFromHWforceUpdate() const; /// Volume Read virtual int readVolumeFromHW( const QString& id, shared_ptr ) = 0; /// Volume Write virtual int writeVolumeToHW( const QString& id, shared_ptr ) = 0; /// Enums virtual void setEnumIdHW(const QString& id, unsigned int); virtual unsigned int enumIdHW(const QString& id); virtual bool moveStream(const QString &id, const QString &destId); virtual QString currentStreamDevice(const QString &id) const; // Future directions: Move media*() methods to MediaController class virtual int mediaPlay(QString ) { return 0; }; // implement in the backend if it supports it virtual int mediaPrev(QString ) { return 0; }; // implement in the backend if it supports it virtual int mediaNext(QString ) { return 0;}; // implement in the backend if it supports it /// Overwrite in the backend if the backend can see changes without polling virtual bool needsPolling() { return true; } shared_ptr recommendedMaster(); /** * Return a translated error text for the given error number. * Subclasses can override this method to produce platform * specific error descriptions. */ virtual QString errorText(int mixer_error); /// Returns translated WhatsThis messages for a control.Translates from virtual QString translateKernelToWhatsthis(const QString &kernelName); // Return an Universal Device Identification (suitable for the OS, especially for Hotplug and Unplug events) virtual QString& udi() { return _udi; }; int m_devnum; /** * User friendly name of the Mixer (e.g. "USB 7.1 Surround System"). If your mixer API gives you a usable name, use that name. */ virtual QString getName() const; virtual QString getId() const; virtual int getCardInstance() const { return _cardInstance; } // All controls of this card MixSet m_mixDevices; /****************************************************************************************** * Please don't access the next vars from the Mixer class (even though Mixer is a friend). * There are proper access methods for them. ******************************************************************************************/ bool m_isOpen; // The MixDevice that would qualify best as MasterDevice (according to the taste of the Backend developer) shared_ptr m_recommendedMaster; // The Mixer is stored her only for one reason: The backend creates the MixDevice's, and it has shown // that it is helpful if the MixDevice's know their corresponding Mixer. KMix lived 10 years without that, // but just believe me. It's *really* better, for example, you can put controls of different soundcards in // one View. That is very cool! Also the MDW doesn't need to store the Mixer any longer (MDW is a GUI element, // so that was 'wrong' anyhow Mixer* _mixer; QTimer* _pollingTimer; QString _udi; // Universal Device Identification mutable bool _readSetFromHWforceUpdate; signals: void controlChanged( void ); // TODO remove? public slots: /** * Re-initialize. Currently only implemented by PulseAudio backend, and this slot might get moved there */ virtual void reinit() {}; protected: void freeMixDevices(); QMap s_mixerNums; /** * Registers the card for this Backend and sets the card discriminator for the given card name. * You MUST call this before creating the first MixDevice. Reason is, that each MixDevice instance register a * DBUS name that includes the mixer ID (and this means also the _cardInstance). * * The discriminator should always be 1, unless a second card with * the same name of a registered card was already registered. Default implementation will return 2, 3 and so on * for more cards. Subclasses can override this and return arbitrary ID's, but any ID that is not 1 will be * displayed to the user everywhere where a mixer name is shown, like in the tab name. * * For the background please see BKO-327471 and read the following info: * "Count mixer nums for every mixer name to identify mixers with equal names. * This is for creating persistent (reusable) primary keys, which can safely * be referenced (especially for config file access, so it is meant to be persistent!)." * * * * @param cardBaseName */ void registerCard(QString cardBaseName) { m_mixerName = cardBaseName; int cardDiscriminator = 1 + s_mixerNums[cardBaseName]; qCDebug(KMIX_LOG) << "cardBaseName=" << cardBaseName << ", cardDiscriminator=" << cardDiscriminator; _cardInstance = cardDiscriminator; _cardRegistered = true; } /** * Unregisters the card of this Backend. The cardDiscriminator counter for this card name is reduced by 1. * See #registerCard() for more info. * * TODO This is not entirely correct. Example: If the first card (cardDiscrimiator == 1) is unpluggged, then * s_mixerNums["cardName"] is changed from 2 to 1. The next plug of registerCard("cardName") will use * cardDiscriminator == 2, but the card with that discriminator was not unplugged => BANG!!! * * @param cardBaseName */ void unregisterCard(QString cardBaseName) { QMap::const_iterator it = s_mixerNums.constFind(cardBaseName); if (it != s_mixerNums.constEnd()) { int beforeValue = it.value(); int afterValue = beforeValue-1; if (beforeValue > 0) s_mixerNums[cardBaseName] = afterValue; qCDebug(KMIX_LOG) << "beforeValue=" << beforeValue << ", afterValue" << afterValue; } } int _cardInstance; bool _cardRegistered; protected slots: virtual void readSetFromHW(); private: QTime _fastPollingEndsAt; QString m_mixerName; }; typedef Mixer_Backend *getMixerFunc( Mixer* mixer, int device ); typedef QString getDriverNameFunc( ); struct MixerFactory { getMixerFunc *getMixer; getDriverNameFunc *getDriverName; }; #endif diff --git a/gui/mdwslider.cpp b/gui/mdwslider.cpp index 531b6fa3..f0861813 100644 --- a/gui/mdwslider.cpp +++ b/gui/mdwslider.cpp @@ -1,1250 +1,1250 @@ /* * KMix -- KDE's full featured mini mixer * * * Copyright (C) 1996-2007 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 "gui/mdwslider.h" #include #include #include #include #include #include #include #include #include #include #include #include "core/ControlManager.h" #include "core/mixer.h" #include "gui/guiprofile.h" #include "gui/volumeslider.h" #include "gui/viewbase.h" #include "gui/ksmallslider.h" #include "gui/verticaltext.h" #include "gui/toggletoolbutton.h" bool MDWSlider::debugMe = false; /** * MixDeviceWidget that represents a single mix device, including PopUp, muteLED, ... * * Used in KMix main window and DockWidget and PanelApplet. * It can be configured to include or exclude the captureLED and the muteLED. * The direction (horizontal, vertical) can be configured and whether it should * be "small" (uses KSmallSlider instead of a normal slider widget). * * Due to the many options, this is the most complicated MixDeviceWidget subclass. */ MDWSlider::MDWSlider(shared_ptr md, MixDeviceWidget::MDWFlags flags, ViewBase *view, ProfControl *pctl) : MixDeviceWidget(md, flags, view, pctl), m_linked(true), m_controlGrid(nullptr), m_controlIcon(nullptr), m_controlLabel(nullptr), m_muteButton(nullptr), m_captureButton(nullptr), m_mediaPlayButton(nullptr), m_controlButtonSize(QSize()), m_moveMenu(nullptr), m_sliderInWork(false), m_waitForSoundSetComplete(0) { //qCDebug(KMIX_LOG) << "for" << mixDevice()->readableName() << "flags" << MixDeviceWidget::flags(); createActions(); createWidgets(); createShortcutActions(); // Yes, this looks odd - monitor all events sent to myself by myself? // But it's so that wheel events over the MDWSlider background can be // handled by eventFilter() in the same way as wheel events over child // widgets. Each child widget apart from the sliders themselves also // also needs to have the event filter installed on it, because QWidget // by default ignores the wheel event and does not propagate it. installEventFilter(this); update(); } MDWSlider::~MDWSlider() { qDeleteAll(m_slidersPlayback); qDeleteAll(m_slidersCapture); } void MDWSlider::createActions() { // create actions (on _mdwActions, see MixDeviceWidget) KToggleAction *taction = _mdwActions->add( "stereo" ); taction->setText( i18n("Split Channels") ); connect( taction, SIGNAL(triggered(bool)), SLOT(toggleStereoLinked()) ); // QAction *action; // if ( ! mixDevice()->mixer()->isDynamic() ) { // action = _mdwActions->add( "hide" ); // action->setText( i18n("&Hide") ); // connect( action, SIGNAL(triggered(bool)), SLOT(setDisabled(bool)) ); // } if( mixDevice()->hasMuteSwitch() ) { taction = _mdwActions->add( "mute" ); taction->setText( i18n("Mute") ); connect( taction, SIGNAL(toggled(bool)), SLOT(toggleMuted()) ); } if( mixDevice()->captureVolume().hasSwitch() ) { taction = _mdwActions->add( "recsrc" ); taction->setText( i18n("Capture") ); connect( taction, SIGNAL(toggled(bool)), SLOT(toggleRecsrc()) ); } if( mixDevice()->isMovable() ) { m_moveMenu = new QMenu( i18n("Use Device"), this); connect( m_moveMenu, SIGNAL(aboutToShow()), SLOT(showMoveMenu()) ); } QAction* qaction = _mdwActions->addAction( "keys" ); qaction->setText( i18n("Channel Shortcuts...") ); connect( qaction, SIGNAL(triggered(bool)), SLOT(defineKeys()) ); } void MDWSlider::addGlobalShortcut(QAction* qaction, const QString& label, bool dynamicControl) { QString finalLabel(label); finalLabel += " - " + mixDevice()->readableName() + ", " + mixDevice()->mixer()->readableName(); qaction->setText(label); if (!dynamicControl) { // virtual / dynamic controls won't get shortcuts // #ifdef __GNUC__ // #warning GLOBAL SHORTCUTS ARE NOW ASSIGNED TO ALL CONTROLS, as enableGlobalShortcut(), has not been committed // #endif // b->enableGlobalShortcut(); // enableGlobalShortcut() is not there => use workaround KGlobalAccel::setGlobalShortcut(qaction, QKeySequence()); } } void MDWSlider::createShortcutActions() { bool dynamicControl = mixDevice()->mixer()->isDynamic(); // The following actions are for the "Configure Shortcuts" dialog /* PLEASE NOTE THAT global shortcuts are saved with the name as set with setName(), instead of their action name. This is a bug according to the thread "Global shortcuts are saved with their text-name and not their action-name - Bug?" on kcd. I work around this by using a text with setText() that is unique, but still readable to the user. */ QString actionSuffix = QString(" - %1, %2").arg( mixDevice()->readableName(), mixDevice()->mixer()->readableName() ); QAction *bi, *bd, *bm; // -1- INCREASE VOLUME SHORTCUT ----------------------------------------- bi = _mdwPopupActions->addAction( QString("Increase volume %1").arg( actionSuffix ) ); QString increaseVolumeName = i18n( "Increase Volume" ); addGlobalShortcut(bi, increaseVolumeName, dynamicControl); if ( ! dynamicControl ) connect( bi, SIGNAL(triggered(bool)), SLOT(increaseVolume()) ); // -2- DECREASE VOLUME SHORTCUT ----------------------------------------- bd = _mdwPopupActions->addAction( QString("Decrease volume %1").arg( actionSuffix ) ); QString decreaseVolumeName = i18n( "Decrease Volume" ); addGlobalShortcut(bd, decreaseVolumeName, dynamicControl); if ( ! dynamicControl ) connect(bd, SIGNAL(triggered(bool)), SLOT(decreaseVolume())); // -3- MUTE VOLUME SHORTCUT ----------------------------------------- bm = _mdwPopupActions->addAction( QString("Toggle mute %1").arg( actionSuffix ) ); QString muteVolumeName = i18n( "Toggle Mute" ); addGlobalShortcut(bm, muteVolumeName, dynamicControl); if ( ! dynamicControl ) connect( bm, SIGNAL(triggered(bool)), SLOT(toggleMuted()) ); } QSizePolicy MDWSlider::sizePolicy() const { if (orientation()==Qt::Vertical) { return QSizePolicy( QSizePolicy::Preferred, QSizePolicy::MinimumExpanding ); } else { return QSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred ); } } QSize MDWSlider::sizeHint() const { return QSize( 90, QWidget::sizeHint().height()); } /** * This method is a helper for users of this class who would like * to show multiple MDWSlider, and align the sliders. * It returns the "height" (if vertical) of this slider's label. * Warning: Line wraps are computed for a fixed size (100), this may be inaccurate in case, * the widgets have different sizes. */ int MDWSlider::labelExtentHint() const { if (m_controlLabel==nullptr) return (0); if (orientation()==Qt::Vertical) return (m_controlLabel->heightForWidth(m_controlLabel->minimumWidth())); else return (m_controlLabel->sizeHint().width()); } /** * If a label from another widget has more lines than this widget, then a spacer is added under the label */ void MDWSlider::setLabelExtent(int extent) { if (m_controlGrid==nullptr) return; if (orientation()==Qt::Vertical) m_controlGrid->setRowMinimumHeight(1, extent); else m_controlGrid->setColumnMinimumWidth(1, extent); } /** * Alignment helper */ bool MDWSlider::hasMuteButton() const { return (m_muteButton!=nullptr); } /** * See "hasMuteButton" */ bool MDWSlider::hasCaptureLED() const { return (m_captureButton!=nullptr); } void MDWSlider::guiAddCaptureButton(const QString &captureTooltipText) { m_captureButton = new ToggleToolButton("media-record", this); m_captureButton->setSmallSize(flags() & MixDeviceWidget::SmallSize); m_captureButton->installEventFilter(this); connect(m_captureButton, SIGNAL(clicked(bool)), this, SLOT(toggleRecsrc())); m_captureButton->setToolTip(captureTooltipText); } void MDWSlider::guiAddMuteButton(const QString &muteTooltipText) { m_muteButton = new ToggleToolButton("audio-volume-high", this); m_muteButton->setInactiveIcon("audio-volume-muted"); m_muteButton->setSmallSize(flags() & MixDeviceWidget::SmallSize); m_muteButton->installEventFilter(this); connect(m_muteButton, SIGNAL(clicked(bool)), this, SLOT(toggleMuted())); m_muteButton->setToolTip(muteTooltipText); } void MDWSlider::guiAddControlLabel(Qt::Alignment alignment, const QString &channelName) { m_controlLabel = new QLabel(channelName, this); m_controlLabel->setWordWrap(true); m_controlLabel->setAlignment(alignment); m_controlLabel->installEventFilter(this); } void MDWSlider::guiAddControlIcon(const QString &tooltipText) { m_controlIcon = new QLabel(this); ToggleToolButton::setIndicatorIcon(mixDevice()->iconName(), m_controlIcon, (flags() & MixDeviceWidget::SmallSize)); m_controlIcon->setToolTip(tooltipText); m_controlIcon->installEventFilter(this); } QWidget *MDWSlider::guiAddButtonSpacer() { if (hasMuteButton() || hasCaptureLED()) return (nullptr); // spacer not needed QWidget *buttonSpacer = new QWidget(this); if (orientation()==Qt::Vertical) // vertical sliders { buttonSpacer->setMinimumHeight(controlButtonSize().height()); buttonSpacer->setMaximumWidth(1); } else // horizontal sliders { buttonSpacer->setMinimumWidth(controlButtonSize().width()); buttonSpacer->setMaximumHeight(1); } buttonSpacer->installEventFilter(this); return (buttonSpacer); } QSize MDWSlider::controlButtonSize() { if (!m_controlButtonSize.isValid()) // not calculated yet { auto *buttonSpacer = new QToolButton(); ToggleToolButton::setIndicatorIcon("unknown", buttonSpacer, (flags() & MixDeviceWidget::SmallSize)); m_controlButtonSize = buttonSpacer->sizeHint(); qCDebug(KMIX_LOG) << m_controlButtonSize; delete buttonSpacer; } return (m_controlButtonSize); } /** * Creates all widgets : Icon, Label, Mute-Button, Slider(s) and Capture-Button. */ void MDWSlider::createWidgets() { const bool includePlayback = profileControl()->useSubcontrolPlayback(); const bool includeCapture = profileControl()->useSubcontrolCapture(); const bool wantsPlaybackSliders = includePlayback && (mixDevice()->playbackVolume().count()>0); const bool wantsCaptureSliders = includeCapture && (mixDevice()->captureVolume().count()>0); const bool wantsCaptureLED = includeCapture && (flags() & MixDeviceWidget::ShowCapture); const bool wantsMuteButton = includePlayback && (flags() & MixDeviceWidget::ShowMute); const MediaController *mediaController = mixDevice()->mediaController(); const bool wantsMediaControls = mediaController->hasControls(); const QString channelName = mixDevice()->readableName(); QString tooltipText = channelName; QString captureTooltipText = i18nc("%1=channel", "Capture/Uncapture %1", channelName); QString muteTooltipText = i18nc("%1=channel", "Mute/Unmute %1", channelName); if (flags() & MixDeviceWidget::ShowMixerName) { const QString mixerName = mixDevice()->mixer()->readableName(); tooltipText = i18nc("%1=device %2=channel", "%1\n%2", mixerName, tooltipText); captureTooltipText = i18nc("%1=device %2=channel", "%1\n%2", mixerName, captureTooltipText); muteTooltipText = i18nc("%1=device %2=channel", "%1\n%2", mixerName, muteTooltipText); } m_controlGrid = new QGridLayout(this); setLayout(m_controlGrid); QBoxLayout *volLayout; if (orientation()==Qt::Vertical) // vertical sliders { m_controlGrid->setContentsMargins(2, 0, 2, 0); const Qt::Alignment sliderAlign = Qt::AlignHCenter|Qt::AlignBottom; // Row 0: Control type icon guiAddControlIcon(tooltipText); m_controlGrid->addWidget(m_controlIcon, 0, 0, 1, -1, Qt::AlignHCenter|Qt::AlignTop); // Row 1: Device name label guiAddControlLabel(Qt::AlignHCenter, channelName); m_controlGrid->addWidget(m_controlLabel, 1, 0, 1, -1, Qt::AlignHCenter|Qt::AlignTop); // Row 2: Sliders int col = 0; // current column being filled int playbackCol = 0; // where these sliders ended up int captureCol = 1; // or default button column if none if (wantsPlaybackSliders) { volLayout = new QHBoxLayout(); volLayout->setAlignment(sliderAlign); addSliders(volLayout, 'p', mixDevice()->playbackVolume(), m_slidersPlayback, tooltipText); m_controlGrid->addLayout(volLayout, 2, col); playbackCol = col; ++col; } if (wantsCaptureSliders) { volLayout = new QHBoxLayout(); volLayout->setAlignment(sliderAlign); addSliders(volLayout, 'c', mixDevice()->captureVolume(), m_slidersCapture, tooltipText); m_controlGrid->addLayout(volLayout, 2, col); captureCol = col; ++col; } if (wantsMediaControls) { volLayout = new QHBoxLayout(); volLayout->setAlignment(sliderAlign); addMediaControls(volLayout); m_controlGrid->addLayout(volLayout, 2, col); } m_controlGrid->setRowStretch(2, 1); // sliders need the most space // Row 3: Control buttons if (wantsMuteButton && mixDevice()->hasMuteSwitch()) { guiAddMuteButton(muteTooltipText); m_controlGrid->addWidget(m_muteButton, 3, playbackCol, Qt::AlignHCenter|Qt::AlignTop); } if (wantsCaptureLED && mixDevice()->captureVolume().hasSwitch()) { guiAddCaptureButton(captureTooltipText); m_controlGrid->addWidget(m_captureButton, 3, captureCol, Qt::AlignHCenter|Qt::AlignTop); } // If nether a mute nor a capture button is present, then put a // dummy spacer button (in column 0, where the mute button would // normally go). This is to maintain the size of the slider // relative to others that do have one or both buttons. // // We have to do this, rather than setting a minimum height for row 3, // as in the case where it is needed row 3 will be empty and QGridLayout // ignores the minimum height set on it. QWidget *buttonSpacer = guiAddButtonSpacer(); if (buttonSpacer!=nullptr) m_controlGrid->addWidget(buttonSpacer, 3, 0); } else // horizontal sliders { const Qt::Alignment sliderAlign = Qt::AlignHCenter|Qt::AlignVCenter; // Column 0: Control type icon guiAddControlIcon(tooltipText); m_controlGrid->addWidget(m_controlIcon, 0, 0, -1, 1, Qt::AlignLeft|Qt::AlignVCenter); // Column 1: Device name label guiAddControlLabel(Qt::AlignLeft, channelName); m_controlGrid->addWidget(m_controlLabel, 0, 1, -1, 1, Qt::AlignLeft|Qt::AlignVCenter); // Column 2: Sliders int row = 0; // current row being filled int playbackRow = 0; // where these sliders ended up int captureRow = 1; // or default button row if none if (wantsPlaybackSliders) { volLayout = new QVBoxLayout(); volLayout->setAlignment(sliderAlign); addSliders(volLayout, 'p', mixDevice()->playbackVolume(), m_slidersPlayback, tooltipText); m_controlGrid->addLayout(volLayout, row, 2); playbackRow = row; ++row; } if (wantsCaptureSliders) { volLayout = new QVBoxLayout(); volLayout->setAlignment(sliderAlign); addSliders(volLayout, 'c', mixDevice()->captureVolume(), m_slidersCapture, tooltipText); m_controlGrid->addLayout(volLayout, row, 2); captureRow = row; ++row; } if (wantsMediaControls) { volLayout = new QVBoxLayout(); volLayout->setAlignment(sliderAlign); addMediaControls(volLayout); m_controlGrid->addLayout(volLayout, row, 2); } m_controlGrid->setColumnStretch(2, 1); // sliders need the most space // Column 3: Control buttons if (wantsMuteButton && mixDevice()->hasMuteSwitch()) { guiAddMuteButton(muteTooltipText); m_controlGrid->addWidget(m_muteButton, playbackRow, 3, Qt::AlignRight|Qt::AlignVCenter); } if (wantsCaptureLED && mixDevice()->captureVolume().hasSwitch()) { guiAddCaptureButton(captureTooltipText); m_controlGrid->addWidget(m_captureButton, captureRow, 3, Qt::AlignRight|Qt::AlignVCenter); } // Dummy spacer button QWidget *buttonSpacer = guiAddButtonSpacer(); if (buttonSpacer!=nullptr) m_controlGrid->addWidget(buttonSpacer, 0, 3); } const bool stereoLinked = !profileControl()->isSplit(); setStereoLinked( stereoLinked ); // Activate it explicitly in KDE3 because of PanelApplet/Kicker issues. // Not sure whether this is necessary 2 generations later. layout()->activate(); } QString MDWSlider::calculatePlaybackIcon(MediaController::PlayState playState) { QString mediaIconName; switch (playState) { case MediaController::PlayPlaying: // playing => show pause icon mediaIconName = "media-playback-pause"; break; case MediaController::PlayPaused: // stopped/paused => show play icon mediaIconName = "media-playback-start"; break; case MediaController::PlayStopped: // stopped/paused => show play icon mediaIconName = "media-playback-start"; break; default: // unknown => not good, probably result from player has not yet arrived => show a play button mediaIconName = "media-playback-start"; break; } return mediaIconName; } void MDWSlider::addMediaControls(QBoxLayout* volLayout) { MediaController *mediaController = mixDevice()->mediaController(); QBoxLayout *mediaLayout; if (orientation()==Qt::Vertical) mediaLayout = new QVBoxLayout(); else mediaLayout = new QHBoxLayout(); // QFrame* frame1 = new QFrame(this); // frame1->setFrameShape(QFrame::StyledPanel); QWidget* frame = this; // or frame1 mediaLayout->addStretch(); if (mediaController->hasMediaPrevControl()) { QToolButton *lbl = addMediaButton("media-skip-backward", mediaLayout, frame); connect(lbl, SIGNAL(clicked(bool)), this, SLOT(mediaPrev(bool))); } if (mediaController->hasMediaPlayControl()) { MediaController::PlayState playState = mediaController->getPlayState(); QString mediaIcon = calculatePlaybackIcon(playState); m_mediaPlayButton = addMediaButton(mediaIcon, mediaLayout, frame); connect(m_mediaPlayButton, SIGNAL(clicked(bool)), this, SLOT(mediaPlay(bool))); } if (mediaController->hasMediaNextControl()) { QToolButton *lbl = addMediaButton("media-skip-forward", mediaLayout, frame); connect(lbl, SIGNAL(clicked(bool)), this, SLOT(mediaNext(bool))); } mediaLayout->addStretch(); volLayout->addLayout(mediaLayout); } QToolButton* MDWSlider::addMediaButton(QString iconName, QLayout* layout, QWidget *parent) { QToolButton *lbl = new QToolButton(parent); lbl->setAutoRaise(true); lbl->setCheckable(false); ToggleToolButton::setIndicatorIcon(iconName, lbl); layout->addWidget(lbl); return lbl; } /** * Updates the icon according to the data model. */ void MDWSlider::updateMediaButton() { if (m_mediaPlayButton == 0) return; // has no media button MediaController *mediaController = mixDevice()->mediaController(); QString mediaIconName = calculatePlaybackIcon(mediaController->getPlayState()); ToggleToolButton::setIndicatorIcon(mediaIconName, m_mediaPlayButton); } void MDWSlider::mediaPrev(bool) { mixDevice()->mediaPrev(); } void MDWSlider::mediaNext(bool) { mixDevice()->mediaNext(); } void MDWSlider::mediaPlay(bool) { mixDevice()->mediaPlay(); } static QWidget *createLabel(QWidget *parent, const QString &label, Qt::Orientation orient, bool small) { QFont qf; qf.setPointSize(8); QWidget *labelWidget; if (orient == Qt::Horizontal) { auto *ql = new QLabel(label, parent); if (small) ql->setFont(qf); labelWidget = ql; } else { auto *vt = new VerticalText(parent, label); if (small) vt->setFont(qf); labelWidget = vt; } return (labelWidget); } void MDWSlider::addSliders( QBoxLayout *volLayout, char type, Volume& vol, QList& ref_sliders, const QString &tooltipText) { const int minSliderSize = fontMetrics().height() * 10; long minvol = vol.minVolume(); long maxvol = vol.maxVolume(); QMap vols = vol.getVolumes(); foreach (VolumeChannel vc, vols ) { //qCDebug(KMIX_LOG) << "Add label to " << vc.chid << ": " << Volume::channelNameReadable(vc.chid); QWidget *subcontrolLabel; QString subcontrolTranslation; if ( type == 'c' ) subcontrolTranslation += i18n("Capture") + ' '; subcontrolTranslation += Volume::channelNameReadable(vc.chid); subcontrolLabel = createLabel(this, subcontrolTranslation, orientation(), true); volLayout->addWidget(subcontrolLabel); QAbstractSlider* slider; if (flags() & MixDeviceWidget::SmallSize) { slider = new KSmallSlider( minvol, maxvol, (maxvol-minvol+1) / Volume::VOLUME_PAGESTEP_DIVISOR, vol.getVolume( vc.chid ), orientation(), this ); } // small else { slider = new VolumeSlider(orientation(), this); slider->setMinimum(minvol); slider->setMaximum(maxvol); slider->setPageStep(maxvol / Volume::VOLUME_PAGESTEP_DIVISOR); slider->setValue( vol.getVolume( vc.chid ) ); volumeValues.push_back( vol.getVolume( vc.chid ) ); extraData(slider).setSubcontrolLabel(subcontrolLabel); if (orientation()==Qt::Vertical) slider->setMinimumHeight(minSliderSize); else slider->setMinimumWidth(minSliderSize); if ( !profileControl()->getBackgroundColor().isEmpty() ) { slider->setStyleSheet("QSlider { background-color: " + profileControl()->getBackgroundColor() + " }"); } } // not small extraData(slider).setChid(vc.chid); // slider->installEventFilter( this ); if ( type == 'p' ) { slider->setToolTip( tooltipText ); } else { QString captureTip( i18n( "%1 (capture)", tooltipText ) ); slider->setToolTip( captureTip ); } volLayout->addWidget( slider ); // add to layout ref_sliders.append ( slider ); // add to list //ref_slidersChids.append(vc.chid); connect( slider, SIGNAL(valueChanged(int)), SLOT(volumeChange(int)) ); connect( slider, SIGNAL(sliderPressed()), SLOT(sliderPressed()) ); connect( slider, SIGNAL(sliderReleased()), SLOT(sliderReleased()) ); } // for all channels of this device } /** * Return the VolumeSliderExtraData from either VolumeSlider or KSmallSlider. * You MUST extend this method, should you decide to add more Slider Widget classes. * * @param slider * @return */ VolumeSliderExtraData& MDWSlider::extraData(QAbstractSlider *slider) { VolumeSlider* sl = qobject_cast(slider); if ( sl ) return sl->extraData; KSmallSlider* sl2 = qobject_cast(slider); return sl2->extraData; } void MDWSlider::sliderPressed() { m_sliderInWork = true; } void MDWSlider::sliderReleased() { m_sliderInWork = false; } QString MDWSlider::iconName() { return mixDevice()->iconName(); } void MDWSlider::toggleStereoLinked() { setStereoLinked( !isStereoLinked() ); } void MDWSlider::setStereoLinked(bool value) { m_linked = value; int overallSlidersToShow = 0; if ( ! m_slidersPlayback.isEmpty() ) overallSlidersToShow += ( m_linked ? 1 : m_slidersPlayback.count() ); if ( ! m_slidersCapture.isEmpty() ) overallSlidersToShow += ( m_linked ? 1 : m_slidersCapture.count() ); bool showSubcontrolLabels = (overallSlidersToShow >= 2); setStereoLinkedInternal(m_slidersPlayback, showSubcontrolLabels); setStereoLinkedInternal(m_slidersCapture , showSubcontrolLabels); update(); // Call update(), so that the sliders can adjust EITHER to the individual values OR the average value. } void MDWSlider::setStereoLinkedInternal(QList& ref_sliders, bool showSubcontrolLabels) { if ( ref_sliders.isEmpty()) return; bool first = true; foreach ( QAbstractSlider* slider1, ref_sliders ) { slider1->setVisible(!m_linked || first); // One slider (the 1st) is always shown extraData(slider1).getSubcontrolLabel()->setVisible(!m_linked && showSubcontrolLabels); // (*) first = false; /* (*) cesken: I have excluded the "|| first" check because the text would not be nice: * It would be "Left" or "Capture Left", while it should be "Playback" and "Capture" in the "linked" case. * * But the only affected situation is when we have playback AND capture on the same control, where we show no label. * It would be nice to put at least a "Capture" label on the capture subcontrol instead. * To achieve this we would need to exchange the Text on the first capture subcontrol dynamically. This can * be done, but I'll leave this open for now. */ } // Redo the tickmarks to last slider in the slider list. // The implementation is not obvious, so lets explain: // We ALWAYS have tickmarks on the LAST slider. Sometimes the slider is not shown, and then we just don't bother. // a) So, if the last slider has tickmarks, we can always call setTicks( true ). // b) if the last slider has NO tickmarks, there ae no tickmarks at all, and we don't need to redo the tickmarks. QSlider* slider = qobject_cast( ref_sliders.last() ); if( slider && slider->tickPosition() != QSlider::NoTicks) setTicks( true ); } void MDWSlider::setLabeled(bool value) { if ( m_controlLabel != 0) m_controlLabel->setVisible(value); layout()->activate(); } void MDWSlider::setTicks( bool value ) { if (m_slidersPlayback.count() != 0) setTicksInternal(m_slidersPlayback, value); if (m_slidersCapture.count() != 0) setTicksInternal(m_slidersCapture, value); } /** * Enables or disables tickmarks * Please note that always only the first and last slider have tickmarks. */ void MDWSlider::setTicksInternal(QList& ref_sliders, bool ticks) { VolumeSlider* slider = qobject_cast( ref_sliders[0]); if (slider == 0 ) return; // Ticks are only in VolumeSlider, but not in KSmallslider if( ticks ) { if( isStereoLinked() ) slider->setTickPosition( QSlider::TicksRight ); else { slider->setTickPosition( QSlider::NoTicks ); slider = qobject_cast(ref_sliders.last()); slider->setTickPosition( QSlider::TicksLeft ); } } else { slider->setTickPosition( QSlider::NoTicks ); slider = qobject_cast(ref_sliders.last()); slider->setTickPosition( QSlider::NoTicks ); } } void MDWSlider::setIcons(bool value) { if ( m_controlIcon != 0 ) { if ( ( !m_controlIcon->isHidden() ) !=value ) { if (value) m_controlIcon->show(); else m_controlIcon->hide(); layout()->activate(); } } // if it has an icon } void MDWSlider::setColors( QColor high, QColor low, QColor back ) { for( int i=0; i(slider); if ( smallSlider ) smallSlider->setColors( high, low, back ); } for( int i=0; i(slider); if ( smallSlider ) smallSlider->setColors( high, low, back ); } } void MDWSlider::setMutedColors( QColor high, QColor low, QColor back ) { for( int i=0; i(slider); if ( smallSlider ) smallSlider->setGrayColors( high, low, back ); } for( int i=0; i(slider); if ( smallSlider ) smallSlider->setGrayColors( high, low, back ); } } /** This slot is called, when a user has changed the volume via the KMix Slider. */ void MDWSlider::volumeChange( int ) { // if ( mixDevice()->id() == "Headphone:0" ) // { // qCDebug(KMIX_LOG) << "headphone bug"; // } if (!m_slidersPlayback.isEmpty()) { ++m_waitForSoundSetComplete; volumeValues.push_back(m_slidersPlayback.first()->value()); volumeChangeInternal(mixDevice()->playbackVolume(), m_slidersPlayback); } if (!m_slidersCapture.isEmpty()) { volumeChangeInternal(mixDevice()->captureVolume(), m_slidersCapture); } QSignalBlocker blocker(view()); mixDevice()->mixer()->commitVolumeChange(mixDevice()); } void MDWSlider::volumeChangeInternal(Volume& vol, QList& ref_sliders) { if (isStereoLinked()) { QAbstractSlider* firstSlider = ref_sliders.first(); mixDevice()->setMuted(false); vol.setAllVolumes(firstSlider->value()); } else { for (int i = 0; i < ref_sliders.count(); i++) { if (mixDevice()->isMuted()) { // changing from muted state: unmute (the "if" above is actually superfluous) mixDevice()->setMuted(false); } QAbstractSlider *sliderWidget = ref_sliders[i]; vol.setVolume(extraData(sliderWidget).getChid(), sliderWidget->value()); } // iterate over all sliders } } /** This slot is called, when a user has clicked the recsrc button. Also it is called by any other associated QAction like the context menu. */ void MDWSlider::toggleRecsrc() { setRecsrc( !mixDevice()->isRecSource() ); } void MDWSlider::setRecsrc(bool value) { if ( mixDevice()->captureVolume().hasSwitch() ) { mixDevice()->setRecSource( value ); mixDevice()->mixer()->commitVolumeChange( mixDevice() ); } } /** This slot is called, when a user has clicked the mute button. Also it is called by any other associated QAction like the context menu. */ void MDWSlider::toggleMuted() { setMuted( !mixDevice()->isMuted() ); } void MDWSlider::setMuted(bool value) { if ( mixDevice()->hasMuteSwitch() ) { mixDevice()->setMuted( value ); mixDevice()->mixer()->commitVolumeChange(mixDevice()); } } void MDWSlider::setDisabled( bool hide ) { emit guiVisibilityChange(this, !hide); } /** * This slot is called on a Keyboard Shortcut event, except for the XF86Audio* shortcuts which are handled by the * KMixWindow class. So for 99.9% of all users, this method is never called. */ void MDWSlider::increaseVolume() { increaseOrDecreaseVolume(false, Volume::Both); } /** * This slot is called on a Keyboard Shortcut event, except for the XF86Audio* shortcuts which hare handled by the * KMixWindow class. So for 99.9% of all users, this method is never called. */ void MDWSlider::decreaseVolume() { increaseOrDecreaseVolume(true, Volume::Both); } /** * Increase or decrease all playback and capture channels of the given control. * This method is very similar to Mixer::increaseOrDecreaseVolume(), but it will * auto-unmute on increase. * * @param mixdeviceID The control name * @param decrease true for decrease. false for increase */ void MDWSlider::increaseOrDecreaseVolume(bool decrease, Volume::VolumeTypeFlag volumeType) { mixDevice()->increaseOrDecreaseVolume(decrease, volumeType); // I should possibly not block, as the changes that come back from the Soundcard // will be ignored (e.g. because of capture groups) // qCDebug(KMIX_LOG) << "MDWSlider is blocking signals for " << view()->id(); // bool oldViewBlockSignalState = view()->blockSignals(true); mixDevice()->mixer()->commitVolumeChange(mixDevice()); // qCDebug(KMIX_LOG) << "MDWSlider is unblocking signals for " << view()->id(); // view()->blockSignals(oldViewBlockSignalState); } /** * Must be called by the triggered(bool) signal from a QAction. */ void MDWSlider::moveStream(bool checked) { Q_UNUSED(checked); QAction *act = qobject_cast(sender()); Q_ASSERT(act!=nullptr); const QString destId = act->data().toString(); mixDevice()->mixer()->moveStream(mixDevice()->id(), destId); } /** * This is called whenever there are volume updates pending from the hardware for this MDW. */ void MDWSlider::update() { // bool debugMe = (mixDevice()->id() == "PCM:0" ); // if (debugMe) qCDebug(KMIX_LOG) << "The update() PCM:0 playback state" << mixDevice()->isMuted() // << ", vol=" << mixDevice()->playbackVolume().getAvgVolumePercent(Volume::MALL); if ( m_slidersPlayback.count() != 0 || mixDevice()->hasMuteSwitch() ) updateInternal(mixDevice()->playbackVolume(), m_slidersPlayback, mixDevice()->isMuted() ); if ( m_slidersCapture.count() != 0 || mixDevice()->captureVolume().hasSwitch() ) updateInternal(mixDevice()->captureVolume(), m_slidersCapture, mixDevice()->isNotRecSource() ); if (m_controlLabel!=nullptr) { QLabel *l; VerticalText *v; if ((l = dynamic_cast(m_controlLabel))) l->setText(mixDevice()->readableName()); else if ((v = dynamic_cast(m_controlLabel))) v->setText(mixDevice()->readableName()); } updateAccesability(); } /** * * @param vol * @param ref_sliders * @param muted Future directions: passing "muted" should not be necessary any longer - due to getVolumeForGUI() */ void MDWSlider::updateInternal(Volume& vol, QList& ref_sliders, bool muted) { // bool debugMe = (mixDevice()->id() == "PCM:0" ); // if (debugMe) // { // qCDebug(KMIX_LOG) << "The updateInternal() PCM:0 playback state" << mixDevice()->isMuted() // << ", vol=" << mixDevice()->playbackVolume().getAvgVolumePercent(Volume::MALL); // } for (int i = 0; i-1 && --m_waitForSoundSetComplete<1) { m_waitForSoundSetComplete = 0; volumeValues.removeAt(volume_index); if (!m_sliderInWork) slider->setValue(useVolume); } else if (!m_sliderInWork && m_waitForSoundSetComplete<1) { slider->setValue(useVolume); } // --- Avoid feedback loops END ----------------- KSmallSlider *smallSlider = qobject_cast(slider); if (smallSlider!=nullptr) // faster than QObject::inherits() { smallSlider->setGray(mixDevice()->isMuted()); } } // for all sliders // update mute state if (m_muteButton!=nullptr) { QSignalBlocker blocker(m_muteButton); m_muteButton->setActive(!mixDevice()->isMuted()); } // update capture state if (m_captureButton!=nullptr) { QSignalBlocker blocker(m_captureButton); m_captureButton->setActive(mixDevice()->isRecSource()); } } #ifndef QT_NO_ACCESSIBILITY void MDWSlider::updateAccesability() { if (m_linked) { if (!m_slidersPlayback.isEmpty()) m_slidersPlayback[0]->setAccessibleName(m_slidersPlayback[0]->toolTip()); if (!m_slidersCapture.isEmpty()) m_slidersCapture[0]->setAccessibleName(m_slidersCapture[0]->toolTip()); } else { QList vols = mixDevice()->playbackVolume().getVolumes().values(); foreach (QAbstractSlider *slider, m_slidersPlayback) { slider->setAccessibleName(slider->toolTip()+ " (" +Volume::channelNameReadable(vols.first().chid)+')'); vols.pop_front(); } vols = mixDevice()->captureVolume().getVolumes().values(); foreach (QAbstractSlider *slider, m_slidersCapture) { slider->setAccessibleName(slider->toolTip()+ " (" +Volume::channelNameReadable(vols.first().chid)+')'); vols.pop_front(); } } } #endif void MDWSlider::showContextMenu(const QPoint &pos) { if (view()==nullptr) return; QMenu *menu = view()->getPopup(); menu->addSection( QIcon::fromTheme( "kmix" ), mixDevice()->readableName() ); if (m_moveMenu) { MixSet *ms = mixDevice()->moveDestinationMixSet(); Q_ASSERT(ms!=nullptr); m_moveMenu->setEnabled(ms->count()>1); // The "Event Sounds" stream cannot be moved at present. This is because // Mixer_PULSE::moveStream() does not record the stream ID in the // output stream list and hence cannot get its PulseAudio stream index. // I don't know whether this is a design decision or a PA limitation. - if (mixDevice()->id().endsWith(":event")) m_moveMenu->setEnabled(false); + if (mixDevice()->id().endsWith(QLatin1String(":event"))) m_moveMenu->setEnabled(false); menu->addMenu( m_moveMenu ); } if ( m_slidersPlayback.count()>1 || m_slidersCapture.count()>1) { KToggleAction *stereo = qobject_cast(_mdwActions->action("stereo")); if (stereo!=nullptr) { QSignalBlocker blocker(stereo); stereo->setChecked(!isStereoLinked()); menu->addAction( stereo ); } } if ( mixDevice()->captureVolume().hasSwitch() ) { KToggleAction *ta = qobject_cast(_mdwActions->action("recsrc")); if (ta!=nullptr) { QSignalBlocker blocker(ta); ta->setChecked( mixDevice()->isRecSource() ); menu->addAction( ta ); } } if ( mixDevice()->hasMuteSwitch() ) { KToggleAction *ta = qobject_cast(_mdwActions->action("mute")); if (ta!=nullptr) { QSignalBlocker blocker(ta); ta->setChecked( mixDevice()->isMuted() ); menu->addAction( ta ); } } // QAction *a = _mdwActions->action( "hide" ); // if ( a ) // menu->addAction( a ); QAction *b = _mdwActions->action( "keys" ); if (b!=nullptr) { menu->addSeparator(); menu->addAction(b); } menu->popup(pos); } void MDWSlider::showMoveMenu() { const MixSet *ms = mixDevice()->moveDestinationMixSet(); Q_ASSERT(ms!=nullptr); const QString cur = mixDevice()->mixer()->currentStreamDevice(mixDevice()->id()); // There is no need to keep a record of the actions (in a KActionCollection // or otherwise); QMenu::clear() will delete them as long as they are owned // by the menu. m_moveMenu->clear(); // Default action QAction *act = new QAction(i18n("Automatic (according to category)"), m_moveMenu); act->setData(QString()); connect(act, &QAction::triggered, this, &MDWSlider::moveStream, Qt::QueuedConnection); m_moveMenu->addAction(act); m_moveMenu->addSeparator(); // Device actions foreach (const shared_ptr md, *ms) { act = new QAction(QIcon::fromTheme(md->iconName()), md->readableName(), m_moveMenu); act->setData(md->id()); act->setCheckable(true); if (md->id()==cur) act->setChecked(true); connect(act, &QAction::triggered, this, &MDWSlider::moveStream, Qt::QueuedConnection); m_moveMenu->addAction(act); } } /** * An event filter for the various widgets making up this control. * * Redirect all wheel events to the main slider, so that they will be * handled consistently regardless of where the pointer actually is. */ bool MDWSlider::eventFilter(QObject *obj, QEvent *ev) { if (ev->type()!=QEvent::Wheel) return (QWidget::eventFilter(obj, ev)); // only want wheel events if (!ev->spontaneous()) return (false); // avoid recursion on slider QAbstractSlider *slider = qobject_cast(obj); if (slider!=nullptr) // event is over a slider { // Do nothing in this case. No event filter is installed // on a slider, and it will handle the wheel event itself. qCWarning(KMIX_LOG) << "unexpected wheel event on slider" << slider; return (false); } // Mouse is not over a slider. Find the principal slider (the first // playback control if there are any, otherwise the first capture // control if any) and redirect the event to that. if (!m_slidersPlayback.isEmpty()) slider = m_slidersPlayback.first(); else if (!m_slidersCapture.isEmpty()) slider = m_slidersCapture.first(); else slider = nullptr; if (slider!=nullptr) { //qCDebug(KMIX_LOG) << "identified for slider" << slider; QCoreApplication::sendEvent(slider, ev); } return (true); // wheel event handled } diff --git a/gui/toggletoolbutton.cpp b/gui/toggletoolbutton.cpp index 4024e59c..41943471 100644 --- a/gui/toggletoolbutton.cpp +++ b/gui/toggletoolbutton.cpp @@ -1,171 +1,171 @@ /* * KMix -- KDE's full featured mini mixer * * Copyright (C) 2018 Jonathan Marten * * 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, see - * . + * . */ #include "toggletoolbutton.h" #include #include #include #include #include static const int iconSmallSize = 10; class DisabledIconEngine : public QIconEngine { QIcon m_icon; public: DisabledIconEngine(const QIcon &icon) : QIconEngine(), m_icon(icon) {} ~DisabledIconEngine() override {} QIconEngine *clone() const override { return new DisabledIconEngine(m_icon); } void paint(QPainter *painter, const QRect &rect, QIcon::Mode, QIcon::State state) override { return m_icon.paint(painter, rect, Qt::AlignCenter, QIcon::Disabled, state); } QPixmap pixmap(const QSize &size, QIcon::Mode, QIcon::State state) override { return m_icon.pixmap(size, QIcon::Disabled, state); } QSize actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state) override { return m_icon.actualSize(size, mode, state); } QList availableSizes(QIcon::Mode mode, QIcon::State state) const override { return m_icon.availableSizes(mode, state); } }; ToggleToolButton::ToggleToolButton(const QString &activeIconName, QWidget *pnt) : QToolButton(pnt) { mActiveLoaded = mInactiveLoaded = false; mActiveIconName = activeIconName; mSmallSize = false; mIsActive = true; mFirstTime = true; setCheckable(false); setAutoRaise(true); } void ToggleToolButton::setActive(bool active) { if (!mFirstTime && (active==mIsActive)) return; // no change required mIsActive = active; // record required state mFirstTime = false; // note now initialised QIcon icon; // new icon to set if (mIsActive) // look at new state { if (mActiveIcon.isNull()) // need icon for active state { // only if not already tried if (!mActiveLoaded) mActiveIcon = QIcon::fromTheme(mActiveIconName); mActiveLoaded = true; // note not to try again } icon = mActiveIcon; // the icon to use } else // want inactive state { if (mInactiveIcon.isNull()) // need icon for inactive state { if (!mInactiveIconName.isEmpty()) // inactive icon is set { if (!mInactiveLoaded) mInactiveIcon = QIcon::fromTheme(mInactiveIconName); } else { // need to derive from active state if (!mActiveLoaded) mActiveIcon = QIcon::fromTheme(mActiveIconName); mActiveLoaded = true; // only if not already tried if (mActiveIcon.isNull()) qCWarning(KMIX_LOG) << "want inactive but no active available"; else { mInactiveIcon = QIcon(new DisabledIconEngine(mActiveIcon)); } } mInactiveLoaded = true; // note not to try again } icon = mInactiveIcon; // the icon to use } if (icon.isNull()) return; // icon not available setIcon(icon); // set button icon } /** * Loads the icon with the given @p iconName * and applies it to the @p label widget. The widget must be either a * QLabel or a QToolButton. * * Originally @c MDWSlider::setIcon(), moved here because it uses the same * icon size parameters as a @c ToggleToolButton, and it can share @c getPixmap(). */ void ToggleToolButton::setIndicatorIcon(const QString &iconName, QWidget *label, bool small) { const auto icon = QIcon::fromTheme(iconName); if (icon.isNull()) { qCWarning(KMIX_LOG) << "Could not get icon for" << iconName; return; } QSize iconSize; if (small) // small size, set for scaled icon { iconSize = QSize(iconSmallSize, iconSmallSize); } else // not small size, set for normal icon { const auto toolBarIconSize = label->style()->pixelMetric(QStyle::PM_TabBarIconSize, nullptr, label); iconSize = QSize(toolBarIconSize, toolBarIconSize); } label->resize(iconSize); label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); QLabel *lbl = qobject_cast(label); if (lbl!=nullptr) { lbl->setPixmap(icon.pixmap(iconSize)); lbl->setAlignment(Qt::AlignHCenter|Qt::AlignVCenter); } else { QToolButton *tbt = qobject_cast(label); if (tbt!=nullptr) tbt->setIcon(icon); } } diff --git a/gui/toggletoolbutton.h b/gui/toggletoolbutton.h index a2cd0db8..e3208255 100644 --- a/gui/toggletoolbutton.h +++ b/gui/toggletoolbutton.h @@ -1,67 +1,67 @@ //-*-C++-*- /* * KMix -- KDE's full featured mini mixer * * Copyright (C) 2018 Jonathan Marten * * 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, see - * . + * . */ #ifndef TOGGLETOOLBUTTON_H #define TOGGLETOOLBUTTON_H #include #include /** * A tool button that can maintain two pixmaps, for an active and inactive state. * * @see QToolButton **/ class ToggleToolButton : public QToolButton { Q_OBJECT public: explicit ToggleToolButton(const QString &activeIconName, QWidget *pnt = nullptr); virtual ~ToggleToolButton() = default; void setActive(bool active = true); void setSmallSize(bool small = true) { mSmallSize = small; } bool isActive() const { return (mIsActive); } void setActiveIcon(const QString &name) { mActiveIconName = name; } void setInactiveIcon(const QString &name) { mInactiveIconName = name; } static void setIndicatorIcon(const QString &iconName, QWidget *label, bool small = false); private: bool mSmallSize; QString mActiveIconName; QString mInactiveIconName; bool mIsActive; bool mFirstTime; QIcon mActiveIcon; bool mActiveLoaded; QIcon mInactiveIcon; bool mInactiveLoaded; }; #endif // TOGGLETOOLBUTTON_H