1 /*
2  * dialog-autofilter.c:  A pair of dialogs for autofilter conditions
3  *
4  * (c) Copyright 2002 Jody Goldberg <jody@gnome.org>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of the
9  * License, or (at your option) version 3.
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
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
19  * USA
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 <commands.h>
30 #include <workbook-control.h>
31 #include <workbook.h>
32 #include <wbc-gtk.h>
33 #include <sheet.h>
34 #include <cell.h>
35 #include <ranges.h>
36 #include <value.h>
37 #include <sheet-filter.h>
38 #include <number-match.h>
39 #include <undo.h>
40 
41 #include <string.h>
42 
43 typedef struct {
44 	GtkBuilder         *gui;
45 	WBCGtk *wbcg;
46 	GtkWidget          *dialog;
47 	GnmFilter	   *filter;
48 	unsigned	    field;
49 	gboolean	    is_expr;
50 } AutoFilterState;
51 
52 #define DIALOG_KEY "autofilter"
53 #define DIALOG_KEY_EXPRESSION "autofilter-expression"
54 #define UNICODE_ELLIPSIS "\xe2\x80\xa6"
55 
56 static char const * const type_group[] = {
57 	"items-largest",
58 	"items-smallest",
59 	"percentage-largest",
60 	"percentage-smallest",
61 	"percentage-largest-number",
62 	"percentage-smallest-number",
63 	NULL
64 };
65 
66 static GnmFilterOp
autofilter_get_type(AutoFilterState * state)67 autofilter_get_type (AutoFilterState *state)
68 {
69 	return (GNM_FILTER_OP_TYPE_BUCKETS |
70 		gnm_gui_group_value (state->gui, type_group));
71 }
72 
73 
74 static void
cb_autofilter_destroy(AutoFilterState * state)75 cb_autofilter_destroy (AutoFilterState *state)
76 {
77 	if (state->gui != NULL) {
78 		g_object_unref (state->gui);
79 		state->gui = NULL;
80 	}
81 
82 	state->dialog = NULL;
83 	g_free (state);
84 }
85 
86 static GnmValue *
map_op(AutoFilterState * state,GnmFilterOp * op,char const * op_widget,char const * val_widget)87 map_op (AutoFilterState *state, GnmFilterOp *op,
88 	char const *op_widget, char const *val_widget)
89 {
90 	int i;
91 	GtkWidget *w = go_gtk_builder_get_widget (state->gui, val_widget);
92 	char const *txt = gtk_entry_get_text (GTK_ENTRY (w));
93 	GnmValue *v = NULL;
94 
95 	*op = GNM_FILTER_UNUSED;
96 	if (txt == NULL || *txt == '\0')
97 		return NULL;
98 
99 	w = go_gtk_builder_get_widget (state->gui, op_widget);
100 	i = gtk_combo_box_get_active (GTK_COMBO_BOX (w));
101 	switch (i) {
102 	case 0: return NULL;
103 	case 1: *op = GNM_FILTER_OP_EQUAL;	break;
104 	case 2: *op = GNM_FILTER_OP_NOT_EQUAL;	break;
105 	case 3: *op = GNM_FILTER_OP_GT;		break;
106 	case 4: *op = GNM_FILTER_OP_GTE;	break;
107 	case 5: *op = GNM_FILTER_OP_LT;		break;
108 	case 6: *op = GNM_FILTER_OP_LTE;	break;
109 
110 	case 7:
111 	case 8: *op = (i == 8) ? GNM_FILTER_OP_NOT_EQUAL : GNM_FILTER_OP_EQUAL;
112 		v = value_new_string_nocopy (g_strconcat (txt, "*", NULL));
113 		break;
114 
115 	case 9:
116 	case 10: *op = (i == 10) ? GNM_FILTER_OP_NOT_EQUAL : GNM_FILTER_OP_EQUAL;
117 		v = value_new_string_nocopy (g_strconcat ("*", txt, NULL));
118 		break;
119 
120 	case 11:
121 	case 12: *op = (i == 12) ? GNM_FILTER_OP_NOT_EQUAL : GNM_FILTER_OP_EQUAL;
122 		v = value_new_string_nocopy (g_strconcat ("*", txt, "*", NULL));
123 		break;
124 	default :
125 		g_warning ("huh?");
126 		return NULL;
127 	}
128 
129 	if (v == NULL) {
130 		Workbook *wb = wb_control_get_workbook (GNM_WBC (state->wbcg));
131 		v = format_match (txt, NULL, workbook_date_conv (wb));
132 	}
133 	if (v == NULL)
134 		v = value_new_string (txt);
135 
136 	return v;
137 }
138 
139 static void
cb_autofilter_ok(G_GNUC_UNUSED GtkWidget * button,AutoFilterState * state)140 cb_autofilter_ok (G_GNUC_UNUSED GtkWidget *button,
141 		  AutoFilterState *state)
142 {
143 	GnmFilterCondition *cond = NULL;
144 	GtkWidget *w;
145 
146 	if (state->is_expr) {
147 		GnmFilterOp op0;
148 		GnmValue *v0 = map_op (state, &op0, "op0", "value0");
149 
150 		if (op0 != GNM_FILTER_UNUSED) {
151 			GnmFilterOp op1;
152 			GnmValue *v1 = map_op (state, &op1, "op1", "value1");
153 			if (op1 != GNM_FILTER_UNUSED) {
154 				w = go_gtk_builder_get_widget (state->gui,
155 							       "and_button");
156 				cond = gnm_filter_condition_new_double
157 					(op0, v0,
158 					 gtk_toggle_button_get_active
159 					 (GTK_TOGGLE_BUTTON (w)),
160 					 op1, v1);
161 			} else
162 				cond = gnm_filter_condition_new_single
163 					(op0, v0);
164 		}
165 	} else {
166 		int count;
167 		GnmFilterOp op = autofilter_get_type (state);
168 
169 		w = go_gtk_builder_get_widget (state->gui, "item_count");
170 		count = gtk_spin_button_get_value (GTK_SPIN_BUTTON (w));
171 
172 		cond = gnm_filter_condition_new_bucket
173 			(!(op & GNM_FILTER_OP_BOTTOM_MASK),
174 			 !(op & GNM_FILTER_OP_PERCENT_MASK),
175 			 !(op & GNM_FILTER_OP_REL_N_MASK),
176 			 count);
177 	}
178 	if (cond != NULL)
179 		cmd_autofilter_set_condition (GNM_WBC (state->wbcg),
180 					      state->filter, state->field,
181 					      cond);
182 
183 	gtk_widget_destroy (state->dialog);
184 }
185 
186 static void
cb_autofilter_cancel(G_GNUC_UNUSED GtkWidget * button,AutoFilterState * state)187 cb_autofilter_cancel (G_GNUC_UNUSED GtkWidget *button,
188 		      AutoFilterState *state)
189 {
190 	gtk_widget_destroy (state->dialog);
191 }
192 
193 static void
cb_top10_count_changed(GtkSpinButton * button,AutoFilterState * state)194 cb_top10_count_changed (GtkSpinButton *button,
195 			AutoFilterState *state)
196 {
197 	int val = 0.5 + gtk_spin_button_get_value (button);
198 	GtkWidget *w;
199 	gchar *label;
200 	int cval = val, count;
201 
202 	count = range_height(&(state->filter->r)) - 1;
203 
204 	if (cval > count)
205 		cval = count;
206 
207 	w = go_gtk_builder_get_widget (state->gui, type_group[0]);
208 	/* xgettext : %d gives the number of items in the autofilter. */
209 	/* This is input to ngettext. */
210 	label = g_strdup_printf (ngettext ("Show the largest item",
211 					   "Show the %3d largest items",
212 					   cval),
213 				 cval);
214 	gtk_button_set_label (GTK_BUTTON (w),label);
215 	g_free(label);
216 
217 	w = go_gtk_builder_get_widget (state->gui, type_group[1]);
218 	/* xgettext : %d gives the number of items in the autofilter. */
219 	/* This is input to ngettext. */
220 	label = g_strdup_printf (ngettext ("Show the smallest item",
221 					   "Show the %3d smallest items",
222 					   cval),
223 				 cval);
224 	gtk_button_set_label (GTK_BUTTON (w),label);
225 	g_free(label);
226 
227 	if (val > 100)
228 		val = 100;
229 
230 	w = go_gtk_builder_get_widget (state->gui, type_group[2]);
231 	/* xgettext : %d gives the percentage of the data range in the autofilter. */
232 	/* This is input to ngettext. */
233 	label = g_strdup_printf
234 		(ngettext ("Show the items in the top %3d%% of the data range",
235 			   "Show the items in the top %3d%% of the data range", val),
236 		 val);
237 	gtk_button_set_label (GTK_BUTTON (w),label);
238 	g_free(label);
239 
240 	w = go_gtk_builder_get_widget (state->gui, type_group[3]);
241 	/* xgettext : %d gives the percentage of the data range in the autofilter. */
242 	/* This is input to ngettext. */
243 	label = g_strdup_printf
244 		(ngettext ("Show the items in the bottom %3d%% of the data range",
245 			   "Show the items in the bottom %3d%% of the data range", val),
246 		 val);
247 	gtk_button_set_label (GTK_BUTTON (w),label);
248 	g_free(label);
249 
250 
251 	w = go_gtk_builder_get_widget (state->gui, type_group[4]);
252 	/* xgettext : %d gives the percentage of item number in the autofilter. */
253 	/* This is input to ngettext. */
254 	label = g_strdup_printf
255 		(ngettext ("Show the top %3d%% of all items",
256 			   "Show the top %3d%% of all items", val),
257 		 val);
258 	gtk_button_set_label (GTK_BUTTON (w),label);
259 	g_free(label);
260 
261 	w = go_gtk_builder_get_widget (state->gui, type_group[5]);
262 	/* xgettext : %d gives the percentage of the item number in the autofilter. */
263 	/* This is input to ngettext. */
264 	label = g_strdup_printf
265 		(ngettext ("Show the bottom %3d%% of all items",
266 			   "Show the bottom %3d%% of all items", val),
267 		 val);
268 	gtk_button_set_label (GTK_BUTTON (w),label);
269 	g_free(label);
270 
271 
272 }
273 
274 static void
cb_top10_type_changed(G_GNUC_UNUSED GtkToggleButton * button,AutoFilterState * state)275 cb_top10_type_changed (G_GNUC_UNUSED GtkToggleButton *button,
276 		       AutoFilterState *state)
277 {
278 	GnmFilterOp op = autofilter_get_type (state);
279 	GtkWidget *spin = go_gtk_builder_get_widget (state->gui, "item_count");
280 	GtkWidget *label = go_gtk_builder_get_widget (state->gui, "cp-label");
281 
282 	if ((op & GNM_FILTER_OP_PERCENT_MASK) != 0) {
283 		gtk_spin_button_set_range (GTK_SPIN_BUTTON (spin), 1.,
284 					   100.);
285 		gtk_label_set_text (GTK_LABEL (label), _("Percentage:"));
286 	} else {
287 		gtk_spin_button_set_range
288 			(GTK_SPIN_BUTTON (spin), 1.,
289 			 range_height(&(state->filter->r)) - 1);
290 		gtk_label_set_text (GTK_LABEL (label), _("Count:"));
291 	}
292 }
293 
294 static void
init_operator(AutoFilterState * state,GnmFilterOp op,GnmValue const * v,char const * op_widget,char const * val_widget)295 init_operator (AutoFilterState *state, GnmFilterOp op, GnmValue const *v,
296 	       char const *op_widget, char const *val_widget)
297 {
298 	GtkWidget *w = go_gtk_builder_get_widget (state->gui, op_widget);
299 	char const *str = v ? value_peek_string (v) : NULL;
300 	char *content = NULL;
301 	int i;
302 
303 	switch (op) {
304 	case GNM_FILTER_OP_EQUAL:	i = 1; break;
305 	case GNM_FILTER_OP_GT:		i = 3; break;
306 	case GNM_FILTER_OP_LT:		i = 5; break;
307 	case GNM_FILTER_OP_GTE:		i = 4; break;
308 	case GNM_FILTER_OP_LTE:		i = 6; break;
309 	case GNM_FILTER_OP_NOT_EQUAL:	i = 2; break;
310 	default :
311 		return;
312 	}
313 
314 	if (v != NULL && VALUE_IS_STRING (v) && (i == 1 || i == 2)) {
315 		unsigned const len = strlen (str);
316 
317 		/* there needs to be at least 1 letter */
318 		int ends = (len > 1 && str[0] == '*') ? 1 : 0; /* as a bool and offset */
319 
320 		if (len > 1 && str[len-1] == '*' && str[len-2] != '~') {
321 			content = g_strdup (str + ends);
322 			content[len - ends - 1] = '\0';
323 			i += (ends ? 10 : 6);
324 		} else if (ends) {
325 			str += 1;
326 			i += 8;
327 		}
328 	}
329 	gtk_combo_box_set_active (GTK_COMBO_BOX (w), i);
330 
331 	w = go_gtk_builder_get_widget (state->gui, val_widget);
332 	gnm_editable_enters (GTK_WINDOW (state->dialog), w);
333 	if (v != NULL)
334 		gtk_entry_set_text (GTK_ENTRY (w), content ? content : str);
335 
336 	g_free (content);
337 }
338 
339 static gchar *
dialog_auto_filter_get_col_name(GnmCell * cell,int col,int len)340 dialog_auto_filter_get_col_name (GnmCell *cell, int col, int len)
341 {
342 	gchar *label;
343 	char *content = gnm_cell_get_rendered_text (cell);
344 	if (g_utf8_strlen (content, -1) > len) {
345 		char *end = g_utf8_find_prev_char
346 			(content, content + len + 1 - strlen (UNICODE_ELLIPSIS));
347 		strcpy (end, UNICODE_ELLIPSIS);
348 	}
349 	label = g_strdup_printf (_("Column %s (\"%s\")"),
350 				 col_name (col), content);
351 	g_free (content);
352 	return label;
353 }
354 static void
dialog_auto_filter_expression(WBCGtk * wbcg,GnmFilter * filter,int field,GnmFilterCondition * cond)355 dialog_auto_filter_expression (WBCGtk *wbcg,
356 			       GnmFilter *filter, int field,
357 			       GnmFilterCondition *cond)
358 {
359 	AutoFilterState *state;
360 	GtkWidget *w;
361 	GtkBuilder *gui;
362 	int col;
363 	gchar *label;
364 	GnmCell *cell;
365 	int const len = 15;
366 
367 	g_return_if_fail (wbcg != NULL);
368 
369 	if (gnm_dialog_raise_if_exists
370 	    (wbcg, DIALOG_KEY_EXPRESSION))
371 		return;
372 	gui = gnm_gtk_builder_load ("res:ui/autofilter-expression.ui",
373 				   NULL, GO_CMD_CONTEXT (wbcg));
374 	if (gui == NULL)
375 		return;
376 
377 	state = g_new (AutoFilterState, 1);
378 	state->wbcg	= wbcg;
379 	state->filter	= filter;
380 	state->field	= field;
381 	state->is_expr	= TRUE;
382 	state->gui	= gui;
383 
384 	g_return_if_fail (state->gui != NULL);
385 
386 	col = filter->r.start.col + field;
387 
388 	cell = sheet_cell_get (filter->sheet, col, filter->r.start.row);
389 
390 	if (cell == NULL || gnm_cell_is_blank (cell))
391 		label = g_strdup_printf (_("Column %s"), col_name (col));
392 	else
393 		label = dialog_auto_filter_get_col_name (cell, col, len);
394 
395 	gtk_label_set_text
396 		(GTK_LABEL (go_gtk_builder_get_widget (state->gui, "col-label1")), label);
397 	gtk_label_set_text
398 		(GTK_LABEL (go_gtk_builder_get_widget (state->gui, "col-label2")), label);
399 	g_free (label);
400 
401 	state->dialog = go_gtk_builder_get_widget (state->gui, "dialog");
402 	if (cond != NULL) {
403 		GnmFilterOp const op = cond->op[0];
404 		if (0 == (op & GNM_FILTER_OP_TYPE_MASK)) {
405 			init_operator (state, cond->op[0],
406 				       cond->value[0], "op0", "value0");
407 			if (cond->op[1] != GNM_FILTER_UNUSED)
408 				init_operator (state, cond->op[1],
409 					       cond->value[1], "op1", "value1");
410 			w = go_gtk_builder_get_widget (state->gui,
411 				cond->is_and ? "and_button" : "or_button");
412 			gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (w), TRUE);
413 		}
414 	} else {
415 		/* initialize the combo boxes (not done by li.ui) */
416 		w = go_gtk_builder_get_widget (state->gui, "op0");
417 		gtk_combo_box_set_active (GTK_COMBO_BOX (w), 0);
418 		w = go_gtk_builder_get_widget (state->gui, "op1");
419 		gtk_combo_box_set_active (GTK_COMBO_BOX (w), 0);
420 	}
421 
422 	w = go_gtk_builder_get_widget (state->gui, "ok_button");
423 	g_signal_connect (G_OBJECT (w),
424 		"clicked",
425 		G_CALLBACK (cb_autofilter_ok), state);
426 	w = go_gtk_builder_get_widget (state->gui, "cancel_button");
427 	g_signal_connect (G_OBJECT (w),
428 		"clicked",
429 		G_CALLBACK (cb_autofilter_cancel), state);
430 
431 	/* a candidate for merging into attach guru */
432 	gnm_init_help_button (
433 		go_gtk_builder_get_widget (state->gui, "help_button"),
434 		GNUMERIC_HELP_LINK_AUTOFILTER_CUSTOM);
435 
436 	gnm_dialog_setup_destroy_handlers (GTK_DIALOG (state->dialog),
437 					   state->wbcg,
438 					   GNM_DIALOG_DESTROY_CURRENT_SHEET_REMOVED);
439 
440 	wbc_gtk_attach_guru (state->wbcg, state->dialog);
441 	g_object_set_data_full (G_OBJECT (state->dialog),
442 		"state", state, (GDestroyNotify)cb_autofilter_destroy);
443 
444 	gnm_keyed_dialog (wbcg, GTK_WINDOW (state->dialog),
445 			       DIALOG_KEY_EXPRESSION);
446 	gtk_widget_show (state->dialog);
447 }
448 
449 void
dialog_auto_filter(WBCGtk * wbcg,GnmFilter * filter,int field,gboolean is_expr,GnmFilterCondition * cond)450 dialog_auto_filter (WBCGtk *wbcg,
451 		    GnmFilter *filter, int field,
452 		    gboolean is_expr, GnmFilterCondition *cond)
453 {
454 	AutoFilterState *state;
455 	GtkWidget *w;
456 	GtkBuilder *gui;
457 	int col;
458 	gchar *label;
459 	GnmCell *cell;
460 	int len = is_expr ? 15 : 30;
461 	char const * const *rb;
462 
463 	if (is_expr) {
464 		dialog_auto_filter_expression (wbcg, filter, field, cond);
465 		return;
466 	}
467 
468 	g_return_if_fail (wbcg != NULL);
469 
470 	if (gnm_dialog_raise_if_exists (wbcg, DIALOG_KEY))
471 		return;
472 	gui = gnm_gtk_builder_load ("res:ui/autofilter-top10.ui",
473 				   NULL, GO_CMD_CONTEXT (wbcg));
474 	if (gui == NULL)
475 		return;
476 
477 	state = g_new (AutoFilterState, 1);
478 	state->wbcg	= wbcg;
479 	state->filter	= filter;
480 	state->field	= field;
481 	state->is_expr	= FALSE;
482 	state->gui	= gui;
483 
484 	g_return_if_fail (state->gui != NULL);
485 
486 	col = filter->r.start.col + field;
487 
488 	cell = sheet_cell_get (filter->sheet, col, filter->r.start.row);
489 
490 	if (cell == NULL || gnm_cell_is_blank (cell))
491 		label = g_strdup_printf (_("Column %s"), col_name (col));
492 	else
493 		label = dialog_auto_filter_get_col_name (cell, col, len);
494 
495 	gtk_label_set_text
496 		(GTK_LABEL (go_gtk_builder_get_widget (state->gui, "col-label")), label);
497 	g_free (label);
498 
499 	state->dialog = go_gtk_builder_get_widget (state->gui, "dialog");
500 	if (cond != NULL && GNM_FILTER_OP_TOP_N == (cond->op[0] & GNM_FILTER_OP_TYPE_MASK)) {
501 		gchar const *radio = NULL;
502 		switch (cond->op[0]) {
503 		case GNM_FILTER_OP_TOP_N:
504 		default:
505 			radio = type_group[0];
506 			break;
507 		case GNM_FILTER_OP_BOTTOM_N:
508 			radio = type_group[1];
509 			break;
510 		case GNM_FILTER_OP_TOP_N_PERCENT:
511 			radio = type_group[2];
512 			break;
513 		case GNM_FILTER_OP_BOTTOM_N_PERCENT:
514 			radio = type_group[3];
515 			break;
516 		case GNM_FILTER_OP_TOP_N_PERCENT_N:
517 			radio = type_group[4];
518 			break;
519 		case GNM_FILTER_OP_BOTTOM_N_PERCENT_N:
520 			radio = type_group[5];
521 			break;
522 		}
523 		w = go_gtk_builder_get_widget (state->gui, radio);
524 		gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (w), TRUE);
525 	} else {
526 		w = go_gtk_builder_get_widget (state->gui, "items-largest");
527 		gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (w), TRUE);
528 	}
529 
530 	w = go_gtk_builder_get_widget (state->gui, "item_count");
531 	g_signal_connect (G_OBJECT (w),
532 			  "value-changed",
533 			  G_CALLBACK (cb_top10_count_changed), state);
534 	if (cond != NULL && GNM_FILTER_OP_TOP_N == (cond->op[0] & GNM_FILTER_OP_TYPE_MASK))
535 		gtk_spin_button_set_value (GTK_SPIN_BUTTON (w), cond->count);
536 	else
537 		gtk_spin_button_set_value (GTK_SPIN_BUTTON (w),
538 				   range_height(&(state->filter->r))/2);
539 	cb_top10_count_changed (GTK_SPIN_BUTTON (w), state);
540 	cb_top10_type_changed (NULL, state);
541 
542 	rb = type_group;
543 	while (*rb != NULL) {
544 		w = go_gtk_builder_get_widget (state->gui, *rb);
545 		g_signal_connect (G_OBJECT (w),
546 				  "toggled",
547 				  G_CALLBACK (cb_top10_type_changed), state);
548 		rb++;
549 	}
550 
551 
552 	w = go_gtk_builder_get_widget (state->gui, "ok_button");
553 	g_signal_connect (G_OBJECT (w),
554 		"clicked",
555 		G_CALLBACK (cb_autofilter_ok), state);
556 	w = go_gtk_builder_get_widget (state->gui, "cancel_button");
557 	g_signal_connect (G_OBJECT (w),
558 		"clicked",
559 		G_CALLBACK (cb_autofilter_cancel), state);
560 
561 	/* a candidate for merging into attach guru */
562 	gnm_init_help_button (
563 		go_gtk_builder_get_widget (state->gui, "help_button"),
564 		GNUMERIC_HELP_LINK_AUTOFILTER_TOP_TEN);
565 
566 	gnm_dialog_setup_destroy_handlers (GTK_DIALOG (state->dialog),
567 					   state->wbcg,
568 					   GNM_DIALOG_DESTROY_CURRENT_SHEET_REMOVED);
569 
570 	wbc_gtk_attach_guru (state->wbcg, state->dialog);
571 	g_object_set_data_full (G_OBJECT (state->dialog),
572 		"state", state, (GDestroyNotify)cb_autofilter_destroy);
573 
574 	gnm_keyed_dialog (wbcg, GTK_WINDOW (state->dialog),
575 			       DIALOG_KEY);
576 	gtk_widget_show (state->dialog);
577 }
578