From 86b7f1a83d7db9c912f32b29c32e1124c0a6454d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 27 Apr 2024 16:51:55 +0200 Subject: Adding upstream version 3.38.2. Signed-off-by: Daniel Baumann --- plugins/color/gcm-edid.c | 450 +++++++ plugins/color/gcm-edid.h | 63 + plugins/color/gcm-self-test.c | 430 ++++++ plugins/color/gnome-datetime-source.c | 286 ++++ plugins/color/gnome-datetime-source.h | 38 + plugins/color/gsd-color-calibrate.c | 412 ++++++ plugins/color/gsd-color-calibrate.h | 38 + plugins/color/gsd-color-manager.c | 502 +++++++ plugins/color/gsd-color-manager.h | 46 + plugins/color/gsd-color-profiles.c | 246 ++++ plugins/color/gsd-color-profiles.h | 40 + plugins/color/gsd-color-state.c | 1592 +++++++++++++++++++++++ plugins/color/gsd-color-state.h | 47 + plugins/color/gsd-night-light-common.c | 137 ++ plugins/color/gsd-night-light-common.h | 39 + plugins/color/gsd-night-light.c | 801 ++++++++++++ plugins/color/gsd-night-light.h | 58 + plugins/color/main.c | 7 + plugins/color/meson.build | 55 + plugins/color/test-data/LG-L225W-External.bin | Bin 0 -> 128 bytes plugins/color/test-data/Lenovo-T61-Internal.bin | Bin 0 -> 128 bytes 21 files changed, 5287 insertions(+) create mode 100644 plugins/color/gcm-edid.c create mode 100644 plugins/color/gcm-edid.h create mode 100644 plugins/color/gcm-self-test.c create mode 100644 plugins/color/gnome-datetime-source.c create mode 100644 plugins/color/gnome-datetime-source.h create mode 100644 plugins/color/gsd-color-calibrate.c create mode 100644 plugins/color/gsd-color-calibrate.h create mode 100644 plugins/color/gsd-color-manager.c create mode 100644 plugins/color/gsd-color-manager.h create mode 100644 plugins/color/gsd-color-profiles.c create mode 100644 plugins/color/gsd-color-profiles.h create mode 100644 plugins/color/gsd-color-state.c create mode 100644 plugins/color/gsd-color-state.h create mode 100644 plugins/color/gsd-night-light-common.c create mode 100644 plugins/color/gsd-night-light-common.h create mode 100644 plugins/color/gsd-night-light.c create mode 100644 plugins/color/gsd-night-light.h create mode 100644 plugins/color/main.c create mode 100644 plugins/color/meson.build create mode 100644 plugins/color/test-data/LG-L225W-External.bin create mode 100644 plugins/color/test-data/Lenovo-T61-Internal.bin (limited to 'plugins/color') 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 + * Copyright (C) 2009-2011 Richard Hughes + * + * 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 +#include +#include +#include +#include +#include + +#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 + * + * 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 +#include + +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 + * + * 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 +#include +#include + +#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 + */ + +#include "config.h" + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include "gnome-datetime-source.h" + +#if HAVE_TIMERFD +#include +#include +#include +#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. + * + * This function currently does not detect time zone + * changes. On Linux, your program should also monitor the + * /etc/timezone file using + * #GFileMonitor. + * + * Clock exampleFIXME: MISSING XINCLUDE CONTENT + * + * 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 + */ + +#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 + +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 + * Copyright (C) 2011-2013 Richard Hughes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include + +#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 + * Copyright (C) 2011-2013 Richard Hughes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#ifndef __GSD_COLOR_CALIBRATE_H +#define __GSD_COLOR_CALIBRATE_H + +#include + +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 + * Copyright (C) 2011-2013 Richard Hughes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#include "config.h" + +#include +#include +#include + +#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[] = +"" +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +""; + +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 + * Copyright (C) 2011 Richard Hughes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#ifndef __GSD_COLOR_MANAGER_H +#define __GSD_COLOR_MANAGER_H + +#include + +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 + * Copyright (C) 2011-2013 Richard Hughes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#include "config.h" + +#include +#include + +#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 + * Copyright (C) 2011-2013 Richard Hughes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#ifndef __GSD_COLOR_PROFILES_H +#define __GSD_COLOR_PROFILES_H + +#include + +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 + * Copyright (C) 2011-2013 Richard Hughes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include + +#ifdef GDK_WINDOWING_X11 +#include +#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 + * Copyright (C) 2011-2013 Richard Hughes + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#ifndef __GSD_COLOR_STATE_H +#define __GSD_COLOR_STATE_H + +#include + +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 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#include "config.h" + +#include +#include + +#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 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + */ + +#ifndef __GSD_NIGHT_LIGHT_COMMON_H +#define __GSD_NIGHT_LIGHT_COMMON_H + +#include + +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 + * + * 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 + +#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 + * + * 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 + +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 Binary files /dev/null and b/plugins/color/test-data/LG-L225W-External.bin 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 Binary files /dev/null and b/plugins/color/test-data/Lenovo-T61-Internal.bin differ -- cgit v1.2.3