1 
2 /*
3  * sheet-merge.c: merged cell support
4  *
5  * Copyright (C) 2000-2002 Jody Goldberg (jody@gnome.org)
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License as
9  * published by the Free Software Foundation; either version 2 of the
10  * License, or (at your option) version 3.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
20  * USA
21  */
22 #include <gnumeric-config.h>
23 #include <glib/gi18n-lib.h>
24 #include <gnumeric.h>
25 #include <sheet-merge.h>
26 
27 #include <sheet-object.h>
28 #include <sheet.h>
29 #include <sheet-view.h>
30 #include <sheet-private.h>
31 #include <ranges.h>
32 #include <cell.h>
33 #include <cellspan.h>
34 #include <sheet-style.h>
35 #include <mstyle.h>
36 #include <expr.h>
37 #include <command-context.h>
38 
39 static gint
range_row_cmp(GnmRange const * a,GnmRange const * b)40 range_row_cmp (GnmRange const *a, GnmRange const *b)
41 {
42 	int tmp = b->start.row - a->start.row;
43 	if (tmp == 0)
44 		tmp = a->start.col - b->start.col; /* YES I DO MEAN a - b */
45 	return tmp;
46 }
47 
48 /**
49  * gnm_sheet_merge_add:
50  * @sheet: the sheet which will contain the region
51  * @r: The region to merge
52  * @clear: should the non-corner content of the region be cleared and the
53  *          style from the corner applied.
54  * @cc: (nullable): the calling context
55  *
56  * Add a range to the list of merge targets.  Checks for array splitting returns
57  * %TRUE if there was an error.  Queues a respan.  Only queues a redraw if @clear
58  * is %TRUE.
59  */
60 gboolean
gnm_sheet_merge_add(Sheet * sheet,GnmRange const * r,gboolean clear,GOCmdContext * cc)61 gnm_sheet_merge_add (Sheet *sheet, GnmRange const *r, gboolean clear,
62 		     GOCmdContext *cc)
63 {
64 	GSList *test;
65 	GnmRange  *r_copy;
66 	GnmCell   *cell;
67 	GnmComment *comment;
68 	GnmRange r2;
69 
70 	g_return_val_if_fail (IS_SHEET (sheet), TRUE);
71 	g_return_val_if_fail (range_is_sane (r), TRUE);
72 	g_return_val_if_fail (r->end.col < gnm_sheet_get_max_cols (sheet), TRUE);
73 	g_return_val_if_fail (r->end.row < gnm_sheet_get_max_rows (sheet), TRUE);
74 
75 	r2 = *r;
76 	range_ensure_sanity (&r2, sheet);
77 
78 	if (sheet_range_splits_array (sheet, &r2, NULL, cc, _("Merge")))
79 		return TRUE;
80 
81 	test = gnm_sheet_merge_get_overlap (sheet, &r2);
82 	if (test != NULL) {
83 		if (cc != NULL)
84 			go_cmd_context_error (cc, g_error_new (go_error_invalid(), 0,
85 				_("There is already a merged region that intersects\n%s!%s"),
86 				sheet->name_unquoted, range_as_string (&r2)));
87 		g_slist_free (test);
88 		return TRUE;
89 	}
90 
91 	if (clear) {
92 		int i;
93 		GnmStyle *style;
94 
95 		sheet_redraw_range (sheet, &r2);
96 
97 		/* Clear the non-corner content */
98 		if (r2.start.col != r2.end.col)
99 			sheet_clear_region (sheet,
100 					    r2.start.col+1, r2.start.row,
101 					    r2.end.col, r2.end.row,
102 					    CLEAR_VALUES | CLEAR_COMMENTS | CLEAR_NOCHECKARRAY | CLEAR_NORESPAN,
103 					    cc);
104 		if (r2.start.row != r2.end.row)
105 			sheet_clear_region (sheet,
106 					    r2.start.col, r2.start.row+1,
107 	    /* yes I mean start.col */	    r2.start.col, r2.end.row,
108 					    CLEAR_VALUES | CLEAR_COMMENTS | CLEAR_NOCHECKARRAY | CLEAR_NORESPAN,
109 					    cc);
110 
111 		/* Apply the corner style to the entire region */
112 		style = gnm_style_dup (sheet_style_get (sheet, r2.start.col,
113 						      r2.start.row));
114 		for (i = MSTYLE_BORDER_TOP; i <= MSTYLE_BORDER_DIAGONAL; i++)
115 			gnm_style_unset_element (style, i);
116 		sheet_style_apply_range (sheet, &r2, style);
117 		sheet_region_queue_recalc (sheet, &r2);
118 	}
119 
120 	r_copy = gnm_range_dup (&r2);
121 	g_hash_table_insert (sheet->hash_merged, &r_copy->start, r_copy);
122 
123 	/* Store in order from bottom to top then LEFT TO RIGHT (by start coord) */
124 	sheet->list_merged = g_slist_insert_sorted (sheet->list_merged, r_copy,
125 						    (GCompareFunc)range_row_cmp);
126 
127 	cell = sheet_cell_get (sheet, r2.start.col, r2.start.row);
128 	if (cell != NULL) {
129 		cell->base.flags |= GNM_CELL_IS_MERGED;
130 		cell_unregister_span (cell);
131 	}
132 	sheet_queue_respan (sheet, r2.start.row, r2.end.row);
133 
134 	/* Ensure that edit pos is not in the center of a region. */
135 	SHEET_FOREACH_VIEW (sheet, sv, {
136 		sv->reposition_selection = TRUE;
137 		if (range_contains (&r2, sv->edit_pos.col, sv->edit_pos.row))
138 			gnm_sheet_view_set_edit_pos (sv, &r2.start);
139 	});
140 
141 	comment = sheet_get_comment (sheet, &r2.start);
142 	if (comment != NULL)
143 		sheet_object_update_bounds (GNM_SO (comment), NULL);
144 
145 	sheet_flag_status_update_range (sheet, &r2);
146 	if (sheet->cols.max_used < r2.end.col) {
147 		sheet->cols.max_used = r2.end.col;
148 		sheet->priv->resize_scrollbar = TRUE;
149 	}
150 	if (sheet->rows.max_used < r2.end.row) {
151 		sheet->rows.max_used = r2.end.row;
152 		sheet->priv->resize_scrollbar = TRUE;
153 	}
154 	return FALSE;
155 }
156 
157 /**
158  * gnm_sheet_merge_remove:
159  * @sheet: the sheet which will contain the region
160  * @r: The region
161  *
162  * Remove a merged range.
163  * Queues a redraw.
164  *
165  * Returns: %TRUE if there was an error.
166  **/
167 gboolean
gnm_sheet_merge_remove(Sheet * sheet,GnmRange const * r)168 gnm_sheet_merge_remove (Sheet *sheet, GnmRange const *r)
169 {
170 	GnmRange   *r_copy;
171 	GnmCell    *cell;
172 	GnmComment *comment;
173 
174 	g_return_val_if_fail (IS_SHEET (sheet), TRUE);
175 	g_return_val_if_fail (r != NULL, TRUE);
176 
177 	r_copy = g_hash_table_lookup (sheet->hash_merged, &r->start);
178 
179 	g_return_val_if_fail (r_copy != NULL, TRUE);
180 	g_return_val_if_fail (range_equal (r, r_copy), TRUE);
181 
182 	g_hash_table_remove (sheet->hash_merged, &r_copy->start);
183 	sheet->list_merged = g_slist_remove (sheet->list_merged, r_copy);
184 
185 	cell = sheet_cell_get (sheet, r->start.col, r->start.row);
186 	if (cell != NULL)
187 		cell->base.flags &= ~GNM_CELL_IS_MERGED;
188 
189 	comment = sheet_get_comment (sheet, &r->start);
190 	if (comment != NULL)
191 		sheet_object_update_bounds (GNM_SO (comment), NULL);
192 
193 	sheet_redraw_range (sheet, r);
194 	sheet_flag_status_update_range (sheet, r);
195 	SHEET_FOREACH_VIEW (sheet, sv, sv->reposition_selection = TRUE;);
196 	g_free (r_copy);
197 	return FALSE;
198 }
199 
200 /**
201  * gnm_sheet_merge_get_overlap:
202  *
203  * Returns: (element-type GnmRange) (transfer container): a list of the merged
204  * regions that overlap the target region.
205  * The list is ordered from top to bottom and RIGHT TO LEFT (by start coord).
206  */
207 GSList *
gnm_sheet_merge_get_overlap(Sheet const * sheet,GnmRange const * range)208 gnm_sheet_merge_get_overlap (Sheet const *sheet, GnmRange const *range)
209 {
210 	GSList *ptr, *res = NULL;
211 
212 	g_return_val_if_fail (IS_SHEET (sheet), NULL);
213 	g_return_val_if_fail (range != NULL, NULL);
214 
215 	for (ptr = sheet->list_merged ; ptr != NULL ; ptr = ptr->next) {
216 		GnmRange * const test = ptr->data;
217 
218 		if (range_overlap (range, test))
219 			res = g_slist_prepend (res, test);
220 	}
221 
222 	return res;
223 }
224 
225 /**
226  * gnm_sheet_merge_contains_pos:
227  * @sheet: #Sheet to query
228  * @pos: Position to look for a merged range.
229  *
230  * Returns: (transfer none) (nullable): the merged range covering @pos, or
231  * %NULL if @pos is not within a merged region.
232  */
233 GnmRange const *
gnm_sheet_merge_contains_pos(Sheet const * sheet,GnmCellPos const * pos)234 gnm_sheet_merge_contains_pos (Sheet const *sheet, GnmCellPos const *pos)
235 {
236 	GSList *ptr;
237 
238 	g_return_val_if_fail (IS_SHEET (sheet), NULL);
239 	g_return_val_if_fail (pos != NULL, NULL);
240 
241 	for (ptr = sheet->list_merged ; ptr != NULL ; ptr = ptr->next) {
242 		GnmRange const * const range = ptr->data;
243 		if (range_contains (range, pos->col, pos->row))
244 			return range;
245 	}
246 	return NULL;
247 }
248 
249 /**
250  * gnm_sheet_merge_get_adjacent:
251  * @sheet: The sheet to look in.
252  * @pos: the cell to test for adjacent regions.
253  * @left: the return for a region on the left
254  * @right: the return for a region on the right
255  *
256  * Returns the nearest regions to either side of @pos.
257  */
258 void
gnm_sheet_merge_get_adjacent(Sheet const * sheet,GnmCellPos const * pos,GnmRange const ** left,GnmRange const ** right)259 gnm_sheet_merge_get_adjacent (Sheet const *sheet, GnmCellPos const *pos,
260 			      GnmRange const **left, GnmRange const **right)
261 {
262 	GSList *ptr;
263 
264 	g_return_if_fail (IS_SHEET (sheet));
265 	g_return_if_fail (pos != NULL);
266 
267 	*left = *right = NULL;
268 	for (ptr = sheet->list_merged ; ptr != NULL ; ptr = ptr->next) {
269 		GnmRange const * const test = ptr->data;
270 		if (test->start.row <= pos->row && pos->row <= test->end.row) {
271 			int const diff = test->end.col - pos->col;
272 
273 			g_return_if_fail (diff != 0);
274 
275 			if (diff < 0) {
276 				if (*left == NULL || (*left)->end.col < test->end.col)
277 					*left = test;
278 			} else {
279 				if (*right == NULL || (*right)->start.col > test->start.col)
280 					*right = test;
281 			}
282 		}
283 	}
284 }
285 
286 /**
287  * gnm_sheet_merge_is_corner:
288  * @sheet: #Sheet to query
289  * @pos: cellpos if top left corner
290  *
291  * Returns: (transfer none): a merged #GnmRange covering @pos if it is the
292  * top-left corner of a merged region.
293  */
294 GnmRange const *
gnm_sheet_merge_is_corner(Sheet const * sheet,GnmCellPos const * pos)295 gnm_sheet_merge_is_corner (Sheet const *sheet, GnmCellPos const *pos)
296 {
297 	g_return_val_if_fail (IS_SHEET (sheet), NULL);
298 	g_return_val_if_fail (pos != NULL, NULL);
299 
300 	return g_hash_table_lookup (sheet->hash_merged, pos);
301 }
302 
303 static void
cb_restore_merge(Sheet * sheet,GSList * restore)304 cb_restore_merge (Sheet *sheet, GSList *restore)
305 {
306 	GSList *l;
307 	for (l = restore; l; l = l->next) {
308 		GnmRange const *r = l->data;
309 		GnmRange const *r2 = g_hash_table_lookup (sheet->hash_merged,
310 							  &r->start);
311 		if (r2 && range_equal (r, r2))
312 			continue;
313 
314 		// The only reason for r2 to be different from r is that we
315 		// clipped.  Moving the clipped region back didn't restore
316 		// the old state, so we'll have to remove the merge and
317 		// create a new.
318 		if (r2)
319 			gnm_sheet_merge_remove (sheet, r2);
320 
321 		gnm_sheet_merge_add (sheet, r, FALSE, NULL);
322 	}
323 }
324 
325 static void
cb_restore_list_free(GSList * restore)326 cb_restore_list_free (GSList *restore)
327 {
328 	g_slist_free_full (restore, g_free);
329 }
330 
331 /**
332  * gnm_sheet_merge_relocate:
333  * @ri: Descriptor of what is moving.
334  * @pundo: (out) (optional) (transfer full): Undo information.
335  *
336  * Shifts merged regions that need to move.
337  */
338 void
gnm_sheet_merge_relocate(GnmExprRelocateInfo const * ri,GOUndo ** pundo)339 gnm_sheet_merge_relocate (GnmExprRelocateInfo const *ri, GOUndo **pundo)
340 {
341 	GSList   *ptr, *copy, *reapply = NULL, *restore = NULL;
342 	GnmRange	 dest;
343 	gboolean change_sheets;
344 
345 	g_return_if_fail (ri != NULL);
346 	g_return_if_fail (IS_SHEET (ri->origin_sheet));
347 	g_return_if_fail (IS_SHEET (ri->target_sheet));
348 
349 	dest = ri->origin;
350 	range_translate (&dest, ri->target_sheet, ri->col_offset, ri->row_offset);
351 	change_sheets = (ri->origin_sheet != ri->target_sheet);
352 
353 	/* Clear the destination range on the target sheet */
354 	if (change_sheets) {
355 		copy = g_slist_copy (ri->target_sheet->list_merged);
356 		for (ptr = copy; ptr != NULL ; ptr = ptr->next) {
357 			GnmRange const *r = ptr->data;
358 			if (range_contains (&dest, r->start.col, r->start.row))
359 				gnm_sheet_merge_remove (ri->target_sheet, r);
360 		}
361 		g_slist_free (copy);
362 	}
363 
364 	copy = g_slist_copy (ri->origin_sheet->list_merged);
365 	for (ptr = copy; ptr != NULL ; ptr = ptr->next ) {
366 		GnmRange const *r = ptr->data;
367 		GnmRange r0 = *r; // Copy because removal invalidates r
368 		GnmRange r2 = *r;
369 		gboolean needs_restore = FALSE;
370 		gboolean needs_reapply = FALSE;
371 
372 		if (range_contains (&ri->origin, r->start.col, r->start.row)) {
373 			range_translate (&r2, ri->target_sheet,
374 					 ri->col_offset, ri->row_offset);
375 			range_ensure_sanity (&r2, ri->target_sheet);
376 
377 			gnm_sheet_merge_remove (ri->origin_sheet, r);
378 			if (range_is_singleton (&r2))
379 				needs_restore = TRUE;
380 			else if (r2.start.col <= r2.end.col &&
381 				 r2.start.row <= r2.end.row) {
382 				needs_restore = TRUE;
383 				needs_reapply = TRUE;
384 			} else {
385 				// Completely deleted.
386 			}
387 		} else if (range_contains (&ri->origin, r->end.col, r->end.row)) {
388 			r2.end.col += ri->col_offset;
389 			r2.end.row += ri->row_offset;
390 			range_ensure_sanity (&r2, ri->target_sheet);
391 			gnm_sheet_merge_remove (ri->origin_sheet, r);
392 			needs_restore = TRUE;
393 			needs_reapply = !range_is_singleton (&r2);
394 		} else if (!change_sheets &&
395 			   range_contains (&dest, r->start.col, r->start.row))
396 			gnm_sheet_merge_remove (ri->origin_sheet, r);
397 
398 		if (needs_reapply)
399 			reapply = g_slist_prepend (reapply,
400 						   gnm_range_dup (&r2));
401 		if (needs_restore && pundo)
402 			restore = g_slist_prepend (restore,
403 						   gnm_range_dup (&r0));
404 	}
405 	g_slist_free (copy);
406 
407 	// Reapply surviving, changed ranges.
408 	for (ptr = reapply ; ptr != NULL ; ptr = ptr->next) {
409 		GnmRange *dest = ptr->data;
410 		gnm_sheet_merge_add (ri->target_sheet, dest, TRUE, NULL);
411 		g_free (dest);
412 	}
413 	g_slist_free (reapply);
414 
415 	if (restore) {
416 		GOUndo *u = go_undo_binary_new
417 			(ri->origin_sheet, restore,
418 			 (GOUndoBinaryFunc)cb_restore_merge,
419 			 NULL,
420 			 (GFreeFunc)cb_restore_list_free);
421 		*pundo = go_undo_combine (*pundo, u);
422 	}
423 }
424 
425 /**
426  * gnm_sheet_merge_find_bounding_box:
427  * @sheet: sheet
428  * @r: the range to extend
429  *
430  * Extends @r such that no merged range is split by its boundary.
431  */
432 void
gnm_sheet_merge_find_bounding_box(Sheet const * sheet,GnmRange * target)433 gnm_sheet_merge_find_bounding_box (Sheet const *sheet, GnmRange *target)
434 {
435 	gboolean changed;
436 	GSList *merged, *ptr;
437 
438 	/* expand to include any merged regions */
439 	do {
440 		changed = FALSE;
441 		merged = gnm_sheet_merge_get_overlap (sheet, target);
442 		for (ptr = merged ; ptr != NULL ; ptr = ptr->next) {
443 			GnmRange const *r = ptr->data;
444 			if (target->start.col > r->start.col) {
445 				target->start.col = r->start.col;
446 				changed = TRUE;
447 			}
448 			if (target->start.row > r->start.row) {
449 				target->start.row = r->start.row;
450 				changed = TRUE;
451 			}
452 			if (target->end.col < r->end.col) {
453 				target->end.col = r->end.col;
454 				changed = TRUE;
455 			}
456 			if (target->end.row < r->end.row) {
457 				target->end.row = r->end.row;
458 				changed = TRUE;
459 			}
460 		}
461 		g_slist_free (merged);
462 	} while (changed);
463 
464 }
465