diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -462,6 +462,10 @@ xkb.cpp gestures.cpp popup_input_filter.cpp + colorcorrection/nightcolor.cpp + colorcorrection/colorcorrectdbusinterface.cpp + colorcorrection/suncalc.cpp + colorcorrection/logging.cpp abstract_opengl_context_attribute_builder.cpp egl_context_attribute_builder.cpp was_user_interaction_x11_filter.cpp @@ -520,6 +524,7 @@ qt5_add_dbus_adaptor( kwin_KDEINIT_SRCS org.kde.KWin.xml dbusinterface.h KWin::DBusInterface ) qt5_add_dbus_adaptor( kwin_KDEINIT_SRCS org.kde.kwin.Compositing.xml dbusinterface.h KWin::CompositorDBusInterface ) +qt5_add_dbus_adaptor( kwin_KDEINIT_SRCS org.kde.kwin.ColorCorrect.xml colorcorrection/colorcorrectdbusinterface.h KWin::ColorCorrect::ColorCorrectDBusInterface ) qt5_add_dbus_adaptor( kwin_KDEINIT_SRCS ${kwin_effects_dbus_xml} effects.h KWin::EffectsHandlerImpl ) qt5_add_dbus_interface( kwin_KDEINIT_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/org.freedesktop.ScreenSaver.xml screenlocker_interface) @@ -660,6 +665,7 @@ FILES org.kde.KWin.xml org.kde.kwin.Compositing.xml + org.kde.kwin.ColorCorrect.xml org.kde.kwin.Effects.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} diff --git a/autotests/integration/CMakeLists.txt b/autotests/integration/CMakeLists.txt --- a/autotests/integration/CMakeLists.txt +++ b/autotests/integration/CMakeLists.txt @@ -54,6 +54,7 @@ integrationTest(NAME testDontCrashUseractionsMenu SRCS dont_crash_useractions_menu.cpp) integrationTest(WAYLAND_ONLY NAME testKWinBindings SRCS kwinbindings_test.cpp) integrationTest(WAYLAND_ONLY NAME testVirtualDesktop SRCS virtual_desktop_test.cpp) +integrationTest(WAYLAND_ONLY NAME testColorCorrectNightColor SRCS colorcorrect_nightcolor_test.cpp) if (XCB_ICCCM_FOUND) integrationTest(NAME testMoveResize SRCS move_resize_window_test.cpp LIBS XCB::ICCCM) diff --git a/autotests/integration/colorcorrect_nightcolor_test.cpp b/autotests/integration/colorcorrect_nightcolor_test.cpp new file mode 100644 --- /dev/null +++ b/autotests/integration/colorcorrect_nightcolor_test.cpp @@ -0,0 +1,334 @@ +/******************************************************************** + 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 "kwin_wayland_test.h" + +#include "platform.h" +#include "wayland_server.h" +#include "colorcorrection/nightcolor.h" +#include "colorcorrection/constants.h" + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_colorcorrect_nightcolor-0"); + +class ColorCorrectNightColorTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testConfigRead_data(); + void testConfigRead(); + void testChangeConfiguration_data(); + void testChangeConfiguration(); + void testAutoLocationUpdate(); +}; + +void ColorCorrectNightColorTest::initTestCase() +{ + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + QMetaObject::invokeMethod(kwinApp()->platform(), "setOutputCount", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void ColorCorrectNightColorTest::init() +{ +} + +void ColorCorrectNightColorTest::cleanup() +{ +} + +void ColorCorrectNightColorTest::testConfigRead_data() +{ + QTest::addColumn("active"); + QTest::addColumn("mode"); + QTest::addColumn("nightTemperature"); + QTest::addColumn("latitudeFixed"); + QTest::addColumn("longitudeFixed"); + QTest::addColumn("morningBeginFixed"); + QTest::addColumn("eveningBeginFixed"); + QTest::addColumn("transitionTime"); + QTest::addColumn("success"); + + QTest::newRow("activeMode0") << "true" << 0 << 4500 << 45.5 << 35.1 << "0600" << "1800" << 30 << true; + QTest::newRow("activeMode1") << "true" << 1 << 2500 << -10.5 << -8. << "0020" << "2000" << 60 << true; + QTest::newRow("notActiveMode2") << "false" << 2 << 5000 << 90. << -180. << "0600" << "1800" << 1 << true; + QTest::newRow("wrongData1") << "fa" << 3 << 7000 << 91. << -181. << "060" << "800" << 999999 << false; + QTest::newRow("wrongData2") << "fa" << 3 << 7000 << 91. << -181. << "060" << "800" << -2 << false; +} + +void ColorCorrectNightColorTest::testConfigRead() +{ + QFETCH(QString, active); + QFETCH(int, mode); + QFETCH(int, nightTemperature); + QFETCH(double, latitudeFixed); + QFETCH(double, longitudeFixed); + QFETCH(QString, morningBeginFixed); + QFETCH(QString, eveningBeginFixed); + QFETCH(int, transitionTime); + QFETCH(bool, success); + + const bool activeDefault = true; + const int modeDefault = 0; + const int nightTemperatureUpperEnd = ColorCorrect::NEUTRAL_TEMPERATURE; + const double latitudeFixedDefault = 0; + const double longitudeFixedDefault = 0; + const QTime morningBeginFixedDefault = QTime(6,0,0); + const QTime eveningBeginFixedDefault = QTime(18,0,0); + const int transitionTimeDefault = 30; + + KConfigGroup cfgGroup = kwinApp()->config()->group("NightColor"); + + cfgGroup.writeEntry("Active", activeDefault); + cfgGroup.writeEntry("Mode", modeDefault); + cfgGroup.writeEntry("NightTemperature", nightTemperatureUpperEnd); + cfgGroup.writeEntry("LatitudeFixed", latitudeFixedDefault); + cfgGroup.writeEntry("LongitudeFixed", longitudeFixedDefault); + cfgGroup.writeEntry("MorningBeginFixed", morningBeginFixedDefault.toString("hhmm")); + cfgGroup.writeEntry("EveningBeginFixed", eveningBeginFixedDefault.toString("hhmm")); + cfgGroup.writeEntry("TransitionTime", transitionTimeDefault); + + ColorCorrect::Manager *manager = kwinApp()->platform()->nightColorManager(); + manager->reparseConfigAndReset(); + auto info = manager->info(); + QVERIFY(!info.isEmpty()); + + QCOMPARE(info.value("Active").toBool(), activeDefault); + QCOMPARE(info.value("Mode").toInt(), modeDefault); + QCOMPARE(info.value("NightTemperature").toInt(), nightTemperatureUpperEnd); + QCOMPARE(info.value("LatitudeFixed").toDouble(), latitudeFixedDefault); + QCOMPARE(info.value("LongitudeFixed").toDouble(), longitudeFixedDefault); + QCOMPARE(QTime::fromString(info.value("MorningBeginFixed").toString(), Qt::ISODate), morningBeginFixedDefault); + QCOMPARE(QTime::fromString(info.value("EveningBeginFixed").toString(), Qt::ISODate), eveningBeginFixedDefault); + QCOMPARE(info.value("TransitionTime").toInt(), transitionTimeDefault); + + cfgGroup.writeEntry("Active", active); + cfgGroup.writeEntry("Mode", mode); + cfgGroup.writeEntry("NightTemperature", nightTemperature); + cfgGroup.writeEntry("LatitudeFixed", latitudeFixed); + cfgGroup.writeEntry("LongitudeFixed", longitudeFixed); + cfgGroup.writeEntry("MorningBeginFixed", morningBeginFixed); + cfgGroup.writeEntry("EveningBeginFixed", eveningBeginFixed); + cfgGroup.writeEntry("TransitionTime", transitionTime); + + manager->reparseConfigAndReset(); + info = manager->info(); + QVERIFY(!info.isEmpty()); + + if (success) { + QCOMPARE(info.value("Active").toBool() ? QString("true") : QString("false"), active); + QCOMPARE(info.value("Mode").toInt(), mode); + QCOMPARE(info.value("NightTemperature").toInt(), nightTemperature); + QCOMPARE(info.value("LatitudeFixed").toDouble(), latitudeFixed); + QCOMPARE(info.value("LongitudeFixed").toDouble(), longitudeFixed); + QCOMPARE(QTime::fromString(info.value("MorningBeginFixed").toString(), Qt::ISODate), QTime::fromString(morningBeginFixed, "hhmm")); + QCOMPARE(QTime::fromString(info.value("EveningBeginFixed").toString(), Qt::ISODate), QTime::fromString(eveningBeginFixed, "hhmm")); + QCOMPARE(info.value("TransitionTime").toInt(), transitionTime); + } else { + QCOMPARE(info.value("Active").toBool(), activeDefault); + QCOMPARE(info.value("Mode").toInt(), modeDefault); + QCOMPARE(info.value("NightTemperature").toInt(), nightTemperatureUpperEnd); + QCOMPARE(info.value("LatitudeFixed").toDouble(), latitudeFixedDefault); + QCOMPARE(info.value("LongitudeFixed").toDouble(), longitudeFixedDefault); + QCOMPARE(QTime::fromString(info.value("MorningBeginFixed").toString(), Qt::ISODate), morningBeginFixedDefault); + QCOMPARE(QTime::fromString(info.value("EveningBeginFixed").toString(), Qt::ISODate), eveningBeginFixedDefault); + QCOMPARE(info.value("TransitionTime").toInt(), transitionTimeDefault); + } +} + +void ColorCorrectNightColorTest::testChangeConfiguration_data() +{ + QTest::addColumn("activeReadIn"); + QTest::addColumn("modeReadIn"); + QTest::addColumn("nightTemperatureReadIn"); + QTest::addColumn("latitudeFixedReadIn"); + QTest::addColumn("longitudeFixedReadIn"); + QTest::addColumn("morBeginFixedReadIn"); + QTest::addColumn("eveBeginFixedReadIn"); + QTest::addColumn("transitionTimeReadIn"); + QTest::addColumn("successReadIn"); + + QTest::newRow("data0") << true << 0 << 4500 << 45.5 << 35.1 << QTime(6,0,0) << QTime(18,0,0) << 30 << true; + QTest::newRow("data1") << true << 1 << 2500 << -10.5 << -8. << QTime(0,2,0) << QTime(20,0,0) << 60 << true; + QTest::newRow("data2") << false << 2 << 5000 << 90. << -180. << QTime(6,0,0) << QTime(19,1,1) << 1 << true; + QTest::newRow("wrongData0") << true << 3 << 4500 << 0. << 0. << QTime(6,0,0) << QTime(18,0,0) << 30 << false; + QTest::newRow("wrongData1") << true << 0 << 500 << 0. << 0. << QTime(6,0,0) << QTime(18,0,0) << 30 << false; + QTest::newRow("wrongData2") << true << 0 << 7000 << 0. << 0. << QTime(6,0,0) << QTime(18,0,0) << 30 << false; + QTest::newRow("wrongData3") << true << 0 << 4500 << 91. << -181. << QTime(6,0,0) << QTime(18,0,0) << 30 << false; + QTest::newRow("wrongData4") << true << 0 << 4500 << 0. << 0. << QTime(18,0,0) << QTime(6,0,0) << 30 << false; + QTest::newRow("wrongData5") << true << 0 << 4500 << 0. << 0. << QTime(6,0,0) << QTime(18,0,0) << 0 << false; + QTest::newRow("wrongData6") << true << 0 << 4500 << 0. << 0. << QTime(6,0,0) << QTime(18,0,0) << -1 << false; + QTest::newRow("wrongData7") << true << 0 << 4500 << 0. << 0. << QTime(12,0,0) << QTime(12,30,0) << 30 << false; + QTest::newRow("wrongData8") << true << 0 << 4500 << 0. << 0. << QTime(1,0,0) << QTime(23,30,0) << 90 << false; +} + +void ColorCorrectNightColorTest::testChangeConfiguration() +{ + QFETCH(bool, activeReadIn); + QFETCH(int, modeReadIn); + QFETCH(int, nightTemperatureReadIn); + QFETCH(double, latitudeFixedReadIn); + QFETCH(double, longitudeFixedReadIn); + QFETCH(QTime, morBeginFixedReadIn); + QFETCH(QTime, eveBeginFixedReadIn); + QFETCH(int, transitionTimeReadIn); + QFETCH(bool, successReadIn); + + const bool activeDefault = true; + const int modeDefault = 0; + const int nightTemperatureDefault = ColorCorrect::DEFAULT_NIGHT_TEMPERATURE; + const double latitudeFixedDefault = 0; + const double longitudeFixedDefault = 0; + const QTime morningBeginFixedDefault = QTime(6,0,0); + const QTime eveningBeginFixedDefault = QTime(18,0,0); + const int transitionTimeDefault = 30; + + // init with default values + bool active = activeDefault; + int mode = modeDefault; + int nightTemperature = nightTemperatureDefault; + double latitudeFixed = latitudeFixedDefault; + double longitudeFixed = longitudeFixedDefault; + QTime morningBeginFixed = morningBeginFixedDefault; + QTime eveningBeginFixed = eveningBeginFixedDefault; + int transitionTime = transitionTimeDefault; + + bool activeExpect = activeDefault; + int modeExpect = modeDefault; + int nightTemperatureExpect = nightTemperatureDefault; + double latitudeFixedExpect = latitudeFixedDefault; + double longitudeFixedExpect = longitudeFixedDefault; + QTime morningBeginFixedExpect = morningBeginFixedDefault; + QTime eveningBeginFixedExpect = eveningBeginFixedDefault; + int transitionTimeExpect = transitionTimeDefault; + + QHash data; + + auto setData = [&active, &mode, &nightTemperature, + &latitudeFixed, &longitudeFixed, + &morningBeginFixed, &eveningBeginFixed, &transitionTime, + &data]() { + data["Active"] = active; + data["Mode"] = mode; + data["NightTemperature"] = nightTemperature; + + data["LatitudeFixed"] = latitudeFixed; + data["LongitudeFixed"] = longitudeFixed; + + data["MorningBeginFixed"] = morningBeginFixed.toString(Qt::ISODate); + data["EveningBeginFixed"] = eveningBeginFixed.toString(Qt::ISODate); + data["TransitionTime"] = transitionTime; + }; + + auto compareValues = [&activeExpect, &modeExpect, &nightTemperatureExpect, + &latitudeFixedExpect, &longitudeFixedExpect, + &morningBeginFixedExpect, &eveningBeginFixedExpect, + &transitionTimeExpect](QHash info) { + QCOMPARE(info.value("Active").toBool(), activeExpect); + QCOMPARE(info.value("Mode").toInt(), modeExpect); + QCOMPARE(info.value("NightTemperature").toInt(), nightTemperatureExpect); + QCOMPARE(info.value("LatitudeFixed").toDouble(), latitudeFixedExpect); + QCOMPARE(info.value("LongitudeFixed").toDouble(), longitudeFixedExpect); + QCOMPARE(info.value("MorningBeginFixed").toString(), morningBeginFixedExpect.toString(Qt::ISODate)); + QCOMPARE(info.value("EveningBeginFixed").toString(), eveningBeginFixedExpect.toString(Qt::ISODate)); + QCOMPARE(info.value("TransitionTime").toInt(), transitionTimeExpect); + }; + + ColorCorrect::Manager *manager = kwinApp()->platform()->nightColorManager(); + + // test with default values + setData(); + manager->changeConfiguration(data); + compareValues(manager->info()); + + // set to test values + active = activeReadIn; + mode = modeReadIn; + nightTemperature = nightTemperatureReadIn; + latitudeFixed = latitudeFixedReadIn; + longitudeFixed = longitudeFixedReadIn; + morningBeginFixed = morBeginFixedReadIn; + eveningBeginFixed = eveBeginFixedReadIn; + transitionTime = transitionTimeReadIn; + + if (successReadIn) { + activeExpect = activeReadIn; + modeExpect = modeReadIn; + nightTemperatureExpect = nightTemperatureReadIn; + latitudeFixedExpect = latitudeFixedReadIn; + longitudeFixedExpect = longitudeFixedReadIn; + morningBeginFixedExpect = morBeginFixedReadIn; + eveningBeginFixedExpect = eveBeginFixedReadIn; + transitionTimeExpect = transitionTimeReadIn; + } + + // test with test values + setData(); + QCOMPARE(manager->changeConfiguration(data), successReadIn); + compareValues(manager->info()); +} + +void ColorCorrectNightColorTest::testAutoLocationUpdate() +{ + ColorCorrect::Manager *manager = kwinApp()->platform()->nightColorManager(); + auto info = manager->info(); + QCOMPARE(info.value("LatitudeAuto").toDouble(), 0.); + QCOMPARE(info.value("LongitudeAuto").toDouble(), 0.); + + // wrong latitude value + manager->autoLocationUpdate(91, 15); + info = manager->info(); + QCOMPARE(info.value("LatitudeAuto").toDouble(), 0.); + QCOMPARE(info.value("LongitudeAuto").toDouble(), 0.); + + // wrong longitude value + manager->autoLocationUpdate(50, -181); + info = manager->info(); + QCOMPARE(info.value("LatitudeAuto").toDouble(), 0.); + QCOMPARE(info.value("LongitudeAuto").toDouble(), 0.); + + // change + manager->autoLocationUpdate(50, -180); + info = manager->info(); + QCOMPARE(info.value("LatitudeAuto").toDouble(), 50.); + QCOMPARE(info.value("LongitudeAuto").toDouble(), -180.); + + // small deviation only + manager->autoLocationUpdate(51.5, -179.5); + info = manager->info(); + QCOMPARE(info.value("LongitudeAuto").toDouble(), -180.); + QCOMPARE(info.value("LatitudeAuto").toDouble(), 50.); +} + +WAYLANDTEST_MAIN(ColorCorrectNightColorTest) +#include "colorcorrect_nightcolor_test.moc" diff --git a/colorcorrection/colorcorrectdbusinterface.h b/colorcorrection/colorcorrectdbusinterface.h new file mode 100644 --- /dev/null +++ b/colorcorrection/colorcorrectdbusinterface.h @@ -0,0 +1,126 @@ +/******************************************************************** + 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 +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.ColorCorrect") + +public: + explicit ColorCorrectDBusInterface(Manager *parent); + virtual ~ColorCorrectDBusInterface() = default; + +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.11 + **/ + 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 succesful, otherwise false. + * A change request for the location or timings needs to provide all relevant fields at the same time + * to be succesful. Otherwise the whole change request will get ignored. A change request will be ignored + * as a whole as well, if one of the provided informations has been sent in a wrong format. + * + * @return bool + * @see nightColorInfo + * @see signalNightColorConfigChange + * @since 5.11 + **/ + bool setNightColorConfig(QHash data); + /** + * @brief For receiving auto location updates, primarily through the KDE Daemon + * @return void + * @since 5.11 + **/ + void nightColorAutoLocationUpdate(double latitude, double longitude); + +Q_SIGNALS: + /** + * @brief Emits that the Night Color configuration has been changed. + * + * The provided variant hash provides the same fields as @link nightColorInfo + * + * @return void + * @see nightColorInfo + * @see nightColorConfigChange + * @since 5.11 + **/ + void nightColorConfigChanged(QHash data); + +private: + Manager *m_manager; +}; + +} + +} + +#endif // KWIN_NIGHTCOLOR_DBUS_INTERFACE_H diff --git a/colorcorrection/colorcorrectdbusinterface.cpp b/colorcorrection/colorcorrectdbusinterface.cpp new file mode 100644 --- /dev/null +++ b/colorcorrection/colorcorrectdbusinterface.cpp @@ -0,0 +1,54 @@ +/******************************************************************** + 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 "nightcolor.h" + +namespace KWin { +namespace ColorCorrect { + +ColorCorrectDBusInterface::ColorCorrectDBusInterface(Manager *parent) + : QObject(parent) + , m_manager(parent) +{ + connect(m_manager, &Manager::configChange, this, &ColorCorrectDBusInterface::nightColorConfigChanged); + new ColorCorrectAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/ColorCorrect"), this); +} + +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); +} + +} +} diff --git a/colorcorrection/constants.h b/colorcorrection/constants.h new file mode 100644 --- /dev/null +++ b/colorcorrection/constants.h @@ -0,0 +1,288 @@ +/******************************************************************** + 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_CONSTANTS_H +#define KWIN_NIGHTCOLOR_CONSTANTS_H + +namespace KWin +{ +namespace ColorCorrect +{ + +static const int MSC_DAY = 86400000; +static const int MIN_TEMPERATURE = 1000; +static const int NEUTRAL_TEMPERATURE = 6500; +static const int DEFAULT_NIGHT_TEMPERATURE = 4500; +static const int FALLBACK_SLOW_UPDATE_TIME = 1800000; + +/* Whitepoint values for temperatures at 100K intervals. + * These will be interpolated for the actual temperature. + * This table was provided by Ingo Thies, 2013. + * See the following file for more information: + * https://github.com/jonls/redshift/blob/master/README-colorramp + */ +static const float blackbodyColor[] = { + 1.00000000, 0.18172716, 0.00000000, /* 1000K */ + 1.00000000, 0.25503671, 0.00000000, /* 1100K */ + 1.00000000, 0.30942099, 0.00000000, /* 1200K */ + 1.00000000, 0.35357379, 0.00000000, /* ... */ + 1.00000000, 0.39091524, 0.00000000, + 1.00000000, 0.42322816, 0.00000000, + 1.00000000, 0.45159884, 0.00000000, + 1.00000000, 0.47675916, 0.00000000, + 1.00000000, 0.49923747, 0.00000000, + 1.00000000, 0.51943421, 0.00000000, + 1.00000000, 0.54360078, 0.08679949, + 1.00000000, 0.56618736, 0.14065513, + 1.00000000, 0.58734976, 0.18362641, + 1.00000000, 0.60724493, 0.22137978, + 1.00000000, 0.62600248, 0.25591950, + 1.00000000, 0.64373109, 0.28819679, + 1.00000000, 0.66052319, 0.31873863, + 1.00000000, 0.67645822, 0.34786758, + 1.00000000, 0.69160518, 0.37579588, + 1.00000000, 0.70602449, 0.40267128, + 1.00000000, 0.71976951, 0.42860152, + 1.00000000, 0.73288760, 0.45366838, + 1.00000000, 0.74542112, 0.47793608, + 1.00000000, 0.75740814, 0.50145662, + 1.00000000, 0.76888303, 0.52427322, + 1.00000000, 0.77987699, 0.54642268, + 1.00000000, 0.79041843, 0.56793692, + 1.00000000, 0.80053332, 0.58884417, + 1.00000000, 0.81024551, 0.60916971, + 1.00000000, 0.81957693, 0.62893653, + 1.00000000, 0.82854786, 0.64816570, + 1.00000000, 0.83717703, 0.66687674, + 1.00000000, 0.84548188, 0.68508786, + 1.00000000, 0.85347859, 0.70281616, + 1.00000000, 0.86118227, 0.72007777, + 1.00000000, 0.86860704, 0.73688797, + 1.00000000, 0.87576611, 0.75326132, + 1.00000000, 0.88267187, 0.76921169, + 1.00000000, 0.88933596, 0.78475236, + 1.00000000, 0.89576933, 0.79989606, + 1.00000000, 0.90198230, 0.81465502, + 1.00000000, 0.90963069, 0.82838210, + 1.00000000, 0.91710889, 0.84190889, + 1.00000000, 0.92441842, 0.85523742, + 1.00000000, 0.93156127, 0.86836903, + 1.00000000, 0.93853986, 0.88130458, + 1.00000000, 0.94535695, 0.89404470, + 1.00000000, 0.95201559, 0.90658983, + 1.00000000, 0.95851906, 0.91894041, + 1.00000000, 0.96487079, 0.93109690, + 1.00000000, 0.97107439, 0.94305985, + 1.00000000, 0.97713351, 0.95482993, + 1.00000000, 0.98305189, 0.96640795, + 1.00000000, 0.98883326, 0.97779486, + 1.00000000, 0.99448139, 0.98899179, + 1.00000000, 1.00000000, 1.00000000, /* 6500K */ + 0.98947904, 0.99348723, 1.00000000, + 0.97940448, 0.98722715, 1.00000000, + 0.96975025, 0.98120637, 1.00000000, + 0.96049223, 0.97541240, 1.00000000, + 0.95160805, 0.96983355, 1.00000000, + 0.94303638, 0.96443333, 1.00000000, + 0.93480451, 0.95923080, 1.00000000, + 0.92689056, 0.95421394, 1.00000000, + 0.91927697, 0.94937330, 1.00000000, + 0.91194747, 0.94470005, 1.00000000, + 0.90488690, 0.94018594, 1.00000000, + 0.89808115, 0.93582323, 1.00000000, + 0.89151710, 0.93160469, 1.00000000, + 0.88518247, 0.92752354, 1.00000000, + 0.87906581, 0.92357340, 1.00000000, + 0.87315640, 0.91974827, 1.00000000, + 0.86744421, 0.91604254, 1.00000000, + 0.86191983, 0.91245088, 1.00000000, + 0.85657444, 0.90896831, 1.00000000, + 0.85139976, 0.90559011, 1.00000000, + 0.84638799, 0.90231183, 1.00000000, + 0.84153180, 0.89912926, 1.00000000, + 0.83682430, 0.89603843, 1.00000000, + 0.83225897, 0.89303558, 1.00000000, + 0.82782969, 0.89011714, 1.00000000, + 0.82353066, 0.88727974, 1.00000000, + 0.81935641, 0.88452017, 1.00000000, + 0.81530175, 0.88183541, 1.00000000, + 0.81136180, 0.87922257, 1.00000000, + 0.80753191, 0.87667891, 1.00000000, + 0.80380769, 0.87420182, 1.00000000, + 0.80018497, 0.87178882, 1.00000000, + 0.79665980, 0.86943756, 1.00000000, + 0.79322843, 0.86714579, 1.00000000, + 0.78988728, 0.86491137, 1.00000000, /* 10000K */ + 0.78663296, 0.86273225, 1.00000000, + 0.78346225, 0.86060650, 1.00000000, + 0.78037207, 0.85853224, 1.00000000, + 0.77735950, 0.85650771, 1.00000000, + 0.77442176, 0.85453121, 1.00000000, + 0.77155617, 0.85260112, 1.00000000, + 0.76876022, 0.85071588, 1.00000000, + 0.76603147, 0.84887402, 1.00000000, + 0.76336762, 0.84707411, 1.00000000, + 0.76076645, 0.84531479, 1.00000000, + 0.75822586, 0.84359476, 1.00000000, + 0.75574383, 0.84191277, 1.00000000, + 0.75331843, 0.84026762, 1.00000000, + 0.75094780, 0.83865816, 1.00000000, + 0.74863017, 0.83708329, 1.00000000, + 0.74636386, 0.83554194, 1.00000000, + 0.74414722, 0.83403311, 1.00000000, + 0.74197871, 0.83255582, 1.00000000, + 0.73985682, 0.83110912, 1.00000000, + 0.73778012, 0.82969211, 1.00000000, + 0.73574723, 0.82830393, 1.00000000, + 0.73375683, 0.82694373, 1.00000000, + 0.73180765, 0.82561071, 1.00000000, + 0.72989845, 0.82430410, 1.00000000, + 0.72802807, 0.82302316, 1.00000000, + 0.72619537, 0.82176715, 1.00000000, + 0.72439927, 0.82053539, 1.00000000, + 0.72263872, 0.81932722, 1.00000000, + 0.72091270, 0.81814197, 1.00000000, + 0.71922025, 0.81697905, 1.00000000, + 0.71756043, 0.81583783, 1.00000000, + 0.71593234, 0.81471775, 1.00000000, + 0.71433510, 0.81361825, 1.00000000, + 0.71276788, 0.81253878, 1.00000000, + 0.71122987, 0.81147883, 1.00000000, + 0.70972029, 0.81043789, 1.00000000, + 0.70823838, 0.80941546, 1.00000000, + 0.70678342, 0.80841109, 1.00000000, + 0.70535469, 0.80742432, 1.00000000, + 0.70395153, 0.80645469, 1.00000000, + 0.70257327, 0.80550180, 1.00000000, + 0.70121928, 0.80456522, 1.00000000, + 0.69988894, 0.80364455, 1.00000000, + 0.69858167, 0.80273941, 1.00000000, + 0.69729688, 0.80184943, 1.00000000, + 0.69603402, 0.80097423, 1.00000000, + 0.69479255, 0.80011347, 1.00000000, + 0.69357196, 0.79926681, 1.00000000, + 0.69237173, 0.79843391, 1.00000000, + 0.69119138, 0.79761446, 1.00000000, /* 15000K */ + 0.69003044, 0.79680814, 1.00000000, + 0.68888844, 0.79601466, 1.00000000, + 0.68776494, 0.79523371, 1.00000000, + 0.68665951, 0.79446502, 1.00000000, + 0.68557173, 0.79370830, 1.00000000, + 0.68450119, 0.79296330, 1.00000000, + 0.68344751, 0.79222975, 1.00000000, + 0.68241029, 0.79150740, 1.00000000, + 0.68138918, 0.79079600, 1.00000000, + 0.68038380, 0.79009531, 1.00000000, + 0.67939381, 0.78940511, 1.00000000, + 0.67841888, 0.78872517, 1.00000000, + 0.67745866, 0.78805526, 1.00000000, + 0.67651284, 0.78739518, 1.00000000, + 0.67558112, 0.78674472, 1.00000000, + 0.67466317, 0.78610368, 1.00000000, + 0.67375872, 0.78547186, 1.00000000, + 0.67286748, 0.78484907, 1.00000000, + 0.67198916, 0.78423512, 1.00000000, + 0.67112350, 0.78362984, 1.00000000, + 0.67027024, 0.78303305, 1.00000000, + 0.66942911, 0.78244457, 1.00000000, + 0.66859988, 0.78186425, 1.00000000, + 0.66778228, 0.78129191, 1.00000000, + 0.66697610, 0.78072740, 1.00000000, + 0.66618110, 0.78017057, 1.00000000, + 0.66539706, 0.77962127, 1.00000000, + 0.66462376, 0.77907934, 1.00000000, + 0.66386098, 0.77854465, 1.00000000, + 0.66310852, 0.77801705, 1.00000000, + 0.66236618, 0.77749642, 1.00000000, + 0.66163375, 0.77698261, 1.00000000, + 0.66091106, 0.77647551, 1.00000000, + 0.66019791, 0.77597498, 1.00000000, + 0.65949412, 0.77548090, 1.00000000, + 0.65879952, 0.77499315, 1.00000000, + 0.65811392, 0.77451161, 1.00000000, + 0.65743716, 0.77403618, 1.00000000, + 0.65676908, 0.77356673, 1.00000000, + 0.65610952, 0.77310316, 1.00000000, + 0.65545831, 0.77264537, 1.00000000, + 0.65481530, 0.77219324, 1.00000000, + 0.65418036, 0.77174669, 1.00000000, + 0.65355332, 0.77130560, 1.00000000, + 0.65293404, 0.77086988, 1.00000000, + 0.65232240, 0.77043944, 1.00000000, + 0.65171824, 0.77001419, 1.00000000, + 0.65112144, 0.76959404, 1.00000000, + 0.65053187, 0.76917889, 1.00000000, + 0.64994941, 0.76876866, 1.00000000, /* 20000K */ + 0.64937392, 0.76836326, 1.00000000, + 0.64880528, 0.76796263, 1.00000000, + 0.64824339, 0.76756666, 1.00000000, + 0.64768812, 0.76717529, 1.00000000, + 0.64713935, 0.76678844, 1.00000000, + 0.64659699, 0.76640603, 1.00000000, + 0.64606092, 0.76602798, 1.00000000, + 0.64553103, 0.76565424, 1.00000000, + 0.64500722, 0.76528472, 1.00000000, + 0.64448939, 0.76491935, 1.00000000, + 0.64397745, 0.76455808, 1.00000000, + 0.64347129, 0.76420082, 1.00000000, + 0.64297081, 0.76384753, 1.00000000, + 0.64247594, 0.76349813, 1.00000000, + 0.64198657, 0.76315256, 1.00000000, + 0.64150261, 0.76281076, 1.00000000, + 0.64102399, 0.76247267, 1.00000000, + 0.64055061, 0.76213824, 1.00000000, + 0.64008239, 0.76180740, 1.00000000, + 0.63961926, 0.76148010, 1.00000000, + 0.63916112, 0.76115628, 1.00000000, + 0.63870790, 0.76083590, 1.00000000, + 0.63825953, 0.76051890, 1.00000000, + 0.63781592, 0.76020522, 1.00000000, + 0.63737701, 0.75989482, 1.00000000, + 0.63694273, 0.75958764, 1.00000000, + 0.63651299, 0.75928365, 1.00000000, + 0.63608774, 0.75898278, 1.00000000, + 0.63566691, 0.75868499, 1.00000000, + 0.63525042, 0.75839025, 1.00000000, + 0.63483822, 0.75809849, 1.00000000, + 0.63443023, 0.75780969, 1.00000000, + 0.63402641, 0.75752379, 1.00000000, + 0.63362667, 0.75724075, 1.00000000, + 0.63323097, 0.75696053, 1.00000000, + 0.63283925, 0.75668310, 1.00000000, + 0.63245144, 0.75640840, 1.00000000, + 0.63206749, 0.75613641, 1.00000000, + 0.63168735, 0.75586707, 1.00000000, + 0.63131096, 0.75560036, 1.00000000, + 0.63093826, 0.75533624, 1.00000000, + 0.63056920, 0.75507467, 1.00000000, + 0.63020374, 0.75481562, 1.00000000, + 0.62984181, 0.75455904, 1.00000000, + 0.62948337, 0.75430491, 1.00000000, + 0.62912838, 0.75405319, 1.00000000, + 0.62877678, 0.75380385, 1.00000000, + 0.62842852, 0.75355685, 1.00000000, + 0.62808356, 0.75331217, 1.00000000, + 0.62774186, 0.75306977, 1.00000000, /* 25000K */ + 0.62740336, 0.75282962, 1.00000000 /* 25100K */ +}; + +} +} + +#endif // KWIN_NIGHTCOLOR_CONSTANTS_H diff --git a/colorcorrection/gammaramp.h b/colorcorrection/gammaramp.h new file mode 100644 --- /dev/null +++ b/colorcorrection/gammaramp.h @@ -0,0 +1,50 @@ +/******************************************************************** + 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_GAMMARAMP_H +#define KWIN_GAMMARAMP_H + +namespace KWin +{ + +namespace ColorCorrect +{ + +struct GammaRamp { + GammaRamp(int _size) { + size = _size; + red = new uint16_t[3 * _size]; + green = red + _size; + blue = green + _size; + } + ~GammaRamp() { + delete[] red; + red = green = blue = nullptr; + } + + uint32_t size = 0; + uint16_t *red = nullptr; + uint16_t *green = nullptr; + uint16_t *blue = nullptr; +}; + +} +} + +#endif // KWIN_GAMMARAMP_H diff --git a/colorcorrection/logging.h b/colorcorrection/logging.h new file mode 100644 --- /dev/null +++ b/colorcorrection/logging.h @@ -0,0 +1,26 @@ +/******************************************************************** + 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_COLORCORRECTION_LOGGING_H +#define KWIN_COLORCORRECTION_LOGGING_H +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(KWIN_COLORCORRECTION) +#endif diff --git a/colorcorrection/logging.cpp b/colorcorrection/logging.cpp new file mode 100644 --- /dev/null +++ b/colorcorrection/logging.cpp @@ -0,0 +1,21 @@ +/******************************************************************** + 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 "logging.h" +Q_LOGGING_CATEGORY(KWIN_COLORCORRECTION, "kwin_colorcorrection", QtCriticalMsg) diff --git a/colorcorrection/nightcolor.h b/colorcorrection/nightcolor.h new file mode 100644 --- /dev/null +++ b/colorcorrection/nightcolor.h @@ -0,0 +1,144 @@ +/******************************************************************** + 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_H +#define KWIN_NIGHTCOLOR_H + +#include "constants.h" +#include + +#include +#include +#include + +class QTimer; + +namespace KWin +{ + +class Platform; + +namespace ColorCorrect +{ + +typedef QPair DATETIMES; +typedef QPair TIMES; + +class ColorCorrectDBusInterface; + +class KWIN_EXPORT Manager : public QObject +{ + Q_OBJECT + +public: + Manager(QObject *parent); + void init(); + + /** + * Get current configuration + * @see changeConfiguration + * @since 5.11 + **/ + QHash info() const; + /** + * Change configuration + * @see info + * @since 5.11 + **/ + bool changeConfiguration(QHash data); + void autoLocationUpdate(double latitude, double longitude); + + // for auto tests + void reparseConfigAndReset(); + +public Q_SLOTS: + void resetSlowUpdateStartTimer(); + void quickAdjust(); + +Q_SIGNALS: + void configChange(QHash data); + +private: + 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(QDate date, double latitude, double longitude, bool morning) const; + bool checkAutomaticSunTimings() const; + bool daylight() const; + + void commitGammaRamps(int temperature); + + enum class Mode { + // timings are based on provided location data + Automatic = 0, + // timings are based on fixed location data + Location, + // fixed timings + Timings + } m_mode; + + ColorCorrectDBusInterface *m_iface; + + bool m_active; + bool m_running = false; + + // 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; +}; + +} +} + +#endif // KWIN_NIGHTCOLOR_H diff --git a/colorcorrection/nightcolor.cpp b/colorcorrection/nightcolor.cpp new file mode 100644 --- /dev/null +++ b/colorcorrection/nightcolor.cpp @@ -0,0 +1,774 @@ +/******************************************************************** + 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 "nightcolor.h" +#include "colorcorrectdbusinterface.h" +#include "suncalc.h" +#include "gammaramp.h" +#include "logging.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#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() +{ + // we may always read in the current config + readConfig(); + + if (!kwinApp()->platform()->supportsNightColor()) { + // at least update the sun timings to make the values accessible via dbus + updateSunTimings(true); + return; + } + + 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(); + updateSunTimings(true); + if (kwinApp()->platform()->supportsNightColor() && m_active) { + m_running = true; + commitGammaRamps(currentTargetTemp()); + } + resetAllTimers(); +} + +void Manager::reparseConfigAndReset() +{ + cancelAllTimers(); + readConfig(); + hardReset(); +} + +void Manager::readConfig() +{ + KConfigGroup cfgGroup(kwinApp()->config(), "NightColor"); + + m_active = cfgGroup.readEntry("Active", false); + + int mode = cfgGroup.readEntry("Mode", 0); + if (mode == 1) { + m_mode = Mode::Location; + } else if (mode == 2) { + m_mode = Mode::Timings; + } else { + m_mode = Mode::Automatic; + } + + m_nightTargetTemp = qBound(MIN_TEMPERATURE, cfgGroup.readEntry("NightTemperature", DEFAULT_NIGHT_TEMPERATURE), NEUTRAL_TEMPERATURE); + + double lat, lng; + auto correctReadin = [&lat, &lng]() { + if (!checkLocation(lat, lng)) { + // out of domain + lat = 0; + lng = 0; + } + }; + // automatic + lat = cfgGroup.readEntry("LatitudeAuto", 0.); + lng = cfgGroup.readEntry("LongitudeAuto", 0.); + correctReadin(); + m_latAuto = lat; + m_lngAuto = lng; + // fixed location + lat = cfgGroup.readEntry("LatitudeFixed", 0.); + lng = cfgGroup.readEntry("LongitudeFixed", 0.); + correctReadin(); + m_latFixed = lat; + m_lngFixed = lng; + + // fixed timings + QTime mrB = QTime::fromString(cfgGroup.readEntry("MorningBeginFixed", "0600"), "hhmm"); + QTime evB = QTime::fromString(cfgGroup.readEntry("EveningBeginFixed", "1800"), "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 = cfgGroup.readEntry("TransitionTime", FALLBACK_SLOW_UPDATE_TIME / 1000 / 60) * 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()->supportsNightColor()) { + 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() +{ + 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; + } + + // 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 == Mode::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); + 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 == Mode::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 == Mode::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; + } + + 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) +{ + int nscreens = Screens::self()->count(); + + for (int screen = 0; screen < nscreens; screen++) { + int rampsize = kwinApp()->platform()->gammaRampSize(screen); + GammaRamp ramp(rampsize); + + /* + * The gamma calculation below is based on the Redshift app: + * https://github.com/jonls/redshift + */ + + // linear default state + for (int i = 0; i < rampsize; i++) { + uint16_t value = (double)i / rampsize * (UINT16_MAX + 1); + ramp.red[i] = value; + ramp.green[i] = value; + ramp.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++) { + ramp.red[i] = (double)ramp.red[i] / (UINT16_MAX+1) * whitePoint[0] * (UINT16_MAX+1); + ramp.green[i] = (double)ramp.green[i] / (UINT16_MAX+1) * whitePoint[1] * (UINT16_MAX+1); + ramp.blue[i] = (double)ramp.blue[i] / (UINT16_MAX+1) * whitePoint[2] * (UINT16_MAX+1); + } + + if (kwinApp()->platform()->setGammaRamp(screen, ramp)) { + m_currentTemp = temperature; + m_failedCommitAttempts = 0; + } else { + m_failedCommitAttempts++; + if (m_failedCommitAttempts < 10) { + qCWarning(KWIN_COLORCORRECTION).nospace() << "Committing Gamma Ramp failed for screen " << screen << + ". 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 +{ + QHash ret; + ret["Available"] = kwinApp()->platform()->supportsNightColor(); + + ret["ActiveEnabled"] = true; + ret["Active"] = m_active; + + ret["ModeEnabled"] = true; + ret["Mode"] = (int)m_mode; + + ret["NightTemperatureEnabled"] = true; + ret["NightTemperature"] = m_nightTargetTemp; + + ret["Running"] = m_running; + ret["CurrentColorTemperature"] = m_currentTemp; + + ret["LatitudeAuto"] = m_latAuto; + ret["LongitudeAuto"] = m_lngAuto; + + ret["LocationEnabled"] = true; + ret["LatitudeFixed"] = m_latFixed; + ret["LongitudeFixed"] = m_lngFixed; + + ret["TimingsEnabled"] = true; + ret["MorningBeginFixed"] = m_morning.toString(Qt::ISODate); + ret["EveningBeginFixed"] = m_evening.toString(Qt::ISODate); + ret["TransitionTime"] = m_trTime; + + return ret; +} + +bool Manager::changeConfiguration(QHash data) +{ + bool activeUpdate, modeUpdate, tempUpdate, locUpdate, timeUpdate; + activeUpdate = modeUpdate = tempUpdate = locUpdate = timeUpdate = false; + + bool active = m_active; + Mode 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 || 2 < mo) { + return false; + } + Mode moM; + switch (mo) { + case 0: + moM = Mode::Automatic; + break; + case 1: + moM = Mode::Location; + break; + case 2: + moM = Mode::Timings; + } + 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 == Mode::Location) || + (timeUpdate && mode == Mode::Timings); + + if (resetNeeded) { + cancelAllTimers(); + } + + KConfigGroup cfgGroup(kwinApp()->config(), "NightColor"); + if (activeUpdate) { + m_active = active; + cfgGroup.writeEntry("Active", active); + } + + if (modeUpdate) { + m_mode = mode; + cfgGroup.writeEntry("Mode", (int)mode); + } + + if (tempUpdate) { + m_nightTargetTemp = nightT; + cfgGroup.writeEntry("NightTemperature", nightT); + } + + if (locUpdate) { + m_latFixed = lat; + m_lngFixed = lng; + cfgGroup.writeEntry("LatitudeFixed", lat); + cfgGroup.writeEntry("LongitudeFixed", lng); + } + + if (timeUpdate) { + m_morning = mor; + m_evening = eve; + m_trTime = trT; + cfgGroup.writeEntry("MorningBeginFixed", mor.toString("hhmm")); + cfgGroup.writeEntry("EveningBeginFixed", eve.toString("hhmm")); + cfgGroup.writeEntry("TransitionTime", trT); + } + cfgGroup.sync(); + + 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; + + KConfigGroup cfgGroup(kwinApp()->config(), "NightColor"); + cfgGroup.writeEntry("LatitudeAuto", latitude); + cfgGroup.writeEntry("LongitudeAuto", longitude); + cfgGroup.sync(); + + resetAllTimers(); + emit configChange(info()); +} + +} +} diff --git a/colorcorrection/suncalc.h b/colorcorrection/suncalc.h new file mode 100644 --- /dev/null +++ b/colorcorrection/suncalc.h @@ -0,0 +1,47 @@ +/******************************************************************** + 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_SUNCALCULATOR_H +#define KWIN_SUNCALCULATOR_H + +#include +#include +#include + +namespace KWin +{ + +namespace ColorCorrect +{ + +/** + * Calculates for a given location and date two of the + * following sun timings in their temporal order: + * - Nautical dawn and sunrise for the morning + * - Sunset and nautical dusk for the evening + * @since 5.11 + **/ + +QPair calculateSunTimings(QDate prompt, double latitude, double longitude, bool morning); + + +} +} + +#endif // KWIN_SUNCALCULATOR_H diff --git a/colorcorrection/suncalc.cpp b/colorcorrection/suncalc.cpp new file mode 100644 --- /dev/null +++ b/colorcorrection/suncalc.cpp @@ -0,0 +1,212 @@ +/******************************************************************** + 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 "suncalc.h" +#include "constants.h" + +#include +#include + +namespace KWin { +namespace ColorCorrect { + +#define TWILIGHT_NAUT -12.0 +#define TWILIGHT_CIVIL -6.0 +#define SUN_RISE_SET -0.833 +#define SUN_HIGH 2.0 + +QPair calculateSunTimings(QDate prompt, double latitude, double longitude, bool morning) +{ + // calculations based on http://aa.quae.nl/en/reken/zonpositie.html + + const qint64 ju2000 = 2451545; // J2000 + + // positioning + const double rad = M_PI / 180; + const double earthObliquity = 23.4397 * rad; // epsilon + const double perihelionLng = 102.9372 * rad; // BigPi + + const double lat = rad * latitude; // phi + double lng = - rad * longitude; // lw + + // times + const double j0 = 0.0009; // J0 + + // substract 0.5, since the first full day ends at noon + const double juPrompt = prompt.toJulianDay() /*- 0.5*/; // J + double juCycle = juPrompt - ju2000 - j0 - lng / (2 * M_PI); // nX + + // geometry + auto modulo = [](double number, double base) { + if (number < 0) { + base = -base; + } + return number - ((int)(number / base)) * base; + }; + auto observer = [lng, ju2000, rad, modulo](double date) { // theta + double ret0 = 280.147 + 360.9856235 * (date - ju2000) - lng / rad; + double ret = std::fmod(ret0, 360) * rad; + return ret; + }; + + auto anomaly = [rad,modulo](double date) { // M + double ret = rad * (357.5291 + 0.98560028 * (date - ju2000)); + return modulo(ret, 2 * M_PI); + }; + + auto center = [rad, modulo](double anomaly) { // C + double ret = modulo(rad * (1.9148 * qSin(anomaly) + 0.02 * qSin(2 * anomaly) + 0.0003 * qSin(3 * anomaly)), 2 * M_PI); + return ret; + }; + + auto ecliptLngMean = [perihelionLng, modulo](double anom) { // Lsun = M + BigPi + 180° + double ret = modulo(anom + perihelionLng + M_PI, 2 * M_PI); + return ret; + }; + + auto ecliptLng = [ecliptLngMean, center](double anom) { // lambda = Lsun + C + double ret = ecliptLngMean(anom) + center(anom); + return ret; + }; + + auto declination = [earthObliquity, anomaly, ecliptLng, modulo](double date) { // delta + double anom = anomaly(date); + double eclLng = ecliptLng(anom); + double ret = qAsin(qSin(earthObliquity) * qSin(eclLng)); + return ret; + }; + + // TODO: This is a bit ineffcient, because we calculate anom and eclLng in declination as well + auto ascension = [earthObliquity, anomaly, ecliptLng, declination,rad,modulo](double date) { // delta + double anom = anomaly(date); + double eclLng = ecliptLng(anom); + double ret = modulo(qAcos(qCos(eclLng) / qCos(declination(date))), 2 * M_PI); + return ret; + }; + + // sun hour angle at noon (angle is 0) + auto hourAngle = [observer, rad, ascension, modulo](double date) { // H + double ret0 = observer(date) - ascension(date); + double ret = modulo(ret0, 2 * M_PI); + if (M_PI < ret) { + ret = ret - 2 * M_PI; + } + return ret; // theta - alpha + }; + // sun hour angle at specific angle + auto hourAngleT = [lat, declination, modulo](double date, double angle) { // M + BigPi + 180° + double decl = declination(date); + double ret0 = (qSin(angle) - qSin(lat) * qSin(decl)) / (qCos(lat) * qCos(decl)); + double ret = modulo(qAcos( ret0 ), 2 * M_PI); + if (M_PI < ret) { + ret = ret - 2 * M_PI; + } + return ret; + }; + + // sun position getters + auto getTransit = [juCycle, anomaly, ecliptLngMean, hourAngle,hourAngleT](double date) { // Jtransit + double estimate = date + qRound64(juCycle) - juCycle; // Jx + double anom = anomaly(estimate); // M + + double jTr = estimate + 0.0053 * qSin(anom) - 0.0068 * qSin(2 * ecliptLngMean(anom)); + // improving + for (int i = 0; i < 5; i++) { + double hA = hourAngle(jTr); + jTr = jTr - (hA / (2 * M_PI)); + if (hA < 0.0001) { + break; + } + } + return jTr; + }; + + auto getSunMorning = [hourAngle, hourAngleT](double angle, double transit) { + double jRise = transit - hourAngleT(transit, angle) / (2 * M_PI); + + // improving + for (int i = 0; i < 5; i++) { + double hA = hourAngle(jRise); + double hAT = hourAngleT(jRise, angle); + + jRise = jRise - (hA + hAT) /(2 * M_PI); + + if (hA + hAT < 0.0001) { + break; + } + } + return jRise; + + }; + + auto getSunEvening = [hourAngle, hourAngleT](double angle, double transit) { + double jSet = transit + hourAngleT(transit, angle) / (2 * M_PI); + + // improving + for (int i = 0; i < 5; i++) { + double hA = hourAngle(jSet); + double hAT = hourAngleT(jSet, angle); + + jSet = jSet - (hA - hAT) /(2 * M_PI); + if (hA - hAT < 0.0001) { + break; + } + } + return jSet; + }; + + /* + * Begin calculations + */ + + // noon - sun at the highest point + double juNoon = getTransit(juPrompt); + + double begin, end; + if (morning) { + begin = getSunMorning(TWILIGHT_CIVIL * rad, juNoon); + end = getSunMorning(SUN_HIGH * rad, juNoon); + } else { + begin = getSunEvening(SUN_HIGH * rad, juNoon); + end = getSunEvening(TWILIGHT_CIVIL * rad, juNoon); + } + // transform to QDateTime + begin += 0.5; + end += 0.5; + + QTime timeBegin, timeEnd; + + if (std::isnan(begin)) { + timeBegin = QTime(); + } else { + double timePart = begin - (int)begin; + timeBegin = QTime::fromMSecsSinceStartOfDay((int)( timePart * MSC_DAY )); + } + if (std::isnan(end)) { + timeEnd = QTime(); + } else { + double timePart = end - (int)end; + timeEnd = QTime::fromMSecsSinceStartOfDay((int)( timePart * MSC_DAY )); + } + + return QPair (timeBegin, timeEnd); +} + +} +} diff --git a/org.kde.kwin.ColorCorrect.xml b/org.kde.kwin.ColorCorrect.xml new file mode 100644 --- /dev/null +++ b/org.kde.kwin.ColorCorrect.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform.h b/platform.h --- a/platform.h +++ b/platform.h @@ -38,6 +38,10 @@ namespace KWin { +namespace ColorCorrect { +class Manager; +struct GammaRamp; +} class Edge; class Compositor; @@ -383,6 +387,28 @@ **/ virtual QVector supportedCompositors() const = 0; + /** + * Whether Night Color is supported by the backend. + * @since 5.11 + **/ + bool supportsNightColor() const { + return m_supportsNightColor; + } + + ColorCorrect::Manager *nightColorManager() { + return m_nightColor; + } + + virtual int gammaRampSize(int screen) const { + Q_UNUSED(screen); + return 0; + } + virtual bool setGammaRamp(int screen, ColorCorrect::GammaRamp &gamma) { + Q_UNUSED(screen); + Q_UNUSED(gamma); + return false; + } + public Q_SLOTS: void pointerMotion(const QPointF &position, quint32 time); void pointerButtonPressed(quint32 button, quint32 time); @@ -435,6 +461,9 @@ void setSupportsPointerWarping(bool set) { m_pointerWarping = set; } + void setSupportsNightColor(bool set) { + m_supportsNightColor = set; + } /** * Actual platform specific way to hide the cursor. @@ -479,6 +508,8 @@ EGLContext m_context = EGL_NO_CONTEXT; EGLSurface m_surface = EGL_NO_SURFACE; int m_hideCursorCounter = 0; + ColorCorrect::Manager *m_nightColor = nullptr; + bool m_supportsNightColor = false; }; } diff --git a/platform.cpp b/platform.cpp --- a/platform.cpp +++ b/platform.cpp @@ -30,6 +30,8 @@ #include "scene.h" #include "screenedge.h" #include "wayland_server.h" +#include "colorcorrection/nightcolor.h" + #include namespace KWin @@ -39,6 +41,7 @@ : QObject(parent) , m_eglDisplay(EGL_NO_DISPLAY) { + m_nightColor = new ColorCorrect::Manager(this); } Platform::~Platform() diff --git a/plugins/platforms/drm/drm_backend.h b/plugins/platforms/drm/drm_backend.h --- a/plugins/platforms/drm/drm_backend.h +++ b/plugins/platforms/drm/drm_backend.h @@ -55,6 +55,10 @@ namespace KWin { +namespace ColorCorrect { +struct GammaRamp; +} + class Udev; class UdevMonitor; @@ -117,6 +121,8 @@ gbm_device *gbmDevice() const { return m_gbmDevice; } + int gammaRampSize(int screen) const override; + bool setGammaRamp(int screen, ColorCorrect::GammaRamp &gamma) override; QVector supportedCompositors() const override; diff --git a/plugins/platforms/drm/drm_backend.cpp b/plugins/platforms/drm/drm_backend.cpp --- a/plugins/platforms/drm/drm_backend.cpp +++ b/plugins/platforms/drm/drm_backend.cpp @@ -31,6 +31,7 @@ #include "screens_drm.h" #include "udev.h" #include "wayland_server.h" +#include #if HAVE_GBM #include "egl_gbm_backend.h" #include @@ -73,6 +74,7 @@ , m_udevMonitor(m_udev->monitor()) , m_dpmsFilter() { + setSupportsNightColor(true); handleOutputs(); m_cursor[0] = nullptr; m_cursor[1] = nullptr; @@ -747,4 +749,20 @@ #endif } +int DrmBackend::gammaRampSize(int screen) const +{ + if (m_outputs.size() <= screen) { + return 0; + } + return m_outputs.at(screen)->m_crtc->getGammaRampSize(); +} + +bool DrmBackend::setGammaRamp(int screen, ColorCorrect::GammaRamp &gamma) +{ + if (m_outputs.size() <= screen) { + return false; + } + return m_outputs.at(screen)->m_crtc->setGammaRamp(gamma); +} + } diff --git a/plugins/platforms/drm/drm_object_crtc.h b/plugins/platforms/drm/drm_object_crtc.h --- a/plugins/platforms/drm/drm_object_crtc.h +++ b/plugins/platforms/drm/drm_object_crtc.h @@ -25,6 +25,10 @@ namespace KWin { +namespace ColorCorrect { +struct GammaRamp; +} + class DrmBackend; class DrmBuffer; class DrmDumbBuffer; @@ -63,8 +67,14 @@ void flipBuffer(); bool blank(); + int getGammaRampSize() const { + return m_gammaRampSize; + } + bool setGammaRamp(ColorCorrect::GammaRamp &gamma); + private: int m_resIndex; + uint32_t m_gammaRampSize = 0; DrmBuffer *m_currentBuffer = nullptr; DrmBuffer *m_nextBuffer = nullptr; diff --git a/plugins/platforms/drm/drm_object_crtc.cpp b/plugins/platforms/drm/drm_object_crtc.cpp --- a/plugins/platforms/drm/drm_object_crtc.cpp +++ b/plugins/platforms/drm/drm_object_crtc.cpp @@ -23,13 +23,20 @@ #include "drm_buffer.h" #include "logging.h" +#include + namespace KWin { DrmCrtc::DrmCrtc(uint32_t crtc_id, DrmBackend *backend, int resIndex) : DrmObject(crtc_id, backend), m_resIndex(resIndex) { + ScopedDrmPointer<_drmModeCrtc, &drmModeFreeCrtc> modeCrtc(drmModeGetCrtc(backend->fd(), crtc_id)); + if (!modeCrtc) { + return; + } + m_gammaRampSize = modeCrtc->gamma_size; } DrmCrtc::~DrmCrtc() @@ -103,4 +110,10 @@ return false; } +bool DrmCrtc::setGammaRamp(ColorCorrect::GammaRamp &gamma) { + bool isError = drmModeCrtcSetGamma(m_backend->fd(), m_id, gamma.size, + gamma.red, gamma.green, gamma.blue); + return !isError; +} + } diff --git a/plugins/platforms/virtual/virtual_backend.h b/plugins/platforms/virtual/virtual_backend.h --- a/plugins/platforms/virtual/virtual_backend.h +++ b/plugins/platforms/virtual/virtual_backend.h @@ -32,6 +32,11 @@ namespace KWin { +namespace ColorCorrect { +class Manager; +struct GammaRamp; +} + class KWIN_EXPORT VirtualBackend : public Platform { @@ -65,6 +70,8 @@ Q_INVOKABLE void setOutputCount(int count) { m_outputCount = count; + m_gammaSizes = QVector(count, 200); + m_gammaResults = QVector(count, true); } Q_INVOKABLE void setOutputScale(qreal scale) { @@ -84,6 +91,8 @@ void setGbmDevice(gbm_device *device) { m_gbmDevice = device; } + virtual int gammaRampSize(int screen) const override; + virtual bool setGammaRamp(int screen, ColorCorrect::GammaRamp &gamma) override; QVector supportedCompositors() const override { return QVector{OpenGLCompositing, QPainterCompositing}; @@ -100,6 +109,9 @@ QScopedPointer m_screenshotDir; int m_drmFd = -1; gbm_device *m_gbmDevice = nullptr; + + QVector m_gammaSizes = QVector(1, 200); + QVector m_gammaResults = QVector(1, true); }; } diff --git a/plugins/platforms/virtual/virtual_backend.cpp b/plugins/platforms/virtual/virtual_backend.cpp --- a/plugins/platforms/virtual/virtual_backend.cpp +++ b/plugins/platforms/virtual/virtual_backend.cpp @@ -33,6 +33,7 @@ #if HAVE_GBM #include #endif +#include namespace KWin { @@ -50,6 +51,7 @@ } } setSupportsPointerWarping(true); + setSupportsNightColor(true); } VirtualBackend::~VirtualBackend() @@ -98,4 +100,13 @@ return new EglGbmBackend(this); } +int VirtualBackend::gammaRampSize(int screen) const { + return m_gammaSizes[screen]; +} + +bool VirtualBackend::setGammaRamp(int screen, ColorCorrect::GammaRamp &gamma) { + Q_UNUSED(gamma); + return m_gammaResults[screen]; +} + }