diff --git a/colorcorrection/manager.cpp b/colorcorrection/manager.cpp index b1ae69476..70580500d 100644 --- a/colorcorrection/manager.cpp +++ b/colorcorrection/manager.cpp @@ -1,858 +1,858 @@ /******************************************************************** 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); } 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, 0); 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) { 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) { // 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); } void Manager::toggle() { if (!kwinApp()->platform()->supportsGammaControl()) { return; } m_active = !m_active; showStatusOsd(m_active); resetAllTimers(); } 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(); 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; } // 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; 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); int diff; if (m_mode == NightColorMode::Timings) { // Timings mode is in local time diff = QDateTime::currentDateTime().msecsTo(m_next.first); } else { diff = QDateTime::currentDateTimeUtc().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; QDateTime now = QDateTime::currentDateTimeUtc(); bool isDay = daylight(); int targetTemp = isDay ? m_dayTargetTemp : m_nightTargetTemp; - if (m_prev.first == m_prev.second) { - // transition time is zero - commitGammaRamps(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 / (qAbs(targetTemp - m_currentTemp) / TEMPERATURE_STEP); 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) { QDateTime todayNow = QDateTime::currentDateTimeUtc(); if (m_mode == NightColorMode::Timings) { QDateTime todayNowLocal = QDateTime::currentDateTime(); QDateTime morB = QDateTime(todayNowLocal.date(), m_morning); QDateTime morE = morB.addSecs(m_trTime * 60); QDateTime eveB = QDateTime(todayNowLocal.date(), m_evening); QDateTime eveE = eveB.addSecs(m_trTime * 60); if (morB <= todayNowLocal && todayNowLocal < eveB) { m_next = DateTimes(eveB, eveE); m_prev = DateTimes(morB, morE); } else if (todayNowLocal < 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.date().addDays(1), lat, lng, true); } else { // next is evening m_prev = m_next; m_next = getSunTimings(todayNow.date(), lat, lng, false); } } if (force || !checkAutomaticSunTimings()) { // in case this fails, reset them DateTimes morning = getSunTimings(todayNow.date(), lat, lng, true); if (todayNow < morning.first) { m_prev = getSunTimings(todayNow.date().addDays(-1), lat, lng, false); m_next = morning; } else { DateTimes evening = getSunTimings(todayNow.date(), lat, lng, false); if (todayNow < evening.first) { m_prev = morning; m_next = evening; } else { m_prev = evening; m_next = getSunTimings(todayNow.date().addDays(1), lat, lng, true); } } } } DateTimes Manager::getSunTimings(QDate date, double latitude, double longitude, bool morning) const { Times times = calculateSunTimings(date, 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. bool beginDefined = !times.first.isNull(); bool endDefined = !times.second.isNull(); if (!beginDefined || !endDefined) { if (beginDefined) { times.second = times.first.addMSecs( FALLBACK_SLOW_UPDATE_TIME ); } else if (endDefined) { times.first = times.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. times.first = morning ? QTime(6,0,0) : QTime(18,0,0); times.second = times.first.addMSecs( FALLBACK_SLOW_UPDATE_TIME ); } } return DateTimes(QDateTime(date, times.first, Qt::UTC), QDateTime(date, times.second, Qt::UTC)); } bool Manager::checkAutomaticSunTimings() const { if (m_prev.first.isValid() && m_prev.second.isValid() && m_next.first.isValid() && m_next.second.isValid()) { QDateTime todayNow = QDateTime::currentDateTimeUtc(); 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) { return NEUTRAL_TEMPERATURE; } if (m_mode == NightColorMode::Constant) { return m_nightTargetTemp; } QDateTime todayNow = QDateTime::currentDateTimeUtc(); 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 commited 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) { 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()); } } }