1 /*
2  * dialog-merge.c: Dialog to merge a list of data into a given range
3  *
4  *
5  * Author:
6  *	Andreas J. Guelzow <aguelzow@taliesin.ca>
7  *
8  * Copyright (C) 2002 Andreas J. Guelzow <aguelzow@taliesin.ca>
9  *
10  * This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation; either version 2 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with this program; if not, see <https://www.gnu.org/licenses/>.
22  */
23 
24 #include <gnumeric-config.h>
25 #include <glib/gi18n-lib.h>
26 #include <gnumeric.h>
27 #include <dialogs/dialogs.h>
28 #include <dialogs/help.h>
29 
30 #include <gui-util.h>
31 #include <wbc-gtk.h>
32 #include <workbook-view.h>
33 #include <workbook.h>
34 #include <sheet.h>
35 #include <ranges.h>
36 #include <value.h>
37 #include <commands.h>
38 #include <selection.h>
39 #include <widgets/gnm-expr-entry.h>
40 
41 
42 #define MERGE_KEY            "merge-dialog"
43 
44 typedef struct {
45 	WBCGtk  *wbcg;
46 	Sheet *sheet;
47 	GtkBuilder *gui;
48 	GtkWidget *dialog;
49 	GtkWidget *warning_dialog;
50 	GtkTreeView *list;
51 	GtkListStore *model;
52 	GnmExprEntry *zone;
53 	GnmExprEntry *data;
54 	GnmExprEntry *field;
55 	GtkWidget *add_btn;
56 	GtkWidget *change_btn;
57 	GtkWidget *delete_btn;
58 	GtkWidget *merge_btn;
59 	GtkWidget *cancel_btn;
60 
61 } MergeState;
62 
63 enum {
64 	DATA_RANGE,
65 	FIELD_LOCATION,
66 	NUM_COLUMNS
67 };
68 
69 static void
cb_merge_update_buttons(G_GNUC_UNUSED gpointer ignored,MergeState * state)70 cb_merge_update_buttons (G_GNUC_UNUSED gpointer ignored,
71 			 MergeState *state)
72 {
73 /* Note: ignored could be NULL or  an expr-entry. */
74 	GtkTreeIter  iter;
75 	GtkTreeSelection *selection = gtk_tree_view_get_selection (state->list);
76 	gboolean has_selection, has_data, has_work;
77 
78 	has_selection = gtk_tree_selection_get_selected (selection, NULL, &iter);
79 	has_data = gnm_expr_entry_is_cell_ref (state->data, state->sheet, TRUE)
80 		&& gnm_expr_entry_is_cell_ref (state->field, state->sheet, FALSE);
81 	has_work = (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (state->model), NULL) > 0)
82 		&& gnm_expr_entry_is_cell_ref (state->zone, state->sheet, TRUE);
83 
84 	gtk_widget_set_sensitive (state->add_btn, has_data);
85 	gtk_widget_set_sensitive (state->change_btn, has_data && has_selection);
86 	gtk_widget_set_sensitive (state->delete_btn, has_selection);
87 	gtk_widget_set_sensitive (state->merge_btn, has_work);
88 }
89 
90 static void
cb_merge_selection_changed(GtkTreeSelection * selection,MergeState * state)91 cb_merge_selection_changed (GtkTreeSelection *selection, MergeState *state)
92 {
93 	GtkTreeIter  iter;
94 	gboolean has_selection;
95 	char *data_string = NULL, *field_string = NULL;
96 
97 	has_selection = gtk_tree_selection_get_selected (selection, NULL, &iter);
98 
99 	if (has_selection) {
100 		gtk_tree_model_get (GTK_TREE_MODEL (state->model), &iter,
101 				    DATA_RANGE, &data_string,
102 				    FIELD_LOCATION, &field_string,
103 				    -1);
104 		gnm_expr_entry_load_from_text (state->data, data_string);
105 		gnm_expr_entry_load_from_text (state->field, field_string);
106 		g_free (data_string);
107 		g_free (field_string);
108 	}
109 	cb_merge_update_buttons (NULL, state);
110 }
111 
112 static void
merge_store_info_in_list(GtkTreeIter * iter,MergeState * state)113 merge_store_info_in_list (GtkTreeIter *iter, MergeState *state)
114 {
115 	char     *data_text, *field_text;
116 	GtkTreeSelection  *selection;
117 
118 	data_text = gnm_expr_entry_global_range_name (state->data, state->sheet);
119 	field_text = gnm_expr_entry_global_range_name (state->field,  state->sheet);
120 
121 	gtk_list_store_set (state->model, iter,
122 			    DATA_RANGE, data_text,
123 			    FIELD_LOCATION, field_text,
124 			    -1);
125 
126 	g_free (data_text);
127 	g_free (field_text);
128 
129 	selection = gtk_tree_view_get_selection (state->list);
130 	gtk_tree_selection_select_iter (selection, iter);
131 }
132 
133 
134 static void
cb_merge_add_clicked(G_GNUC_UNUSED GtkWidget * ignore,MergeState * state)135 cb_merge_add_clicked (G_GNUC_UNUSED GtkWidget *ignore,
136 		      MergeState *state)
137 {
138 	GtkTreeIter iter;
139 	GtkTreeIter sel_iter;
140 	GtkTreeSelection  *selection = gtk_tree_view_get_selection (state->list);
141 
142 	if (!gtk_tree_selection_get_selected (selection, NULL, &sel_iter))
143 		gtk_list_store_append (state->model, &iter);
144 	else
145 		gtk_list_store_insert_before (state->model, &iter, &sel_iter);
146 
147 	merge_store_info_in_list (&iter, state);
148 }
149 
150 static void
cb_merge_change_clicked(G_GNUC_UNUSED GtkWidget * ignore,MergeState * state)151 cb_merge_change_clicked (G_GNUC_UNUSED GtkWidget *ignore,
152 			 MergeState *state)
153 {
154 	GtkTreeIter iter;
155 	GtkTreeSelection  *selection = gtk_tree_view_get_selection (state->list);
156 
157 	if (gtk_tree_selection_get_selected (selection, NULL, &iter))
158 		merge_store_info_in_list (&iter, state);
159 }
160 
161 static void
cb_merge_delete_clicked(G_GNUC_UNUSED GtkWidget * ignore,MergeState * state)162 cb_merge_delete_clicked (G_GNUC_UNUSED GtkWidget *ignore,
163 			 MergeState *state)
164 {
165 	GtkTreeIter iter;
166 	GtkTreeSelection  *selection = gtk_tree_view_get_selection (state->list);
167 
168 	if (gtk_tree_selection_get_selected (selection, NULL, &iter)) {
169 		gtk_list_store_remove (state->model, &iter);
170 	}
171 }
172 
173 static void
cb_merge_cancel_clicked(G_GNUC_UNUSED GtkWidget * ignore,MergeState * state)174 cb_merge_cancel_clicked (G_GNUC_UNUSED GtkWidget *ignore,
175 			 MergeState *state)
176 {
177 	    gtk_widget_destroy (GTK_WIDGET (state->dialog));
178 }
179 
180 static void
cb_merge_find_shortest_column(gpointer data,gpointer lp)181 cb_merge_find_shortest_column (gpointer data, gpointer lp)
182 {
183 	GnmValue *range = data;
184 	int *length = lp;
185 	int r_length = range->v_range.cell.b.row - range->v_range.cell.a.row + 1;
186 
187 	if (r_length < *length)
188 		*length = r_length;
189 }
190 
191 static void
cb_merge_find_longest_column(gpointer data,gpointer lp)192 cb_merge_find_longest_column (gpointer data, gpointer lp)
193 {
194 	GnmValue *range = data;
195 	int *length = lp;
196 	int r_length = range->v_range.cell.b.row - range->v_range.cell.a.row + 1;
197 
198 	if (r_length > *length)
199 		*length = r_length;
200 }
201 
202 static void
cb_merge_trim_data(gpointer data,gpointer lp)203 cb_merge_trim_data (gpointer data, gpointer lp)
204 {
205 	GnmValue *range = data;
206 	int *length = lp;
207 	int r_length = range->v_range.cell.b.row - range->v_range.cell.a.row + 1;
208 
209 	if (r_length > *length)
210 		range->v_range.cell.b.row = range->v_range.cell.a.row + *length - 1;
211 	range->v_range.cell.b.col = range->v_range.cell.a.col;
212 }
213 
214 
215 static void
cb_merge_merge_clicked(G_GNUC_UNUSED GtkWidget * ignore,MergeState * state)216 cb_merge_merge_clicked (G_GNUC_UNUSED GtkWidget *ignore,
217 			MergeState *state)
218 {
219 	GtkTreeIter this_iter;
220 	gint n = 0;
221 	char *data_string = NULL, *field_string = NULL;
222 	GSList *data_list = NULL, *field_list = NULL;
223 	GnmValue *v_zone;
224 	gint field_problems = 0;
225 	gint min_length = gnm_sheet_get_max_rows (state->sheet);
226 	gint max_length = 0;
227 
228 	v_zone = gnm_expr_entry_parse_as_value (state->zone, state->sheet);
229 	g_return_if_fail (v_zone != NULL);
230 
231 	while (gtk_tree_model_iter_nth_child  (GTK_TREE_MODEL (state->model),
232 					       &this_iter, NULL, n)) {
233 		GnmValue *v_data, *v_field;
234 		gtk_tree_model_get (GTK_TREE_MODEL (state->model), &this_iter,
235 				    DATA_RANGE, &data_string,
236 				    FIELD_LOCATION, &field_string,
237 				    -1);
238 		v_data = value_new_cellrange_str (state->sheet, data_string);
239 		v_field = value_new_cellrange_str (state->sheet, field_string);
240 		g_free (data_string);
241 		g_free (field_string);
242 
243 		g_return_if_fail (v_data != NULL && v_field != NULL);
244 		if (!global_range_contained (state->sheet, v_field, v_zone))
245 			field_problems++;
246 		data_list = g_slist_prepend (data_list, v_data);
247 		field_list = g_slist_prepend (field_list, v_field);
248 		n++;
249 	}
250 
251 	if (field_problems > 0) {
252 		char *text;
253 		if (field_problems == 1)
254 			text = g_strdup (_("One field is not part of the merge zone!"));
255 		else
256 			text = g_strdup_printf (_("%i fields are not part of the merge zone!"),
257 						field_problems);
258 		go_gtk_notice_nonmodal_dialog ((GtkWindow *) state->dialog,
259 					       &(state->warning_dialog),
260 					       GTK_MESSAGE_ERROR,
261 					       "%s", text);
262 		g_free (text);
263 		value_release (v_zone);
264 		range_list_destroy (data_list);
265 		range_list_destroy (field_list);
266 		return;
267 	}
268 
269 	g_slist_foreach (data_list, cb_merge_find_shortest_column, &min_length);
270 	g_slist_foreach (data_list, cb_merge_find_longest_column, &max_length);
271 
272 	if (min_length < max_length) {
273 		char *text = g_strdup_printf (_("The data columns range in length from "
274 						"%i to %i. Shall we trim the lengths to "
275 						"%i and proceed?"), min_length, max_length,
276 					      min_length);
277 
278 		if (go_gtk_query_yes_no (GTK_WINDOW (state->dialog), TRUE,
279 					 "%s", text)) {
280 			g_slist_foreach (data_list, cb_merge_trim_data, &min_length);
281 			g_free (text);
282 		} else {
283 			g_free (text);
284 			value_release (v_zone);
285 			range_list_destroy (data_list);
286 			range_list_destroy (field_list);
287 			return;
288 		}
289 
290 	}
291 
292 	if (!cmd_merge_data (GNM_WBC (state->wbcg), state->sheet,
293 			     v_zone, field_list, data_list))
294 		gtk_widget_destroy (state->dialog);
295 }
296 
297 static void
cb_merge_destroy(MergeState * state)298 cb_merge_destroy (MergeState *state)
299 {
300 	if (state->model != NULL)
301 		g_object_unref (state->model);
302 	if (state->gui != NULL)
303 		g_object_unref (state->gui);
304 	g_free (state);
305 }
306 
307 void
dialog_merge(WBCGtk * wbcg)308 dialog_merge (WBCGtk *wbcg)
309 {
310 	MergeState *state;
311 	GtkBuilder *gui;
312 	GtkGrid *grid;
313 	GtkWidget *scrolled;
314 	GtkTreeViewColumn *column;
315 	GtkTreeSelection  *selection;
316 	GnmRange const *r;
317 
318 	g_return_if_fail (wbcg != NULL);
319 
320 	if (gnm_dialog_raise_if_exists (wbcg, MERGE_KEY))
321 		return;
322 	gui = gnm_gtk_builder_load ("res:ui/merge.ui", NULL, GO_CMD_CONTEXT (wbcg));
323         if (gui == NULL)
324                 return;
325 
326 	state = g_new0 (MergeState, 1);
327 	state->gui = gui;
328 	state->wbcg = wbcg;
329 	state->sheet = wb_control_cur_sheet (GNM_WBC (state->wbcg));
330 	state->dialog     = go_gtk_builder_get_widget (gui, "Merge");
331 	state->warning_dialog = NULL;
332 
333 	state->add_btn   = go_gtk_builder_get_widget (gui, "add_button");
334 	state->delete_btn   = go_gtk_builder_get_widget (gui, "remove_button");
335 	state->merge_btn  = go_gtk_builder_get_widget (gui, "merge_button");
336 	state->change_btn  = go_gtk_builder_get_widget (gui, "change_button");
337 	state->cancel_btn  = go_gtk_builder_get_widget (gui, "cancel_button");
338 	gtk_widget_set_size_request (state->delete_btn, 100, -1);
339 
340 	gtk_button_set_alignment (GTK_BUTTON (state->add_btn),    0., .5);
341 	gtk_button_set_alignment (GTK_BUTTON (state->delete_btn), 0., .5);
342 	gtk_button_set_alignment (GTK_BUTTON (state->change_btn), 0., .5);
343 
344 	grid = GTK_GRID (go_gtk_builder_get_widget (gui, "main-grid"));
345 	state->zone = gnm_expr_entry_new (wbcg, TRUE);
346 	gnm_expr_entry_set_flags (state->zone, GNM_EE_SINGLE_RANGE, GNM_EE_MASK);
347 	gnm_editable_enters (GTK_WINDOW (state->dialog),
348 				  GTK_WIDGET (state->zone));
349 	gtk_label_set_mnemonic_widget (GTK_LABEL (go_gtk_builder_get_widget (gui, "var1-label")),
350 				       GTK_WIDGET (state->zone));
351 	gtk_widget_set_hexpand (GTK_WIDGET (state->zone), TRUE);
352 	gtk_grid_attach (grid, GTK_WIDGET (state->zone), 1, 0, 2, 1);
353 	r = selection_first_range (
354 		wb_control_cur_sheet_view (GNM_WBC (wbcg)), NULL, NULL);
355 	if (r != NULL)
356 		gnm_expr_entry_load_from_range (state->zone,
357 			state->sheet, r);
358 
359 	state->data = gnm_expr_entry_new (wbcg, TRUE);
360 	gnm_expr_entry_set_flags (state->data, GNM_EE_SINGLE_RANGE, GNM_EE_MASK);
361 	gtk_widget_set_hexpand (GTK_WIDGET (state->data), TRUE);
362 	gtk_grid_attach (grid, GTK_WIDGET (state->data), 0, 5, 1, 1);
363 
364 	state->field = gnm_expr_entry_new (wbcg, TRUE);
365 	gnm_expr_entry_set_flags (state->field, GNM_EE_SINGLE_RANGE, GNM_EE_MASK);
366 	gtk_widget_set_hexpand (GTK_WIDGET (state->field), TRUE);
367 	gtk_grid_attach (grid, GTK_WIDGET (state->field), 1, 5, 1, 1);
368 
369 	scrolled = go_gtk_builder_get_widget (state->gui, "scrolled");
370 	state->model = gtk_list_store_new (NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING);
371 	state->list = GTK_TREE_VIEW (gtk_tree_view_new_with_model
372 					   (GTK_TREE_MODEL (state->model)));
373 	selection = gtk_tree_view_get_selection (state->list);
374 	gtk_tree_selection_set_mode (selection, GTK_SELECTION_BROWSE);
375 
376 	column = gtk_tree_view_column_new_with_attributes (_("Input Data"),
377 							   gtk_cell_renderer_text_new (),
378 							   "text", DATA_RANGE,
379 							   NULL);
380 	gtk_tree_view_column_set_sort_column_id (column, DATA_RANGE);
381 	gtk_tree_view_column_set_min_width (column, 150);
382 	gtk_tree_view_append_column (state->list, column);
383 	column = gtk_tree_view_column_new_with_attributes (_("Merge Field"),
384 							   gtk_cell_renderer_text_new (),
385 							   "text", FIELD_LOCATION,
386 							   NULL);
387 	gtk_tree_view_column_set_sort_column_id (column, FIELD_LOCATION);
388 	gtk_tree_view_column_set_min_width (column, 100);
389 	gtk_tree_view_append_column (state->list, column);
390 
391 	gtk_tree_view_set_headers_clickable (state->list, TRUE);
392 	gtk_container_add (GTK_CONTAINER (scrolled), GTK_WIDGET (state->list));
393 
394 	cb_merge_update_buttons (NULL, state);
395 	g_signal_connect (selection,
396 		"changed",
397 		G_CALLBACK (cb_merge_selection_changed), state);
398 
399 	g_signal_connect_after (G_OBJECT (state->zone),
400 		"changed",
401 		G_CALLBACK (cb_merge_update_buttons), state);
402 	g_signal_connect_after (G_OBJECT (state->data),
403 		"changed",
404 		G_CALLBACK (cb_merge_update_buttons), state);
405 	g_signal_connect_after (G_OBJECT (state->field),
406 		"changed",
407 		G_CALLBACK (cb_merge_update_buttons), state);
408 
409 	g_signal_connect (G_OBJECT (state->add_btn),
410 		"clicked",
411 		G_CALLBACK (cb_merge_add_clicked), state);
412 	g_signal_connect (G_OBJECT (state->change_btn),
413 		"clicked",
414 		G_CALLBACK (cb_merge_change_clicked), state);
415 	g_signal_connect (G_OBJECT (state->delete_btn),
416 		"clicked",
417 		G_CALLBACK (cb_merge_delete_clicked), state);
418 	g_signal_connect (G_OBJECT (state->merge_btn),
419 		"clicked",
420 		G_CALLBACK (cb_merge_merge_clicked), state);
421 	g_signal_connect (G_OBJECT (state->cancel_btn),
422 		"clicked",
423 		G_CALLBACK (cb_merge_cancel_clicked), state);
424 
425 	gnm_dialog_setup_destroy_handlers (GTK_DIALOG (state->dialog),
426 					   state->wbcg,
427 					   GNM_DIALOG_DESTROY_CURRENT_SHEET_REMOVED);
428 
429 	gnm_init_help_button (
430 		go_gtk_builder_get_widget (state->gui, "help_button"),
431 		GNUMERIC_HELP_LINK_DATA_MERGE);
432 
433 	gnm_keyed_dialog (state->wbcg, GTK_WINDOW (state->dialog),
434 			       MERGE_KEY);
435 
436 	/* a candidate for merging into attach guru */
437 	g_object_set_data_full (G_OBJECT (state->dialog),
438 		"state", state, (GDestroyNotify) cb_merge_destroy);
439 	go_gtk_nonmodal_dialog (wbcg_toplevel (state->wbcg),
440 				   GTK_WINDOW (state->dialog));
441 	wbc_gtk_attach_guru (state->wbcg, GTK_WIDGET (state->dialog));
442 	gtk_widget_show_all (GTK_WIDGET (state->dialog));
443 }
444