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