1 /*
2  * Lays out events for the Week & Month views of the calendar. It is also
3  * used for printing.
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 "e-week-view-layout.h"
26 #include "calendar-config.h"
27 
28 static void e_week_view_layout_event	(EWeekViewEvent	*event,
29 					 guint8		*grid,
30 					 GArray		*spans,
31 					 GArray		*old_spans,
32 					 gboolean	 multi_week_view,
33 					 gint		 weeks_shown,
34 					 gboolean	 compress_weekend,
35 					 gint		 start_weekday,
36 					 time_t		*day_starts,
37 					 gint		*rows_per_day);
38 static gint e_week_view_find_day	(time_t		 time_to_find,
39 					 gboolean	 include_midnight_in_prev_day,
40 					 gint		 days_shown,
41 					 time_t		*day_starts);
42 static gint e_week_view_find_span_end	(gboolean	 multi_week_view,
43 					 gboolean	 compress_weekend,
44 					 GDateWeekday	 display_start_day,
45 					 gint		 day);
46 
47 GArray *
e_week_view_layout_events(GArray * events,GArray * old_spans,gboolean multi_week_view,gint weeks_shown,gboolean compress_weekend,gint start_weekday,time_t * day_starts,gint * rows_per_day)48 e_week_view_layout_events (GArray *events,
49                            GArray *old_spans,
50                            gboolean multi_week_view,
51                            gint weeks_shown,
52                            gboolean compress_weekend,
53                            gint start_weekday,
54                            time_t *day_starts,
55                            gint *rows_per_day)
56 {
57 	EWeekViewEvent *event;
58 	EWeekViewEventSpan *span;
59 	gint num_days, day, event_num, span_num;
60 	guint8 *grid;
61 	GArray *spans;
62 
63 	/* This is a temporary 2-d grid which is used to place events.
64 	 * Each element is 0 if the position is empty, or 1 if occupied.
65 	 * We allocate the maximum size possible here, assuming that each
66 	 * event will need its own row. */
67 	grid = g_new0 (guint8, E_WEEK_VIEW_MAX_ROWS_PER_CELL * 7
68 		       * E_WEEK_VIEW_MAX_WEEKS);
69 
70 	/* We create a new array of spans, which will replace the old one. */
71 	spans = g_array_new (FALSE, FALSE, sizeof (EWeekViewEventSpan));
72 
73 	/* Clear the number of rows used per day. */
74 	num_days = multi_week_view ? weeks_shown * 7 : 7;
75 	for (day = 0; day < num_days; day++) {
76 		rows_per_day[day] = 0;
77 	}
78 
79 	/* Iterate over the events, finding which weeks they cover, and putting
80 	 * them in the first free row available. */
81 	for (event_num = 0; event_num < events->len; event_num++) {
82 		event = &g_array_index (events, EWeekViewEvent, event_num);
83 		e_week_view_layout_event (
84 			event, grid, spans, old_spans,
85 			multi_week_view,
86 			weeks_shown, compress_weekend,
87 			start_weekday, day_starts,
88 			rows_per_day);
89 	}
90 
91 	/* Free the grid. */
92 	g_free (grid);
93 
94 	/* Destroy the old spans array, destroying any unused canvas items. */
95 	if (old_spans) {
96 		for (span_num = 0; span_num < old_spans->len; span_num++) {
97 			span = &g_array_index (old_spans, EWeekViewEventSpan,
98 					       span_num);
99 			if (span->background_item)
100 				g_object_run_dispose (G_OBJECT (span->background_item));
101 			if (span->text_item)
102 				g_object_run_dispose (G_OBJECT (span->text_item));
103 		}
104 		g_array_free (old_spans, TRUE);
105 	}
106 
107 	return spans;
108 }
109 
110 static void
e_week_view_layout_event(EWeekViewEvent * event,guint8 * grid,GArray * spans,GArray * old_spans,gboolean multi_week_view,gint weeks_shown,gboolean compress_weekend,gint start_weekday,time_t * day_starts,gint * rows_per_day)111 e_week_view_layout_event (EWeekViewEvent *event,
112                                  guint8 *grid,
113                                  GArray *spans,
114                                  GArray *old_spans,
115                                  gboolean multi_week_view,
116                                  gint weeks_shown,
117                                  gboolean compress_weekend,
118                                  gint start_weekday,
119                                  time_t *day_starts,
120                                  gint *rows_per_day)
121 {
122 	gint start_day, end_day, span_start_day, span_end_day, rows_per_cell;
123 	gint free_row, row, day, span_num, spans_index, num_spans, days_shown;
124 	EWeekViewEventSpan span, *old_span;
125 
126 	days_shown = multi_week_view ? weeks_shown * 7 : 7;
127 	start_day = e_week_view_find_day (
128 		event->start, FALSE, days_shown,
129 		day_starts);
130 	end_day = e_week_view_find_day (
131 		event->end, event->start != event->end, days_shown,
132 					day_starts);
133 	start_day = CLAMP (start_day, 0, days_shown - 1);
134 	end_day = CLAMP (end_day, 0, days_shown - 1);
135 
136 #if 0
137 	g_print (
138 		"In e_week_view_layout_event Start:%i End: %i\n",
139 		start_day, end_day);
140 #endif
141 
142 	/* Iterate through each of the spans of the event, where each span
143 	 * is a sequence of 1 or more days displayed next to each other. */
144 	span_start_day = start_day;
145 	rows_per_cell = E_WEEK_VIEW_MAX_ROWS_PER_CELL;
146 	span_num = 0;
147 	spans_index = spans->len;
148 	num_spans = 0;
149 	while (span_start_day <= end_day) {
150 		span_end_day = e_week_view_find_span_end (
151 			multi_week_view,
152 			compress_weekend,
153 			start_weekday,
154 			span_start_day);
155 		span_end_day = MIN (span_end_day, end_day);
156 #if 0
157 		g_print (
158 			"  Span start:%i end:%i\n", span_start_day,
159 			span_end_day);
160 #endif
161 		/* Try each row until we find a free one or we fall off the
162 		 * bottom of the available rows. */
163 		row = 0;
164 		free_row = -1;
165 		while (free_row == -1 && row < rows_per_cell) {
166 			free_row = row;
167 			for (day = span_start_day; day <= span_end_day;
168 			     day++) {
169 				if (grid[day * rows_per_cell + row]) {
170 					free_row = -1;
171 					break;
172 				}
173 			}
174 			row++;
175 		};
176 
177 		if (free_row != -1) {
178 			/* Mark the cells as full. */
179 			for (day = span_start_day; day <= span_end_day;
180 			     day++) {
181 				grid[day * rows_per_cell + free_row] = 1;
182 				rows_per_day[day] = MAX (
183 					rows_per_day[day],
184 					free_row + 1);
185 			}
186 #if 0
187 			g_print (
188 				"  Span start:%i end:%i row:%i\n",
189 				span_start_day, span_end_day, free_row);
190 #endif
191 			/* Add the span to the array, and try to reuse any
192 			 * canvas items from the old spans. */
193 			span.start_day = span_start_day;
194 			span.num_days = span_end_day - span_start_day + 1;
195 			span.row = free_row;
196 			span.background_item = NULL;
197 			span.text_item = NULL;
198 			if (event->num_spans > span_num) {
199 				old_span = &g_array_index (
200 					old_spans, EWeekViewEventSpan,
201 					event->spans_index + span_num);
202 				span.background_item = old_span->background_item;
203 				span.text_item = old_span->text_item;
204 				old_span->background_item = NULL;
205 				old_span->text_item = NULL;
206 			}
207 
208 			g_array_append_val (spans, span);
209 			num_spans++;
210 		}
211 
212 		span_start_day = span_end_day + 1;
213 		span_num++;
214 	}
215 
216 	/* Set the event's spans. */
217 	event->spans_index = spans_index;
218 	event->num_spans = num_spans;
219 }
220 
221 /* Finds the day containing the given time.
222  * If include_midnight_in_prev_day is TRUE then if the time exactly
223  * matches the start of a day the previous day is returned. This is useful
224  * when calculating the end day of an event. */
225 static gint
e_week_view_find_day(time_t time_to_find,gboolean include_midnight_in_prev_day,gint days_shown,time_t * day_starts)226 e_week_view_find_day (time_t time_to_find,
227                       gboolean include_midnight_in_prev_day,
228                       gint days_shown,
229                       time_t *day_starts)
230 {
231 	gint day;
232 
233 	if (time_to_find < day_starts[0])
234 		return -1;
235 	if (time_to_find > day_starts[days_shown])
236 		return days_shown;
237 
238 	for (day = 1; day <= days_shown; day++) {
239 		if (time_to_find <= day_starts[day]) {
240 			if (time_to_find == day_starts[day]
241 			    && !include_midnight_in_prev_day)
242 				return day;
243 			return day - 1;
244 		}
245 	}
246 
247 	g_return_val_if_reached (days_shown);
248 }
249 
250 /* This returns the last possible day in the same span as the given day.
251  * A span is all the days which are displayed next to each other from left to
252  * right. In the week view all spans are only 1 day, since Tuesday is below
253  * Monday rather than beside it etc. In the month view, if the weekends are not
254  * compressed then each week is a span, otherwise we have to break a span up
255  * on Saturday, use a separate span for Sunday, and start again on Monday. */
256 static gint
e_week_view_find_span_end(gboolean multi_week_view,gboolean compress_weekend,GDateWeekday display_start_day,gint day)257 e_week_view_find_span_end (gboolean multi_week_view,
258                            gboolean compress_weekend,
259                            GDateWeekday display_start_day,
260                            gint day)
261 {
262 	gint week, col, sat_col, end_col;
263 
264 	if (multi_week_view) {
265 		week = day / 7;
266 		col = day % 7;
267 
268 		/* We default to the last column in the row. */
269 		end_col = 6;
270 
271 		/* If the weekend is compressed we must end any spans on
272 		 * Saturday and Sunday. */
273 		if (compress_weekend) {
274 			sat_col = e_weekday_get_days_between (
275 				display_start_day, G_DATE_SATURDAY);
276 			if (col <= sat_col)
277 				end_col = sat_col;
278 			else if (col == sat_col + 1)
279 				end_col = sat_col + 1;
280 		}
281 
282 		return week * 7 + end_col;
283 	} else {
284 		return day;
285 	}
286 }
287 
288 void
e_week_view_layout_get_day_position(gint day,gboolean multi_week_view,gint weeks_shown,GDateWeekday display_start_day,gboolean compress_weekend,gint * day_x,gint * day_y,gint * rows)289 e_week_view_layout_get_day_position (gint day,
290                                      gboolean multi_week_view,
291                                      gint weeks_shown,
292                                      GDateWeekday display_start_day,
293                                      gboolean compress_weekend,
294                                      gint *day_x,
295                                      gint *day_y,
296                                      gint *rows)
297 {
298 	GDateWeekday day_of_week;
299 	gint week, col, weekend_col;
300 
301 	*day_x = *day_y = *rows = 0;
302 	g_return_if_fail (day >= 0);
303 
304 	if (multi_week_view) {
305 		g_return_if_fail (day < weeks_shown * 7);
306 
307 		week = day / 7;
308 		col = day % 7;
309 		day_of_week = e_weekday_add_days (display_start_day, day);
310 		if (compress_weekend && day_of_week >= G_DATE_SATURDAY) {
311 			/* In the compressed view Saturday is above Sunday and
312 			 * both have just one row as opposed to 2 for all the
313 			 * other days. */
314 			if (day_of_week == G_DATE_SATURDAY) {
315 				*day_y = week * 2;
316 				*rows = 1;
317 			} else {
318 				*day_y = week * 2 + 1;
319 				*rows = 1;
320 				col--;
321 			}
322 			/* Both Saturday and Sunday are in the same column. */
323 			*day_x = col;
324 		} else {
325 			/* If the weekend is compressed and the day is after
326 			 * the weekend we have to move back a column. */
327 			if (compress_weekend) {
328 				/* Calculate where the weekend column is. */
329 				weekend_col = e_weekday_get_days_between (
330 					display_start_day, G_DATE_SATURDAY);
331 				if (col > weekend_col)
332 					col--;
333 			}
334 
335 			*day_y = week * 2;
336 			*rows = 2;
337 			*day_x = col;
338 		}
339 	} else {
340 		GSettings *settings;
341 		gint arr[4] = {1, 1, 1, 1};
342 		gint edge, i, wd, m, M;
343 		gboolean any = TRUE;
344 		gboolean days_left_to_right;
345 		gint n_work_days_mon_wed = 0;
346 		gint n_work_days_thu_sun = 0;
347 
348 		/* 0 = Monday, 6 = Sunday */
349 		gint work_days[7] = { 0, 0, 0, 0, 0, 0, 0 };
350 
351 		g_return_if_fail (day < 7);
352 
353 		settings = e_util_ref_settings ("org.gnome.evolution.calendar");
354 
355 		days_left_to_right = g_settings_get_boolean (settings, "week-view-days-left-to-right");
356 
357 		if (g_settings_get_boolean (settings, "work-day-monday"))
358 			work_days[0] = 1, n_work_days_mon_wed++;
359 		if (g_settings_get_boolean (settings, "work-day-tuesday"))
360 			work_days[1] = 1, n_work_days_mon_wed++;
361 		if (g_settings_get_boolean (settings, "work-day-wednesday"))
362 			work_days[2] = 1, n_work_days_mon_wed++;
363 		if (g_settings_get_boolean (settings, "work-day-thursday"))
364 			work_days[3] = 1, n_work_days_thu_sun++;
365 		if (g_settings_get_boolean (settings, "work-day-friday"))
366 			work_days[4] = 1, n_work_days_thu_sun++;
367 		if (g_settings_get_boolean (settings, "work-day-saturday"))
368 			work_days[5] = 1, n_work_days_thu_sun++;
369 		if (g_settings_get_boolean (settings, "work-day-sunday"))
370 			work_days[6] = 1, n_work_days_thu_sun++;
371 
372 		g_object_unref (settings);
373 
374 		if (n_work_days_mon_wed < n_work_days_thu_sun)
375 			edge = 4;  /* Friday */
376 		else
377 			edge = 3;  /* Thursday */
378 
379 		if (days_left_to_right) {
380 			/* The default view is always with two columns:
381 			     Mon  Thu
382 			     Tue  Fri
383 			     Wed  Sat/Sun
384 			   eventually:
385 			     Mon  Fri
386 			     Tue  Sat
387 			     Wed  Sun
388 			     Thu
389 			*/
390 			if (edge == 3) {
391 				const gint transform[] = { 0, 3, 1, 4, 2, 5, 6 };
392 				day = transform[day];
393 			} else {
394 				const gint transform[] = { 0, 4, 1, 5, 2, 3, 6 };
395 				day = transform[day];
396 			}
397 		}
398 
399 		if (day < edge) {
400 			*day_x = 0;
401 			m = 0;
402 			M = edge;
403 		} else {
404 			*day_x = 1;
405 			m = edge;
406 			M = 7;
407 		}
408 
409 		wd = 0; /* number of used rows in column */
410 		for (i = m; i < M; i++) {
411 			arr[i - m] += work_days[i];
412 			wd += arr[i - m];
413 		}
414 
415 		while (wd != 6 && any) {
416 			any = FALSE;
417 
418 			for (i = M - 1; i >= m; i--) {
419 				if (arr[i - m] > 1) {
420 					any = TRUE;
421 
422 					/* too many rows, make last shorter */
423 					if (wd > 6) {
424 						arr[i - m] --;
425 						wd--;
426 
427 					/* free rows left, enlarge those bigger */
428 					} else if (wd < 6) {
429 						arr[i - m] ++;
430 						wd++;
431 					}
432 
433 					if (wd == 6)
434 						break;
435 				}
436 			}
437 
438 			if (!any && wd != 6) {
439 				any = TRUE;
440 
441 				for (i = m; i < M; i++) {
442 					arr[i - m] += 3;
443 					wd += 3;
444 				}
445 			}
446 		}
447 
448 		*rows = arr [day - m];
449 
450 		*day_y = 0;
451 		for (i = m; i < day; i++) {
452 			*day_y += arr [i - m];
453 		}
454 
455 		#undef wk
456 	}
457 }
458 
459 /* Returns TRUE if the event span is visible or FALSE if it isn't.
460  * It also returns the number of days of the span that are visible.
461  * Usually this can easily be determined by the start & end days and row of
462  * the span, which are set in e_week_view_layout_event (). Though we need a
463  * special case for the weekends when they are compressed, since the span may
464  * not fit. */
465 gboolean
e_week_view_layout_get_span_position(EWeekViewEvent * event,EWeekViewEventSpan * span,gint rows_per_cell,gint rows_per_compressed_cell,GDateWeekday display_start_day,gboolean multi_week_view,gboolean compress_weekend,gint * span_num_days)466 e_week_view_layout_get_span_position (EWeekViewEvent *event,
467                                       EWeekViewEventSpan *span,
468                                       gint rows_per_cell,
469                                       gint rows_per_compressed_cell,
470                                       GDateWeekday display_start_day,
471                                       gboolean multi_week_view,
472                                       gboolean compress_weekend,
473                                       gint *span_num_days)
474 {
475 	GDateWeekday end_day_of_week;
476 	guint n_days;
477 
478 	if (multi_week_view && span->row >= rows_per_cell)
479 		return FALSE;
480 
481 	n_days = span->start_day + span->num_days - 1;
482 	end_day_of_week = e_weekday_add_days (display_start_day, n_days);
483 
484 	*span_num_days = span->num_days;
485 	/* Check if the row will not be visible in compressed cells. */
486 	if (span->row >= rows_per_compressed_cell) {
487 		if (multi_week_view) {
488 			if (compress_weekend) {
489 				/* If it ends on a Saturday and is 1 day glong
490 				 * we skip it, else we shorten it. If it ends
491 				 * on a Sunday it must be 1 day long and we
492 				 * skip it. */
493 				if (end_day_of_week == G_DATE_SATURDAY) {
494 					if (*span_num_days == 1) {
495 						return FALSE;
496 					} else {
497 						(*span_num_days)--;
498 					}
499 				} else if (end_day_of_week == G_DATE_SUNDAY) {
500 					return FALSE;
501 				}
502 			}
503 		} else {
504 			gint day_x, day_y, rows = 0;
505 			e_week_view_layout_get_day_position (
506 				end_day_of_week - 1, multi_week_view, 1,
507 				display_start_day, compress_weekend,
508 				&day_x, &day_y, &rows);
509 
510 			if (((rows / 2) * rows_per_cell) + ((rows % 2) *
511 				rows_per_compressed_cell) <= span->row)
512 				return FALSE;
513 		}
514 	}
515 
516 	return TRUE;
517 }
518