1 
2 /*
3  * dialog-goto-cell.c:  Implements the "goto cell/navigator" functionality
4  *
5  * Author:
6  * Andreas J. Guelzow <aguelzow@pyrshep.ca>
7  *
8  * Copyright (C) Andreas J. Guelzow <aguelzow@pyrshep.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 <workbook-control.h>
32 #include <ranges.h>
33 #include <value.h>
34 #include <expr-name.h>
35 #include <expr.h>
36 #include <sheet.h>
37 #include <workbook.h>
38 #include <workbook-view.h>
39 #include <workbook-control.h>
40 #include <selection.h>
41 #include <parse-util.h>
42 #include <sheet-view.h>
43 
44 #include <wbc-gtk.h>
45 
46 
47 #define GOTO_KEY "goto-dialog"
48 
49 typedef struct {
50 	WBCGtk  *wbcg;
51 	Workbook  *wb;
52 
53 	GtkBuilder *gui;
54 	GtkWidget *dialog;
55 	GtkWidget *close_button;
56 	GtkWidget *go_button;
57 	GtkEntry *goto_text;
58 
59 	GtkSpinButton *spin_rows, *spin_cols;
60 
61 	GtkTreeStore  *model;
62 	GtkTreeView   *treeview;
63 	GtkTreeSelection   *selection;
64 
65 	gulong sheet_order_changed_listener;
66 	gulong sheet_added_listener;
67 	gulong sheet_deleted_listener;
68 } GotoState;
69 
70 enum {
71 	ITEM_NAME,
72 	SHEET_NAME,
73 	SHEET_POINTER,
74 	EXPRESSION,
75 	NUM_COLUMNS
76 };
77 
78 static void
cb_dialog_goto_free(GotoState * state)79 cb_dialog_goto_free (GotoState  *state)
80 {
81 	if (state->sheet_order_changed_listener)
82 		g_signal_handler_disconnect (G_OBJECT (state->wb),
83 					     state->sheet_order_changed_listener);
84 	if (state->sheet_added_listener)
85 		g_signal_handler_disconnect (G_OBJECT (state->wb),
86 					     state->sheet_added_listener);
87 	if (state->sheet_deleted_listener)
88 		g_signal_handler_disconnect (G_OBJECT (state->wb),
89 					     state->sheet_deleted_listener);
90 
91 	if (state->gui != NULL)
92 		g_object_unref (state->gui);
93 	if (state->model != NULL)
94 		g_object_unref (state->model);
95 	g_free (state);
96 }
97 
98 static void
cb_dialog_goto_close_clicked(G_GNUC_UNUSED GtkWidget * button,GotoState * state)99 cb_dialog_goto_close_clicked (G_GNUC_UNUSED GtkWidget *button,
100 			      GotoState *state)
101 {
102 	gtk_widget_destroy (state->dialog);
103 }
104 
105 static GnmValue *
dialog_goto_get_val(GotoState * state)106 dialog_goto_get_val (GotoState *state)
107 {
108 	char const *text = gtk_entry_get_text (state->goto_text);
109 	Sheet *sheet = wb_control_cur_sheet (GNM_WBC (state->wbcg));
110 	GnmValue *val = value_new_cellrange_str (sheet, text);
111 
112 	if (val == NULL) {
113 		GnmParsePos pp;
114 		GnmNamedExpr *nexpr = expr_name_lookup
115 			(parse_pos_init_sheet (&pp, sheet), text);
116 		if (nexpr != NULL && !expr_name_is_placeholder (nexpr)) {
117 			val = gnm_expr_top_get_range (nexpr->texpr);
118 		}
119 	}
120 	return val;
121 }
122 
123 static void
cb_dialog_goto_go_clicked(G_GNUC_UNUSED GtkWidget * button,GotoState * state)124 cb_dialog_goto_go_clicked (G_GNUC_UNUSED GtkWidget *button,
125 			   GotoState *state)
126 {
127 	GnmEvalPos ep;
128 	GnmRangeRef range;
129 	gint cols = gtk_spin_button_get_value_as_int (state->spin_cols);
130 	gint rows = gtk_spin_button_get_value_as_int (state->spin_rows);
131 	GnmValue *val = dialog_goto_get_val (state);
132 	Sheet *sheet = wb_control_cur_sheet (GNM_WBC (state->wbcg));
133 
134 	if (val == NULL)
135 		return;
136 
137 	val->v_range.cell.b.row = val->v_range.cell.a.row + (rows - 1);
138 	val->v_range.cell.b.col = val->v_range.cell.a.col + (cols - 1);
139 	eval_pos_init_sheet (&ep, sheet);
140 	gnm_cellref_make_abs (&range.a, &val->v_range.cell.a, &ep);
141 	gnm_cellref_make_abs (&range.b, &val->v_range.cell.b, &ep);
142 	value_release (val);
143 
144 	wb_control_jump (GNM_WBC (state->wbcg), sheet, &range);
145 	return;
146 }
147 
148 static void
cb_dialog_goto_update_sensitivity(G_GNUC_UNUSED GtkWidget * dummy,GotoState * state)149 cb_dialog_goto_update_sensitivity (G_GNUC_UNUSED GtkWidget *dummy,
150 				   GotoState *state)
151 {
152 	GnmValue *val = dialog_goto_get_val (state);
153 	if (val != NULL) {
154 		gint cols, rows;
155 		Sheet *sheet = val->v_range.cell.a.sheet;
156 		GnmSheetSize const *ssz;
157 
158 		if (sheet == NULL)
159 			sheet = wb_control_cur_sheet (GNM_WBC (state->wbcg));
160 		ssz = gnm_sheet_get_size (sheet);
161 
162 		cols = ssz->max_cols;
163 		rows = ssz->max_rows;
164 
165 		if (val->v_range.cell.a.sheet != NULL &&
166 		    val->v_range.cell.b.sheet != NULL &&
167 		    val->v_range.cell.a.sheet != val->v_range.cell.b.sheet) {
168 			ssz = gnm_sheet_get_size (sheet);
169 			if (cols > ssz->max_cols)
170 				cols = ssz->max_cols;
171 			if (rows > ssz->max_rows)
172 				cols = ssz->max_rows;
173 		}
174 		cols -= val->v_range.cell.a.col;
175 		rows -= val->v_range.cell.a.row;
176 
177 		if (cols < 1) cols = 1;
178 		if (rows < 1) rows = 1;
179 
180 		gtk_spin_button_set_range (state->spin_cols, 1, cols);
181 		gtk_spin_button_set_range (state->spin_rows, 1, rows);
182 
183 		gtk_widget_set_sensitive (state->go_button, TRUE);
184 
185 		value_release (val);
186 	} else
187 		gtk_widget_set_sensitive (state->go_button, FALSE);
188 	gtk_entry_set_activates_default (state->goto_text, (val != NULL));
189 }
190 
191 typedef struct {
192 	GtkTreeIter  iter;
193 	GotoState   *state;
194 } LoadNames;
195 
196 static void
cb_load_names(G_GNUC_UNUSED gpointer key,GnmNamedExpr * nexpr,LoadNames * user)197 cb_load_names (G_GNUC_UNUSED gpointer key,
198 	       GnmNamedExpr *nexpr, LoadNames *user)
199 {
200 	GtkTreeIter iter;
201 	gboolean is_address = gnm_expr_top_is_rangeref (nexpr->texpr);
202 
203 	if (expr_name_is_placeholder (nexpr))
204 		return;
205 
206 	if (is_address) {
207 		gtk_tree_store_append (user->state->model, &iter, &user->iter);
208 		gtk_tree_store_set (user->state->model, &iter,
209 				    ITEM_NAME, expr_name_name (nexpr),
210 				    SHEET_POINTER, nexpr->pos.sheet,
211 				    EXPRESSION,	nexpr,
212 				    -1);
213 	}
214 }
215 
216 static void
dialog_goto_load_names(GotoState * state)217 dialog_goto_load_names (GotoState *state)
218 {
219 	Sheet *sheet;
220 	LoadNames closure;
221 	int i, l;
222 
223 	gtk_tree_store_clear (state->model);
224 
225 	closure.state = state;
226 	gtk_tree_store_append (state->model, &closure.iter, NULL);
227 	gtk_tree_store_set (state->model, &closure.iter,
228 			    SHEET_NAME,		_("Workbook Level"),
229 			    ITEM_NAME,		NULL,
230 			    SHEET_POINTER,	NULL,
231 			    EXPRESSION,		NULL,
232 			    -1);
233 	workbook_foreach_name (state->wb, FALSE,
234 			       (GHFunc)cb_load_names, &closure);
235 
236 	l = workbook_sheet_count (state->wb);
237 	for (i = 0; i < l; i++) {
238 		sheet = workbook_sheet_by_index (state->wb, i);
239 		gtk_tree_store_append (state->model, &closure.iter, NULL);
240 		gtk_tree_store_set (state->model, &closure.iter,
241 				    SHEET_NAME,		sheet->name_unquoted,
242 				    ITEM_NAME,		NULL,
243 				    SHEET_POINTER,	sheet,
244 				    EXPRESSION,		NULL,
245 				    -1);
246 	}
247 }
248 
249 static void
cb_dialog_goto_selection_changed(GtkTreeSelection * the_selection,GotoState * state)250 cb_dialog_goto_selection_changed (GtkTreeSelection *the_selection, GotoState *state)
251 {
252 	GtkTreeIter  iter;
253 	GtkTreeModel *model;
254 	Sheet        *sheet;
255 	GnmNamedExpr *name;
256 
257 	if (gtk_tree_selection_get_selected (the_selection, &model, &iter)) {
258 		gtk_tree_model_get (model, &iter,
259 				    SHEET_POINTER, &sheet,
260 				    EXPRESSION, &name,
261 				    -1);
262 		if (name && gnm_expr_top_is_rangeref (name->texpr)) {
263 			GnmParsePos pp;
264 			char *where_to;
265 
266 			if (NULL == sheet)
267 				sheet = wb_control_cur_sheet ( GNM_WBC (state->wbcg));
268 
269 			parse_pos_init_sheet (&pp, sheet);
270 			where_to = expr_name_as_string  (name, &pp, gnm_conventions_default);
271 			if (wb_control_parse_and_jump (GNM_WBC (state->wbcg), where_to))
272 				gtk_entry_set_text (state->goto_text,
273 						    where_to);
274 			g_free (where_to);
275 			return;
276 		}
277 		if (sheet)
278 			wb_view_sheet_focus (
279 				wb_control_view (GNM_WBC (state->wbcg)), sheet);
280 	}
281 }
282 
283 
284 static void
cb_sheet_order_changed(G_GNUC_UNUSED Workbook * wb,GotoState * state)285 cb_sheet_order_changed (G_GNUC_UNUSED Workbook *wb, GotoState *state)
286 {
287 	dialog_goto_load_names (state);
288 }
289 
290 static void
cb_sheet_deleted(G_GNUC_UNUSED Workbook * wb,GotoState * state)291 cb_sheet_deleted (G_GNUC_UNUSED Workbook *wb, GotoState *state)
292 {
293 	dialog_goto_load_names (state);
294 	cb_dialog_goto_update_sensitivity (NULL, state);
295 }
296 
297 static void
cb_sheet_added(G_GNUC_UNUSED Workbook * wb,GotoState * state)298 cb_sheet_added (G_GNUC_UNUSED Workbook *wb, GotoState *state)
299 {
300 	dialog_goto_load_names (state);
301 	cb_dialog_goto_update_sensitivity (NULL, state);
302 }
303 
304 static void
dialog_goto_load_selection(GotoState * state)305 dialog_goto_load_selection (GotoState *state)
306 {
307 	SheetView *sv = wb_control_cur_sheet_view
308 		(GNM_WBC (state->wbcg));
309 	GnmRange const *first = selection_first_range (sv, NULL, NULL);
310 
311 	if (first != NULL) {
312 		gint rows = range_height (first);
313 		gint cols = range_width (first);
314 		GnmConventionsOut out;
315 		GString *str = g_string_new (NULL);
316 		GnmParsePos pp;
317 		GnmRangeRef rr;
318 
319 		out.accum = str;
320 		out.pp = parse_pos_init_sheet (&pp, sv->sheet);
321 		out.convs = sheet_get_conventions (sv->sheet);
322 		gnm_cellref_init (&rr.a, NULL, first->start.col,
323 				  first->start.row, TRUE);
324 		gnm_cellref_init (&rr.b, NULL, first->start.col,
325 				  first->start.row, TRUE);
326 		rangeref_as_string (&out, &rr);
327 		gtk_entry_set_text (state->goto_text, str->str);
328 		gtk_editable_select_region (GTK_EDITABLE (state->goto_text),
329 					    0, -1);
330 		g_string_free (str, TRUE);
331 		cb_dialog_goto_update_sensitivity (NULL, state);
332 		gtk_spin_button_set_value (state->spin_rows, rows);
333 		gtk_spin_button_set_value (state->spin_cols, cols);
334 	} else
335 		cb_dialog_goto_update_sensitivity (NULL, state);
336 
337 }
338 
339 /**
340  * dialog_goto_init:
341  * @state:
342  *
343  * Create the dialog (guru).
344  **/
345 static gboolean
dialog_goto_init(GotoState * state)346 dialog_goto_init (GotoState *state)
347 {
348 	GtkGrid *grid;
349 	GtkWidget *scrolled;
350 	GtkTreeViewColumn *column;
351 
352 	grid = GTK_GRID (go_gtk_builder_get_widget (state->gui, "names"));
353 	state->goto_text = GTK_ENTRY (gtk_entry_new ());
354 	gtk_widget_set_hexpand (GTK_WIDGET (state->goto_text), TRUE);
355 	gtk_grid_attach (grid, GTK_WIDGET (state->goto_text), 0, 2, 1, 1);
356 	g_signal_connect_after (G_OBJECT (state->goto_text),
357 		"changed",
358 		G_CALLBACK (cb_dialog_goto_update_sensitivity), state);
359 
360 	state->spin_rows = GTK_SPIN_BUTTON
361 		(go_gtk_builder_get_widget (state->gui, "spin-rows"));
362 	state->spin_cols = GTK_SPIN_BUTTON
363 		(go_gtk_builder_get_widget (state->gui, "spin-columns"));
364 
365 	/* Set-up treeview */
366 	scrolled = go_gtk_builder_get_widget (state->gui, "scrolled");
367 	gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled),
368 					     GTK_SHADOW_ETCHED_IN);
369 
370 	state->model = gtk_tree_store_new (NUM_COLUMNS, G_TYPE_STRING,
371 					   G_TYPE_STRING, G_TYPE_POINTER,
372 					   G_TYPE_POINTER);
373 	state->treeview = GTK_TREE_VIEW (
374 		gtk_tree_view_new_with_model (GTK_TREE_MODEL (state->model)));
375 	state->selection = gtk_tree_view_get_selection (state->treeview);
376 	gtk_tree_selection_set_mode (state->selection, GTK_SELECTION_BROWSE);
377 	g_signal_connect (state->selection,
378 		"changed",
379 		G_CALLBACK (cb_dialog_goto_selection_changed), state);
380 
381 	column = gtk_tree_view_column_new_with_attributes (_("Sheet"),
382 							   gtk_cell_renderer_text_new (),
383 							   "text", SHEET_NAME, NULL);
384 	gtk_tree_view_column_set_sort_column_id (column, SHEET_NAME);
385 	gtk_tree_view_append_column (state->treeview, column);
386 
387 	column = gtk_tree_view_column_new_with_attributes (_("Cell"),
388 							   gtk_cell_renderer_text_new (),
389 							   "text", ITEM_NAME, NULL);
390 	gtk_tree_view_column_set_sort_column_id (column, ITEM_NAME);
391 	gtk_tree_view_append_column (state->treeview, column);
392 	gtk_tree_view_set_headers_visible (state->treeview, TRUE);
393 	gtk_container_add (GTK_CONTAINER (scrolled), GTK_WIDGET (state->treeview));
394 	dialog_goto_load_names (state);
395 	/* Finished set-up of treeview */
396 
397 	/* Listen for sheet changes */
398 	state->sheet_order_changed_listener = g_signal_connect (G_OBJECT (state->wb),
399 		"sheet_order_changed", G_CALLBACK (cb_sheet_order_changed),
400 		state);
401 	state->sheet_added_listener = g_signal_connect (G_OBJECT (state->wb),
402 		"sheet_added", G_CALLBACK (cb_sheet_added),
403 		state);
404 	state->sheet_deleted_listener = g_signal_connect (G_OBJECT (state->wb),
405 		"sheet_deleted", G_CALLBACK (cb_sheet_deleted),
406 		state);
407 
408 	state->close_button  = go_gtk_builder_get_widget (state->gui, "close_button");
409 	g_signal_connect (G_OBJECT (state->close_button),
410 		"clicked",
411 		G_CALLBACK (cb_dialog_goto_close_clicked), state);
412 
413 	state->go_button  = go_gtk_builder_get_widget (state->gui, "go_button");
414 	g_signal_connect (G_OBJECT (state->go_button),
415 		"clicked",
416 		G_CALLBACK (cb_dialog_goto_go_clicked), state);
417 	gtk_window_set_default (GTK_WINDOW (state->dialog), state->go_button);
418 
419 	gnm_init_help_button (
420 		go_gtk_builder_get_widget (state->gui, "help_button"),
421 		GNUMERIC_HELP_LINK_GOTO_CELL);
422 
423 	dialog_goto_load_selection (state);
424 
425 	wbc_gtk_attach_guru (state->wbcg, state->dialog);
426 	g_object_set_data_full (G_OBJECT (state->dialog),
427 		"state", state, (GDestroyNotify) cb_dialog_goto_free);
428 
429 	return FALSE;
430 }
431 
432 
433 void
dialog_goto_cell(WBCGtk * wbcg)434 dialog_goto_cell (WBCGtk *wbcg)
435 {
436 	GotoState* state;
437 	GtkBuilder *gui;
438 
439 	g_return_if_fail (wbcg != NULL);
440 
441 	if (gnm_dialog_raise_if_exists (wbcg, GOTO_KEY))
442 		return;
443 	gui = gnm_gtk_builder_load ("res:ui/goto.ui", NULL, GO_CMD_CONTEXT (wbcg));
444         if (gui == NULL)
445                 return;
446 
447 	state = g_new (GotoState, 1);
448 	state->wbcg   = wbcg;
449 	state->wb     = wb_control_get_workbook (GNM_WBC (wbcg));
450 	state->gui    = gui;
451         state->dialog = go_gtk_builder_get_widget (state->gui, "goto_dialog");
452 
453 	if (dialog_goto_init (state)) {
454 		go_gtk_notice_dialog (wbcg_toplevel (wbcg), GTK_MESSAGE_ERROR,
455 				 _("Could not create the goto dialog."));
456 		g_free (state);
457 		return;
458 	}
459 
460 	gnm_keyed_dialog (state->wbcg, GTK_WINDOW (state->dialog),
461 			       GOTO_KEY);
462 
463 	gtk_widget_show_all (state->dialog);
464 }
465