1 /*
2  * This program is free software; you can redistribute it and/or modify it
3  * under the terms of the GNU Lesser General Public License as published by
4  * the Free Software Foundation.
5  *
6  * This program is distributed in the hope that it will be useful, but
7  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
8  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
9  * for more details.
10  *
11  * You should have received a copy of the GNU Lesser General Public License
12  * along with this program; if not, see <http://www.gnu.org/licenses/>.
13  *
14  *
15  * Authors:
16  *		Damon Chaplin <damon@ximian.com>
17  *
18  * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
19  *
20  */
21 
22 /*
23  * ECellDateEdit - a subclass of ECellPopup used to show a date with a popup
24  * window to edit it.
25  */
26 
27 #include "evolution-config.h"
28 
29 #include "e-cell-date-edit.h"
30 
31 #include <string.h>
32 #include <time.h>
33 
34 #include <gtk/gtk.h>
35 #include <glib/gi18n.h>
36 #include <gdk/gdkkeysyms.h>
37 
38 #include <libedataserver/libedataserver.h>
39 
40 #include "e-calendar.h"
41 #include "e-cell-text.h"
42 #include "e-table-item.h"
43 
44 static void e_cell_date_edit_get_property	(GObject	*object,
45 						 guint		 property_id,
46 						 GValue		*value,
47 						 GParamSpec	*pspec);
48 static void e_cell_date_edit_set_property	(GObject	*object,
49 						 guint		 property_id,
50 						 const GValue	*value,
51 						 GParamSpec	*pspec);
52 static void e_cell_date_edit_dispose		(GObject	*object);
53 
54 static gint e_cell_date_edit_do_popup		(ECellPopup	*ecp,
55 						 GdkEvent	*event,
56 						 gint             row,
57 						 gint             view_col);
58 static void e_cell_date_edit_set_popup_values	(ECellDateEdit	*ecde);
59 static void e_cell_date_edit_select_matching_time (ECellDateEdit	*ecde,
60 						  gchar		*time);
61 static void e_cell_date_edit_show_popup		(ECellDateEdit	*ecde,
62 						 gint             row,
63 						 gint             view_col);
64 static void e_cell_date_edit_get_popup_pos	(ECellDateEdit	*ecde,
65 						 gint             row,
66 						 gint             view_col,
67 						 gint		*x,
68 						 gint		*y,
69 						 gint		*height,
70 						 gint		*width);
71 
72 static void e_cell_date_edit_rebuild_time_list	(ECellDateEdit	*ecde);
73 
74 static gint e_cell_date_edit_key_press		(GtkWidget	*popup_window,
75 						 GdkEventKey	*event,
76 						 ECellDateEdit	*ecde);
77 static gint  e_cell_date_edit_button_press	(GtkWidget	*popup_window,
78 						 GdkEvent	*button_event,
79 						 ECellDateEdit	*ecde);
80 static void e_cell_date_edit_on_ok_clicked	(GtkWidget	*button,
81 						 ECellDateEdit	*ecde);
82 static void e_cell_date_edit_show_time_invalid_warning	(ECellDateEdit	*ecde);
83 static void e_cell_date_edit_on_now_clicked	(GtkWidget	*button,
84 						 ECellDateEdit	*ecde);
85 static void e_cell_date_edit_on_none_clicked	(GtkWidget	*button,
86 						 ECellDateEdit	*ecde);
87 static void e_cell_date_edit_on_today_clicked	(GtkWidget	*button,
88 						 ECellDateEdit	*ecde);
89 static void e_cell_date_edit_update_cell	(ECellDateEdit	*ecde,
90 						 const gchar	*text);
91 static void e_cell_date_edit_on_time_selected	(GtkTreeSelection *selection,
92 						 ECellDateEdit *ecde);
93 static void e_cell_date_edit_hide_popup		(ECellDateEdit	*ecde);
94 
95 /* Our arguments. */
96 enum {
97 	PROP_0,
98 	PROP_SHOW_TIME,
99 	PROP_SHOW_NOW_BUTTON,
100 	PROP_SHOW_TODAY_BUTTON,
101 	PROP_ALLOW_NO_DATE_SET,
102 	PROP_USE_24_HOUR_FORMAT,
103 	PROP_LOWER_HOUR,
104 	PROP_UPPER_HOUR
105 };
106 
107 enum {
108 	BEFORE_POPUP,
109 	LAST_SIGNAL
110 };
111 
112 static guint signals[LAST_SIGNAL];
113 
G_DEFINE_TYPE(ECellDateEdit,e_cell_date_edit,E_TYPE_CELL_POPUP)114 G_DEFINE_TYPE (ECellDateEdit, e_cell_date_edit, E_TYPE_CELL_POPUP)
115 
116 static void
117 e_cell_date_edit_class_init (ECellDateEditClass *class)
118 {
119 	GObjectClass *object_class;
120 	ECellPopupClass	*ecpc;
121 
122 	object_class = G_OBJECT_CLASS (class);
123 	object_class->get_property = e_cell_date_edit_get_property;
124 	object_class->set_property = e_cell_date_edit_set_property;
125 	object_class->dispose = e_cell_date_edit_dispose;
126 
127 	ecpc = E_CELL_POPUP_CLASS (class);
128 	ecpc->popup = e_cell_date_edit_do_popup;
129 
130 	g_object_class_install_property (
131 		object_class,
132 		PROP_SHOW_TIME,
133 		g_param_spec_boolean (
134 			"show_time",
135 			NULL,
136 			NULL,
137 			TRUE,
138 			G_PARAM_READWRITE));
139 
140 	g_object_class_install_property (
141 		object_class,
142 		PROP_SHOW_NOW_BUTTON,
143 		g_param_spec_boolean (
144 			"show_now_button",
145 			NULL,
146 			NULL,
147 			TRUE,
148 			G_PARAM_READWRITE));
149 
150 	g_object_class_install_property (
151 		object_class,
152 		PROP_SHOW_TODAY_BUTTON,
153 		g_param_spec_boolean (
154 			"show_today_button",
155 			NULL,
156 			NULL,
157 			TRUE,
158 			G_PARAM_READWRITE));
159 
160 	g_object_class_install_property (
161 		object_class,
162 		PROP_ALLOW_NO_DATE_SET,
163 		g_param_spec_boolean (
164 			"allow_no_date_set",
165 			NULL,
166 			NULL,
167 			TRUE,
168 			G_PARAM_READWRITE));
169 
170 	g_object_class_install_property (
171 		object_class,
172 		PROP_USE_24_HOUR_FORMAT,
173 		g_param_spec_boolean (
174 			"use_24_hour_format",
175 			NULL,
176 			NULL,
177 			TRUE,
178 			G_PARAM_READWRITE));
179 
180 	g_object_class_install_property (
181 		object_class,
182 		PROP_LOWER_HOUR,
183 		g_param_spec_int (
184 			"lower_hour",
185 			NULL,
186 			NULL,
187 			G_MININT,
188 			G_MAXINT,
189 			0,
190 			G_PARAM_READWRITE));
191 
192 	g_object_class_install_property (
193 		object_class,
194 		PROP_UPPER_HOUR,
195 		g_param_spec_int (
196 			"upper_hour",
197 			NULL,
198 			NULL,
199 			G_MININT,
200 			G_MAXINT,
201 			24,
202 			G_PARAM_READWRITE));
203 
204 	signals[BEFORE_POPUP] = g_signal_new (
205 		"before-popup",
206 		G_TYPE_FROM_CLASS (class),
207 		G_SIGNAL_RUN_FIRST,
208 		0,
209 		NULL, NULL, NULL,
210 		G_TYPE_NONE, 2,
211 		G_TYPE_INT, G_TYPE_INT);
212 }
213 
214 static void
e_cell_date_edit_init(ECellDateEdit * ecde)215 e_cell_date_edit_init (ECellDateEdit *ecde)
216 {
217 	GtkWidget *frame, *vbox, *hbox, *vbox2;
218 	GtkWidget *scrolled_window, *bbox, *tree_view;
219 	GtkWidget *now_button, *today_button, *none_button, *ok_button;
220 	GtkListStore *store;
221 	GtkCellRenderer *renderer;
222 	PangoAttrList *tnum;
223 	PangoAttribute *attr;
224 
225 	ecde->lower_hour = 0;
226 	ecde->upper_hour = 24;
227 	ecde->use_24_hour_format = TRUE;
228 	ecde->need_time_list_rebuild = TRUE;
229 	ecde->freeze_count = 0;
230 	ecde->time_callback = NULL;
231 	ecde->time_callback_data = NULL;
232 	ecde->time_callback_destroy = NULL;
233 
234 	/* We create one popup window for the ECell, since there will only
235 	 * ever be one popup in use at a time. */
236 	ecde->popup_window = gtk_window_new (GTK_WINDOW_POPUP);
237 
238 	gtk_window_set_type_hint (
239 		GTK_WINDOW (ecde->popup_window),
240 		GDK_WINDOW_TYPE_HINT_COMBO);
241 	gtk_window_set_resizable (GTK_WINDOW (ecde->popup_window), TRUE);
242 
243 	frame = gtk_frame_new (NULL);
244 	gtk_container_add (GTK_CONTAINER (ecde->popup_window), frame);
245 	gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_OUT);
246 	gtk_widget_show (frame);
247 
248 	vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
249 	gtk_container_set_border_width (GTK_CONTAINER (vbox), 6);
250 	gtk_container_add (GTK_CONTAINER (frame), vbox);
251 	gtk_widget_show (vbox);
252 
253 	hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
254 	gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0);
255 	gtk_widget_show (hbox);
256 
257 	ecde->calendar = e_calendar_new ();
258 	gnome_canvas_item_set (
259 		GNOME_CANVAS_ITEM (e_calendar_get_item (E_CALENDAR (ecde->calendar))),
260 		"move_selection_when_moving", FALSE,
261 		NULL);
262 	gtk_box_pack_start (GTK_BOX (hbox), ecde->calendar, TRUE, TRUE, 0);
263 	gtk_widget_show (ecde->calendar);
264 
265 	vbox2 = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
266 	gtk_style_context_add_class (
267 		gtk_widget_get_style_context (vbox2), "linked");
268 	gtk_box_pack_start (GTK_BOX (hbox), vbox2, TRUE, TRUE, 0);
269 	gtk_widget_show (vbox2);
270 
271 	ecde->time_entry = gtk_entry_new ();
272 	gtk_widget_set_size_request (ecde->time_entry, 50, -1);
273 	gtk_box_pack_start (
274 		GTK_BOX (vbox2), ecde->time_entry,
275 		FALSE, FALSE, 0);
276 	gtk_widget_show (ecde->time_entry);
277 
278 	scrolled_window = gtk_scrolled_window_new (NULL, NULL);
279 	gtk_scrolled_window_set_shadow_type (
280 		GTK_SCROLLED_WINDOW (scrolled_window),
281 		GTK_SHADOW_IN);
282 	gtk_box_pack_start (GTK_BOX (vbox2), scrolled_window, TRUE, TRUE, 0);
283 	gtk_scrolled_window_set_policy (
284 		GTK_SCROLLED_WINDOW (scrolled_window),
285 		GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
286 	gtk_widget_show (scrolled_window);
287 
288 	store = gtk_list_store_new (1, G_TYPE_STRING);
289 	tree_view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store));
290 	g_object_unref (store);
291 
292 	renderer = gtk_cell_renderer_text_new ();
293 	tnum = pango_attr_list_new ();
294 	attr = pango_attr_font_features_new ("tnum=1");
295 	pango_attr_list_insert_before (tnum, attr);
296 	g_object_set (renderer, "attributes", tnum, NULL);
297 	pango_attr_list_unref (tnum);
298 
299 	gtk_tree_view_append_column (
300 		GTK_TREE_VIEW (tree_view),
301 		gtk_tree_view_column_new_with_attributes (
302 		"Text", renderer, "text", 0, NULL));
303 
304 	gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (tree_view), FALSE);
305 
306 	gtk_scrolled_window_add_with_viewport (
307 		GTK_SCROLLED_WINDOW (scrolled_window), tree_view);
308 	gtk_container_set_focus_vadjustment (
309 		GTK_CONTAINER (tree_view),
310 		gtk_scrolled_window_get_vadjustment (
311 		GTK_SCROLLED_WINDOW (scrolled_window)));
312 	gtk_container_set_focus_hadjustment (
313 		GTK_CONTAINER (tree_view),
314 		gtk_scrolled_window_get_hadjustment (
315 		GTK_SCROLLED_WINDOW (scrolled_window)));
316 	gtk_widget_show (tree_view);
317 	ecde->time_tree_view = tree_view;
318 	g_signal_connect (
319 		gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view)), "changed",
320 		G_CALLBACK (e_cell_date_edit_on_time_selected), ecde);
321 
322 	e_binding_bind_property (ecde->time_entry, "visible",
323 		vbox2, "visible", G_BINDING_DEFAULT);
324 
325 	bbox = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
326 	gtk_box_set_spacing (GTK_BOX (bbox), 2);
327 	gtk_box_pack_start (GTK_BOX (vbox), bbox, FALSE, FALSE, 0);
328 	gtk_widget_show (bbox);
329 
330 	now_button = gtk_button_new_with_label (_("Now"));
331 	gtk_container_add (GTK_CONTAINER (bbox), now_button);
332 	gtk_widget_show (now_button);
333 	g_signal_connect (
334 		now_button, "clicked",
335 		G_CALLBACK (e_cell_date_edit_on_now_clicked), ecde);
336 	ecde->now_button = now_button;
337 
338 	today_button = gtk_button_new_with_label (_("Today"));
339 	gtk_container_add (GTK_CONTAINER (bbox), today_button);
340 	gtk_widget_show (today_button);
341 	g_signal_connect (
342 		today_button, "clicked",
343 		G_CALLBACK (e_cell_date_edit_on_today_clicked), ecde);
344 	ecde->today_button = today_button;
345 
346 	/* Translators: "None" as a label of a button to unset date in a
347 	 * date table cell. */
348 	none_button = gtk_button_new_with_label (C_("table-date", "None"));
349 	gtk_container_add (GTK_CONTAINER (bbox), none_button);
350 	gtk_widget_show (none_button);
351 	g_signal_connect (
352 		none_button, "clicked",
353 		G_CALLBACK (e_cell_date_edit_on_none_clicked), ecde);
354 	ecde->none_button = none_button;
355 
356 	ok_button = gtk_button_new_with_label (_("OK"));
357 	gtk_container_add (GTK_CONTAINER (bbox), ok_button);
358 	gtk_widget_show (ok_button);
359 	g_signal_connect (
360 		ok_button, "clicked",
361 		G_CALLBACK (e_cell_date_edit_on_ok_clicked), ecde);
362 
363 	g_signal_connect (
364 		ecde->popup_window, "key_press_event",
365 		G_CALLBACK (e_cell_date_edit_key_press), ecde);
366 	g_signal_connect (
367 		ecde->popup_window, "button_press_event",
368 		G_CALLBACK (e_cell_date_edit_button_press), ecde);
369 }
370 
371 /**
372  * e_cell_date_edit_new:
373  *
374  * Creates a new ECellDateEdit renderer.
375  *
376  * Returns: an ECellDateEdit object.
377  */
378 ECell *
e_cell_date_edit_new(void)379 e_cell_date_edit_new (void)
380 {
381 	return g_object_new (e_cell_date_edit_get_type (), NULL);
382 }
383 
384 static void
e_cell_date_edit_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)385 e_cell_date_edit_get_property (GObject *object,
386                                guint property_id,
387                                GValue *value,
388                                GParamSpec *pspec)
389 {
390 	ECellDateEdit *ecde;
391 
392 	ecde = E_CELL_DATE_EDIT (object);
393 
394 	switch (property_id) {
395 	case PROP_SHOW_TIME:
396 		g_value_set_boolean (value, gtk_widget_get_visible (ecde->time_entry));
397 		return;
398 	case PROP_SHOW_NOW_BUTTON:
399 		g_value_set_boolean (value, gtk_widget_get_visible (ecde->now_button));
400 		return;
401 	case PROP_SHOW_TODAY_BUTTON:
402 		g_value_set_boolean (value, gtk_widget_get_visible (ecde->today_button));
403 		return;
404 	case PROP_ALLOW_NO_DATE_SET:
405 		g_value_set_boolean (value, gtk_widget_get_visible (ecde->none_button));
406 		return;
407 	case PROP_USE_24_HOUR_FORMAT:
408 		g_value_set_boolean (value, ecde->use_24_hour_format);
409 		return;
410 	case PROP_LOWER_HOUR:
411 		g_value_set_int (value, ecde->lower_hour);
412 		return;
413 	case PROP_UPPER_HOUR:
414 		g_value_set_int (value, ecde->upper_hour);
415 		return;
416 	}
417 
418 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
419 }
420 
421 static void
e_cell_date_edit_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)422 e_cell_date_edit_set_property (GObject *object,
423                                guint property_id,
424                                const GValue *value,
425                                GParamSpec *pspec)
426 {
427 	ECellDateEdit *ecde;
428 	gint ivalue;
429 	gboolean bvalue;
430 
431 	ecde = E_CELL_DATE_EDIT (object);
432 
433 	switch (property_id) {
434 	case PROP_SHOW_TIME:
435 		if (g_value_get_boolean (value)) {
436 			gtk_widget_show (ecde->time_entry);
437 			gtk_widget_show (ecde->time_tree_view);
438 		} else {
439 			gtk_widget_hide (ecde->time_entry);
440 			gtk_widget_hide (ecde->time_tree_view);
441 		}
442 		return;
443 	case PROP_SHOW_NOW_BUTTON:
444 		if (g_value_get_boolean (value)) {
445 			gtk_widget_show (ecde->now_button);
446 		} else {
447 			gtk_widget_hide (ecde->now_button);
448 		}
449 		return;
450 	case PROP_SHOW_TODAY_BUTTON:
451 		if (g_value_get_boolean (value)) {
452 			gtk_widget_show (ecde->today_button);
453 		} else {
454 			gtk_widget_hide (ecde->today_button);
455 		}
456 		return;
457 	case PROP_ALLOW_NO_DATE_SET:
458 		if (g_value_get_boolean (value)) {
459 			gtk_widget_show (ecde->none_button);
460 		} else {
461 			/* FIXME: What if we have no date set now. */
462 			gtk_widget_hide (ecde->none_button);
463 		}
464 		return;
465 	case PROP_USE_24_HOUR_FORMAT:
466 		bvalue = g_value_get_boolean (value);
467 		if (ecde->use_24_hour_format != bvalue) {
468 			ecde->use_24_hour_format = bvalue;
469 			ecde->need_time_list_rebuild = TRUE;
470 		}
471 		return;
472 	case PROP_LOWER_HOUR:
473 		ivalue = g_value_get_int (value);
474 		ivalue = CLAMP (ivalue, 0, 24);
475 		if (ecde->lower_hour != ivalue) {
476 			ecde->lower_hour = ivalue;
477 			ecde->need_time_list_rebuild = TRUE;
478 		}
479 		return;
480 	case PROP_UPPER_HOUR:
481 		ivalue = g_value_get_int (value);
482 		ivalue = CLAMP (ivalue, 0, 24);
483 		if (ecde->upper_hour != ivalue) {
484 			ecde->upper_hour = ivalue;
485 			ecde->need_time_list_rebuild = TRUE;
486 		}
487 		return;
488 	}
489 
490 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
491 }
492 
493 static void
e_cell_date_edit_dispose(GObject * object)494 e_cell_date_edit_dispose (GObject *object)
495 {
496 	ECellDateEdit *ecde = E_CELL_DATE_EDIT (object);
497 
498 	e_cell_date_edit_set_get_time_callback (ecde, NULL, NULL, NULL);
499 
500 	g_clear_pointer (&ecde->popup_window, gtk_widget_destroy);
501 
502 	/* Chain up to parent's dispose() method. */
503 	G_OBJECT_CLASS (e_cell_date_edit_parent_class)->dispose (object);
504 }
505 
506 static gint
e_cell_date_edit_do_popup(ECellPopup * ecp,GdkEvent * event,gint row,gint view_col)507 e_cell_date_edit_do_popup (ECellPopup *ecp,
508                            GdkEvent *event,
509                            gint row,
510                            gint view_col)
511 {
512 	ECellDateEdit *ecde = E_CELL_DATE_EDIT (ecp);
513 	GdkWindow *window;
514 
515 	g_signal_emit (ecp, signals[BEFORE_POPUP], 0, row, view_col);
516 
517 	e_cell_date_edit_show_popup (ecde, row, view_col);
518 	e_cell_date_edit_set_popup_values (ecde);
519 
520 	gtk_grab_add (ecde->popup_window);
521 
522 	/* Set the focus to the first widget. */
523 	gtk_widget_grab_focus (ecde->time_entry);
524 	window = gtk_widget_get_window (ecde->popup_window);
525 	gdk_window_focus (window, GDK_CURRENT_TIME);
526 
527 	return TRUE;
528 }
529 
530 static void
e_cell_date_edit_set_popup_values(ECellDateEdit * ecde)531 e_cell_date_edit_set_popup_values (ECellDateEdit *ecde)
532 {
533 	ECellPopup *ecp = E_CELL_POPUP (ecde);
534 	ECellText *ecell_text = E_CELL_TEXT (ecp->child);
535 	ECellView *ecv = (ECellView *) ecp->popup_cell_view;
536 	ETableItem *eti;
537 	ETableCol *ecol;
538 	gchar *cell_text;
539 	ETimeParseStatus status;
540 	struct tm date_tm;
541 	GDate date;
542 	ECalendarItem *calitem;
543 	gchar buffer[64];
544 	gboolean is_date = TRUE;
545 
546 	eti = E_TABLE_ITEM (ecp->popup_cell_view->cell_view.e_table_item_view);
547 	ecol = e_table_header_get_column (eti->header, ecp->popup_view_col);
548 
549 	cell_text = e_cell_text_get_text (
550 		ecell_text, ecv->e_table_model,
551 		ecol->spec->model_col, ecp->popup_row);
552 
553 	/* Try to parse just a date first. If the value is only a date, we
554 	 * use a DATE value. */
555 	status = e_time_parse_date (cell_text, &date_tm);
556 	if (status == E_TIME_PARSE_INVALID) {
557 		is_date = FALSE;
558 		status = e_time_parse_date_and_time (cell_text, &date_tm);
559 	}
560 
561 	/* If there is no date and time set, or the date is invalid, we clear
562 	 * the selections, else we select the appropriate date & time. */
563 	calitem = E_CALENDAR_ITEM (e_calendar_get_item (E_CALENDAR (ecde->calendar)));
564 	if (status == E_TIME_PARSE_NONE || status == E_TIME_PARSE_INVALID) {
565 		gtk_entry_set_text (GTK_ENTRY (ecde->time_entry), "");
566 		e_calendar_item_set_selection (calitem, NULL, NULL);
567 		gtk_tree_selection_unselect_all (
568 			gtk_tree_view_get_selection (
569 			GTK_TREE_VIEW (ecde->time_tree_view)));
570 	} else {
571 		if (is_date) {
572 			buffer[0] = '\0';
573 		} else {
574 			e_time_format_time (
575 				&date_tm, ecde->use_24_hour_format,
576 				FALSE, buffer, sizeof (buffer));
577 		}
578 		gtk_entry_set_text (GTK_ENTRY (ecde->time_entry), buffer);
579 
580 		g_date_clear (&date, 1);
581 		g_date_set_dmy (
582 			&date,
583 			date_tm.tm_mday,
584 			date_tm.tm_mon + 1,
585 			date_tm.tm_year + 1900);
586 		e_calendar_item_set_selection (calitem, &date, &date);
587 
588 		if (is_date) {
589 			gtk_tree_selection_unselect_all (
590 				gtk_tree_view_get_selection (
591 				GTK_TREE_VIEW (ecde->time_tree_view)));
592 		} else {
593 			e_cell_date_edit_select_matching_time (ecde, buffer);
594 		}
595 	}
596 
597 	e_cell_text_free_text (ecell_text, ecv->e_table_model,
598 		ecol->spec->model_col, cell_text);
599 }
600 
601 static void
e_cell_date_edit_select_matching_time(ECellDateEdit * ecde,gchar * time)602 e_cell_date_edit_select_matching_time (ECellDateEdit *ecde,
603                                        gchar *time)
604 {
605 	gboolean found = FALSE;
606 	gboolean valid;
607 	GtkTreeSelection *selection;
608 	GtkTreeIter iter;
609 	GtkTreeModel *model;
610 
611 	model = gtk_tree_view_get_model (GTK_TREE_VIEW (ecde->time_tree_view));
612 	selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ecde->time_tree_view));
613 
614 	for (valid = gtk_tree_model_get_iter_first (model, &iter);
615 	     valid && !found;
616 	     valid = gtk_tree_model_iter_next (model, &iter)) {
617 		gchar *str = NULL;
618 
619 		gtk_tree_model_get (model, &iter, 0, &str, -1);
620 
621 		if (g_str_equal (str, time)) {
622 			GtkTreePath *path = gtk_tree_model_get_path (model, &iter);
623 
624 			gtk_tree_view_set_cursor (
625 				GTK_TREE_VIEW (ecde->time_tree_view),
626 				path, NULL, FALSE);
627 			gtk_tree_view_scroll_to_cell (
628 				GTK_TREE_VIEW (ecde->time_tree_view),
629 				path, NULL, FALSE, 0.0, 0.0);
630 			gtk_tree_path_free (path);
631 
632 			found = TRUE;
633 		}
634 
635 		g_free (str);
636 	}
637 
638 	if (!found) {
639 		gtk_tree_selection_unselect_all (selection);
640 		gtk_tree_view_scroll_to_point (GTK_TREE_VIEW (ecde->time_tree_view), 0, 0);
641 	}
642 }
643 
644 static void
e_cell_date_edit_show_popup(ECellDateEdit * ecde,gint row,gint view_col)645 e_cell_date_edit_show_popup (ECellDateEdit *ecde,
646                              gint row,
647                              gint view_col)
648 {
649 	ECellView *ecv = (ECellView *) E_CELL_POPUP (ecde)->popup_cell_view;
650 	GtkWidget *toplevel;
651 	gint x, y, width, height;
652 
653 	if (ecde->need_time_list_rebuild)
654 		e_cell_date_edit_rebuild_time_list (ecde);
655 
656 	/* This code is practically copied from GtkCombo. */
657 
658 	toplevel = gtk_widget_get_toplevel (GTK_WIDGET (GNOME_CANVAS_ITEM (ecv->e_table_item_view)->canvas));
659 	if (GTK_IS_WINDOW (toplevel))
660 		gtk_window_set_transient_for (GTK_WINDOW (ecde->popup_window), GTK_WINDOW (toplevel));
661 
662 	e_cell_date_edit_get_popup_pos (ecde, row, view_col, &x, &y, &height, &width);
663 
664 	gtk_window_move (GTK_WINDOW (ecde->popup_window), x, y);
665 	gtk_widget_set_size_request (ecde->popup_window, width, height);
666 	gtk_widget_realize (ecde->popup_window);
667 	gdk_window_resize (gtk_widget_get_window (ecde->popup_window), width, height);
668 	gtk_widget_show (ecde->popup_window);
669 
670 	e_cell_popup_set_shown (E_CELL_POPUP (ecde), TRUE);
671 }
672 
673 /* Calculates the size and position of the popup window (like GtkCombo). */
674 static void
e_cell_date_edit_get_popup_pos(ECellDateEdit * ecde,gint row,gint view_col,gint * x,gint * y,gint * height,gint * width)675 e_cell_date_edit_get_popup_pos (ECellDateEdit *ecde,
676                                 gint row,
677                                 gint view_col,
678                                 gint *x,
679                                 gint *y,
680                                 gint *height,
681                                 gint *width)
682 {
683 	ECellPopup *ecp = E_CELL_POPUP (ecde);
684 	ETableItem *eti;
685 	GtkWidget *canvas;
686 	GtkRequisition popup_requisition;
687 	GtkAdjustment *adjustment;
688 	GtkScrollable *scrollable;
689 	GdkWindow *window;
690 	gint avail_height, screen_width, column_width, row_height;
691 	gdouble x1, y1, wx, wy;
692 	gint value;
693 
694 	eti = E_TABLE_ITEM (ecp->popup_cell_view->cell_view.e_table_item_view);
695 	canvas = GTK_WIDGET (GNOME_CANVAS_ITEM (eti)->canvas);
696 
697 	window = gtk_widget_get_window (canvas);
698 	gdk_window_get_origin (window, x, y);
699 
700 	x1 = e_table_header_col_diff (eti->header, 0, view_col + 1);
701 	y1 = e_table_item_row_diff (eti, 0, row + 1);
702 	column_width = e_table_header_col_diff (
703 		eti->header, view_col, view_col + 1);
704 	row_height = e_table_item_row_diff (eti, row, row + 1);
705 	gnome_canvas_item_i2w (GNOME_CANVAS_ITEM (eti), &x1, &y1);
706 
707 	gnome_canvas_world_to_window (
708 		GNOME_CANVAS (canvas), x1, y1, &wx, &wy);
709 
710 	x1 = wx;
711 	y1 = wy;
712 
713 	*x += x1;
714 	/* The ETable positions don't include the grid lines, I think, so we
715 	 * add 1. */
716 	scrollable = GTK_SCROLLABLE (&GNOME_CANVAS (canvas)->layout);
717 	adjustment = gtk_scrollable_get_vadjustment (scrollable);
718 	value = (gint) gtk_adjustment_get_value (adjustment);
719 	*y += y1 + 1 - value + ((GnomeCanvas *)canvas)->zoom_yofs;
720 
721 	avail_height = gdk_screen_height () - *y;
722 
723 	/* We'll use the entire screen width if needed, but we save space for
724 	 * the vertical scrollbar in case we need to show that. */
725 	screen_width = gdk_screen_width ();
726 
727 	gtk_widget_get_preferred_size (ecde->popup_window, &popup_requisition, NULL);
728 
729 	/* Calculate the desired width. */
730 	*width = popup_requisition.width;
731 
732 	/* Use at least the same width as the column. */
733 	if (*width < column_width)
734 		*width = column_width;
735 
736 	/* Check if it fits in the available height. */
737 	if (popup_requisition.height > avail_height) {
738 		/* It doesn't fit, so we see if we have the minimum space
739 		 * needed. */
740 		if (*y - row_height > avail_height) {
741 			/* We don't, so we show the popup above the cell
742 			 * instead of below it. */
743 			*y -= (popup_requisition.height + row_height);
744 			if (*y < 0)
745 				*y = 0;
746 		}
747 	}
748 
749 	/* We try to line it up with the right edge of the column, but we don't
750 	 * want it to go off the edges of the screen. */
751 	if (*x > screen_width)
752 		*x = screen_width;
753 	*x -= *width;
754 	if (*x < 0)
755 		*x = 0;
756 
757 	*height = popup_requisition.height;
758 }
759 
760 /* This handles key press events in the popup window. If the Escape key is
761  * pressed we hide the popup, and do not change the cell contents. */
762 static gint
e_cell_date_edit_key_press(GtkWidget * popup_window,GdkEventKey * event,ECellDateEdit * ecde)763 e_cell_date_edit_key_press (GtkWidget *popup_window,
764                             GdkEventKey *event,
765                             ECellDateEdit *ecde)
766 {
767 	/* If the Escape key is pressed we hide the popup. */
768 	if (event->keyval != GDK_KEY_Escape)
769 		return FALSE;
770 
771 	e_cell_date_edit_hide_popup (ecde);
772 
773 	return TRUE;
774 }
775 
776 /* This handles button press events in the popup window. If the button is
777  * pressed outside the popup, we hide it and do not change the cell contents.
778 */
779 static gint
e_cell_date_edit_button_press(GtkWidget * popup_window,GdkEvent * button_event,ECellDateEdit * ecde)780 e_cell_date_edit_button_press (GtkWidget *popup_window,
781                                GdkEvent *button_event,
782                                ECellDateEdit *ecde)
783 {
784 	GtkWidget *event_widget;
785 
786 	event_widget = gtk_get_event_widget (button_event);
787 
788 	if (gtk_widget_get_toplevel (event_widget) != popup_window)
789 		e_cell_date_edit_hide_popup (ecde);
790 
791 	return TRUE;
792 }
793 
794 /* Clears the time list and rebuilds it using the lower_hour, upper_hour
795  * and use_24_hour_format settings. */
796 static void
e_cell_date_edit_rebuild_time_list(ECellDateEdit * ecde)797 e_cell_date_edit_rebuild_time_list (ECellDateEdit *ecde)
798 {
799 	GtkListStore *store;
800 	gchar buffer[40];
801 	struct tm tmp_tm;
802 	gint hour, min;
803 
804 	store = GTK_LIST_STORE (gtk_tree_view_get_model (
805 		GTK_TREE_VIEW (ecde->time_tree_view)));
806 	gtk_list_store_clear (store);
807 
808 	/* Fill the struct tm with some sane values. */
809 	tmp_tm.tm_year = 2000;
810 	tmp_tm.tm_mon = 0;
811 	tmp_tm.tm_mday = 1;
812 	tmp_tm.tm_sec = 0;
813 	tmp_tm.tm_isdst = 0;
814 
815 	for (hour = ecde->lower_hour; hour <= ecde->upper_hour; hour++) {
816 		/* We don't want to display midnight at the end, since that is
817 		 * really in the next day. */
818 		if (hour == 24)
819 			break;
820 
821 		/* We want to finish on upper_hour, with min == 0. */
822 		for (min = 0;
823 		     min == 0 || (min < 60 && hour != ecde->upper_hour);
824 		     min += 30) {
825 			GtkTreeIter iter;
826 
827 			tmp_tm.tm_hour = hour;
828 			tmp_tm.tm_min = min;
829 			e_time_format_time (&tmp_tm, ecde->use_24_hour_format,
830 					    FALSE, buffer, sizeof (buffer));
831 
832 			gtk_list_store_append (store, &iter);
833 			gtk_list_store_set (store, &iter, 0, buffer, -1);
834 		}
835 	}
836 
837 	ecde->need_time_list_rebuild = FALSE;
838 }
839 
840 static void
e_cell_date_edit_on_ok_clicked(GtkWidget * button,ECellDateEdit * ecde)841 e_cell_date_edit_on_ok_clicked (GtkWidget *button,
842                                 ECellDateEdit *ecde)
843 {
844 	ECalendarItem *calitem;
845 	GDate start_date, end_date;
846 	gboolean day_selected;
847 	struct tm date_tm;
848 	gchar buffer[64];
849 	const gchar *text;
850 	ETimeParseStatus status;
851 	gboolean is_date = FALSE;
852 
853 	calitem = E_CALENDAR_ITEM (e_calendar_get_item (E_CALENDAR (ecde->calendar)));
854 	day_selected = e_calendar_item_get_selection (
855 		calitem, &start_date, &end_date);
856 
857 	text = gtk_entry_get_text (GTK_ENTRY (ecde->time_entry));
858 	status = e_time_parse_time (text, &date_tm);
859 	if (status == E_TIME_PARSE_INVALID) {
860 		e_cell_date_edit_show_time_invalid_warning (ecde);
861 		return;
862 	} else if (status == E_TIME_PARSE_NONE || !gtk_widget_get_visible (ecde->time_entry)) {
863 		is_date = TRUE;
864 	}
865 
866 	if (day_selected) {
867 		date_tm.tm_year = g_date_get_year (&start_date) - 1900;
868 		date_tm.tm_mon = g_date_get_month (&start_date) - 1;
869 		date_tm.tm_mday = g_date_get_day (&start_date);
870 		/* We need to call this to set the weekday. */
871 		mktime (&date_tm);
872 		e_time_format_date_and_time (&date_tm,
873 					     ecde->use_24_hour_format,
874 					     !is_date, FALSE,
875 					     buffer, sizeof (buffer));
876 	} else {
877 		buffer[0] = '\0';
878 	}
879 
880 	e_cell_date_edit_update_cell (ecde, buffer);
881 	e_cell_date_edit_hide_popup (ecde);
882 }
883 
884 static void
e_cell_date_edit_show_time_invalid_warning(ECellDateEdit * ecde)885 e_cell_date_edit_show_time_invalid_warning (ECellDateEdit *ecde)
886 {
887 	GtkWidget *dialog;
888 	struct tm date_tm;
889 	gchar buffer[64];
890 
891 	/* Create a useful error message showing the correct format. */
892 	date_tm.tm_year = 100;
893 	date_tm.tm_mon = 0;
894 	date_tm.tm_mday = 1;
895 	date_tm.tm_hour = 1;
896 	date_tm.tm_min = 30;
897 	date_tm.tm_sec = 0;
898 	date_tm.tm_isdst = -1;
899 	e_time_format_time (&date_tm, ecde->use_24_hour_format, FALSE,
900 			    buffer, sizeof (buffer));
901 
902 	/* FIXME: Fix transient settings - I'm not sure it works with popup
903 	 * windows. Maybe we need to use a normal window without decorations.*/
904 	dialog = gtk_message_dialog_new (
905 		GTK_WINDOW (ecde->popup_window),
906 		GTK_DIALOG_DESTROY_WITH_PARENT,
907 		GTK_MESSAGE_ERROR, GTK_BUTTONS_OK,
908 		_("The time must be in the format: %s"),
909 		buffer);
910 	gtk_dialog_run (GTK_DIALOG (dialog));
911 	gtk_widget_destroy (dialog);
912 }
913 
914 static void
e_cell_date_edit_on_now_clicked(GtkWidget * button,ECellDateEdit * ecde)915 e_cell_date_edit_on_now_clicked (GtkWidget *button,
916                                  ECellDateEdit *ecde)
917 {
918 	struct tm tmp_tm;
919 	time_t t;
920 	gchar buffer[64];
921 
922 	if (ecde->time_callback) {
923 		tmp_tm = ecde->time_callback (
924 			ecde, ecde->time_callback_data);
925 	} else {
926 		t = time (NULL);
927 		tmp_tm = *localtime (&t);
928 	}
929 
930 	e_time_format_date_and_time (
931 		&tmp_tm, ecde->use_24_hour_format,
932 		TRUE, FALSE, buffer, sizeof (buffer));
933 
934 	e_cell_date_edit_update_cell (ecde, buffer);
935 	e_cell_date_edit_hide_popup (ecde);
936 }
937 
938 static void
e_cell_date_edit_on_none_clicked(GtkWidget * button,ECellDateEdit * ecde)939 e_cell_date_edit_on_none_clicked (GtkWidget *button,
940                                   ECellDateEdit *ecde)
941 {
942 	e_cell_date_edit_update_cell (ecde, "");
943 	e_cell_date_edit_hide_popup (ecde);
944 }
945 
946 static void
e_cell_date_edit_on_today_clicked(GtkWidget * button,ECellDateEdit * ecde)947 e_cell_date_edit_on_today_clicked (GtkWidget *button,
948                                    ECellDateEdit *ecde)
949 {
950 	struct tm tmp_tm;
951 	time_t t;
952 	gchar buffer[64];
953 
954 	if (ecde->time_callback) {
955 		tmp_tm = ecde->time_callback (
956 			ecde, ecde->time_callback_data);
957 	} else {
958 		t = time (NULL);
959 		tmp_tm = *localtime (&t);
960 	}
961 
962 	tmp_tm.tm_hour = 0;
963 	tmp_tm.tm_min = 0;
964 	tmp_tm.tm_sec = 0;
965 
966 	e_time_format_date_and_time (
967 		&tmp_tm, ecde->use_24_hour_format,
968 		FALSE, FALSE, buffer, sizeof (buffer));
969 
970 	e_cell_date_edit_update_cell (ecde, buffer);
971 	e_cell_date_edit_hide_popup (ecde);
972 }
973 
974 static void
e_cell_date_edit_update_cell(ECellDateEdit * ecde,const gchar * text)975 e_cell_date_edit_update_cell (ECellDateEdit *ecde,
976                               const gchar *text)
977 {
978 	ECellPopup *ecp = E_CELL_POPUP (ecde);
979 	ECellText *ecell_text = E_CELL_TEXT (ecp->child);
980 	ECellView *ecv = (ECellView *) ecp->popup_cell_view;
981 	ETableItem *eti = E_TABLE_ITEM (ecv->e_table_item_view);
982 	ETableCol *ecol;
983 	gchar *old_text;
984 
985 	/* Compare the new text with the existing cell contents. */
986 	ecol = e_table_header_get_column (eti->header, ecp->popup_view_col);
987 
988 	old_text = e_cell_text_get_text (
989 		ecell_text, ecv->e_table_model,
990 		ecol->spec->model_col, ecp->popup_row);
991 
992 	/* If they are different, update the cell contents. */
993 	if (strcmp (old_text, text)) {
994 		e_cell_text_set_value (
995 			ecell_text, ecv->e_table_model,
996 			ecol->spec->model_col, ecp->popup_row, text);
997 		e_cell_leave_edit (
998 			ecv, ecol->spec->model_col,
999 			ecp->popup_view_col, ecp->popup_row, NULL);
1000 	}
1001 
1002 	e_cell_text_free_text (ecell_text, ecv->e_table_model,
1003 		ecol->spec->model_col, old_text);
1004 }
1005 
1006 static void
e_cell_date_edit_on_time_selected(GtkTreeSelection * selection,ECellDateEdit * ecde)1007 e_cell_date_edit_on_time_selected (GtkTreeSelection *selection,
1008                                    ECellDateEdit *ecde)
1009 {
1010 	gchar *list_item_text = NULL;
1011 	GtkTreeIter iter;
1012 	GtkTreeModel *model;
1013 
1014 	if (!gtk_tree_selection_get_selected (selection, &model, &iter))
1015 		return;
1016 
1017 	gtk_tree_model_get (model, &iter, 0, &list_item_text, -1);
1018 
1019 	g_return_if_fail (list_item_text != NULL);
1020 
1021 	gtk_entry_set_text (GTK_ENTRY (ecde->time_entry), list_item_text);
1022 
1023 	g_free (list_item_text);
1024 }
1025 
1026 static void
e_cell_date_edit_hide_popup(ECellDateEdit * ecde)1027 e_cell_date_edit_hide_popup (ECellDateEdit *ecde)
1028 {
1029 	gtk_grab_remove (ecde->popup_window);
1030 	gtk_widget_hide (ecde->popup_window);
1031 	e_cell_popup_set_shown (E_CELL_POPUP (ecde), FALSE);
1032 }
1033 
1034 /* These freeze and thaw the rebuilding of the time list. They are useful when
1035  * setting several properties which result in rebuilds of the list, e.g. the
1036  * lower_hour, upper_hour and use_24_hour_format properties. */
1037 void
e_cell_date_edit_freeze(ECellDateEdit * ecde)1038 e_cell_date_edit_freeze (ECellDateEdit *ecde)
1039 {
1040 	g_return_if_fail (E_IS_CELL_DATE_EDIT (ecde));
1041 
1042 	ecde->freeze_count++;
1043 }
1044 
1045 void
e_cell_date_edit_thaw(ECellDateEdit * ecde)1046 e_cell_date_edit_thaw (ECellDateEdit *ecde)
1047 {
1048 	g_return_if_fail (E_IS_CELL_DATE_EDIT (ecde));
1049 
1050 	if (ecde->freeze_count > 0) {
1051 		ecde->freeze_count--;
1052 
1053 		if (ecde->freeze_count == 0)
1054 			e_cell_date_edit_rebuild_time_list (ecde);
1055 	}
1056 }
1057 
1058 /* Sets a callback to use to get the current time. This is useful if the
1059  * application needs to use its own timezone data rather than rely on the
1060  * Unix timezone. */
1061 void
e_cell_date_edit_set_get_time_callback(ECellDateEdit * ecde,ECellDateEditGetTimeCallback cb,gpointer data,GDestroyNotify destroy)1062 e_cell_date_edit_set_get_time_callback (ECellDateEdit *ecde,
1063                                         ECellDateEditGetTimeCallback cb,
1064                                         gpointer data,
1065                                         GDestroyNotify destroy)
1066 {
1067 	g_return_if_fail (E_IS_CELL_DATE_EDIT (ecde));
1068 
1069 	if (ecde->time_callback_data && ecde->time_callback_destroy)
1070 		(*ecde->time_callback_destroy) (ecde->time_callback_data);
1071 
1072 	ecde->time_callback = cb;
1073 	ecde->time_callback_data = data;
1074 	ecde->time_callback_destroy = destroy;
1075 }
1076