Commit f44cde35 authored by Adrien Plazas's avatar Adrien Plazas

timezone: Use a location list on small screens

parent ac07d07a
Pipeline #22702 failed with stage
in 0 seconds
......@@ -205,18 +205,20 @@ cc_timezone_map_size_allocate (GtkWidget *widget,
else
pixbuf = priv->orig_background;
/* Bilinear scaling costs about 2500ms on my system. */
priv->background = gdk_pixbuf_scale_simple (pixbuf,
allocation->width,
allocation->height,
GDK_INTERP_BILINEAR);
GDK_INTERP_NEAREST);
if (priv->color_map)
g_object_unref (priv->color_map);
/* Bilinear scaling costs about 2500ms on my system. */
priv->color_map = gdk_pixbuf_scale_simple (priv->orig_color_map,
allocation->width,
allocation->height,
GDK_INTERP_BILINEAR);
GDK_INTERP_NEAREST);
priv->visible_map_pixels = gdk_pixbuf_get_pixels (priv->color_map);
priv->visible_map_rowstride = gdk_pixbuf_get_rowstride (priv->color_map);
......@@ -407,9 +409,9 @@ cc_timezone_map_draw (GtkWidget *widget,
}
else
{
/* Bilinear scaling costs about 1500ms on my system. */
hilight = gdk_pixbuf_scale_simple (orig_hilight, alloc.width,
alloc.height, GDK_INTERP_BILINEAR);
alloc.height, GDK_INTERP_NEAREST);
gdk_cairo_set_source_pixbuf (cr, hilight, 0, 0);
cairo_paint (cr);
......
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
* Copyright (C) 2019 Purism SPC
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*
* Written by:
* Adrien Plazas <kekun.plazas@laposte.net>
*/
#include "config.h"
#include "gis-location-list.h"
#include <glib/gi18n.h>
#include <gio/gio.h>
#define HANDY_USE_UNSTABLE_API
#include <handy.h>
#define GWEATHER_I_KNOW_THIS_IS_UNSTABLE
#include <libgweather/gweather.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
GWeatherLocation *location;
GWeatherLocation *top;
gboolean show_named_timezones;
gchar *text;
gboolean custom_text;
GCancellable *cancellable;
} GisLocationListPrivate;
G_DEFINE_TYPE_WITH_PRIVATE (GisLocationList, gis_location_list, GTK_TYPE_LIST_BOX);
enum
{
PROP_0,
PROP_TOP,
PROP_SHOW_NAMED_TIMEZONES,
PROP_LOCATION,
PROP_TEXT,
PROP_LAST,
};
static GParamSpec *props[PROP_LAST];
static gboolean matcher (GtkListBoxRow *row,
gpointer user_data);
static void
add_location_row (GisLocationList *self,
GWeatherLocation *location,
const gchar *display_name,
const gchar *local_compare_name,
const gchar *english_compare_name)
{
HdyActionRow *row = hdy_action_row_new ();
gtk_widget_show (GTK_WIDGET (row));
hdy_action_row_set_title (row, display_name);
g_object_set_data_full (G_OBJECT (row), "location", location, (GDestroyNotify) gweather_location_unref);
g_object_set_data_full (G_OBJECT (row), "local_compare_name", local_compare_name, g_free);
g_object_set_data_full (G_OBJECT (row), "english_compare_name", english_compare_name, g_free);
gtk_container_add (GTK_CONTAINER (self), row);
}
static void
fill_location_list_model (GisLocationList *self,
GWeatherLocation *loc,
const char *parent_display_name,
const char *parent_compare_local_name,
const char *parent_compare_english_name,
gboolean show_named_timezones)
{
GWeatherLocation **children;
const gchar *loc_local_name = gweather_location_get_name (loc);
const gchar *loc_local_sort_name = gweather_location_get_sort_name (loc);
/* FIXME This should use the english name. */
const gchar *loc_english_sort_name = gweather_location_get_sort_name (loc);
char *display_name, *local_compare_name, *english_compare_name;
int i;
children = gweather_location_get_children (loc);
switch (gweather_location_get_level (loc)) {
case GWEATHER_LOCATION_WORLD:
case GWEATHER_LOCATION_REGION:
/* Ignore these levels of hierarchy; just recurse, passing on
* the names from the parent node.
*/
for (i = 0; children[i]; i++) {
fill_location_list_model (self, children[i],
parent_display_name,
parent_compare_local_name,
parent_compare_english_name,
show_named_timezones);
}
break;
case GWEATHER_LOCATION_COUNTRY:
/* Recurse, initializing the names to the country name */
for (i = 0; children[i]; i++) {
fill_location_list_model (self, children[i],
loc_local_name,
loc_local_sort_name,
loc_english_sort_name,
show_named_timezones);
}
break;
case GWEATHER_LOCATION_ADM1:
/* Recurse, adding the ADM1 name to the country name */
/* Translators: this is the name of a location followed by a region, for example:
* 'London, United Kingdom'
* You shouldn't need to translate this string unless the language has a different comma.
*/
display_name = g_strdup_printf (_("%s, %s"), loc_local_name, parent_display_name);
local_compare_name = g_strdup_printf ("%s, %s", loc_local_sort_name, parent_compare_local_name);
english_compare_name = g_strdup_printf ("%s, %s", loc_english_sort_name, parent_compare_english_name);
for (i = 0; children[i]; i++) {
fill_location_list_model (self, children[i],
display_name, local_compare_name, english_compare_name,
show_named_timezones);
}
g_free (display_name);
g_free (local_compare_name);
g_free (english_compare_name);
break;
case GWEATHER_LOCATION_CITY:
/* If there are multiple (<location>) children, we use the one
* closest to the city center.
*
* Locations are already sorted by increasing distance from
* the city.
*/
case GWEATHER_LOCATION_WEATHER_STATION:
/* <location> with no parent <city> */
/* Translators: this is the name of a location followed by a region, for example:
* 'London, United Kingdom'
* You shouldn't need to translate this string unless the language has a different comma.
*/
display_name = g_strdup_printf (_("%s, %s"),
loc_local_name, parent_display_name);
local_compare_name = g_strdup_printf ("%s, %s",
loc_local_sort_name, parent_compare_local_name);
english_compare_name = g_strdup_printf ("%s, %s",
loc_english_sort_name, parent_compare_english_name);
add_location_row (self, loc, display_name, local_compare_name, english_compare_name);
g_free (display_name);
g_free (local_compare_name);
g_free (english_compare_name);
break;
case GWEATHER_LOCATION_NAMED_TIMEZONE:
if (show_named_timezones)
add_location_row (self, loc, loc_local_name, loc_local_sort_name, loc_english_sort_name);
break;
case GWEATHER_LOCATION_DETACHED:
g_assert_not_reached ();
}
}
static void
gis_location_list_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GisLocationList *self = GIS_LOCATION_LIST (object);
GisLocationListPrivate *priv = gis_location_list_get_instance_private (self);
switch (prop_id) {
case PROP_SHOW_NAMED_TIMEZONES:
g_value_set_boolean (value, priv->show_named_timezones);
break;
case PROP_LOCATION:
g_value_set_boxed (value, priv->location);
break;
case PROP_TEXT:
g_value_set_string (value, priv->text);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gis_location_list_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GisLocationList *self = GIS_LOCATION_LIST (object);
GisLocationListPrivate *priv = gis_location_list_get_instance_private (self);
switch (prop_id) {
case PROP_TOP:
priv->top = g_value_dup_boxed (value);
break;
case PROP_SHOW_NAMED_TIMEZONES:
priv->show_named_timezones = g_value_get_boolean (value);
break;
case PROP_LOCATION:
gis_location_list_set_location (self, g_value_get_boxed (value));
break;
case PROP_TEXT:
gis_location_list_set_text (self, g_value_get_string (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gis_location_list_constructed (GObject *object)
{
GisLocationList *self = GIS_LOCATION_LIST (object);
GisLocationListPrivate *priv = gis_location_list_get_instance_private (self);
if (!priv->top)
priv->top = gweather_location_ref (gweather_location_get_world ());
fill_location_list_model (self, priv->top, NULL, NULL, NULL, priv->show_named_timezones);
gtk_list_box_set_filter_func (GTK_LIST_BOX (self), matcher, self, NULL);
gtk_list_box_set_header_func (GTK_LIST_BOX (self), hdy_list_box_separator_header, NULL, NULL);
G_OBJECT_CLASS (gis_location_list_parent_class)->constructed (object);
}
static void
gis_location_list_dispose (GObject *object)
{
GisLocationList *self = GIS_LOCATION_LIST (object);
GisLocationListPrivate *priv = gis_location_list_get_instance_private (self);
if (priv->cancellable) {
g_cancellable_cancel (priv->cancellable);
g_object_unref (priv->cancellable);
priv->cancellable = NULL;
}
G_OBJECT_CLASS (gis_location_list_parent_class)->dispose (object);
}
static void
gis_location_list_finalize (GObject *object)
{
GisLocationList *self = GIS_LOCATION_LIST (object);
GisLocationListPrivate *priv = gis_location_list_get_instance_private (self);
if (priv->location)
gweather_location_unref (priv->location);
if (priv->top)
gweather_location_unref (priv->top);
g_clear_pointer (&priv->text, g_free);
G_OBJECT_CLASS (gis_location_list_parent_class)->finalize (object);
}
static void
gis_location_list_class_init (GisLocationListClass *klass)
{
GisLocationListClass *location_list_class = GIS_LOCATION_LIST_CLASS (klass);
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->get_property = gis_location_list_get_property;
object_class->set_property = gis_location_list_set_property;
object_class->constructed = gis_location_list_constructed;
object_class->dispose = gis_location_list_dispose;
object_class->finalize = gis_location_list_finalize;
/* properties */
props[PROP_TOP] =
g_param_spec_boxed ("top",
"Top Location",
"The GWeatherLocation whose children will be used to fill in the entry",
GWEATHER_TYPE_LOCATION,
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY);
props[PROP_SHOW_NAMED_TIMEZONES] =
g_param_spec_boolean ("show-named-timezones",
"Show named timezones",
"Whether UTC and other named timezones are shown in the list of locations",
FALSE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
props[PROP_LOCATION] =
g_param_spec_boxed ("location",
"Location",
"The selected GWeatherLocation",
GWEATHER_TYPE_LOCATION,
G_PARAM_READWRITE);
props[PROP_TEXT] =
g_param_spec_string ("text",
"Text",
"The search text",
NULL,
G_PARAM_READWRITE);
g_object_class_install_properties (object_class, PROP_LAST, props);
}
static void
gis_location_list_init (GisLocationList *self)
{
/* GtkEntryCompletion *completion; */
/* GisLocationListPrivate *priv; */
/* completion = gtk_entry_completion_new (); */
/* gtk_entry_completion_set_popup_set_width (completion, FALSE); */
/* gtk_entry_completion_set_text_column (completion, LOC_GIS_LOCATION_LIST_COL_DISPLAY_NAME); */
/* gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL); */
/* gtk_entry_completion_set_inline_completion (completion, TRUE); */
/* g_signal_connect (completion, "match-selected", */
/* G_CALLBACK (match_selected), self); */
/* g_signal_connect (completion, "no-matches", */
/* G_CALLBACK (_no_matches), self); */
/* gtk_entry_set_completion (GTK_ENTRY (self), completion); */
/* g_object_unref (completion); */
/* priv->custom_text = FALSE; */
/* g_signal_connect (self, "changed", */
/* G_CALLBACK (entry_changed), NULL); */
}
/**
* gis_location_list_new:
* @top: the top-level location for the list.
*
* Creates a new #GisLocationList.
*
* @top will normally be the location returned from
* gweather_location_get_world(), but you can create a list thatonly accepts a
* smaller set of locations if you want.
*
* Return value: the new #GisLocationList
**/
GisLocationList *
gis_location_list_new (GWeatherLocation *top)
{
return g_object_new (GIS_TYPE_LOCATION_LIST,
"top", top,
NULL);
}
/* static void */
/* entry_changed (GisLocationList *self) */
/* { */
/* GtkEntryCompletion *completion; */
/* const gchar *text; */
/* completion = gtk_entry_get_completion (GTK_ENTRY (self)); */
/* if (priv->cancellable) { */
/* g_cancellable_cancel (priv->cancellable); */
/* g_object_unref (priv->cancellable); */
/* priv->cancellable = NULL; */
/* gtk_entry_completion_delete_action (completion, 0); */
/* } */
/* gtk_entry_completion_set_match_func (gtk_entry_get_completion (GTK_ENTRY (self)), matcher, NULL, NULL); */
/* gtk_entry_completion_set_model (gtk_entry_get_completion (GTK_ENTRY (self)), priv->model); */
/* text = gtk_entry_get_text (GTK_ENTRY (self)); */
/* if (text && *text) */
/* priv->custom_text = TRUE; */
/* else */
/* set_location_internal (self, NULL, NULL, NULL); */
/* } */
static void
set_location_internal (GisLocationList *self,
GtkTreeModel *model,
GtkTreeIter *iter,
GWeatherLocation *loc)
{
GisLocationListPrivate *priv;
char *name;
priv = gis_location_list_get_instance_private (self);
if (priv->location)
gweather_location_unref (priv->location);
g_assert (iter == NULL || loc == NULL);
if (iter) {
/* gtk_tree_model_get (model, iter, */
/* LOC_GIS_LOCATION_LIST_COL_DISPLAY_NAME, &name, */
/* LOC_GIS_LOCATION_LIST_COL_LOCATION, &priv->location, */
/* -1); */
gtk_entry_set_text (GTK_ENTRY (self), name);
priv->custom_text = FALSE;
g_free (name);
} else if (loc) {
priv->location = gweather_location_ref (loc);
gtk_entry_set_text (GTK_ENTRY (self), gweather_location_get_name (loc));
priv->custom_text = FALSE;
} else {
priv->location = NULL;
gtk_entry_set_text (GTK_ENTRY (self), "");
priv->custom_text = TRUE;
}
gtk_editable_set_position (GTK_EDITABLE (self), -1);
g_object_notify (G_OBJECT (self), "location");
}
/**
* gis_location_list_set_location:
* @entry: a #GisLocationList
* @loc: (allow-none): a #GWeatherLocation in @entry, or %NULL to
* clear @entry
*
* Sets @entry's location to @loc, and updates the text of the
* entry accordingly.
* Note that if the database contains a location that compares
* equal to @loc, that will be chosen in place of @loc.
**/
void
gis_location_list_set_location (GisLocationList *self,
GWeatherLocation *loc)
{
GtkEntryCompletion *completion;
GtkTreeModel *model;
GtkTreeIter iter;
GWeatherLocation *cmploc;
g_return_if_fail (GIS_IS_LOCATION_LIST (self));
completion = gtk_entry_get_completion (GTK_ENTRY (self));
model = gtk_entry_completion_get_model (completion);
if (loc == NULL) {
set_location_internal (self, model, NULL, NULL);
return;
}
gtk_tree_model_get_iter_first (model, &iter);
do {
/* gtk_tree_model_get (model, &iter, */
/* LOC_GIS_LOCATION_LIST_COL_LOCATION, &cmploc, */
/* -1); */
if (gweather_location_equal (loc, cmploc)) {
set_location_internal (self, model, &iter, NULL);
gweather_location_unref (cmploc);
return;
}
gweather_location_unref (cmploc);
} while (gtk_tree_model_iter_next (model, &iter));
set_location_internal (self, model, NULL, loc);
}
/**
* gis_location_list_get_location:
* @entry: a #GisLocationList
*
* Gets the location that was set by a previous call to
* gis_location_list_set_location() or was selected by the user.
*
* Return value: (transfer full) (allow-none): the selected location
* (which you must unref when you are done with it), or %NULL if no
* location is selected.
**/
GWeatherLocation *
gis_location_list_get_location (GisLocationList *self)
{
GisLocationListPrivate *priv;
g_return_val_if_fail (GIS_IS_LOCATION_LIST (self), NULL);
priv = gis_location_list_get_instance_private (self);
if (priv->location)
return gweather_location_ref (priv->location);
else
return NULL;
}
/**
* gis_location_list_has_custom_text:
* @entry: a #GisLocationList
*
* Checks whether or not @entry's text has been modified by the user.
* Note that this does not mean that no location is associated with @entry.
* gis_location_list_get_location() should be used for this.
*
* Return value: %TRUE if @entry's text was modified by the user, or %FALSE if
* it's set to the default text of a location.
**/
gboolean
gis_location_list_has_custom_text (GisLocationList *self)
{
GisLocationListPrivate *priv;
g_return_val_if_fail (GIS_IS_LOCATION_LIST (self), FALSE);
priv = gis_location_list_get_instance_private (self);
return priv->custom_text;
}
/**
* gis_location_list_set_city:
* @entry: a #GisLocationList
* @city_name: (allow-none): the city name, or %NULL
* @code: the METAR station code
*
* Sets @entry's location to a city with the given @code, and given
* @city_name, if non-%NULL. If there is no matching city, sets
* @entry's location to %NULL.
*
* Return value: %TRUE if @entry's location could be set to a matching city,
* %FALSE otherwise.
**/
gboolean
gis_location_list_set_city (GisLocationList *self,
const char *city_name,
const char *code)
{
/* GtkEntryCompletion *completion; */
GtkTreeModel *model;
GtkTreeIter iter;
GWeatherLocation *cmploc;
const char *cmpcode;
char *cmpname;
g_return_val_if_fail (GIS_IS_LOCATION_LIST (self), FALSE);
g_return_val_if_fail (code != NULL, FALSE);
/* completion = gtk_entry_get_completion (GTK_ENTRY (self)); */
/* model = gtk_entry_completion_get_model (completion); */
gtk_tree_model_get_iter_first (model, &iter);
do {
/* gtk_tree_model_get (model, &iter, */
/* LOC_GIS_LOCATION_LIST_COL_LOCATION, &cmploc, */
/* -1); */
cmpcode = gweather_location_get_code (cmploc);
if (!cmpcode || strcmp (cmpcode, code) != 0) {
gweather_location_unref (cmploc);
continue;
}
if (city_name) {
cmpname = gweather_location_get_city_name (cmploc);
if (!cmpname || strcmp (cmpname, city_name) != 0) {
gweather_location_unref (cmploc);
g_free (cmpname);
continue;
}
g_free (cmpname);
}
set_location_internal (self, model, &iter, NULL);
gweather_location_unref (cmploc);
return TRUE;
} while (gtk_tree_model_iter_next (model, &iter));
set_location_internal (self, model, NULL, NULL);
return FALSE;
}
static char *
find_word (const char *full_name,
const char *word,
int word_len,
gboolean whole_word,
gboolean is_first_word)
{
char *p;
if (word == NULL || *word == '\0')
return NULL;
p = (char *)full_name - 1;
while ((p = strchr (p + 1, *word))) {
if (strncmp (p, word, word_len) != 0)
continue;
if (p > (char *)full_name) {
char *prev = g_utf8_prev_char (p);
/* Make sure p points to the start of a word */
if (g_unichar_isalpha (g_utf8_get_char (prev)))
continue;
/* If we're matching the first word of the key, it has to
* match the first word of the location, city, state, or
* country, or the abbreviation (in parenthesis).
* Eg, it either matches the start of the string
* (which we already know it doesn't at this point) or
* it is preceded by the string ", " or "(" (which isn't actually
* a perfect test. FIXME)
*/
if (is_first_word) {
if (prev == (char *)full_name ||
((prev - 1 <= full_name && strncmp (prev - 1, ", ", 2) != 0)
&& *prev != '('))
continue;
}
}
if (whole_word && g_unichar_isalpha (g_utf8_get_char (p + word_len)))
continue;
return p;
}
return NULL;
}
static gboolean
match_compare_name (const char *key, const char *name)
{
gboolean is_first_word = TRUE;
size_t len;
/* Ignore whitespace before the string */
key += strspn (key, " ");
/* All but the last word in KEY must match a full word from NAME,
* in order (but possibly skipping some words from NAME).
*/
len = strcspn (key, " ");
while (key[len]) {
name = find_word (name, key, len, TRUE, is_first_word);
if (!name)
return FALSE;
key += len;
while (*key && !g_unichar_isalpha (g_utf8_get_char (key)))
key = g_utf8_next_char (key);
while (*name && !g_unichar_isalpha (g_utf8_get_char (name)))
name = g_utf8_next_char (name);
len = strcspn (key, " ");
is_first_word = FALSE;
}
/* The last word in KEY must match a prefix of a following word in NAME */
if (len == 0) {
return TRUE;
} else {
// if we get here, key[len] == 0, so...
g_assert (len == strlen(key));
return find_word (name, key, len, FALSE, is_first_word) != NULL;
}