File indexing completed on 2024-12-01 05:05:10
0001 /* 0002 SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de> 0003 SPDX-FileCopyrightText: 2020 MBition GmbH 0004 Author: Kai Uwe Broulik <kai_uwe.broulik@mbition.io> 0005 0006 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0007 */ 0008 0009 #include "microphoneindicator.h" 0010 0011 #include <QAction> 0012 #include <QIcon> 0013 #include <QMenu> 0014 #include <QTimer> 0015 0016 #include <KLocalizedString> 0017 #include <KStatusNotifierItem> 0018 0019 #include "client.h" 0020 #include "context.h" 0021 #include "pulseaudio.h" 0022 #include "source.h" 0023 0024 #include "volumeosd.h" 0025 0026 using namespace QPulseAudio; 0027 0028 MicrophoneIndicator::MicrophoneIndicator(QObject *parent) 0029 : QObject(parent) 0030 , m_sourceModel(new SourceModel(this)) 0031 , m_sourceOutputModel(new SourceOutputModel(this)) 0032 , m_updateTimer(new QTimer(this)) 0033 { 0034 connect(m_sourceModel, &QAbstractItemModel::rowsInserted, this, &MicrophoneIndicator::scheduleUpdate); 0035 connect(m_sourceModel, &QAbstractItemModel::rowsRemoved, this, &MicrophoneIndicator::scheduleUpdate); 0036 connect(m_sourceModel, &QAbstractItemModel::dataChanged, this, &MicrophoneIndicator::scheduleUpdate); 0037 0038 connect(m_sourceOutputModel, &QAbstractItemModel::rowsInserted, this, &MicrophoneIndicator::scheduleUpdate); 0039 connect(m_sourceOutputModel, &QAbstractItemModel::rowsRemoved, this, &MicrophoneIndicator::scheduleUpdate); 0040 connect(m_sourceOutputModel, &QAbstractItemModel::dataChanged, this, &MicrophoneIndicator::scheduleUpdate); 0041 0042 m_updateTimer->setInterval(0); 0043 m_updateTimer->setSingleShot(true); 0044 connect(m_updateTimer, &QTimer::timeout, this, &MicrophoneIndicator::update); 0045 0046 scheduleUpdate(); 0047 } 0048 0049 MicrophoneIndicator::~MicrophoneIndicator() = default; 0050 0051 void MicrophoneIndicator::init() 0052 { 0053 // does nothing, just prompts QML engine to create an instance of the singleton 0054 } 0055 0056 void MicrophoneIndicator::scheduleUpdate() 0057 { 0058 if (!m_updateTimer->isActive()) { 0059 m_updateTimer->start(); 0060 } 0061 } 0062 0063 void MicrophoneIndicator::update() 0064 { 0065 const auto apps = recordingApplications(); 0066 if (apps.isEmpty()) { 0067 m_showOsdOnUpdate = false; 0068 delete m_sni; 0069 m_sni = nullptr; 0070 return; 0071 } 0072 0073 if (!m_sni) { 0074 m_sni = new KStatusNotifierItem(QStringLiteral("microphone")); 0075 m_sni->setCategory(KStatusNotifierItem::Hardware); 0076 // always Active since it is completely removed when microphone isn't in use 0077 m_sni->setStatus(KStatusNotifierItem::Active); 0078 0079 // but also middle click to be consistent with volume icon 0080 connect(m_sni, &KStatusNotifierItem::secondaryActivateRequested, this, &MicrophoneIndicator::toggleMuted); 0081 connect(m_sni, &KStatusNotifierItem::activateRequested, this, &MicrophoneIndicator::toggleMuted); 0082 0083 connect(m_sni, &KStatusNotifierItem::scrollRequested, this, [this](int delta, Qt::Orientation orientation) { 0084 if (orientation != Qt::Vertical) { 0085 return; 0086 } 0087 0088 m_wheelDelta += delta; 0089 0090 while (m_wheelDelta >= 120) { 0091 m_wheelDelta -= 120; 0092 adjustVolume(+1); 0093 } 0094 while (m_wheelDelta <= -120) { 0095 m_wheelDelta += 120; 0096 adjustVolume(-1); 0097 } 0098 }); 0099 0100 QMenu *menu = m_sni->contextMenu(); 0101 0102 m_muteAction = menu->addAction(QIcon::fromTheme(QStringLiteral("microphone-sensitivity-muted")), i18n("Mute")); 0103 m_muteAction->setCheckable(true); 0104 connect(m_muteAction.data(), &QAction::triggered, this, &MicrophoneIndicator::setMuted); 0105 0106 // don't let it quit plasmashell 0107 m_sni->setStandardActionsEnabled(false); 0108 } 0109 0110 const bool allMuted = muted(); 0111 0112 QString iconName; 0113 if (allMuted) { 0114 iconName = QStringLiteral("microphone-sensitivity-muted"); 0115 } else { 0116 if (Source *defaultSource = m_sourceModel->defaultSource()) { 0117 const int percent = volumePercent(defaultSource); 0118 iconName = QStringLiteral("microphone-sensitivity"); 0119 // it deliberately never shows the "muted" icon unless *all* microphones are muted 0120 if (percent <= 25) { 0121 iconName.append(QStringLiteral("-low")); 0122 } else if (percent <= 75) { 0123 iconName.append(QStringLiteral("-medium")); 0124 } else { 0125 iconName.append(QStringLiteral("-high")); 0126 } 0127 } else { 0128 iconName = QStringLiteral("microphone-sensitivity-high"); 0129 } 0130 } 0131 0132 m_sni->setTitle(i18n("Microphone")); 0133 m_sni->setIconByName(iconName); 0134 m_sni->setToolTip(QIcon::fromTheme(iconName), allMuted ? i18n("Microphone Muted") : i18n("Microphone"), toolTipForApps(apps)); 0135 0136 if (m_muteAction) { 0137 m_muteAction->setChecked(allMuted); 0138 } 0139 0140 if (m_showOsdOnUpdate) { 0141 showOsd(); 0142 m_showOsdOnUpdate = false; 0143 } 0144 } 0145 0146 bool MicrophoneIndicator::muted() const 0147 { 0148 static const int s_mutedRole = m_sourceModel->role(QByteArrayLiteral("Muted")); 0149 Q_ASSERT(s_mutedRole > -1); 0150 0151 for (int row = 0; row < m_sourceModel->rowCount(); ++row) { 0152 const QModelIndex idx = m_sourceModel->index(row); 0153 if (!idx.data(s_mutedRole).toBool()) { 0154 // this is deliberately checking if *all* microphones are muted rather than the preferred one 0155 return false; 0156 } 0157 } 0158 0159 return true; 0160 } 0161 0162 void MicrophoneIndicator::setMuted(bool muted) 0163 { 0164 static const int s_mutedRole = m_sourceModel->role(QByteArrayLiteral("Muted")); 0165 Q_ASSERT(s_mutedRole > -1); 0166 0167 m_showOsdOnUpdate = true; 0168 0169 if (muted) { 0170 for (int row = 0; row < m_sourceModel->rowCount(); ++row) { 0171 const QModelIndex idx = m_sourceModel->index(row); 0172 if (!idx.data(s_mutedRole).toBool()) { 0173 m_sourceModel->setData(idx, true, s_mutedRole); 0174 m_mutedIndices.append(QPersistentModelIndex(idx)); 0175 continue; 0176 } 0177 } 0178 return; 0179 } 0180 0181 // If we didn't mute it, unmute all 0182 if (m_mutedIndices.isEmpty()) { 0183 for (int i = 0; i < m_sourceModel->rowCount(); ++i) { 0184 m_sourceModel->setData(m_sourceModel->index(i), false, s_mutedRole); 0185 } 0186 return; 0187 } 0188 0189 // Otherwise unmute the devices we muted 0190 for (auto &idx : std::as_const(m_mutedIndices)) { 0191 if (!idx.isValid()) { 0192 continue; 0193 } 0194 m_sourceModel->setData(idx, false, s_mutedRole); 0195 } 0196 m_mutedIndices.clear(); 0197 0198 // no update() needed as the model signals a change 0199 } 0200 0201 void MicrophoneIndicator::toggleMuted() 0202 { 0203 setMuted(!muted()); 0204 } 0205 0206 void MicrophoneIndicator::adjustVolume(int direction) 0207 { 0208 Source *source = m_sourceModel->defaultSource(); 0209 if (!source) { 0210 return; 0211 } 0212 0213 const int step = qRound(5 * Context::NormalVolume / 100.0); 0214 0215 const auto newVolume = qBound(Context::MinimalVolume, // 0216 source->volume() + direction * step, // 0217 Context::NormalVolume); 0218 0219 source->setVolume(newVolume); 0220 source->setMuted(newVolume == Context::MinimalVolume); 0221 0222 m_showOsdOnUpdate = true; 0223 } 0224 0225 int MicrophoneIndicator::volumePercent(Source *source) 0226 { 0227 return source->isMuted() ? 0 : qRound(source->volume() / static_cast<qreal>(Context::NormalVolume) * 100); 0228 } 0229 0230 void MicrophoneIndicator::showOsd() 0231 { 0232 if (!m_osd) { 0233 m_osd = new VolumeOSD(this); 0234 } 0235 0236 auto *preferredSource = m_sourceModel->defaultSource(); 0237 if (!preferredSource) { 0238 return; 0239 } 0240 0241 m_osd->showMicrophone(volumePercent(preferredSource)); 0242 } 0243 0244 QList<QModelIndex> MicrophoneIndicator::recordingApplications() const 0245 { 0246 QList<QModelIndex> indices; 0247 0248 // If there are no microphones present, there's nothing to record 0249 if (m_sourceModel->rowCount() == 0) { 0250 return indices; 0251 } 0252 0253 static const int s_virtualStreamRole = m_sourceOutputModel->role(QByteArrayLiteral("VirtualStream")); 0254 Q_ASSERT(s_virtualStreamRole > -1); 0255 0256 indices.reserve(m_sourceOutputModel->rowCount()); 0257 0258 for (int i = 0; i < m_sourceOutputModel->rowCount(); ++i) { 0259 const QModelIndex idx = m_sourceOutputModel->index(i); 0260 0261 if (idx.data(s_virtualStreamRole).toBool()) { 0262 continue; 0263 } 0264 0265 indices.append(idx); 0266 } 0267 0268 return indices; 0269 } 0270 0271 QString MicrophoneIndicator::toolTipForApps(const QList<QModelIndex> &apps) const 0272 { 0273 Q_ASSERT(!apps.isEmpty()); 0274 0275 if (apps.count() > 1) { 0276 QStringList names; 0277 names.reserve(apps.count()); 0278 for (const QModelIndex &idx : apps) { 0279 names.append(sourceOutputDisplayName(idx)); 0280 } 0281 names.removeDuplicates(); 0282 // Still more than one app? 0283 if (names.count() > 1) { 0284 return i18nc("List of apps is using mic", "%1 are using the microphone", names.join(i18nc("list separator", ", "))); 0285 } 0286 } 0287 0288 const QModelIndex appIdx = apps.constFirst(); 0289 0290 // If there is more than one microphone, show which one is being used. 0291 // An app could record multiple microphones simultaneously, or the user having the same app running 0292 // multiple times recording the same microphone, but this isn't covered here for simplicity. 0293 if (apps.count() == 1 && m_sourceModel->rowCount() > 1) { 0294 static const int s_sourceModelDescriptionRole = m_sourceModel->role(QByteArrayLiteral("Description")); 0295 Q_ASSERT(s_sourceModelDescriptionRole > -1); 0296 static const int s_sourceModelIndexRole = m_sourceModel->role("Index"); 0297 Q_ASSERT(s_sourceModelIndexRole > -1); 0298 0299 static const int s_sourceOutputModelDeviceIndexRole = m_sourceOutputModel->role("DeviceIndex"); 0300 Q_ASSERT(s_sourceOutputModelDeviceIndexRole > -1); 0301 0302 const int sourceOutputDeviceIndex = appIdx.data(s_sourceOutputModelDeviceIndexRole).toInt(); 0303 0304 for (int i = 0; i < m_sourceModel->rowCount(); ++i) { 0305 const QModelIndex sourceDeviceIdx = m_sourceModel->index(i, 0); 0306 const int sourceDeviceIndex = sourceDeviceIdx.data(s_sourceModelIndexRole).toInt(); 0307 0308 if (sourceDeviceIndex == sourceOutputDeviceIndex) { 0309 return i18nc("App %1 is using mic with name %2", 0310 "%1 is using the microphone (%2)", 0311 sourceOutputDisplayName(appIdx), 0312 sourceDeviceIdx.data(s_sourceModelDescriptionRole).toString()); 0313 } 0314 } 0315 } 0316 0317 return i18nc("App is using mic", "%1 is using the microphone", sourceOutputDisplayName(appIdx)); 0318 } 0319 0320 QString MicrophoneIndicator::sourceOutputDisplayName(const QModelIndex &idx) const 0321 { 0322 Q_ASSERT(idx.model() == m_sourceOutputModel); 0323 0324 static const int s_nameRole = m_sourceOutputModel->role(QByteArrayLiteral("Name")); 0325 Q_ASSERT(s_nameRole > -1); 0326 static const int s_clientRole = m_sourceOutputModel->role(QByteArrayLiteral("Client")); 0327 Q_ASSERT(s_clientRole > -1); 0328 0329 auto *client = qobject_cast<Client *>(idx.data(s_clientRole).value<QObject *>()); 0330 0331 return client ? client->name() : idx.data(s_nameRole).toString(); 0332 }