1 /*
2    Kickshaw - A Menu Editor for Openbox
3 
4    Copyright (c) 2010–2018        Marcus Schätzle
5 
6    This program is free software; you can redistribute it and/or modify
7    it under the terms of the GNU General Public License as published by
8    the Free Software Foundation; either version 2 of the License, or
9    (at your option) any later version.
10 
11    This program is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14    GNU General Public License for more details.
15 
16    You should have received a copy of the GNU General Public License along
17    with Kickshaw. If not, see http://www.gnu.org/licenses/.
18 */
19 
20 #include <gtk/gtk.h>
21 
22 #include "definitions_and_enumerations.h"
23 #include "find.h"
24 
25 void show_or_hide_find_grid (void);
26 void find_buttons_management (gchar *column_check_button_clicked);
27 static inline void clear_list_of_rows_with_found_occurrences (void);
28 static gboolean add_occurrence_to_list (G_GNUC_UNUSED GtkTreeModel *local_model,
29                                                       GtkTreePath  *local_path,
30                                                       GtkTreeIter  *local_iter);
31 GList *create_list_of_rows_with_found_occurrences (void);
32 gboolean check_for_match (GtkTreeIter *local_iter, guint8 column_number);
33 static gboolean ensure_visibility_of_match (G_GNUC_UNUSED GtkTreeModel *foreach_or_local_model,
34                                                           GtkTreePath  *foreach_or_local_path,
35                                                           GtkTreeIter  *foreach_or_local_iter);
36 void run_search (void);
37 void jump_to_previous_or_next_occurrence (gpointer direction_pointer);
38 
39 /*
40 
41     Shows or hides (and resets the settings of the elements inside) the find grid.
42 
43 */
44 
show_or_hide_find_grid(void)45 void show_or_hide_find_grid (void)
46 {
47     if (gtk_widget_get_visible (ks.find_grid)) {
48         guint8 columns_cnt;
49 
50         gtk_widget_hide (ks.find_grid);
51         g_string_assign (ks.search_term, "");
52         clear_list_of_rows_with_found_occurrences ();
53         gtk_entry_set_text (GTK_ENTRY (ks.find_entry), "");
54         gtk_style_context_remove_class (gtk_widget_get_style_context(ks.find_entry), "mandatory_missing");
55         for (columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; columns_cnt++) {
56             gtk_style_context_remove_class (gtk_widget_get_style_context(ks.find_in_columns[columns_cnt]),
57                                             "mandatory_missing");
58         }
59         gtk_style_context_remove_class (gtk_widget_get_style_context(ks.find_in_all_columns), "mandatory_missing");
60         gtk_widget_set_sensitive (ks.find_entry_buttons[BACK], FALSE);
61         gtk_widget_set_sensitive (ks.find_entry_buttons[FORWARD], FALSE);
62         gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ks.find_in_all_columns), TRUE);
63         gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ks.find_match_case), FALSE);
64         gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ks.find_regular_expression), TRUE);
65         gtk_widget_queue_draw (GTK_WIDGET (ks.treeview)); // Force redrawing of treeview.
66     }
67     else {
68         gtk_widget_show (ks.find_grid);
69         gtk_widget_grab_focus (ks.find_entry);
70     }
71 }
72 
73 /*
74 
75     (De)activates all other column check buttons if "All columns" is (un)selected.
76     Search results are updated for any change of the chosen columns and criteria ("match case" and "regular expression").
77 
78 */
79 
find_buttons_management(gchar * column_check_button_clicked)80 void find_buttons_management (gchar *column_check_button_clicked)
81 {
82     // TRUE if any find_in_columns check button or find_in_all_columns check button clicked.
83     if (column_check_button_clicked) {
84         gboolean marking_active;
85         gboolean all_columns_check_button_clicked = STREQ (column_check_button_clicked, "all");
86 
87         marking_active = gtk_style_context_has_class (gtk_widget_get_style_context (ks.find_in_all_columns),
88                                                       "mandatory_missing");
89 
90         if (marking_active || all_columns_check_button_clicked) {
91             gboolean find_in_all_activated = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ks.find_in_all_columns));
92 
93             guint8 columns_cnt;
94 
95             for (columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; columns_cnt++) {
96                 if (all_columns_check_button_clicked) {
97                     g_signal_handler_block (ks.find_in_columns[columns_cnt], ks.handler_id_find_in_columns[columns_cnt]);
98                     gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ks.find_in_columns[columns_cnt]), find_in_all_activated);
99                     g_signal_handler_unblock (ks.find_in_columns[columns_cnt], ks.handler_id_find_in_columns[columns_cnt]);
100                     gtk_widget_set_sensitive (ks.find_in_columns[columns_cnt], !find_in_all_activated);
101                 }
102                 if (marking_active) {
103                     gtk_style_context_remove_class (gtk_widget_get_style_context(ks.find_in_columns[columns_cnt]),
104                                                     "mandatory_missing");
105                 }
106             }
107             if (marking_active) {
108                 gtk_style_context_remove_class (gtk_widget_get_style_context(ks.find_in_all_columns), "mandatory_missing");
109             }
110         }
111     }
112 
113     if (*ks.search_term->str) {
114         if (create_list_of_rows_with_found_occurrences ()) {
115             gtk_tree_model_foreach (ks.model, (GtkTreeModelForeachFunc) ensure_visibility_of_match, NULL);
116         }
117         row_selected (); // Reset status of forward and back buttons.
118     }
119 
120     gtk_widget_queue_draw (GTK_WIDGET (ks.treeview)); // Force redrawing of treeview (for highlighting of search results).
121 }
122 
123 /*
124 
125     Clears the list of rows so it can be rebuild later.
126 
127 */
128 
clear_list_of_rows_with_found_occurrences(void)129 static inline void clear_list_of_rows_with_found_occurrences (void) {
130     g_list_free_full (ks.rows_with_found_occurrences, (GDestroyNotify) gtk_tree_path_free);
131     ks.rows_with_found_occurrences = NULL;
132 }
133 
134 /*
135 
136     Adds a row that contains a column matching the search term to a list.
137 
138 */
139 
add_occurrence_to_list(G_GNUC_UNUSED GtkTreeModel * local_model,GtkTreePath * local_path,GtkTreeIter * local_iter)140 static gboolean add_occurrence_to_list (G_GNUC_UNUSED GtkTreeModel *local_model,
141                                                       GtkTreePath  *local_path,
142                                                       GtkTreeIter  *local_iter)
143 {
144     guint8 columns_cnt;
145 
146     for (columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; columns_cnt++) {
147         if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ks.find_in_columns[columns_cnt])) &&
148             check_for_match (local_iter, columns_cnt)) {
149             // Row references are not used here, since the list is recreated everytime the treestore is changed.
150             ks.rows_with_found_occurrences = g_list_prepend (ks.rows_with_found_occurrences, gtk_tree_path_copy (local_path));
151             break;
152         }
153     }
154 
155     return FALSE;
156 }
157 
158 /*
159 
160     Creates a list of all rows that contain at least one cell with the search term.
161 
162     rows_with_found_occurrences is a global GList, the purpose of returning the pointer is that the function to
163     which is returned to can immediately check whether a list has been created or not, in the latter case the
164     return value is NULL.
165 
166 */
167 
create_list_of_rows_with_found_occurrences(void)168 GList *create_list_of_rows_with_found_occurrences (void)
169 {
170     clear_list_of_rows_with_found_occurrences ();
171     gtk_tree_model_foreach (ks.model, (GtkTreeModelForeachFunc) add_occurrence_to_list, NULL);
172     return ks.rows_with_found_occurrences = g_list_reverse (ks.rows_with_found_occurrences);
173 }
174 
175 /*
176 
177     Checks for each column if it contains the search term.
178 
179 */
180 
check_for_match(GtkTreeIter * local_iter,guint8 column_number)181 gboolean check_for_match (GtkTreeIter *local_iter,
182                           guint8       column_number)
183 {
184     gboolean match_found = FALSE; // Default
185     gchar *current_column;
186 
187     gtk_tree_model_get (ks.model, local_iter, column_number + TREEVIEW_COLUMN_OFFSET, &current_column, -1);
188     if (current_column) {
189         gchar *search_term_str_escaped = (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ks.find_regular_expression))) ?
190                                           NULL : g_regex_escape_string (ks.search_term->str, -1);
191 
192         if (g_regex_match_simple ((search_term_str_escaped) ? search_term_str_escaped : ks.search_term->str,
193                                   current_column,
194                                   (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ks.find_match_case))) ?
195                                   0 : G_REGEX_CASELESS,
196                                   G_REGEX_MATCH_NOTEMPTY)) {
197             match_found = TRUE;
198         }
199 
200         // Cleanup
201         g_free (current_column);
202         g_free (search_term_str_escaped);
203     }
204 
205     return match_found;
206 }
207 
208 /*
209 
210     If the search term is contained inside...
211     ...a row whose parent is not expanded expand the latter.
212     ...a column which is hidden show this column.
213 
214 */
215 
ensure_visibility_of_match(G_GNUC_UNUSED GtkTreeModel * foreach_or_local_model,GtkTreePath * foreach_or_local_path,GtkTreeIter * foreach_or_local_iter)216 static gboolean ensure_visibility_of_match (G_GNUC_UNUSED GtkTreeModel *foreach_or_local_model,
217                                                           GtkTreePath  *foreach_or_local_path,
218                                                           GtkTreeIter  *foreach_or_local_iter)
219 {
220     guint8 columns_cnt;
221 
222     for (columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; columns_cnt++) {
223         if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ks.find_in_columns[columns_cnt])) &&
224             check_for_match (foreach_or_local_iter, columns_cnt)) {
225             if (gtk_tree_path_get_depth (foreach_or_local_path) > 1 &&
226                 !gtk_tree_view_row_expanded (GTK_TREE_VIEW (ks.treeview), foreach_or_local_path)) {
227                 gtk_tree_view_expand_to_path (GTK_TREE_VIEW (ks.treeview), foreach_or_local_path);
228                 gtk_tree_view_collapse_row (GTK_TREE_VIEW (ks.treeview), foreach_or_local_path);
229             }
230             if (!gtk_tree_view_column_get_visible (ks.columns[columns_cnt])) {
231                 gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (ks.mb_view_and_options[(columns_cnt == COL_MENU_ID) ?
232                                                                                              SHOW_MENU_ID_COL :
233                                                                                              SHOW_EXECUTE_COL]),
234                                                 TRUE);
235             }
236         }
237     }
238 
239     return FALSE;
240 }
241 
242 /*
243 
244     Runs a search on the entered search term.
245 
246 */
247 
run_search(void)248 void run_search (void)
249 {
250     // If regular expressions are activated, check if the regular expression is valid.
251     if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ks.find_regular_expression))) {
252         GError *error = NULL;
253         GRegex *regex = g_regex_new (gtk_entry_get_text (GTK_ENTRY (ks.find_entry)), 0, 0, &error);
254 
255         if (error) {
256             show_errmsg (error->message);
257             // Cleanup
258             g_error_free (error);
259 
260             return;
261         }
262 
263         g_regex_unref (regex);
264     }
265 
266     g_string_assign (ks.search_term, gtk_entry_get_text (GTK_ENTRY (ks.find_entry)));
267 
268     gboolean no_find_in_columns_buttons_clicked = TRUE; // Default
269 
270     guint8 columns_cnt;
271 
272     if (*ks.search_term->str) {
273         gtk_style_context_remove_class (gtk_widget_get_style_context (ks.find_entry), "mandatory_missing");
274     }
275     else {
276         wrong_or_missing (ks.find_entry, ks.find_entry_css_provider);
277     }
278 
279     for (columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; columns_cnt++) {
280         if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (ks.find_in_columns[columns_cnt]))) {
281             no_find_in_columns_buttons_clicked = FALSE;
282             break;
283         }
284     }
285 
286     if (!(*ks.search_term->str) || no_find_in_columns_buttons_clicked) {
287         if (no_find_in_columns_buttons_clicked) {
288             for (columns_cnt = 0; columns_cnt < COL_ELEMENT_VISIBILITY; columns_cnt++) {
289                 wrong_or_missing (ks.find_in_columns[columns_cnt], ks.find_in_columns_css_providers[columns_cnt]);
290             }
291             wrong_or_missing (ks.find_in_all_columns, ks.find_in_all_columns_css_provider);
292         }
293         clear_list_of_rows_with_found_occurrences ();
294     }
295     else if (create_list_of_rows_with_found_occurrences ()) {
296         GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));
297 
298         gtk_tree_model_foreach (ks.model, (GtkTreeModelForeachFunc) ensure_visibility_of_match, NULL);
299         gtk_tree_selection_unselect_all (selection);
300         gtk_tree_selection_select_path (selection, ks.rows_with_found_occurrences->data);
301         /*
302             There is no horizontically movement to a specific GtkTreeViewColumn; this is indicated by NULL.
303             Alignment arguments (row_align and col_align) aren't used; this is indicated by FALSE.
304         */
305         gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (ks.treeview), ks.rows_with_found_occurrences->data, NULL, FALSE, 0, 0);
306     }
307 
308     row_selected (); // Reset status of forward and back buttons.
309 
310     gtk_widget_queue_draw (GTK_WIDGET (ks.treeview)); // Force redrawing of treeview (for highlighting of search results).
311 }
312 
313 /*
314 
315     Enables the possibility to move between the found occurrences.
316 
317 */
318 
jump_to_previous_or_next_occurrence(gpointer direction_pointer)319 void jump_to_previous_or_next_occurrence (gpointer direction_pointer)
320 {
321     gboolean forward = GPOINTER_TO_INT (direction_pointer);
322 
323     GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (ks.treeview));
324     GtkTreePath *path = gtk_tree_model_get_path (ks.model, &ks.iter), *path_of_occurrence;
325     GtkTreeIter iter_of_occurrence;
326 
327     GList *rows_with_found_occurrences_loop;
328 
329     for (rows_with_found_occurrences_loop = (forward) ?
330          ks.rows_with_found_occurrences : g_list_last (ks.rows_with_found_occurrences);
331          rows_with_found_occurrences_loop;
332          rows_with_found_occurrences_loop = (forward) ? rows_with_found_occurrences_loop->next :
333          rows_with_found_occurrences_loop->prev) {
334         if ((forward) ? (gtk_tree_path_compare (path, rows_with_found_occurrences_loop->data) < 0) :
335             (gtk_tree_path_compare (path, rows_with_found_occurrences_loop->data) > 0)) {
336             break;
337         }
338     }
339 
340     path_of_occurrence = rows_with_found_occurrences_loop->data;
341     gtk_tree_model_get_iter (ks.model, &iter_of_occurrence, path_of_occurrence);
342 
343     // Ensure_visibility_of_match is not called by gtk_tree_model_foreach; model is unused so this argument is set to NULL.
344     ensure_visibility_of_match (NULL, path_of_occurrence, &iter_of_occurrence);
345 
346     gtk_tree_selection_unselect_all (selection);
347     gtk_tree_selection_select_path (selection, path_of_occurrence);
348     /*
349         There is no horizontically movement to a specific GtkTreeViewColumn; this is indicated by NULL.
350         Alignment arguments (row_align and col_align) aren't used; this is indicated by FALSE.
351     */
352     gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (ks.treeview), path_of_occurrence, NULL, FALSE, 0, 0);
353 
354     // Cleanup
355     gtk_tree_path_free (path);
356 }
357