KWin
Loading...
Searching...
No Matches
nightcolormanager.cpp
Go to the documentation of this file.
1/*
2 KWin - the KDE window manager
3 This file is part of the KDE project.
4
5 SPDX-FileCopyrightText: 2017 Roman Gilg <subdiff@gmail.com>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9#include "nightcolormanager.h"
10#include "clockskewnotifier.h"
11#include "colors/colordevice.h"
12#include "colors/colormanager.h"
14#include "nightcolorlogging.h"
15#include "nightcolorsettings.h"
16#include "suncalc.h"
17
18#include <core/outputbackend.h>
19#include <core/session.h>
20#include <input.h>
21#include <main.h>
22#include <workspace.h>
23
24#include <KGlobalAccel>
25#include <KLocalizedString>
26
27#include <QAction>
28#include <QDBusConnection>
29#include <QDBusMessage>
30#include <QDBusPendingReply>
31#include <QDBusReply>
32#include <QTimer>
33
34namespace KWin
35{
36
37static const int QUICK_ADJUST_DURATION = 2000;
38static const int TEMPERATURE_STEP = 50;
39static NightColorManager *s_instance = nullptr;
40
41static bool checkLocation(double lat, double lng)
42{
43 return -90 <= lat && lat <= 90 && -180 <= lng && lng <= 180;
44}
45
47{
48 return s_instance;
49}
50
52{
53 NightColorSettings::instance(kwinApp()->config());
54 s_instance = this;
55
56 m_iface = new NightColorDBusInterface(this);
57 m_skewNotifier = new ClockSkewNotifier(this);
58
59 // Display a message when Night Color is (un)inhibited.
60 connect(this, &NightColorManager::inhibitedChanged, this, [this] {
61 const QString iconName = isInhibited()
62 ? QStringLiteral("redshift-status-off")
63 : m_daylight && m_targetTemperature != DEFAULT_DAY_TEMPERATURE ? QStringLiteral("redshift-status-day")
64 : QStringLiteral("redshift-status-on");
65
66 const QString text = isInhibited()
67 ? i18nc("Night Light was disabled", "Night Light Off")
68 : i18nc("Night Light was enabled", "Night Light On");
69
70 QDBusMessage message = QDBusMessage::createMethodCall(
71 QStringLiteral("org.kde.plasmashell"),
72 QStringLiteral("/org/kde/osdService"),
73 QStringLiteral("org.kde.osdService"),
74 QStringLiteral("showText"));
75 message.setArguments({iconName, text});
76
77 QDBusConnection::sessionBus().asyncCall(message);
78 });
79
80 m_configWatcher = KConfigWatcher::create(kwinApp()->config());
81 connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, &NightColorManager::reconfigure);
82
83 // we may always read in the current config
84 readConfig();
85
86 QAction *toggleAction = new QAction(this);
87 toggleAction->setProperty("componentName", QStringLiteral("kwin"));
88 toggleAction->setObjectName(QStringLiteral("Toggle Night Color"));
89 toggleAction->setText(i18n("Toggle Night Light"));
90 KGlobalAccel::setGlobalShortcut(toggleAction, QList<QKeySequence>());
91 connect(toggleAction, &QAction::triggered, this, &NightColorManager::toggle);
92
93 connect(kwinApp()->colorManager(), &ColorManager::deviceAdded, this, &NightColorManager::hardReset);
94
95 connect(kwinApp()->session(), &Session::activeChanged, this, [this](bool active) {
96 if (active) {
97 hardReset();
98 } else {
99 cancelAllTimers();
100 }
101 });
102
103 connect(m_skewNotifier, &ClockSkewNotifier::clockSkewed, this, [this]() {
104 // check if we're resuming from suspend - in this case do a hard reset
105 // Note: We're using the time clock to detect a suspend phase instead of connecting to the
106 // provided logind dbus signal, because this signal would be received way too late.
107 QDBusMessage message = QDBusMessage::createMethodCall("org.freedesktop.login1",
108 "/org/freedesktop/login1",
109 "org.freedesktop.DBus.Properties",
110 QStringLiteral("Get"));
111 message.setArguments(QVariantList({"org.freedesktop.login1.Manager", QStringLiteral("PreparingForSleep")}));
112 QDBusReply<QVariant> reply = QDBusConnection::systemBus().call(message);
113 bool comingFromSuspend;
114 if (reply.isValid()) {
115 comingFromSuspend = reply.value().toBool();
116 } else {
117 qCDebug(KWIN_NIGHTCOLOR) << "Failed to get PreparingForSleep Property of logind session:" << reply.error().message();
118 // Always do a hard reset in case we have no further information.
119 comingFromSuspend = true;
120 }
121
122 if (comingFromSuspend) {
123 hardReset();
124 } else {
125 resetAllTimers();
126 }
127 });
128
129 hardReset();
130}
131
133{
134 s_instance = nullptr;
135}
136
137void NightColorManager::hardReset()
138{
139 cancelAllTimers();
140
141 updateTransitionTimings(true);
142 updateTargetTemperature();
143
144 if (isEnabled() && !isInhibited()) {
145 setRunning(true);
146 commitGammaRamps(currentTargetTemp());
147 }
148 resetAllTimers();
149}
150
152{
153 cancelAllTimers();
154 readConfig();
155 resetAllTimers();
156}
157
159{
160 m_isGloballyInhibited = !m_isGloballyInhibited;
161 m_isGloballyInhibited ? inhibit() : uninhibit();
162}
163
165{
166 return m_inhibitReferenceCount;
167}
168
170{
171 m_inhibitReferenceCount++;
172
173 if (m_inhibitReferenceCount == 1) {
174 resetAllTimers();
175 Q_EMIT inhibitedChanged();
176 }
177}
178
180{
181 m_inhibitReferenceCount--;
182
183 if (!m_inhibitReferenceCount) {
184 resetAllTimers();
185 Q_EMIT inhibitedChanged();
186 }
187}
188
190{
191 return m_active;
192}
193
195{
196 return m_running;
197}
198
200{
201 return m_currentTemp;
202}
203
205{
206 return m_targetTemperature;
207}
208
210{
211 return m_mode;
212}
213
215{
216 return m_prev.first;
217}
218
220{
221 return m_prev.first.msecsTo(m_prev.second);
222}
223
225{
226 return m_next.first;
227}
228
230{
231 return m_next.first.msecsTo(m_next.second);
232}
233
234void NightColorManager::readConfig()
235{
236 NightColorSettings *s = NightColorSettings::self();
237 s->load();
238
239 setEnabled(s->active());
240
241 const NightColorMode mode = s->mode();
242 switch (s->mode()) {
247 setMode(mode);
248 break;
249 default:
250 // Fallback for invalid setting values.
252 break;
253 }
254
255 m_dayTargetTemp = std::clamp(s->dayTemperature(), MIN_TEMPERATURE, DEFAULT_DAY_TEMPERATURE);
256 m_nightTargetTemp = std::clamp(s->nightTemperature(), MIN_TEMPERATURE, DEFAULT_DAY_TEMPERATURE);
257
258 double lat, lng;
259 auto correctReadin = [&lat, &lng]() {
260 if (!checkLocation(lat, lng)) {
261 // out of domain
262 lat = 0;
263 lng = 0;
264 }
265 };
266 // automatic
267 lat = s->latitudeAuto();
268 lng = s->longitudeAuto();
269 correctReadin();
270 m_latAuto = lat;
271 m_lngAuto = lng;
272 // fixed location
273 lat = s->latitudeFixed();
274 lng = s->longitudeFixed();
275 correctReadin();
276 m_latFixed = lat;
277 m_lngFixed = lng;
278
279 // fixed timings
280 QTime mrB = QTime::fromString(s->morningBeginFixed(), "hhmm");
281 QTime evB = QTime::fromString(s->eveningBeginFixed(), "hhmm");
282
283 int diffME = evB > mrB ? mrB.msecsTo(evB) : evB.msecsTo(mrB);
284 int diffMin = std::min(diffME, MSC_DAY - diffME);
285
286 int trTime = s->transitionTime() * 1000 * 60;
287 if (trTime < 0 || diffMin <= trTime) {
288 // transition time too long - use defaults
289 mrB = QTime(6, 0);
290 evB = QTime(18, 0);
291 trTime = FALLBACK_SLOW_UPDATE_TIME;
292 }
293 m_morning = mrB;
294 m_evening = evB;
295 m_trTime = std::max(trTime / 1000 / 60, 1);
296}
297
298void NightColorManager::resetAllTimers()
299{
300 cancelAllTimers();
301 setRunning(isEnabled() && !isInhibited());
302 // we do this also for active being false in order to reset the temperature back to the day value
303 updateTransitionTimings(false);
304 updateTargetTemperature();
305 resetQuickAdjustTimer(currentTargetTemp());
306}
307
308void NightColorManager::cancelAllTimers()
309{
310 m_slowUpdateStartTimer.reset();
311 m_slowUpdateTimer.reset();
312 m_quickAdjustTimer.reset();
313}
314
315void NightColorManager::resetQuickAdjustTimer(int targetTemp)
316{
317 int tempDiff = std::abs(targetTemp - m_currentTemp);
318 // allow tolerance of one TEMPERATURE_STEP to compensate if a slow update is coincidental
319 if (tempDiff > TEMPERATURE_STEP) {
320 cancelAllTimers();
321 m_quickAdjustTimer = std::make_unique<QTimer>();
322 m_quickAdjustTimer->setSingleShot(false);
323 connect(m_quickAdjustTimer.get(), &QTimer::timeout, this, [this, targetTemp]() {
324 quickAdjust(targetTemp);
325 });
326
327 int interval = (QUICK_ADJUST_DURATION / (m_previewTimer && m_previewTimer->isActive() ? 8 : 1)) / (tempDiff / TEMPERATURE_STEP);
328 if (interval == 0) {
329 interval = 1;
330 }
331 m_quickAdjustTimer->start(interval);
332 } else {
334 }
335}
336
338{
339 if (!m_quickAdjustTimer) {
340 return;
341 }
342
343 int nextTemp;
344
345 if (m_currentTemp < targetTemp) {
346 nextTemp = std::min(m_currentTemp + TEMPERATURE_STEP, targetTemp);
347 } else {
348 nextTemp = std::max(m_currentTemp - TEMPERATURE_STEP, targetTemp);
349 }
350 commitGammaRamps(nextTemp);
351
352 if (nextTemp == targetTemp) {
353 // stop timer, we reached the target temp
354 m_quickAdjustTimer.reset();
356 }
357}
358
360{
361 m_slowUpdateStartTimer.reset();
362
363 if (!m_running || m_quickAdjustTimer) {
364 // only reenable the slow update start timer when quick adjust is not active anymore
365 return;
366 }
367
368 // There is no need for starting the slow update timer. Screen color temperature
369 // will be constant all the time now.
370 if (m_mode == NightColorMode::Constant) {
371 return;
372 }
373
374 // set up the next slow update
375 m_slowUpdateStartTimer = std::make_unique<QTimer>();
376 m_slowUpdateStartTimer->setSingleShot(true);
377 connect(m_slowUpdateStartTimer.get(), &QTimer::timeout, this, &NightColorManager::resetSlowUpdateStartTimer);
378
379 updateTransitionTimings(false);
380 updateTargetTemperature();
381
382 const int diff = QDateTime::currentDateTime().msecsTo(m_next.first);
383 if (diff <= 0) {
384 qCCritical(KWIN_NIGHTCOLOR) << "Error in time calculation. Deactivating Night Color.";
385 return;
386 }
387 m_slowUpdateStartTimer->start(diff);
388
389 // start the current slow update
390 resetSlowUpdateTimer();
391}
392
393void NightColorManager::resetSlowUpdateTimer()
394{
395 m_slowUpdateTimer.reset();
396
397 const QDateTime now = QDateTime::currentDateTime();
398 const bool isDay = daylight();
399 const int targetTemp = isDay ? m_dayTargetTemp : m_nightTargetTemp;
400
401 // We've reached the target color temperature or the transition time is zero.
402 if (m_prev.first == m_prev.second || m_currentTemp == targetTemp) {
403 commitGammaRamps(targetTemp);
404 return;
405 }
406
407 if (m_prev.first <= now && now <= m_prev.second) {
408 int availTime = now.msecsTo(m_prev.second);
409 m_slowUpdateTimer = std::make_unique<QTimer>();
410 m_slowUpdateTimer->setSingleShot(false);
411 if (isDay) {
412 connect(m_slowUpdateTimer.get(), &QTimer::timeout, this, [this]() {
413 slowUpdate(m_dayTargetTemp);
414 });
415 } else {
416 connect(m_slowUpdateTimer.get(), &QTimer::timeout, this, [this]() {
417 slowUpdate(m_nightTargetTemp);
418 });
419 }
420
421 // calculate interval such as temperature is changed by TEMPERATURE_STEP K per timer timeout
422 int interval = availTime * TEMPERATURE_STEP / std::abs(targetTemp - m_currentTemp);
423 if (interval == 0) {
424 interval = 1;
425 }
426 m_slowUpdateTimer->start(interval);
427 }
428}
429
430void NightColorManager::slowUpdate(int targetTemp)
431{
432 if (!m_slowUpdateTimer) {
433 return;
434 }
435 int nextTemp;
436 if (m_currentTemp < targetTemp) {
437 nextTemp = std::min(m_currentTemp + TEMPERATURE_STEP, targetTemp);
438 } else {
439 nextTemp = std::max(m_currentTemp - TEMPERATURE_STEP, targetTemp);
440 }
441 commitGammaRamps(nextTemp);
442 if (nextTemp == targetTemp) {
443 // stop timer, we reached the target temp
444 m_slowUpdateTimer.reset();
445 }
446}
447
448void NightColorManager::preview(uint previewTemp)
449{
450 previewTemp = std::clamp<uint>(previewTemp, MIN_TEMPERATURE, DEFAULT_DAY_TEMPERATURE);
451 resetQuickAdjustTimer((int)previewTemp);
452 if (m_previewTimer) {
453 m_previewTimer.reset();
454 }
455 m_previewTimer = std::make_unique<QTimer>();
456 m_previewTimer->setSingleShot(true);
457 connect(m_previewTimer.get(), &QTimer::timeout, this, &NightColorManager::stopPreview);
458 m_previewTimer->start(15000);
459
460 QDBusMessage message = QDBusMessage::createMethodCall(
461 QStringLiteral("org.kde.plasmashell"),
462 QStringLiteral("/org/kde/osdService"),
463 QStringLiteral("org.kde.osdService"),
464 QStringLiteral("showText"));
465 message.setArguments(
466 {QStringLiteral("redshift-status-on"),
467 i18n("Color Temperature Preview")});
468 QDBusConnection::sessionBus().asyncCall(message);
469}
470
472{
473 if (m_previewTimer && m_previewTimer->isActive()) {
474 updateTransitionTimings(false);
475 updateTargetTemperature();
476 resetQuickAdjustTimer(currentTargetTemp());
477 }
478}
479
480void NightColorManager::updateTargetTemperature()
481{
482 const int targetTemperature = mode() != NightColorMode::Constant && daylight() ? m_dayTargetTemp : m_nightTargetTemp;
483
484 if (m_targetTemperature == targetTemperature) {
485 return;
486 }
487
488 m_targetTemperature = targetTemperature;
489
491}
492
493void NightColorManager::updateTransitionTimings(bool force)
494{
495 const auto oldPrev = m_prev;
496 const auto oldNext = m_next;
497
498 if (m_mode == NightColorMode::Constant) {
499 setDaylight(false);
500 m_next = DateTimes();
501 m_prev = DateTimes();
502 } else if (m_mode == NightColorMode::Timings) {
503 const QDateTime todayNow = QDateTime::currentDateTime();
504
505 const QDateTime nextMorB = QDateTime(todayNow.date().addDays(m_morning < todayNow.time()), m_morning);
506 const QDateTime nextMorE = nextMorB.addSecs(m_trTime * 60);
507 const QDateTime nextEveB = QDateTime(todayNow.date().addDays(m_evening < todayNow.time()), m_evening);
508 const QDateTime nextEveE = nextEveB.addSecs(m_trTime * 60);
509
510 if (nextEveB < nextMorB) {
511 setDaylight(true);
512 m_next = DateTimes(nextEveB, nextEveE);
513 m_prev = DateTimes(nextMorB.addDays(-1), nextMorE.addDays(-1));
514 } else {
515 setDaylight(false);
516 m_next = DateTimes(nextMorB, nextMorE);
517 m_prev = DateTimes(nextEveB.addDays(-1), nextEveE.addDays(-1));
518 }
519 } else {
520 const QDateTime todayNow = QDateTime::currentDateTime();
521
522 double lat, lng;
523 if (m_mode == NightColorMode::Automatic) {
524 lat = m_latAuto;
525 lng = m_lngAuto;
526 } else {
527 lat = m_latFixed;
528 lng = m_lngFixed;
529 }
530
531 if (!force) {
532 // first try by only switching the timings
533 if (m_prev.first.date() == m_next.first.date()) {
534 // next is evening
535 setDaylight(true);
536 m_prev = m_next;
537 m_next = getSunTimings(todayNow, lat, lng, false);
538 } else {
539 // next is morning
540 setDaylight(false);
541 m_prev = m_next;
542 m_next = getSunTimings(todayNow.addDays(1), lat, lng, true);
543 }
544 }
545
546 if (force || !checkAutomaticSunTimings()) {
547 // in case this fails, reset them
548 DateTimes morning = getSunTimings(todayNow, lat, lng, true);
549 if (todayNow < morning.first) {
550 setDaylight(false);
551 m_prev = getSunTimings(todayNow.addDays(-1), lat, lng, false);
552 m_next = morning;
553 } else {
554 DateTimes evening = getSunTimings(todayNow, lat, lng, false);
555 if (todayNow < evening.first) {
556 setDaylight(true);
557 m_prev = morning;
558 m_next = evening;
559 } else {
560 setDaylight(false);
561 m_prev = evening;
562 m_next = getSunTimings(todayNow.addDays(1), lat, lng, true);
563 }
564 }
565 }
566 }
567
568 if (oldPrev != m_prev) {
570 }
571 if (oldNext != m_next) {
573 }
574}
575
576DateTimes NightColorManager::getSunTimings(const QDateTime &dateTime, double latitude, double longitude, bool morning) const
577{
578 DateTimes dateTimes = calculateSunTimings(dateTime, latitude, longitude, morning);
579 // At locations near the poles it is possible, that we can't
580 // calculate some or all sun timings (midnight sun).
581 // In this case try to fallback to sensible default values.
582 const bool beginDefined = !dateTimes.first.isNull();
583 const bool endDefined = !dateTimes.second.isNull();
584 if (!beginDefined || !endDefined) {
585 if (beginDefined) {
586 dateTimes.second = dateTimes.first.addMSecs(FALLBACK_SLOW_UPDATE_TIME);
587 } else if (endDefined) {
588 dateTimes.first = dateTimes.second.addMSecs(-FALLBACK_SLOW_UPDATE_TIME);
589 } else {
590 // Just use default values for morning and evening, but the user
591 // will probably deactivate Night Color anyway if he is living
592 // in a region without clear sun rise and set.
593 const QTime referenceTime = morning ? QTime(6, 0) : QTime(18, 0);
594 dateTimes.first = QDateTime(dateTime.date(), referenceTime);
595 dateTimes.second = dateTimes.first.addMSecs(FALLBACK_SLOW_UPDATE_TIME);
596 }
597 }
598 return dateTimes;
599}
600
601bool NightColorManager::checkAutomaticSunTimings() const
602{
603 if (m_prev.first.isValid() && m_prev.second.isValid() && m_next.first.isValid() && m_next.second.isValid()) {
604 const QDateTime todayNow = QDateTime::currentDateTime();
605 return m_prev.first <= todayNow && todayNow < m_next.first && m_prev.first.msecsTo(m_next.first) < MSC_DAY * 23. / 24;
606 }
607 return false;
608}
609
611{
612 return m_daylight;
613}
614
615int NightColorManager::currentTargetTemp() const
616{
617 if (!m_running) {
618 return DEFAULT_DAY_TEMPERATURE;
619 }
620
621 if (m_mode == NightColorMode::Constant) {
622 return m_nightTargetTemp;
623 }
624
625 const QDateTime todayNow = QDateTime::currentDateTime();
626
627 auto f = [this, todayNow](int target1, int target2) {
628 if (todayNow <= m_prev.second) {
629 double residueQuota = todayNow.msecsTo(m_prev.second) / (double)m_prev.first.msecsTo(m_prev.second);
630
631 double ret = (int)((1. - residueQuota) * (double)target2 + residueQuota * (double)target1);
632 // remove single digits
633 ret = ((int)(0.1 * ret)) * 10;
634 return (int)ret;
635 } else {
636 return target2;
637 }
638 };
639
640 if (daylight()) {
641 return f(m_nightTargetTemp, m_dayTargetTemp);
642 } else {
643 return f(m_dayTargetTemp, m_nightTargetTemp);
644 }
645}
646
647void NightColorManager::commitGammaRamps(int temperature)
648{
649 const QList<ColorDevice *> devices = kwinApp()->colorManager()->devices();
650 for (ColorDevice *device : devices) {
651 device->setTemperature(temperature);
652 }
653
654 setCurrentTemperature(temperature);
655}
656
657void NightColorManager::autoLocationUpdate(double latitude, double longitude)
658{
659 qCDebug(KWIN_NIGHTCOLOR, "Received new location (lat: %f, lng: %f)", latitude, longitude);
660
661 if (!checkLocation(latitude, longitude)) {
662 return;
663 }
664
665 // we tolerate small deviations with minimal impact on sun timings
666 if (std::abs(m_latAuto - latitude) < 2 && std::abs(m_lngAuto - longitude) < 1) {
667 return;
668 }
669 cancelAllTimers();
670 m_latAuto = latitude;
671 m_lngAuto = longitude;
672
673 NightColorSettings *s = NightColorSettings::self();
674 s->setLatitudeAuto(latitude);
675 s->setLongitudeAuto(longitude);
676 s->save();
677
678 resetAllTimers();
679}
680
681void NightColorManager::setEnabled(bool enabled)
682{
683 if (m_active == enabled) {
684 return;
685 }
686 m_active = enabled;
687 m_skewNotifier->setActive(enabled);
688 Q_EMIT enabledChanged();
689}
690
691void NightColorManager::setRunning(bool running)
692{
693 if (m_running == running) {
694 return;
695 }
696 m_running = running;
697 Q_EMIT runningChanged();
698}
699
700void NightColorManager::setCurrentTemperature(int temperature)
701{
702 if (m_currentTemp == temperature) {
703 return;
704 }
705 m_currentTemp = temperature;
707}
708
709void NightColorManager::setMode(NightColorMode mode)
710{
711 if (m_mode == mode) {
712 return;
713 }
714 m_mode = mode;
715 Q_EMIT modeChanged();
716}
717
718void NightColorManager::setDaylight(bool daylight)
719{
720 if (m_daylight == daylight) {
721 return;
722 }
723 m_daylight = daylight;
724 Q_EMIT daylightChanged();
725}
726
727} // namespace KWin
728
729#include "moc_nightcolormanager.cpp"
void deviceAdded(ColorDevice *device)
NightColorMode mode() const
static NightColorManager * self()
QDateTime previousTransitionDateTime() const
qint64 previousTransitionDuration() const
void scheduledTransitionTimingsChanged()
QDateTime scheduledTransitionDateTime() const
void quickAdjust(int targetTemp)
void autoLocationUpdate(double latitude, double longitude)
qint64 scheduledTransitionDuration() const
void previousTransitionTimingsChanged()
void preview(uint previewTemp)
void activeChanged(bool active)
QPair< QDateTime, QDateTime > calculateSunTimings(const QDateTime &dateTime, double latitude, double longitude, bool morning)
Definition suncalc.cpp:31
QPair< QDateTime, QDateTime > DateTimes