diff --git a/colorcorrection/colorcorrectdbusinterface.cpp b/colorcorrection/colorcorrectdbusinterface.cpp index 5376d9b22..c1a4d6fc9 100644 --- a/colorcorrection/colorcorrectdbusinterface.cpp +++ b/colorcorrection/colorcorrectdbusinterface.cpp @@ -1,54 +1,131 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright 2017 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "colorcorrectdbusinterface.h" #include "colorcorrectadaptor.h" #include "manager.h" +#include + namespace KWin { namespace ColorCorrect { ColorCorrectDBusInterface::ColorCorrectDBusInterface(Manager *parent) : QObject(parent) , m_manager(parent) + , m_inhibitorWatcher(new QDBusServiceWatcher(this)) { + m_inhibitorWatcher->setConnection(QDBusConnection::sessionBus()); + m_inhibitorWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + connect(m_inhibitorWatcher, &QDBusServiceWatcher::serviceUnregistered, + this, &ColorCorrectDBusInterface::removeInhibitorService); + + // Argh, all this code is just to send one innocent signal... + connect(m_manager, &Manager::inhibitedChanged, this, [this] { + QVariantMap changedProperties; + changedProperties.insert(QStringLiteral("inhibited"), m_manager->isInhibited()); + + QDBusMessage message = QDBusMessage::createSignal( + QStringLiteral("/ColorCorrect"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged") + ); + + message.setArguments({ + QStringLiteral("org.kde.kwin.ColorCorrect"), + changedProperties, + QStringList(), // invalidated_properties + }); + + QDBusConnection::sessionBus().send(message); + }); + connect(m_manager, &Manager::configChange, this, &ColorCorrectDBusInterface::nightColorConfigChanged); new ColorCorrectAdaptor(this); QDBusConnection::sessionBus().registerObject(QStringLiteral("/ColorCorrect"), this); } +bool ColorCorrectDBusInterface::isInhibited() const +{ + return m_manager->isInhibited(); +} + QHash ColorCorrectDBusInterface::nightColorInfo() { return m_manager->info(); } bool ColorCorrectDBusInterface::setNightColorConfig(QHash data) { return m_manager->changeConfiguration(data); } void ColorCorrectDBusInterface::nightColorAutoLocationUpdate(double latitude, double longitude) { m_manager->autoLocationUpdate(latitude, longitude); } +uint ColorCorrectDBusInterface::inhibit() +{ + const QString serviceName = QDBusContext::message().service(); + + if (m_inhibitors.values(serviceName).isEmpty()) { + m_inhibitorWatcher->addWatchedService(serviceName); + } + + m_inhibitors.insert(serviceName, ++m_lastInhibitionCookie); + + m_manager->inhibit(); + + return m_lastInhibitionCookie; +} + +void ColorCorrectDBusInterface::uninhibit(uint cookie) +{ + const QString serviceName = QDBusContext::message().service(); + + uninhibit(serviceName, cookie); +} + +void ColorCorrectDBusInterface::uninhibit(const QString &serviceName, uint cookie) +{ + const int removedCount = m_inhibitors.remove(serviceName, cookie); + if (!removedCount) { + return; + } + + if (m_inhibitors.values(serviceName).isEmpty()) { + m_inhibitorWatcher->removeWatchedService(serviceName); + } + + m_manager->uninhibit(); +} + +void ColorCorrectDBusInterface::removeInhibitorService(const QString &serviceName) +{ + const auto cookies = m_inhibitors.values(serviceName); + for (const uint &cookie : cookies) { + uninhibit(serviceName, cookie); + } +} + } } diff --git a/colorcorrection/colorcorrectdbusinterface.h b/colorcorrection/colorcorrectdbusinterface.h index 22570e86f..481f71fa7 100644 --- a/colorcorrection/colorcorrectdbusinterface.h +++ b/colorcorrection/colorcorrectdbusinterface.h @@ -1,126 +1,147 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright 2017 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_NIGHTCOLOR_DBUS_INTERFACE_H #define KWIN_NIGHTCOLOR_DBUS_INTERFACE_H #include #include namespace KWin { namespace ColorCorrect { class Manager; -class ColorCorrectDBusInterface : public QObject +class ColorCorrectDBusInterface : public QObject, public QDBusContext { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.ColorCorrect") + Q_PROPERTY(bool inhibited READ isInhibited) public: explicit ColorCorrectDBusInterface(Manager *parent); ~ColorCorrectDBusInterface() override = default; + bool isInhibited() const; + public Q_SLOTS: /** * @brief Gives information about the current state of Night Color. * * The returned variant hash has always the fields: * - ActiveEnabled * - Active * - Mode * - NightTemperatureEnabled * - NightTemperature * - Running * - CurrentColorTemperature * - LatitudeAuto * - LongitudeAuto * - LocationEnabled * - LatitudeFixed * - LongitudeFixed * - TimingsEnabled * - MorningBeginFixed * - EveningBeginFixed * - TransitionTime * * @return QHash * @see nightColorConfigChange * @see signalNightColorConfigChange * @since 5.12 */ QHash nightColorInfo(); /** * @brief Allows changing the Night Color configuration. * * The provided variant hash can have the following fields: * - Active * - Mode * - NightTemperature * - LatitudeAuto * - LongitudeAuto * - LatitudeFixed * - LongitudeFixed * - MorningBeginFixed * - EveningBeginFixed * - TransitionTime * * It returns true if the configuration change was successful, otherwise false. * A change request for the location or timings needs to provide all relevant fields at the same time * to be successful. Otherwise the whole change request will get ignored. A change request will be ignored * as a whole as well, if one of the provided information has been sent in a wrong format. * * @return bool * @see nightColorInfo * @see signalNightColorConfigChange * @since 5.12 */ bool setNightColorConfig(QHash data); /** * @brief For receiving auto location updates, primarily through the KDE Daemon * @return void * @since 5.12 */ void nightColorAutoLocationUpdate(double latitude, double longitude); + /** + * @brief Temporarily blocks Night Color. + * @since 5.18 + */ + uint inhibit(); + /** + * @brief Cancels the previous call to inhibit(). + * @since 5.18 + */ + void uninhibit(uint cookie); Q_SIGNALS: /** * @brief Emits that the Night Color configuration has been changed. * * The provided variant hash provides the same fields as nightColorInfo * * @return void * @see nightColorInfo * @see nightColorConfigChange * @since 5.12 */ void nightColorConfigChanged(QHash data); +private Q_SLOTS: + void removeInhibitorService(const QString &serviceName); + private: + void uninhibit(const QString &serviceName, uint cookie); + Manager *m_manager; + QDBusServiceWatcher *m_inhibitorWatcher; + QMultiHash m_inhibitors; + uint m_lastInhibitionCookie = 0; }; } } #endif // KWIN_NIGHTCOLOR_DBUS_INTERFACE_H diff --git a/colorcorrection/manager.cpp b/colorcorrection/manager.cpp index f3ba2e64f..502673019 100644 --- a/colorcorrection/manager.cpp +++ b/colorcorrection/manager.cpp @@ -1,852 +1,865 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright 2017 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "manager.h" #include "colorcorrectdbusinterface.h" #include "suncalc.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_LINUX #include #endif #include #include namespace KWin { namespace ColorCorrect { static const int QUICK_ADJUST_DURATION = 2000; static const int TEMPERATURE_STEP = 50; static bool checkLocation(double lat, double lng) { return -90 <= lat && lat <= 90 && -180 <= lng && lng <= 180; } Manager::Manager(QObject *parent) : QObject(parent) { m_iface = new ColorCorrectDBusInterface(this); connect(kwinApp(), &Application::workspaceCreated, this, &Manager::init); + + // Display a message when Night Color is (un)inhibited. + connect(this, &Manager::inhibitedChanged, this, [this] { + // TODO: Maybe use different icons? + const QString iconName = isInhibited() + ? QStringLiteral("preferences-desktop-display-nightcolor-off") + : QStringLiteral("preferences-desktop-display-nightcolor-on"); + + const QString text = isInhibited() + ? i18nc("Night Color was disabled", "Night Color Off") + : i18nc("Night Color was enabled", "Night Color On"); + + QDBusMessage message = QDBusMessage::createMethodCall( + QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/org/kde/osdService"), + QStringLiteral("org.kde.osdService"), + QStringLiteral("showText")); + message.setArguments({ iconName, text }); + + QDBusConnection::sessionBus().asyncCall(message); + }); } void Manager::init() { Settings::instance(kwinApp()->config()); // we may always read in the current config readConfig(); if (!kwinApp()->platform()->supportsGammaControl()) { return; } connect(Screens::self(), &Screens::countChanged, this, &Manager::hardReset); connect(LogindIntegration::self(), &LogindIntegration::sessionActiveChanged, this, [this](bool active) { if (active) { hardReset(); } else { cancelAllTimers(); } } ); #ifdef Q_OS_LINUX // monitor for system clock changes - from the time dataengine auto timeChangedFd = ::timerfd_create(CLOCK_REALTIME, O_CLOEXEC | O_NONBLOCK); ::itimerspec timespec; //set all timers to 0, which creates a timer that won't do anything ::memset(×pec, 0, sizeof(timespec)); // Monitor for the time changing (flags == TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET). // However these are not exposed in glibc so value is hardcoded: ::timerfd_settime(timeChangedFd, 3, ×pec, nullptr); connect(this, &QObject::destroyed, [timeChangedFd]() { ::close(timeChangedFd); }); auto notifier = new QSocketNotifier(timeChangedFd, QSocketNotifier::Read, this); connect(notifier, &QSocketNotifier::activated, this, [this](int fd) { uint64_t c; ::read(fd, &c, 8); // check if we're resuming from suspend - in this case do a hard reset // Note: We're using the time clock to detect a suspend phase instead of connecting to the // provided logind dbus signal, because this signal would be received way too late. QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.DBus.Properties", QStringLiteral("Get")); message.setArguments(QVariantList({"org.freedesktop.login1.Manager", QStringLiteral("PreparingForSleep")})); QDBusReply reply = QDBusConnection::systemBus().call(message); bool comingFromSuspend; if (reply.isValid()) { comingFromSuspend = reply.value().toBool(); } else { qCDebug(KWIN_COLORCORRECTION) << "Failed to get PreparingForSleep Property of logind session:" << reply.error().message(); // Always do a hard reset in case we have no further information. comingFromSuspend = true; } if (comingFromSuspend) { hardReset(); } else { resetAllTimers(); } }); #else // TODO: Alternative method for BSD. #endif hardReset(); } void Manager::hardReset() { cancelAllTimers(); // Timings of the Sun are not used in the constant mode. if (m_mode != NightColorMode::Constant) { updateSunTimings(true); } - if (kwinApp()->platform()->supportsGammaControl() && m_active) { + if (kwinApp()->platform()->supportsGammaControl() && m_active && !isInhibited()) { m_running = true; commitGammaRamps(currentTargetTemp()); } resetAllTimers(); } void Manager::reparseConfigAndReset() { cancelAllTimers(); readConfig(); hardReset(); } -// FIXME: The internal OSD service doesn't work on X11 right now. Once the QPA -// is ported away from Wayland, drop this function in favor of the internal -// OSD service. -static void showStatusOsd(bool enabled) +void Manager::toggle() { - // TODO: Maybe use different icons? - const QString iconName = enabled - ? QStringLiteral("preferences-desktop-display-nightcolor-on") - : QStringLiteral("preferences-desktop-display-nightcolor-off"); - - const QString text = enabled - ? i18nc("Night Color was enabled", "Night Color On") - : i18nc("Night Color was disabled", "Night Color Off"); - - QDBusMessage message = QDBusMessage::createMethodCall( - QStringLiteral("org.kde.plasmashell"), - QStringLiteral("/org/kde/osdService"), - QStringLiteral("org.kde.osdService"), - QStringLiteral("showText")); - message.setArguments({ iconName, text }); - - QDBusConnection::sessionBus().asyncCall(message); + m_isGloballyInhibited = !m_isGloballyInhibited; + m_isGloballyInhibited ? inhibit() : uninhibit(); } -void Manager::toggle() +bool Manager::isInhibited() const { - if (!kwinApp()->platform()->supportsGammaControl()) { - return; - } + return m_inhibitReferenceCount; +} + +void Manager::inhibit() +{ + m_inhibitReferenceCount++; - m_active = !m_active; + if (m_inhibitReferenceCount == 1) { + resetAllTimers(); + emit inhibitedChanged(); + } +} - showStatusOsd(m_active); +void Manager::uninhibit() +{ + m_inhibitReferenceCount--; - resetAllTimers(); + if (!m_inhibitReferenceCount) { + resetAllTimers(); + emit inhibitedChanged(); + } } void Manager::initShortcuts() { QAction *toggleAction = new QAction(this); toggleAction->setProperty("componentName", QStringLiteral(KWIN_NAME)); toggleAction->setObjectName(i18n("Toggle Night Color")); toggleAction->setText(i18n("Toggle Night Color")); KGlobalAccel::setGlobalShortcut(toggleAction, QList()); input()->registerShortcut(QKeySequence(), toggleAction, this, &Manager::toggle); } void Manager::readConfig() { Settings *s = Settings::self(); s->load(); m_active = s->active(); const NightColorMode mode = s->mode(); switch (s->mode()) { case NightColorMode::Automatic: case NightColorMode::Location: case NightColorMode::Timings: case NightColorMode::Constant: m_mode = mode; break; default: // Fallback for invalid setting values. m_mode = NightColorMode::Automatic; break; } m_nightTargetTemp = qBound(MIN_TEMPERATURE, s->nightTemperature(), NEUTRAL_TEMPERATURE); double lat, lng; auto correctReadin = [&lat, &lng]() { if (!checkLocation(lat, lng)) { // out of domain lat = 0; lng = 0; } }; // automatic lat = s->latitudeAuto(); lng = s->longitudeAuto(); correctReadin(); m_latAuto = lat; m_lngAuto = lng; // fixed location lat = s->latitudeFixed(); lng = s->longitudeFixed(); correctReadin(); m_latFixed = lat; m_lngFixed = lng; // fixed timings QTime mrB = QTime::fromString(s->morningBeginFixed(), "hhmm"); QTime evB = QTime::fromString(s->eveningBeginFixed(), "hhmm"); int diffME = mrB.msecsTo(evB); if (diffME <= 0) { // morning not strictly before evening - use defaults mrB = QTime(6,0); evB = QTime(18,0); diffME = mrB.msecsTo(evB); } int diffMin = qMin(diffME, MSC_DAY - diffME); int trTime = s->transitionTime() * 1000 * 60; if (trTime < 0 || diffMin <= trTime) { // transition time too long - use defaults mrB = QTime(6,0); evB = QTime(18,0); trTime = FALLBACK_SLOW_UPDATE_TIME; } m_morning = mrB; m_evening = evB; m_trTime = qMax(trTime / 1000 / 60, 1); } void Manager::resetAllTimers() { cancelAllTimers(); if (kwinApp()->platform()->supportsGammaControl()) { - if (m_active) { - m_running = true; - } + m_running = m_active && !isInhibited(); // we do this also for active being false in order to reset the temperature back to the day value resetQuickAdjustTimer(); } else { m_running = false; } } void Manager::cancelAllTimers() { delete m_slowUpdateStartTimer; delete m_slowUpdateTimer; delete m_quickAdjustTimer; m_slowUpdateStartTimer = nullptr; m_slowUpdateTimer = nullptr; m_quickAdjustTimer = nullptr; } void Manager::resetQuickAdjustTimer() { // We don't use timings of the Sun in the constant mode. if (m_mode != NightColorMode::Constant) { updateSunTimings(false); } int tempDiff = qAbs(currentTargetTemp() - m_currentTemp); // allow tolerance of one TEMPERATURE_STEP to compensate if a slow update is coincidental if (tempDiff > TEMPERATURE_STEP) { cancelAllTimers(); m_quickAdjustTimer = new QTimer(this); m_quickAdjustTimer->setSingleShot(false); connect(m_quickAdjustTimer, &QTimer::timeout, this, &Manager::quickAdjust); int interval = QUICK_ADJUST_DURATION / (tempDiff / TEMPERATURE_STEP); if (interval == 0) { interval = 1; } m_quickAdjustTimer->start(interval); } else { resetSlowUpdateStartTimer(); } } void Manager::quickAdjust() { if (!m_quickAdjustTimer) { return; } int nextTemp; const int targetTemp = currentTargetTemp(); if (m_currentTemp < targetTemp) { nextTemp = qMin(m_currentTemp + TEMPERATURE_STEP, targetTemp); } else { nextTemp = qMax(m_currentTemp - TEMPERATURE_STEP, targetTemp); } commitGammaRamps(nextTemp); if (nextTemp == targetTemp) { // stop timer, we reached the target temp delete m_quickAdjustTimer; m_quickAdjustTimer = nullptr; resetSlowUpdateStartTimer(); } } void Manager::resetSlowUpdateStartTimer() { delete m_slowUpdateStartTimer; m_slowUpdateStartTimer = nullptr; if (!m_running || m_quickAdjustTimer) { // only reenable the slow update start timer when quick adjust is not active anymore return; } // There is no need for starting the slow update timer. Screen color temperature // will be constant all the time now. if (m_mode == NightColorMode::Constant) { return; } // set up the next slow update m_slowUpdateStartTimer = new QTimer(this); m_slowUpdateStartTimer->setSingleShot(true); connect(m_slowUpdateStartTimer, &QTimer::timeout, this, &Manager::resetSlowUpdateStartTimer); updateSunTimings(false); const int diff = QDateTime::currentDateTime().msecsTo(m_next.first); if (diff <= 0) { qCCritical(KWIN_COLORCORRECTION) << "Error in time calculation. Deactivating Night Color."; return; } m_slowUpdateStartTimer->start(diff); // start the current slow update resetSlowUpdateTimer(); } void Manager::resetSlowUpdateTimer() { delete m_slowUpdateTimer; m_slowUpdateTimer = nullptr; const QDateTime now = QDateTime::currentDateTime(); const bool isDay = daylight(); const int targetTemp = isDay ? m_dayTargetTemp : m_nightTargetTemp; // We've reached the target color temperature or the transition time is zero. if (m_prev.first == m_prev.second || m_currentTemp == targetTemp) { commitGammaRamps(targetTemp); return; } if (m_prev.first <= now && now <= m_prev.second) { int availTime = now.msecsTo(m_prev.second); m_slowUpdateTimer = new QTimer(this); m_slowUpdateTimer->setSingleShot(false); if (isDay) { connect(m_slowUpdateTimer, &QTimer::timeout, this, [this]() {slowUpdate(m_dayTargetTemp);}); } else { connect(m_slowUpdateTimer, &QTimer::timeout, this, [this]() {slowUpdate(m_nightTargetTemp);}); } // calculate interval such as temperature is changed by TEMPERATURE_STEP K per timer timeout int interval = availTime * TEMPERATURE_STEP / qAbs(targetTemp - m_currentTemp); if (interval == 0) { interval = 1; } m_slowUpdateTimer->start(interval); } } void Manager::slowUpdate(int targetTemp) { if (!m_slowUpdateTimer) { return; } int nextTemp; if (m_currentTemp < targetTemp) { nextTemp = qMin(m_currentTemp + TEMPERATURE_STEP, targetTemp); } else { nextTemp = qMax(m_currentTemp - TEMPERATURE_STEP, targetTemp); } commitGammaRamps(nextTemp); if (nextTemp == targetTemp) { // stop timer, we reached the target temp delete m_slowUpdateTimer; m_slowUpdateTimer = nullptr; } } void Manager::updateSunTimings(bool force) { const QDateTime todayNow = QDateTime::currentDateTime(); if (m_mode == NightColorMode::Timings) { const QDateTime morB = QDateTime(todayNow.date(), m_morning); const QDateTime morE = morB.addSecs(m_trTime * 60); const QDateTime eveB = QDateTime(todayNow.date(), m_evening); const QDateTime eveE = eveB.addSecs(m_trTime * 60); if (morB <= todayNow && todayNow < eveB) { m_next = DateTimes(eveB, eveE); m_prev = DateTimes(morB, morE); } else if (todayNow < morB) { m_next = DateTimes(morB, morE); m_prev = DateTimes(eveB.addDays(-1), eveE.addDays(-1)); } else { m_next = DateTimes(morB.addDays(1), morE.addDays(1)); m_prev = DateTimes(eveB, eveE); } return; } double lat, lng; if (m_mode == NightColorMode::Automatic) { lat = m_latAuto; lng = m_lngAuto; } else { lat = m_latFixed; lng = m_lngFixed; } if (!force) { // first try by only switching the timings if (daylight()) { // next is morning m_prev = m_next; m_next = getSunTimings(todayNow.addDays(1), lat, lng, true); } else { // next is evening m_prev = m_next; m_next = getSunTimings(todayNow, lat, lng, false); } } if (force || !checkAutomaticSunTimings()) { // in case this fails, reset them DateTimes morning = getSunTimings(todayNow, lat, lng, true); if (todayNow < morning.first) { m_prev = getSunTimings(todayNow.addDays(-1), lat, lng, false); m_next = morning; } else { DateTimes evening = getSunTimings(todayNow, lat, lng, false); if (todayNow < evening.first) { m_prev = morning; m_next = evening; } else { m_prev = evening; m_next = getSunTimings(todayNow.addDays(1), lat, lng, true); } } } } DateTimes Manager::getSunTimings(const QDateTime &dateTime, double latitude, double longitude, bool morning) const { DateTimes dateTimes = calculateSunTimings(dateTime, latitude, longitude, morning); // At locations near the poles it is possible, that we can't // calculate some or all sun timings (midnight sun). // In this case try to fallback to sensible default values. const bool beginDefined = !dateTimes.first.isNull(); const bool endDefined = !dateTimes.second.isNull(); if (!beginDefined || !endDefined) { if (beginDefined) { dateTimes.second = dateTimes.first.addMSecs( FALLBACK_SLOW_UPDATE_TIME ); } else if (endDefined) { dateTimes.first = dateTimes.second.addMSecs( - FALLBACK_SLOW_UPDATE_TIME ); } else { // Just use default values for morning and evening, but the user // will probably deactivate Night Color anyway if he is living // in a region without clear sun rise and set. const QTime referenceTime = morning ? QTime(6, 0) : QTime(18, 0); dateTimes.first = QDateTime(dateTime.date(), referenceTime); dateTimes.second = dateTimes.first.addMSecs( FALLBACK_SLOW_UPDATE_TIME ); } } return dateTimes; } bool Manager::checkAutomaticSunTimings() const { if (m_prev.first.isValid() && m_prev.second.isValid() && m_next.first.isValid() && m_next.second.isValid()) { const QDateTime todayNow = QDateTime::currentDateTime(); return m_prev.first <= todayNow && todayNow < m_next.first && m_prev.first.msecsTo(m_next.first) < MSC_DAY * 23./24; } return false; } bool Manager::daylight() const { return m_prev.first.date() == m_next.first.date(); } int Manager::currentTargetTemp() const { - if (!m_active) { + if (!m_running) { return NEUTRAL_TEMPERATURE; } if (m_mode == NightColorMode::Constant) { return m_nightTargetTemp; } const QDateTime todayNow = QDateTime::currentDateTime(); auto f = [this, todayNow](int target1, int target2) { if (todayNow <= m_prev.second) { double residueQuota = todayNow.msecsTo(m_prev.second) / (double)m_prev.first.msecsTo(m_prev.second); double ret = (int)((1. - residueQuota) * (double)target2 + residueQuota * (double)target1); // remove single digits ret = ((int)(0.1 * ret)) * 10; return (int)ret; } else { return target2; } }; if (daylight()) { return f(m_nightTargetTemp, m_dayTargetTemp); } else { return f(m_dayTargetTemp, m_nightTargetTemp); } } void Manager::commitGammaRamps(int temperature) { const auto outs = kwinApp()->platform()->outputs(); for (auto *o : outs) { int rampsize = o->gammaRampSize(); GammaRamp ramp(rampsize); /* * The gamma calculation below is based on the Redshift app: * https://github.com/jonls/redshift */ uint16_t *red = ramp.red(); uint16_t *green = ramp.green(); uint16_t *blue = ramp.blue(); // linear default state for (int i = 0; i < rampsize; i++) { uint16_t value = (double)i / rampsize * (UINT16_MAX + 1); red[i] = value; green[i] = value; blue[i] = value; } // approximate white point float whitePoint[3]; float alpha = (temperature % 100) / 100.; int bbCIndex = ((temperature - 1000) / 100) * 3; whitePoint[0] = (1. - alpha) * blackbodyColor[bbCIndex] + alpha * blackbodyColor[bbCIndex + 3]; whitePoint[1] = (1. - alpha) * blackbodyColor[bbCIndex + 1] + alpha * blackbodyColor[bbCIndex + 4]; whitePoint[2] = (1. - alpha) * blackbodyColor[bbCIndex + 2] + alpha * blackbodyColor[bbCIndex + 5]; for (int i = 0; i < rampsize; i++) { red[i] = qreal(red[i]) / (UINT16_MAX+1) * whitePoint[0] * (UINT16_MAX+1); green[i] = qreal(green[i]) / (UINT16_MAX+1) * whitePoint[1] * (UINT16_MAX+1); blue[i] = qreal(blue[i]) / (UINT16_MAX+1) * whitePoint[2] * (UINT16_MAX+1); } if (o->setGammaRamp(ramp)) { m_currentTemp = temperature; m_failedCommitAttempts = 0; } else { m_failedCommitAttempts++; if (m_failedCommitAttempts < 10) { qCWarning(KWIN_COLORCORRECTION).nospace() << "Committing Gamma Ramp failed for output " << o->name() << ". Trying " << (10 - m_failedCommitAttempts) << " times more."; } else { // TODO: On multi monitor setups we could try to rollback earlier changes for already committed outputs qCWarning(KWIN_COLORCORRECTION) << "Gamma Ramp commit failed too often. Deactivating color correction for now."; m_failedCommitAttempts = 0; // reset so we can try again later (i.e. after suspend phase or config change) m_running = false; cancelAllTimers(); } } } } QHash Manager::info() const { return QHash { { QStringLiteral("Available"), kwinApp()->platform()->supportsGammaControl() }, { QStringLiteral("ActiveEnabled"), true}, { QStringLiteral("Active"), m_active}, { QStringLiteral("ModeEnabled"), true}, { QStringLiteral("Mode"), (int)m_mode}, { QStringLiteral("NightTemperatureEnabled"), true}, { QStringLiteral("NightTemperature"), m_nightTargetTemp}, { QStringLiteral("Running"), m_running}, { QStringLiteral("CurrentColorTemperature"), m_currentTemp}, { QStringLiteral("LatitudeAuto"), m_latAuto}, { QStringLiteral("LongitudeAuto"), m_lngAuto}, { QStringLiteral("LocationEnabled"), true}, { QStringLiteral("LatitudeFixed"), m_latFixed}, { QStringLiteral("LongitudeFixed"), m_lngFixed}, { QStringLiteral("TimingsEnabled"), true}, { QStringLiteral("MorningBeginFixed"), m_morning.toString(Qt::ISODate)}, { QStringLiteral("EveningBeginFixed"), m_evening.toString(Qt::ISODate)}, { QStringLiteral("TransitionTime"), m_trTime}, }; } bool Manager::changeConfiguration(QHash data) { bool activeUpdate, modeUpdate, tempUpdate, locUpdate, timeUpdate; activeUpdate = modeUpdate = tempUpdate = locUpdate = timeUpdate = false; bool active = m_active; NightColorMode mode = m_mode; int nightT = m_nightTargetTemp; double lat = m_latFixed; double lng = m_lngFixed; QTime mor = m_morning; QTime eve = m_evening; int trT = m_trTime; QHash::const_iterator iter1, iter2, iter3; iter1 = data.constFind("Active"); if (iter1 != data.constEnd()) { if (!iter1.value().canConvert()) { return false; } bool act = iter1.value().toBool(); activeUpdate = m_active != act; active = act; } iter1 = data.constFind("Mode"); if (iter1 != data.constEnd()) { if (!iter1.value().canConvert()) { return false; } int mo = iter1.value().toInt(); if (mo < 0 || 3 < mo) { return false; } NightColorMode moM; switch (mo) { case 0: moM = NightColorMode::Automatic; break; case 1: moM = NightColorMode::Location; break; case 2: moM = NightColorMode::Timings; break; case 3: moM = NightColorMode::Constant; break; } modeUpdate = m_mode != moM; mode = moM; } iter1 = data.constFind("NightTemperature"); if (iter1 != data.constEnd()) { if (!iter1.value().canConvert()) { return false; } int nT = iter1.value().toInt(); if (nT < MIN_TEMPERATURE || NEUTRAL_TEMPERATURE < nT) { return false; } tempUpdate = m_nightTargetTemp != nT; nightT = nT; } iter1 = data.constFind("LatitudeFixed"); iter2 = data.constFind("LongitudeFixed"); if (iter1 != data.constEnd() && iter2 != data.constEnd()) { if (!iter1.value().canConvert() || !iter2.value().canConvert()) { return false; } double la = iter1.value().toDouble(); double ln = iter2.value().toDouble(); if (!checkLocation(la, ln)) { return false; } locUpdate = m_latFixed != la || m_lngFixed != ln; lat = la; lng = ln; } iter1 = data.constFind("MorningBeginFixed"); iter2 = data.constFind("EveningBeginFixed"); iter3 = data.constFind("TransitionTime"); if (iter1 != data.constEnd() && iter2 != data.constEnd() && iter3 != data.constEnd()) { if (!iter1.value().canConvert() || !iter2.value().canConvert() || !iter3.value().canConvert()) { return false; } QTime mo = QTime::fromString(iter1.value().toString(), Qt::ISODate); QTime ev = QTime::fromString(iter2.value().toString(), Qt::ISODate); if (!mo.isValid() || !ev.isValid()) { return false; } int tT = iter3.value().toInt(); int diffME = mo.msecsTo(ev); if (diffME <= 0 || qMin(diffME, MSC_DAY - diffME) <= tT * 60 * 1000 || tT < 1) { // morning not strictly before evening, transition time too long or transition time out of bounds return false; } timeUpdate = m_morning != mo || m_evening != ev || m_trTime != tT; mor = mo; eve = ev; trT = tT; } if (!(activeUpdate || modeUpdate || tempUpdate || locUpdate || timeUpdate)) { return true; } bool resetNeeded = activeUpdate || modeUpdate || tempUpdate || (locUpdate && mode == NightColorMode::Location) || (timeUpdate && mode == NightColorMode::Timings); if (resetNeeded) { cancelAllTimers(); } Settings *s = Settings::self(); if (activeUpdate) { m_active = active; s->setActive(active); } if (modeUpdate) { m_mode = mode; s->setMode(mode); } if (tempUpdate) { m_nightTargetTemp = nightT; s->setNightTemperature(nightT); } if (locUpdate) { m_latFixed = lat; m_lngFixed = lng; s->setLatitudeFixed(lat); s->setLongitudeFixed(lng); } if (timeUpdate) { m_morning = mor; m_evening = eve; m_trTime = trT; s->setMorningBeginFixed(mor.toString("hhmm")); s->setEveningBeginFixed(eve.toString("hhmm")); s->setTransitionTime(trT); } s->save(); if (resetNeeded) { resetAllTimers(); } emit configChange(info()); return true; } void Manager::autoLocationUpdate(double latitude, double longitude) { qCDebug(KWIN_COLORCORRECTION, "Received new location (lat: %f, lng: %f)", latitude, longitude); if (!checkLocation(latitude, longitude)) { return; } // we tolerate small deviations with minimal impact on sun timings if (qAbs(m_latAuto - latitude) < 2 && qAbs(m_lngAuto - longitude) < 1) { return; } cancelAllTimers(); m_latAuto = latitude; m_lngAuto = longitude; Settings *s = Settings::self(); s->setLatitudeAuto(latitude); s->setLongitudeAuto(longitude); s->save(); resetAllTimers(); emit configChange(info()); } } } diff --git a/colorcorrection/manager.h b/colorcorrection/manager.h index 0f4e20248..cad325acd 100644 --- a/colorcorrection/manager.h +++ b/colorcorrection/manager.h @@ -1,203 +1,233 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright 2017 Roman Gilg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_COLORCORRECT_MANAGER_H #define KWIN_COLORCORRECT_MANAGER_H #include "constants.h" #include #include #include #include class QTimer; namespace KWin { class Workspace; namespace ColorCorrect { typedef QPair DateTimes; typedef QPair Times; class ColorCorrectDBusInterface; /** * This enum type is used to specify operation mode of the night color manager. */ enum NightColorMode { /** * Color temperature is computed based on the current position of the Sun. * * Location of the user is provided by Plasma. */ Automatic, /** * Color temperature is computed based on the current position of the Sun. * * Location of the user is provided by themselves. */ Location, /** * Color temperature is computed based on the current time. * * Sunrise and sunset times have to be specified by the user. */ Timings, /** * Color temperature is constant thoughout the day. */ Constant, }; /** * The night color manager is a blue light filter similar to Redshift. * * There are four modes this manager can operate in: Automatic, Location, Timings, * and Constant. Both Automatic and Location modes derive screen color temperature * from the current position of the Sun, the only difference between two is how * coordinates of the user are specified. If the user is located near the North or * South pole, we can't compute correct position of the Sun, that's why we need * Timings and Constant mode. * * With the Timings mode, screen color temperature is computed based on the clock * time. The user needs to specify timings of the sunset and sunrise as well the * transition time. * * With the Constant mode, screen color temperature is always constant. */ class KWIN_EXPORT Manager : public QObject { Q_OBJECT public: Manager(QObject *parent); void init(); /** * Get current configuration * @see changeConfiguration * @since 5.12 */ QHash info() const; /** * Change configuration * @see info * @since 5.12 */ bool changeConfiguration(QHash data); void autoLocationUpdate(double latitude, double longitude); /** * Toggles the active state of the filter. * * A quick transition will be started if the difference between current screen * color temperature and target screen color temperature is too large. Target * temperature is defined in context of the new active state. * * If the filter becomes inactive after calling this method, the target color * temperature is 6500 K. * * If the filter becomes active after calling this method, the target screen * color temperature is defined by the current operation mode. * * Note that this method is a no-op if the underlying platform doesn't support * adjusting gamma ramps. */ void toggle(); + /** + * Returns @c true if the night color manager is blocked; otherwise @c false. + */ + bool isInhibited() const; + + /** + * Temporarily blocks the night color manager. + * + * After calling this method, the screen color temperature will be reverted + * back to 6500C. When you're done, call uninhibit() method. + */ + void inhibit(); + + /** + * Attempts to unblock the night color manager. + */ + void uninhibit(); + // for auto tests void reparseConfigAndReset(); public Q_SLOTS: void resetSlowUpdateStartTimer(); void quickAdjust(); Q_SIGNALS: void configChange(QHash data); + /** + * Emitted whenever the night color manager is blocked or unblocked. + */ + void inhibitedChanged(); + private: void initShortcuts(); void readConfig(); void hardReset(); void slowUpdate(int targetTemp); void resetAllTimers(); int currentTargetTemp() const; void cancelAllTimers(); /** * Quick shift on manual change to current target Temperature */ void resetQuickAdjustTimer(); /** * Slow shift to daytime target Temperature */ void resetSlowUpdateTimer(); void updateSunTimings(bool force); DateTimes getSunTimings(const QDateTime &dateTime, double latitude, double longitude, bool morning) const; bool checkAutomaticSunTimings() const; bool daylight() const; void commitGammaRamps(int temperature); ColorCorrectDBusInterface *m_iface; + // Specifies whether Night Color is enabled. bool m_active; + + // Specifies whether Night Color is currently running. bool m_running = false; + // Specifies whether Night Color is inhibited globally. + bool m_isGloballyInhibited = false; + NightColorMode m_mode = NightColorMode::Automatic; // the previous and next sunrise/sunset intervals - in UTC time DateTimes m_prev = DateTimes(); DateTimes m_next = DateTimes(); // manual times from config QTime m_morning = QTime(6,0); QTime m_evening = QTime(18,0); int m_trTime = 30; // saved in minutes > 1 // auto location provided by work space double m_latAuto; double m_lngAuto; // manual location from config double m_latFixed; double m_lngFixed; QTimer *m_slowUpdateStartTimer = nullptr; QTimer *m_slowUpdateTimer = nullptr; QTimer *m_quickAdjustTimer = nullptr; int m_currentTemp = NEUTRAL_TEMPERATURE; int m_dayTargetTemp = NEUTRAL_TEMPERATURE; int m_nightTargetTemp = DEFAULT_NIGHT_TEMPERATURE; int m_failedCommitAttempts = 0; + int m_inhibitReferenceCount = 0; // The Workspace class needs to call initShortcuts during initialization. friend class KWin::Workspace; }; } } #endif // KWIN_COLORCORRECT_MANAGER_H diff --git a/org.kde.kwin.ColorCorrect.xml b/org.kde.kwin.ColorCorrect.xml index 0b83a0129..dd5b298a8 100644 --- a/org.kde.kwin.ColorCorrect.xml +++ b/org.kde.kwin.ColorCorrect.xml @@ -1,22 +1,56 @@ + + + + + + + + + + + + +