summaryrefslogtreecommitdiffstats
path: root/plugins/color
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/color')
-rw-r--r--plugins/color/gcm-edid.c450
-rw-r--r--plugins/color/gcm-edid.h63
-rw-r--r--plugins/color/gcm-self-test.c430
-rw-r--r--plugins/color/gnome-datetime-source.c286
-rw-r--r--plugins/color/gnome-datetime-source.h38
-rw-r--r--plugins/color/gsd-color-calibrate.c412
-rw-r--r--plugins/color/gsd-color-calibrate.h38
-rw-r--r--plugins/color/gsd-color-manager.c502
-rw-r--r--plugins/color/gsd-color-manager.h46
-rw-r--r--plugins/color/gsd-color-profiles.c246
-rw-r--r--plugins/color/gsd-color-profiles.h40
-rw-r--r--plugins/color/gsd-color-state.c1592
-rw-r--r--plugins/color/gsd-color-state.h47
-rw-r--r--plugins/color/gsd-night-light-common.c137
-rw-r--r--plugins/color/gsd-night-light-common.h39
-rw-r--r--plugins/color/gsd-night-light.c801
-rw-r--r--plugins/color/gsd-night-light.h58
-rw-r--r--plugins/color/main.c7
-rw-r--r--plugins/color/meson.build55
-rw-r--r--plugins/color/test-data/LG-L225W-External.binbin0 -> 128 bytes
-rw-r--r--plugins/color/test-data/Lenovo-T61-Internal.binbin0 -> 128 bytes
21 files changed, 5287 insertions, 0 deletions
diff --git a/plugins/color/gcm-edid.c b/plugins/color/gcm-edid.c
new file mode 100644
index 0000000..a00d2ce
--- /dev/null
+++ b/plugins/color/gcm-edid.c
@@ -0,0 +1,450 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Soren Sandmann <sandmann@redhat.com>
+ * Copyright (C) 2009-2011 Richard Hughes <richard@hughsie.com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <glib-object.h>
+#include <math.h>
+#include <string.h>
+#include <gio/gio.h>
+#include <stdlib.h>
+#include <libgnome-desktop/gnome-pnp-ids.h>
+
+#include "gcm-edid.h"
+
+static void gcm_edid_finalize (GObject *object);
+
+struct _GcmEdid
+{
+ GObject parent;
+
+ gchar *monitor_name;
+ gchar *vendor_name;
+ gchar *serial_number;
+ gchar *eisa_id;
+ gchar *checksum;
+ gchar *pnp_id;
+ guint width;
+ guint height;
+ gfloat gamma;
+ CdColorYxy *red;
+ CdColorYxy *green;
+ CdColorYxy *blue;
+ CdColorYxy *white;
+ GnomePnpIds *pnp_ids;
+};
+
+G_DEFINE_TYPE (GcmEdid, gcm_edid, G_TYPE_OBJECT)
+
+#define GCM_EDID_OFFSET_PNPID 0x08
+#define GCM_EDID_OFFSET_SERIAL 0x0c
+#define GCM_EDID_OFFSET_SIZE 0x15
+#define GCM_EDID_OFFSET_GAMMA 0x17
+#define GCM_EDID_OFFSET_DATA_BLOCKS 0x36
+#define GCM_EDID_OFFSET_LAST_BLOCK 0x6c
+#define GCM_EDID_OFFSET_EXTENSION_BLOCK_COUNT 0x7e
+
+#define GCM_DESCRIPTOR_DISPLAY_PRODUCT_NAME 0xfc
+#define GCM_DESCRIPTOR_DISPLAY_PRODUCT_SERIAL_NUMBER 0xff
+#define GCM_DESCRIPTOR_COLOR_MANAGEMENT_DATA 0xf9
+#define GCM_DESCRIPTOR_ALPHANUMERIC_DATA_STRING 0xfe
+#define GCM_DESCRIPTOR_COLOR_POINT 0xfb
+
+GQuark
+gcm_edid_error_quark (void)
+{
+ static GQuark quark = 0;
+ if (!quark)
+ quark = g_quark_from_static_string ("gcm_edid_error");
+ return quark;
+}
+
+const gchar *
+gcm_edid_get_monitor_name (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), NULL);
+ return edid->monitor_name;
+}
+
+const gchar *
+gcm_edid_get_vendor_name (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), NULL);
+
+ if (edid->vendor_name == NULL)
+ edid->vendor_name = gnome_pnp_ids_get_pnp_id (edid->pnp_ids, edid->pnp_id);
+ return edid->vendor_name;
+}
+
+const gchar *
+gcm_edid_get_serial_number (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), NULL);
+ return edid->serial_number;
+}
+
+const gchar *
+gcm_edid_get_eisa_id (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), NULL);
+ return edid->eisa_id;
+}
+
+const gchar *
+gcm_edid_get_checksum (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), NULL);
+ return edid->checksum;
+}
+
+const gchar *
+gcm_edid_get_pnp_id (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), NULL);
+ return edid->pnp_id;
+}
+
+guint
+gcm_edid_get_width (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), 0);
+ return edid->width;
+}
+
+guint
+gcm_edid_get_height (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), 0);
+ return edid->height;
+}
+
+gfloat
+gcm_edid_get_gamma (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), 0.0f);
+ return edid->gamma;
+}
+
+const CdColorYxy *
+gcm_edid_get_red (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), NULL);
+ return edid->red;
+}
+
+const CdColorYxy *
+gcm_edid_get_green (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), NULL);
+ return edid->green;
+}
+
+const CdColorYxy *
+gcm_edid_get_blue (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), NULL);
+ return edid->blue;
+}
+
+const CdColorYxy *
+gcm_edid_get_white (GcmEdid *edid)
+{
+ g_return_val_if_fail (GCM_IS_EDID (edid), NULL);
+ return edid->white;
+}
+
+void
+gcm_edid_reset (GcmEdid *edid)
+{
+ g_return_if_fail (GCM_IS_EDID (edid));
+
+ /* free old data */
+ g_free (edid->monitor_name);
+ g_free (edid->vendor_name);
+ g_free (edid->serial_number);
+ g_free (edid->eisa_id);
+ g_free (edid->checksum);
+
+ /* do not deallocate, just blank */
+ edid->pnp_id[0] = '\0';
+
+ /* set to default values */
+ edid->monitor_name = NULL;
+ edid->vendor_name = NULL;
+ edid->serial_number = NULL;
+ edid->eisa_id = NULL;
+ edid->checksum = NULL;
+ edid->width = 0;
+ edid->height = 0;
+ edid->gamma = 0.0f;
+}
+
+static gint
+gcm_edid_get_bit (gint in, gint bit)
+{
+ return (in & (1 << bit)) >> bit;
+}
+
+/**
+ * gcm_edid_get_bits:
+ **/
+static gint
+gcm_edid_get_bits (gint in, gint begin, gint end)
+{
+ gint mask = (1 << (end - begin + 1)) - 1;
+
+ return (in >> begin) & mask;
+}
+
+/**
+ * gcm_edid_decode_fraction:
+ **/
+static gdouble
+gcm_edid_decode_fraction (gint high, gint low)
+{
+ gdouble result = 0.0;
+ gint i;
+
+ high = (high << 2) | low;
+ for (i = 0; i < 10; ++i)
+ result += gcm_edid_get_bit (high, i) * pow (2, i - 10);
+ return result;
+}
+
+static gchar *
+gcm_edid_parse_string (const guint8 *data)
+{
+ gchar *text;
+ guint i;
+ guint replaced = 0;
+
+ /* this is always 13 bytes, but we can't guarantee it's null
+ * terminated or not junk. */
+ text = g_strndup ((const gchar *) data, 13);
+
+ /* remove insane newline chars */
+ g_strdelimit (text, "\n\r", '\0');
+
+ /* remove spaces */
+ g_strchomp (text);
+
+ /* nothing left? */
+ if (text[0] == '\0') {
+ g_free (text);
+ text = NULL;
+ goto out;
+ }
+
+ /* ensure string is printable */
+ for (i = 0; text[i] != '\0'; i++) {
+ if (!g_ascii_isprint (text[i])) {
+ text[i] = '-';
+ replaced++;
+ }
+ }
+
+ /* if the string is junk, ignore the string */
+ if (replaced > 4) {
+ g_free (text);
+ text = NULL;
+ goto out;
+ }
+out:
+ return text;
+}
+
+gboolean
+gcm_edid_parse (GcmEdid *edid, const guint8 *data, gsize length, GError **error)
+{
+ gboolean ret = TRUE;
+ guint i;
+ guint32 serial;
+ gchar *tmp;
+
+ /* check header */
+ if (length < 128) {
+ g_set_error_literal (error,
+ GCM_EDID_ERROR,
+ GCM_EDID_ERROR_FAILED_TO_PARSE,
+ "EDID length is too small");
+ ret = FALSE;
+ goto out;
+ }
+ if (data[0] != 0x00 || data[1] != 0xff) {
+ g_set_error_literal (error,
+ GCM_EDID_ERROR,
+ GCM_EDID_ERROR_FAILED_TO_PARSE,
+ "Failed to parse EDID header");
+ ret = FALSE;
+ goto out;
+ }
+
+ /* free old data */
+ gcm_edid_reset (edid);
+
+ /* decode the PNP ID from three 5 bit words packed into 2 bytes
+ * /--08--\/--09--\
+ * 7654321076543210
+ * |\---/\---/\---/
+ * R C1 C2 C3 */
+ edid->pnp_id[0] = 'A' + ((data[GCM_EDID_OFFSET_PNPID+0] & 0x7c) / 4) - 1;
+ edid->pnp_id[1] = 'A' + ((data[GCM_EDID_OFFSET_PNPID+0] & 0x3) * 8) + ((data[GCM_EDID_OFFSET_PNPID+1] & 0xe0) / 32) - 1;
+ edid->pnp_id[2] = 'A' + (data[GCM_EDID_OFFSET_PNPID+1] & 0x1f) - 1;
+
+ /* maybe there isn't a ASCII serial number descriptor, so use this instead */
+ serial = (guint32) data[GCM_EDID_OFFSET_SERIAL+0];
+ serial += (guint32) data[GCM_EDID_OFFSET_SERIAL+1] * 0x100;
+ serial += (guint32) data[GCM_EDID_OFFSET_SERIAL+2] * 0x10000;
+ serial += (guint32) data[GCM_EDID_OFFSET_SERIAL+3] * 0x1000000;
+ if (serial > 0)
+ edid->serial_number = g_strdup_printf ("%" G_GUINT32_FORMAT, serial);
+
+ /* get the size */
+ edid->width = data[GCM_EDID_OFFSET_SIZE+0];
+ edid->height = data[GCM_EDID_OFFSET_SIZE+1];
+
+ /* we don't care about aspect */
+ if (edid->width == 0 || edid->height == 0) {
+ edid->width = 0;
+ edid->height = 0;
+ }
+
+ /* get gamma */
+ if (data[GCM_EDID_OFFSET_GAMMA] == 0xff) {
+ edid->gamma = 1.0f;
+ } else {
+ edid->gamma = ((gfloat) data[GCM_EDID_OFFSET_GAMMA] / 100) + 1;
+ }
+
+ /* get color red */
+ edid->red->x = gcm_edid_decode_fraction (data[0x1b], gcm_edid_get_bits (data[0x19], 6, 7));
+ edid->red->y = gcm_edid_decode_fraction (data[0x1c], gcm_edid_get_bits (data[0x19], 4, 5));
+
+ /* get color green */
+ edid->green->x = gcm_edid_decode_fraction (data[0x1d], gcm_edid_get_bits (data[0x19], 2, 3));
+ edid->green->y = gcm_edid_decode_fraction (data[0x1e], gcm_edid_get_bits (data[0x19], 0, 1));
+
+ /* get color blue */
+ edid->blue->x = gcm_edid_decode_fraction (data[0x1f], gcm_edid_get_bits (data[0x1a], 6, 7));
+ edid->blue->y = gcm_edid_decode_fraction (data[0x20], gcm_edid_get_bits (data[0x1a], 4, 5));
+
+ /* get color white */
+ edid->white->x = gcm_edid_decode_fraction (data[0x21], gcm_edid_get_bits (data[0x1a], 2, 3));
+ edid->white->y = gcm_edid_decode_fraction (data[0x22], gcm_edid_get_bits (data[0x1a], 0, 1));
+
+ /* parse EDID data */
+ for (i = GCM_EDID_OFFSET_DATA_BLOCKS;
+ i <= GCM_EDID_OFFSET_LAST_BLOCK;
+ i += 18) {
+ /* ignore pixel clock data */
+ if (data[i] != 0)
+ continue;
+ if (data[i+2] != 0)
+ continue;
+
+ /* any useful blocks? */
+ if (data[i+3] == GCM_DESCRIPTOR_DISPLAY_PRODUCT_NAME) {
+ tmp = gcm_edid_parse_string (&data[i+5]);
+ if (tmp != NULL) {
+ g_free (edid->monitor_name);
+ edid->monitor_name = tmp;
+ }
+ } else if (data[i+3] == GCM_DESCRIPTOR_DISPLAY_PRODUCT_SERIAL_NUMBER) {
+ tmp = gcm_edid_parse_string (&data[i+5]);
+ if (tmp != NULL) {
+ g_free (edid->serial_number);
+ edid->serial_number = tmp;
+ }
+ } else if (data[i+3] == GCM_DESCRIPTOR_COLOR_MANAGEMENT_DATA) {
+ g_warning ("failing to parse color management data");
+ } else if (data[i+3] == GCM_DESCRIPTOR_ALPHANUMERIC_DATA_STRING) {
+ tmp = gcm_edid_parse_string (&data[i+5]);
+ if (tmp != NULL) {
+ g_free (edid->eisa_id);
+ edid->eisa_id = tmp;
+ }
+ } else if (data[i+3] == GCM_DESCRIPTOR_COLOR_POINT) {
+ if (data[i+3+9] != 0xff) {
+ /* extended EDID block(1) which contains
+ * a better gamma value */
+ edid->gamma = ((gfloat) data[i+3+9] / 100) + 1;
+ }
+ if (data[i+3+14] != 0xff) {
+ /* extended EDID block(2) which contains
+ * a better gamma value */
+ edid->gamma = ((gfloat) data[i+3+9] / 100) + 1;
+ }
+ }
+ }
+
+ /* calculate checksum */
+ edid->checksum = g_compute_checksum_for_data (G_CHECKSUM_MD5, data, length);
+out:
+ return ret;
+}
+
+static void
+gcm_edid_class_init (GcmEdidClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = gcm_edid_finalize;
+}
+
+static void
+gcm_edid_init (GcmEdid *edid)
+{
+ edid->pnp_ids = gnome_pnp_ids_new ();
+ edid->pnp_id = g_new0 (gchar, 4);
+ edid->red = cd_color_yxy_new ();
+ edid->green = cd_color_yxy_new ();
+ edid->blue = cd_color_yxy_new ();
+ edid->white = cd_color_yxy_new ();
+}
+
+static void
+gcm_edid_finalize (GObject *object)
+{
+ GcmEdid *edid = GCM_EDID (object);
+
+ g_free (edid->monitor_name);
+ g_free (edid->vendor_name);
+ g_free (edid->serial_number);
+ g_free (edid->eisa_id);
+ g_free (edid->checksum);
+ g_free (edid->pnp_id);
+ cd_color_yxy_free (edid->white);
+ cd_color_yxy_free (edid->red);
+ cd_color_yxy_free (edid->green);
+ cd_color_yxy_free (edid->blue);
+ g_object_unref (edid->pnp_ids);
+
+ G_OBJECT_CLASS (gcm_edid_parent_class)->finalize (object);
+}
+
+GcmEdid *
+gcm_edid_new (void)
+{
+ GcmEdid *edid;
+ edid = g_object_new (GCM_TYPE_EDID, NULL);
+ return GCM_EDID (edid);
+}
+
diff --git a/plugins/color/gcm-edid.h b/plugins/color/gcm-edid.h
new file mode 100644
index 0000000..d50a32e
--- /dev/null
+++ b/plugins/color/gcm-edid.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2009-2010 Richard Hughes <richard@hughsie.com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GCM_EDID_H
+#define __GCM_EDID_H
+
+#include <glib-object.h>
+#include <colord.h>
+
+G_BEGIN_DECLS
+
+#define GCM_TYPE_EDID (gcm_edid_get_type ())
+G_DECLARE_FINAL_TYPE (GcmEdid, gcm_edid, GCM, EDID, GObject)
+
+#define GCM_EDID_ERROR (gcm_edid_error_quark ())
+enum
+{
+ GCM_EDID_ERROR_FAILED_TO_PARSE
+};
+
+GQuark gcm_edid_error_quark (void);
+GcmEdid *gcm_edid_new (void);
+void gcm_edid_reset (GcmEdid *edid);
+gboolean gcm_edid_parse (GcmEdid *edid,
+ const guint8 *data,
+ gsize length,
+ GError **error);
+const gchar *gcm_edid_get_monitor_name (GcmEdid *edid);
+const gchar *gcm_edid_get_vendor_name (GcmEdid *edid);
+const gchar *gcm_edid_get_serial_number (GcmEdid *edid);
+const gchar *gcm_edid_get_eisa_id (GcmEdid *edid);
+const gchar *gcm_edid_get_checksum (GcmEdid *edid);
+const gchar *gcm_edid_get_pnp_id (GcmEdid *edid);
+guint gcm_edid_get_width (GcmEdid *edid);
+guint gcm_edid_get_height (GcmEdid *edid);
+gfloat gcm_edid_get_gamma (GcmEdid *edid);
+const CdColorYxy *gcm_edid_get_red (GcmEdid *edid);
+const CdColorYxy *gcm_edid_get_green (GcmEdid *edid);
+const CdColorYxy *gcm_edid_get_blue (GcmEdid *edid);
+const CdColorYxy *gcm_edid_get_white (GcmEdid *edid);
+
+G_END_DECLS
+
+#endif /* __GCM_EDID_H */
+
diff --git a/plugins/color/gcm-self-test.c b/plugins/color/gcm-self-test.c
new file mode 100644
index 0000000..7ff01f6
--- /dev/null
+++ b/plugins/color/gcm-self-test.c
@@ -0,0 +1,430 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2007-2011 Richard Hughes <richard@hughsie.com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib-object.h>
+#include <stdlib.h>
+
+#include "gcm-edid.h"
+#include "gsd-color-state.h"
+#include "gsd-night-light.h"
+#include "gsd-night-light-common.h"
+
+GMainLoop *mainloop;
+
+static void
+on_notify (GsdNightLight *nlight,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ guint *cnt = (guint *) user_data;
+ (*cnt)++;
+}
+
+static gboolean
+quit_mainloop (gpointer user_data)
+{
+ g_main_loop_quit (mainloop);
+
+ return FALSE;
+}
+
+static void
+gcm_test_night_light (void)
+{
+ gboolean ret;
+ guint active_cnt = 0;
+ guint disabled_until_tmw_cnt = 0;
+ guint sunrise_cnt = 0;
+ guint sunset_cnt = 0;
+ guint temperature_cnt = 0;
+ g_autoptr(GDateTime) datetime_override = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsdNightLight) nlight = NULL;
+ g_autoptr(GSettings) settings = NULL;
+
+ nlight = gsd_night_light_new ();
+ g_assert (GSD_IS_NIGHT_LIGHT (nlight));
+ g_signal_connect (nlight, "notify::active",
+ G_CALLBACK (on_notify), &active_cnt);
+ g_signal_connect (nlight, "notify::sunset",
+ G_CALLBACK (on_notify), &sunset_cnt);
+ g_signal_connect (nlight, "notify::sunrise",
+ G_CALLBACK (on_notify), &sunrise_cnt);
+ g_signal_connect (nlight, "notify::temperature",
+ G_CALLBACK (on_notify), &temperature_cnt);
+ g_signal_connect (nlight, "notify::disabled-until-tmw",
+ G_CALLBACK (on_notify), &disabled_until_tmw_cnt);
+
+ /* hardcode a specific date and time */
+ datetime_override = g_date_time_new_utc (2017, 2, 8, 20, 0, 0);
+ gsd_night_light_set_date_time_now (nlight, datetime_override);
+
+ /* do not start geoclue */
+ gsd_night_light_set_geoclue_enabled (nlight, FALSE);
+
+ /* do not smooth the transition */
+ gsd_night_light_set_smooth_enabled (nlight, FALSE);
+
+ /* switch off */
+ settings = g_settings_new ("org.gnome.settings-daemon.plugins.color");
+ g_settings_set_boolean (settings, "night-light-enabled", FALSE);
+ g_settings_set_uint (settings, "night-light-temperature", 4000);
+
+ /* check default values */
+ g_assert (!gsd_night_light_get_active (nlight));
+ g_assert_cmpint ((gint) gsd_night_light_get_sunrise (nlight), ==, -1);
+ g_assert_cmpint ((gint) gsd_night_light_get_sunset (nlight), ==, -1);
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT);
+ g_assert (!gsd_night_light_get_disabled_until_tmw (nlight));
+
+ /* start module, disabled */
+ ret = gsd_night_light_start (nlight, &error);
+ g_assert_no_error (error);
+ g_assert (ret);
+ g_assert (!gsd_night_light_get_active (nlight));
+ g_assert_cmpint (active_cnt, ==, 0);
+ g_assert_cmpint (sunset_cnt, ==, 0);
+ g_assert_cmpint (sunrise_cnt, ==, 0);
+ g_assert_cmpint (temperature_cnt, ==, 0);
+ g_assert_cmpint (disabled_until_tmw_cnt, ==, 0);
+
+ /* enable automatic mode */
+ g_settings_set_value (settings, "night-light-last-coordinates",
+ g_variant_new ("(dd)", 51.5, -0.1278));
+ g_settings_set_boolean (settings, "night-light-schedule-automatic", TRUE);
+ g_settings_set_boolean (settings, "night-light-enabled", TRUE);
+ g_assert (gsd_night_light_get_active (nlight));
+ g_assert_cmpint (active_cnt, ==, 1);
+ g_assert_cmpint (sunset_cnt, ==, 1);
+ g_assert_cmpint (sunrise_cnt, ==, 1);
+ g_assert_cmpint (temperature_cnt, ==, 1);
+ g_assert_cmpint (disabled_until_tmw_cnt, ==, 0);
+ g_assert_cmpint ((gint) gsd_night_light_get_sunrise (nlight), ==, 7);
+ g_assert_cmpint ((gint) gsd_night_light_get_sunset (nlight), ==, 17);
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, 4000);
+ g_assert (!gsd_night_light_get_disabled_until_tmw (nlight));
+
+ /* disable for one day */
+ gsd_night_light_set_disabled_until_tmw (nlight, TRUE);
+ gsd_night_light_set_disabled_until_tmw (nlight, TRUE);
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT);
+ g_assert (gsd_night_light_get_active (nlight));
+ g_assert (gsd_night_light_get_disabled_until_tmw (nlight));
+ g_assert_cmpint (temperature_cnt, ==, 2);
+ g_assert_cmpint (disabled_until_tmw_cnt, ==, 1);
+
+ /* change our mind */
+ gsd_night_light_set_disabled_until_tmw (nlight, FALSE);
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, 4000);
+ g_assert (gsd_night_light_get_active (nlight));
+ g_assert (!gsd_night_light_get_disabled_until_tmw (nlight));
+ g_assert_cmpint (active_cnt, ==, 1);
+ g_assert_cmpint (temperature_cnt, ==, 3);
+ g_assert_cmpint (disabled_until_tmw_cnt, ==, 2);
+
+ /* enabled manual mode (night shift) */
+ g_settings_set_double (settings, "night-light-schedule-from", 4.0);
+ g_settings_set_double (settings, "night-light-schedule-to", 16.f);
+ g_settings_set_boolean (settings, "night-light-schedule-automatic", FALSE);
+ g_assert_cmpint (active_cnt, ==, 2);
+ g_assert_cmpint (sunset_cnt, ==, 1);
+ g_assert_cmpint (sunrise_cnt, ==, 1);
+ g_assert_cmpint (temperature_cnt, ==, 4);
+ g_assert_cmpint (disabled_until_tmw_cnt, ==, 2);
+ g_assert (!gsd_night_light_get_active (nlight));
+ g_assert_cmpint ((gint) gsd_night_light_get_sunrise (nlight), ==, 7);
+ g_assert_cmpint ((gint) gsd_night_light_get_sunset (nlight), ==, 17);
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT);
+ g_assert (!gsd_night_light_get_disabled_until_tmw (nlight));
+
+ /* disable, with no changes */
+ g_settings_set_boolean (settings, "night-light-enabled", FALSE);
+ g_assert (!gsd_night_light_get_active (nlight));
+ g_assert_cmpint (active_cnt, ==, 2);
+ g_assert_cmpint (sunset_cnt, ==, 1);
+ g_assert_cmpint (sunrise_cnt, ==, 1);
+ g_assert_cmpint (temperature_cnt, ==, 4);
+ g_assert_cmpint (disabled_until_tmw_cnt, ==, 2);
+
+
+ /* Finally, check that cancelling a smooth transition works */
+ gsd_night_light_set_smooth_enabled (nlight, TRUE);
+ /* Enable night light and automatic scheduling */
+ g_settings_set_boolean (settings, "night-light-schedule-automatic", TRUE);
+ g_settings_set_boolean (settings, "night-light-enabled", TRUE);
+ /* It should be active again, and a smooth transition is being done,
+ * so the color temperature is still the default at this point. */
+ g_assert (gsd_night_light_get_active (nlight));
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT);
+
+ /* Turn off immediately, before the first timeout event is fired. */
+ g_settings_set_boolean (settings, "night-light-schedule-automatic", FALSE);
+ g_settings_set_boolean (settings, "night-light-enabled", FALSE);
+ g_assert (!gsd_night_light_get_active (nlight));
+
+ /* Now, sleep for a bit (the smooth transition time is 5 seconds) */
+ g_timeout_add (5000, quit_mainloop, NULL);
+ g_main_loop_run (mainloop);
+
+ /* Ensure that the color temperature is still the default one.*/
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT);
+
+
+ /* Check that disabled until tomorrow resets again correctly. */
+ g_settings_set_double (settings, "night-light-schedule-from", 17.0);
+ g_settings_set_double (settings, "night-light-schedule-to", 7.f);
+ g_settings_set_boolean (settings, "night-light-enabled", TRUE);
+ gsd_night_light_set_disabled_until_tmw (nlight, TRUE);
+
+ /* Move time past midnight */
+ g_clear_pointer (&datetime_override, g_date_time_unref);
+ datetime_override = g_date_time_new_utc (2017, 2, 9, 1, 0, 0);
+ gsd_night_light_set_date_time_now (nlight, datetime_override);
+ g_assert_true (gsd_night_light_get_disabled_until_tmw (nlight));
+
+ /* Move past sunrise */
+ g_clear_pointer (&datetime_override, g_date_time_unref);
+ datetime_override = g_date_time_new_utc (2017, 2, 9, 8, 0, 0);
+ gsd_night_light_set_date_time_now (nlight, datetime_override);
+ g_assert_false (gsd_night_light_get_disabled_until_tmw (nlight));
+
+ gsd_night_light_set_disabled_until_tmw (nlight, TRUE);
+
+ /* Move into night more than 24h in the future */
+ g_clear_pointer (&datetime_override, g_date_time_unref);
+ datetime_override = g_date_time_new_utc (2017, 2, 10, 20, 0, 0);
+ gsd_night_light_set_date_time_now (nlight, datetime_override);
+ g_assert_false (gsd_night_light_get_disabled_until_tmw (nlight));
+
+
+ /* Check that we are always in night mode if from/to are equal. */
+ gsd_night_light_set_smooth_enabled (nlight, FALSE);
+ g_settings_set_double (settings, "night-light-schedule-from", 6.0);
+ g_settings_set_double (settings, "night-light-schedule-to", 6.0);
+ g_settings_set_boolean (settings, "night-light-enabled", TRUE);
+
+ datetime_override = g_date_time_new_utc (2017, 2, 10, 5, 50, 0);
+ gsd_night_light_set_date_time_now (nlight, datetime_override);
+ g_assert (gsd_night_light_get_active (nlight));
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, 4000);
+
+ datetime_override = g_date_time_new_utc (2017, 2, 10, 6, 0, 0);
+ gsd_night_light_set_date_time_now (nlight, datetime_override);
+ g_assert (gsd_night_light_get_active (nlight));
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, 4000);
+
+ datetime_override = g_date_time_new_utc (2017, 2, 10, 6, 10, 0);
+ gsd_night_light_set_date_time_now (nlight, datetime_override);
+ g_assert (gsd_night_light_get_active (nlight));
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, 4000);
+
+
+ /* Check that the smearing time is lowered correctly when the times are close. */
+ g_settings_set_double (settings, "night-light-schedule-from", 6.0);
+ g_settings_set_double (settings, "night-light-schedule-to", 6.1);
+
+ /* Not enabled 10 minutes before sunset */
+ datetime_override = g_date_time_new_utc (2017, 2, 10, 5, 50, 0);
+ gsd_night_light_set_date_time_now (nlight, datetime_override);
+ g_assert_false (gsd_night_light_get_active (nlight));
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT);
+
+ /* Not enabled >10 minutes after sunrise */
+ datetime_override = g_date_time_new_utc (2017, 2, 10, 6, 20, 0);
+ gsd_night_light_set_date_time_now (nlight, datetime_override);
+ g_assert_false (gsd_night_light_get_active (nlight));
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), ==, GSD_COLOR_TEMPERATURE_DEFAULT);
+
+ /* ~50% smeared 3 min before sunrise (sunrise at 6 past) */
+ datetime_override = g_date_time_new_utc (2017, 2, 10, 6, 3, 0);
+ gsd_night_light_set_date_time_now (nlight, datetime_override);
+ g_assert_true (gsd_night_light_get_active (nlight));
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), <=, (GSD_COLOR_TEMPERATURE_DEFAULT + 4000) / 2 + 20);
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), >=, (GSD_COLOR_TEMPERATURE_DEFAULT + 4000) / 2 - 20);
+
+ /* ~50% smeared 3 min before sunset (sunset at 6 past) */
+ g_settings_set_double (settings, "night-light-schedule-from", 6.1);
+ g_settings_set_double (settings, "night-light-schedule-to", 6.0);
+ datetime_override = g_date_time_new_utc (2017, 2, 10, 6, 3, 0);
+ gsd_night_light_set_date_time_now (nlight, datetime_override);
+ g_assert_true (gsd_night_light_get_active (nlight));
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), <=, (GSD_COLOR_TEMPERATURE_DEFAULT + 4000) / 2 + 20);
+ g_assert_cmpint (gsd_night_light_get_temperature (nlight), >=, (GSD_COLOR_TEMPERATURE_DEFAULT + 4000) / 2 - 20);
+}
+
+static const gboolean
+gcm_vendor_is_goldstar (const char * const vendor) {
+ if (g_strcmp0 (vendor, "Goldstar Company Ltd") == 0)
+ return TRUE;
+ /* Goldstar was changed to LG in hwdb (systemd) 240.
+ * https://github.com/systemd/systemd/commit/c6d7a5e9a3836f8
+ */
+ if (g_strcmp0 (vendor, "LG Electronics") == 0)
+ return TRUE;
+ return FALSE;
+}
+
+static void
+gcm_test_edid_func (void)
+{
+ GcmEdid *edid;
+ gchar *data;
+ gboolean ret;
+ GError *error = NULL;
+ gsize length = 0;
+
+ edid = gcm_edid_new ();
+ g_assert (edid != NULL);
+
+ /* LG 21" LCD panel */
+ ret = g_file_get_contents (TESTDATADIR "/LG-L225W-External.bin",
+ &data, &length, &error);
+ g_assert_no_error (error);
+ g_assert (ret);
+ ret = gcm_edid_parse (edid, (const guint8 *) data, length, &error);
+ g_assert_no_error (error);
+ g_assert (ret);
+
+ g_assert_cmpstr (gcm_edid_get_monitor_name (edid), ==, "L225W");
+ g_assert_true (gcm_vendor_is_goldstar (gcm_edid_get_vendor_name (edid)));
+ g_assert_cmpstr (gcm_edid_get_serial_number (edid), ==, "34398");
+ g_assert_cmpstr (gcm_edid_get_eisa_id (edid), ==, NULL);
+ g_assert_cmpstr (gcm_edid_get_checksum (edid), ==, "0bb44865bb29984a4bae620656c31368");
+ g_assert_cmpstr (gcm_edid_get_pnp_id (edid), ==, "GSM");
+ g_assert_cmpint (gcm_edid_get_height (edid), ==, 30);
+ g_assert_cmpint (gcm_edid_get_width (edid), ==, 47);
+ g_assert_cmpfloat (gcm_edid_get_gamma (edid), >=, 2.2f - 0.01);
+ g_assert_cmpfloat (gcm_edid_get_gamma (edid), <, 2.2f + 0.01);
+ g_free (data);
+
+ /* Lenovo T61 internal Panel */
+ ret = g_file_get_contents (TESTDATADIR "/Lenovo-T61-Internal.bin",
+ &data, &length, &error);
+ g_assert_no_error (error);
+ g_assert (ret);
+ ret = gcm_edid_parse (edid, (const guint8 *) data, length, &error);
+ g_assert_no_error (error);
+ g_assert (ret);
+
+ g_assert_cmpstr (gcm_edid_get_monitor_name (edid), ==, NULL);
+ g_assert_cmpstr (gcm_edid_get_vendor_name (edid), ==, "IBM Brasil");
+ g_assert_cmpstr (gcm_edid_get_serial_number (edid), ==, NULL);
+ g_assert_cmpstr (gcm_edid_get_eisa_id (edid), ==, "LTN154P2-L05");
+ g_assert_cmpstr (gcm_edid_get_checksum (edid), ==, "e1865128c7cd5e5ed49ecfc8102f6f9c");
+ g_assert_cmpstr (gcm_edid_get_pnp_id (edid), ==, "IBM");
+ g_assert_cmpint (gcm_edid_get_height (edid), ==, 21);
+ g_assert_cmpint (gcm_edid_get_width (edid), ==, 33);
+ g_assert_cmpfloat (gcm_edid_get_gamma (edid), >=, 2.2f - 0.01);
+ g_assert_cmpfloat (gcm_edid_get_gamma (edid), <, 2.2f + 0.01);
+ g_free (data);
+
+ g_object_unref (edid);
+}
+
+static void
+gcm_test_sunset_sunrise (void)
+{
+ gdouble sunrise;
+ gdouble sunrise_actual = 7.6;
+ gdouble sunset;
+ gdouble sunset_actual = 16.8;
+ g_autoptr(GDateTime) dt = g_date_time_new_utc (2007, 2, 1, 0, 0, 0);
+
+ /* get for London, today */
+ gsd_night_light_get_sunrise_sunset (dt, 51.5, -0.1278, &sunrise, &sunset);
+ g_assert_cmpfloat (sunrise, <, sunrise_actual + 0.1);
+ g_assert_cmpfloat (sunrise, >, sunrise_actual - 0.1);
+ g_assert_cmpfloat (sunset, <, sunset_actual + 0.1);
+ g_assert_cmpfloat (sunset, >, sunset_actual - 0.1);
+}
+
+static void
+gcm_test_sunset_sunrise_fractional_timezone (void)
+{
+ gdouble sunrise;
+ gdouble sunrise_actual = 7.6 + 1.5;
+ gdouble sunset;
+ gdouble sunset_actual = 16.8 + 1.5;
+ g_autoptr(GTimeZone) tz = NULL;
+ g_autoptr(GDateTime) dt = NULL;
+
+ tz = g_time_zone_new ("+01:30");
+ dt = g_date_time_new (tz, 2007, 2, 1, 0, 0, 0);
+
+ /* get for our made up timezone, today */
+ gsd_night_light_get_sunrise_sunset (dt, 51.5, -0.1278, &sunrise, &sunset);
+ g_assert_cmpfloat (sunrise, <, sunrise_actual + 0.1);
+ g_assert_cmpfloat (sunrise, >, sunrise_actual - 0.1);
+ g_assert_cmpfloat (sunset, <, sunset_actual + 0.1);
+ g_assert_cmpfloat (sunset, >, sunset_actual - 0.1);
+}
+
+static void
+gcm_test_frac_day (void)
+{
+ g_autoptr(GDateTime) dt = g_date_time_new_utc (2007, 2, 1, 12, 59, 59);
+ gdouble fd;
+ gdouble fd_actual = 12.99;
+
+ /* test for 12:59:59 */
+ fd = gsd_night_light_frac_day_from_dt (dt);
+ g_assert_cmpfloat (fd, >, fd_actual - 0.01);
+ g_assert_cmpfloat (fd, <, fd_actual + 0.01);
+
+ /* test same day */
+ g_assert_true (gsd_night_light_frac_day_is_between (12, 6, 20));
+ g_assert_false (gsd_night_light_frac_day_is_between (5, 6, 20));
+ g_assert_true (gsd_night_light_frac_day_is_between (12, 0, 24));
+ g_assert_true (gsd_night_light_frac_day_is_between (12, -1, 25));
+
+ /* test rollover to next day */
+ g_assert_true (gsd_night_light_frac_day_is_between (23, 20, 6));
+ g_assert_false (gsd_night_light_frac_day_is_between (12, 20, 6));
+
+ /* test rollover to the previous day */
+ g_assert_true (gsd_night_light_frac_day_is_between (5, 16, 8));
+
+ /* test equality */
+ g_assert_true (gsd_night_light_frac_day_is_between (12, 0.5, 24.5));
+ g_assert_true (gsd_night_light_frac_day_is_between (0.5, 0.5, 0.5));
+}
+
+int
+main (int argc, char **argv)
+{
+ g_setenv ("GSETTINGS_BACKEND", "memory", TRUE);
+
+ g_test_init (&argc, &argv, NULL);
+
+ mainloop = g_main_loop_new (g_main_context_default (), FALSE);
+
+ g_test_add_func ("/color/edid", gcm_test_edid_func);
+ g_test_add_func ("/color/sunset-sunrise", gcm_test_sunset_sunrise);
+ g_test_add_func ("/color/sunset-sunrise/fractional-timezone", gcm_test_sunset_sunrise_fractional_timezone);
+ g_test_add_func ("/color/fractional-day", gcm_test_frac_day);
+ g_test_add_func ("/color/night-light", gcm_test_night_light);
+
+ return g_test_run ();
+}
+
diff --git a/plugins/color/gnome-datetime-source.c b/plugins/color/gnome-datetime-source.c
new file mode 100644
index 0000000..287ba2d
--- /dev/null
+++ b/plugins/color/gnome-datetime-source.c
@@ -0,0 +1,286 @@
+/* -*- mode: C; c-file-style: "linux"; indent-tabs-mode: t -*-
+ * gdatetime-source.c - copy&paste from https://bugzilla.gnome.org/show_bug.cgi?id=655129
+ *
+ * Copyright (C) 2011 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library 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
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ *
+ * Author: Colin Walters <walters@verbum.org>
+ */
+
+#include "config.h"
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include "gnome-datetime-source.h"
+
+#if HAVE_TIMERFD
+#include <sys/timerfd.h>
+#include <unistd.h>
+#include <string.h>
+#endif
+
+typedef struct _GDateTimeSource GDateTimeSource;
+struct _GDateTimeSource
+{
+ GSource source;
+
+ gint64 real_expiration;
+ gint64 wakeup_expiration;
+
+ gboolean cancel_on_set : 1;
+ gboolean initially_expired : 1;
+
+ GPollFD pollfd;
+};
+
+static inline void
+g_datetime_source_reschedule (GDateTimeSource *datetime_source,
+ gint64 from_monotonic)
+{
+ datetime_source->wakeup_expiration = from_monotonic + G_TIME_SPAN_SECOND;
+}
+
+static gboolean
+g_datetime_source_is_expired (GDateTimeSource *datetime_source)
+{
+ gint64 real_now;
+ gint64 monotonic_now;
+
+ real_now = g_get_real_time ();
+ monotonic_now = g_source_get_time ((GSource*)datetime_source);
+
+ if (datetime_source->initially_expired)
+ return TRUE;
+
+ if (datetime_source->real_expiration <= real_now)
+ return TRUE;
+
+ /* We can't really detect without system support when things
+ * change; so just trigger every second (i.e. our wakeup
+ * expiration)
+ */
+ if (datetime_source->cancel_on_set && monotonic_now >= datetime_source->wakeup_expiration)
+ return TRUE;
+
+ return FALSE;
+}
+
+/* In prepare, we're just checking the monotonic time against
+ * our projected wakeup.
+ */
+static gboolean
+g_datetime_source_prepare (GSource *source,
+ gint *timeout)
+{
+ GDateTimeSource *datetime_source = (GDateTimeSource*)source;
+ gint64 monotonic_now;
+
+#if HAVE_TIMERFD
+ if (datetime_source->pollfd.fd != -1) {
+ *timeout = -1;
+ return datetime_source->initially_expired; /* Should be TRUE at most one time, FALSE forever after */
+ }
+#endif
+
+ monotonic_now = g_source_get_time (source);
+
+ if (monotonic_now < datetime_source->wakeup_expiration) {
+ /* Round up to ensure that we don't try again too early */
+ *timeout = (datetime_source->wakeup_expiration - monotonic_now + 999) / 1000;
+ return FALSE;
+ }
+
+ *timeout = 0;
+ return g_datetime_source_is_expired (datetime_source);
+}
+
+/* In check, we're looking at the wall clock.
+ */
+static gboolean
+g_datetime_source_check (GSource *source)
+{
+ GDateTimeSource *datetime_source = (GDateTimeSource*)source;
+
+#if HAVE_TIMERFD
+ if (datetime_source->pollfd.fd != -1)
+ return datetime_source->pollfd.revents != 0;
+#endif
+
+ if (g_datetime_source_is_expired (datetime_source))
+ return TRUE;
+
+ g_datetime_source_reschedule (datetime_source, g_source_get_time (source));
+
+ return FALSE;
+}
+
+static gboolean
+g_datetime_source_dispatch (GSource *source,
+ GSourceFunc callback,
+ gpointer user_data)
+{
+ GDateTimeSource *datetime_source = (GDateTimeSource*)source;
+
+ datetime_source->initially_expired = FALSE;
+
+ if (!callback) {
+ g_warning ("Timeout source dispatched without callback\n"
+ "You must call g_source_set_callback().");
+ return FALSE;
+ }
+
+ (callback) (user_data);
+
+ /* Always false as this source is documented to run once */
+ return FALSE;
+}
+
+static void
+g_datetime_source_finalize (GSource *source)
+{
+#if HAVE_TIMERFD
+ GDateTimeSource *datetime_source = (GDateTimeSource*)source;
+ if (datetime_source->pollfd.fd != -1)
+ close (datetime_source->pollfd.fd);
+#endif
+}
+
+static GSourceFuncs g_datetime_source_funcs = {
+ g_datetime_source_prepare,
+ g_datetime_source_check,
+ g_datetime_source_dispatch,
+ g_datetime_source_finalize
+};
+
+#if HAVE_TIMERFD
+static gboolean
+g_datetime_source_init_timerfd (GDateTimeSource *datetime_source,
+ gint64 expected_now_seconds,
+ gint64 unix_seconds)
+{
+ struct itimerspec its;
+ int settime_flags;
+
+ datetime_source->pollfd.fd = timerfd_create (CLOCK_REALTIME, TFD_CLOEXEC);
+ if (datetime_source->pollfd.fd == -1)
+ return FALSE;
+
+ memset (&its, 0, sizeof (its));
+ its.it_value.tv_sec = (time_t) unix_seconds;
+
+ /* http://article.gmane.org/gmane.linux.kernel/1132138 */
+#ifndef TFD_TIMER_CANCEL_ON_SET
+#define TFD_TIMER_CANCEL_ON_SET (1 << 1)
+#endif
+
+ settime_flags = TFD_TIMER_ABSTIME;
+ if (datetime_source->cancel_on_set)
+ settime_flags |= TFD_TIMER_CANCEL_ON_SET;
+
+ if (timerfd_settime (datetime_source->pollfd.fd, settime_flags, &its, NULL) < 0) {
+ close (datetime_source->pollfd.fd);
+ datetime_source->pollfd.fd = -1;
+ return FALSE;
+ }
+
+ /* Now we need to check that the clock didn't go backwards before we
+ * had the timerfd set up. See
+ * https://bugzilla.gnome.org/show_bug.cgi?id=655129
+ */
+ clock_gettime (CLOCK_REALTIME, &its.it_value);
+ if (its.it_value.tv_sec < expected_now_seconds)
+ datetime_source->initially_expired = TRUE;
+
+ datetime_source->pollfd.events = G_IO_IN;
+
+ g_source_add_poll ((GSource*) datetime_source, &datetime_source->pollfd);
+
+ return TRUE;
+}
+#endif
+
+/**
+ * _gnome_date_time_source_new:
+ * @now: The expected current time
+ * @expiry: Time to await
+ * @cancel_on_set: Also invoke callback if the system clock changes discontiguously
+ *
+ * This function is designed for programs that want to schedule an
+ * event based on real (wall clock) time, as returned by
+ * g_get_real_time(). For example, HOUR:MINUTE wall-clock displays
+ * and calendaring software. The callback will be invoked when the
+ * specified wall clock time @expiry is reached. This includes
+ * events such as the system clock being set past the given time.
+ *
+ * Compare versus g_timeout_source_new() which is defined to use
+ * monotonic time as returned by g_get_monotonic_time().
+ *
+ * The parameter @now is necessary to avoid a race condition in
+ * between getting the current time and calling this function.
+ *
+ * If @cancel_on_set is given, the callback will also be invoked at
+ * most a second after the system clock is changed. This includes
+ * being set backwards or forwards, and system
+ * resume from suspend. Not all operating systems allow detecting all
+ * relevant events efficiently - this function may cause the process
+ * to wake up once a second in those cases.
+ *
+ * A wall clock display should use @cancel_on_set; a calendaring
+ * program shouldn't need to.
+ *
+ * Note that the return value from the associated callback will be
+ * ignored; this is a one time watch.
+ *
+ * <note><para>This function currently does not detect time zone
+ * changes. On Linux, your program should also monitor the
+ * <literal>/etc/timezone</literal> file using
+ * #GFileMonitor.</para></note>
+ *
+ * <example id="gdatetime-example-watch"><title>Clock example</title><programlisting><xi:include xmlns:xi="http://www.w3.org/2001/XInclude" parse="text" href="../../../../glib/tests/glib-clock.c"><xi:fallback>FIXME: MISSING XINCLUDE CONTENT</xi:fallback></xi:include></programlisting></example>
+ *
+ * Return value: A newly-constructed #GSource
+ *
+ * Since: 2.30
+ **/
+GSource *
+_gnome_datetime_source_new (GDateTime *now,
+ GDateTime *expiry,
+ gboolean cancel_on_set)
+{
+ GDateTimeSource *datetime_source;
+ gint64 unix_seconds;
+
+ unix_seconds = g_date_time_to_unix (expiry);
+
+ datetime_source = (GDateTimeSource*) g_source_new (&g_datetime_source_funcs, sizeof (GDateTimeSource));
+
+ datetime_source->cancel_on_set = cancel_on_set;
+
+#if HAVE_TIMERFD
+ {
+ gint64 expected_now_seconds = g_date_time_to_unix (now);
+ if (g_datetime_source_init_timerfd (datetime_source, expected_now_seconds, unix_seconds))
+ return (GSource*)datetime_source;
+ /* Fall through to non-timerfd code */
+ }
+#endif
+
+ datetime_source->real_expiration = unix_seconds * 1000000;
+ g_datetime_source_reschedule (datetime_source, g_get_monotonic_time ());
+
+ return (GSource*)datetime_source;
+}
+
diff --git a/plugins/color/gnome-datetime-source.h b/plugins/color/gnome-datetime-source.h
new file mode 100644
index 0000000..e9ecbf0
--- /dev/null
+++ b/plugins/color/gnome-datetime-source.h
@@ -0,0 +1,38 @@
+/* gnome-rr.h
+ *
+ * Copyright 2011, Red Hat, Inc.
+ *
+ * This file is part of the Gnome Library.
+ *
+ * The Gnome Library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * The Gnome Library 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
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with the Gnome Library; see the file COPYING.LIB. If not,
+ * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ *
+ * Author: Colin Walters <walters@verbum.org>
+ */
+
+#ifndef GNOME_DATETIME_SOURCE_H
+#define GNOME_DATETIME_SOURCE_H
+
+#ifndef GNOME_DESKTOP_USE_UNSTABLE_API
+#error This is unstable API. You must define GNOME_DESKTOP_USE_UNSTABLE_API
+#endif
+
+#include <glib.h>
+
+GSource *_gnome_datetime_source_new (GDateTime *now,
+ GDateTime *expiry,
+ gboolean cancel_on_set);
+
+#endif /* GNOME_DATETIME_SOURCE_H */
diff --git a/plugins/color/gsd-color-calibrate.c b/plugins/color/gsd-color-calibrate.c
new file mode 100644
index 0000000..f642e18
--- /dev/null
+++ b/plugins/color/gsd-color-calibrate.c
@@ -0,0 +1,412 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
+ * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <colord.h>
+#include <libnotify/notify.h>
+#include <canberra-gtk.h>
+
+#include "gsd-color-calibrate.h"
+
+#define GCM_SESSION_NOTIFY_TIMEOUT 30000 /* ms */
+#define GCM_SETTINGS_RECALIBRATE_PRINTER_THRESHOLD "recalibrate-printer-threshold"
+#define GCM_SETTINGS_RECALIBRATE_DISPLAY_THRESHOLD "recalibrate-display-threshold"
+
+struct _GsdColorCalibrate
+{
+ GObject parent;
+
+ CdClient *client;
+ GSettings *settings;
+};
+
+static void gsd_color_calibrate_class_init (GsdColorCalibrateClass *klass);
+static void gsd_color_calibrate_init (GsdColorCalibrate *color_calibrate);
+static void gsd_color_calibrate_finalize (GObject *object);
+
+G_DEFINE_TYPE (GsdColorCalibrate, gsd_color_calibrate, G_TYPE_OBJECT)
+
+typedef struct {
+ GsdColorCalibrate *calibrate;
+ CdProfile *profile;
+ CdDevice *device;
+ guint32 output_id;
+} GcmSessionAsyncHelper;
+
+static void
+gcm_session_async_helper_free (GcmSessionAsyncHelper *helper)
+{
+ if (helper->calibrate != NULL)
+ g_object_unref (helper->calibrate);
+ if (helper->profile != NULL)
+ g_object_unref (helper->profile);
+ if (helper->device != NULL)
+ g_object_unref (helper->device);
+ g_free (helper);
+}
+
+static void
+gcm_session_exec_control_center (GsdColorCalibrate *calibrate)
+{
+ gboolean ret;
+ GError *error = NULL;
+ GAppInfo *app_info;
+ GdkAppLaunchContext *launch_context;
+
+ /* setup the launch context so the startup notification is correct */
+ launch_context = gdk_display_get_app_launch_context (gdk_display_get_default ());
+ app_info = g_app_info_create_from_commandline (BINDIR "/gnome-control-center color",
+ "gnome-control-center",
+ G_APP_INFO_CREATE_SUPPORTS_STARTUP_NOTIFICATION,
+ &error);
+ if (app_info == NULL) {
+ g_warning ("failed to create application info: %s",
+ error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ /* launch gnome-control-center */
+ ret = g_app_info_launch (app_info,
+ NULL,
+ G_APP_LAUNCH_CONTEXT (launch_context),
+ &error);
+ if (!ret) {
+ g_warning ("failed to launch gnome-control-center: %s",
+ error->message);
+ g_error_free (error);
+ goto out;
+ }
+out:
+ g_object_unref (launch_context);
+ if (app_info != NULL)
+ g_object_unref (app_info);
+}
+
+static void
+gcm_session_notify_cb (NotifyNotification *notification,
+ gchar *action,
+ gpointer user_data)
+{
+ GsdColorCalibrate *calibrate = GSD_COLOR_CALIBRATE (user_data);
+
+ if (g_strcmp0 (action, "recalibrate") == 0) {
+ notify_notification_close (notification, NULL);
+ gcm_session_exec_control_center (calibrate);
+ }
+}
+
+static void
+closed_cb (NotifyNotification *notification, gpointer data)
+{
+ g_object_unref (notification);
+}
+
+static gboolean
+gcm_session_notify_recalibrate (GsdColorCalibrate *calibrate,
+ const gchar *title,
+ const gchar *message,
+ CdDeviceKind kind)
+{
+ gboolean ret;
+ GError *error = NULL;
+ NotifyNotification *notification;
+
+ /* show a bubble */
+ notification = notify_notification_new (title, message, "preferences-color");
+ notify_notification_set_timeout (notification, GCM_SESSION_NOTIFY_TIMEOUT);
+ notify_notification_set_urgency (notification, NOTIFY_URGENCY_LOW);
+ notify_notification_set_app_name (notification, _("Color"));
+ notify_notification_set_hint_string (notification, "desktop-entry", "gnome-color-panel");
+
+ notify_notification_add_action (notification,
+ "recalibrate",
+ /* TRANSLATORS: button: this is to open GCM */
+ _("Recalibrate now"),
+ gcm_session_notify_cb,
+ calibrate, NULL);
+
+ g_signal_connect (notification, "closed", G_CALLBACK (closed_cb), NULL);
+ ret = notify_notification_show (notification, &error);
+ if (!ret) {
+ g_warning ("failed to show notification: %s",
+ error->message);
+ g_error_free (error);
+ }
+ return ret;
+}
+
+static gchar *
+gcm_session_device_get_title (CdDevice *device)
+{
+ const gchar *vendor;
+ const gchar *model;
+
+ model = cd_device_get_model (device);
+ vendor = cd_device_get_vendor (device);
+ if (model != NULL && vendor != NULL)
+ return g_strdup_printf ("%s - %s", vendor, model);
+ if (vendor != NULL)
+ return g_strdup (vendor);
+ if (model != NULL)
+ return g_strdup (model);
+ return g_strdup (cd_device_get_id (device));
+}
+
+static void
+gcm_session_notify_device (GsdColorCalibrate *calibrate, CdDevice *device)
+{
+ CdDeviceKind kind;
+ const gchar *title;
+ gchar *device_title = NULL;
+ gchar *message;
+ guint threshold;
+ glong since;
+
+ /* TRANSLATORS: this is when the device has not been recalibrated in a while */
+ title = _("Recalibration required");
+ device_title = gcm_session_device_get_title (device);
+
+ /* check we care */
+ kind = cd_device_get_kind (device);
+ if (kind == CD_DEVICE_KIND_DISPLAY) {
+
+ /* get from GSettings */
+ threshold = g_settings_get_uint (calibrate->settings,
+ GCM_SETTINGS_RECALIBRATE_DISPLAY_THRESHOLD);
+
+ /* TRANSLATORS: this is when the display has not been recalibrated in a while */
+ message = g_strdup_printf (_("The display “%s” should be recalibrated soon."),
+ device_title);
+ } else {
+
+ /* get from GSettings */
+ threshold = g_settings_get_uint (calibrate->settings,
+ GCM_SETTINGS_RECALIBRATE_PRINTER_THRESHOLD);
+
+ /* TRANSLATORS: this is when the printer has not been recalibrated in a while */
+ message = g_strdup_printf (_("The printer “%s” should be recalibrated soon."),
+ device_title);
+ }
+
+ /* check if we need to notify */
+ since = (g_get_real_time () - cd_device_get_modified (device)) / G_USEC_PER_SEC;
+ if (threshold > since)
+ gcm_session_notify_recalibrate (calibrate, title, message, kind);
+ g_free (device_title);
+ g_free (message);
+}
+
+static void
+gcm_session_profile_connect_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ const gchar *filename;
+ gboolean ret;
+ gchar *basename = NULL;
+ const gchar *data_source;
+ GError *error = NULL;
+ CdProfile *profile = CD_PROFILE (object);
+ GcmSessionAsyncHelper *helper = (GcmSessionAsyncHelper *) user_data;
+ GsdColorCalibrate *calibrate = GSD_COLOR_CALIBRATE (helper->calibrate);
+
+ ret = cd_profile_connect_finish (profile,
+ res,
+ &error);
+ if (!ret) {
+ g_warning ("failed to connect to profile: %s",
+ error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ /* ensure it's a profile generated by us */
+ data_source = cd_profile_get_metadata_item (profile,
+ CD_PROFILE_METADATA_DATA_SOURCE);
+ if (data_source == NULL) {
+
+ /* existing profiles from gnome-color-calibrate < 3.1
+ * won't have the extra metadata values added */
+ filename = cd_profile_get_filename (profile);
+ if (filename == NULL)
+ goto out;
+ basename = g_path_get_basename (filename);
+ if (!g_str_has_prefix (basename, "GCM")) {
+ g_debug ("not a GCM profile for %s: %s",
+ cd_device_get_id (helper->device), filename);
+ goto out;
+ }
+
+ /* ensure it's been created from a calibration, rather than from
+ * auto-EDID */
+ } else if (g_strcmp0 (data_source,
+ CD_PROFILE_METADATA_DATA_SOURCE_CALIB) != 0) {
+ g_debug ("not a calib profile for %s",
+ cd_device_get_id (helper->device));
+ goto out;
+ }
+
+ /* handle device */
+ gcm_session_notify_device (calibrate, helper->device);
+out:
+ gcm_session_async_helper_free (helper);
+ g_free (basename);
+}
+
+static void
+gcm_session_device_connect_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ gboolean ret;
+ GError *error = NULL;
+ CdDeviceKind kind;
+ CdProfile *profile = NULL;
+ CdDevice *device = CD_DEVICE (object);
+ GsdColorCalibrate *calibrate = GSD_COLOR_CALIBRATE (user_data);
+ GcmSessionAsyncHelper *helper;
+
+ ret = cd_device_connect_finish (device,
+ res,
+ &error);
+ if (!ret) {
+ g_warning ("failed to connect to device: %s",
+ error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ /* check we care */
+ kind = cd_device_get_kind (device);
+ if (kind != CD_DEVICE_KIND_DISPLAY &&
+ kind != CD_DEVICE_KIND_PRINTER)
+ goto out;
+
+ /* ensure we have a profile */
+ profile = cd_device_get_default_profile (device);
+ if (profile == NULL) {
+ g_debug ("no profile set for %s", cd_device_get_id (device));
+ goto out;
+ }
+
+ /* connect to the profile */
+ helper = g_new0 (GcmSessionAsyncHelper, 1);
+ helper->calibrate = g_object_ref (calibrate);
+ helper->device = g_object_ref (device);
+ cd_profile_connect (profile,
+ NULL,
+ gcm_session_profile_connect_cb,
+ helper);
+out:
+ if (profile != NULL)
+ g_object_unref (profile);
+}
+
+static void
+gcm_session_device_added_notify_cb (CdClient *client,
+ CdDevice *device,
+ GsdColorCalibrate *calibrate)
+{
+ /* connect to the device to get properties */
+ cd_device_connect (device,
+ NULL,
+ gcm_session_device_connect_cb,
+ calibrate);
+}
+
+static void
+gcm_session_sensor_added_cb (CdClient *client,
+ CdSensor *sensor,
+ GsdColorCalibrate *calibrate)
+{
+ ca_context_play (ca_gtk_context_get (), 0,
+ CA_PROP_EVENT_ID, "device-added",
+ /* TRANSLATORS: this is the application name */
+ CA_PROP_APPLICATION_NAME, _("GNOME Settings Daemon Color Plugin"),
+ /* TRANSLATORS: this is a sound description */
+ CA_PROP_EVENT_DESCRIPTION, _("Color calibration device added"), NULL);
+
+ /* open up the color prefs window */
+ gcm_session_exec_control_center (calibrate);
+}
+
+static void
+gcm_session_sensor_removed_cb (CdClient *client,
+ CdSensor *sensor,
+ GsdColorCalibrate *calibrate)
+{
+ ca_context_play (ca_gtk_context_get (), 0,
+ CA_PROP_EVENT_ID, "device-removed",
+ /* TRANSLATORS: this is the application name */
+ CA_PROP_APPLICATION_NAME, _("GNOME Settings Daemon Color Plugin"),
+ /* TRANSLATORS: this is a sound description */
+ CA_PROP_EVENT_DESCRIPTION, _("Color calibration device removed"), NULL);
+}
+
+static void
+gsd_color_calibrate_class_init (GsdColorCalibrateClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gsd_color_calibrate_finalize;
+}
+
+static void
+gsd_color_calibrate_init (GsdColorCalibrate *calibrate)
+{
+ calibrate->settings = g_settings_new ("org.gnome.settings-daemon.plugins.color");
+ calibrate->client = cd_client_new ();
+ g_signal_connect (calibrate->client, "device-added",
+ G_CALLBACK (gcm_session_device_added_notify_cb),
+ calibrate);
+ g_signal_connect (calibrate->client, "sensor-added",
+ G_CALLBACK (gcm_session_sensor_added_cb),
+ calibrate);
+ g_signal_connect (calibrate->client, "sensor-removed",
+ G_CALLBACK (gcm_session_sensor_removed_cb),
+ calibrate);
+}
+
+static void
+gsd_color_calibrate_finalize (GObject *object)
+{
+ GsdColorCalibrate *calibrate;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GSD_IS_COLOR_CALIBRATE (object));
+
+ calibrate = GSD_COLOR_CALIBRATE (object);
+
+ g_clear_object (&calibrate->settings);
+ g_clear_object (&calibrate->client);
+
+ G_OBJECT_CLASS (gsd_color_calibrate_parent_class)->finalize (object);
+}
+
+GsdColorCalibrate *
+gsd_color_calibrate_new (void)
+{
+ GsdColorCalibrate *calibrate;
+ calibrate = g_object_new (GSD_TYPE_COLOR_CALIBRATE, NULL);
+ return GSD_COLOR_CALIBRATE (calibrate);
+}
diff --git a/plugins/color/gsd-color-calibrate.h b/plugins/color/gsd-color-calibrate.h
new file mode 100644
index 0000000..8980098
--- /dev/null
+++ b/plugins/color/gsd-color-calibrate.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
+ * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef __GSD_COLOR_CALIBRATE_H
+#define __GSD_COLOR_CALIBRATE_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GSD_TYPE_COLOR_CALIBRATE (gsd_color_calibrate_get_type ())
+G_DECLARE_FINAL_TYPE (GsdColorCalibrate, gsd_color_calibrate, GSD, COLOR_CALIBRATE, GObject)
+
+GType gsd_color_calibrate_get_type (void);
+GQuark gsd_color_calibrate_error_quark (void);
+
+GsdColorCalibrate * gsd_color_calibrate_new (void);
+
+G_END_DECLS
+
+#endif /* __GSD_COLOR_CALIBRATE_H */
diff --git a/plugins/color/gsd-color-manager.c b/plugins/color/gsd-color-manager.c
new file mode 100644
index 0000000..62a417e
--- /dev/null
+++ b/plugins/color/gsd-color-manager.c
@@ -0,0 +1,502 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
+ * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gdk/gdk.h>
+#include <gtk/gtk.h>
+
+#include "gnome-settings-profile.h"
+#include "gsd-color-calibrate.h"
+#include "gsd-color-manager.h"
+#include "gsd-color-profiles.h"
+#include "gsd-color-state.h"
+#include "gsd-night-light.h"
+
+#define GSD_DBUS_NAME "org.gnome.SettingsDaemon"
+#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon"
+#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon"
+
+#define GSD_COLOR_DBUS_NAME GSD_DBUS_NAME ".Color"
+#define GSD_COLOR_DBUS_PATH GSD_DBUS_PATH "/Color"
+#define GSD_COLOR_DBUS_INTERFACE GSD_DBUS_BASE_INTERFACE ".Color"
+
+static const gchar introspection_xml[] =
+"<node>"
+" <interface name='org.gnome.SettingsDaemon.Color'>"
+" <method name='NightLightPreview'>"
+" <arg type='u' name='duration' direction='in'/>"
+" </method>"
+" <property name='NightLightActive' type='b' access='read'/>"
+" <property name='Temperature' type='u' access='readwrite'/>"
+" <property name='DisabledUntilTomorrow' type='b' access='readwrite'/>"
+" <property name='Sunrise' type='d' access='read'/>"
+" <property name='Sunset' type='d' access='read'/>"
+" </interface>"
+"</node>";
+
+struct _GsdColorManager
+{
+ GObject parent;
+
+ /* D-Bus */
+ guint name_id;
+ GDBusNodeInfo *introspection_data;
+ GDBusConnection *connection;
+ GCancellable *bus_cancellable;
+
+ GsdColorCalibrate *calibrate;
+ GsdColorProfiles *profiles;
+ GsdColorState *state;
+ GsdNightLight *nlight;
+
+ guint nlight_forced_timeout_id;
+};
+
+enum {
+ PROP_0,
+};
+
+static void gsd_color_manager_class_init (GsdColorManagerClass *klass);
+static void gsd_color_manager_init (GsdColorManager *color_manager);
+static void gsd_color_manager_finalize (GObject *object);
+
+G_DEFINE_TYPE (GsdColorManager, gsd_color_manager, G_TYPE_OBJECT)
+
+static gpointer manager_object = NULL;
+
+GQuark
+gsd_color_manager_error_quark (void)
+{
+ static GQuark quark = 0;
+ if (!quark)
+ quark = g_quark_from_static_string ("gsd_color_manager_error");
+ return quark;
+}
+
+gboolean
+gsd_color_manager_start (GsdColorManager *manager,
+ GError **error)
+{
+ gboolean ret;
+
+ g_debug ("Starting color manager");
+ gnome_settings_profile_start (NULL);
+
+ /* start the device probing */
+ gsd_color_state_start (manager->state);
+
+ /* start the profiles collection */
+ ret = gsd_color_profiles_start (manager->profiles, error);
+ if (!ret)
+ goto out;
+out:
+ gnome_settings_profile_end (NULL);
+ return ret;
+}
+
+void
+gsd_color_manager_stop (GsdColorManager *manager)
+{
+ g_debug ("Stopping color manager");
+ gsd_color_state_stop (manager->state);
+ gsd_color_profiles_stop (manager->profiles);
+}
+
+static void
+gsd_color_manager_class_init (GsdColorManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gsd_color_manager_finalize;
+}
+
+static void
+emit_property_changed (GsdColorManager *manager,
+ const gchar *property_name,
+ GVariant *property_value)
+{
+ GVariantBuilder builder;
+ GVariantBuilder invalidated_builder;
+
+ /* not yet connected */
+ if (manager->connection == NULL)
+ return;
+
+ /* build the dict */
+ g_variant_builder_init (&invalidated_builder, G_VARIANT_TYPE ("as"));
+ g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY);
+ g_variant_builder_add (&builder,
+ "{sv}",
+ property_name,
+ property_value);
+ g_dbus_connection_emit_signal (manager->connection,
+ NULL,
+ GSD_COLOR_DBUS_PATH,
+ "org.freedesktop.DBus.Properties",
+ "PropertiesChanged",
+ g_variant_new ("(sa{sv}as)",
+ GSD_COLOR_DBUS_INTERFACE,
+ &builder,
+ &invalidated_builder),
+ NULL);
+ g_variant_builder_clear (&builder);
+ g_variant_builder_clear (&invalidated_builder);
+}
+
+static void
+on_active_notify (GsdNightLight *nlight,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsdColorManager *manager = GSD_COLOR_MANAGER (user_data);
+ emit_property_changed (manager, "NightLightActive",
+ g_variant_new_boolean (gsd_night_light_get_active (manager->nlight)));
+}
+
+static void
+on_sunset_notify (GsdNightLight *nlight,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsdColorManager *manager = GSD_COLOR_MANAGER (user_data);
+ emit_property_changed (manager, "Sunset",
+ g_variant_new_double (gsd_night_light_get_sunset (manager->nlight)));
+}
+
+static void
+on_sunrise_notify (GsdNightLight *nlight,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsdColorManager *manager = GSD_COLOR_MANAGER (user_data);
+ emit_property_changed (manager, "Sunrise",
+ g_variant_new_double (gsd_night_light_get_sunrise (manager->nlight)));
+}
+
+static void
+on_disabled_until_tmw_notify (GsdNightLight *nlight,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsdColorManager *manager = GSD_COLOR_MANAGER (user_data);
+ emit_property_changed (manager, "DisabledUntilTomorrow",
+ g_variant_new_boolean (gsd_night_light_get_disabled_until_tmw (manager->nlight)));
+}
+
+static void
+on_temperature_notify (GsdNightLight *nlight,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsdColorManager *manager = GSD_COLOR_MANAGER (user_data);
+ gdouble temperature = gsd_night_light_get_temperature (manager->nlight);
+ gsd_color_state_set_temperature (manager->state, temperature);
+ emit_property_changed (manager, "Temperature",
+ g_variant_new_double (temperature));
+}
+
+static void
+gsd_color_manager_init (GsdColorManager *manager)
+{
+ /* setup calibration features */
+ manager->calibrate = gsd_color_calibrate_new ();
+ manager->profiles = gsd_color_profiles_new ();
+ manager->state = gsd_color_state_new ();
+
+ /* night light features */
+ manager->nlight = gsd_night_light_new ();
+ g_signal_connect (manager->nlight, "notify::active",
+ G_CALLBACK (on_active_notify), manager);
+ g_signal_connect (manager->nlight, "notify::sunset",
+ G_CALLBACK (on_sunset_notify), manager);
+ g_signal_connect (manager->nlight, "notify::sunrise",
+ G_CALLBACK (on_sunrise_notify), manager);
+ g_signal_connect (manager->nlight, "notify::temperature",
+ G_CALLBACK (on_temperature_notify), manager);
+ g_signal_connect (manager->nlight, "notify::disabled-until-tmw",
+ G_CALLBACK (on_disabled_until_tmw_notify), manager);
+}
+
+static void
+gsd_color_manager_finalize (GObject *object)
+{
+ GsdColorManager *manager;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GSD_IS_COLOR_MANAGER (object));
+
+ manager = GSD_COLOR_MANAGER (object);
+
+ gsd_color_manager_stop (manager);
+
+ if (manager->bus_cancellable != NULL) {
+ g_cancellable_cancel (manager->bus_cancellable);
+ g_clear_object (&manager->bus_cancellable);
+ }
+
+ g_clear_pointer (&manager->introspection_data, g_dbus_node_info_unref);
+ g_clear_object (&manager->connection);
+
+ if (manager->name_id != 0) {
+ g_bus_unown_name (manager->name_id);
+ manager->name_id = 0;
+ }
+
+ if (manager->nlight_forced_timeout_id)
+ g_source_remove (manager->nlight_forced_timeout_id);
+
+ g_clear_object (&manager->calibrate);
+ g_clear_object (&manager->profiles);
+ g_clear_object (&manager->state);
+ g_clear_object (&manager->nlight);
+
+ G_OBJECT_CLASS (gsd_color_manager_parent_class)->finalize (object);
+}
+
+static gboolean
+nlight_forced_timeout_cb (gpointer user_data)
+{
+ GsdColorManager *manager = GSD_COLOR_MANAGER (user_data);
+
+ manager->nlight_forced_timeout_id = 0;
+ gsd_night_light_set_forced (manager->nlight, FALSE);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+handle_method_call (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ gpointer user_data)
+{
+ GsdColorManager *manager = GSD_COLOR_MANAGER (user_data);
+
+ if (g_strcmp0 (method_name, "NightLightPreview") == 0) {
+ guint32 duration = 0;
+
+ if (!manager->nlight) {
+ g_dbus_method_invocation_return_error_literal (invocation,
+ G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED,
+ "Night-light is currently unavailable");
+
+ return;
+ }
+
+ g_variant_get (parameters, "(u)", &duration);
+
+ if (duration == 0 || duration > 120) {
+ g_dbus_method_invocation_return_error_literal (invocation,
+ G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+ "Duration is out of the range (0-120].");
+
+ return;
+ }
+
+ if (manager->nlight_forced_timeout_id)
+ g_source_remove (manager->nlight_forced_timeout_id);
+ manager->nlight_forced_timeout_id = g_timeout_add_seconds (duration, nlight_forced_timeout_cb, manager);
+
+ gsd_night_light_set_forced (manager->nlight, TRUE);
+
+ g_dbus_method_invocation_return_value (invocation, NULL);
+ } else {
+ g_assert_not_reached ();
+ }
+}
+
+static GVariant *
+handle_get_property (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *property_name,
+ GError **error, gpointer user_data)
+{
+ GsdColorManager *manager = GSD_COLOR_MANAGER (user_data);
+
+ if (g_strcmp0 (interface_name, GSD_COLOR_DBUS_INTERFACE) != 0) {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "No such interface: %s", interface_name);
+ return NULL;
+ }
+
+ if (g_strcmp0 (property_name, "NightLightActive") == 0)
+ return g_variant_new_boolean (gsd_night_light_get_active (manager->nlight));
+
+ if (g_strcmp0 (property_name, "Temperature") == 0) {
+ guint temperature;
+ temperature = gsd_color_state_get_temperature (manager->state);
+ return g_variant_new_uint32 (temperature);
+ }
+
+ if (g_strcmp0 (property_name, "DisabledUntilTomorrow") == 0)
+ return g_variant_new_boolean (gsd_night_light_get_disabled_until_tmw (manager->nlight));
+
+ if (g_strcmp0 (property_name, "Sunrise") == 0)
+ return g_variant_new_double (gsd_night_light_get_sunrise (manager->nlight));
+
+ if (g_strcmp0 (property_name, "Sunset") == 0)
+ return g_variant_new_double (gsd_night_light_get_sunset (manager->nlight));
+
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "Failed to get property: %s", property_name);
+ return NULL;
+}
+
+static gboolean
+handle_set_property (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *property_name,
+ GVariant *value,
+ GError **error, gpointer user_data)
+{
+ GsdColorManager *manager = GSD_COLOR_MANAGER (user_data);
+
+ if (g_strcmp0 (interface_name, GSD_COLOR_DBUS_INTERFACE) != 0) {
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "No such interface: %s", interface_name);
+ return FALSE;
+ }
+
+ if (g_strcmp0 (property_name, "Temperature") == 0) {
+ guint32 temperature;
+ g_variant_get (value, "u", &temperature);
+ if (temperature < GSD_COLOR_TEMPERATURE_MIN) {
+ g_set_error (error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_ARGUMENT,
+ "%" G_GUINT32_FORMAT "K is < min %" G_GUINT32_FORMAT "K",
+ temperature, GSD_COLOR_TEMPERATURE_MIN);
+ return FALSE;
+ }
+ if (temperature > GSD_COLOR_TEMPERATURE_MAX) {
+ g_set_error (error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_ARGUMENT,
+ "%" G_GUINT32_FORMAT "K is > max %" G_GUINT32_FORMAT "K",
+ temperature, GSD_COLOR_TEMPERATURE_MAX);
+ return FALSE;
+ }
+ gsd_color_state_set_temperature (manager->state, temperature);
+ return TRUE;
+ }
+
+ if (g_strcmp0 (property_name, "DisabledUntilTomorrow") == 0) {
+ gsd_night_light_set_disabled_until_tmw (manager->nlight,
+ g_variant_get_boolean (value));
+ return TRUE;
+ }
+
+ g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+ "No such property: %s", property_name);
+ return FALSE;
+}
+
+static const GDBusInterfaceVTable interface_vtable =
+{
+ handle_method_call,
+ handle_get_property,
+ handle_set_property
+};
+
+static void
+name_lost_handler_cb (GDBusConnection *connection, const gchar *name, gpointer user_data)
+{
+ g_debug ("lost name, so exiting");
+ gtk_main_quit ();
+}
+
+static void
+on_bus_gotten (GObject *source_object,
+ GAsyncResult *res,
+ GsdColorManager *manager)
+{
+ GDBusConnection *connection;
+ GError *error = NULL;
+
+ connection = g_bus_get_finish (res, &error);
+ if (connection == NULL) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Could not get session bus: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ manager->connection = connection;
+
+ g_dbus_connection_register_object (connection,
+ GSD_COLOR_DBUS_PATH,
+ manager->introspection_data->interfaces[0],
+ &interface_vtable,
+ manager,
+ NULL,
+ NULL);
+
+ manager->name_id = g_bus_own_name_on_connection (connection,
+ GSD_COLOR_DBUS_NAME,
+ G_BUS_NAME_OWNER_FLAGS_NONE,
+ NULL,
+ name_lost_handler_cb,
+ manager,
+ NULL);
+
+ /* setup night light module */
+ if (!gsd_night_light_start (manager->nlight, &error)) {
+ g_warning ("Could not start night light module: %s", error->message);
+ g_error_free (error);
+ }
+}
+
+static void
+register_manager_dbus (GsdColorManager *manager)
+{
+ manager->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL);
+ g_assert (manager->introspection_data != NULL);
+ manager->bus_cancellable = g_cancellable_new ();
+
+ g_bus_get (G_BUS_TYPE_SESSION,
+ manager->bus_cancellable,
+ (GAsyncReadyCallback) on_bus_gotten,
+ manager);
+}
+
+GsdColorManager *
+gsd_color_manager_new (void)
+{
+ if (manager_object != NULL) {
+ g_object_ref (manager_object);
+ } else {
+ manager_object = g_object_new (GSD_TYPE_COLOR_MANAGER, NULL);
+ g_object_add_weak_pointer (manager_object,
+ (gpointer *) &manager_object);
+ register_manager_dbus (manager_object);
+ }
+
+ return GSD_COLOR_MANAGER (manager_object);
+}
diff --git a/plugins/color/gsd-color-manager.h b/plugins/color/gsd-color-manager.h
new file mode 100644
index 0000000..ba71fca
--- /dev/null
+++ b/plugins/color/gsd-color-manager.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
+ * Copyright (C) 2011 Richard Hughes <richard@hughsie.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef __GSD_COLOR_MANAGER_H
+#define __GSD_COLOR_MANAGER_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GSD_TYPE_COLOR_MANAGER (gsd_color_manager_get_type ())
+#define GSD_COLOR_MANAGER_ERROR (gsd_color_manager_error_quark ())
+G_DECLARE_FINAL_TYPE (GsdColorManager, gsd_color_manager, GSD, COLOR_MANAGER, GObject)
+
+enum
+{
+ GSD_COLOR_MANAGER_ERROR_FAILED
+};
+
+GQuark gsd_color_manager_error_quark (void);
+
+GsdColorManager * gsd_color_manager_new (void);
+gboolean gsd_color_manager_start (GsdColorManager *manager,
+ GError **error);
+void gsd_color_manager_stop (GsdColorManager *manager);
+
+G_END_DECLS
+
+#endif /* __GSD_COLOR_MANAGER_H */
diff --git a/plugins/color/gsd-color-profiles.c b/plugins/color/gsd-color-profiles.c
new file mode 100644
index 0000000..c73abd7
--- /dev/null
+++ b/plugins/color/gsd-color-profiles.c
@@ -0,0 +1,246 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
+ * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <colord.h>
+
+#include "gsd-color-profiles.h"
+
+struct _GsdColorProfiles
+{
+ GObject parent;
+
+ GCancellable *cancellable;
+ CdClient *client;
+ CdIccStore *icc_store;
+};
+
+static void gsd_color_profiles_class_init (GsdColorProfilesClass *klass);
+static void gsd_color_profiles_init (GsdColorProfiles *color_profiles);
+static void gsd_color_profiles_finalize (GObject *object);
+
+G_DEFINE_TYPE (GsdColorProfiles, gsd_color_profiles, G_TYPE_OBJECT)
+
+static void
+gsd_color_profiles_class_init (GsdColorProfilesClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gsd_color_profiles_finalize;
+}
+
+static void
+gcm_session_client_connect_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ gboolean ret;
+ GError *error = NULL;
+ CdClient *client = CD_CLIENT (source_object);
+ GsdColorProfiles *profiles;
+
+ /* connected */
+ ret = cd_client_connect_finish (client, res, &error);
+ if (!ret) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("failed to connect to colord: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ /* is there an available colord instance? */
+ profiles = GSD_COLOR_PROFILES (user_data);
+ ret = cd_client_get_has_server (profiles->client);
+ if (!ret) {
+ g_warning ("There is no colord server available");
+ return;
+ }
+
+ /* add profiles */
+ ret = cd_icc_store_search_kind (profiles->icc_store,
+ CD_ICC_STORE_SEARCH_KIND_USER,
+ CD_ICC_STORE_SEARCH_FLAGS_CREATE_LOCATION,
+ profiles->cancellable,
+ &error);
+ if (!ret) {
+ g_warning ("failed to add user icc: %s", error->message);
+ g_error_free (error);
+ }
+}
+
+gboolean
+gsd_color_profiles_start (GsdColorProfiles *profiles,
+ GError **error)
+{
+ /* use a fresh cancellable for each start->stop operation */
+ g_cancellable_cancel (profiles->cancellable);
+ g_clear_object (&profiles->cancellable);
+ profiles->cancellable = g_cancellable_new ();
+
+ cd_client_connect (profiles->client,
+ profiles->cancellable,
+ gcm_session_client_connect_cb,
+ profiles);
+
+ return TRUE;
+}
+
+void
+gsd_color_profiles_stop (GsdColorProfiles *profiles)
+{
+ g_cancellable_cancel (profiles->cancellable);
+}
+
+static void
+gcm_session_create_profile_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ CdProfile *profile;
+ GError *error = NULL;
+ CdClient *client = CD_CLIENT (object);
+
+ profile = cd_client_create_profile_finish (client, res, &error);
+ if (profile == NULL) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
+ !g_error_matches (error, CD_CLIENT_ERROR, CD_CLIENT_ERROR_ALREADY_EXISTS))
+ g_warning ("%s", error->message);
+ g_error_free (error);
+ return;
+ }
+ g_object_unref (profile);
+}
+
+static void
+gcm_session_icc_store_added_cb (CdIccStore *icc_store,
+ CdIcc *icc,
+ GsdColorProfiles *profiles)
+{
+ cd_client_create_profile_for_icc (profiles->client,
+ icc,
+ CD_OBJECT_SCOPE_TEMP,
+ profiles->cancellable,
+ gcm_session_create_profile_cb,
+ profiles);
+}
+
+static void
+gcm_session_delete_profile_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ gboolean ret;
+ GError *error = NULL;
+ CdClient *client = CD_CLIENT (object);
+
+ ret = cd_client_delete_profile_finish (client, res, &error);
+ if (!ret) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("%s", error->message);
+ g_error_free (error);
+ }
+}
+
+static void
+gcm_session_find_profile_by_filename_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ CdProfile *profile;
+ CdClient *client = CD_CLIENT (object);
+ GsdColorProfiles *profiles = GSD_COLOR_PROFILES (user_data);
+
+ profile = cd_client_find_profile_by_filename_finish (client, res, &error);
+ if (profile == NULL) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("%s", error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ /* remove it from colord */
+ cd_client_delete_profile (profiles->client,
+ profile,
+ profiles->cancellable,
+ gcm_session_delete_profile_cb,
+ profiles);
+out:
+ if (profile != NULL)
+ g_object_unref (profile);
+}
+
+static void
+gcm_session_icc_store_removed_cb (CdIccStore *icc_store,
+ CdIcc *icc,
+ GsdColorProfiles *profiles)
+{
+ /* find the ID for the filename */
+ g_debug ("filename %s removed", cd_icc_get_filename (icc));
+ cd_client_find_profile_by_filename (profiles->client,
+ cd_icc_get_filename (icc),
+ profiles->cancellable,
+ gcm_session_find_profile_by_filename_cb,
+ profiles);
+}
+
+static void
+gsd_color_profiles_init (GsdColorProfiles *profiles)
+{
+ /* have access to all user profiles */
+ profiles->client = cd_client_new ();
+ profiles->icc_store = cd_icc_store_new ();
+ cd_icc_store_set_load_flags (profiles->icc_store,
+ CD_ICC_LOAD_FLAGS_FALLBACK_MD5);
+ g_signal_connect (profiles->icc_store, "added",
+ G_CALLBACK (gcm_session_icc_store_added_cb),
+ profiles);
+ g_signal_connect (profiles->icc_store, "removed",
+ G_CALLBACK (gcm_session_icc_store_removed_cb),
+ profiles);
+}
+
+static void
+gsd_color_profiles_finalize (GObject *object)
+{
+ GsdColorProfiles *profiles;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GSD_IS_COLOR_PROFILES (object));
+
+ profiles = GSD_COLOR_PROFILES (object);
+
+ g_cancellable_cancel (profiles->cancellable);
+ g_clear_object (&profiles->cancellable);
+ g_clear_object (&profiles->icc_store);
+ g_clear_object (&profiles->client);
+
+ G_OBJECT_CLASS (gsd_color_profiles_parent_class)->finalize (object);
+}
+
+GsdColorProfiles *
+gsd_color_profiles_new (void)
+{
+ GsdColorProfiles *profiles;
+ profiles = g_object_new (GSD_TYPE_COLOR_PROFILES, NULL);
+ return GSD_COLOR_PROFILES (profiles);
+}
diff --git a/plugins/color/gsd-color-profiles.h b/plugins/color/gsd-color-profiles.h
new file mode 100644
index 0000000..24aed34
--- /dev/null
+++ b/plugins/color/gsd-color-profiles.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
+ * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef __GSD_COLOR_PROFILES_H
+#define __GSD_COLOR_PROFILES_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GSD_TYPE_COLOR_PROFILES (gsd_color_profiles_get_type ())
+G_DECLARE_FINAL_TYPE (GsdColorProfiles, gsd_color_profiles, GSD, COLOR_PROFILES, GObject)
+
+GQuark gsd_color_profiles_error_quark (void);
+
+GsdColorProfiles * gsd_color_profiles_new (void);
+gboolean gsd_color_profiles_start (GsdColorProfiles *profiles,
+ GError **error);
+void gsd_color_profiles_stop (GsdColorProfiles *profiles);
+
+G_END_DECLS
+
+#endif /* __GSD_COLOR_PROFILES_H */
diff --git a/plugins/color/gsd-color-state.c b/plugins/color/gsd-color-state.c
new file mode 100644
index 0000000..746cf23
--- /dev/null
+++ b/plugins/color/gsd-color-state.c
@@ -0,0 +1,1592 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
+ * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <colord.h>
+#include <gdk/gdk.h>
+#include <stdlib.h>
+#include <lcms2.h>
+#include <canberra-gtk.h>
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include <libgnome-desktop/gnome-rr.h>
+
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#endif
+
+#include "gnome-settings-bus.h"
+
+#include "gsd-color-manager.h"
+#include "gsd-color-state.h"
+#include "gcm-edid.h"
+
+#define GSD_DBUS_NAME "org.gnome.SettingsDaemon"
+#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon"
+#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon"
+
+static void gcm_session_set_gamma_for_all_devices (GsdColorState *state);
+
+struct _GsdColorState
+{
+ GObject parent;
+
+ GCancellable *cancellable;
+ GsdSessionManager *session;
+ CdClient *client;
+ GnomeRRScreen *state_screen;
+ GHashTable *edid_cache;
+ GdkWindow *gdk_window;
+ gboolean session_is_active;
+ GHashTable *device_assign_hash;
+ guint color_temperature;
+};
+
+static void gsd_color_state_class_init (GsdColorStateClass *klass);
+static void gsd_color_state_init (GsdColorState *color_state);
+static void gsd_color_state_finalize (GObject *object);
+
+G_DEFINE_TYPE (GsdColorState, gsd_color_state, G_TYPE_OBJECT)
+
+/* see http://www.oyranos.org/wiki/index.php?title=ICC_Profiles_in_X_Specification_0.3 */
+#define GCM_ICC_PROFILE_IN_X_VERSION_MAJOR 0
+#define GCM_ICC_PROFILE_IN_X_VERSION_MINOR 3
+
+typedef struct {
+ guint32 red;
+ guint32 green;
+ guint32 blue;
+} GnomeRROutputClutItem;
+
+GQuark
+gsd_color_state_error_quark (void)
+{
+ static GQuark quark = 0;
+ if (!quark)
+ quark = g_quark_from_static_string ("gsd_color_state_error");
+ return quark;
+}
+
+static GcmEdid *
+gcm_session_get_output_edid (GsdColorState *state, GnomeRROutput *output, GError **error)
+{
+ const guint8 *data;
+ gsize size;
+ GcmEdid *edid = NULL;
+ gboolean ret;
+
+ /* can we find it in the cache */
+ edid = g_hash_table_lookup (state->edid_cache,
+ gnome_rr_output_get_name (output));
+ if (edid != NULL) {
+ g_object_ref (edid);
+ return edid;
+ }
+
+ /* parse edid */
+ data = gnome_rr_output_get_edid_data (output, &size);
+ if (data == NULL || size == 0) {
+ g_set_error_literal (error,
+ GNOME_RR_ERROR,
+ GNOME_RR_ERROR_UNKNOWN,
+ "unable to get EDID for output");
+ return NULL;
+ }
+ edid = gcm_edid_new ();
+ ret = gcm_edid_parse (edid, data, size, error);
+ if (!ret) {
+ g_object_unref (edid);
+ return NULL;
+ }
+
+ /* add to cache */
+ g_hash_table_insert (state->edid_cache,
+ g_strdup (gnome_rr_output_get_name (output)),
+ g_object_ref (edid));
+
+ return edid;
+}
+
+static gboolean
+gcm_session_screen_set_icc_profile (GsdColorState *state,
+ const gchar *filename,
+ GError **error)
+{
+ gchar *data = NULL;
+ gsize length;
+ guint version_data;
+
+ g_return_val_if_fail (filename != NULL, FALSE);
+
+ /* wayland */
+ if (state->gdk_window == NULL) {
+ g_debug ("not setting atom as running under wayland");
+ return TRUE;
+ }
+
+ g_debug ("setting root window ICC profile atom from %s", filename);
+
+ /* get contents of file */
+ if (!g_file_get_contents (filename, &data, &length, error))
+ return FALSE;
+
+ /* set profile property */
+ gdk_property_change (state->gdk_window,
+ gdk_atom_intern_static_string ("_ICC_PROFILE"),
+ gdk_atom_intern_static_string ("CARDINAL"),
+ 8,
+ GDK_PROP_MODE_REPLACE,
+ (const guchar *) data, length);
+
+ /* set version property */
+ version_data = GCM_ICC_PROFILE_IN_X_VERSION_MAJOR * 100 +
+ GCM_ICC_PROFILE_IN_X_VERSION_MINOR * 1;
+ gdk_property_change (state->gdk_window,
+ gdk_atom_intern_static_string ("_ICC_PROFILE_IN_X_VERSION"),
+ gdk_atom_intern_static_string ("CARDINAL"),
+ 8,
+ GDK_PROP_MODE_REPLACE,
+ (const guchar *) &version_data, 1);
+
+ g_free (data);
+ return TRUE;
+}
+
+void
+gsd_color_state_set_temperature (GsdColorState *state, guint temperature)
+{
+ g_return_if_fail (GSD_IS_COLOR_STATE (state));
+
+ if (state->color_temperature == temperature)
+ return;
+
+ state->color_temperature = temperature;
+ gcm_session_set_gamma_for_all_devices (state);
+}
+
+guint
+gsd_color_state_get_temperature (GsdColorState *state)
+{
+ g_return_val_if_fail (GSD_IS_COLOR_STATE (state), 0);
+ return state->color_temperature;
+}
+
+static gchar *
+gcm_session_get_output_id (GsdColorState *state, GnomeRROutput *output)
+{
+ const gchar *name;
+ const gchar *serial;
+ const gchar *vendor;
+ GcmEdid *edid = NULL;
+ GString *device_id;
+ GError *error = NULL;
+
+ /* all output devices are prefixed with this */
+ device_id = g_string_new ("xrandr");
+
+ /* get the output EDID if possible */
+ edid = gcm_session_get_output_edid (state, output, &error);
+ if (edid == NULL) {
+ g_debug ("no edid for %s [%s], falling back to connection name",
+ gnome_rr_output_get_name (output),
+ error->message);
+ g_error_free (error);
+ g_string_append_printf (device_id,
+ "-%s",
+ gnome_rr_output_get_name (output));
+ goto out;
+ }
+
+ /* check EDID data is okay to use */
+ vendor = gcm_edid_get_vendor_name (edid);
+ name = gcm_edid_get_monitor_name (edid);
+ serial = gcm_edid_get_serial_number (edid);
+ if (vendor == NULL && name == NULL && serial == NULL) {
+ g_debug ("edid invalid for %s, falling back to connection name",
+ gnome_rr_output_get_name (output));
+ g_string_append_printf (device_id,
+ "-%s",
+ gnome_rr_output_get_name (output));
+ goto out;
+ }
+
+ /* use EDID data */
+ if (vendor != NULL)
+ g_string_append_printf (device_id, "-%s", vendor);
+ if (name != NULL)
+ g_string_append_printf (device_id, "-%s", name);
+ if (serial != NULL)
+ g_string_append_printf (device_id, "-%s", serial);
+out:
+ if (edid != NULL)
+ g_object_unref (edid);
+ return g_string_free (device_id, FALSE);
+}
+
+typedef struct {
+ GsdColorState *state;
+ CdProfile *profile;
+ CdDevice *device;
+ guint32 output_id;
+} GcmSessionAsyncHelper;
+
+static void
+gcm_session_async_helper_free (GcmSessionAsyncHelper *helper)
+{
+ if (helper->state != NULL)
+ g_object_unref (helper->state);
+ if (helper->profile != NULL)
+ g_object_unref (helper->profile);
+ if (helper->device != NULL)
+ g_object_unref (helper->device);
+ g_free (helper);
+}
+
+static gboolean
+gcm_utils_mkdir_for_filename (GFile *file, GError **error)
+{
+ gboolean ret = FALSE;
+ GFile *parent_dir = NULL;
+
+ /* get parent directory */
+ parent_dir = g_file_get_parent (file);
+ if (parent_dir == NULL) {
+ g_set_error_literal (error,
+ GSD_COLOR_MANAGER_ERROR,
+ GSD_COLOR_MANAGER_ERROR_FAILED,
+ "could not get parent dir");
+ goto out;
+ }
+
+ /* ensure desination does not already exist */
+ ret = g_file_query_exists (parent_dir, NULL);
+ if (ret)
+ goto out;
+ ret = g_file_make_directory_with_parents (parent_dir, NULL, error);
+ if (!ret)
+ goto out;
+out:
+ if (parent_dir != NULL)
+ g_object_unref (parent_dir);
+ return ret;
+}
+
+static gboolean
+gcm_get_system_icc_profile (GsdColorState *state,
+ GFile *file)
+{
+ const char efi_path[] = "/sys/firmware/efi/efivars/INTERNAL_PANEL_COLOR_INFO-01e1ada1-79f2-46b3-8d3e-71fc0996ca6b";
+ /* efi variables have a 4-byte header */
+ const int efi_var_header_length = 4;
+ g_autoptr(GFile) efi_file = g_file_new_for_path (efi_path);
+ gboolean ret;
+ g_autofree char *data = NULL;
+ gsize length;
+ g_autoptr(GError) error = NULL;
+
+ ret = g_file_query_exists (efi_file, NULL);
+ if (!ret)
+ return FALSE;
+
+ ret = g_file_load_contents (efi_file,
+ NULL /* cancellable */,
+ &data,
+ &length,
+ NULL /* etag_out */,
+ &error);
+
+ if (!ret) {
+ g_warning ("failed to read EFI system color profile: %s",
+ error->message);
+ return FALSE;
+ }
+
+ if (length <= efi_var_header_length) {
+ g_warning ("EFI system color profile was too short");
+ return FALSE;
+ }
+
+ ret = g_file_replace_contents (file,
+ data + efi_var_header_length,
+ length - efi_var_header_length,
+ NULL /* etag */,
+ FALSE /* make_backup */,
+ G_FILE_CREATE_NONE,
+ NULL /* new_etag */,
+ NULL /* cancellable */,
+ &error);
+ if (!ret) {
+ g_warning ("failed to write system color profile: %s",
+ error->message);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+gcm_apply_create_icc_profile_for_edid (GsdColorState *state,
+ CdDevice *device,
+ GcmEdid *edid,
+ GFile *file,
+ GError **error)
+{
+ CdIcc *icc = NULL;
+ const gchar *data;
+ gboolean ret = FALSE;
+
+ /* ensure the per-user directory exists */
+ ret = gcm_utils_mkdir_for_filename (file, error);
+ if (!ret)
+ goto out;
+
+ /* create our generated profile */
+ icc = cd_icc_new ();
+ ret = cd_icc_create_from_edid (icc,
+ gcm_edid_get_gamma (edid),
+ gcm_edid_get_red (edid),
+ gcm_edid_get_green (edid),
+ gcm_edid_get_blue (edid),
+ gcm_edid_get_white (edid),
+ error);
+ if (!ret)
+ goto out;
+
+ /* set copyright */
+ cd_icc_set_copyright (icc, NULL,
+ /* deliberately not translated */
+ "This profile is free of known copyright restrictions.");
+
+ /* set model and title */
+ data = gcm_edid_get_monitor_name (edid);
+ if (data == NULL)
+ data = cd_client_get_system_model (state->client);
+ if (data == NULL)
+ data = "Unknown monitor";
+ cd_icc_set_model (icc, NULL, data);
+ cd_icc_set_description (icc, NULL, data);
+
+ /* get manufacturer */
+ data = gcm_edid_get_vendor_name (edid);
+ if (data == NULL)
+ data = cd_client_get_system_vendor (state->client);
+ if (data == NULL)
+ data = "Unknown vendor";
+ cd_icc_set_manufacturer (icc, NULL, data);
+
+ /* set the framework creator metadata */
+ cd_icc_add_metadata (icc,
+ CD_PROFILE_METADATA_CMF_PRODUCT,
+ PACKAGE_NAME);
+ cd_icc_add_metadata (icc,
+ CD_PROFILE_METADATA_CMF_BINARY,
+ PACKAGE_NAME);
+ cd_icc_add_metadata (icc,
+ CD_PROFILE_METADATA_CMF_VERSION,
+ PACKAGE_VERSION);
+ cd_icc_add_metadata (icc,
+ CD_PROFILE_METADATA_MAPPING_DEVICE_ID,
+ cd_device_get_id (device));
+
+ /* set 'ICC meta Tag for Monitor Profiles' data */
+ cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_MD5, gcm_edid_get_checksum (edid));
+ data = gcm_edid_get_monitor_name (edid);
+ if (data != NULL)
+ cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_MODEL, data);
+ data = gcm_edid_get_serial_number (edid);
+ if (data != NULL)
+ cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_SERIAL, data);
+ data = gcm_edid_get_pnp_id (edid);
+ if (data != NULL)
+ cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_MNFT, data);
+ data = gcm_edid_get_vendor_name (edid);
+ if (data != NULL)
+ cd_icc_add_metadata (icc, CD_PROFILE_METADATA_EDID_VENDOR, data);
+
+ /* save */
+ ret = cd_icc_save_file (icc, file, CD_ICC_SAVE_FLAGS_NONE, NULL, error);
+ if (!ret)
+ goto out;
+out:
+ if (icc != NULL)
+ g_object_unref (icc);
+ return ret;
+}
+
+static GPtrArray *
+gcm_session_generate_vcgt (CdProfile *profile, guint color_temperature, guint size)
+{
+ GnomeRROutputClutItem *tmp;
+ GPtrArray *array = NULL;
+ const cmsToneCurve **vcgt;
+ cmsFloat32Number in;
+ guint i;
+ cmsHPROFILE lcms_profile;
+ CdIcc *icc = NULL;
+ CdColorRGB temp;
+
+ /* invalid size */
+ if (size == 0)
+ goto out;
+
+ /* open file */
+ icc = cd_profile_load_icc (profile, CD_ICC_LOAD_FLAGS_NONE, NULL, NULL);
+ if (icc == NULL)
+ goto out;
+
+ /* get tone curves from profile */
+ lcms_profile = cd_icc_get_handle (icc);
+ vcgt = cmsReadTag (lcms_profile, cmsSigVcgtTag);
+ if (vcgt == NULL || vcgt[0] == NULL) {
+ g_debug ("profile does not have any VCGT data");
+ goto out;
+ }
+
+ /* get the color temperature */
+ if (!cd_color_get_blackbody_rgb_full (color_temperature,
+ &temp,
+ CD_COLOR_BLACKBODY_FLAG_USE_PLANCKIAN)) {
+ g_warning ("failed to get blackbody for %uK", color_temperature);
+ cd_color_rgb_set (&temp, 1.0, 1.0, 1.0);
+ } else {
+ g_debug ("using VCGT gamma of %uK = %.1f,%.1f,%.1f",
+ color_temperature, temp.R, temp.G, temp.B);
+ }
+
+ /* create array */
+ array = g_ptr_array_new_with_free_func (g_free);
+ for (i = 0; i < size; i++) {
+ in = (gdouble) i / (gdouble) (size - 1);
+ tmp = g_new0 (GnomeRROutputClutItem, 1);
+ tmp->red = cmsEvalToneCurveFloat(vcgt[0], in) * temp.R * (gdouble) 0xffff;
+ tmp->green = cmsEvalToneCurveFloat(vcgt[1], in) * temp.G * (gdouble) 0xffff;
+ tmp->blue = cmsEvalToneCurveFloat(vcgt[2], in) * temp.B * (gdouble) 0xffff;
+ g_ptr_array_add (array, tmp);
+ }
+out:
+ if (icc != NULL)
+ g_object_unref (icc);
+ return array;
+}
+
+static guint
+gnome_rr_output_get_gamma_size (GnomeRROutput *output)
+{
+ GnomeRRCrtc *crtc;
+ gint len = 0;
+
+ crtc = gnome_rr_output_get_crtc (output);
+ if (crtc == NULL)
+ return 0;
+ gnome_rr_crtc_get_gamma (crtc,
+ &len,
+ NULL, NULL, NULL);
+ return (guint) len;
+}
+
+static gboolean
+gcm_session_output_set_gamma (GnomeRROutput *output,
+ GPtrArray *array,
+ GError **error)
+{
+ gboolean ret = TRUE;
+ guint16 *red = NULL;
+ guint16 *green = NULL;
+ guint16 *blue = NULL;
+ guint i;
+ GnomeRROutputClutItem *data;
+ GnomeRRCrtc *crtc;
+
+ /* no length? */
+ if (array->len == 0) {
+ ret = FALSE;
+ g_set_error_literal (error,
+ GSD_COLOR_MANAGER_ERROR,
+ GSD_COLOR_MANAGER_ERROR_FAILED,
+ "no data in the CLUT array");
+ goto out;
+ }
+
+ /* convert to a type X understands */
+ red = g_new (guint16, array->len);
+ green = g_new (guint16, array->len);
+ blue = g_new (guint16, array->len);
+ for (i = 0; i < array->len; i++) {
+ data = g_ptr_array_index (array, i);
+ red[i] = data->red;
+ green[i] = data->green;
+ blue[i] = data->blue;
+ }
+
+ /* send to LUT */
+ crtc = gnome_rr_output_get_crtc (output);
+ if (crtc == NULL) {
+ ret = FALSE;
+ g_set_error (error,
+ GSD_COLOR_MANAGER_ERROR,
+ GSD_COLOR_MANAGER_ERROR_FAILED,
+ "failed to get ctrc for %s",
+ gnome_rr_output_get_name (output));
+ goto out;
+ }
+ gnome_rr_crtc_set_gamma (crtc, array->len,
+ red, green, blue);
+out:
+ g_free (red);
+ g_free (green);
+ g_free (blue);
+ return ret;
+}
+
+static gboolean
+gcm_session_device_set_gamma (GnomeRROutput *output,
+ CdProfile *profile,
+ guint color_temperature,
+ GError **error)
+{
+ gboolean ret = FALSE;
+ guint size;
+ GPtrArray *clut = NULL;
+
+ /* create a lookup table */
+ size = gnome_rr_output_get_gamma_size (output);
+ if (size == 0) {
+ ret = TRUE;
+ goto out;
+ }
+ clut = gcm_session_generate_vcgt (profile, color_temperature, size);
+ if (clut == NULL) {
+ g_set_error_literal (error,
+ GSD_COLOR_MANAGER_ERROR,
+ GSD_COLOR_MANAGER_ERROR_FAILED,
+ "failed to generate vcgt");
+ goto out;
+ }
+
+ /* apply the vcgt to this output */
+ ret = gcm_session_output_set_gamma (output, clut, error);
+ if (!ret)
+ goto out;
+out:
+ if (clut != NULL)
+ g_ptr_array_unref (clut);
+ return ret;
+}
+
+static gboolean
+gcm_session_device_reset_gamma (GnomeRROutput *output,
+ guint color_temperature,
+ GError **error)
+{
+ gboolean ret;
+ guint i;
+ guint size;
+ guint32 value;
+ GPtrArray *clut;
+ GnomeRROutputClutItem *data;
+ CdColorRGB temp;
+
+ /* create a linear ramp */
+ g_debug ("falling back to dummy ramp");
+ clut = g_ptr_array_new_with_free_func (g_free);
+ size = gnome_rr_output_get_gamma_size (output);
+ if (size == 0) {
+ ret = TRUE;
+ goto out;
+ }
+
+ /* get the color temperature */
+ if (!cd_color_get_blackbody_rgb_full (color_temperature,
+ &temp,
+ CD_COLOR_BLACKBODY_FLAG_USE_PLANCKIAN)) {
+ g_warning ("failed to get blackbody for %uK", color_temperature);
+ cd_color_rgb_set (&temp, 1.0, 1.0, 1.0);
+ } else {
+ g_debug ("using reset gamma of %uK = %.1f,%.1f,%.1f",
+ color_temperature, temp.R, temp.G, temp.B);
+ }
+
+ for (i = 0; i < size; i++) {
+ value = (i * 0xffff) / (size - 1);
+ data = g_new0 (GnomeRROutputClutItem, 1);
+ data->red = value * temp.R;
+ data->green = value * temp.G;
+ data->blue = value * temp.B;
+ g_ptr_array_add (clut, data);
+ }
+
+ /* apply the vcgt to this output */
+ ret = gcm_session_output_set_gamma (output, clut, error);
+ if (!ret)
+ goto out;
+out:
+ g_ptr_array_unref (clut);
+ return ret;
+}
+
+static GnomeRROutput *
+gcm_session_get_state_output_by_id (GsdColorState *state,
+ const gchar *device_id,
+ GError **error)
+{
+ gchar *output_id;
+ GnomeRROutput *output = NULL;
+ GnomeRROutput **outputs = NULL;
+ guint i;
+
+ /* search all STATE outputs for the device id */
+ outputs = gnome_rr_screen_list_outputs (state->state_screen);
+ if (outputs == NULL) {
+ g_set_error_literal (error,
+ GSD_COLOR_MANAGER_ERROR,
+ GSD_COLOR_MANAGER_ERROR_FAILED,
+ "Failed to get outputs");
+ goto out;
+ }
+ for (i = 0; outputs[i] != NULL && output == NULL; i++) {
+ output_id = gcm_session_get_output_id (state, outputs[i]);
+ if (g_strcmp0 (output_id, device_id) == 0)
+ output = outputs[i];
+ g_free (output_id);
+ }
+ if (output == NULL) {
+ g_set_error (error,
+ GSD_COLOR_MANAGER_ERROR,
+ GSD_COLOR_MANAGER_ERROR_FAILED,
+ "Failed to find output %s",
+ device_id);
+ }
+out:
+ return output;
+}
+
+/* this function is more complicated than it should be, due to the
+ * fact that XOrg sometimes assigns no primary devices when using
+ * "xrandr --auto" or when the version of RANDR is < 1.3 */
+static gboolean
+gcm_session_use_output_profile_for_screen (GsdColorState *state,
+ GnomeRROutput *output)
+{
+ gboolean has_laptop = FALSE;
+ gboolean has_primary = FALSE;
+ GnomeRROutput **outputs;
+ GnomeRROutput *connected = NULL;
+ guint i;
+
+ /* do we have any screens marked as primary */
+ outputs = gnome_rr_screen_list_outputs (state->state_screen);
+ if (outputs == NULL || outputs[0] == NULL) {
+ g_warning ("failed to get outputs");
+ return FALSE;
+ }
+ for (i = 0; outputs[i] != NULL; i++) {
+ if (connected == NULL)
+ connected = outputs[i];
+ if (gnome_rr_output_get_is_primary (outputs[i]))
+ has_primary = TRUE;
+ if (gnome_rr_output_is_builtin_display (outputs[i]))
+ has_laptop = TRUE;
+ }
+
+ /* we have an assigned primary device, are we that? */
+ if (has_primary)
+ return gnome_rr_output_get_is_primary (output);
+
+ /* choosing the internal panel is probably sane */
+ if (has_laptop)
+ return gnome_rr_output_is_builtin_display (output);
+
+ /* we have to choose one, so go for the first connected device */
+ if (connected != NULL)
+ return gnome_rr_output_get_id (connected) == gnome_rr_output_get_id (output);
+
+ return FALSE;
+}
+
+/* TODO: remove when we can dep on a released version of colord */
+#ifndef CD_PROFILE_METADATA_SCREEN_BRIGHTNESS
+#define CD_PROFILE_METADATA_SCREEN_BRIGHTNESS "SCREEN_brightness"
+#endif
+
+#define GSD_DBUS_NAME_POWER GSD_DBUS_NAME ".Power"
+#define GSD_DBUS_INTERFACE_POWER_SCREEN GSD_DBUS_BASE_INTERFACE ".Power.Screen"
+#define GSD_DBUS_PATH_POWER GSD_DBUS_PATH "/Power"
+
+static void
+gcm_session_set_output_percentage (guint percentage)
+{
+ GDBusConnection *connection;
+
+ /* get a ref to the existing bus connection */
+ connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL);
+ if (connection == NULL)
+ return;
+ g_dbus_connection_call (connection,
+ GSD_DBUS_NAME_POWER,
+ GSD_DBUS_PATH_POWER,
+ "org.freedesktop.DBus.Properties",
+ "Set",
+ g_variant_new_parsed ("('" GSD_DBUS_INTERFACE_POWER_SCREEN "',"
+ "'Brightness', %v)",
+ g_variant_new_int32 (percentage)),
+ NULL,
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, NULL, NULL);
+ g_object_unref (connection);
+}
+
+static void
+gcm_session_device_assign_profile_connect_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ CdProfile *profile = CD_PROFILE (object);
+ const gchar *brightness_profile;
+ const gchar *filename;
+ gboolean ret;
+ GError *error = NULL;
+ GnomeRROutput *output;
+ guint brightness_percentage;
+ GcmSessionAsyncHelper *helper = (GcmSessionAsyncHelper *) user_data;
+ GsdColorState *state = GSD_COLOR_STATE (helper->state);
+
+ /* get properties */
+ ret = cd_profile_connect_finish (profile, res, &error);
+ if (!ret) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("failed to connect to profile: %s", error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ /* get the filename */
+ filename = cd_profile_get_filename (profile);
+ g_assert (filename != NULL);
+
+ /* get the output (can't save in helper as GnomeRROutput isn't
+ * a GObject, just a pointer */
+ output = gnome_rr_screen_get_output_by_id (state->state_screen,
+ helper->output_id);
+ if (output == NULL)
+ goto out;
+
+ /* if output is a laptop screen and the profile has a
+ * calibration brightness then set this new brightness */
+ brightness_profile = cd_profile_get_metadata_item (profile,
+ CD_PROFILE_METADATA_SCREEN_BRIGHTNESS);
+ if (gnome_rr_output_is_builtin_display (output) &&
+ brightness_profile != NULL) {
+ /* the percentage is stored in the profile metadata as
+ * a string, not ideal, but it's all we have... */
+ brightness_percentage = atoi (brightness_profile);
+ gcm_session_set_output_percentage (brightness_percentage);
+ }
+
+ /* set the _ICC_PROFILE atom */
+ ret = gcm_session_use_output_profile_for_screen (state, output);
+ if (ret) {
+ ret = gcm_session_screen_set_icc_profile (state,
+ filename,
+ &error);
+ if (!ret) {
+ g_warning ("failed to set screen _ICC_PROFILE: %s",
+ error->message);
+ g_clear_error (&error);
+ }
+ }
+
+ /* create a vcgt for this icc file */
+ ret = cd_profile_get_has_vcgt (profile);
+ if (ret) {
+ ret = gcm_session_device_set_gamma (output,
+ profile,
+ state->color_temperature,
+ &error);
+ if (!ret) {
+ g_warning ("failed to set %s gamma tables: %s",
+ cd_device_get_id (helper->device),
+ error->message);
+ g_error_free (error);
+ goto out;
+ }
+ } else {
+ ret = gcm_session_device_reset_gamma (output,
+ state->color_temperature,
+ &error);
+ if (!ret) {
+ g_warning ("failed to reset %s gamma tables: %s",
+ cd_device_get_id (helper->device),
+ error->message);
+ g_error_free (error);
+ goto out;
+ }
+ }
+out:
+ gcm_session_async_helper_free (helper);
+}
+
+/*
+ * Check to see if the on-disk profile has the MAPPING_device_id
+ * metadata, and if not, we should delete the profile and re-create it
+ * so that it gets mapped by the daemon.
+ */
+static gboolean
+gcm_session_check_profile_device_md (GFile *file)
+{
+ const gchar *key_we_need = CD_PROFILE_METADATA_MAPPING_DEVICE_ID;
+ CdIcc *icc;
+ gboolean ret;
+
+ icc = cd_icc_new ();
+ ret = cd_icc_load_file (icc, file, CD_ICC_LOAD_FLAGS_METADATA, NULL, NULL);
+ if (!ret)
+ goto out;
+ ret = cd_icc_get_metadata_item (icc, key_we_need) != NULL;
+ if (!ret) {
+ g_debug ("auto-edid profile is old, and contains no %s data",
+ key_we_need);
+ }
+out:
+ g_object_unref (icc);
+ return ret;
+}
+
+static void
+gcm_session_device_assign_connect_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ CdDeviceKind kind;
+ CdProfile *profile = NULL;
+ gboolean ret;
+ gchar *autogen_filename = NULL;
+ gchar *autogen_path = NULL;
+ GcmEdid *edid = NULL;
+ GnomeRROutput *output = NULL;
+ GError *error = NULL;
+ GFile *file = NULL;
+ const gchar *xrandr_id;
+ GcmSessionAsyncHelper *helper;
+ CdDevice *device = CD_DEVICE (object);
+ GsdColorState *state = GSD_COLOR_STATE (user_data);
+
+ /* remove from assign array */
+ g_hash_table_remove (state->device_assign_hash,
+ cd_device_get_object_path (device));
+
+ /* get properties */
+ ret = cd_device_connect_finish (device, res, &error);
+ if (!ret) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("failed to connect to device: %s", error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ /* check we care */
+ kind = cd_device_get_kind (device);
+ if (kind != CD_DEVICE_KIND_DISPLAY)
+ goto out;
+
+ g_debug ("need to assign display device %s",
+ cd_device_get_id (device));
+
+ /* get the GnomeRROutput for the device id */
+ xrandr_id = cd_device_get_id (device);
+ output = gcm_session_get_state_output_by_id (state,
+ xrandr_id,
+ &error);
+ if (output == NULL) {
+ g_warning ("no %s device found: %s",
+ cd_device_get_id (device),
+ error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ /* create profile from device edid if it exists */
+ edid = gcm_session_get_output_edid (state, output, &error);
+ if (edid == NULL) {
+ g_warning ("unable to get EDID for %s: %s",
+ cd_device_get_id (device),
+ error->message);
+ g_clear_error (&error);
+
+ } else {
+ autogen_filename = g_strdup_printf ("edid-%s.icc",
+ gcm_edid_get_checksum (edid));
+ autogen_path = g_build_filename (g_get_user_data_dir (),
+ "icc", autogen_filename, NULL);
+
+ /* check if auto-profile has up-to-date metadata */
+ file = g_file_new_for_path (autogen_path);
+ if (gcm_session_check_profile_device_md (file)) {
+ g_debug ("auto-profile edid %s exists with md", autogen_path);
+ } else {
+ g_debug ("auto-profile edid does not exist, creating as %s",
+ autogen_path);
+
+ /* check if the system has a built-in profile */
+ ret = gnome_rr_output_is_builtin_display (output) &&
+ gcm_get_system_icc_profile (state, file);
+
+ /* try creating one from the EDID */
+ if (!ret) {
+ ret = gcm_apply_create_icc_profile_for_edid (state,
+ device,
+ edid,
+ file,
+ &error);
+ }
+
+ if (!ret) {
+ g_warning ("failed to create profile from EDID data: %s",
+ error->message);
+ g_clear_error (&error);
+ }
+ }
+ }
+
+ /* get the default profile for the device */
+ profile = cd_device_get_default_profile (device);
+ if (profile == NULL) {
+ g_debug ("%s has no default profile to set",
+ cd_device_get_id (device));
+
+ /* the default output? */
+ if (gnome_rr_output_get_is_primary (output) &&
+ state->gdk_window != NULL) {
+ gdk_property_delete (state->gdk_window,
+ gdk_atom_intern_static_string ("_ICC_PROFILE"));
+ gdk_property_delete (state->gdk_window,
+ gdk_atom_intern_static_string ("_ICC_PROFILE_IN_X_VERSION"));
+ }
+
+ /* reset, as we want linear profiles for profiling */
+ ret = gcm_session_device_reset_gamma (output,
+ state->color_temperature,
+ &error);
+ if (!ret) {
+ g_warning ("failed to reset %s gamma tables: %s",
+ cd_device_get_id (device),
+ error->message);
+ g_error_free (error);
+ goto out;
+ }
+ goto out;
+ }
+
+ /* get properties */
+ helper = g_new0 (GcmSessionAsyncHelper, 1);
+ helper->output_id = gnome_rr_output_get_id (output);
+ helper->state = g_object_ref (state);
+ helper->device = g_object_ref (device);
+ cd_profile_connect (profile,
+ state->cancellable,
+ gcm_session_device_assign_profile_connect_cb,
+ helper);
+out:
+ g_free (autogen_filename);
+ g_free (autogen_path);
+ if (file != NULL)
+ g_object_unref (file);
+ if (edid != NULL)
+ g_object_unref (edid);
+ if (profile != NULL)
+ g_object_unref (profile);
+}
+
+static void
+gcm_session_device_assign (GsdColorState *state, CdDevice *device)
+{
+ const gchar *key;
+ gpointer found;
+
+ /* are we already assigning this device */
+ key = cd_device_get_object_path (device);
+ found = g_hash_table_lookup (state->device_assign_hash, key);
+ if (found != NULL) {
+ g_debug ("assign for %s already in progress", key);
+ return;
+ }
+ g_hash_table_insert (state->device_assign_hash,
+ g_strdup (key),
+ GINT_TO_POINTER (TRUE));
+ cd_device_connect (device,
+ state->cancellable,
+ gcm_session_device_assign_connect_cb,
+ state);
+}
+
+static void
+gcm_session_device_added_assign_cb (CdClient *client,
+ CdDevice *device,
+ GsdColorState *state)
+{
+ gcm_session_device_assign (state, device);
+}
+
+static void
+gcm_session_device_changed_assign_cb (CdClient *client,
+ CdDevice *device,
+ GsdColorState *state)
+{
+ g_debug ("%s changed", cd_device_get_object_path (device));
+ gcm_session_device_assign (state, device);
+}
+
+static void
+gcm_session_create_device_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ CdDevice *device;
+ GError *error = NULL;
+
+ device = cd_client_create_device_finish (CD_CLIENT (object),
+ res,
+ &error);
+ if (device == NULL) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
+ !g_error_matches (error, CD_CLIENT_ERROR, CD_CLIENT_ERROR_ALREADY_EXISTS))
+ g_warning ("failed to create device: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+ g_object_unref (device);
+}
+
+static void
+gcm_session_add_state_output (GsdColorState *state, GnomeRROutput *output)
+{
+ const gchar *edid_checksum = NULL;
+ const gchar *model = NULL;
+ const gchar *output_name = NULL;
+ const gchar *serial = NULL;
+ const gchar *vendor = NULL;
+ gboolean ret;
+ gchar *device_id = NULL;
+ GcmEdid *edid;
+ GError *error = NULL;
+ GHashTable *device_props = NULL;
+
+ /* VNC creates a fake device that cannot be color managed */
+ output_name = gnome_rr_output_get_name (output);
+ if (output_name != NULL && g_str_has_prefix (output_name, "VNC-")) {
+ g_debug ("ignoring %s as fake VNC device detected", output_name);
+ return;
+ }
+
+ /* try to get edid */
+ edid = gcm_session_get_output_edid (state, output, &error);
+ if (edid == NULL) {
+ g_warning ("failed to get edid: %s",
+ error->message);
+ g_clear_error (&error);
+ }
+
+ /* prefer DMI data for the internal output */
+ ret = gnome_rr_output_is_builtin_display (output);
+ if (ret) {
+ model = cd_client_get_system_model (state->client);
+ vendor = cd_client_get_system_vendor (state->client);
+ }
+
+ /* use EDID data if we have it */
+ if (edid != NULL) {
+ edid_checksum = gcm_edid_get_checksum (edid);
+ if (model == NULL)
+ model = gcm_edid_get_monitor_name (edid);
+ if (vendor == NULL)
+ vendor = gcm_edid_get_vendor_name (edid);
+ if (serial == NULL)
+ serial = gcm_edid_get_serial_number (edid);
+ }
+
+ /* ensure mandatory fields are set */
+ if (model == NULL)
+ model = gnome_rr_output_get_name (output);
+ if (vendor == NULL)
+ vendor = "unknown";
+ if (serial == NULL)
+ serial = "unknown";
+
+ device_id = gcm_session_get_output_id (state, output);
+ g_debug ("output %s added", device_id);
+ device_props = g_hash_table_new_full (g_str_hash, g_str_equal,
+ NULL, NULL);
+ g_hash_table_insert (device_props,
+ (gpointer) CD_DEVICE_PROPERTY_KIND,
+ (gpointer) cd_device_kind_to_string (CD_DEVICE_KIND_DISPLAY));
+ g_hash_table_insert (device_props,
+ (gpointer) CD_DEVICE_PROPERTY_MODE,
+ (gpointer) cd_device_mode_to_string (CD_DEVICE_MODE_PHYSICAL));
+ g_hash_table_insert (device_props,
+ (gpointer) CD_DEVICE_PROPERTY_COLORSPACE,
+ (gpointer) cd_colorspace_to_string (CD_COLORSPACE_RGB));
+ g_hash_table_insert (device_props,
+ (gpointer) CD_DEVICE_PROPERTY_VENDOR,
+ (gpointer) vendor);
+ g_hash_table_insert (device_props,
+ (gpointer) CD_DEVICE_PROPERTY_MODEL,
+ (gpointer) model);
+ g_hash_table_insert (device_props,
+ (gpointer) CD_DEVICE_PROPERTY_SERIAL,
+ (gpointer) serial);
+ g_hash_table_insert (device_props,
+ (gpointer) CD_DEVICE_METADATA_XRANDR_NAME,
+ (gpointer) gnome_rr_output_get_name (output));
+ g_hash_table_insert (device_props,
+ (gpointer) CD_DEVICE_METADATA_OUTPUT_PRIORITY,
+ gnome_rr_output_get_is_primary (output) ?
+ (gpointer) CD_DEVICE_METADATA_OUTPUT_PRIORITY_PRIMARY :
+ (gpointer) CD_DEVICE_METADATA_OUTPUT_PRIORITY_SECONDARY);
+ if (edid_checksum != NULL) {
+ g_hash_table_insert (device_props,
+ (gpointer) CD_DEVICE_METADATA_OUTPUT_EDID_MD5,
+ (gpointer) edid_checksum);
+ }
+ /* set this so we can call the device a 'Laptop Screen' in the
+ * control center main panel */
+ if (gnome_rr_output_is_builtin_display (output)) {
+ g_hash_table_insert (device_props,
+ (gpointer) CD_DEVICE_PROPERTY_EMBEDDED,
+ NULL);
+ }
+ cd_client_create_device (state->client,
+ device_id,
+ CD_OBJECT_SCOPE_TEMP,
+ device_props,
+ state->cancellable,
+ gcm_session_create_device_cb,
+ state);
+ g_free (device_id);
+ if (device_props != NULL)
+ g_hash_table_unref (device_props);
+ if (edid != NULL)
+ g_object_unref (edid);
+}
+
+
+static void
+gnome_rr_screen_output_added_cb (GnomeRRScreen *screen,
+ GnomeRROutput *output,
+ GsdColorState *state)
+{
+ gcm_session_add_state_output (state, output);
+}
+
+static void
+gcm_session_screen_removed_delete_device_cb (GObject *object, GAsyncResult *res, gpointer user_data)
+{
+ gboolean ret;
+ GError *error = NULL;
+
+ /* deleted device */
+ ret = cd_client_delete_device_finish (CD_CLIENT (object),
+ res,
+ &error);
+ if (!ret) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("failed to delete device: %s", error->message);
+ g_error_free (error);
+ }
+}
+
+static void
+gcm_session_screen_removed_find_device_cb (GObject *object, GAsyncResult *res, gpointer user_data)
+{
+ GError *error = NULL;
+ CdDevice *device;
+ GsdColorState *state = GSD_COLOR_STATE (user_data);
+
+ device = cd_client_find_device_finish (state->client,
+ res,
+ &error);
+ if (device == NULL) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("failed to find device: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+ g_debug ("output %s found, and will be removed",
+ cd_device_get_object_path (device));
+ cd_client_delete_device (state->client,
+ device,
+ state->cancellable,
+ gcm_session_screen_removed_delete_device_cb,
+ state);
+ g_object_unref (device);
+}
+
+static void
+gnome_rr_screen_output_removed_cb (GnomeRRScreen *screen,
+ GnomeRROutput *output,
+ GsdColorState *state)
+{
+ g_debug ("output %s removed",
+ gnome_rr_output_get_name (output));
+ g_hash_table_remove (state->edid_cache,
+ gnome_rr_output_get_name (output));
+ cd_client_find_device_by_property (state->client,
+ CD_DEVICE_METADATA_XRANDR_NAME,
+ gnome_rr_output_get_name (output),
+ state->cancellable,
+ gcm_session_screen_removed_find_device_cb,
+ state);
+}
+
+static void
+gcm_session_get_devices_cb (GObject *object, GAsyncResult *res, gpointer user_data)
+{
+ CdDevice *device;
+ GError *error = NULL;
+ GPtrArray *array;
+ guint i;
+ GsdColorState *state = GSD_COLOR_STATE (user_data);
+
+ array = cd_client_get_devices_finish (CD_CLIENT (object), res, &error);
+ if (array == NULL) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("failed to get devices: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+ for (i = 0; i < array->len; i++) {
+ device = g_ptr_array_index (array, i);
+ gcm_session_device_assign (state, device);
+ }
+
+ if (array != NULL)
+ g_ptr_array_unref (array);
+}
+
+static void
+gcm_session_profile_gamma_find_device_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ CdClient *client = CD_CLIENT (object);
+ CdDevice *device = NULL;
+ GError *error = NULL;
+ GsdColorState *state = GSD_COLOR_STATE (user_data);
+
+ device = cd_client_find_device_by_property_finish (client,
+ res,
+ &error);
+ if (device == NULL) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("could not find device: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ /* get properties */
+ cd_device_connect (device,
+ state->cancellable,
+ gcm_session_device_assign_connect_cb,
+ state);
+
+ if (device != NULL)
+ g_object_unref (device);
+}
+
+static void
+gcm_session_set_gamma_for_all_devices (GsdColorState *state)
+{
+ GnomeRROutput **outputs;
+ guint i;
+
+ /* setting the temperature before we get the list of devices is fine,
+ * as we use the temperature in the calculation */
+ if (state->state_screen == NULL)
+ return;
+
+ /* get STATE outputs */
+ outputs = gnome_rr_screen_list_outputs (state->state_screen);
+ if (outputs == NULL) {
+ g_warning ("failed to get outputs");
+ return;
+ }
+ for (i = 0; outputs[i] != NULL; i++) {
+ /* get CdDevice for this output */
+ cd_client_find_device_by_property (state->client,
+ CD_DEVICE_METADATA_XRANDR_NAME,
+ gnome_rr_output_get_name (outputs[i]),
+ state->cancellable,
+ gcm_session_profile_gamma_find_device_cb,
+ state);
+ }
+}
+
+/* We have to reset the gamma tables each time as if the primary output
+ * has changed then different crtcs are going to be used.
+ * See https://bugzilla.gnome.org/show_bug.cgi?id=660164 for an example */
+static void
+gnome_rr_screen_output_changed_cb (GnomeRRScreen *screen,
+ GsdColorState *state)
+{
+ gcm_session_set_gamma_for_all_devices (state);
+}
+
+static gboolean
+has_changed (char **strv,
+ const char *str)
+{
+ guint i;
+ for (i = 0; strv[i] != NULL; i++) {
+ if (g_str_equal (str, strv[i]))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+gcm_session_active_changed_cb (GDBusProxy *session,
+ GVariant *changed,
+ char **invalidated,
+ GsdColorState *state)
+{
+ GVariant *active_v = NULL;
+ gboolean is_active;
+
+ if (has_changed (invalidated, "SessionIsActive"))
+ return;
+
+ /* not yet connected to the daemon */
+ if (!cd_client_get_connected (state->client))
+ return;
+
+ active_v = g_dbus_proxy_get_cached_property (session, "SessionIsActive");
+ g_return_if_fail (active_v != NULL);
+ is_active = g_variant_get_boolean (active_v);
+ g_variant_unref (active_v);
+
+ /* When doing the fast-user-switch into a new account, load the
+ * new users chosen profiles.
+ *
+ * If this is the first time the GnomeSettingsSession has been
+ * loaded, then we'll get a change from unknown to active
+ * and we want to avoid reprobing the devices for that.
+ */
+ if (is_active && !state->session_is_active) {
+ g_debug ("Done switch to new account, reload devices");
+ cd_client_get_devices (state->client,
+ state->cancellable,
+ gcm_session_get_devices_cb,
+ state);
+ }
+ state->session_is_active = is_active;
+}
+
+static void
+gcm_session_client_connect_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ gboolean ret;
+ GError *error = NULL;
+ GnomeRROutput **outputs;
+ guint i;
+ GsdColorState *state = GSD_COLOR_STATE (user_data);
+
+ /* connected */
+ ret = cd_client_connect_finish (state->client, res, &error);
+ if (!ret) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("failed to connect to colord: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ /* is there an available colord instance? */
+ ret = cd_client_get_has_server (state->client);
+ if (!ret) {
+ g_warning ("There is no colord server available");
+ return;
+ }
+
+ /* watch if sessions change */
+ g_signal_connect_object (state->session, "g-properties-changed",
+ G_CALLBACK (gcm_session_active_changed_cb),
+ state, 0);
+
+ /* add screens */
+ gnome_rr_screen_refresh (state->state_screen, &error);
+ if (error != NULL) {
+ g_warning ("failed to refresh: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ /* get STATE outputs */
+ outputs = gnome_rr_screen_list_outputs (state->state_screen);
+ if (outputs == NULL) {
+ g_warning ("failed to get outputs");
+ return;
+ }
+ for (i = 0; outputs[i] != NULL; i++) {
+ gcm_session_add_state_output (state, outputs[i]);
+ }
+
+ /* only connect when colord is awake */
+ g_signal_connect (state->state_screen, "output-connected",
+ G_CALLBACK (gnome_rr_screen_output_added_cb),
+ state);
+ g_signal_connect (state->state_screen, "output-disconnected",
+ G_CALLBACK (gnome_rr_screen_output_removed_cb),
+ state);
+ g_signal_connect (state->state_screen, "changed",
+ G_CALLBACK (gnome_rr_screen_output_changed_cb),
+ state);
+
+ g_signal_connect (state->client, "device-added",
+ G_CALLBACK (gcm_session_device_added_assign_cb),
+ state);
+ g_signal_connect (state->client, "device-changed",
+ G_CALLBACK (gcm_session_device_changed_assign_cb),
+ state);
+
+ /* set for each device that already exist */
+ cd_client_get_devices (state->client,
+ state->cancellable,
+ gcm_session_get_devices_cb,
+ state);
+}
+
+static void
+on_rr_screen_acquired (GObject *object,
+ GAsyncResult *result,
+ gpointer data)
+{
+ GsdColorState *state = data;
+ GnomeRRScreen *screen;
+ GError *error = NULL;
+
+ /* gnome_rr_screen_new_async() does not take a GCancellable */
+ if (g_cancellable_is_cancelled (state->cancellable))
+ goto out;
+
+ screen = gnome_rr_screen_new_finish (result, &error);
+ if (screen == NULL) {
+ g_warning ("failed to get screens: %s", error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ state->state_screen = screen;
+
+ cd_client_connect (state->client,
+ state->cancellable,
+ gcm_session_client_connect_cb,
+ state);
+out:
+ /* manually added */
+ g_object_unref (state);
+}
+
+void
+gsd_color_state_start (GsdColorState *state)
+{
+ /* use a fresh cancellable for each start->stop operation */
+ g_cancellable_cancel (state->cancellable);
+ g_clear_object (&state->cancellable);
+ state->cancellable = g_cancellable_new ();
+
+ /* coldplug the list of screens */
+ gnome_rr_screen_new_async (gdk_screen_get_default (),
+ on_rr_screen_acquired,
+ g_object_ref (state));
+}
+
+void
+gsd_color_state_stop (GsdColorState *state)
+{
+ g_cancellable_cancel (state->cancellable);
+}
+
+static void
+gsd_color_state_class_init (GsdColorStateClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = gsd_color_state_finalize;
+}
+
+static void
+gsd_color_state_init (GsdColorState *state)
+{
+ /* track the active session */
+ state->session = gnome_settings_bus_get_session_proxy ();
+
+#ifdef GDK_WINDOWING_X11
+ /* set the _ICC_PROFILE atoms on the root screen */
+ if (GDK_IS_X11_DISPLAY (gdk_display_get_default ()))
+ state->gdk_window = gdk_screen_get_root_window (gdk_screen_get_default ());
+#endif
+
+ /* parsing the EDID is expensive */
+ state->edid_cache = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ g_free,
+ g_object_unref);
+
+ /* we don't want to assign devices multiple times at startup */
+ state->device_assign_hash = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ g_free,
+ NULL);
+
+ /* default color temperature */
+ state->color_temperature = GSD_COLOR_TEMPERATURE_DEFAULT;
+
+ state->client = cd_client_new ();
+}
+
+static void
+gsd_color_state_finalize (GObject *object)
+{
+ GsdColorState *state;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GSD_IS_COLOR_STATE (object));
+
+ state = GSD_COLOR_STATE (object);
+
+ g_cancellable_cancel (state->cancellable);
+ g_clear_object (&state->cancellable);
+ g_clear_object (&state->client);
+ g_clear_object (&state->session);
+ g_clear_pointer (&state->edid_cache, g_hash_table_destroy);
+ g_clear_pointer (&state->device_assign_hash, g_hash_table_destroy);
+ g_clear_object (&state->state_screen);
+
+ G_OBJECT_CLASS (gsd_color_state_parent_class)->finalize (object);
+}
+
+GsdColorState *
+gsd_color_state_new (void)
+{
+ GsdColorState *state;
+ state = g_object_new (GSD_TYPE_COLOR_STATE, NULL);
+ return GSD_COLOR_STATE (state);
+}
diff --git a/plugins/color/gsd-color-state.h b/plugins/color/gsd-color-state.h
new file mode 100644
index 0000000..f1c8827
--- /dev/null
+++ b/plugins/color/gsd-color-state.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2007 William Jon McCann <mccann@jhu.edu>
+ * Copyright (C) 2011-2013 Richard Hughes <richard@hughsie.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef __GSD_COLOR_STATE_H
+#define __GSD_COLOR_STATE_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GSD_TYPE_COLOR_STATE (gsd_color_state_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsdColorState, gsd_color_state, GSD, COLOR_STATE, GObject)
+
+#define GSD_COLOR_TEMPERATURE_MIN 1000 /* Kelvin */
+#define GSD_COLOR_TEMPERATURE_DEFAULT 6500 /* Kelvin, is RGB [1.0,1.0,1.0] */
+#define GSD_COLOR_TEMPERATURE_MAX 10000 /* Kelvin */
+
+GQuark gsd_color_state_error_quark (void);
+
+GsdColorState * gsd_color_state_new (void);
+void gsd_color_state_start (GsdColorState *state);
+void gsd_color_state_stop (GsdColorState *state);
+void gsd_color_state_set_temperature (GsdColorState *state,
+ guint temperature);
+guint gsd_color_state_get_temperature (GsdColorState *state);
+
+G_END_DECLS
+
+#endif /* __GSD_COLOR_STATE_H */
diff --git a/plugins/color/gsd-night-light-common.c b/plugins/color/gsd-night-light-common.c
new file mode 100644
index 0000000..5fe756e
--- /dev/null
+++ b/plugins/color/gsd-night-light-common.c
@@ -0,0 +1,137 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include <glib.h>
+#include <math.h>
+
+#include "gsd-night-light-common.h"
+
+static gdouble
+deg2rad (gdouble degrees)
+{
+ return (M_PI * degrees) / 180.f;
+}
+
+static gdouble
+rad2deg (gdouble radians)
+{
+ return radians * (180.f / M_PI);
+}
+
+/*
+ * Formulas taken from https://www.esrl.noaa.gov/gmd/grad/solcalc/calcdetails.html
+ *
+ * The returned values are fractional hours, so 6am would be 6.0 and 4:30pm
+ * would be 16.5.
+ *
+ * The values returned by this function might not make sense for locations near
+ * the polar regions. For example, in the north of Lapland there might not be
+ * a sunrise at all.
+ */
+gboolean
+gsd_night_light_get_sunrise_sunset (GDateTime *dt,
+ gdouble pos_lat, gdouble pos_long,
+ gdouble *sunrise, gdouble *sunset)
+{
+ g_autoptr(GDateTime) dt_zero = g_date_time_new_utc (1900, 1, 1, 0, 0, 0);
+ GTimeSpan ts = g_date_time_difference (dt, dt_zero);
+
+ g_return_val_if_fail (pos_lat <= 90.f && pos_lat >= -90.f, FALSE);
+ g_return_val_if_fail (pos_long <= 180.f && pos_long >= -180.f, FALSE);
+
+ gdouble tz_offset = (gdouble) g_date_time_get_utc_offset (dt) / G_USEC_PER_SEC / 60 / 60; // B5
+ gdouble date_as_number = ts / G_USEC_PER_SEC / 24 / 60 / 60 + 2; // B7
+ gdouble time_past_local_midnight = 0; // E2, unused in this calculation
+ gdouble julian_day = date_as_number + 2415018.5 +
+ time_past_local_midnight - tz_offset / 24;
+ gdouble julian_century = (julian_day - 2451545) / 36525;
+ gdouble geom_mean_long_sun = fmod (280.46646 + julian_century *
+ (36000.76983 + julian_century * 0.0003032), 360); // I2
+ gdouble geom_mean_anom_sun = 357.52911 + julian_century *
+ (35999.05029 - 0.0001537 * julian_century); // J2
+ gdouble eccent_earth_orbit = 0.016708634 - julian_century *
+ (0.000042037 + 0.0000001267 * julian_century); // K2
+ gdouble sun_eq_of_ctr = sin (deg2rad (geom_mean_anom_sun)) *
+ (1.914602 - julian_century * (0.004817 + 0.000014 * julian_century)) +
+ sin (deg2rad (2 * geom_mean_anom_sun)) * (0.019993 - 0.000101 * julian_century) +
+ sin (deg2rad (3 * geom_mean_anom_sun)) * 0.000289; // L2
+ gdouble sun_true_long = geom_mean_long_sun + sun_eq_of_ctr; // M2
+ gdouble sun_app_long = sun_true_long - 0.00569 - 0.00478 *
+ sin (deg2rad (125.04 - 1934.136 * julian_century)); // P2
+ gdouble mean_obliq_ecliptic = 23 + (26 + ((21.448 - julian_century *
+ (46.815 + julian_century * (0.00059 - julian_century * 0.001813)))) / 60) / 60; // Q2
+ gdouble obliq_corr = mean_obliq_ecliptic + 0.00256 *
+ cos (deg2rad (125.04 - 1934.136 * julian_century)); // R2
+ gdouble sun_declin = rad2deg (asin (sin (deg2rad (obliq_corr)) *
+ sin (deg2rad (sun_app_long)))); // T2
+ gdouble var_y = tan (deg2rad (obliq_corr/2)) * tan (deg2rad (obliq_corr / 2)); // U2
+ gdouble eq_of_time = 4 * rad2deg (var_y * sin (2 * deg2rad (geom_mean_long_sun)) -
+ 2 * eccent_earth_orbit * sin (deg2rad (geom_mean_anom_sun)) +
+ 4 * eccent_earth_orbit * var_y *
+ sin (deg2rad (geom_mean_anom_sun)) *
+ cos (2 * deg2rad (geom_mean_long_sun)) -
+ 0.5 * var_y * var_y * sin (4 * deg2rad (geom_mean_long_sun)) -
+ 1.25 * eccent_earth_orbit * eccent_earth_orbit *
+ sin (2 * deg2rad (geom_mean_anom_sun))); // V2
+ gdouble ha_sunrise = rad2deg (acos (cos (deg2rad (90.833)) / (cos (deg2rad (pos_lat)) *
+ cos (deg2rad (sun_declin))) - tan (deg2rad (pos_lat)) *
+ tan (deg2rad (sun_declin)))); // W2
+ gdouble solar_noon = (720 - 4 * pos_long - eq_of_time + tz_offset * 60) / 1440; // X2
+ gdouble sunrise_time = solar_noon - ha_sunrise * 4 / 1440; // Y2
+ gdouble sunset_time = solar_noon + ha_sunrise * 4 / 1440; // Z2
+
+ /* convert to hours */
+ if (sunrise != NULL)
+ *sunrise = sunrise_time * 24;
+ if (sunset != NULL)
+ *sunset = sunset_time * 24;
+ return TRUE;
+}
+
+gdouble
+gsd_night_light_frac_day_from_dt (GDateTime *dt)
+{
+ return g_date_time_get_hour (dt) +
+ (gdouble) g_date_time_get_minute (dt) / 60.f +
+ (gdouble) g_date_time_get_second (dt) / 3600.f;
+}
+
+gboolean
+gsd_night_light_frac_day_is_between (gdouble value,
+ gdouble start,
+ gdouble end)
+{
+ /* wrap end to the next day if it is before start,
+ * considering equal values as a full 24h period
+ */
+ if (end <= start)
+ end += 24;
+
+ /* wrap value to the next day if it is before the range */
+ if (value < start && value < end)
+ value += 24;
+
+ /* Check whether value falls into range; together with the 24h
+ * wrap around above this means that TRUE is always returned when
+ * start == end.
+ */
+ return value >= start && value < end;
+}
diff --git a/plugins/color/gsd-night-light-common.h b/plugins/color/gsd-night-light-common.h
new file mode 100644
index 0000000..4995da5
--- /dev/null
+++ b/plugins/color/gsd-night-light-common.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef __GSD_NIGHT_LIGHT_COMMON_H
+#define __GSD_NIGHT_LIGHT_COMMON_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+gboolean gsd_night_light_get_sunrise_sunset (GDateTime *dt,
+ gdouble pos_lat,
+ gdouble pos_long,
+ gdouble *sunrise,
+ gdouble *sunset);
+gdouble gsd_night_light_frac_day_from_dt (GDateTime *dt);
+gboolean gsd_night_light_frac_day_is_between (gdouble value,
+ gdouble start,
+ gdouble end);
+
+G_END_DECLS
+
+#endif /* __GSD_NIGHT_LIGHT_COMMON_H */
diff --git a/plugins/color/gsd-night-light.c b/plugins/color/gsd-night-light.c
new file mode 100644
index 0000000..635f160
--- /dev/null
+++ b/plugins/color/gsd-night-light.c
@@ -0,0 +1,801 @@
+/*
+ * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <geoclue.h>
+
+#define GNOME_DESKTOP_USE_UNSTABLE_API
+#include "gnome-datetime-source.h"
+
+#include "gsd-color-state.h"
+
+#include "gsd-night-light.h"
+#include "gsd-night-light-common.h"
+
+struct _GsdNightLight {
+ GObject parent;
+ GSettings *settings;
+ gboolean forced;
+ gboolean disabled_until_tmw;
+ GDateTime *disabled_until_tmw_dt;
+ gboolean geoclue_enabled;
+ GSource *source;
+ guint validate_id;
+ GClueClient *geoclue_client;
+ GClueSimple *geoclue_simple;
+ GSettings *location_settings;
+ gdouble cached_sunrise;
+ gdouble cached_sunset;
+ gdouble cached_temperature;
+ gboolean cached_active;
+ gboolean smooth_enabled;
+ GTimer *smooth_timer;
+ guint smooth_id;
+ gdouble smooth_target_temperature;
+ GCancellable *cancellable;
+ GDateTime *datetime_override;
+};
+
+enum {
+ PROP_0,
+ PROP_ACTIVE,
+ PROP_SUNRISE,
+ PROP_SUNSET,
+ PROP_TEMPERATURE,
+ PROP_DISABLED_UNTIL_TMW,
+ PROP_FORCED,
+ PROP_LAST
+};
+
+#define GSD_NIGHT_LIGHT_SCHEDULE_TIMEOUT 5 /* seconds */
+#define GSD_NIGHT_LIGHT_POLL_TIMEOUT 60 /* seconds */
+#define GSD_NIGHT_LIGHT_POLL_SMEAR 1 /* hours */
+#define GSD_NIGHT_LIGHT_SMOOTH_SMEAR 5.f /* seconds */
+
+#define GSD_FRAC_DAY_MAX_DELTA (1.f/60.f) /* 1 minute */
+#define GSD_TEMPERATURE_MAX_DELTA (10.f) /* Kelvin */
+
+#define DESKTOP_ID "gnome-color-panel"
+
+static void poll_timeout_destroy (GsdNightLight *self);
+static void poll_timeout_create (GsdNightLight *self);
+static void night_light_recheck (GsdNightLight *self);
+
+G_DEFINE_TYPE (GsdNightLight, gsd_night_light, G_TYPE_OBJECT);
+
+static GDateTime *
+gsd_night_light_get_date_time_now (GsdNightLight *self)
+{
+ if (self->datetime_override != NULL)
+ return g_date_time_ref (self->datetime_override);
+ return g_date_time_new_now_local ();
+}
+
+void
+gsd_night_light_set_date_time_now (GsdNightLight *self, GDateTime *datetime)
+{
+ if (self->datetime_override != NULL)
+ g_date_time_unref (self->datetime_override);
+ self->datetime_override = g_date_time_ref (datetime);
+
+ night_light_recheck (self);
+}
+
+static void
+poll_smooth_destroy (GsdNightLight *self)
+{
+ if (self->smooth_id != 0) {
+ g_source_remove (self->smooth_id);
+ self->smooth_id = 0;
+ }
+ if (self->smooth_timer != NULL)
+ g_clear_pointer (&self->smooth_timer, g_timer_destroy);
+}
+
+void
+gsd_night_light_set_smooth_enabled (GsdNightLight *self,
+ gboolean smooth_enabled)
+{
+ /* ensure the timeout is stopped if called at runtime */
+ if (!smooth_enabled)
+ poll_smooth_destroy (self);
+ self->smooth_enabled = smooth_enabled;
+}
+
+static gdouble
+linear_interpolate (gdouble val1, gdouble val2, gdouble factor)
+{
+ g_return_val_if_fail (factor >= 0.f, -1.f);
+ g_return_val_if_fail (factor <= 1.f, -1.f);
+ return ((val1 - val2) * factor) + val2;
+}
+
+static gboolean
+update_cached_sunrise_sunset (GsdNightLight *self)
+{
+ gboolean ret = FALSE;
+ gdouble latitude;
+ gdouble longitude;
+ gdouble sunrise;
+ gdouble sunset;
+ g_autoptr(GVariant) tmp = NULL;
+ g_autoptr(GDateTime) dt_now = gsd_night_light_get_date_time_now (self);
+
+ /* calculate the sunrise/sunset for the location */
+ tmp = g_settings_get_value (self->settings, "night-light-last-coordinates");
+ g_variant_get (tmp, "(dd)", &latitude, &longitude);
+ if (latitude > 90.f || latitude < -90.f)
+ return FALSE;
+ if (longitude > 180.f || longitude < -180.f)
+ return FALSE;
+ if (!gsd_night_light_get_sunrise_sunset (dt_now, latitude, longitude,
+ &sunrise, &sunset)) {
+ g_warning ("failed to get sunset/sunrise for %.3f,%.3f",
+ longitude, longitude);
+ return FALSE;
+ }
+
+ /* anything changed */
+ if (ABS (self->cached_sunrise - sunrise) > GSD_FRAC_DAY_MAX_DELTA) {
+ self->cached_sunrise = sunrise;
+ g_object_notify (G_OBJECT (self), "sunrise");
+ ret = TRUE;
+ }
+ if (ABS (self->cached_sunset - sunset) > GSD_FRAC_DAY_MAX_DELTA) {
+ self->cached_sunset = sunset;
+ g_object_notify (G_OBJECT (self), "sunset");
+ ret = TRUE;
+ }
+ return ret;
+}
+
+static void
+gsd_night_light_set_temperature_internal (GsdNightLight *self, gdouble temperature)
+{
+ if (ABS (self->cached_temperature - temperature) <= GSD_TEMPERATURE_MAX_DELTA)
+ return;
+ self->cached_temperature = temperature;
+ g_object_notify (G_OBJECT (self), "temperature");
+}
+
+static gboolean
+gsd_night_light_smooth_cb (gpointer user_data)
+{
+ GsdNightLight *self = GSD_NIGHT_LIGHT (user_data);
+ gdouble tmp;
+ gdouble frac;
+
+ /* find fraction */
+ frac = g_timer_elapsed (self->smooth_timer, NULL) / GSD_NIGHT_LIGHT_SMOOTH_SMEAR;
+ if (frac >= 1.f) {
+ gsd_night_light_set_temperature_internal (self,
+ self->smooth_target_temperature);
+ self->smooth_id = 0;
+ return G_SOURCE_REMOVE;
+ }
+
+ /* set new temperature step using log curve */
+ tmp = self->smooth_target_temperature - self->cached_temperature;
+ tmp *= frac;
+ tmp += self->cached_temperature;
+ gsd_night_light_set_temperature_internal (self, tmp);
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+poll_smooth_create (GsdNightLight *self, gdouble temperature)
+{
+ g_assert (self->smooth_id == 0);
+ self->smooth_target_temperature = temperature;
+ self->smooth_timer = g_timer_new ();
+ self->smooth_id = g_timeout_add (50, gsd_night_light_smooth_cb, self);
+}
+
+static void
+gsd_night_light_set_temperature (GsdNightLight *self, gdouble temperature)
+{
+ /* immediate */
+ if (!self->smooth_enabled) {
+ gsd_night_light_set_temperature_internal (self, temperature);
+ return;
+ }
+
+ /* Destroy any smooth transition, it will be recreated if neccessary */
+ poll_smooth_destroy (self);
+
+ /* small jump */
+ if (ABS (temperature - self->cached_temperature) < GSD_TEMPERATURE_MAX_DELTA) {
+ gsd_night_light_set_temperature_internal (self, temperature);
+ return;
+ }
+
+ /* smooth out the transition */
+ poll_smooth_create (self, temperature);
+}
+
+static void
+gsd_night_light_set_active (GsdNightLight *self, gboolean active)
+{
+ if (self->cached_active == active)
+ return;
+ self->cached_active = active;
+
+ /* ensure set to unity temperature */
+ if (!active)
+ gsd_night_light_set_temperature (self, GSD_COLOR_TEMPERATURE_DEFAULT);
+
+ g_object_notify (G_OBJECT (self), "active");
+}
+
+static void
+night_light_recheck (GsdNightLight *self)
+{
+ gdouble frac_day;
+ gdouble schedule_from = -1.f;
+ gdouble schedule_to = -1.f;
+ gdouble smear = GSD_NIGHT_LIGHT_POLL_SMEAR; /* hours */
+ guint temperature;
+ guint temp_smeared;
+ g_autoptr(GDateTime) dt_now = gsd_night_light_get_date_time_now (self);
+
+ /* Forced mode, just set the temperature to night light.
+ * Proper rechecking will happen once forced mode is disabled again */
+ if (self->forced) {
+ temperature = g_settings_get_uint (self->settings, "night-light-temperature");
+ gsd_night_light_set_temperature (self, temperature);
+ return;
+ }
+
+ /* enabled */
+ if (!g_settings_get_boolean (self->settings, "night-light-enabled")) {
+ g_debug ("night light disabled, resetting");
+ gsd_night_light_set_active (self, FALSE);
+ return;
+ }
+
+ /* calculate the position of the sun */
+ if (g_settings_get_boolean (self->settings, "night-light-schedule-automatic")) {
+ update_cached_sunrise_sunset (self);
+ if (self->cached_sunrise > 0.f && self->cached_sunset > 0.f) {
+ schedule_to = self->cached_sunrise;
+ schedule_from = self->cached_sunset;
+ }
+ }
+
+ /* fall back to manual settings */
+ if (schedule_to <= 0.f || schedule_from <= 0.f) {
+ schedule_from = g_settings_get_double (self->settings,
+ "night-light-schedule-from");
+ schedule_to = g_settings_get_double (self->settings,
+ "night-light-schedule-to");
+ }
+
+ /* get the current hour of a day as a fraction */
+ frac_day = gsd_night_light_frac_day_from_dt (dt_now);
+ g_debug ("fractional day = %.3f, limits = %.3f->%.3f",
+ frac_day, schedule_from, schedule_to);
+
+ /* disabled until tomorrow */
+ if (self->disabled_until_tmw) {
+ GTimeSpan time_span;
+ gboolean reset = FALSE;
+
+ time_span = g_date_time_difference (dt_now, self->disabled_until_tmw_dt);
+
+ /* Reset if disabled until tomorrow is more than 24h ago. */
+ if (time_span > (GTimeSpan) 24 * 60 * 60 * 1000000) {
+ g_debug ("night light disabled until tomorrow is older than 24h, resetting disabled until tomorrow");
+ reset = TRUE;
+ } else if (time_span > 0) {
+ /* Or if a sunrise lies between the time it was disabled and now. */
+ gdouble frac_disabled;
+ frac_disabled = gsd_night_light_frac_day_from_dt (self->disabled_until_tmw_dt);
+ if (frac_disabled != frac_day &&
+ gsd_night_light_frac_day_is_between (schedule_to,
+ frac_disabled,
+ frac_day)) {
+ g_debug ("night light sun rise happened, resetting disabled until tomorrow");
+ reset = TRUE;
+ }
+ }
+
+ if (reset) {
+ self->disabled_until_tmw = FALSE;
+ g_clear_pointer(&self->disabled_until_tmw_dt, g_date_time_unref);
+ g_object_notify (G_OBJECT (self), "disabled-until-tmw");
+ } else {
+ g_debug ("night light still day-disabled, resetting");
+ gsd_night_light_set_temperature (self,
+ GSD_COLOR_TEMPERATURE_DEFAULT);
+ return;
+ }
+ }
+
+ /* lower smearing period to be smaller than the time between start/stop */
+ smear = MIN (smear,
+ MIN ( ABS (schedule_to - schedule_from),
+ 24 - ABS (schedule_to - schedule_from)));
+
+ if (!gsd_night_light_frac_day_is_between (frac_day,
+ schedule_from - smear,
+ schedule_to)) {
+ g_debug ("not time for night-light");
+ gsd_night_light_set_active (self, FALSE);
+ return;
+ }
+
+ /* smear the temperature for a short duration before the set limits
+ *
+ * |----------------------| = from->to
+ * |-| = smear down
+ * |-| = smear up
+ *
+ * \ /
+ * \ /
+ * \--------------------/
+ */
+ temperature = g_settings_get_uint (self->settings, "night-light-temperature");
+ if (smear < 0.01) {
+ /* Don't try to smear for extremely short or zero periods */
+ temp_smeared = temperature;
+ } else if (gsd_night_light_frac_day_is_between (frac_day,
+ schedule_from - smear,
+ schedule_from)) {
+ gdouble factor = 1.f - ((frac_day - (schedule_from - smear)) / smear);
+ temp_smeared = linear_interpolate (GSD_COLOR_TEMPERATURE_DEFAULT,
+ temperature, factor);
+ } else if (gsd_night_light_frac_day_is_between (frac_day,
+ schedule_to - smear,
+ schedule_to)) {
+ gdouble factor = (frac_day - (schedule_to - smear)) / smear;
+ temp_smeared = linear_interpolate (GSD_COLOR_TEMPERATURE_DEFAULT,
+ temperature, factor);
+ } else {
+ temp_smeared = temperature;
+ }
+ g_debug ("night light mode on, using temperature of %uK (aiming for %uK)",
+ temp_smeared, temperature);
+ gsd_night_light_set_active (self, TRUE);
+ gsd_night_light_set_temperature (self, temp_smeared);
+}
+
+static gboolean
+night_light_recheck_schedule_cb (gpointer user_data)
+{
+ GsdNightLight *self = GSD_NIGHT_LIGHT (user_data);
+ night_light_recheck (self);
+ self->validate_id = 0;
+ return G_SOURCE_REMOVE;
+}
+
+/* called when something changed */
+static void
+night_light_recheck_schedule (GsdNightLight *self)
+{
+ if (self->validate_id != 0)
+ g_source_remove (self->validate_id);
+ self->validate_id =
+ g_timeout_add_seconds (GSD_NIGHT_LIGHT_SCHEDULE_TIMEOUT,
+ night_light_recheck_schedule_cb,
+ self);
+}
+
+/* called when the time may have changed */
+static gboolean
+night_light_recheck_cb (gpointer user_data)
+{
+ GsdNightLight *self = GSD_NIGHT_LIGHT (user_data);
+
+ /* recheck parameters, then reschedule a new timeout */
+ night_light_recheck (self);
+ poll_timeout_destroy (self);
+ poll_timeout_create (self);
+
+ /* return value ignored for a one-time watch */
+ return G_SOURCE_REMOVE;
+}
+
+static void
+poll_timeout_create (GsdNightLight *self)
+{
+ g_autoptr(GDateTime) dt_now = NULL;
+ g_autoptr(GDateTime) dt_expiry = NULL;
+
+ if (self->source != NULL)
+ return;
+
+ /* It is not a good idea to make this overridable, it just creates
+ * an infinite loop as a fixed date for testing just doesn't work. */
+ dt_now = g_date_time_new_now_local ();
+ dt_expiry = g_date_time_add_seconds (dt_now, GSD_NIGHT_LIGHT_POLL_TIMEOUT);
+ self->source = _gnome_datetime_source_new (dt_now,
+ dt_expiry,
+ TRUE);
+ g_source_set_callback (self->source,
+ night_light_recheck_cb,
+ self, NULL);
+ g_source_attach (self->source, NULL);
+}
+
+static void
+poll_timeout_destroy (GsdNightLight *self)
+{
+
+ if (self->source == NULL)
+ return;
+
+ g_source_destroy (self->source);
+ g_source_unref (self->source);
+ self->source = NULL;
+}
+
+static void
+settings_changed_cb (GSettings *settings, gchar *key, gpointer user_data)
+{
+ GsdNightLight *self = GSD_NIGHT_LIGHT (user_data);
+ g_debug ("settings changed");
+ night_light_recheck (self);
+}
+
+static void
+on_location_notify (GClueSimple *simple,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsdNightLight *self = GSD_NIGHT_LIGHT (user_data);
+ GClueLocation *location;
+ gdouble latitude, longitude;
+
+ location = gclue_simple_get_location (simple);
+ latitude = gclue_location_get_latitude (location);
+ longitude = gclue_location_get_longitude (location);
+
+ g_settings_set_value (self->settings,
+ "night-light-last-coordinates",
+ g_variant_new ("(dd)", latitude, longitude));
+
+ g_debug ("got geoclue latitude %f, longitude %f", latitude, longitude);
+
+ /* recheck the levels if the location changed significantly */
+ if (update_cached_sunrise_sunset (self))
+ night_light_recheck_schedule (self);
+}
+
+static void
+on_geoclue_simple_ready (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsdNightLight *self = GSD_NIGHT_LIGHT (user_data);
+ GClueSimple *geoclue_simple;
+ g_autoptr(GError) error = NULL;
+
+ geoclue_simple = gclue_simple_new_finish (res, &error);
+ if (geoclue_simple == NULL) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("Failed to connect to GeoClue2 service: %s", error->message);
+ return;
+ }
+
+ self->geoclue_simple = geoclue_simple;
+ self->geoclue_client = gclue_simple_get_client (self->geoclue_simple);
+ g_object_set (G_OBJECT (self->geoclue_client),
+ "time-threshold", 60*60, NULL); /* 1 hour */
+
+ g_signal_connect (self->geoclue_simple, "notify::location",
+ G_CALLBACK (on_location_notify), user_data);
+
+ on_location_notify (self->geoclue_simple, NULL, user_data);
+}
+
+static void
+start_geoclue (GsdNightLight *self)
+{
+ self->cancellable = g_cancellable_new ();
+ gclue_simple_new (DESKTOP_ID,
+ GCLUE_ACCURACY_LEVEL_CITY,
+ self->cancellable,
+ on_geoclue_simple_ready,
+ self);
+
+}
+
+static void
+stop_geoclue (GsdNightLight *self)
+{
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+
+ if (self->geoclue_client != NULL) {
+ gclue_client_call_stop (self->geoclue_client, NULL, NULL, NULL);
+ self->geoclue_client = NULL;
+ }
+ g_clear_object (&self->geoclue_simple);
+}
+
+static void
+check_location_settings (GsdNightLight *self)
+{
+ if (g_settings_get_boolean (self->location_settings, "enabled") && self->geoclue_enabled)
+ start_geoclue (self);
+ else
+ stop_geoclue (self);
+}
+
+void
+gsd_night_light_set_disabled_until_tmw (GsdNightLight *self, gboolean value)
+{
+ g_autoptr(GDateTime) dt = gsd_night_light_get_date_time_now (self);
+
+ if (self->disabled_until_tmw == value)
+ return;
+
+ self->disabled_until_tmw = value;
+ g_clear_pointer (&self->disabled_until_tmw_dt, g_date_time_unref);
+ if (self->disabled_until_tmw)
+ self->disabled_until_tmw_dt = g_steal_pointer (&dt);
+ night_light_recheck (self);
+ g_object_notify (G_OBJECT (self), "disabled-until-tmw");
+}
+
+gboolean
+gsd_night_light_get_disabled_until_tmw (GsdNightLight *self)
+{
+ return self->disabled_until_tmw;
+}
+
+void
+gsd_night_light_set_forced (GsdNightLight *self, gboolean value)
+{
+ if (self->forced == value)
+ return;
+
+ self->forced = value;
+ g_object_notify (G_OBJECT (self), "forced");
+
+ /* A simple recheck might not reset the temperature if
+ * night light is currently disabled. */
+ if (!self->forced && !self->cached_active)
+ gsd_night_light_set_temperature (self, GSD_COLOR_TEMPERATURE_DEFAULT);
+
+ night_light_recheck (self);
+}
+
+gboolean
+gsd_night_light_get_forced (GsdNightLight *self)
+{
+ return self->forced;
+}
+
+gboolean
+gsd_night_light_get_active (GsdNightLight *self)
+{
+ return self->cached_active;
+}
+
+gdouble
+gsd_night_light_get_sunrise (GsdNightLight *self)
+{
+ return self->cached_sunrise;
+}
+
+gdouble
+gsd_night_light_get_sunset (GsdNightLight *self)
+{
+ return self->cached_sunset;
+}
+
+gdouble
+gsd_night_light_get_temperature (GsdNightLight *self)
+{
+ return self->cached_temperature;
+}
+
+void
+gsd_night_light_set_geoclue_enabled (GsdNightLight *self, gboolean enabled)
+{
+ self->geoclue_enabled = enabled;
+}
+
+gboolean
+gsd_night_light_start (GsdNightLight *self, GError **error)
+{
+ night_light_recheck (self);
+ poll_timeout_create (self);
+
+ /* care about changes */
+ g_signal_connect (self->settings, "changed",
+ G_CALLBACK (settings_changed_cb), self);
+
+ g_signal_connect_swapped (self->location_settings, "changed::enabled",
+ G_CALLBACK (check_location_settings), self);
+ check_location_settings (self);
+
+ return TRUE;
+}
+
+static void
+gsd_night_light_finalize (GObject *object)
+{
+ GsdNightLight *self = GSD_NIGHT_LIGHT (object);
+
+ stop_geoclue (self);
+
+ poll_timeout_destroy (self);
+ poll_smooth_destroy (self);
+
+ g_clear_object (&self->settings);
+ g_clear_pointer (&self->datetime_override, g_date_time_unref);
+ g_clear_pointer (&self->disabled_until_tmw_dt, g_date_time_unref);
+
+ if (self->validate_id > 0) {
+ g_source_remove (self->validate_id);
+ self->validate_id = 0;
+ }
+
+ g_clear_object (&self->location_settings);
+ G_OBJECT_CLASS (gsd_night_light_parent_class)->finalize (object);
+}
+
+static void
+gsd_night_light_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GsdNightLight *self = GSD_NIGHT_LIGHT (object);
+
+ switch (prop_id) {
+ case PROP_SUNRISE:
+ self->cached_sunrise = g_value_get_double (value);
+ break;
+ case PROP_SUNSET:
+ self->cached_sunset = g_value_get_double (value);
+ break;
+ case PROP_TEMPERATURE:
+ self->cached_temperature = g_value_get_double (value);
+ break;
+ case PROP_DISABLED_UNTIL_TMW:
+ gsd_night_light_set_disabled_until_tmw (self, g_value_get_boolean (value));
+ break;
+ case PROP_FORCED:
+ gsd_night_light_set_forced (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gsd_night_light_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GsdNightLight *self = GSD_NIGHT_LIGHT (object);
+
+ switch (prop_id) {
+ case PROP_ACTIVE:
+ g_value_set_boolean (value, self->cached_active);
+ break;
+ case PROP_SUNRISE:
+ g_value_set_double (value, self->cached_sunrise);
+ break;
+ case PROP_SUNSET:
+ g_value_set_double (value, self->cached_sunrise);
+ break;
+ case PROP_TEMPERATURE:
+ g_value_set_double (value, self->cached_sunrise);
+ break;
+ case PROP_DISABLED_UNTIL_TMW:
+ g_value_set_boolean (value, gsd_night_light_get_disabled_until_tmw (self));
+ break;
+ case PROP_FORCED:
+ g_value_set_boolean (value, gsd_night_light_get_forced (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gsd_night_light_class_init (GsdNightLightClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = gsd_night_light_finalize;
+
+ object_class->set_property = gsd_night_light_set_property;
+ object_class->get_property = gsd_night_light_get_property;
+
+ g_object_class_install_property (object_class,
+ PROP_ACTIVE,
+ g_param_spec_boolean ("active",
+ "Active",
+ "If night light functionality is active right now",
+ FALSE,
+ G_PARAM_READABLE));
+
+ g_object_class_install_property (object_class,
+ PROP_SUNRISE,
+ g_param_spec_double ("sunrise",
+ "Sunrise",
+ "Sunrise in fractional hours",
+ 0,
+ 24.f,
+ 12,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property (object_class,
+ PROP_SUNSET,
+ g_param_spec_double ("sunset",
+ "Sunset",
+ "Sunset in fractional hours",
+ 0,
+ 24.f,
+ 12,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property (object_class,
+ PROP_TEMPERATURE,
+ g_param_spec_double ("temperature",
+ "Temperature",
+ "Temperature in Kelvin",
+ GSD_COLOR_TEMPERATURE_MIN,
+ GSD_COLOR_TEMPERATURE_MAX,
+ GSD_COLOR_TEMPERATURE_DEFAULT,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property (object_class,
+ PROP_DISABLED_UNTIL_TMW,
+ g_param_spec_boolean ("disabled-until-tmw",
+ "Disabled until tomorrow",
+ "If the night light is disabled until the next day",
+ FALSE,
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property (object_class,
+ PROP_FORCED,
+ g_param_spec_boolean ("forced",
+ "Forced",
+ "Whether night light should be forced on, useful for previewing",
+ FALSE,
+ G_PARAM_READWRITE));
+
+}
+
+static void
+gsd_night_light_init (GsdNightLight *self)
+{
+ self->geoclue_enabled = TRUE;
+ self->smooth_enabled = TRUE;
+ self->cached_sunrise = -1.f;
+ self->cached_sunset = -1.f;
+ self->cached_temperature = GSD_COLOR_TEMPERATURE_DEFAULT;
+ self->settings = g_settings_new ("org.gnome.settings-daemon.plugins.color");
+ self->location_settings = g_settings_new ("org.gnome.system.location");
+}
+
+GsdNightLight *
+gsd_night_light_new (void)
+{
+ return g_object_new (GSD_TYPE_NIGHT_LIGHT, NULL);
+}
diff --git a/plugins/color/gsd-night-light.h b/plugins/color/gsd-night-light.h
new file mode 100644
index 0000000..8803c33
--- /dev/null
+++ b/plugins/color/gsd-night-light.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GSD_NIGHT_LIGHT_H__
+#define __GSD_NIGHT_LIGHT_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GSD_TYPE_NIGHT_LIGHT (gsd_night_light_get_type ())
+G_DECLARE_FINAL_TYPE (GsdNightLight, gsd_night_light, GSD, NIGHT_LIGHT, GObject)
+
+GsdNightLight *gsd_night_light_new (void);
+gboolean gsd_night_light_start (GsdNightLight *self,
+ GError **error);
+
+gboolean gsd_night_light_get_active (GsdNightLight *self);
+gdouble gsd_night_light_get_sunrise (GsdNightLight *self);
+gdouble gsd_night_light_get_sunset (GsdNightLight *self);
+gdouble gsd_night_light_get_temperature (GsdNightLight *self);
+
+gboolean gsd_night_light_get_disabled_until_tmw (GsdNightLight *self);
+void gsd_night_light_set_disabled_until_tmw (GsdNightLight *self,
+ gboolean value);
+
+gboolean gsd_night_light_get_forced (GsdNightLight *self);
+void gsd_night_light_set_forced (GsdNightLight *self,
+ gboolean value);
+
+/* only for the self test program */
+void gsd_night_light_set_geoclue_enabled (GsdNightLight *self,
+ gboolean enabled);
+void gsd_night_light_set_date_time_now (GsdNightLight *self,
+ GDateTime *datetime);
+void gsd_night_light_set_smooth_enabled (GsdNightLight *self,
+ gboolean smooth_enabled);
+
+G_END_DECLS
+
+#endif
diff --git a/plugins/color/main.c b/plugins/color/main.c
new file mode 100644
index 0000000..5dda3e7
--- /dev/null
+++ b/plugins/color/main.c
@@ -0,0 +1,7 @@
+#define NEW gsd_color_manager_new
+#define START gsd_color_manager_start
+#define STOP gsd_color_manager_stop
+#define MANAGER GsdColorManager
+#include "gsd-color-manager.h"
+
+#include "daemon-skeleton-gtk.h"
diff --git a/plugins/color/meson.build b/plugins/color/meson.build
new file mode 100644
index 0000000..2ae1740
--- /dev/null
+++ b/plugins/color/meson.build
@@ -0,0 +1,55 @@
+sources = files(
+ 'gcm-edid.c',
+ 'gnome-datetime-source.c',
+ 'gsd-color-calibrate.c',
+ 'gsd-color-manager.c',
+ 'gsd-color-profiles.c',
+ 'gsd-color-state.c',
+ 'gsd-night-light.c',
+ 'gsd-night-light-common.c',
+ 'main.c'
+)
+
+deps = plugins_deps + [
+ colord_dep,
+ gnome_desktop_dep,
+ lcms_dep,
+ libcanberra_gtk_dep,
+ libgeoclue_dep,
+ libnotify_dep,
+ m_dep,
+]
+
+cflags += ['-DBINDIR="@0@"'.format(gsd_bindir)]
+
+executable(
+ 'gsd-' + plugin_name,
+ sources,
+ include_directories: [top_inc, common_inc],
+ dependencies: deps,
+ c_args: cflags,
+ install: true,
+ install_rpath: gsd_pkglibdir,
+ install_dir: gsd_libexecdir
+)
+
+sources = files(
+ 'gcm-edid.c',
+ 'gcm-self-test.c',
+ 'gnome-datetime-source.c',
+ 'gsd-night-light.c',
+ 'gsd-night-light-common.c'
+)
+
+test_unit = 'gcm-self-test'
+
+exe = executable(
+ test_unit,
+ sources,
+ include_directories: top_inc,
+ dependencies: deps,
+ c_args: '-DTESTDATADIR="@0@"'.format(join_paths(meson.current_source_dir(), 'test-data'))
+)
+
+envs = ['GSETTINGS_SCHEMA_DIR=@0@'.format(join_paths(meson.build_root(), 'data'))]
+test(test_unit, exe, env: envs)
diff --git a/plugins/color/test-data/LG-L225W-External.bin b/plugins/color/test-data/LG-L225W-External.bin
new file mode 100644
index 0000000..f08310a
--- /dev/null
+++ b/plugins/color/test-data/LG-L225W-External.bin
Binary files differ
diff --git a/plugins/color/test-data/Lenovo-T61-Internal.bin b/plugins/color/test-data/Lenovo-T61-Internal.bin
new file mode 100644
index 0000000..45aec9d
--- /dev/null
+++ b/plugins/color/test-data/Lenovo-T61-Internal.bin
Binary files differ