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