1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 4 -*- */
2 /* location-entry.c - Location-selecting text entry
3  *
4  * Copyright 2008, Red Hat, Inc.
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public License
8  * as published by the Free Software Foundation; either version 2.1 of
9  * the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this library; if not, see
18  * <http://www.gnu.org/licenses/>.
19  */
20 
21 #ifdef HAVE_CONFIG_H
22 #include <config.h>
23 #endif
24 
25 #define MATEWEATHER_I_KNOW_THIS_IS_UNSTABLE
26 #include "location-entry.h"
27 
28 #include <string.h>
29 
30 /**
31  * SECTION:location-entry
32  * @Title: MateWeatherLocationEntry
33  *
34  * A subclass of #GtkEntry that provides autocompletion on
35  * #MateWeatherLocation<!-- -->s
36  */
37 
38 G_DEFINE_TYPE (MateWeatherLocationEntry, mateweather_location_entry, GTK_TYPE_ENTRY)
39 
40 enum {
41     PROP_0,
42 
43     PROP_TOP,
44     PROP_LOCATION,
45 
46     LAST_PROP
47 };
48 
49 static void mateweather_location_entry_build_model (MateWeatherLocationEntry *entry,
50 						 MateWeatherLocation *top);
51 static void set_property (GObject *object, guint prop_id,
52 			  const GValue *value, GParamSpec *pspec);
53 static void get_property (GObject *object, guint prop_id,
54 			  GValue *value, GParamSpec *pspec);
55 
56 enum
57 {
58     MATEWEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME = 0,
59     MATEWEATHER_LOCATION_ENTRY_COL_LOCATION,
60     MATEWEATHER_LOCATION_ENTRY_COL_COMPARE_NAME,
61     MATEWEATHER_LOCATION_ENTRY_COL_SORT_NAME,
62     MATEWEATHER_LOCATION_ENTRY_NUM_COLUMNS
63 };
64 
65 static gboolean matcher (GtkEntryCompletion *completion, const char *key,
66 			 GtkTreeIter *iter, gpointer user_data);
67 static gboolean match_selected (GtkEntryCompletion *completion,
68 				GtkTreeModel       *model,
69 				GtkTreeIter        *iter,
70 				gpointer            entry);
71 static void     entry_changed (MateWeatherLocationEntry *entry);
72 
73 static void
mateweather_location_entry_init(MateWeatherLocationEntry * entry)74 mateweather_location_entry_init (MateWeatherLocationEntry *entry)
75 {
76     GtkEntryCompletion *completion;
77 
78     completion = gtk_entry_completion_new ();
79 
80     gtk_entry_completion_set_popup_set_width (completion, FALSE);
81     gtk_entry_completion_set_text_column (completion, MATEWEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME);
82     gtk_entry_completion_set_match_func (completion, matcher, NULL, NULL);
83 
84     g_signal_connect (completion, "match_selected",
85 		      G_CALLBACK (match_selected), entry);
86 
87     gtk_entry_set_completion (GTK_ENTRY (entry), completion);
88     g_object_unref (completion);
89 
90     entry->custom_text = FALSE;
91     g_signal_connect (entry, "changed",
92 		      G_CALLBACK (entry_changed), NULL);
93 }
94 
95 static void
finalize(GObject * object)96 finalize (GObject *object)
97 {
98     MateWeatherLocationEntry *entry = MATEWEATHER_LOCATION_ENTRY (object);
99 
100     if (entry->location)
101 	mateweather_location_unref (entry->location);
102     if (entry->top)
103 	mateweather_location_unref (entry->top);
104 
105     G_OBJECT_CLASS (mateweather_location_entry_parent_class)->finalize (object);
106 }
107 
108 static void
mateweather_location_entry_class_init(MateWeatherLocationEntryClass * location_entry_class)109 mateweather_location_entry_class_init (MateWeatherLocationEntryClass *location_entry_class)
110 {
111     GObjectClass *object_class = G_OBJECT_CLASS (location_entry_class);
112 
113     object_class->finalize = finalize;
114     object_class->set_property = set_property;
115     object_class->get_property = get_property;
116 
117     /* properties */
118     g_object_class_install_property (
119 	object_class, PROP_TOP,
120 	g_param_spec_pointer ("top",
121 			      "Top Location",
122 			      "The MateWeatherLocation whose children will be used to fill in the entry",
123 			      G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
124     g_object_class_install_property (
125 	object_class, PROP_LOCATION,
126 	g_param_spec_pointer ("location",
127 			      "Location",
128 			      "The selected MateWeatherLocation",
129 			      G_PARAM_READWRITE));
130 }
131 
132 static void
set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)133 set_property (GObject *object, guint prop_id,
134 	      const GValue *value, GParamSpec *pspec)
135 {
136     switch (prop_id) {
137     case PROP_TOP:
138 	mateweather_location_entry_build_model (MATEWEATHER_LOCATION_ENTRY (object),
139 					     g_value_get_pointer (value));
140 	break;
141     case PROP_LOCATION:
142 	mateweather_location_entry_set_location (MATEWEATHER_LOCATION_ENTRY (object),
143 					      g_value_get_pointer (value));
144 	break;
145     default:
146 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
147 	break;
148     }
149 }
150 
151 static void
get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)152 get_property (GObject *object, guint prop_id,
153 	      GValue *value, GParamSpec *pspec)
154 {
155     MateWeatherLocationEntry *entry = MATEWEATHER_LOCATION_ENTRY (object);
156 
157     switch (prop_id) {
158     case PROP_LOCATION:
159 	g_value_set_pointer (value, entry->location);
160 	break;
161     default:
162 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
163 	break;
164     }
165 }
166 
167 static void
entry_changed(MateWeatherLocationEntry * entry)168 entry_changed (MateWeatherLocationEntry *entry)
169 {
170     entry->custom_text = TRUE;
171 }
172 
173 static void
set_location_internal(MateWeatherLocationEntry * entry,GtkTreeModel * model,GtkTreeIter * iter)174 set_location_internal (MateWeatherLocationEntry *entry,
175 		       GtkTreeModel          *model,
176 		       GtkTreeIter           *iter)
177 {
178     MateWeatherLocation *loc;
179     char *name;
180 
181     if (entry->location)
182 	mateweather_location_unref (entry->location);
183 
184     if (iter) {
185 	gtk_tree_model_get (model, iter,
186 			    MATEWEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME, &name,
187 			    MATEWEATHER_LOCATION_ENTRY_COL_LOCATION, &loc,
188 			    -1);
189 	entry->location = mateweather_location_ref (loc);
190 	gtk_entry_set_text (GTK_ENTRY (entry), name);
191 	entry->custom_text = FALSE;
192 	g_free (name);
193     } else {
194 	entry->location = NULL;
195 	gtk_entry_set_text (GTK_ENTRY (entry), "");
196 	entry->custom_text = TRUE;
197     }
198 
199     gtk_editable_select_region (GTK_EDITABLE (entry), 0, -1);
200     g_object_notify (G_OBJECT (entry), "location");
201 }
202 
203 /**
204  * mateweather_location_entry_set_location:
205  * @entry: a #MateWeatherLocationEntry
206  * @loc: (allow-none): a #MateWeatherLocation in @entry, or %NULL to
207  * clear @entry
208  *
209  * Sets @entry's location to @loc, and updates the text of the
210  * entry accordingly.
211  **/
212 void
mateweather_location_entry_set_location(MateWeatherLocationEntry * entry,MateWeatherLocation * loc)213 mateweather_location_entry_set_location (MateWeatherLocationEntry *entry,
214 				      MateWeatherLocation      *loc)
215 {
216     GtkEntryCompletion *completion;
217     GtkTreeModel *model;
218     GtkTreeIter iter;
219     MateWeatherLocation *cmploc;
220 
221     g_return_if_fail (MATEWEATHER_IS_LOCATION_ENTRY (entry));
222 
223     completion = gtk_entry_get_completion (GTK_ENTRY (entry));
224     model = gtk_entry_completion_get_model (completion);
225 
226     gtk_tree_model_get_iter_first (model, &iter);
227     do {
228 	gtk_tree_model_get (model, &iter,
229 			    MATEWEATHER_LOCATION_ENTRY_COL_LOCATION, &cmploc,
230 			    -1);
231 	if (loc == cmploc) {
232 	    set_location_internal (entry, model, &iter);
233 	    return;
234 	}
235     } while (gtk_tree_model_iter_next (model, &iter));
236 
237     set_location_internal (entry, model, NULL);
238 }
239 
240 /**
241  * mateweather_location_entry_get_location:
242  * @entry: a #MateWeatherLocationEntry
243  *
244  * Gets the location that was set by a previous call to
245  * mateweather_location_entry_set_location() or was selected by the user.
246  *
247  * Return value: (transfer full) (allow-none): the selected location
248  * (which you must unref when you are done with it), or %NULL if no
249  * location is selected.
250  **/
251 MateWeatherLocation *
mateweather_location_entry_get_location(MateWeatherLocationEntry * entry)252 mateweather_location_entry_get_location (MateWeatherLocationEntry *entry)
253 {
254     g_return_val_if_fail (MATEWEATHER_IS_LOCATION_ENTRY (entry), NULL);
255 
256     if (entry->location)
257 	return mateweather_location_ref (entry->location);
258     else
259 	return NULL;
260 }
261 
262 /**
263  * mateweather_location_entry_has_custom_text:
264  * @entry: a #MateWeatherLocationEntry
265  *
266  * Checks whether or not @entry's text has been modified by the user.
267  * Note that this does not mean that no location is associated with @entry.
268  * mateweather_location_entry_get_location() should be used for this.
269  *
270  * Return value: %TRUE if @entry's text was modified by the user, or %FALSE if
271  * it's set to the default text of a location.
272  **/
273 gboolean
mateweather_location_entry_has_custom_text(MateWeatherLocationEntry * entry)274 mateweather_location_entry_has_custom_text (MateWeatherLocationEntry *entry)
275 {
276     g_return_val_if_fail (MATEWEATHER_IS_LOCATION_ENTRY (entry), FALSE);
277 
278     return entry->custom_text;
279 }
280 
281 /**
282  * mateweather_location_entry_set_city:
283  * @entry: a #MateWeatherLocationEntry
284  * @city_name: (allow-none): the city name, or %NULL
285  * @code: the METAR station code
286  *
287  * Sets @entry's location to a city with the given @code, and given
288  * @city_name, if non-%NULL. If there is no matching city, sets
289  * @entry's location to %NULL.
290  *
291  * Return value: %TRUE if @entry's location could be set to a matching city,
292  * %FALSE otherwise.
293  **/
294 gboolean
mateweather_location_entry_set_city(MateWeatherLocationEntry * entry,const char * city_name,const char * code)295 mateweather_location_entry_set_city (MateWeatherLocationEntry *entry,
296 				  const char            *city_name,
297 				  const char            *code)
298 {
299     GtkEntryCompletion *completion;
300     GtkTreeModel *model;
301     GtkTreeIter iter;
302     MateWeatherLocation *cmploc;
303     const char *cmpcode;
304     char *cmpname;
305 
306     g_return_val_if_fail (MATEWEATHER_IS_LOCATION_ENTRY (entry), FALSE);
307     g_return_val_if_fail (code != NULL, FALSE);
308 
309     completion = gtk_entry_get_completion (GTK_ENTRY (entry));
310     model = gtk_entry_completion_get_model (completion);
311 
312     gtk_tree_model_get_iter_first (model, &iter);
313     do {
314 	gtk_tree_model_get (model, &iter,
315 			    MATEWEATHER_LOCATION_ENTRY_COL_LOCATION, &cmploc,
316 			    -1);
317 
318 	cmpcode = mateweather_location_get_code (cmploc);
319 	if (!cmpcode || strcmp (cmpcode, code) != 0)
320 	    continue;
321 
322 	if (city_name) {
323 	    cmpname = mateweather_location_get_city_name (cmploc);
324 	    if (!cmpname || strcmp (cmpname, city_name) != 0) {
325 		g_free (cmpname);
326 		continue;
327 	    }
328 	    g_free (cmpname);
329 	}
330 
331 	set_location_internal (entry, model, &iter);
332 	return TRUE;
333     } while (gtk_tree_model_iter_next (model, &iter));
334 
335     set_location_internal (entry, model, NULL);
336 
337     return FALSE;
338 }
339 
340 static void
fill_location_entry_model(GtkTreeStore * store,MateWeatherLocation * loc,const char * parent_display_name,const char * parent_compare_name)341 fill_location_entry_model (GtkTreeStore *store, MateWeatherLocation *loc,
342 			   const char *parent_display_name,
343 			   const char *parent_compare_name)
344 {
345     MateWeatherLocation **children;
346     char *display_name, *compare_name;
347     GtkTreeIter iter;
348     int i;
349 
350     children = mateweather_location_get_children (loc);
351 
352     switch (mateweather_location_get_level (loc)) {
353     case MATEWEATHER_LOCATION_WORLD:
354     case MATEWEATHER_LOCATION_REGION:
355     case MATEWEATHER_LOCATION_ADM2:
356 	/* Ignore these levels of hierarchy; just recurse, passing on
357 	 * the names from the parent node.
358 	 */
359 	for (i = 0; children[i]; i++) {
360 	    fill_location_entry_model (store, children[i],
361 				       parent_display_name,
362 				       parent_compare_name);
363 	}
364 	break;
365 
366     case MATEWEATHER_LOCATION_COUNTRY:
367 	/* Recurse, initializing the names to the country name */
368 	for (i = 0; children[i]; i++) {
369 	    fill_location_entry_model (store, children[i],
370 				       mateweather_location_get_name (loc),
371 				       mateweather_location_get_sort_name (loc));
372 	}
373 	break;
374 
375     case MATEWEATHER_LOCATION_ADM1:
376 	/* Recurse, adding the ADM1 name to the country name */
377 	display_name = g_strdup_printf ("%s, %s", mateweather_location_get_name (loc), parent_display_name);
378 	compare_name = g_strdup_printf ("%s, %s", mateweather_location_get_sort_name (loc), parent_compare_name);
379 
380 	for (i = 0; children[i]; i++) {
381 	    fill_location_entry_model (store, children[i],
382 				       display_name, compare_name);
383 	}
384 
385 	g_free (display_name);
386 	g_free (compare_name);
387 	break;
388 
389     case MATEWEATHER_LOCATION_CITY:
390 	if (children[0] && children[1]) {
391 	    /* If there are multiple (<location>) children, add a line
392 	     * for each of them.
393 	     */
394 	    for (i = 0; children[i]; i++) {
395 		display_name = g_strdup_printf ("%s (%s), %s",
396 						mateweather_location_get_name (loc),
397 						mateweather_location_get_name (children[i]),
398 						parent_display_name);
399 		compare_name = g_strdup_printf ("%s (%s), %s",
400 						mateweather_location_get_sort_name (loc),
401 						mateweather_location_get_sort_name (children[i]),
402 						parent_compare_name);
403 
404 		gtk_tree_store_append (store, &iter, NULL);
405 		gtk_tree_store_set (store, &iter,
406 				    MATEWEATHER_LOCATION_ENTRY_COL_LOCATION, children[i],
407 				    MATEWEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME, display_name,
408 				    MATEWEATHER_LOCATION_ENTRY_COL_COMPARE_NAME, compare_name,
409 				    -1);
410 
411 		g_free (display_name);
412 		g_free (compare_name);
413 	    }
414 	} else if (children[0]) {
415 	    /* Else there's only one location. This is a mix of the
416 	     * city-with-multiple-location case above and the
417 	     * location-with-no-city case below.
418 	     */
419 	    display_name = g_strdup_printf ("%s, %s",
420 					    mateweather_location_get_name (loc),
421 					    parent_display_name);
422 	    compare_name = g_strdup_printf ("%s, %s",
423 					    mateweather_location_get_sort_name (loc),
424 					    parent_compare_name);
425 
426 	    gtk_tree_store_append (store, &iter, NULL);
427 	    gtk_tree_store_set (store, &iter,
428 				MATEWEATHER_LOCATION_ENTRY_COL_LOCATION, children[0],
429 				MATEWEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME, display_name,
430 				MATEWEATHER_LOCATION_ENTRY_COL_COMPARE_NAME, compare_name,
431 				-1);
432 
433 	    g_free (display_name);
434 	    g_free (compare_name);
435 	}
436 	break;
437 
438     case MATEWEATHER_LOCATION_WEATHER_STATION:
439 	/* <location> with no parent <city>, or <city> with a single
440 	 * child <location>.
441 	 */
442 	display_name = g_strdup_printf ("%s, %s",
443 					mateweather_location_get_name (loc),
444 					parent_display_name);
445 	compare_name = g_strdup_printf ("%s, %s",
446 					mateweather_location_get_sort_name (loc),
447 					parent_compare_name);
448 
449 	gtk_tree_store_append (store, &iter, NULL);
450 	gtk_tree_store_set (store, &iter,
451 			    MATEWEATHER_LOCATION_ENTRY_COL_LOCATION, loc,
452 			    MATEWEATHER_LOCATION_ENTRY_COL_DISPLAY_NAME, display_name,
453 			    MATEWEATHER_LOCATION_ENTRY_COL_COMPARE_NAME, compare_name,
454 			    -1);
455 
456 	g_free (display_name);
457 	g_free (compare_name);
458 	break;
459     }
460 
461     mateweather_location_free_children (loc, children);
462 }
463 
464 static void
mateweather_location_entry_build_model(MateWeatherLocationEntry * entry,MateWeatherLocation * top)465 mateweather_location_entry_build_model (MateWeatherLocationEntry *entry,
466 				     MateWeatherLocation *top)
467 {
468     GtkTreeStore *store = NULL;
469 
470     g_return_if_fail (MATEWEATHER_IS_LOCATION_ENTRY (entry));
471     entry->top = mateweather_location_ref (top);
472 
473     store = gtk_tree_store_new (4, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_STRING, G_TYPE_STRING);
474     fill_location_entry_model (store, top, NULL, NULL);
475     gtk_entry_completion_set_model (gtk_entry_get_completion (GTK_ENTRY (entry)),
476 				    GTK_TREE_MODEL (store));
477     g_object_unref (store);
478 }
479 
480 static char *
find_word(const char * full_name,const char * word,int word_len,gboolean whole_word,gboolean is_first_word)481 find_word (const char *full_name, const char *word, int word_len,
482 	   gboolean whole_word, gboolean is_first_word)
483 {
484     char *p = (char *)full_name - 1;
485 
486     while ((p = strchr (p + 1, *word))) {
487 	if (strncmp (p, word, word_len) != 0)
488 	    continue;
489 
490 	if (p > (char *)full_name) {
491 	    char *prev = g_utf8_prev_char (p);
492 
493 	    /* Make sure p points to the start of a word */
494 	    if (g_unichar_isalpha (g_utf8_get_char (prev)))
495 		continue;
496 
497 	    /* If we're matching the first word of the key, it has to
498 	     * match the first word of the location, city, state, or
499 	     * country. Eg, it either matches the start of the string
500 	     * (which we already know it doesn't at this point) or
501 	     * it is preceded by the string ", " (which isn't actually
502 	     * a perfect test. FIXME)
503 	     */
504 	    if (is_first_word) {
505 		if (prev == (char *)full_name || strncmp (prev - 1, ", ", 2) != 0)
506 		    continue;
507 	    }
508 	}
509 
510 	if (whole_word && g_unichar_isalpha (g_utf8_get_char (p + word_len)))
511 	    continue;
512 
513 	return p;
514     }
515     return NULL;
516 }
517 
518 static gboolean
matcher(GtkEntryCompletion * completion,const char * key,GtkTreeIter * iter,gpointer user_data)519 matcher (GtkEntryCompletion *completion, const char *key,
520 	 GtkTreeIter *iter, gpointer user_data)
521 {
522     char *name, *name_mem;
523     MateWeatherLocation *loc;
524     gboolean is_first_word = TRUE, match;
525     int len;
526 
527     gtk_tree_model_get (gtk_entry_completion_get_model (completion), iter,
528 			MATEWEATHER_LOCATION_ENTRY_COL_COMPARE_NAME, &name_mem,
529 			MATEWEATHER_LOCATION_ENTRY_COL_LOCATION, &loc,
530 			-1);
531     name = name_mem;
532 
533     if (!loc) {
534 	g_free (name_mem);
535 	return FALSE;
536     }
537 
538     /* All but the last word in KEY must match a full word from NAME,
539      * in order (but possibly skipping some words from NAME).
540      */
541     len = strcspn (key, " ");
542     while (key[len]) {
543 	name = find_word (name, key, len, TRUE, is_first_word);
544 	if (!name) {
545 	    g_free (name_mem);
546 	    return FALSE;
547 	}
548 
549 	key += len;
550 	while (*key && !g_unichar_isalpha (g_utf8_get_char (key)))
551 	    key = g_utf8_next_char (key);
552 	while (*name && !g_unichar_isalpha (g_utf8_get_char (name)))
553 	    name = g_utf8_next_char (name);
554 
555 	len = strcspn (key, " ");
556 	is_first_word = FALSE;
557     }
558 
559     /* The last word in KEY must match a prefix of a following word in NAME */
560     match = find_word (name, key, strlen (key), FALSE, is_first_word) != NULL;
561     g_free (name_mem);
562     return match;
563 }
564 
565 static gboolean
match_selected(GtkEntryCompletion * completion,GtkTreeModel * model,GtkTreeIter * iter,gpointer entry)566 match_selected (GtkEntryCompletion *completion,
567 		GtkTreeModel       *model,
568 		GtkTreeIter        *iter,
569 		gpointer            entry)
570 {
571     set_location_internal (entry, model, iter);
572     return TRUE;
573 }
574 
575 /**
576  * mateweather_location_entry_new:
577  * @top: the top-level location for the entry.
578  *
579  * Creates a new #MateWeatherLocationEntry.
580  *
581  * @top will normally be a location returned from
582  * mateweather_location_new_world(), but you can create an entry that
583  * only accepts a smaller set of locations if you want.
584  *
585  * Return value: the new #MateWeatherLocationEntry
586  **/
587 GtkWidget *
mateweather_location_entry_new(MateWeatherLocation * top)588 mateweather_location_entry_new (MateWeatherLocation *top)
589 {
590     return g_object_new (MATEWEATHER_TYPE_LOCATION_ENTRY,
591 			 "top", top,
592 			 NULL);
593 }
594