1 /********************************************************************\
2  * gnc-amount-edit.h -- amount editor widget                        *
3  *                                                                  *
4  * Copyright (C) 2000 Dave Peticolas <dave@krondo.com>              *
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   *
9  * the License, or (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*
17  * along with this program; if not, contact:                        *
18  *                                                                  *
19  * Free Software Foundation           Voice:  +1-617-542-5942       *
20  * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652       *
21  * Boston, MA  02110-1301,  USA       gnu@gnu.org                   *
22 \********************************************************************/
23 /*
24   @NOTATION@
25  */
26 
27 #include <config.h>
28 
29 #include <gtk/gtk.h>
30 #include <gdk/gdkkeysyms.h>
31 #include <glib/gi18n.h>
32 
33 #include "gnc-amount-edit.h"
34 #include "gnc-exp-parser.h"
35 #include "gnc-locale-utils.h"
36 #include "gnc-ui-util.h"
37 #include "qof.h"
38 #include "dialog-utils.h"
39 #include "gnc-ui.h"
40 
41 #ifdef G_OS_WIN32
42 # include <gdk/gdkwin32.h>
43 #endif
44 
45 /* Signal codes */
46 enum
47 {
48     ACTIVATE,
49     CHANGED,
50     AMOUNT_CHANGED,
51     LAST_SIGNAL
52 };
53 
54 static guint amount_edit_signals [LAST_SIGNAL] = { 0 };
55 
56 static void gnc_amount_edit_init (GNCAmountEdit *gae);
57 static void gnc_amount_edit_class_init (GNCAmountEditClass *klass);
58 static void gnc_amount_edit_changed (GtkEditable *gae,
59                                      gpointer user_data);
60 static void gnc_amount_edit_paste_clipboard (GtkEntry *entry,
61                                              gpointer user_data);
62 static gint gnc_amount_edit_key_press (GtkWidget   *widget,
63                                        GdkEventKey *event,
64                                        gpointer user_data);
65 
66 #define GNC_AMOUNT_EDIT_PATH "gnc-amount-edit-path"
67 
G_DEFINE_TYPE(GNCAmountEdit,gnc_amount_edit,GTK_TYPE_BOX)68 G_DEFINE_TYPE (GNCAmountEdit, gnc_amount_edit, GTK_TYPE_BOX)
69 
70 static void
71 gnc_amount_edit_finalize (GObject *object)
72 {
73     g_return_if_fail (object != NULL);
74     g_return_if_fail (GNC_IS_AMOUNT_EDIT(object));
75 
76     G_OBJECT_CLASS (gnc_amount_edit_parent_class)->finalize (object);
77 }
78 
79 static void
gnc_amount_edit_dispose(GObject * object)80 gnc_amount_edit_dispose (GObject *object)
81 {
82     GNCAmountEdit *gae;
83 
84     g_return_if_fail (object != NULL);
85     g_return_if_fail (GNC_IS_AMOUNT_EDIT(object));
86 
87     gae = GNC_AMOUNT_EDIT(object);
88 
89     if (gae->disposed)
90         return;
91 
92     gae->disposed = TRUE;
93 
94     gtk_widget_destroy (GTK_WIDGET(gae->entry));
95     gae->entry = NULL;
96 
97     gtk_widget_destroy (GTK_WIDGET(gae->image));
98     gae->image = NULL;
99 
100     G_OBJECT_CLASS (gnc_amount_edit_parent_class)->dispose (object);
101 }
102 
103 static void
gnc_amount_edit_class_init(GNCAmountEditClass * klass)104 gnc_amount_edit_class_init (GNCAmountEditClass *klass)
105 {
106     GObjectClass *object_class = G_OBJECT_CLASS(klass);
107     GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
108 
109     object_class->dispose = gnc_amount_edit_dispose;
110     object_class->finalize = gnc_amount_edit_finalize;
111 
112     amount_edit_signals [ACTIVATE] =
113         g_signal_new ("activate",
114                       G_OBJECT_CLASS_TYPE(object_class),
115                       G_SIGNAL_RUN_FIRST,
116                       G_STRUCT_OFFSET(GNCAmountEditClass, activate),
117                       NULL,
118                       NULL,
119                       g_cclosure_marshal_VOID__VOID,
120                       G_TYPE_NONE,
121                       0);
122 
123     amount_edit_signals [CHANGED] =
124         g_signal_new ("changed",
125                       G_OBJECT_CLASS_TYPE(object_class),
126                       G_SIGNAL_RUN_FIRST,
127                       G_STRUCT_OFFSET(GNCAmountEditClass, changed),
128                       NULL,
129                       NULL,
130                       g_cclosure_marshal_VOID__VOID,
131                       G_TYPE_NONE,
132                       0);
133 
134     amount_edit_signals [AMOUNT_CHANGED] =
135         g_signal_new ("amount_changed",
136                       G_OBJECT_CLASS_TYPE(object_class),
137                       G_SIGNAL_RUN_FIRST,
138                       G_STRUCT_OFFSET(GNCAmountEditClass, amount_changed),
139                       NULL,
140                       NULL,
141                       g_cclosure_marshal_VOID__VOID,
142                       G_TYPE_NONE,
143                       0);
144 }
145 
146 static void
gnc_amount_edit_init(GNCAmountEdit * gae)147 gnc_amount_edit_init (GNCAmountEdit *gae)
148 {
149     gtk_orientable_set_orientation (GTK_ORIENTABLE(gae),
150                                     GTK_ORIENTATION_HORIZONTAL);
151 
152     gae->entry = GTK_ENTRY(gtk_entry_new());
153     gae->need_to_parse = FALSE;
154     gae->amount = gnc_numeric_zero ();
155     gae->print_info = gnc_default_print_info (FALSE);
156     gae->fraction = 0;
157     gae->evaluate_on_enter = FALSE;
158     gae->validate_on_change = FALSE;
159     gae->block_changed = FALSE;
160     gae->show_warning_symbol = TRUE;
161     gae->disposed = FALSE;
162 
163     // Set the name for this widget so it can be easily manipulated with css
164     gtk_widget_set_name (GTK_WIDGET(gae), "gnc-id-amount-edit");
165 
166     g_signal_connect (G_OBJECT(gae->entry), "key-press-event",
167                       G_CALLBACK(gnc_amount_edit_key_press), gae);
168 
169     g_signal_connect (G_OBJECT(gae->entry), "changed",
170                       G_CALLBACK(gnc_amount_edit_changed), gae);
171 
172     g_signal_connect (G_OBJECT(gae->entry), "paste-clipboard",
173                       G_CALLBACK(gnc_amount_edit_paste_clipboard), gae);
174 }
175 
176 static void
gnc_amount_edit_changed(GtkEditable * editable,gpointer user_data)177 gnc_amount_edit_changed (GtkEditable *editable, gpointer user_data)
178 {
179     GNCAmountEdit *gae = GNC_AMOUNT_EDIT(user_data);
180 
181     gae->need_to_parse = TRUE;
182 
183     if (gae->block_changed)
184         return;
185 
186     if (gae->validate_on_change)
187     {
188         gnc_numeric amount;
189         gnc_amount_edit_expr_is_valid (gae, &amount, TRUE, NULL);
190     }
191     g_signal_emit (gae, amount_edit_signals [CHANGED], 0);
192 }
193 
194 static void
gnc_amount_edit_paste_clipboard(GtkEntry * entry,gpointer user_data)195 gnc_amount_edit_paste_clipboard (GtkEntry *entry, gpointer user_data)
196 {
197     GNCAmountEdit *gae = GNC_AMOUNT_EDIT(user_data);
198     GtkClipboard *clipboard = gtk_widget_get_clipboard (GTK_WIDGET(entry),
199                                                         GDK_SELECTION_CLIPBOARD);
200     gchar *text = gtk_clipboard_wait_for_text (clipboard);
201     gchar *filtered_text;
202     gint start_pos, end_pos;
203     gint position;
204 
205     if (!text)
206         return;
207 
208     if (gtk_widget_get_visible (GTK_WIDGET(gae->image)))
209     {
210         gtk_widget_hide (GTK_WIDGET(gae->image));
211         gtk_widget_set_tooltip_text (GTK_WIDGET(gae->image), NULL);
212     }
213 
214     filtered_text = gnc_filter_text_for_control_chars (text);
215 
216     if (!filtered_text)
217     {
218         g_free (text);
219         return;
220     }
221 
222     position = gtk_editable_get_position (GTK_EDITABLE(entry));
223 
224     if (gtk_editable_get_selection_bounds (GTK_EDITABLE(entry),
225                                            &start_pos, &end_pos))
226     {
227         position = start_pos;
228 
229         gae->block_changed = TRUE;
230         gtk_editable_delete_selection (GTK_EDITABLE(entry));
231         gae->block_changed = FALSE;
232         gtk_editable_insert_text (GTK_EDITABLE(entry),
233                                   filtered_text, -1, &position);
234     }
235     else
236         gtk_editable_insert_text (GTK_EDITABLE(entry),
237                                   filtered_text, -1, &position);
238 
239     gtk_editable_set_position (GTK_EDITABLE(entry), position);
240 
241     g_signal_stop_emission_by_name (G_OBJECT(entry), "paste-clipboard");
242 
243     g_free (text);
244     g_free (filtered_text);
245 }
246 
247 static gint
gnc_amount_edit_key_press(GtkWidget * widget,GdkEventKey * event,gpointer user_data)248 gnc_amount_edit_key_press (GtkWidget *widget, GdkEventKey *event, gpointer user_data)
249 {
250     GNCAmountEdit *gae = GNC_AMOUNT_EDIT(user_data);
251     gint result;
252 
253     if (gtk_widget_get_visible (GTK_WIDGET(gae->image)))
254     {
255         gtk_widget_hide (GTK_WIDGET(gae->image));
256         gtk_widget_set_tooltip_text (GTK_WIDGET(gae->image), NULL);
257     }
258 
259 #ifdef G_OS_WIN32
260     /* gdk never sends GDK_KEY_KP_Decimal on win32. See #486658 */
261     if (event->hardware_keycode == VK_DECIMAL)
262         event->keyval = GDK_KEY_KP_Decimal;
263 #endif
264     if (event->keyval == GDK_KEY_KP_Decimal)
265     {
266         if (gae->print_info.monetary)
267         {
268             struct lconv *lc = gnc_localeconv ();
269             event->keyval = lc->mon_decimal_point[0];
270             event->string[0] = lc->mon_decimal_point[0];
271         }
272     }
273 
274     result = (* GTK_WIDGET_GET_CLASS(widget)->key_press_event)(widget, event);
275 
276     switch (event->keyval)
277     {
278     case GDK_KEY_Return:
279         if (event->state & (GDK_MODIFIER_INTENT_DEFAULT_MOD_MASK))
280             break;
281     case GDK_KEY_KP_Enter:
282         if (gae->evaluate_on_enter)
283             break;
284         else
285             g_signal_emit (gae, amount_edit_signals [ACTIVATE], 0);
286         return result;
287     default:
288         return result;
289     }
290 
291     gnc_amount_edit_evaluate (gae, NULL);
292     g_signal_emit (gae, amount_edit_signals [ACTIVATE], 0);
293 
294     return TRUE;
295 }
296 
297 GtkWidget *
gnc_amount_edit_new(void)298 gnc_amount_edit_new (void)
299 {
300     GNCAmountEdit *gae = g_object_new (GNC_TYPE_AMOUNT_EDIT, NULL);
301 
302     gtk_box_pack_start (GTK_BOX(gae), GTK_WIDGET(gae->entry), TRUE, TRUE, 0);
303     gtk_entry_set_width_chars (GTK_ENTRY(gae->entry), 12);
304     gae->image = gtk_image_new_from_icon_name ("dialog-warning", GTK_ICON_SIZE_SMALL_TOOLBAR);
305     gtk_box_pack_start (GTK_BOX(gae), GTK_WIDGET(gae->image), FALSE, FALSE, 6);
306     gtk_widget_set_no_show_all (GTK_WIDGET(gae->image), TRUE);
307     gtk_widget_hide (GTK_WIDGET(gae->image));
308     gtk_widget_show_all (GTK_WIDGET(gae));
309 
310     return GTK_WIDGET(gae);
311 }
312 
313 static gint
get_original_error_position(const gchar * string,const gchar * symbol,gint error_pos)314 get_original_error_position (const gchar *string, const gchar *symbol,
315                              gint error_pos)
316 {
317     gint original_error_pos = error_pos;
318     gint text_len;
319     gint symbol_len;
320 
321     if (error_pos == 0)
322         return 0;
323 
324     if (!string || !symbol)
325         return error_pos;
326 
327     if (g_strrstr (string, symbol) == NULL)
328         return error_pos;
329 
330     if (!g_utf8_validate (string, -1, NULL))
331         return error_pos;
332 
333     text_len = g_utf8_strlen (string, -1);
334     symbol_len = g_utf8_strlen (symbol, -1);
335 
336     for (gint x = 0; x < text_len; x++)
337     {
338         gchar *temp = g_utf8_offset_to_pointer (string, x);
339 
340         if (g_str_has_prefix (temp, symbol))
341             original_error_pos = original_error_pos + symbol_len;
342 
343         if (x >= original_error_pos)
344             break;
345 
346         if (g_strrstr (temp, symbol) == NULL)
347             break;
348     }
349     return original_error_pos;
350 }
351 
352 static inline GQuark
exp_validate_quark(void)353 exp_validate_quark (void)
354 {
355     return g_quark_from_static_string ("exp_validate");
356 }
357 
358 gint
gnc_amount_edit_expr_is_valid(GNCAmountEdit * gae,gnc_numeric * amount,gboolean empty_ok,GError ** error)359 gnc_amount_edit_expr_is_valid (GNCAmountEdit *gae, gnc_numeric *amount,
360                                gboolean empty_ok, GError **error)
361 {
362     const char *string;
363     char *error_loc;
364     gboolean ok;
365     gchar *err_msg = NULL;
366     gint err_code;
367     const gnc_commodity *comm;
368     char *filtered_string;
369     const gchar *symbol = NULL;
370 
371     g_return_val_if_fail (gae != NULL, -1);
372     g_return_val_if_fail (GNC_IS_AMOUNT_EDIT(gae), -1);
373 
374     string = gtk_entry_get_text (GTK_ENTRY(gae->entry));
375 
376     if (gtk_widget_get_visible (GTK_WIDGET(gae->image)))
377     {
378         gtk_widget_hide (GTK_WIDGET(gae->image));
379         gtk_widget_set_tooltip_text (GTK_WIDGET(gae->image), NULL);
380     }
381 
382     comm = gae->print_info.commodity;
383 
384     filtered_string = gnc_filter_text_for_currency_commodity (comm, string, &symbol);
385 
386     if (!filtered_string || *filtered_string == '\0')
387     {
388         *amount = gnc_numeric_zero ();
389         g_free (filtered_string);
390         if (empty_ok)
391             return -1; /* indicate an empty field */
392         else
393             return 0; /* indicate successfully parsed as 0 */
394     }
395 
396     error_loc = NULL;
397     ok = gnc_exp_parser_parse (filtered_string, amount, &error_loc);
398 
399     if (ok)
400     {
401         g_free (filtered_string);
402         return 0;
403     }
404 
405     /* Not ok */
406     if (error_loc != NULL)
407     {
408         err_code = get_original_error_position (string, symbol,
409                                                (error_loc - filtered_string));
410 
411         err_msg = g_strdup_printf (_("An error occurred while processing '%s' at position %d"),
412                                    string, err_code);
413     }
414     else
415     {
416         err_code = 1000;
417         err_msg = g_strdup_printf (_("An error occurred while processing '%s'"),
418                                    string);
419     }
420 
421     if (error)
422         g_set_error_literal (error, exp_validate_quark(), err_code, err_msg);
423 
424     if (gae->show_warning_symbol)
425     {
426         gtk_widget_set_tooltip_text (GTK_WIDGET(gae->image), err_msg);
427         gtk_widget_show (GTK_WIDGET(gae->image));
428         gtk_widget_queue_resize (GTK_WIDGET(gae->entry));
429     }
430 
431     g_free (filtered_string);
432     g_free (err_msg);
433     return 1;
434 }
435 
436 gboolean
gnc_amount_edit_evaluate(GNCAmountEdit * gae,GError ** error)437 gnc_amount_edit_evaluate (GNCAmountEdit *gae, GError **error)
438 {
439     gint result;
440     gnc_numeric amount;
441     GError *tmp_error = NULL;
442 
443     g_return_val_if_fail (gae != NULL, FALSE);
444     g_return_val_if_fail (GNC_IS_AMOUNT_EDIT(gae), FALSE);
445 
446     if (!gae->need_to_parse)
447         return TRUE;
448 
449     result = gnc_amount_edit_expr_is_valid (gae, &amount, FALSE, &tmp_error);
450 
451     if (result == -1)  /* field was empty and may remain so */
452         return TRUE;
453 
454     if (result == 0)  /* parsing successful */
455     {
456         gnc_numeric old_amount = gae->amount;
457 
458         if (gae->fraction > 0)
459             amount = gnc_numeric_convert (amount, gae->fraction, GNC_HOW_RND_ROUND_HALF_UP);
460 
461         gnc_amount_edit_set_amount (gae, amount);
462 
463         if (!gnc_numeric_equal (amount, old_amount))
464             g_signal_emit (gae, amount_edit_signals [AMOUNT_CHANGED], 0);
465 
466         gtk_editable_set_position (GTK_EDITABLE(gae->entry), -1);
467         return TRUE;
468     }
469 
470     /* Parse error */
471     if (tmp_error)
472     {
473         if (tmp_error->code < 1000)
474             gtk_editable_set_position (GTK_EDITABLE(gae->entry), tmp_error->code);
475 
476         if (error)
477             g_propagate_error (error, tmp_error);
478         else
479             g_error_free (tmp_error);
480     }
481     return FALSE;
482 }
483 
484 gnc_numeric
gnc_amount_edit_get_amount(GNCAmountEdit * gae)485 gnc_amount_edit_get_amount (GNCAmountEdit *gae)
486 {
487     g_return_val_if_fail (gae != NULL, gnc_numeric_zero ());
488     g_return_val_if_fail (GNC_IS_AMOUNT_EDIT(gae), gnc_numeric_zero ());
489 
490     gnc_amount_edit_evaluate (gae, NULL);
491 
492     return gae->amount;
493 }
494 
495 double
gnc_amount_edit_get_damount(GNCAmountEdit * gae)496 gnc_amount_edit_get_damount (GNCAmountEdit *gae)
497 {
498     g_return_val_if_fail (gae != NULL, 0.0);
499     g_return_val_if_fail (GNC_IS_AMOUNT_EDIT(gae), 0.0);
500 
501     gnc_amount_edit_evaluate (gae, NULL);
502 
503     return gnc_numeric_to_double (gae->amount);
504 }
505 
506 void
gnc_amount_edit_set_amount(GNCAmountEdit * gae,gnc_numeric amount)507 gnc_amount_edit_set_amount (GNCAmountEdit *gae, gnc_numeric amount)
508 {
509     const char * amount_string;
510 
511     g_return_if_fail (gae != NULL);
512     g_return_if_fail (GNC_IS_AMOUNT_EDIT(gae));
513     g_return_if_fail (!gnc_numeric_check (amount));
514 
515     if (gtk_widget_get_visible (GTK_WIDGET(gae->image)))
516     {
517         gtk_widget_hide (GTK_WIDGET(gae->image));
518         gtk_widget_set_tooltip_text (GTK_WIDGET(gae->image), NULL);
519     }
520 
521     /* Update the display. */
522     amount_string = xaccPrintAmount (amount, gae->print_info);
523     gtk_entry_set_text (GTK_ENTRY(gae->entry), amount_string);
524 
525     gae->amount = amount;
526     gae->need_to_parse = FALSE;
527 }
528 
529 void
gnc_amount_edit_set_damount(GNCAmountEdit * gae,double damount)530 gnc_amount_edit_set_damount (GNCAmountEdit *gae, double damount)
531 {
532     gnc_numeric amount;
533     int fraction;
534 
535     g_return_if_fail (gae != NULL);
536     g_return_if_fail (GNC_IS_AMOUNT_EDIT(gae));
537 
538     if (gae->fraction > 0)
539         fraction = gae->fraction;
540     else
541         fraction = 100000;
542 
543     amount = double_to_gnc_numeric (damount, fraction, GNC_HOW_RND_ROUND_HALF_UP);
544 
545     gnc_amount_edit_set_amount (gae, amount);
546 }
547 
548 void
gnc_amount_edit_set_print_info(GNCAmountEdit * gae,GNCPrintAmountInfo print_info)549 gnc_amount_edit_set_print_info (GNCAmountEdit *gae,
550                                 GNCPrintAmountInfo print_info)
551 {
552     g_return_if_fail (gae != NULL);
553     g_return_if_fail (GNC_IS_AMOUNT_EDIT(gae));
554 
555     gae->print_info = print_info;
556     gae->print_info.use_symbol = 0;
557 }
558 
559 void
gnc_amount_edit_set_fraction(GNCAmountEdit * gae,int fraction)560 gnc_amount_edit_set_fraction (GNCAmountEdit *gae, int fraction)
561 {
562     g_return_if_fail (gae != NULL);
563     g_return_if_fail (GNC_IS_AMOUNT_EDIT(gae));
564 
565     fraction = MAX (0, fraction);
566 
567     gae->fraction = fraction;
568 }
569 
570 GtkWidget *
gnc_amount_edit_gtk_entry(GNCAmountEdit * gae)571 gnc_amount_edit_gtk_entry (GNCAmountEdit *gae)
572 {
573     g_return_val_if_fail (gae != NULL, NULL);
574     g_return_val_if_fail (GNC_IS_AMOUNT_EDIT(gae), NULL);
575 
576     return GTK_WIDGET(gae->entry);
577 }
578 
579 void
gnc_amount_edit_set_evaluate_on_enter(GNCAmountEdit * gae,gboolean evaluate_on_enter)580 gnc_amount_edit_set_evaluate_on_enter (GNCAmountEdit *gae,
581                                        gboolean evaluate_on_enter)
582 {
583     g_return_if_fail (gae != NULL);
584     g_return_if_fail (GNC_IS_AMOUNT_EDIT(gae));
585 
586     gae->evaluate_on_enter = evaluate_on_enter;
587 }
588 
589 void
gnc_amount_edit_set_validate_on_change(GNCAmountEdit * gae,gboolean validate_on_change)590 gnc_amount_edit_set_validate_on_change (GNCAmountEdit *gae,
591                                         gboolean validate_on_change)
592 {
593     g_return_if_fail (gae != NULL);
594     g_return_if_fail (GNC_IS_AMOUNT_EDIT(gae));
595 
596     gae->validate_on_change = validate_on_change;
597 }
598 
599 void
gnc_amount_edit_select_region(GNCAmountEdit * gae,gint start_pos,gint end_pos)600 gnc_amount_edit_select_region (GNCAmountEdit *gae,
601                                gint start_pos,
602                                gint end_pos)
603 {
604     g_return_if_fail (gae != NULL);
605     g_return_if_fail (GNC_IS_AMOUNT_EDIT(gae));
606 
607     gtk_editable_select_region (GTK_EDITABLE(gae->entry),
608                                 start_pos,
609                                 end_pos);
610 }
611 
612 void
gnc_amount_edit_show_warning_symbol(GNCAmountEdit * gae,gboolean show)613 gnc_amount_edit_show_warning_symbol (GNCAmountEdit *gae, gboolean show)
614 {
615     g_return_if_fail (gae != NULL);
616     g_return_if_fail (GNC_IS_AMOUNT_EDIT(gae));
617 
618     gae->show_warning_symbol = show;
619 }
620 
621 void
gnc_amount_edit_make_mnemonic_target(GNCAmountEdit * gae,GtkWidget * label)622 gnc_amount_edit_make_mnemonic_target (GNCAmountEdit *gae, GtkWidget *label)
623 {
624     if (!gae)
625         return;
626 
627     gtk_label_set_mnemonic_widget (GTK_LABEL(label), GTK_WIDGET(gae->entry));
628 }
629