1 /*
2  * EDayViewTimeItem - canvas item which displays the times down the left of
3  * the EDayView.
4  *
5  * This program is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU Lesser General Public License as published by
7  * the Free Software Foundation.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this program; if not, see <http://www.gnu.org/licenses/>.
16  *
17  * Authors:
18  *		Damon Chaplin <damon@ximian.com>
19  *
20  * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
21  */
22 
23 #include "evolution-config.h"
24 
25 #include <string.h>
26 #include <gtk/gtk.h>
27 #include <glib/gi18n.h>
28 
29 #include "e-day-view-time-item.h"
30 #include "calendar-config.h"
31 
32 /* The spacing between items in the time column. GRID_X_PAD is the space down
33  * either side of the column, i.e. outside the main horizontal grid lines.
34  * HOUR_L_PAD & HOUR_R_PAD are the spaces on the left & right side of the
35  * big hour number (this is inside the horizontal grid lines).
36  * MIN_X_PAD is the spacing either side of the minute number. The smaller
37  * horizontal grid lines match with this.
38  * 60_MIN_X_PAD is the space either side of the HH:MM display used when
39  * we are displaying 60 mins per row (inside the main grid lines).
40  * LARGE_HOUR_Y_PAD is the offset of the large hour string from the top of the
41  * row.
42  * SMALL_FONT_Y_PAD is the offset of the small time/minute string from the top
43  * of the row. */
44 #define E_DVTMI_TIME_GRID_X_PAD		4
45 #define E_DVTMI_HOUR_L_PAD		4
46 #define E_DVTMI_HOUR_R_PAD		2
47 #define E_DVTMI_MIN_X_PAD		2
48 #define E_DVTMI_60_MIN_X_PAD		4
49 #define E_DVTMI_LARGE_HOUR_Y_PAD	1
50 #define E_DVTMI_SMALL_FONT_Y_PAD	1
51 
52 #define E_DAY_VIEW_TIME_ITEM_GET_PRIVATE(obj) \
53 	(G_TYPE_INSTANCE_GET_PRIVATE \
54 	((obj), E_TYPE_DAY_VIEW_TIME_ITEM, EDayViewTimeItemPrivate))
55 
56 struct _EDayViewTimeItemPrivate {
57 	/* The parent EDayView widget. */
58 	EDayView *day_view;
59 
60 	/* The width of the time column. */
61 	gint column_width;
62 
63 	/* TRUE if we are currently dragging the selection times. */
64 	gboolean dragging_selection;
65 
66 	/* The second timezone if shown, or else NULL. */
67 	ICalTimezone *second_zone;
68 };
69 
70 static void	e_day_view_time_item_update	(GnomeCanvasItem *item,
71 						 const cairo_matrix_t *i2c,
72 						 gint flags);
73 static void	e_day_view_time_item_draw	(GnomeCanvasItem *item,
74 						 cairo_t *cr,
75 						 gint x,
76 						 gint y,
77 						 gint width,
78 						 gint height);
79 static GnomeCanvasItem *
80 		e_day_view_time_item_point	(GnomeCanvasItem *item,
81 						 gdouble x,
82 						 gdouble y,
83 						 gint cx,
84 						 gint cy);
85 static gint	e_day_view_time_item_event	(GnomeCanvasItem *item,
86 						 GdkEvent *event);
87 static void	e_day_view_time_item_increment_time
88 						(gint *hour,
89 						 gint *minute,
90 						 gint time_divisions);
91 static void	e_day_view_time_item_show_popup_menu
92 						(EDayViewTimeItem *time_item,
93 						 GdkEvent *event);
94 static void	e_day_view_time_item_on_set_divisions
95 						(GtkWidget *item,
96 						 EDayViewTimeItem *time_item);
97 static void	e_day_view_time_item_on_button_press
98 						(EDayViewTimeItem *time_item,
99 						 GdkEvent *event);
100 static void	e_day_view_time_item_on_button_release
101 						(EDayViewTimeItem *time_item,
102 						 GdkEvent *event);
103 static void	e_day_view_time_item_on_motion_notify
104 						(EDayViewTimeItem *time_item,
105 						 GdkEvent *event);
106 static gint	e_day_view_time_item_convert_position_to_row
107 						(EDayViewTimeItem *time_item,
108 						 gint y);
109 
110 static void	edvti_second_zone_changed_cb	(GSettings *settings,
111 						 const gchar *key,
112 						 gpointer user_data);
113 
114 enum {
115 	PROP_0,
116 	PROP_DAY_VIEW
117 };
118 
G_DEFINE_TYPE(EDayViewTimeItem,e_day_view_time_item,GNOME_TYPE_CANVAS_ITEM)119 G_DEFINE_TYPE (
120 	EDayViewTimeItem,
121 	e_day_view_time_item,
122 	GNOME_TYPE_CANVAS_ITEM)
123 
124 static void
125 day_view_time_item_set_property (GObject *object,
126                                  guint property_id,
127                                  const GValue *value,
128                                  GParamSpec *pspec)
129 {
130 	switch (property_id) {
131 		case PROP_DAY_VIEW:
132 			e_day_view_time_item_set_day_view (
133 				E_DAY_VIEW_TIME_ITEM (object),
134 				g_value_get_object (value));
135 			return;
136 	}
137 
138 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
139 }
140 
141 static void
day_view_time_item_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)142 day_view_time_item_get_property (GObject *object,
143                                  guint property_id,
144                                  GValue *value,
145                                  GParamSpec *pspec)
146 {
147 	switch (property_id) {
148 		case PROP_DAY_VIEW:
149 			g_value_set_object (
150 				value, e_day_view_time_item_get_day_view (
151 				E_DAY_VIEW_TIME_ITEM (object)));
152 			return;
153 	}
154 
155 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
156 }
157 
158 static void
day_view_time_item_dispose(GObject * object)159 day_view_time_item_dispose (GObject *object)
160 {
161 	EDayViewTimeItemPrivate *priv;
162 
163 	priv = E_DAY_VIEW_TIME_ITEM_GET_PRIVATE (object);
164 	g_clear_object (&priv->day_view);
165 
166 	/* Chain up to parent's dispose() method. */
167 	G_OBJECT_CLASS (e_day_view_time_item_parent_class)->dispose (object);
168 }
169 
170 static void
day_view_time_item_finalize(GObject * object)171 day_view_time_item_finalize (GObject *object)
172 {
173 	EDayViewTimeItem *time_item;
174 
175 	time_item = E_DAY_VIEW_TIME_ITEM (object);
176 
177 	calendar_config_remove_notification (
178 		(CalendarConfigChangedFunc)
179 		edvti_second_zone_changed_cb, time_item);
180 
181 	/* Chain up to parent's dispose() method. */
182 	G_OBJECT_CLASS (e_day_view_time_item_parent_class)->finalize (object);
183 }
184 
185 static void
e_day_view_time_item_class_init(EDayViewTimeItemClass * class)186 e_day_view_time_item_class_init (EDayViewTimeItemClass *class)
187 {
188 	GObjectClass *object_class;
189 	GnomeCanvasItemClass *item_class;
190 
191 	g_type_class_add_private (class, sizeof (EDayViewTimeItemPrivate));
192 
193 	object_class = G_OBJECT_CLASS (class);
194 	object_class->set_property = day_view_time_item_set_property;
195 	object_class->get_property = day_view_time_item_get_property;
196 	object_class->dispose = day_view_time_item_dispose;
197 	object_class->finalize = day_view_time_item_finalize;
198 
199 	item_class = GNOME_CANVAS_ITEM_CLASS (class);
200 	item_class->update = e_day_view_time_item_update;
201 	item_class->draw = e_day_view_time_item_draw;
202 	item_class->point = e_day_view_time_item_point;
203 	item_class->event = e_day_view_time_item_event;
204 
205 	g_object_class_install_property (
206 		object_class,
207 		PROP_DAY_VIEW,
208 		g_param_spec_object (
209 			"day-view",
210 			"Day View",
211 			NULL,
212 			E_TYPE_DAY_VIEW,
213 			G_PARAM_READWRITE));
214 }
215 
216 static void
e_day_view_time_item_init(EDayViewTimeItem * time_item)217 e_day_view_time_item_init (EDayViewTimeItem *time_item)
218 {
219 	gchar *last;
220 
221 	time_item->priv = E_DAY_VIEW_TIME_ITEM_GET_PRIVATE (time_item);
222 
223 	last = calendar_config_get_day_second_zone ();
224 
225 	if (last) {
226 		if (*last)
227 			time_item->priv->second_zone =
228 				i_cal_timezone_get_builtin_timezone (last);
229 		g_free (last);
230 	}
231 
232 	calendar_config_add_notification_day_second_zone (
233 		(CalendarConfigChangedFunc) edvti_second_zone_changed_cb,
234 		time_item);
235 }
236 
237 static void
e_day_view_time_item_update(GnomeCanvasItem * item,const cairo_matrix_t * i2c,gint flags)238 e_day_view_time_item_update (GnomeCanvasItem *item,
239                              const cairo_matrix_t *i2c,
240                              gint flags)
241 {
242 	if (GNOME_CANVAS_ITEM_CLASS (e_day_view_time_item_parent_class)->update)
243 		(* GNOME_CANVAS_ITEM_CLASS (e_day_view_time_item_parent_class)->update) (item, i2c, flags);
244 
245 	/* The item covers the entire canvas area. */
246 	item->x1 = 0;
247 	item->y1 = 0;
248 	item->x2 = INT_MAX;
249 	item->y2 = INT_MAX;
250 }
251 
252 /*
253  * DRAWING ROUTINES - functions to paint the canvas item.
254  */
255 static void
edvti_draw_zone(GnomeCanvasItem * canvas_item,cairo_t * cr,gint x,gint y,gint width,gint height,gint x_offset,ICalTimezone * use_zone)256 edvti_draw_zone (GnomeCanvasItem *canvas_item,
257 		 cairo_t *cr,
258 		 gint x,
259 		 gint y,
260 		 gint width,
261 		 gint height,
262 		 gint x_offset,
263 		 ICalTimezone *use_zone)
264 {
265 	EDayView *day_view;
266 	EDayViewTimeItem *time_item;
267 	ECalendarView *cal_view;
268 	ECalModel *model;
269 	const gchar *suffix;
270 	gchar buffer[64], *midnight_day = NULL, *midnight_month = NULL;
271 	gint time_divisions;
272 	gint hour, display_hour, minute, row;
273 	gint row_y, start_y, large_hour_y_offset, small_font_y_offset;
274 	gint long_line_x1, long_line_x2, short_line_x1;
275 	gint large_hour_x2, minute_x2;
276 	gint hour_width, minute_width, suffix_width;
277 	gint max_suffix_width, max_minute_or_suffix_width;
278 	PangoLayout *layout;
279 	PangoAttrList *tnum;
280 	PangoAttribute *attr;
281 	PangoContext *context;
282 	PangoFontMetrics *large_font_metrics, *small_font_metrics;
283 	GtkWidget *widget;
284 	GdkRGBA fg, dark;
285 	GdkColor mb_color;
286 
287 	time_item = E_DAY_VIEW_TIME_ITEM (canvas_item);
288 	day_view = e_day_view_time_item_get_day_view (time_item);
289 	g_return_if_fail (day_view != NULL);
290 
291 	widget = GTK_WIDGET (day_view);
292 	cal_view = E_CALENDAR_VIEW (day_view);
293 	model = e_calendar_view_get_model (cal_view);
294 	time_divisions = e_calendar_view_get_time_divisions (cal_view);
295 
296 	context = gtk_widget_get_pango_context (GTK_WIDGET (day_view));
297 	small_font_metrics = pango_context_get_metrics (
298 		context, NULL,
299 		pango_context_get_language (context));
300 	large_font_metrics = pango_context_get_metrics (
301 		context, day_view->large_font_desc,
302 		pango_context_get_language (context));
303 
304 	e_utils_get_theme_color (widget, "theme_fg_color,theme_text_color", E_UTILS_DEFAULT_THEME_FG_COLOR, &fg);
305 	e_utils_get_theme_color (widget, "theme_base_color", E_UTILS_DEFAULT_THEME_BASE_COLOR, &dark);
306 
307 	tnum = pango_attr_list_new ();
308 	attr = pango_attr_font_features_new ("tnum=1");
309 	pango_attr_list_insert_before (tnum, attr);
310 
311 	/* The start and end of the long horizontal line between hours. */
312 	long_line_x1 =
313 		(use_zone ? 0 : E_DVTMI_TIME_GRID_X_PAD) - x + x_offset;
314 	long_line_x2 =
315 		time_item->priv->column_width -
316 		E_DVTMI_TIME_GRID_X_PAD - x -
317 		(use_zone ? E_DVTMI_TIME_GRID_X_PAD : 0) + x_offset;
318 
319 	if (time_divisions == 60) {
320 		/* The right edge of the complete time string in 60-min
321 		 * divisions, e.g. "14:00" or "2 pm". */
322 		minute_x2 = long_line_x2 - E_DVTMI_60_MIN_X_PAD;
323 
324 		/* These aren't used for 60-minute divisions, but we initialize
325 		 * them to keep gcc happy. */
326 		short_line_x1 = 0;
327 		large_hour_x2 = 0;
328 	} else {
329 		max_suffix_width = MAX (
330 			day_view->am_string_width,
331 			day_view->pm_string_width);
332 
333 		max_minute_or_suffix_width = MAX (
334 			max_suffix_width,
335 			day_view->max_minute_width);
336 
337 		/* The start of the short horizontal line between the periods
338 		 * within each hour. */
339 		short_line_x1 = long_line_x2 - E_DVTMI_MIN_X_PAD * 2
340 			- max_minute_or_suffix_width;
341 
342 		/* The right edge of the large hour string. */
343 		large_hour_x2 = short_line_x1 - E_DVTMI_HOUR_R_PAD;
344 
345 		/* The right edge of the minute part of the time. */
346 		minute_x2 = long_line_x2 - E_DVTMI_MIN_X_PAD;
347 	}
348 
349 	/* Start with the first hour & minute shown in the EDayView. */
350 	hour = day_view->first_hour_shown;
351 	minute = day_view->first_minute_shown;
352 
353 	if (use_zone) {
354 		/* shift time with a difference between
355 		 * local time and the other timezone */
356 		ICalTimezone *cal_zone;
357 		ICalTime *tt;
358 		gint is_daylight = 0; /* Its value is ignored, but libical-glib 3.0.5 API requires it */
359 		gint diff;
360 		struct tm mn;
361 
362 		cal_zone = e_calendar_view_get_timezone (
363 			E_CALENDAR_VIEW (day_view));
364 		tt = i_cal_time_new_from_timet_with_zone (
365 			day_view->day_starts[0], 0, cal_zone);
366 
367 		/* diff is number of minutes */
368 		diff = (i_cal_timezone_get_utc_offset (use_zone, tt, &is_daylight) -
369 			i_cal_timezone_get_utc_offset (cal_zone, tt, &is_daylight)) / 60;
370 
371 		g_clear_object (&tt);
372 
373 		tt = i_cal_time_new_from_timet_with_zone (day_view->day_starts[0], 0, cal_zone);
374 		i_cal_time_set_is_date (tt, FALSE);
375 		i_cal_time_set_timezone (tt, cal_zone);
376 		i_cal_time_convert_to_zone_inplace (tt, use_zone);
377 
378 		if (diff != 0) {
379 			/* shows the next midnight */
380 			i_cal_time_adjust (tt, 1, 0, 0, 0);
381 		}
382 
383 		mn = e_cal_util_icaltime_to_tm (tt);
384 
385 		g_clear_object (&tt);
386 
387 		/* up to two characters/numbers */
388 		e_utf8_strftime (buffer, sizeof (buffer), "%d", &mn);
389 		midnight_day = g_strdup (buffer);
390 		/* up to three characters, abbreviated month name */
391 		e_utf8_strftime (buffer, sizeof (buffer), "%b", &mn);
392 		midnight_month = g_strdup (buffer);
393 
394 		minute += (diff % 60);
395 		hour += (diff / 60) + (minute / 60);
396 
397 		minute = minute % 60;
398 		if (minute < 0) {
399 			hour--;
400 			minute += 60;
401 		}
402 
403 		hour = (hour + 48) % 24;
404 	}
405 
406 	/* The offset of the large hour string from the top of the row. */
407 	large_hour_y_offset = E_DVTMI_LARGE_HOUR_Y_PAD;
408 
409 	/* The offset of the small time/minute string from top of row. */
410 	small_font_y_offset = E_DVTMI_SMALL_FONT_Y_PAD;
411 
412 	/* Calculate the minimum y position of the first row we need to draw.
413 	 * This is normally one row height above the 0 position, but if we
414 	 * are using the large font we may have to go back a bit further. */
415 	start_y = 0 - MAX (day_view->row_height,
416 			   (pango_font_metrics_get_ascent (large_font_metrics) +
417 			    pango_font_metrics_get_descent (large_font_metrics)) / PANGO_SCALE +
418 			   E_DVTMI_LARGE_HOUR_Y_PAD);
419 
420 	/* Draw the Marcus Bains Line first, so it appears under other elements. */
421 	if (e_day_view_marcus_bains_get_show_line (day_view)) {
422 		ICalTime *time_now;
423 		const gchar *marcus_bains_time_bar_color;
424 		gint marcus_bains_y;
425 
426 		cairo_save (cr);
427 		gdk_cairo_set_source_color (
428 			cr, &day_view->colors[E_DAY_VIEW_COLOR_MARCUS_BAINS_LINE]);
429 
430 		marcus_bains_time_bar_color =
431 			e_day_view_marcus_bains_get_time_bar_color (day_view);
432 		if (marcus_bains_time_bar_color == NULL)
433 			marcus_bains_time_bar_color = "";
434 
435 		if (gdk_color_parse (marcus_bains_time_bar_color, &mb_color)) {
436 			gdk_cairo_set_source_color (cr, &mb_color);
437 		} else {
438 			mb_color = day_view->colors[E_DAY_VIEW_COLOR_MARCUS_BAINS_LINE];
439 		}
440 
441 		time_now = i_cal_time_new_current_with_zone (
442 			e_calendar_view_get_timezone (
443 			E_CALENDAR_VIEW (day_view)));
444 		marcus_bains_y =
445 			(i_cal_time_get_hour (time_now) * 60 + i_cal_time_get_minute (time_now)) *
446 			day_view->row_height / time_divisions - y;
447 		cairo_set_line_width (cr, 1.5);
448 		cairo_move_to (
449 			cr, long_line_x1 -
450 			(use_zone ? E_DVTMI_TIME_GRID_X_PAD : 0),
451 			marcus_bains_y);
452 		cairo_line_to (cr, long_line_x2, marcus_bains_y);
453 		cairo_stroke (cr);
454 		cairo_restore (cr);
455 
456 		g_clear_object (&time_now);
457 	} else {
458 		const gchar *marcus_bains_time_bar_color;
459 
460 		marcus_bains_time_bar_color =
461 			e_day_view_marcus_bains_get_time_bar_color (day_view);
462 		if (marcus_bains_time_bar_color == NULL)
463 			marcus_bains_time_bar_color = "";
464 
465 		mb_color = day_view->colors[E_DAY_VIEW_COLOR_MARCUS_BAINS_LINE];
466 
467 		if (gdk_color_parse (marcus_bains_time_bar_color, &mb_color))
468 			gdk_cairo_set_source_color (cr, &mb_color);
469 	}
470 
471 	/* Step through each row, drawing the times and the horizontal lines
472 	 * between them. */
473 	for (row = 0, row_y = 0 - y;
474 	     row < day_view->rows && row_y < height;
475 	     row++, row_y += day_view->row_height) {
476 		gboolean show_midnight_date;
477 
478 		show_midnight_date =
479 			use_zone && hour == 0 &&
480 			(minute == 0 || time_divisions == 60) &&
481 			midnight_day && midnight_month;
482 
483 		/* If the row is above the first row we want to draw just
484 		 * increment the time and skip to the next row. */
485 		if (row_y < start_y) {
486 			e_day_view_time_item_increment_time (
487 				&hour, &minute, time_divisions);
488 			continue;
489 		}
490 
491 		/* Calculate the actual hour number to display. For 12-hour
492 		 * format we convert 0-23 to 12-11am / 12 - 11pm. */
493 		e_day_view_convert_time_to_display (
494 			day_view, hour,
495 			&display_hour,
496 			&suffix, &suffix_width);
497 
498 		if (time_divisions == 60) {
499 			/* 60 minute intervals - draw a long horizontal line
500 			 * between hours and display as one long string,
501 			 * e.g. "14:00" or "2 pm". */
502 			cairo_save (cr);
503 			gdk_cairo_set_source_rgba (cr, &dark);
504 			cairo_save (cr);
505 			cairo_set_line_width (cr, 0.7);
506 			cairo_move_to (cr, long_line_x1, row_y);
507 			cairo_line_to (cr, long_line_x2, row_y);
508 			cairo_stroke (cr);
509 			cairo_restore (cr);
510 
511 			if (show_midnight_date) {
512 				g_snprintf (buffer, sizeof (buffer), "%s %s", midnight_day, midnight_month);
513 			} else if (e_cal_model_get_use_24_hour_format (model)) {
514 				g_snprintf (
515 					buffer, sizeof (buffer), "%i:%02i",
516 					display_hour, minute);
517 			} else {
518 				g_snprintf (
519 					buffer, sizeof (buffer), "%i %s",
520 					display_hour, suffix);
521 			}
522 
523 			cairo_save (cr);
524 			if (show_midnight_date)
525 				gdk_cairo_set_source_color (cr, &mb_color);
526 			else
527 				gdk_cairo_set_source_rgba (cr, &fg);
528 			layout = gtk_widget_create_pango_layout (GTK_WIDGET (day_view), NULL);
529 			pango_layout_set_attributes (layout, tnum);
530 			pango_layout_set_text (layout, buffer, -1);
531 			pango_layout_get_pixel_size (layout, &minute_width, NULL);
532 			cairo_translate (
533 				cr, minute_x2 - minute_width,
534 				row_y + small_font_y_offset);
535 			pango_cairo_update_layout (cr, layout);
536 			pango_cairo_show_layout (cr, layout);
537 			cairo_restore (cr);
538 
539 			g_object_unref (layout);
540 
541 			cairo_restore (cr);
542 		} else {
543 			/* 5/10/15/30 minute intervals. */
544 
545 			if (minute == 0) {
546 				/* On the hour - draw a long horizontal line
547 				 * before the hour and display the hour in the
548 				 * large font. */
549 
550 				cairo_save (cr);
551 				gdk_cairo_set_source_rgba (cr, &dark);
552 				if (show_midnight_date)
553 					g_snprintf (buffer, sizeof (buffer), "%s", midnight_day);
554 				else
555 					g_snprintf (
556 						buffer, sizeof (buffer), "%i",
557 						display_hour);
558 
559 				cairo_set_line_width (cr, 0.7);
560 				cairo_move_to (cr, long_line_x1, row_y);
561 				cairo_line_to (cr, long_line_x2, row_y);
562 				cairo_stroke (cr);
563 				cairo_restore (cr);
564 
565 				cairo_save (cr);
566 				if (show_midnight_date)
567 					gdk_cairo_set_source_color (cr, &mb_color);
568 				else
569 					gdk_cairo_set_source_rgba (cr, &fg);
570 				layout = gtk_widget_create_pango_layout (GTK_WIDGET (day_view), NULL);
571 				pango_layout_set_attributes (layout, tnum);
572 				pango_layout_set_text (layout, buffer, -1);
573 				pango_layout_set_font_description (
574 					layout, day_view->large_font_desc);
575 				pango_layout_get_pixel_size (
576 					layout, &hour_width, NULL);
577 				cairo_translate (
578 					cr, large_hour_x2 - hour_width,
579 					row_y + large_hour_y_offset);
580 				pango_cairo_update_layout (cr, layout);
581 				pango_cairo_show_layout (cr, layout);
582 				cairo_restore (cr);
583 
584 				g_object_unref (layout);
585 			} else {
586 				/* Within the hour - draw a short line before
587 				 * the time. */
588 				cairo_save (cr);
589 				gdk_cairo_set_source_rgba (cr, &dark);
590 				cairo_set_line_width (cr, 0.7);
591 				cairo_move_to (cr, short_line_x1, row_y);
592 				cairo_line_to (cr, long_line_x2, row_y);
593 				cairo_stroke (cr);
594 				cairo_restore (cr);
595 			}
596 
597 			/* Normally we display the minute in each
598 			 * interval, but when using 30-minute intervals
599 			 * we don't display the '30'. */
600 			if (time_divisions != 30 || minute != 30) {
601 				/* In 12-hour format we display 'am' or 'pm'
602 				 * instead of '00'. */
603 				if (show_midnight_date)
604 					g_snprintf (buffer, sizeof (buffer), "%s", midnight_month);
605 				else if (minute == 0
606 				    && !e_cal_model_get_use_24_hour_format (model)) {
607 					g_snprintf (buffer, sizeof (buffer), "%s", suffix);
608 				} else {
609 					g_snprintf (
610 						buffer, sizeof (buffer),
611 						"%02i", minute);
612 				}
613 
614 				cairo_save (cr);
615 				if (show_midnight_date)
616 					gdk_cairo_set_source_color (cr, &mb_color);
617 				else
618 					gdk_cairo_set_source_rgba (cr, &fg);
619 				layout = gtk_widget_create_pango_layout (GTK_WIDGET (day_view), NULL);
620 				pango_layout_set_attributes (layout, tnum);
621 				pango_layout_set_text (layout, buffer, -1);
622 				pango_layout_set_font_description (
623 					layout, day_view->small_font_desc);
624 				pango_layout_get_pixel_size (
625 					layout, &minute_width, NULL);
626 				cairo_translate (
627 					cr, minute_x2 - minute_width,
628 					row_y + small_font_y_offset);
629 				pango_cairo_update_layout (cr, layout);
630 				pango_cairo_show_layout (cr, layout);
631 				cairo_restore (cr);
632 
633 				g_object_unref (layout);
634 			}
635 		}
636 
637 		e_day_view_time_item_increment_time (
638 			&hour, &minute,
639 			time_divisions);
640 	}
641 
642 	pango_attr_list_unref (tnum);
643 	pango_font_metrics_unref (large_font_metrics);
644 	pango_font_metrics_unref (small_font_metrics);
645 
646 	g_free (midnight_day);
647 	g_free (midnight_month);
648 }
649 
650 static void
e_day_view_time_item_draw(GnomeCanvasItem * canvas_item,cairo_t * cr,gint x,gint y,gint width,gint height)651 e_day_view_time_item_draw (GnomeCanvasItem *canvas_item,
652                            cairo_t *cr,
653                            gint x,
654                            gint y,
655                            gint width,
656                            gint height)
657 {
658 	EDayViewTimeItem *time_item;
659 
660 	time_item = E_DAY_VIEW_TIME_ITEM (canvas_item);
661 	g_return_if_fail (time_item != NULL);
662 
663 	edvti_draw_zone (canvas_item, cr, x, y, width, height, 0, NULL);
664 
665 	if (time_item->priv->second_zone)
666 		edvti_draw_zone (
667 			canvas_item, cr, x, y, width, height,
668 			time_item->priv->column_width,
669 			time_item->priv->second_zone);
670 }
671 
672 /* Increment the time by the 5/10/15/30/60 minute interval.
673  * Note that time_divisions is never > 60, so we never have to
674  * worry about adding more than 60 minutes. */
675 static void
e_day_view_time_item_increment_time(gint * hour,gint * minute,gint time_divisions)676 e_day_view_time_item_increment_time (gint *hour,
677                                      gint *minute,
678                                      gint time_divisions)
679 {
680 	*minute += time_divisions;
681 	if (*minute >= 60) {
682 		*minute -= 60;
683 		/* Currently we never wrap around to the next day, but
684 		 * we may do if we display extra timezones. */
685 		*hour = (*hour + 1) % 24;
686 	}
687 }
688 
689 static GnomeCanvasItem *
e_day_view_time_item_point(GnomeCanvasItem * item,gdouble x,gdouble y,gint cx,gint cy)690 e_day_view_time_item_point (GnomeCanvasItem *item,
691                             gdouble x,
692                             gdouble y,
693                             gint cx,
694                             gint cy)
695 {
696 	return item;
697 }
698 
699 static gint
e_day_view_time_item_event(GnomeCanvasItem * item,GdkEvent * event)700 e_day_view_time_item_event (GnomeCanvasItem *item,
701                             GdkEvent *event)
702 {
703 	EDayViewTimeItem *time_item;
704 
705 	time_item = E_DAY_VIEW_TIME_ITEM (item);
706 
707 	switch (event->type) {
708 	case GDK_BUTTON_PRESS:
709 		if (event->button.button == 1) {
710 			e_day_view_time_item_on_button_press (time_item, event);
711 		} else if (event->button.button == 3) {
712 			e_day_view_time_item_show_popup_menu (time_item, event);
713 			return TRUE;
714 		}
715 		break;
716 	case GDK_BUTTON_RELEASE:
717 		if (event->button.button == 1)
718 			e_day_view_time_item_on_button_release (
719 				time_item, event);
720 		break;
721 
722 	case GDK_MOTION_NOTIFY:
723 		e_day_view_time_item_on_motion_notify (time_item, event);
724 		break;
725 
726 	default:
727 		break;
728 	}
729 
730 	return FALSE;
731 }
732 
733 static void
edvti_second_zone_changed_cb(GSettings * settings,const gchar * key,gpointer user_data)734 edvti_second_zone_changed_cb (GSettings *settings,
735                               const gchar *key,
736                               gpointer user_data)
737 {
738 	EDayViewTimeItem *time_item = user_data;
739 	EDayView *day_view;
740 	ICalTimezone *second_zone;
741 	gchar *location;
742 
743 	g_return_if_fail (user_data != NULL);
744 	g_return_if_fail (E_IS_DAY_VIEW_TIME_ITEM (time_item));
745 
746 	location = calendar_config_get_day_second_zone ();
747 	second_zone = location ? i_cal_timezone_get_builtin_timezone (location) : NULL;
748 	g_free (location);
749 
750 	if (second_zone == time_item->priv->second_zone)
751 		return;
752 
753 	time_item->priv->second_zone = second_zone;
754 
755 	day_view = e_day_view_time_item_get_day_view (time_item);
756 	gtk_widget_set_size_request (
757 		day_view->time_canvas,
758 		e_day_view_time_item_get_column_width (time_item), -1);
759 	gtk_widget_queue_draw (day_view->time_canvas);
760 
761 	e_day_view_update_timezone_name_labels (day_view);
762 }
763 
764 static void
edvti_on_select_zone(GtkWidget * item,EDayViewTimeItem * time_item)765 edvti_on_select_zone (GtkWidget *item,
766                       EDayViewTimeItem *time_item)
767 {
768 	calendar_config_select_day_second_zone (gtk_widget_get_toplevel (item));
769 }
770 
771 static void
edvti_on_set_zone(GtkWidget * item,EDayViewTimeItem * time_item)772 edvti_on_set_zone (GtkWidget *item,
773                    EDayViewTimeItem *time_item)
774 {
775 	if (!gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item)))
776 		return;
777 
778 	calendar_config_set_day_second_zone (
779 		g_object_get_data (G_OBJECT (item), "timezone"));
780 }
781 
782 static void
e_day_view_time_item_show_popup_menu(EDayViewTimeItem * time_item,GdkEvent * event)783 e_day_view_time_item_show_popup_menu (EDayViewTimeItem *time_item,
784                                       GdkEvent *event)
785 {
786 	static gint divisions[] = { 60, 30, 15, 10, 5 };
787 	EDayView *day_view;
788 	ECalendarView *cal_view;
789 	GtkWidget *menu, *item, *submenu;
790 	gchar buffer[256];
791 	GSList *group = NULL, *recent_zones, *s;
792 	gint current_divisions, i;
793 	ICalTimezone *zone;
794 
795 	day_view = e_day_view_time_item_get_day_view (time_item);
796 	g_return_if_fail (day_view != NULL);
797 
798 	cal_view = E_CALENDAR_VIEW (day_view);
799 	current_divisions = e_calendar_view_get_time_divisions (cal_view);
800 
801 	menu = gtk_menu_new ();
802 
803 	/* Make sure the menu is destroyed when it disappears. */
804 	g_signal_connect (
805 		menu, "selection-done",
806 		G_CALLBACK (gtk_widget_destroy), NULL);
807 
808 	for (i = 0; i < G_N_ELEMENTS (divisions); i++) {
809 		g_snprintf (
810 			buffer, sizeof (buffer),
811 			/* Translators: %02i is the number of minutes;
812 			 * this is a context menu entry to change the
813 			 * length of the time division in the calendar
814 			 * day view, e.g. a day is displayed in
815 			 * 24 "60 minute divisions" or
816 			 * 48 "30 minute divisions". */
817 			_("%02i minute divisions"), divisions[i]);
818 		item = gtk_radio_menu_item_new_with_label (group, buffer);
819 		group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (item));
820 		gtk_widget_show (item);
821 		gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
822 
823 		if (current_divisions == divisions[i])
824 			gtk_check_menu_item_set_active (
825 				GTK_CHECK_MENU_ITEM (item), TRUE);
826 
827 		g_object_set_data (
828 			G_OBJECT (item), "divisions",
829 			GINT_TO_POINTER (divisions[i]));
830 
831 		g_signal_connect (
832 			item, "toggled",
833 			G_CALLBACK (e_day_view_time_item_on_set_divisions),
834 			time_item);
835 	}
836 
837 	item = gtk_separator_menu_item_new ();
838 	gtk_widget_show (item);
839 	gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
840 
841 	submenu = gtk_menu_new ();
842 	item = gtk_menu_item_new_with_label (_("Show the second time zone"));
843 	gtk_widget_show (item);
844 	gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
845 	gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu);
846 
847 	zone = e_calendar_view_get_timezone (E_CALENDAR_VIEW (day_view));
848 	if (zone)
849 		item = gtk_menu_item_new_with_label (i_cal_timezone_get_display_name (zone));
850 	else
851 		item = gtk_menu_item_new_with_label ("---");
852 	gtk_widget_set_sensitive (item, FALSE);
853 	gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item);
854 
855 	item = gtk_separator_menu_item_new ();
856 	gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item);
857 
858 	group = NULL;
859 	item = gtk_radio_menu_item_new_with_label (group, C_("cal-second-zone", "None"));
860 	group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (item));
861 	if (!time_item->priv->second_zone)
862 		gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), TRUE);
863 	gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item);
864 	g_signal_connect (
865 		item, "toggled",
866 		G_CALLBACK (edvti_on_set_zone), time_item);
867 
868 	recent_zones = calendar_config_get_day_second_zones ();
869 	for (s = recent_zones; s != NULL; s = s->next) {
870 		zone = i_cal_timezone_get_builtin_timezone (s->data);
871 		if (!zone)
872 			continue;
873 
874 		item = gtk_radio_menu_item_new_with_label (
875 			group, i_cal_timezone_get_display_name (zone));
876 		group = gtk_radio_menu_item_get_group (GTK_RADIO_MENU_ITEM (item));
877 		/* both comes from builtin, thus no problem to compare pointers */
878 		if (zone == time_item->priv->second_zone)
879 			gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), TRUE);
880 		gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item);
881 		g_object_set_data_full (
882 			G_OBJECT (item), "timezone",
883 			g_strdup (s->data), g_free);
884 		g_signal_connect (
885 			item, "toggled",
886 			G_CALLBACK (edvti_on_set_zone), time_item);
887 	}
888 	calendar_config_free_day_second_zones (recent_zones);
889 
890 	item = gtk_separator_menu_item_new ();
891 	gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item);
892 
893 	item = gtk_menu_item_new_with_label (_("Select…"));
894 	g_signal_connect (
895 		item, "activate",
896 		G_CALLBACK (edvti_on_select_zone), time_item);
897 	gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item);
898 
899 	gtk_widget_show_all (submenu);
900 
901 	gtk_menu_attach_to_widget (GTK_MENU (menu),
902 				   GTK_WIDGET (day_view),
903 				   NULL);
904 	gtk_menu_popup_at_pointer (GTK_MENU (menu), event);
905 }
906 
907 static void
e_day_view_time_item_on_set_divisions(GtkWidget * item,EDayViewTimeItem * time_item)908 e_day_view_time_item_on_set_divisions (GtkWidget *item,
909                                        EDayViewTimeItem *time_item)
910 {
911 	EDayView *day_view;
912 	ECalendarView *cal_view;
913 	gint divisions;
914 
915 	day_view = e_day_view_time_item_get_day_view (time_item);
916 	g_return_if_fail (day_view != NULL);
917 
918 	if (!gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item)))
919 		return;
920 
921 	divisions = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "divisions"));
922 
923 	cal_view = E_CALENDAR_VIEW (day_view);
924 	e_calendar_view_set_time_divisions (cal_view, divisions);
925 }
926 
927 static void
e_day_view_time_item_on_button_press(EDayViewTimeItem * time_item,GdkEvent * event)928 e_day_view_time_item_on_button_press (EDayViewTimeItem *time_item,
929                                       GdkEvent *event)
930 {
931 	GdkWindow *window;
932 	EDayView *day_view;
933 	GnomeCanvas *canvas;
934 	GdkGrabStatus grab_status;
935 	GdkDevice *event_device;
936 	guint32 event_time;
937 	gint row;
938 
939 	day_view = e_day_view_time_item_get_day_view (time_item);
940 	g_return_if_fail (day_view != NULL);
941 
942 	canvas = GNOME_CANVAS_ITEM (time_item)->canvas;
943 
944 	row = e_day_view_time_item_convert_position_to_row (
945 		time_item,
946 		event->button.y);
947 
948 	if (row == -1)
949 		return;
950 
951 	if (!gtk_widget_has_focus (GTK_WIDGET (day_view)))
952 		gtk_widget_grab_focus (GTK_WIDGET (day_view));
953 
954 	window = gtk_layout_get_bin_window (GTK_LAYOUT (canvas));
955 
956 	event_device = gdk_event_get_device (event);
957 	event_time = gdk_event_get_time (event);
958 
959 	grab_status = gdk_device_grab (
960 		event_device,
961 		window,
962 		GDK_OWNERSHIP_NONE,
963 		FALSE,
964 		GDK_POINTER_MOTION_MASK |
965 		GDK_BUTTON_RELEASE_MASK,
966 		NULL,
967 		event_time);
968 
969 	if (grab_status == GDK_GRAB_SUCCESS) {
970 		e_day_view_start_selection (day_view, -1, row);
971 		time_item->priv->dragging_selection = TRUE;
972 	}
973 }
974 
975 static void
e_day_view_time_item_on_button_release(EDayViewTimeItem * time_item,GdkEvent * event)976 e_day_view_time_item_on_button_release (EDayViewTimeItem *time_item,
977                                         GdkEvent *event)
978 {
979 	EDayView *day_view;
980 
981 	day_view = e_day_view_time_item_get_day_view (time_item);
982 	g_return_if_fail (day_view != NULL);
983 
984 	if (time_item->priv->dragging_selection) {
985 		GdkDevice *event_device;
986 		guint32 event_time;
987 
988 		event_device = gdk_event_get_device (event);
989 		event_time = gdk_event_get_time (event);
990 		gdk_device_ungrab (event_device, event_time);
991 
992 		e_day_view_finish_selection (day_view);
993 		e_day_view_stop_auto_scroll (day_view);
994 	}
995 
996 	time_item->priv->dragging_selection = FALSE;
997 }
998 
999 static void
e_day_view_time_item_on_motion_notify(EDayViewTimeItem * time_item,GdkEvent * event)1000 e_day_view_time_item_on_motion_notify (EDayViewTimeItem *time_item,
1001                                        GdkEvent *event)
1002 {
1003 	EDayView *day_view;
1004 	GnomeCanvas *canvas;
1005 	gdouble window_y;
1006 	gint y, row;
1007 
1008 	if (!time_item->priv->dragging_selection)
1009 		return;
1010 
1011 	day_view = e_day_view_time_item_get_day_view (time_item);
1012 	g_return_if_fail (day_view != NULL);
1013 
1014 	canvas = GNOME_CANVAS_ITEM (time_item)->canvas;
1015 
1016 	y = event->motion.y;
1017 	row = e_day_view_time_item_convert_position_to_row (time_item, y);
1018 
1019 	if (row != -1) {
1020 		gnome_canvas_world_to_window (
1021 			canvas, 0, event->motion.y,
1022 			NULL, &window_y);
1023 		e_day_view_update_selection (day_view, -1, row);
1024 		e_day_view_check_auto_scroll (day_view, -1, (gint) window_y);
1025 	}
1026 }
1027 
1028 /* Returns the row corresponding to the y position, or -1. */
1029 static gint
e_day_view_time_item_convert_position_to_row(EDayViewTimeItem * time_item,gint y)1030 e_day_view_time_item_convert_position_to_row (EDayViewTimeItem *time_item,
1031                                               gint y)
1032 {
1033 	EDayView *day_view;
1034 	gint row;
1035 
1036 	day_view = e_day_view_time_item_get_day_view (time_item);
1037 	g_return_val_if_fail (day_view != NULL, -1);
1038 
1039 	if (y < 0)
1040 		return -1;
1041 
1042 	row = y / day_view->row_height;
1043 	if (row >= day_view->rows)
1044 		return -1;
1045 
1046 	return row;
1047 }
1048 
1049 EDayView *
e_day_view_time_item_get_day_view(EDayViewTimeItem * time_item)1050 e_day_view_time_item_get_day_view (EDayViewTimeItem *time_item)
1051 {
1052 	g_return_val_if_fail (E_IS_DAY_VIEW_TIME_ITEM (time_item), NULL);
1053 
1054 	return time_item->priv->day_view;
1055 }
1056 
1057 void
e_day_view_time_item_set_day_view(EDayViewTimeItem * time_item,EDayView * day_view)1058 e_day_view_time_item_set_day_view (EDayViewTimeItem *time_item,
1059                                    EDayView *day_view)
1060 {
1061 	g_return_if_fail (E_IS_DAY_VIEW_TIME_ITEM (time_item));
1062 	g_return_if_fail (E_IS_DAY_VIEW (day_view));
1063 
1064 	if (time_item->priv->day_view == day_view)
1065 		return;
1066 
1067 	if (time_item->priv->day_view != NULL)
1068 		g_object_unref (time_item->priv->day_view);
1069 
1070 	time_item->priv->day_view = g_object_ref (day_view);
1071 
1072 	g_object_notify (G_OBJECT (time_item), "day-view");
1073 }
1074 
1075 /* Returns the minimum width needed for the column, by adding up all the
1076  * maximum widths of the strings. The string widths are all calculated in
1077  * the style_updated handlers of EDayView and EDayViewTimeCanvas. */
1078 gint
e_day_view_time_item_get_column_width(EDayViewTimeItem * time_item)1079 e_day_view_time_item_get_column_width (EDayViewTimeItem *time_item)
1080 {
1081 	EDayView *day_view;
1082 	PangoAttrList *tnum;
1083 	PangoAttribute *attr;
1084 	gint digit, large_digit_width, max_large_digit_width = 0;
1085 	gint max_suffix_width, max_minute_or_suffix_width;
1086 	gint column_width_default, column_width_60_min_rows;
1087 
1088 	day_view = e_day_view_time_item_get_day_view (time_item);
1089 	g_return_val_if_fail (day_view != NULL, 0);
1090 
1091 	tnum = pango_attr_list_new ();
1092 	attr = pango_attr_font_features_new ("tnum=1");
1093 	pango_attr_list_insert_before (tnum, attr);
1094 
1095 	/* Find the maximum width a digit can have. FIXME: We could use pango's
1096 	 * approximation function, but I worry it won't be precise enough. Also
1097 	 * it needs a language tag that I don't know where to get. */
1098 	for (digit = '0'; digit <= '9'; digit++) {
1099 		PangoLayout *layout;
1100 		gchar digit_str[2];
1101 
1102 		digit_str[0] = digit;
1103 		digit_str[1] = '\0';
1104 
1105 		layout = gtk_widget_create_pango_layout (GTK_WIDGET (day_view), digit_str);
1106 		pango_layout_set_attributes (layout, tnum);
1107 		pango_layout_set_font_description (layout, day_view->large_font_desc);
1108 		pango_layout_get_pixel_size (layout, &large_digit_width, NULL);
1109 
1110 		g_object_unref (layout);
1111 
1112 		max_large_digit_width = MAX (
1113 			max_large_digit_width,
1114 			large_digit_width);
1115 	}
1116 
1117 	pango_attr_list_unref (tnum);
1118 
1119 	/* Calculate the width of each time column, using the maximum of the
1120 	 * default format with large hour numbers, and the 60-min divisions
1121 	 * format which uses small text. */
1122 	max_suffix_width = MAX (
1123 		day_view->am_string_width,
1124 		day_view->pm_string_width);
1125 
1126 	max_minute_or_suffix_width = MAX (
1127 		max_suffix_width,
1128 		day_view->max_minute_width);
1129 
1130 	column_width_default = max_large_digit_width * 2
1131 		+ max_minute_or_suffix_width
1132 		+ E_DVTMI_MIN_X_PAD * 2
1133 		+ E_DVTMI_HOUR_L_PAD
1134 		+ E_DVTMI_HOUR_R_PAD
1135 		+ E_DVTMI_TIME_GRID_X_PAD * 2;
1136 
1137 	column_width_60_min_rows = day_view->max_small_hour_width
1138 		+ day_view->colon_width
1139 		+ max_minute_or_suffix_width
1140 		+ E_DVTMI_60_MIN_X_PAD * 2
1141 		+ E_DVTMI_TIME_GRID_X_PAD * 2;
1142 
1143 	time_item->priv->column_width =
1144 		MAX (column_width_default, column_width_60_min_rows);
1145 
1146 	if (time_item->priv->second_zone)
1147 		return (2 * time_item->priv->column_width) -
1148 			E_DVTMI_TIME_GRID_X_PAD;
1149 
1150 	return time_item->priv->column_width;
1151 }
1152 
1153 ICalTimezone *
e_day_view_time_item_get_second_zone(EDayViewTimeItem * time_item)1154 e_day_view_time_item_get_second_zone (EDayViewTimeItem *time_item)
1155 {
1156 	g_return_val_if_fail (E_IS_DAY_VIEW_TIME_ITEM (time_item), NULL);
1157 
1158 	return time_item->priv->second_zone;
1159 }
1160