/* This file is part of the KDE project SPDX-FileCopyrightText: 2006 Kevin Ottens SPDX-FileCopyrightText: 2008-2010 Dario Freddi SPDX-FileCopyrightText: 2010 Alejandro Fiestas SPDX-FileCopyrightText: 2010-2013 Lukáš Tinkl SPDX-FileCopyrightText: 2015 Kai Uwe Broulik SPDX-FileCopyrightText: 2024 Jakob Petsovits SPDX-FileCopyrightText: 2024 Xaver Hugl SPDX-License-Identifier: LGPL-2.0-only */ #include "screenbrightnesscontroller.h" #include "backlightbrightness.h" #include "ddcutildetector.h" #include "displaybrightness.h" #include "externalbrightnesscontrol.h" #include "kwinbrightness.h" #include #include #include #include #include #include #include #include #include #include // std::ranges::find_if ScreenBrightnessController::ScreenBrightnessController() : QObject() , m_detectors({ { .detector = new KWinDisplayDetector(this), .debugName = "kwin brightness control", .displayIdPrefix = "kwin:", }, { .detector = new BacklightDetector(this), .debugName = "internal display backlight", .displayIdPrefix = "backlight:", }, { .detector = new DDCutilDetector(this), .debugName = "libddcutil", .displayIdPrefix = "ddc:", }, }) , m_externalBrightnessController(std::make_unique()) { connect(m_externalBrightnessController.get(), &ExternalBrightnessController::activeChanged, this, &ScreenBrightnessController::onDetectorDisplaysChanged); const auto op = new KScreen::GetConfigOperation(KScreen::GetConfigOperation::NoOptions, this); connect(op, &KScreen::GetConfigOperation::finished, this, [this](KScreen::ConfigOperation *configOp) { if (configOp->hasError()) { return; } m_kscreenConfig = static_cast(configOp)->config(); KScreen::ConfigMonitor::instance()->addConfig(m_kscreenConfig); }); } ScreenBrightnessController::~ScreenBrightnessController() { } void ScreenBrightnessController::detectDisplays() { qCDebug(POWERDEVIL) << "Trying to detect displays for brightness control..."; m_finishedDetectingCount = 0; for (const DetectorInfo &detectorInfo : m_detectors) { DisplayBrightnessDetector *detector = detectorInfo.detector; disconnect(detector, nullptr, this, nullptr); connect(detector, &DisplayBrightnessDetector::detectionFinished, this, [this, detector]() { disconnect(detector, &DisplayBrightnessDetector::detectionFinished, this, nullptr); if (++m_finishedDetectingCount; m_finishedDetectingCount == m_detectors.size()) { onDetectorDisplaysChanged(); Q_EMIT detectionFinished(); } connect(detector, &DisplayBrightnessDetector::displaysChanged, this, &ScreenBrightnessController::onDetectorDisplaysChanged); }); detector->detect(); } } bool ScreenBrightnessController::isSupported() const { return !m_sortedDisplayIds.isEmpty(); } QStringList ScreenBrightnessController::displayIds() const { return m_sortedDisplayIds; } void ScreenBrightnessController::onDisplayDestroyed(QObject *obj) { for (auto &[id, info] : m_displaysById) { if (info.display == obj) { // we'll do the proper removal in onDetectorDisplaysChanged() which should come // right afterwards, just don't call it anymore including through QObject::disconnect() info.zombie = true; } } } void ScreenBrightnessController::onDetectorDisplaysChanged() { m_sortedDisplayIds.clear(); QStringList legacyDisplayIds; std::unordered_map newDisplayById; QList newForExternalControl; // for backwards compatibility with legacy API clients, set the same brightness to all displays // of the first detector - e.g. to only the backlight display, or to all external DDC monitors DisplayBrightnessDetector *firstSupportedDetector = nullptr; // add new displays for (const DetectorInfo &detectorInfo : m_detectors) { const bool shouldUseExternalControl = m_externalBrightnessController->isActive() && !dynamic_cast(detectorInfo.detector); QList detectorDisplays = detectorInfo.detector->displays(); if (!detectorDisplays.isEmpty() && !shouldUseExternalControl) { if (firstSupportedDetector == nullptr) { firstSupportedDetector = detectorInfo.detector; } qCDebug(POWERDEVIL) << "Using" << detectorInfo.debugName << "for brightness controls."; } for (DisplayBrightness *display : std::as_const(detectorDisplays)) { const QString displayId = QString::fromLocal8Bit(detectorInfo.displayIdPrefix) + display->id(); if (shouldUseExternalControl) { newForExternalControl.push_back(display); } else { auto &info = newDisplayById[displayId]; info = DisplayInfo{ .display = display, .detector = detectorInfo.detector, }; info.brightnessLogic.setValueRange(display->knownSafeMinBrightness(), display->maxBrightness()); info.brightnessLogic.setValue(display->brightness()); m_sortedDisplayIds.push_back(displayId); if (detectorInfo.detector == firstSupportedDetector) { legacyDisplayIds.append(displayId); } } } } QStringList removedDisplayIds; for (const auto &[displayId, info] : m_displaysById) { const auto it = newDisplayById.find(displayId); const bool removed = it == newDisplayById.end(); const bool replaced = !removed && it->second.display != info.display; if ((removed || replaced) && !info.zombie) { disconnect(info.display, nullptr, this, nullptr); } if (removed) { removedDisplayIds.push_back(displayId); } } QStringList addedDisplayIds; QStringList brightnessChangedDisplayIds; for (auto &[displayId, info] : newDisplayById) { const auto it = m_displaysById.find(displayId); const bool added = it == m_displaysById.end(); const bool replaced = !added && info.display != it->second.display; const bool valueChanged = replaced && (it->second.brightnessLogic.info().value != info.display->brightness() || it->second.brightnessLogic.info().valueMax != info.display->maxBrightness()); if (added || replaced) { connect(info.display, &QObject::destroyed, this, &ScreenBrightnessController::onDisplayDestroyed); connect(info.display, &DisplayBrightness::externalBrightnessChangeObserved, this, &ScreenBrightnessController::onExternalBrightnessChangeObserved); } else { // migrate any local state from the old element to its replacement info.trackingError = it->second.trackingError; } if (valueChanged) { brightnessChangedDisplayIds.push_back(displayId); } if (added) { addedDisplayIds.push_back(displayId); } } m_displaysById = std::move(newDisplayById); m_externalBrightnessController->setDisplays(newForExternalControl); for (const QString &removed : removedDisplayIds) { Q_EMIT displayRemoved(removed); } for (const QString &added : addedDisplayIds) { Q_EMIT displayAdded(added); } if (!addedDisplayIds.isEmpty() || !removedDisplayIds.isEmpty()) { Q_EMIT displayIdsChanged(m_sortedDisplayIds); } for (const QString &changed : brightnessChangedDisplayIds) { const auto &[id, info] = *m_displaysById.find(changed); qCDebug(POWERDEVIL) << "Screen brightness of display" << changed << "after detection/reconfiguration:" << info.brightnessLogic.info().value; Q_EMIT brightnessChanged(changed, info.brightnessLogic.info(), QString(), QString(), SuppressIndicator); } const QString previousFirstDisplayId = m_legacyDisplayIds.value(0, QString()); if (m_legacyDisplayIds != legacyDisplayIds) { m_legacyDisplayIds = legacyDisplayIds; Q_EMIT legacyDisplayIdsChanged(m_legacyDisplayIds); } if (!isSupported()) { qCDebug(POWERDEVIL) << "No suitable displays detected. Brightness controls are unsupported in this configuration."; return; } // legacy API needs to emit a brightness change signal for the first display, regardless of // whether the new first display was newly added or an existing display moved up to index 0 const QString &newFirstDisplayId = m_legacyDisplayIds.first(); if (newFirstDisplayId != previousFirstDisplayId || brightnessChangedDisplayIds.contains(newFirstDisplayId)) { const auto &[id, info] = *m_displaysById.find(newFirstDisplayId); qCDebug(POWERDEVIL) << "Screen brightness of first display after detection/reconfiguration:" << info.brightnessLogic.info().value; Q_EMIT legacyBrightnessInfoChanged(info.brightnessLogic.info(), SuppressIndicator); } } QString ScreenBrightnessController::label(const QString &displayId) const { if (const auto it = m_displaysById.find(displayId); it != m_displaysById.end() && !it->second.zombie) { QString result = it->second.display->label(); qCDebug(POWERDEVIL) << "Screen label of" << displayId << "is" << result; return result; } qCWarning(POWERDEVIL) << "Query screen label failed: no display with id" << displayId; return QString(); } bool ScreenBrightnessController::isInternal(const QString &displayId) const { if (const auto it = m_displaysById.find(displayId); it != m_displaysById.end() && !it->second.zombie) { bool result = it->second.display->isInternal(); qCDebug(POWERDEVIL) << "Screen " << displayId << (result ? "is internal" : "is not internal"); return result; } qCWarning(POWERDEVIL) << "Query screen is internal failed: no display with id" << displayId; return false; } int ScreenBrightnessController::knownSafeMinBrightness(const QString &displayId) const { if (const auto it = m_displaysById.find(displayId); it != m_displaysById.end() && !it->second.zombie) { int result = it->second.display->knownSafeMinBrightness(); qCDebug(POWERDEVIL) << "Screen knownSafeMinBrightness of" << displayId << "is" << result; return result; } qCWarning(POWERDEVIL) << "Query screen knownSafeMinBrightness failed: no display with id" << displayId; return 0; } int ScreenBrightnessController::minBrightness(const QString &displayId) const { if (const auto it = m_displaysById.find(displayId); it != m_displaysById.end()) { int result = it->second.brightnessLogic.info().valueMin; qCDebug(POWERDEVIL) << "Screen minBrightness of" << displayId << "is" << result; return result; } qCWarning(POWERDEVIL) << "Query screen minBrightness failed: no display with id" << displayId; return 0; } int ScreenBrightnessController::maxBrightness(const QString &displayId) const { if (const auto it = m_displaysById.find(displayId); it != m_displaysById.end()) { int result = it->second.brightnessLogic.info().valueMax; qCDebug(POWERDEVIL) << "Screen maxBrightness of" << displayId << "is" << result; return result; } qCWarning(POWERDEVIL) << "Query screen maxBrightness failed: no display with id" << displayId; return 0; } int ScreenBrightnessController::brightness(const QString &displayId) const { if (const auto it = m_displaysById.find(displayId); it != m_displaysById.end()) { int result = it->second.brightnessLogic.info().value; qCDebug(POWERDEVIL) << "Screen brightness of" << displayId << "is" << result; return result; } qCWarning(POWERDEVIL) << "Query screen brightness failed: no display with id" << displayId; return 0; } void ScreenBrightnessController::setBrightness(const QString &displayId, int value, const QString &sourceClientName, const QString &sourceClientContext, IndicatorHint hint) { if (auto it = m_displaysById.find(displayId); it != m_displaysById.end() && !it->second.zombie) { auto &[id, info] = *it; const PowerDevil::BrightnessLogic::BrightnessInfo bi = info.brightnessLogic.info(); const int boundedValue = qBound(bi.valueMin, value, bi.valueMax); qCDebug(POWERDEVIL) << "Set screen brightness of" << displayId << "to" << boundedValue << "/" << bi.valueMax; if (value != boundedValue) { qCDebug(POWERDEVIL) << "- clamped from" << value; } // notify only when the internally tracked brightness value is actually different if (bi.value != boundedValue) { info.brightnessLogic.setValue(boundedValue); info.trackingError = 0.0; Q_EMIT brightnessChanged(displayId, info.brightnessLogic.info(), sourceClientName, sourceClientContext, hint); // legacy API without displayId parameter: notify only if the first supported display changed if (displayId == m_legacyDisplayIds.first()) { Q_EMIT legacyBrightnessInfoChanged(info.brightnessLogic.info(), hint); } } // but always call setBrightness() on the display, in case we're unaware of an external change info.display->setBrightness(boundedValue); } else { qCWarning(POWERDEVIL) << "Set screen brightness failed: no display with id" << displayId; } } void ScreenBrightnessController::adjustBrightnessRatio(const QString &displayId, double delta, const QString &sourceClientName, const QString &sourceClientContext, IndicatorHint hint) { if (auto it = m_displaysById.find(displayId); it != m_displaysById.end() && !it->second.zombie) { auto &[id, info] = *it; double oldRatio = info.brightnessLogic.valueAsRatio(); double targetRatio = oldRatio + delta + info.trackingError; setBrightness(displayId, info.brightnessLogic.valueFromRatio(targetRatio), sourceClientName, sourceClientContext, hint); info.trackingError = targetRatio - info.brightnessLogic.valueAsRatio(); } else { qCWarning(POWERDEVIL) << "Adjust screen brightness ratio failed: no display with id" << displayId; } } void ScreenBrightnessController::adjustBrightnessRatio(double delta, const QString &sourceClientName, const QString &sourceClientContext, IndicatorHint hint) { if (m_sortedDisplayIds.isEmpty()) { qCWarning(POWERDEVIL) << "Adjust screen brightness ratio failed: no displays available to adjust"; return; } // if we're going to adjust brightness and accumulate tracking errors, let's make sure at least // one display will actually change its brightness as a result bool any = std::ranges::any_of(std::as_const(m_sortedDisplayIds), [this, delta](const QString &displayId) { if (const auto it = m_displaysById.find(displayId); it != m_displaysById.end() && !it->second.zombie) { // return true if the display still has room to go in the direction of the delta const PowerDevil::BrightnessLogic::BrightnessInfo bi = it->second.brightnessLogic.info(); return (bi.value < bi.valueMax && delta > 0.0) || (bi.value > bi.valueMin && delta < 0.0); } return false; }); if (!any) { return; } for (const QString &displayId : std::as_const(m_sortedDisplayIds)) { adjustBrightnessRatio(displayId, delta, sourceClientName, sourceClientContext, hint); } } void ScreenBrightnessController::adjustBrightnessStep(PowerDevil::BrightnessLogic::StepAdjustmentAction adjustment, const QString &sourceClientName, const QString &sourceClientContext, IndicatorHint hint) { if (m_sortedDisplayIds.isEmpty()) { qCWarning(POWERDEVIL) << "Adjust screen brightness step failed: no displays available to adjust"; return; } double referenceDisplayDelta = 0.0; for (const QString &displayId : std::as_const(m_sortedDisplayIds)) { if (const auto it = m_displaysById.find(displayId); it != m_displaysById.end() && !it->second.zombie) { const auto &[id, info] = *it; double oldRatio = info.brightnessLogic.valueAsRatio(); int referenceDisplayBrightness = info.brightnessLogic.adjusted(adjustment); if (referenceDisplayBrightness < 0) { return; } if (referenceDisplayBrightness != info.brightnessLogic.info().value) { referenceDisplayDelta = info.brightnessLogic.ratio(referenceDisplayBrightness) - oldRatio; break; } } } if (referenceDisplayDelta == 0.0) { return; } for (const QString &displayId : std::as_const(m_sortedDisplayIds)) { adjustBrightnessRatio(displayId, referenceDisplayDelta, sourceClientName, sourceClientContext, hint); } } int ScreenBrightnessController::brightnessSteps(const QString &displayId) const { if (const auto it = m_displaysById.find(displayId); it != m_displaysById.end()) { return it->second.brightnessLogic.steps(); } qCWarning(POWERDEVIL) << "Query screen brightnessSteps failed: no display with id" << displayId; return 1; } KScreen::OutputPtr ScreenBrightnessController::tryMatchKScreenOutput(const QString &displayId) const { if (const auto it = m_displaysById.find(displayId); it != m_displaysById.end() && !it->second.zombie && m_kscreenConfig) { const DisplayBrightness *display = it->second.display; for (const KScreen::OutputPtr &output : m_kscreenConfig->outputs()) { if (display->id() == output->name()) { // for KWinDisplayDetector, primarily return output; } if (output->type() == KScreen::Output::Panel && display->isInternal()) { return output; } const std::optional displayEdid = display->edidData(); if (displayEdid && output->edid() && output->edid()->rawData().startsWith(*displayEdid)) { return output; } } } else if (!m_kscreenConfig) { qCWarning(POWERDEVIL) << "Match KScreen::Output failed: config not initialized"; return KScreen::OutputPtr{}; } else { qCWarning(POWERDEVIL) << "Match KScreen::Output failed: no display with id" << displayId; return KScreen::OutputPtr{}; } // no match found (not an error) return KScreen::OutputPtr{}; } void ScreenBrightnessController::onExternalBrightnessChangeObserved(DisplayBrightness *display, int value) { const auto it = std::ranges::find_if(m_displaysById, [display](const std::pair &pair) { return pair.second.display == display; }); if (it == m_displaysById.end()) { qCWarning(POWERDEVIL) << "External brightness change of untracked display" << display->id() << "to" << value << "/" << display->maxBrightness(); return; } auto &[displayId, info] = *it; if (value == info.brightnessLogic.info().value) { qCDebug(POWERDEVIL) << "External brightness change of display" << displayId << "ignored - same as previous value"; return; } qCDebug(POWERDEVIL) << "External brightness change of display" << displayId << "to" << value << "/" << info.brightnessLogic.info().valueMax; info.brightnessLogic.setValue(value); info.trackingError = 0.0; Q_EMIT brightnessChanged(displayId, info.brightnessLogic.info(), QString(), QString(), SuppressIndicator); // legacy API without displayId parameter: notify only if the first supported display changed if (displayId == m_legacyDisplayIds.first()) { Q_EMIT legacyBrightnessInfoChanged(info.brightnessLogic.info(), SuppressIndicator); } } // // legacy API without displayId parameter QStringList ScreenBrightnessController::legacyDisplayIds() const { return m_legacyDisplayIds; } int ScreenBrightnessController::knownSafeMinBrightness() const { return knownSafeMinBrightness(m_legacyDisplayIds.value(0, QString())); } int ScreenBrightnessController::minBrightness() const { return minBrightness(m_legacyDisplayIds.value(0, QString())); } int ScreenBrightnessController::maxBrightness() const { return maxBrightness(m_legacyDisplayIds.value(0, QString())); } int ScreenBrightnessController::brightness() const { return brightness(m_legacyDisplayIds.value(0, QString())); } void ScreenBrightnessController::setBrightness(int value, IndicatorHint hint) { if (m_legacyDisplayIds.isEmpty()) { qCWarning(POWERDEVIL) << "Set screen brightness failed: no supported display available"; } for (const QString &displayId : std::as_const(m_legacyDisplayIds)) { setBrightness(displayId, value, QString(), QString(), hint); } } int ScreenBrightnessController::brightnessSteps() const { return brightnessSteps(m_legacyDisplayIds.value(0, QString())); } #include "moc_screenbrightnesscontroller.cpp"