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