1 
2 /*
3  * cellspan.c: Keep track of the columns on which a cell
4  * displays information.
5  *
6  * Author:
7  *   Miguel de Icaza (miguel@gnu.org)
8  *   Jody Goldberg (jody@gnome.org)
9  *
10  * The information on cell spanning is attached in the row ColRowInfo
11  * structures.  The actual representation of this information is
12  * opaque to the code that uses it (the idea is: this first
13  * implementation is not really awesome).
14  *
15  * The reason we need this is that the Grid draw code expects to find
16  * the "owner" of the cell to be able to repaint its contents.
17  */
18 #include <gnumeric-config.h>
19 #include <gnumeric.h>
20 #include <cellspan.h>
21 
22 #include <cell.h>
23 #include <sheet.h>
24 #include <sheet-merge.h>
25 #include <sheet-style.h>
26 #include <style.h>
27 #include <colrow.h>
28 #include <value.h>
29 #include <rendered-value.h>
30 
31 static guint
col_hash(gconstpointer key)32 col_hash (gconstpointer key)
33 {
34 	return GPOINTER_TO_INT(key);
35 }
36 
37 static gint
col_compare(gconstpointer a,gconstpointer b)38 col_compare (gconstpointer a, gconstpointer b)
39 {
40 	if (GPOINTER_TO_INT(a) == GPOINTER_TO_INT(b))
41 		return 1;
42 	return 0;
43 }
44 
45 static void
free_hash_value(G_GNUC_UNUSED gpointer key,gpointer value,G_GNUC_UNUSED gpointer user_data)46 free_hash_value (G_GNUC_UNUSED gpointer key, gpointer value,
47 		 G_GNUC_UNUSED gpointer user_data)
48 {
49 	g_free (value);
50 }
51 
52 void
row_destroy_span(ColRowInfo * ri)53 row_destroy_span (ColRowInfo *ri)
54 {
55 	if (ri == NULL || ri->spans == NULL)
56 		return;
57 
58 	g_hash_table_foreach (ri->spans, free_hash_value, NULL);
59 	g_hash_table_destroy (ri->spans);
60 	ri->spans = NULL;
61 }
62 
63 /*
64  * cell_register_span
65  * @cell:  The cell to register the span
66  * @left:  the leftmost column used by the cell
67  * @right: the rightmost column used by the cell
68  *
69  * Registers the region
70  */
71 void
cell_register_span(GnmCell const * cell,int left,int right)72 cell_register_span (GnmCell const *cell, int left, int right)
73 {
74 	ColRowInfo *ri;
75 	int row, i;
76 
77 	g_return_if_fail (cell != NULL);
78 	g_return_if_fail (left <= right);
79 
80 	row = cell->pos.row;
81 	ri = sheet_row_get (cell->base.sheet, row);
82 
83 	if (left == right)
84 		return;
85 
86 	if (ri->spans == NULL)
87 		ri->spans = g_hash_table_new (col_hash, col_compare);
88 
89 	for (i = left; i <= right; i++){
90 		CellSpanInfo *spaninfo = g_new (CellSpanInfo, 1);
91 
92 		spaninfo->cell  = cell;
93 		spaninfo->left  = left;
94 		spaninfo->right = right;
95 
96 		g_return_if_fail (row_span_get (ri, i) == NULL);
97 		g_hash_table_insert (ri->spans, GINT_TO_POINTER(i), spaninfo);
98 	}
99 }
100 
101 static gboolean
span_remove(G_GNUC_UNUSED gpointer key,gpointer value,gpointer user_data)102 span_remove (G_GNUC_UNUSED gpointer key, gpointer value,
103 	     gpointer user_data)
104 {
105 	CellSpanInfo *span = (CellSpanInfo *)value;
106 	GnmCell *cell = user_data;
107 
108 	if (cell == span->cell) {
109 		g_free (span); /* free the span descriptor */
110 		return TRUE;
111 	}
112 	return FALSE;
113 }
114 
115 /*
116  * sheet_cell_unregister_span
117  * @cell: The cell to remove from the span information
118  *
119  * Remove all references to this cell in the span hashtable
120  */
121 void
cell_unregister_span(GnmCell const * const cell)122 cell_unregister_span (GnmCell const * const cell)
123 {
124 	ColRowInfo *ri;
125 
126 	g_return_if_fail (cell != NULL);
127 
128 	ri = sheet_row_get (cell->base.sheet, cell->pos.row);
129 
130 	if (ri->spans == NULL)
131 		return;
132 
133 	g_hash_table_foreach_remove (ri->spans,
134 				     &span_remove, (gpointer)cell);
135 }
136 
137 /*
138  * row_span_get
139  * @ri: The ColRowInfo for the row we are looking up
140  * @col: the column position
141  *
142  * Returns SpanInfo of the spanning cell being display at the
143  * column.  Including
144  *   - the cell whose contents span.
145  *   - The first and last col in the span.
146  */
147 CellSpanInfo const *
row_span_get(ColRowInfo const * const ri,int const col)148 row_span_get (ColRowInfo const * const ri, int const col)
149 {
150 	g_return_val_if_fail (ri != NULL, NULL);
151 
152 	if (ri->spans == NULL)
153 		return NULL;
154 	return g_hash_table_lookup (ri->spans, GINT_TO_POINTER(col));
155 }
156 
157 /* making CellSpanInfo a boxed type. As this objects are constant, no need
158  * to really copy free them. Right? */
159 static const CellSpanInfo*
cell_span_info_copy(CellSpanInfo const * sp)160 cell_span_info_copy (CellSpanInfo const *sp)
161 {
162 	return sp;
163 }
164 
165 GType
cell_span_info_get_type(void)166 cell_span_info_get_type (void)
167 {
168 	static GType t = 0;
169 
170 	if (t == 0) {
171 		t = g_boxed_type_register_static ("CellSpanInfo",
172 			 (GBoxedCopyFunc)cell_span_info_copy,
173 			 (GBoxedFreeFunc)cell_span_info_copy);
174 	}
175 	return t;
176 }
177 
178 /**
179  * cellspan_is_empty:
180  *
181  * Utility to ensure that a cell is completely empty.
182  *    - no spans
183  *    - no merged regions
184  *    - no content
185  *
186  * No need to check for merged cells here.  We have already bounded the search region
187  * using adjacent merged cells.
188  *
189  * We could probably have done the same thing with the span regions too, but
190  * the current representation is not well suited to that type of search
191  * returns %TRUE if the cell is empty.
192  */
193 static inline gboolean
cellspan_is_empty(int col,GnmCell const * ok_span_cell)194 cellspan_is_empty (int col, GnmCell const *ok_span_cell)
195 {
196 	Sheet *sheet = ok_span_cell->base.sheet;
197 	int row = ok_span_cell->pos.row;
198 	ColRowInfo *ri = sheet_row_get (sheet, row);
199 	CellSpanInfo const *span = row_span_get (ri, col);
200 	GnmCell const *tmp;
201 
202 	if (span != NULL && span->cell != ok_span_cell)
203 		return FALSE;
204 
205 	tmp = sheet_cell_get (sheet, col, row);
206 
207 	/* FIXME : cannot use gnm_cell_is_empty until expressions can span.
208 	 * because cells with expressions start out with value Empty
209 	 * existing spans continue to flow through, but never get removed
210 	 * because we don't respan expression results.
211 	 */
212 	return (tmp == NULL || tmp->value == NULL ||
213 		(VALUE_IS_EMPTY (tmp->value) && !gnm_cell_has_expr(tmp)));
214 }
215 
216 /*
217  * cell_calc_span:
218  * @cell:   The cell we will examine
219  * @col1:   return value: the first column used by this cell
220  * @col2:   return value: the last column used by this cell
221  *
222  * This routine returns the column interval used by a GnmCell.
223  */
224 void
cell_calc_span(GnmCell const * cell,int * col1,int * col2)225 cell_calc_span (GnmCell const *cell, int *col1, int *col2)
226 {
227 	Sheet *sheet;
228 	int h_align, v_align, left, max_col, min_col;
229 	int pos;
230 	int cell_width_pixel, indented_w;
231 	GnmStyle const *style;
232 	ColRowInfo const *ci;
233 	GnmRange const *merge_left;
234 	GnmRange const *merge_right;
235 
236 	g_return_if_fail (cell != NULL);
237 
238 	sheet = cell->base.sheet;
239 	style = gnm_cell_get_effective_style (cell);
240 	h_align = gnm_style_default_halign (style, cell);
241 
242         /*
243 	 * Report only one column is used if
244 	 *	- Cell is in a hidden col
245 	 *	- Cell is a number
246 	 *	- Cell is the top left of a merged cell
247 	 *	- The text fits inside column (for non center across selection)
248 	 *	- The alignment mode are set to "justify"
249 	 */
250 	if (sheet != NULL &&
251 	    h_align != GNM_HALIGN_CENTER_ACROSS_SELECTION &&
252 	    (gnm_cell_is_merged (cell) ||
253 	     (!sheet->display_formulas && gnm_cell_is_number (cell)))) {
254 		*col1 = *col2 = cell->pos.col;
255 		return;
256 	}
257 
258 	v_align = gnm_style_get_align_v (style);
259 	indented_w = cell_width_pixel = gnm_cell_rendered_width (cell);
260 	if (h_align == GNM_HALIGN_LEFT || h_align == GNM_HALIGN_RIGHT) {
261 		GnmRenderedValue *rv = gnm_cell_get_rendered_value (cell);
262 		char const *text = (rv)? pango_layout_get_text (rv->layout): NULL;
263 		PangoDirection dir = (text && *text)? pango_find_base_dir (text, -1): PANGO_DIRECTION_LTR;
264 		if (gnm_style_get_align_h (style) == GNM_HALIGN_GENERAL && dir == PANGO_DIRECTION_RTL)
265 			h_align = GNM_HALIGN_RIGHT;
266 		indented_w += gnm_cell_rendered_offset (cell);
267 		if (sheet->text_is_rtl)
268 			h_align = (h_align == GNM_HALIGN_LEFT) ? GNM_HALIGN_RIGHT : GNM_HALIGN_LEFT;
269 	}
270 
271 	ci = sheet_col_get_info	(sheet, cell->pos.col);
272 	if (gnm_cell_is_empty (cell) ||
273 	    !ci->visible ||
274 	    (h_align != GNM_HALIGN_CENTER_ACROSS_SELECTION &&
275 		 (gnm_style_get_wrap_text (style) ||
276 		  indented_w <= COL_INTERNAL_WIDTH (ci))) ||
277 	    h_align == GNM_HALIGN_JUSTIFY ||
278 	    h_align == GNM_HALIGN_FILL ||
279 	    h_align == GNM_HALIGN_DISTRIBUTED ||
280 	    v_align == GNM_VALIGN_JUSTIFY ||
281 	    v_align == GNM_VALIGN_DISTRIBUTED) {
282 		*col1 = *col2 = cell->pos.col;
283 		return;
284 	}
285 
286 	gnm_sheet_merge_get_adjacent (sheet, &cell->pos, &merge_left, &merge_right);
287 	min_col = (merge_left != NULL) ? merge_left->end.col : -1;
288 	max_col = (merge_right != NULL) ? merge_right->start.col : gnm_sheet_get_max_cols (sheet);
289 
290 	*col1 = *col2 = cell->pos.col;
291 	switch (h_align) {
292 	case GNM_HALIGN_LEFT:
293 		pos = cell->pos.col + 1;
294 		left = indented_w - COL_INTERNAL_WIDTH (ci);
295 
296 		for (; left > 0 && pos < max_col; pos++){
297 			ColRowInfo const *ci = sheet_col_get_info (sheet, pos);
298 
299 			if (ci->visible) {
300 				if (!cellspan_is_empty (pos, cell))
301 					return;
302 
303 				/* The space consumed is:
304 				 *   - The margin_b from the last column
305 				 *   - The width of the cell
306 				 */
307 				left -= ci->size_pixels - 1;
308 				*col2 = pos;
309 			}
310 		}
311 		return;
312 
313 	case GNM_HALIGN_RIGHT:
314 		pos = cell->pos.col - 1;
315 		left = indented_w - COL_INTERNAL_WIDTH (ci);
316 
317 		for (; left > 0 && pos > min_col; pos--){
318 			ColRowInfo const *ci = sheet_col_get_info (sheet, pos);
319 
320 			if (ci->visible) {
321 				if (!cellspan_is_empty (pos, cell))
322 					return;
323 
324 				/* The space consumed is:
325 				 *   - The margin_a from the last column
326 				 *   - The width of this cell
327 				 */
328 				left -= ci->size_pixels - 1;
329 				*col1 = pos;
330 			}
331 		}
332 		return;
333 
334 	case GNM_HALIGN_CENTER: {
335 		int remain_left, remain_right;
336 		int pos_l, pos_r;
337 
338 		pos_l = pos_r = cell->pos.col;
339 		left = cell_width_pixel - COL_INTERNAL_WIDTH (ci);
340 
341 		remain_left  = left / 2 + (left % 2);
342 		remain_right = left / 2;
343 
344 		for (; remain_left > 0;)
345 			if (--pos_l > min_col){
346 				ColRowInfo const *ci = sheet_col_get_info (sheet, pos_l);
347 
348 				if (ci->visible) {
349 					if (cellspan_is_empty (pos_l, cell)) {
350 						remain_left -= ci->size_pixels - 1;
351 						*col1 = pos_l;
352 					} else
353 						remain_left = 0;
354 				}
355 			} else
356 				remain_left = 0;
357 
358 		for (; remain_right > 0;)
359 			if (++pos_r < max_col){
360 				ColRowInfo const *ci = sheet_col_get_info (sheet, pos_r);
361 
362 				if (ci->visible) {
363 					if (cellspan_is_empty (pos_r, cell)) {
364 						remain_right -= ci->size_pixels - 1;
365 						*col2 = pos_r;
366 					} else
367 						max_col = remain_right = 0;
368 				}
369 			} else
370 				remain_right = 0;
371 		break;
372 	} /* case GNM_HALIGN_CENTER */
373 
374 	case GNM_HALIGN_CENTER_ACROSS_SELECTION: {
375 		int const row = cell->pos.row;
376 		int pos_l, pos_r;
377 
378 		pos_l = pos_r = cell->pos.col;
379 		while (--pos_l > min_col) {
380 			ColRowInfo const *ci = sheet_col_get_info (sheet, pos_l);
381 			if (ci->visible) {
382 				if (cellspan_is_empty (pos_l, cell)) {
383 					GnmStyle const * const style =
384 						sheet_style_get (cell->base.sheet, pos_l, row);
385 
386 					if (gnm_style_get_align_h (style) != GNM_HALIGN_CENTER_ACROSS_SELECTION)
387 						break;
388 					*col1 = pos_l;
389 				} else
390 					break;
391 			}
392 		}
393 		while (++pos_r < max_col) {
394 			ColRowInfo const *ci = sheet_col_get_info (sheet, pos_r);
395 			if (ci->visible) {
396 				if (cellspan_is_empty (pos_r, cell)) {
397 					GnmStyle const * const style =
398 						sheet_style_get (cell->base.sheet, pos_r, row);
399 
400 					if (gnm_style_get_align_h (style) != GNM_HALIGN_CENTER_ACROSS_SELECTION)
401 						break;
402 					*col2 = pos_r;
403 				} else
404 					break;
405 			}
406 		}
407 		break;
408 	}
409 
410 	default:
411 		g_warning ("Unknown horizontal alignment type %x.", h_align);
412 	} /* switch */
413 }
414 
415 void
row_calc_spans(ColRowInfo * ri,int row,Sheet const * sheet)416 row_calc_spans (ColRowInfo *ri, int row, Sheet const *sheet)
417 {
418 	int left, right, col;
419 	GnmRange const *merged;
420 	GnmCell *cell;
421 	int const last = sheet->cols.max_used;
422 
423 	row_destroy_span (ri);
424 	for (col = 0 ; col <= last ; ) {
425 		cell = sheet_cell_get (sheet, col, row);
426 		if (cell == NULL) {
427 			/* skip segments with no cells */
428 			if (col == COLROW_SEGMENT_START (col) &&
429 			    NULL == COLROW_GET_SEGMENT (&(sheet->cols), col))
430 				col = COLROW_SEGMENT_END (col) + 1;
431 			else
432 				col++;
433 			continue;
434 		}
435 
436 		/* render as necessary */
437 		(void)gnm_cell_fetch_rendered_value (cell, TRUE);
438 
439 		if (gnm_cell_is_merged (cell)) {
440 			merged = gnm_sheet_merge_is_corner (sheet, &cell->pos);
441 			if (NULL != merged) {
442 				col = merged->end.col + 1;
443 				continue;
444 			}
445 		}
446 
447 		cell_calc_span (cell, &left, &right);
448 		if (left != right) {
449 			cell_register_span (cell, left, right);
450 			col = right + 1;
451 		} else
452 			col++;
453 	}
454 
455 	ri->needs_respan = FALSE;
456 }
457