1 /*
2 * dialog-search.c:
3 * Dialog for entering a search query.
4 *
5 * Author:
6 * Morten Welinder (terra@gnome.org)
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
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.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, see <https://www.gnu.org/licenses/>.
20 */
21
22 #include <gnumeric-config.h>
23 #include <glib/gi18n-lib.h>
24 #include <gnumeric.h>
25 #include <dialogs/dialogs.h>
26 #include <dialogs/help.h>
27
28 #include <gui-util.h>
29 #include <gnumeric-conf.h>
30 #include <search.h>
31 #include <sheet.h>
32 #include <sheet-view.h>
33 #include <workbook.h>
34 #include <workbook-view.h>
35 #include <selection.h>
36 #include <cell.h>
37 #include <value.h>
38 #include <parse-util.h>
39 #include <wbc-gtk.h>
40 #include <sheet-object-cell-comment.h>
41 #include <selection.h>
42
43 #include <widgets/gnm-expr-entry.h>
44 #include <string.h>
45
46 #define SEARCH_KEY "search-dialog"
47
48 #undef USE_GURU
49
50 enum {
51 COL_SHEET = 0,
52 COL_CELL,
53 COL_TYPE,
54 COL_CONTENTS
55 };
56
57 enum {
58 ITEM_MATCH
59 };
60
61 typedef struct {
62 WBCGtk *wbcg;
63
64 GtkBuilder *gui;
65 GtkDialog *dialog;
66 GnmExprEntry *rangetext;
67 GtkEntry *gentry;
68 GtkWidget *prev_button, *next_button;
69 GtkNotebook *notebook;
70 int notebook_matches_page;
71
72 GtkTreeView *matches_table;
73 GPtrArray *matches;
74 } DialogState;
75
76 static const char * const search_type_group[] = {
77 "search_type_text",
78 "search_type_regexp",
79 "search_type_number",
80 NULL
81 };
82
83 static const char * const scope_group[] = {
84 "scope_workbook",
85 "scope_sheet",
86 "scope_range",
87 NULL
88 };
89
90 static const char * const direction_group[] = {
91 "row_major",
92 "column_major",
93 NULL
94 };
95
96 /* ------------------------------------------------------------------------- */
97
98 static GtkTreeModel *
make_matches_model(DialogState * dd)99 make_matches_model (DialogState *dd)
100 {
101 GtkListStore *list_store = gtk_list_store_new (1, G_TYPE_POINTER);
102 unsigned ui;
103 GPtrArray *matches = dd->matches;
104
105 for (ui = 0; ui < matches->len; ui++) {
106 GtkTreeIter iter;
107
108 gtk_list_store_append (list_store, &iter);
109 gtk_list_store_set (list_store, &iter,
110 ITEM_MATCH, g_ptr_array_index (matches, ui),
111 -1);
112 }
113
114 return GTK_TREE_MODEL (list_store);
115 }
116
117 static void
free_state(DialogState * dd)118 free_state (DialogState *dd)
119 {
120 gnm_search_filter_matching_free (dd->matches);
121 g_object_unref (dd->gui);
122 memset (dd, 0, sizeof (*dd));
123 g_free (dd);
124 }
125
126 static gboolean
range_focused(G_GNUC_UNUSED GtkWidget * widget,G_GNUC_UNUSED GdkEventFocus * event,DialogState * dd)127 range_focused (G_GNUC_UNUSED GtkWidget *widget,
128 G_GNUC_UNUSED GdkEventFocus *event,
129 DialogState *dd)
130 {
131 GtkWidget *scope_range = go_gtk_builder_get_widget (dd->gui, "scope_range");
132 gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (scope_range), TRUE);
133 return FALSE;
134 }
135
136 static gboolean
is_checked(GtkBuilder * gui,const char * name)137 is_checked (GtkBuilder *gui, const char *name)
138 {
139 GtkWidget *w = go_gtk_builder_get_widget (gui, name);
140 return gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (w));
141 }
142
143 static void
dialog_search_save_in_prefs(DialogState * dd)144 dialog_search_save_in_prefs (DialogState *dd)
145 {
146 GtkBuilder *gui = dd->gui;
147
148 #define SETW(w,f) f (is_checked (gui, w));
149 SETW("search_expr", gnm_conf_set_searchreplace_change_cell_expressions);
150 SETW("search_other", gnm_conf_set_searchreplace_change_cell_other);
151 SETW("search_string", gnm_conf_set_searchreplace_change_cell_strings);
152 SETW("search_comments", gnm_conf_set_searchreplace_change_comments);
153 SETW("search_expr_results", gnm_conf_set_searchreplace_search_results);
154 SETW("ignore_case", gnm_conf_set_searchreplace_ignore_case);
155 SETW("match_words", gnm_conf_set_searchreplace_whole_words_only);
156 SETW("column_major", gnm_conf_set_searchreplace_columnmajor);
157 #undef SETW
158
159 gnm_conf_set_searchreplace_regex
160 (go_gtk_builder_group_value (gui, search_type_group));
161 gnm_conf_set_searchreplace_scope
162 (go_gtk_builder_group_value (gui, scope_group));
163 }
164
165
166 static void
cursor_change(GtkTreeView * tree_view,DialogState * dd)167 cursor_change (GtkTreeView *tree_view, DialogState *dd)
168 {
169 int matchno;
170 int lastmatch = dd->matches->len - 1;
171 GtkTreePath *path;
172
173 gtk_tree_view_get_cursor (tree_view, &path, NULL);
174 if (path) {
175 matchno = gtk_tree_path_get_indices (path)[0];
176 gtk_tree_path_free (path);
177 } else {
178 matchno = -1;
179 }
180
181 gtk_widget_set_sensitive (dd->prev_button, matchno > 0);
182 gtk_widget_set_sensitive (dd->next_button,
183 matchno >= 0 && matchno < lastmatch);
184
185 if (matchno >= 0 && matchno <= lastmatch) {
186 GnmSearchFilterResult *item = g_ptr_array_index (dd->matches, matchno);
187 int col = item->ep.eval.col;
188 int row = item->ep.eval.row;
189 WorkbookControl *wbc = GNM_WBC (dd->wbcg);
190 WorkbookView *wbv = wb_control_view (wbc);
191 SheetView *sv;
192
193 if (!sheet_is_visible (item->ep.sheet))
194 return;
195
196 if (wb_control_cur_sheet (wbc) != item->ep.sheet)
197 wb_view_sheet_focus (wbv, item->ep.sheet);
198 sv = wb_view_cur_sheet_view (wbv);
199 gnm_sheet_view_set_edit_pos (sv, &item->ep.eval);
200 sv_selection_set (sv, &item->ep.eval, col, row, col, row);
201 gnm_sheet_view_make_cell_visible (sv, col, row, FALSE);
202 gnm_sheet_view_update (sv);
203 }
204 }
205
206
207 static void
search_clicked(G_GNUC_UNUSED GtkWidget * widget,DialogState * dd)208 search_clicked (G_GNUC_UNUSED GtkWidget *widget, DialogState *dd)
209 {
210 GtkBuilder *gui = dd->gui;
211 WBCGtk *wbcg = dd->wbcg;
212 WorkbookControl *wbc = GNM_WBC (wbcg);
213 GnmSearchReplace *sr;
214 char *err;
215 int i;
216 GnmSearchReplaceScope scope;
217 char *text;
218 gboolean is_regexp, is_number;
219
220 i = go_gtk_builder_group_value (gui, scope_group);
221 scope = (i == -1) ? GNM_SRS_SHEET : (GnmSearchReplaceScope)i;
222
223 i = go_gtk_builder_group_value (gui, search_type_group);
224 is_regexp = (i == 1);
225 is_number = (i == 2);
226
227 text = gnm_search_normalize (gtk_entry_get_text (dd->gentry));
228
229 sr = g_object_new (GNM_SEARCH_REPLACE_TYPE,
230 "sheet", wb_control_cur_sheet (wbc),
231 "scope", scope,
232 "range-text", gnm_expr_entry_get_text (dd->rangetext),
233 "search-text", text,
234 "is-regexp", is_regexp,
235 "is-number", is_number,
236 "ignore-case", is_checked (gui, "ignore_case"),
237 "match-words", is_checked (gui, "match_words"),
238 "search-strings", is_checked (gui, "search_string"),
239 "search-other-values", is_checked (gui, "search_other"),
240 "search-expressions", is_checked (gui, "search_expr"),
241 "search-expression-results", is_checked (gui, "search_expr_results"),
242 "search-comments", is_checked (gui, "search_comments"),
243 "by-row", go_gtk_builder_group_value (gui, direction_group) == 0,
244 NULL);
245
246 g_free (text);
247
248 err = gnm_search_replace_verify (sr, FALSE);
249 if (err) {
250 go_gtk_notice_dialog (GTK_WINDOW (dd->dialog),
251 GTK_MESSAGE_ERROR, "%s", err);
252 g_free (err);
253 g_object_unref (sr);
254 return;
255 } else if (!sr->search_strings &&
256 !sr->search_other_values &&
257 !sr->search_expressions &&
258 !sr->search_expression_results &&
259 !sr->search_comments) {
260 go_gtk_notice_dialog (GTK_WINDOW (dd->dialog), GTK_MESSAGE_ERROR,
261 _("You must select some cell types to search."));
262 g_object_unref (sr);
263 return;
264 }
265
266 if (is_checked (gui, "save-in-prefs"))
267 dialog_search_save_in_prefs (dd);
268
269 {
270 GtkTreeModel *model;
271 GPtrArray *cells;
272
273 /* Clear current table. */
274 gtk_tree_view_set_model (dd->matches_table, NULL);
275 gnm_search_filter_matching_free (dd->matches);
276
277 cells = gnm_search_collect_cells (sr);
278 dd->matches = gnm_search_filter_matching (sr, cells);
279 gnm_search_collect_cells_free (cells);
280
281 model = make_matches_model (dd);
282 gtk_tree_view_set_model (dd->matches_table, model);
283 g_object_unref (model);
284
285 /* Set sensitivity of buttons. */
286 cursor_change (dd->matches_table, dd);
287 }
288
289 gtk_notebook_set_current_page (dd->notebook, dd->notebook_matches_page);
290 gtk_widget_grab_focus (GTK_WIDGET (dd->matches_table));
291
292 g_object_unref (sr);
293 }
294
295 static void
prev_next_clicked(DialogState * dd,int delta)296 prev_next_clicked (DialogState *dd, int delta)
297 {
298 gboolean res;
299 GtkWidget *w = GTK_WIDGET (dd->matches_table);
300
301 gtk_widget_grab_focus (w);
302 g_signal_emit_by_name (w, "move_cursor",
303 GTK_MOVEMENT_DISPLAY_LINES, delta,
304 &res);
305 }
306
307 static void
prev_clicked(G_GNUC_UNUSED GtkWidget * widget,DialogState * dd)308 prev_clicked (G_GNUC_UNUSED GtkWidget *widget, DialogState *dd)
309 {
310 prev_next_clicked (dd, -1);
311 }
312
313 static void
next_clicked(G_GNUC_UNUSED GtkWidget * widget,DialogState * dd)314 next_clicked (G_GNUC_UNUSED GtkWidget *widget, DialogState *dd)
315 {
316 prev_next_clicked (dd, +1);
317 }
318
319 static gboolean
cb_next(G_GNUC_UNUSED GtkWidget * widget,G_GNUC_UNUSED gboolean start_editing,DialogState * dd)320 cb_next (G_GNUC_UNUSED GtkWidget *widget,
321 G_GNUC_UNUSED gboolean start_editing,
322 DialogState *dd)
323 {
324 prev_next_clicked (dd, +1);
325 return TRUE;
326 }
327
328 static void
cb_focus_on_entry(GtkWidget * widget,GtkWidget * entry)329 cb_focus_on_entry (GtkWidget *widget, GtkWidget *entry)
330 {
331 if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget)))
332 gtk_widget_grab_focus (GTK_WIDGET (gnm_expr_entry_get_entry
333 (GNM_EXPR_ENTRY (entry))));
334 }
335
336 static void
match_renderer_func(GtkTreeViewColumn * tree_column,GtkCellRenderer * cr,GtkTreeModel * model,GtkTreeIter * iter,gpointer user_data)337 match_renderer_func (GtkTreeViewColumn *tree_column,
338 GtkCellRenderer *cr,
339 GtkTreeModel *model,
340 GtkTreeIter *iter,
341 gpointer user_data)
342 {
343 int column = GPOINTER_TO_INT (user_data);
344 GnmSearchFilterResult *m;
345 GnmCell *cell;
346 GnmComment *comment;
347 const char *text = NULL;
348 char *free_text = NULL;
349
350 gtk_tree_model_get (model, iter, ITEM_MATCH, &m, -1);
351
352 if (m->locus == GNM_SRL_COMMENT) {
353 cell = NULL;
354 comment = sheet_get_comment (m->ep.sheet, &m->ep.eval);
355 } else {
356 cell = sheet_cell_get (m->ep.sheet,
357 m->ep.eval.col,
358 m->ep.eval.row);
359 comment = NULL;
360 }
361
362 switch (column) {
363 case COL_SHEET:
364 text = m->ep.sheet->name_unquoted;
365 break;
366 case COL_CELL:
367 text = cellpos_as_string (&m->ep.eval);
368 break;
369 case COL_TYPE:
370 switch (m->locus) {
371 case GNM_SRL_COMMENT:
372 text = _("Comment");
373 break;
374 case GNM_SRL_VALUE:
375 text = _("Result");
376 break;
377 case GNM_SRL_CONTENTS: {
378 GnmValue *v = cell ? cell->value : NULL;
379 gboolean is_expr = cell && gnm_cell_has_expr (cell);
380 gboolean is_value = !is_expr && !gnm_cell_is_empty (cell) && v;
381
382 if (!cell)
383 text = _("Deleted");
384 else if (is_expr)
385 text = _("Expression");
386 else if (is_value && VALUE_IS_STRING (v))
387 text = _("String");
388 else if (is_value && VALUE_IS_FLOAT (v))
389 text = _("Number");
390 else
391 text = _("Other value");
392 break;
393 }
394 default:
395 g_assert_not_reached ();
396 }
397 break;
398
399 case COL_CONTENTS:
400 switch (m->locus) {
401 case GNM_SRL_COMMENT:
402 text = comment
403 ? cell_comment_text_get (comment)
404 : _("Deleted");
405 break;
406 case GNM_SRL_VALUE:
407 text = cell && cell->value
408 ? value_peek_string (cell->value)
409 : _("Deleted");
410 break;
411 case GNM_SRL_CONTENTS:
412 text = cell
413 ? (free_text = gnm_cell_get_entered_text (cell))
414 : _("Deleted");
415 break;
416 default:
417 g_assert_not_reached ();
418 }
419 break;
420
421 default:
422 g_assert_not_reached ();
423 }
424
425 g_object_set (cr, "text", text, NULL);
426 g_free (free_text);
427 }
428
429
430 static GtkTreeView *
make_matches_table(DialogState * dd)431 make_matches_table (DialogState *dd)
432 {
433 GtkTreeView *tree_view;
434 GtkTreeModel *model = GTK_TREE_MODEL (make_matches_model (dd));
435 int i;
436 static const char *const columns[4] = {
437 N_("Sheet"), N_("Cell"), N_("Type"), N_("Content")
438 };
439
440 tree_view = GTK_TREE_VIEW (gtk_tree_view_new_with_model (model));
441
442 for (i = 0; i < (int)G_N_ELEMENTS (columns); i++) {
443 GtkTreeViewColumn *tvc = gtk_tree_view_column_new ();
444 GtkCellRenderer *cr = gtk_cell_renderer_text_new ();
445
446 g_object_set (cr, "single-paragraph-mode", TRUE, NULL);
447 if (i == COL_CONTENTS)
448 g_object_set (cr, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
449
450 gtk_tree_view_column_set_title (tvc, _(columns[i]));
451 gtk_tree_view_column_set_cell_data_func
452 (tvc, cr,
453 match_renderer_func,
454 GINT_TO_POINTER (i), NULL);
455 gtk_tree_view_column_pack_start (tvc, cr, TRUE);
456
457 gtk_tree_view_column_set_sizing (tvc, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
458 gtk_tree_view_append_column (tree_view, tvc);
459 }
460
461 g_object_unref (model);
462 return tree_view;
463 }
464
465 void
dialog_search(WBCGtk * wbcg)466 dialog_search (WBCGtk *wbcg)
467 {
468 GtkBuilder *gui;
469 GtkDialog *dialog;
470 DialogState *dd;
471 GtkGrid *grid;
472
473 g_return_if_fail (wbcg != NULL);
474
475 #ifdef USE_GURU
476 /* Only one guru per workbook. */
477 if (wbc_gtk_get_guru (wbcg))
478 return;
479 #endif
480
481 gui = gnm_gtk_builder_load ("res:ui/search.ui", NULL, GO_CMD_CONTEXT (wbcg));
482 if (gui == NULL)
483 return;
484
485 dialog = GTK_DIALOG (gtk_builder_get_object (gui, "search_dialog"));
486
487 dd = g_new (DialogState, 1);
488 dd->wbcg = wbcg;
489 dd->gui = gui;
490 dd->dialog = dialog;
491 dd->matches = g_ptr_array_new ();
492
493 dd->prev_button = go_gtk_builder_get_widget (gui, "prev_button");
494 dd->next_button = go_gtk_builder_get_widget (gui, "next_button");
495
496 dd->notebook = GTK_NOTEBOOK (gtk_builder_get_object (gui, "notebook"));
497 dd->notebook_matches_page =
498 gtk_notebook_page_num (dd->notebook,
499 go_gtk_builder_get_widget (gui, "matches_tab"));
500
501 dd->rangetext = gnm_expr_entry_new
502 (wbcg,
503 #ifdef USE_GURU
504 TRUE
505 #else
506 FALSE
507 #endif
508 );
509 gnm_expr_entry_set_flags (dd->rangetext, 0, GNM_EE_MASK);
510 grid = GTK_GRID (gtk_builder_get_object (gui, "normal-grid"));
511 gtk_widget_set_hexpand (GTK_WIDGET (dd->rangetext), TRUE);
512 gtk_grid_attach (grid, GTK_WIDGET (dd->rangetext), 1, 6, 1, 1);
513 {
514 char *selection_text =
515 selection_to_string (
516 wb_control_cur_sheet_view (GNM_WBC (wbcg)),
517 TRUE);
518 gnm_expr_entry_load_from_text (dd->rangetext, selection_text);
519 g_free (selection_text);
520 }
521
522 dd->gentry = GTK_ENTRY (gtk_entry_new ());
523 gtk_widget_set_hexpand (GTK_WIDGET (dd->gentry), TRUE);
524 gtk_grid_attach (grid, GTK_WIDGET (dd->gentry), 1, 0, 1, 1);
525 gtk_widget_grab_focus (GTK_WIDGET (dd->gentry));
526 gnm_editable_enters (GTK_WINDOW (dialog), GTK_WIDGET (dd->gentry));
527
528 dd->matches_table = make_matches_table (dd);
529
530 {
531 GtkWidget *scrolled_window =
532 gtk_scrolled_window_new (NULL, NULL);
533 gtk_container_add (GTK_CONTAINER (scrolled_window),
534 GTK_WIDGET (dd->matches_table));
535 gtk_box_pack_start (GTK_BOX (gtk_builder_get_object (gui, "matches_vbox")),
536 scrolled_window,
537 TRUE, TRUE, 0);
538 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
539 GTK_POLICY_NEVER,
540 GTK_POLICY_ALWAYS);
541 }
542
543 /* Set sensitivity of buttons. */
544 cursor_change (dd->matches_table, dd);
545
546 #define SETW(w,f) gtk_toggle_button_set_active \
547 (GTK_TOGGLE_BUTTON (gtk_builder_get_object (gui, w)), f())
548 SETW("search_expr", gnm_conf_get_searchreplace_change_cell_expressions);
549 SETW("search_other", gnm_conf_get_searchreplace_change_cell_other);
550 SETW("search_string", gnm_conf_get_searchreplace_change_cell_strings);
551 SETW("search_comments", gnm_conf_get_searchreplace_change_comments);
552 SETW("search_expr_results", gnm_conf_get_searchreplace_search_results);
553 SETW("ignore_case", gnm_conf_get_searchreplace_ignore_case);
554 SETW("match_words", gnm_conf_get_searchreplace_whole_words_only);
555 #undef SETW
556
557 gtk_toggle_button_set_active
558 (GTK_TOGGLE_BUTTON
559 (gtk_builder_get_object
560 (gui,
561 search_type_group[gnm_conf_get_searchreplace_regex ()])), TRUE);
562 gtk_toggle_button_set_active
563 (GTK_TOGGLE_BUTTON
564 (gtk_builder_get_object
565 (gui,
566 direction_group
567 [gnm_conf_get_searchreplace_columnmajor () ? 1 : 0])), TRUE);
568 gtk_toggle_button_set_active
569 (GTK_TOGGLE_BUTTON
570 (gtk_builder_get_object
571 (gui,
572 scope_group[gnm_conf_get_searchreplace_scope ()])), TRUE);
573
574 g_signal_connect (G_OBJECT (dd->matches_table), "cursor_changed",
575 G_CALLBACK (cursor_change), dd);
576 g_signal_connect (G_OBJECT (dd->matches_table), "select_cursor_row",
577 G_CALLBACK (cb_next), dd);
578 go_gtk_builder_signal_connect (gui, "search_button", "clicked",
579 G_CALLBACK (search_clicked), dd);
580 g_signal_connect (G_OBJECT (dd->prev_button), "clicked",
581 G_CALLBACK (prev_clicked), dd);
582 g_signal_connect (G_OBJECT (dd->next_button), "clicked",
583 G_CALLBACK (next_clicked), dd);
584 go_gtk_builder_signal_connect_swapped (gui, "close_button", "clicked",
585 G_CALLBACK (gtk_widget_destroy), dd->dialog);
586 g_signal_connect (G_OBJECT (gnm_expr_entry_get_entry (dd->rangetext)), "focus-in-event",
587 G_CALLBACK (range_focused), dd);
588 go_gtk_builder_signal_connect (gui, "scope_range", "toggled",
589 G_CALLBACK (cb_focus_on_entry), dd->rangetext);
590
591 #ifdef USE_GURU
592 wbc_gtk_attach_guru_with_unfocused_rs (wbcg, GTK_WIDGET (dialog), dd->rangetext);
593 #endif
594 g_object_set_data_full (G_OBJECT (dialog),
595 "state", dd, (GDestroyNotify) free_state);
596 gnm_dialog_setup_destroy_handlers (dialog, wbcg,
597 GNM_DIALOG_DESTROY_SHEET_REMOVED);
598 gnm_init_help_button (
599 go_gtk_builder_get_widget (gui, "help_button"),
600 GNUMERIC_HELP_LINK_SEARCH);
601 gnm_restore_window_geometry (GTK_WINDOW (dialog), SEARCH_KEY);
602
603 go_gtk_nonmodal_dialog (wbcg_toplevel (wbcg), GTK_WINDOW (dialog));
604 gtk_widget_show_all (GTK_WIDGET (dialog));
605 }
606
607 /* ------------------------------------------------------------------------- */
608