3 * sheet-merge.c: merged cell support
5 * Copyright (C) 2000-2002 Jody Goldberg (jody@gnome.org)
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.
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.
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
22 #include <gnumeric-config.h>
23 #include <glib/gi18n-lib.h>
25 #include <sheet-merge.h>
27 #include <sheet-object.h>
29 #include <sheet-view.h>
30 #include <sheet-private.h>
34 #include <sheet-style.h>
37 #include <command-context.h>
40 range_row_cmp (GnmRange
const *a
, GnmRange
const *b
)
42 int tmp
= b
->start
.row
- a
->start
.row
;
44 tmp
= a
->start
.col
- b
->start
.col
; /* YES I DO MEAN a - b */
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
56 * Add a range to the list of merge targets. Checks for array spliting returns
57 * %TRUE if there was an error. Queues a respan. Only queus a redraw if @clear
61 gnm_sheet_merge_add (Sheet
*sheet
, GnmRange
const *r
, gboolean clear
,
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
);
76 range_ensure_sanity (&r2
, sheet
);
78 if (sheet_range_splits_array (sheet
, &r2
, NULL
, cc
, _("Merge")))
81 test
= gnm_sheet_merge_get_overlap (sheet
, &r2
);
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
)));
95 sheet_redraw_range (sheet
, &r2
);
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
,
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
,
111 /* Apply the corner style to the entire region */
112 style
= gnm_style_dup (sheet_style_get (sheet
, r2
.start
.col
,
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
);
120 r_copy
= gnm_range_dup (&r2
);
121 g_hash_table_insert (sheet
->hash_merged
, &r_copy
->start
, r_copy
);
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
);
127 cell
= sheet_cell_get (sheet
, r2
.start
.col
, r2
.start
.row
);
129 cell
->base
.flags
|= GNM_CELL_IS_MERGED
;
130 cell_unregister_span (cell
);
132 sheet_queue_respan (sheet
, r2
.start
.row
, r2
.end
.row
);
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
);
141 comment
= sheet_get_comment (sheet
, &r2
.start
);
143 sheet_object_update_bounds (GNM_SO (comment
), NULL
);
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
;
150 if (sheet
->rows
.max_used
< r2
.end
.row
) {
151 sheet
->rows
.max_used
= r2
.end
.row
;
152 sheet
->priv
->resize_scrollbar
= TRUE
;
158 * gnm_sheet_merge_remove:
159 * @sheet: the sheet which will contain the region
162 * Remove a merged range.
165 * Returns: %TRUE if there was an error.
168 gnm_sheet_merge_remove (Sheet
*sheet
, GnmRange
const *r
)
174 g_return_val_if_fail (IS_SHEET (sheet
), TRUE
);
175 g_return_val_if_fail (r
!= NULL
, TRUE
);
177 r_copy
= g_hash_table_lookup (sheet
->hash_merged
, &r
->start
);
179 g_return_val_if_fail (r_copy
!= NULL
, TRUE
);
180 g_return_val_if_fail (range_equal (r
, r_copy
), TRUE
);
182 g_hash_table_remove (sheet
->hash_merged
, &r_copy
->start
);
183 sheet
->list_merged
= g_slist_remove (sheet
->list_merged
, r_copy
);
185 cell
= sheet_cell_get (sheet
, r
->start
.col
, r
->start
.row
);
187 cell
->base
.flags
&= ~GNM_CELL_IS_MERGED
;
189 comment
= sheet_get_comment (sheet
, &r
->start
);
191 sheet_object_update_bounds (GNM_SO (comment
), NULL
);
193 sheet_redraw_range (sheet
, r
);
194 sheet_flag_status_update_range (sheet
, r
);
195 SHEET_FOREACH_VIEW (sheet
, sv
, sv
->reposition_selection
= TRUE
;);
201 * gnm_sheet_merge_get_overlap:
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).
208 gnm_sheet_merge_get_overlap (Sheet
const *sheet
, GnmRange
const *range
)
210 GSList
*ptr
, *res
= NULL
;
212 g_return_val_if_fail (IS_SHEET (sheet
), NULL
);
213 g_return_val_if_fail (range
!= NULL
, NULL
);
215 for (ptr
= sheet
->list_merged
; ptr
!= NULL
; ptr
= ptr
->next
) {
216 GnmRange
* const test
= ptr
->data
;
218 if (range_overlap (range
, test
))
219 res
= g_slist_prepend (res
, test
);
226 * gnm_sheet_merge_contains_pos:
227 * @sheet: #Sheet to query
228 * @pos: Position to look for a merged range.
230 * Returns: (transfer none) (nullable): the merged range covering @pos, or
231 * %NULL if @pos is not within a merged region.
234 gnm_sheet_merge_contains_pos (Sheet
const *sheet
, GnmCellPos
const *pos
)
238 g_return_val_if_fail (IS_SHEET (sheet
), NULL
);
239 g_return_val_if_fail (pos
!= NULL
, NULL
);
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
))
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
256 * Returns the nearest regions to either side of @pos.
259 gnm_sheet_merge_get_adjacent (Sheet
const *sheet
, GnmCellPos
const *pos
,
260 GnmRange
const **left
, GnmRange
const **right
)
264 g_return_if_fail (IS_SHEET (sheet
));
265 g_return_if_fail (pos
!= NULL
);
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
;
273 g_return_if_fail (diff
!= 0);
276 if (*left
== NULL
|| (*left
)->end
.col
< test
->end
.col
)
279 if (*right
== NULL
|| (*right
)->start
.col
> test
->start
.col
)
287 * gnm_sheet_merge_is_corner:
288 * @sheet: #Sheet to query
289 * @pos: cellpos if top left corner
291 * Returns: (transfer none): a merged #GnmRange covering @pos if is the
292 * top-left corner of a merged region.
295 gnm_sheet_merge_is_corner (Sheet
const *sheet
, GnmCellPos
const *pos
)
297 g_return_val_if_fail (IS_SHEET (sheet
), NULL
);
298 g_return_val_if_fail (pos
!= NULL
, NULL
);
300 return g_hash_table_lookup (sheet
->hash_merged
, pos
);
304 cb_restore_merge (Sheet
*sheet
, GSList
*restore
)
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
,
311 if (r2
&& range_equal (r
, r2
))
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
319 gnm_sheet_merge_remove (sheet
, r2
);
321 gnm_sheet_merge_add (sheet
, r
, FALSE
, NULL
);
326 cb_restore_list_free (GSList
*restore
)
328 g_slist_free_full (restore
, g_free
);
332 * gnm_sheet_merge_relocate:
333 * @ri: Descriptor of what is moving.
334 * @pundo: (out) (optional) (transfer full): Undo information.
336 * Shifts merged regions that need to move.
339 gnm_sheet_merge_relocate (GnmExprRelocateInfo
const *ri
, GOUndo
**pundo
)
341 GSList
*ptr
, *copy
, *reapply
= NULL
, *restore
= NULL
;
343 gboolean change_sheets
;
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
));
350 range_translate (&dest
, ri
->target_sheet
, ri
->col_offset
, ri
->row_offset
);
351 change_sheets
= (ri
->origin_sheet
!= ri
->target_sheet
);
353 /* Clear the destination range on the target sheet */
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
);
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
369 gboolean needs_restore
= FALSE
;
370 gboolean needs_reapply
= FALSE
;
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
);
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
;
385 // Completely deleted.
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
);
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
));
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
);
413 g_slist_free (reapply
);
416 GOUndo
*u
= go_undo_binary_new
417 (ri
->origin_sheet
, restore
,
418 (GOUndoBinaryFunc
)cb_restore_merge
,
420 (GFreeFunc
)cb_restore_list_free
);
421 *pundo
= go_undo_combine (*pundo
, u
);
426 * gnm_sheet_merge_find_bounding_box:
428 * @r: the range to exten
430 * Extends @r such that no merged range is split by its boundary.
433 gnm_sheet_merge_find_bounding_box (Sheet
const *sheet
, GnmRange
*target
)
436 GSList
*merged
, *ptr
;
438 /* expand to include any merged regions */
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
;
448 if (target
->start
.row
> r
->start
.row
) {
449 target
->start
.row
= r
->start
.row
;
452 if (target
->end
.col
< r
->end
.col
) {
453 target
->end
.col
= r
->end
.col
;
456 if (target
->end
.row
< r
->end
.row
) {
457 target
->end
.row
= r
->end
.row
;
461 g_slist_free (merged
);