GnuCash c935c2f+
Loading...
Searching...
No Matches
gnucash-item-edit.c
1/********************************************************************\
2 * gnucash-item-edit.c -- cell editor cut-n-paste from gnumeric *
3 * *
4 * This program is free software; you can redistribute it and/or *
5 * modify it under the terms of the GNU General Public License as *
6 * published by the Free Software Foundation; either version 2 of *
7 * the License, or (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License*
15 * along with this program; if not, contact: *
16 * *
17 * Free Software Foundation Voice: +1-617-542-5942 *
18 * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
19 * Boston, MA 02110-1301, USA gnu@gnu.org *
20 * *
21\********************************************************************/
22
23/*
24 * An editor for the gnucash sheet.
25 * Cut and pasted from the gnumeric item-edit.c file.
26 *
27 * And then substantially rewritten by Dave Peticolas <dave@krondo.com>.
28 */
29
30
31#include <config.h>
32
33#include <string.h>
34#include <qof.h>
35
36#include "gnucash-color.h"
37#include "gnucash-cursor.h"
38#include "gnucash-item-edit.h"
39#include "gnucash-sheet.h"
40#include "gnucash-sheetP.h"
41#include "gnucash-style.h"
42
43#include "gnc-ui-util.h"
44
45/* The arguments we take */
46enum
47{
48 PROP_0,
49 PROP_SHEET, /* The sheet property */
50};
51
52/* values for selection info */
53enum
54{
55 TARGET_UTF8_STRING,
56 TARGET_STRING,
57 TARGET_TEXT,
58 TARGET_COMPOUND_TEXT
59};
60
61#define MIN_BUTT_WIDTH 20 // minimum size for a button excluding border
62
63static QofLogModule log_module = G_LOG_DOMAIN;
64
65static void gnc_item_edit_destroying (GtkWidget *this, gpointer data);
66
67G_DEFINE_TYPE (GncItemEdit, gnc_item_edit, GTK_TYPE_BOX)
68
69G_DEFINE_TYPE (GncItemEditTb, gnc_item_edit_tb, GTK_TYPE_TOGGLE_BUTTON)
70
71static void
72gnc_item_edit_tb_init (GncItemEditTb *item_edit_tb)
73{
74 item_edit_tb->sheet = NULL;
75}
76
77static void
78gnc_item_edit_tb_get_property (GObject *object,
79 guint param_id,
80 GValue *value,
81 GParamSpec *pspec)
82{
83 GncItemEditTb *item_edit_tb = GNC_ITEM_EDIT_TB(object);
84
85 switch (param_id)
86 {
87 case PROP_SHEET:
88 g_value_take_object (value, item_edit_tb->sheet);
89 break;
90 default:
91 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
92 break;
93 }
94}
95
96static void
97gnc_item_edit_tb_set_property (GObject *object,
98 guint param_id,
99 const GValue *value,
100 GParamSpec *pspec)
101{
102 GncItemEditTb *item_edit_tb = GNC_ITEM_EDIT_TB(object);
103
104 switch (param_id)
105 {
106 case PROP_SHEET:
107 item_edit_tb->sheet = GNUCASH_SHEET(g_value_get_object (value));
108 break;
109 default:
110 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
111 break;
112 }
113}
114
115static void
116gnc_item_edit_tb_get_preferred_width (GtkWidget *widget,
117 gint *minimal_width,
118 gint *natural_width)
119{
120 GncItemEditTb *tb = GNC_ITEM_EDIT_TB(widget);
121 GncItemEdit *item_edit = GNC_ITEM_EDIT(tb->sheet->item_editor);
122 GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET(tb));
123 GtkBorder border;
124 gint x, y, w, h = 2, width = 0;
125 gnc_item_edit_get_pixel_coords (GNC_ITEM_EDIT(item_edit), &x, &y, &w, &h);
126 width = ((h - 2)*2)/3;
127
128 gtk_style_context_get_border (context, GTK_STATE_FLAG_NORMAL, &border);
129
130 if (width < MIN_BUTT_WIDTH + border.left + border.right)
131 width = MIN_BUTT_WIDTH + border.left + border.right;
132
133 *minimal_width = *natural_width = width;
134 item_edit->button_width = width;
135}
136
137static void
138gnc_item_edit_tb_get_preferred_height (GtkWidget *widget,
139 gint *minimal_width,
140 gint *natural_width)
141{
142 GncItemEditTb *tb = GNC_ITEM_EDIT_TB(widget);
143 GncItemEdit *item_edit = GNC_ITEM_EDIT(tb->sheet->item_editor);
144 gint x, y, w, h = 2;
145 gnc_item_edit_get_pixel_coords (GNC_ITEM_EDIT(item_edit), &x, &y, &w, &h);
146 *minimal_width = *natural_width = (h - 2);
147}
148
149static void
150gnc_item_edit_tb_class_init (GncItemEditTbClass *gnc_item_edit_tb_class)
151{
152 GObjectClass *object_class;
153 GtkWidgetClass *widget_class;
154
155 gtk_widget_class_set_css_name (GTK_WIDGET_CLASS(gnc_item_edit_tb_class), "button");
156
157 object_class = G_OBJECT_CLASS(gnc_item_edit_tb_class);
158 widget_class = GTK_WIDGET_CLASS(gnc_item_edit_tb_class);
159
160 object_class->get_property = gnc_item_edit_tb_get_property;
161 object_class->set_property = gnc_item_edit_tb_set_property;
162
163 g_object_class_install_property (object_class,
164 PROP_SHEET,
165 g_param_spec_object ("sheet",
166 "Sheet Value",
167 "Sheet Value",
168 GNUCASH_TYPE_SHEET,
169 G_PARAM_READWRITE));
170
171 /* GtkWidget method overrides */
172 widget_class->get_preferred_width = gnc_item_edit_tb_get_preferred_width;
173 widget_class->get_preferred_height = gnc_item_edit_tb_get_preferred_height;
174}
175
176GtkWidget *
177gnc_item_edit_tb_new (GnucashSheet *sheet)
178{
179 GtkStyleContext *context;
180 GncItemEditTb *item_edit_tb = g_object_new (GNC_TYPE_ITEM_EDIT_TB,
181 "sheet", sheet,
182 NULL);
183
184 context = gtk_widget_get_style_context (GTK_WIDGET(item_edit_tb));
185 gtk_style_context_add_class (context, GTK_STYLE_CLASS_BUTTON);
186
187 return GTK_WIDGET(item_edit_tb);
188}
189
190static gboolean
191tb_button_press_cb (G_GNUC_UNUSED GtkWidget *widget, GdkEventButton *event,
192 G_GNUC_UNUSED gpointer *user_data)
193{
194 /* Ignore double-clicks and triple-clicks */
195 if (event->button == 3 && event->type == GDK_BUTTON_PRESS)
196 {
197 // block a right click
198 return TRUE;
199 }
200 return FALSE;
201}
202
203/*
204 * Returns the coordinates for the editor bounding box
205 */
206void
207gnc_item_edit_get_pixel_coords (GncItemEdit *item_edit,
208 int *x, int *y,
209 int *w, int *h)
210{
211 GnucashSheet *sheet = item_edit->sheet;
212 SheetBlock *block;
213 int xd, yd;
214
215 if (sheet == NULL)
216 return;
217
218 block = gnucash_sheet_get_block (sheet, item_edit->virt_loc.vcell_loc);
219 if (block == NULL)
220 return;
221
222 xd = block->origin_x;
223 yd = block->origin_y;
224
225 gnucash_sheet_style_get_cell_pixel_rel_coords (item_edit->style,
226 item_edit->virt_loc.phys_row_offset,
227 item_edit->virt_loc.phys_col_offset,
228 x, y, w, h);
229
230 // alter cell size of first column
231 if (item_edit->virt_loc.phys_col_offset == 0)
232 {
233 *x = *x + 1;
234 *w = *w - 1;
235 }
236 *x += xd;
237 *y += yd;
238}
239
240static gboolean
241gnc_item_edit_update (GncItemEdit *item_edit)
242{
243 gint x = 0, y = 0, w, h;
244
245 if (item_edit == NULL || item_edit->sheet == NULL)
246 return FALSE;
247 gnc_item_edit_get_pixel_coords (item_edit, &x, &y, &w, &h);
248 gtk_layout_move (GTK_LAYOUT(item_edit->sheet),
249 GTK_WIDGET(item_edit), x, y);
250
251 if (item_edit->is_popup)
252 {
253 gtk_widget_show (item_edit->popup_toggle.ebox);
254 if (item_edit->show_popup)
255 gnc_item_edit_show_popup (item_edit);
256 }
257 return FALSE;
258}
259
260void
261gnc_item_edit_focus_in (GncItemEdit *item_edit)
262{
263 GdkEventFocus ev;
264
265 g_return_if_fail (item_edit != NULL);
266 g_return_if_fail (GNC_IS_ITEM_EDIT(item_edit));
267
268 ev.type = GDK_FOCUS_CHANGE;
269 ev.window = gtk_widget_get_window (GTK_WIDGET(item_edit->sheet));
270 ev.in = TRUE;
271 gtk_widget_event (item_edit->editor, (GdkEvent*) &ev);
272}
273
274void
275gnc_item_edit_focus_out (GncItemEdit *item_edit)
276{
277 GdkEventFocus ev;
278
279 g_return_if_fail (item_edit != NULL);
280 g_return_if_fail (GNC_IS_ITEM_EDIT(item_edit));
281
282 if (item_edit->show_popup)
283 return; // Prevent recursion
284
285 ev.type = GDK_FOCUS_CHANGE;
286 ev.window = gtk_widget_get_window (GTK_WIDGET(item_edit->sheet));
287 ev.in = FALSE;
288 gtk_widget_event (item_edit->editor, (GdkEvent*) &ev);
289}
290
291/*
292 * Instance initialization
293 */
294static void
295gnc_item_edit_init (GncItemEdit *item_edit)
296{
297 /* Set invalid values so that we know when we have been fully
298 initialized */
299 gtk_orientable_set_orientation (GTK_ORIENTABLE(item_edit),
300 GTK_ORIENTATION_HORIZONTAL);
301
302 item_edit->sheet = NULL;
303 item_edit->editor = NULL;
304 item_edit->preedit_length = 0;
305
306 item_edit->is_popup = FALSE;
307 item_edit->show_popup = FALSE;
308
309 item_edit->popup_toggle.ebox = NULL;
310 item_edit->popup_toggle.tbutton = NULL;
311 item_edit->popup_toggle.arrow_down = TRUE;
312 item_edit->popup_toggle.signals_connected = FALSE;
313
314 item_edit->popup_item = NULL;
315 item_edit->popup_get_height = NULL;
316 item_edit->popup_autosize = NULL;
317 item_edit->popup_set_focus = NULL;
318 item_edit->popup_post_show = NULL;
319 item_edit->popup_user_data = NULL;
320 item_edit->popup_returned_height = 0;
321 item_edit->popup_height_signal_id = 0;
322 item_edit->popup_allocation_height = -1;
323
324 item_edit->style = NULL;
325 item_edit->button_width = MIN_BUTT_WIDTH;
326
327 gnc_virtual_location_init (&item_edit->virt_loc);
328}
329
330void
331gnc_item_edit_configure (GncItemEdit *item_edit)
332{
333 GnucashSheet *sheet = item_edit->sheet;
334 GnucashCursor *cursor;
335 gfloat xalign;
336
337 cursor = GNUCASH_CURSOR(sheet->cursor);
338
339 item_edit->virt_loc.vcell_loc.virt_row = cursor->row;
340 item_edit->virt_loc.vcell_loc.virt_col = cursor->col;
341
342 item_edit->style = gnucash_sheet_get_style (sheet,
343 item_edit->virt_loc.vcell_loc);
344
345 item_edit->virt_loc.phys_row_offset = cursor->cell.row;
346 item_edit->virt_loc.phys_col_offset = cursor->cell.col;
347
348 switch (gnc_table_get_align (sheet->table, item_edit->virt_loc))
349 {
350 default:
351 case CELL_ALIGN_LEFT:
352 xalign = 0;
353 break;
354
355 case CELL_ALIGN_RIGHT:
356 xalign = 1;
357 break;
358
359 case CELL_ALIGN_CENTER:
360 xalign = 0.5;
361 break;
362 }
363 gtk_entry_set_alignment (GTK_ENTRY(item_edit->editor), xalign);
364
365 if (!gnc_table_is_popup (sheet->table, item_edit->virt_loc))
366 gnc_item_edit_set_popup (item_edit, NULL, NULL, NULL,
367 NULL, NULL, NULL, NULL);
368
369 g_idle_add_full (G_PRIORITY_HIGH_IDLE,
370 (GSourceFunc)gnc_item_edit_update, item_edit, NULL);
371}
372
373
374void
375gnc_item_edit_cut_clipboard (GncItemEdit *item_edit)
376{
377 gtk_editable_cut_clipboard (GTK_EDITABLE(item_edit->editor));
378}
379
380void
381gnc_item_edit_copy_clipboard (GncItemEdit *item_edit)
382{
383 gtk_editable_copy_clipboard (GTK_EDITABLE(item_edit->editor));
384}
385
386void
387gnc_item_edit_paste_clipboard (GncItemEdit *item_edit)
388{
389 GtkClipboard *clipboard = gtk_widget_get_clipboard (GTK_WIDGET(item_edit->editor),
390 GDK_SELECTION_CLIPBOARD);
391 gchar *text = gtk_clipboard_wait_for_text (clipboard);
392 gchar *filtered_text;
393 gint start_pos, end_pos;
394 gint position;
395
396 if (!text)
397 return;
398
399 filtered_text = gnc_filter_text_for_control_chars (text);
400
401 if (!filtered_text)
402 {
403 g_free (text);
404 return;
405 }
406
407 position = gtk_editable_get_position (GTK_EDITABLE(item_edit->editor));
408
409 if (gtk_editable_get_selection_bounds (GTK_EDITABLE(item_edit->editor),
410 &start_pos, &end_pos))
411 {
412 position = start_pos;
413
414 gtk_editable_delete_selection (GTK_EDITABLE(item_edit->editor));
415 gtk_editable_insert_text (GTK_EDITABLE(item_edit->editor),
416 filtered_text, -1, &position);
417 }
418 else
419 gtk_editable_insert_text (GTK_EDITABLE(item_edit->editor),
420 filtered_text, -1, &position);
421
422 gtk_editable_set_position (GTK_EDITABLE(item_edit->editor), position);
423
424 g_free (text);
425 g_free (filtered_text);
426}
427
428
429static gboolean
430key_press_popup_cb (GtkWidget *widget, GdkEventKey *event, gpointer data)
431{
432 GncItemEdit *item_edit = GNC_ITEM_EDIT(data);
433
434 g_signal_stop_emission_by_name (widget, "key_press_event");
435
436 gtk_widget_event (GTK_WIDGET(item_edit->sheet), (GdkEvent *) event);
437
438 return TRUE;
439}
440
441
442static void
443gnc_item_edit_popup_toggled (GtkToggleButton *button, gpointer data)
444{
445 GncItemEdit *item_edit = GNC_ITEM_EDIT(data);
446 gboolean show_popup;
447
448 show_popup = gtk_toggle_button_get_active (button);
449 if (show_popup)
450 {
451 Table *table;
452 VirtualLocation virt_loc;
453
454 table = item_edit->sheet->table;
455 virt_loc = table->current_cursor_loc;
456
457 if (!gnc_table_confirm_change (table, virt_loc))
458 {
459 g_signal_handlers_block_matched
460 (button, G_SIGNAL_MATCH_DATA,
461 0, 0, NULL, NULL, data);
462
463 gtk_toggle_button_set_active (button, FALSE);
464
465 g_signal_handlers_unblock_matched
466 (button, G_SIGNAL_MATCH_DATA,
467 0, 0, NULL, NULL, data);
468
469 return;
470 }
471 }
472
473 item_edit->show_popup = show_popup;
474
475 if (!item_edit->show_popup)
476 gnc_item_edit_hide_popup (item_edit);
477
478 gnc_item_edit_configure (item_edit);
479}
480
481
482static void
483block_toggle_signals (GncItemEdit *item_edit)
484{
485 GObject *obj;
486
487 if (!item_edit->popup_toggle.signals_connected)
488 return;
489
490 obj = G_OBJECT(item_edit->popup_toggle.tbutton);
491
492 g_signal_handlers_block_matched (obj, G_SIGNAL_MATCH_DATA,
493 0, 0, NULL, NULL, item_edit);
494}
495
496
497static void
498unblock_toggle_signals (GncItemEdit *item_edit)
499{
500 GObject *obj;
501
502 if (!item_edit->popup_toggle.signals_connected)
503 return;
504
505 obj = G_OBJECT(item_edit->popup_toggle.tbutton);
506
507 g_signal_handlers_unblock_matched (obj, G_SIGNAL_MATCH_DATA,
508 0, 0, NULL, NULL, item_edit);
509}
510
511
512static gboolean
513draw_background_cb (GtkWidget *widget, cairo_t *cr, gpointer user_data)
514{
515 GtkStyleContext *stylectxt = gtk_widget_get_style_context (widget);
516 GncItemEdit *item_edit = GNC_ITEM_EDIT(user_data);
517 gint width = gtk_widget_get_allocated_width (widget);
518 gint height = gtk_widget_get_allocated_height (widget);
519 guint32 color_type;
520
521 gtk_style_context_save (stylectxt);
522
523 // Get the color type and apply the css class
524 color_type = gnc_table_get_color (item_edit->sheet->table, item_edit->virt_loc, NULL);
525 gnucash_get_style_classes (item_edit->sheet, stylectxt, color_type, FALSE);
526
527 gtk_render_background (stylectxt, cr, 0, 1, width, height - 2);
528
529 gtk_style_context_restore (stylectxt);
530 return FALSE;
531}
532
533/* The signal is emitted at the beginning of gtk_entry_preedit_changed_cb which
534 * proceeds to set its private members preedit_length = strlen(preedit) and
535 * preeditc_cursor = g_utf8_strlen(preedit, -1), then calls gtk_entry_recompute
536 * which in turn queues a redraw.
537 */
538static void
539preedit_changed_cb (GtkEntry* entry, gchar *preedit, GncItemEdit* item_edit)
540{
541 item_edit->preedit_length = g_utf8_strlen (preedit, -1); // Note codepoints not bytes
542 DEBUG("%s %lu", preedit, item_edit->preedit_length);
543}
544
545
546static gboolean
547draw_text_cursor_cb (GtkWidget *widget, cairo_t *cr, gpointer user_data)
548{
549 GncItemEdit *item_edit = GNC_ITEM_EDIT(user_data);
550 GtkEditable *editable = GTK_EDITABLE(widget);
551 GtkStyleContext *stylectxt = gtk_widget_get_style_context (GTK_WIDGET(widget));
552 GtkStateFlags flags = gtk_widget_get_state_flags (GTK_WIDGET(widget));
553 gint height = gtk_widget_get_allocated_height (widget);
554 PangoLayout *layout = gtk_entry_get_layout (GTK_ENTRY(widget));
555 const char *pango_text = pango_layout_get_text (layout);
556 GdkRGBA *fg_color;
557 GdkRGBA color;
558 gint x_offset;
559 gint cursor_x = 0;
560
561 // Get the layout x offset
562 gtk_entry_get_layout_offsets (GTK_ENTRY(widget), &x_offset, NULL);
563
564 // Get the foreground color
565 gdk_rgba_parse (&color, "black");
566 gtk_style_context_get_color (stylectxt, flags, &color);
567 fg_color = &color;
568
569
570 if (pango_text && *pango_text)
571 {
572 PangoRectangle strong_pos;
573 glong text_len = g_utf8_strlen (pango_text, -1);
574 gint cursor_pos =
575 gtk_editable_get_position (editable) + item_edit->preedit_length;
576 gint cursor_byte_pos = cursor_pos < text_len ?
577 g_utf8_offset_to_pointer (pango_text, cursor_pos) - pango_text :
578 strlen (pango_text);
579 DEBUG("Cursor: %d, byte offset %d, text byte len %zu", cursor_pos,
580 cursor_byte_pos, strlen (pango_text));
581 pango_layout_get_cursor_pos (layout, cursor_byte_pos,
582 &strong_pos, NULL);
583 cursor_x = x_offset + PANGO_PIXELS (strong_pos.x);
584 }
585 else
586 {
587 DEBUG("No text, cursor at %d.", x_offset);
588 cursor_x = x_offset;
589 }
590 // Now draw a vertical line
591 cairo_set_source_rgb (cr, fg_color->red, fg_color->green, fg_color->blue);
592 cairo_set_line_width (cr, 1.0);
593
594 cairo_move_to (cr, cursor_x + 0.5,
595 gnc_item_edit_get_margin (item_edit, top) +
596 gnc_item_edit_get_padding_border (item_edit, top));
597 cairo_rel_line_to (cr, 0,
598 height - gnc_item_edit_get_margin (item_edit, top_bottom)
599 - gnc_item_edit_get_padding_border (item_edit,
600 top_bottom));
601
602 cairo_stroke (cr);
603
604 return FALSE;
605}
606
607
608static gboolean
609draw_arrow_cb (GtkWidget *widget, cairo_t *cr, gpointer data)
610{
611 GncItemEdit *item_edit = GNC_ITEM_EDIT(data);
612 GtkStyleContext *context = gtk_widget_get_style_context (widget);
613 gint width = gtk_widget_get_allocated_width (widget);
614 gint height = gtk_widget_get_allocated_height (widget);
615 gint size;
616
617 // allow room for a border
618 gtk_render_background (context, cr, 2, 2, width - 4, height - 4);
619
620 gtk_style_context_add_class (context, GTK_STYLE_CLASS_ARROW);
621
622 size = MIN(width / 2, height / 2);
623
624 if (item_edit->popup_toggle.arrow_down == 0)
625 gtk_render_arrow (context, cr, 0,
626 (width - size)/2, (height - size)/2, size);
627 else
628 gtk_render_arrow (context, cr, G_PI,
629 (width - size)/2, (height - size)/2, size);
630
631 return FALSE;
632}
633
634
635static void
636connect_popup_toggle_signals (GncItemEdit *item_edit)
637{
638 GObject *object;
639
640 g_return_if_fail (GNC_IS_ITEM_EDIT(item_edit));
641
642 if (item_edit->popup_toggle.signals_connected)
643 return;
644
645 object = G_OBJECT(item_edit->popup_toggle.tbutton);
646
647 g_signal_connect (object, "toggled",
648 G_CALLBACK(gnc_item_edit_popup_toggled),
649 item_edit);
650
651 g_signal_connect (object, "key_press_event",
652 G_CALLBACK(key_press_popup_cb),
653 item_edit);
654
655 g_signal_connect_after (object, "draw",
656 G_CALLBACK(draw_arrow_cb),
657 item_edit);
658
659 item_edit->popup_toggle.signals_connected = TRUE;
660}
661
662
663static void
664disconnect_popup_toggle_signals (GncItemEdit *item_edit)
665{
666 g_return_if_fail (GNC_IS_ITEM_EDIT(item_edit));
667
668 if (!item_edit->popup_toggle.signals_connected)
669 return;
670
671 g_signal_handlers_disconnect_matched (item_edit->popup_toggle.tbutton,
672 G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, item_edit);
673
674 item_edit->popup_toggle.signals_connected = FALSE;
675}
676
677/* Note that g_value_set_object() refs the object, as does
678 * g_object_get(). But g_object_get() only unrefs once when it disgorges
679 * the object, leaving an unbalanced ref, which leaks. So instead of
680 * using g_value_set_object(), use g_value_take_object() which doesn't
681 * ref the object when used in get_property().
682 */
683static void
684gnc_item_edit_get_property (GObject *object,
685 guint param_id,
686 GValue *value,
687 GParamSpec *pspec)
688{
689 GncItemEdit *item_edit = GNC_ITEM_EDIT(object);
690
691 switch (param_id)
692 {
693 case PROP_SHEET:
694 g_value_take_object (value, item_edit->sheet);
695 break;
696 default:
697 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
698 break;
699 }
700}
701
702static void
703gnc_item_edit_set_property (GObject *object,
704 guint param_id,
705 const GValue *value,
706 GParamSpec *pspec)
707{
708 GncItemEdit *item_edit = GNC_ITEM_EDIT(object);
709 switch (param_id)
710 {
711 case PROP_SHEET:
712 item_edit->sheet = GNUCASH_SHEET(g_value_get_object (value));
713 break;
714 default:
715 G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
716 break;
717 }
718}
719
720static void
721gnc_item_edit_get_preferred_width (GtkWidget *widget,
722 gint *minimal_width,
723 gint *natural_width)
724{
725 gint x, y, w = 1, h;
726 gnc_item_edit_get_pixel_coords (GNC_ITEM_EDIT(widget), &x, &y, &w, &h);
727 *minimal_width = *natural_width = w - 1;
728}
729
730
731static void
732gnc_item_edit_get_preferred_height (GtkWidget *widget,
733 gint *minimal_width,
734 gint *natural_width)
735{
736 gint x, y, w, h = 1;
737 gnc_item_edit_get_pixel_coords (GNC_ITEM_EDIT(widget), &x, &y, &w, &h);
738 *minimal_width = *natural_width = h - 1;
739}
740
741/*
742 * GncItemEdit class initialization
743 */
744static void
745gnc_item_edit_class_init (GncItemEditClass *gnc_item_edit_class)
746{
747 GObjectClass *object_class;
748 GtkWidgetClass *widget_class;
749
750 gtk_widget_class_set_css_name (GTK_WIDGET_CLASS(gnc_item_edit_class), "gnc-id-cursor");
751
752 object_class = G_OBJECT_CLASS(gnc_item_edit_class);
753 widget_class = GTK_WIDGET_CLASS(gnc_item_edit_class);
754
755 object_class->get_property = gnc_item_edit_get_property;
756 object_class->set_property = gnc_item_edit_set_property;
757
758 g_object_class_install_property (object_class,
759 PROP_SHEET,
760 g_param_spec_object ("sheet",
761 "Sheet Value",
762 "Sheet Value",
763 GNUCASH_TYPE_SHEET,
764 G_PARAM_READWRITE));
765
766 /* GtkWidget method overrides */
767 widget_class->get_preferred_width = gnc_item_edit_get_preferred_width;
768 widget_class->get_preferred_height = gnc_item_edit_get_preferred_height;
769}
770
771gint
772gnc_item_edit_get_margin (GncItemEdit *item_edit, Sides side)
773{
774 switch (side)
775 {
776 case left:
777 return item_edit->margin.left;
778 case right:
779 return item_edit->margin.right;
780 case top:
781 return item_edit->margin.top;
782 case bottom:
783 return item_edit->margin.bottom;
784 case left_right:
785 return item_edit->margin.left + item_edit->margin.right;
786 case top_bottom:
787 return item_edit->margin.top + item_edit->margin.bottom;
788 default:
789 return 2;
790 }
791}
792
793gint
794gnc_item_edit_get_padding_border (GncItemEdit *item_edit, Sides side)
795{
796 switch (side)
797 {
798 case left:
799 return item_edit->padding.left + item_edit->border.left;
800 case right:
801 return item_edit->padding.right + item_edit->border.right;
802 case top:
803 return item_edit->padding.top + item_edit->border.top;
804 case bottom:
805 return item_edit->padding.bottom + item_edit->border.bottom;
806 case left_right:
807 return item_edit->padding.left + item_edit->border.left +
808 item_edit->padding.right + item_edit->border.right;
809 case top_bottom:
810 return item_edit->padding.top + item_edit->border.top +
811 item_edit->padding.bottom + item_edit->border.bottom;
812 default:
813 return 2;
814 }
815}
816
817gint
818gnc_item_edit_get_button_width (GncItemEdit *item_edit)
819{
820 if (item_edit)
821 {
822 if (gtk_widget_is_visible (GTK_WIDGET(item_edit->popup_toggle.tbutton)))
823 return item_edit->button_width;
824 else
825 {
826 GtkStyleContext *context = gtk_widget_get_style_context (
827 GTK_WIDGET(item_edit->popup_toggle.tbutton));
828 GtkBorder border;
829
830 gtk_style_context_get_border (context, GTK_STATE_FLAG_NORMAL, &border);
831 return MIN_BUTT_WIDTH + border.left + border.right;
832 }
833 }
834 return MIN_BUTT_WIDTH + 2; // add the default border
835}
836
837static gboolean
838button_press_cb (GtkWidget *widget, GdkEventButton *event, gpointer *pointer)
839{
840 GncItemEdit *item_edit = GNC_ITEM_EDIT(pointer);
841 GnucashSheet *sheet = item_edit->sheet;
842
843 /* Ignore double-clicks and triple-clicks */
844 if (event->button == 3 && event->type == GDK_BUTTON_PRESS)
845 {
846 if (!item_edit->show_popup)
847 {
848 // This is a right click event so over ride entry menu and
849 // display main register popup menu if no item_edit popup
850 // is showing.
851 g_signal_emit_by_name (sheet->reg, "show_popup_menu");
852 }
853 return TRUE;
854 }
855
856 if (!gtk_widget_has_focus (GTK_WIDGET(sheet)))
857 gtk_widget_grab_focus (GTK_WIDGET(sheet));
858
859 return FALSE;
860}
861
862GtkWidget *
863gnc_item_edit_new (GnucashSheet *sheet)
864{
865 GtkStyleContext *stylectxt;
866 GtkBorder padding;
867 GtkBorder margin;
868 GtkBorder border;
869 GncItemEdit *item_edit = g_object_new (GNC_TYPE_ITEM_EDIT,
870 "sheet", sheet,
871 "spacing", 0,
872 "homogeneous", FALSE,
873 NULL);
874 gtk_layout_put (GTK_LAYOUT(sheet), GTK_WIDGET(item_edit), 0, 0);
875
876 /* Create the text entry */
877 item_edit->editor = gtk_entry_new ();
878 sheet->entry = item_edit->editor;
879 gtk_entry_set_width_chars (GTK_ENTRY(item_edit->editor), 1);
880 gtk_box_pack_start (GTK_BOX(item_edit), item_edit->editor, TRUE, TRUE, 0);
881
882 // Get the CSS space settings for the entry
883 stylectxt = gtk_widget_get_style_context (GTK_WIDGET(item_edit->editor));
884 gtk_style_context_add_class (stylectxt, "gnc-class-register-foreground");
885 gtk_style_context_get_padding (stylectxt, GTK_STATE_FLAG_NORMAL, &padding);
886 gtk_style_context_get_margin (stylectxt, GTK_STATE_FLAG_NORMAL, &margin);
887 gtk_style_context_get_border (stylectxt, GTK_STATE_FLAG_NORMAL, &border);
888
889 item_edit->padding = padding;
890 item_edit->margin = margin;
891 item_edit->border = border;
892
893 // Make sure the Entry can not have focus and no frame
894 gtk_widget_set_can_focus (GTK_WIDGET(item_edit->editor), FALSE);
895 gtk_entry_set_has_frame (GTK_ENTRY(item_edit->editor), FALSE);
896
897 // Connect to the draw signal so we can draw a cursor
898 g_signal_connect_after (item_edit->editor, "draw",
899 G_CALLBACK(draw_text_cursor_cb), item_edit);
900
901 g_signal_connect (item_edit->editor, "preedit-changed",
902 G_CALLBACK(preedit_changed_cb), item_edit);
903
904 // Fill in the background so the underlying sheet cell can not be seen
905 g_signal_connect (item_edit, "draw",
906 G_CALLBACK(draw_background_cb), item_edit);
907
908 // This call back intercepts the mouse button event so the main
909 // register popup menu can be displayed instead of the entry one.
910 g_signal_connect (item_edit->editor, "button-press-event",
911 G_CALLBACK(button_press_cb), item_edit);
912
913 /* Create the popup button
914 It will only be displayed when the cell being edited provides
915 a popup item (like a calendar or account list) */
916 item_edit->popup_toggle.tbutton = gnc_item_edit_tb_new (sheet);
917 gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON(item_edit->popup_toggle.tbutton), FALSE);
918
919 /* Wrap the popup button in an event box to give it its own gdkwindow.
920 * Without one the button would disappear behind the grid object. */
921 item_edit->popup_toggle.ebox = gtk_event_box_new ();
922 g_object_ref (item_edit->popup_toggle.ebox);
923 gtk_container_add (GTK_CONTAINER(item_edit->popup_toggle.ebox),
924 item_edit->popup_toggle.tbutton);
925
926 // This call back intercepts the right mouse button event to stop the
927 // gnucash_sheet_button_press_event from running.
928 g_signal_connect (item_edit->popup_toggle.ebox, "button-press-event",
929 G_CALLBACK(tb_button_press_cb), NULL);
930
931 gtk_box_pack_start (GTK_BOX(item_edit), item_edit->popup_toggle.ebox, FALSE, FALSE, 0);
932 gtk_widget_show_all (GTK_WIDGET(item_edit));
933 g_signal_connect (G_OBJECT(item_edit), "destroy",
934 G_CALLBACK(gnc_item_edit_destroying), NULL);
935 return GTK_WIDGET(item_edit);
936}
937
938static void
939gnc_item_edit_destroying (GtkWidget *item_edit, gpointer data)
940{
941 if (GNC_ITEM_EDIT(item_edit)->popup_height_signal_id > 0)
942 g_signal_handler_disconnect (GNC_ITEM_EDIT(item_edit)->popup_item,
943 GNC_ITEM_EDIT(item_edit)->popup_height_signal_id);
944
945 while (g_idle_remove_by_data ((gpointer)item_edit))
946 continue;
947}
948
949static void
950check_popup_height_is_true (GtkWidget *widget,
951 GdkRectangle *allocation,
952 gpointer user_data)
953{
954 GncItemEdit *item_edit = GNC_ITEM_EDIT(user_data);
955
956 // the popup returned height value on first pop sometimes does not reflect the true height
957 // but the minimum height so just to be sure check this value against the allocated one.
958 if (allocation->height != item_edit->popup_returned_height)
959 {
960 item_edit->popup_allocation_height = allocation->height;
961 gtk_container_remove (GTK_CONTAINER(item_edit->sheet), item_edit->popup_item);
962
963 g_idle_add_full (G_PRIORITY_HIGH_IDLE,
964 (GSourceFunc)gnc_item_edit_update, item_edit, NULL);
965 }
966}
967
968void
969gnc_item_edit_show_popup (GncItemEdit *item_edit)
970{
971 GtkToggleButton *toggle;
972 GtkAdjustment *vadj, *hadj;
973 GtkAllocation alloc;
974 GnucashSheet *sheet;
975 gint x = 0, y = 0, w = 0, h = 0;
976 gint y_offset, x_offset;
977 gint popup_x, popup_y;
978 gint popup_w = -1, popup_h = -1;
979 gint popup_max_width, popup_max_height;
980 gint view_height;
981 gint down_height, up_height;
982 gint sheet_width;
983
984 g_return_if_fail (item_edit != NULL);
985 g_return_if_fail (GNC_IS_ITEM_EDIT(item_edit));
986
987 if (!item_edit->is_popup)
988 return;
989
990 sheet = item_edit->sheet;
991
992 sheet_width = sheet->width;
993
994 gtk_widget_get_allocation (GTK_WIDGET(sheet), &alloc);
995 view_height = alloc.height;
996
997 vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE(sheet));
998 hadj = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE(sheet));
999
1000 y_offset = gtk_adjustment_get_value (vadj);
1001 x_offset = gtk_adjustment_get_value (hadj);
1002 gnc_item_edit_get_pixel_coords (item_edit, &x, &y, &w, &h);
1003
1004 popup_x = x;
1005
1006 up_height = y - y_offset;
1007 down_height = view_height - (up_height + h);
1008
1009 popup_max_height = MAX(up_height, down_height);
1010 popup_max_width = sheet_width - popup_x + x_offset; // always pops to the right
1011
1012 if (item_edit->popup_get_height)
1013 popup_h = item_edit->popup_get_height
1014 (item_edit->popup_item, popup_max_height, h,
1015 item_edit->popup_user_data);
1016
1017 if (item_edit->popup_autosize)
1018 popup_w =
1019 item_edit->popup_autosize (item_edit->popup_item,
1020 popup_max_width,
1021 item_edit->popup_user_data);
1022 else
1023 popup_w = 0;
1024
1025 // Adjust the popup_y point based on popping above or below
1026 if (up_height > down_height)
1027 popup_y = y - popup_h - 1;
1028 else
1029 popup_y = y + h;
1030
1031 if (!gtk_widget_get_parent (item_edit->popup_item))
1032 gtk_layout_put (GTK_LAYOUT(sheet), item_edit->popup_item, popup_x, popup_y);
1033
1034 // Lets check popup height is the true height
1035 item_edit->popup_returned_height = popup_h;
1036
1037 gtk_widget_get_allocation (GTK_WIDGET(item_edit), &alloc);
1038
1039 // the calendar will be 0
1040 if ((popup_w != 0) && (popup_w < alloc.width))
1041 popup_w = alloc.width;
1042
1043 if (popup_h == popup_max_height)
1044 gtk_widget_set_size_request (item_edit->popup_item, popup_w - 1, popup_h);
1045 else
1046 gtk_widget_set_size_request (item_edit->popup_item, popup_w - 1, -1);
1047
1048 toggle = GTK_TOGGLE_BUTTON(item_edit->popup_toggle.tbutton);
1049
1050 if (!gtk_toggle_button_get_active (toggle))
1051 {
1052 block_toggle_signals (item_edit);
1053 gtk_toggle_button_set_active (toggle, TRUE);
1054 unblock_toggle_signals (item_edit);
1055 }
1056
1057 // set the popup arrow direction up
1058 item_edit->popup_toggle.arrow_down = FALSE;
1059 item_edit->show_popup = TRUE;
1060
1061 if (item_edit->popup_set_focus)
1062 item_edit->popup_set_focus (item_edit->popup_item,
1063 item_edit->popup_user_data);
1064
1065 if (item_edit->popup_post_show)
1066 item_edit->popup_post_show (item_edit->popup_item,
1067 item_edit->popup_user_data);
1068
1069 if (item_edit->popup_get_width)
1070 {
1071 int popup_width;
1072
1073 popup_width = item_edit->popup_get_width
1074 (item_edit->popup_item,
1075 item_edit->popup_user_data);
1076
1077 if (popup_width > popup_w)
1078 popup_width = popup_w;
1079
1080 if (popup_width > popup_max_width)
1081 {
1082 popup_x -= popup_width - popup_max_width;
1083 popup_x = MAX(0, popup_x);
1084 }
1085 else
1086 popup_x = x;
1087
1088 gtk_layout_move (GTK_LAYOUT(sheet), item_edit->popup_item, popup_x, popup_y);
1089 }
1090}
1091
1092
1093void
1094gnc_item_edit_hide_popup (GncItemEdit *item_edit)
1095{
1096 g_return_if_fail (item_edit != NULL);
1097 g_return_if_fail (GNC_IS_ITEM_EDIT(item_edit));
1098
1099 if (!item_edit->is_popup)
1100 return;
1101
1102 if (gtk_widget_get_parent (GTK_WIDGET(item_edit->popup_item)) != GTK_WIDGET(item_edit->sheet))
1103 return;
1104
1105 gtk_container_remove (GTK_CONTAINER(item_edit->sheet), item_edit->popup_item);
1106
1107 // set the popup arrow direction down
1108 item_edit->popup_toggle.arrow_down = TRUE;
1109
1110 gtk_toggle_button_set_active
1111 (GTK_TOGGLE_BUTTON(item_edit->popup_toggle.tbutton), FALSE);
1112
1113 item_edit->popup_allocation_height = -1;
1114
1115 gtk_widget_grab_focus (GTK_WIDGET(item_edit->sheet));
1116}
1117
1118
1119void
1120gnc_item_edit_set_popup (GncItemEdit *item_edit,
1121 GtkWidget *popup_item,
1122 PopupGetHeight popup_get_height,
1123 PopupAutosize popup_autosize,
1124 PopupSetFocus popup_set_focus,
1125 PopupPostShow popup_post_show,
1126 PopupGetWidth popup_get_width,
1127 gpointer popup_user_data)
1128{
1129 g_return_if_fail (GNC_IS_ITEM_EDIT(item_edit));
1130
1131 if (item_edit->is_popup)
1132 gnc_item_edit_hide_popup (item_edit);
1133
1134 /* setup size-allocate callback for popup_item height, done here as
1135 item_edit is constant and popup_item changes per cell */
1136 if (popup_item)
1137 {
1138 item_edit->popup_height_signal_id = g_signal_connect_after (
1139 popup_item, "size-allocate",
1140 G_CALLBACK(check_popup_height_is_true),
1141 item_edit);
1142 }
1143 else
1144 {
1145 if (GNC_ITEM_EDIT(item_edit)->popup_height_signal_id > 0)
1146 {
1147 g_signal_handler_disconnect (item_edit->popup_item, item_edit->popup_height_signal_id);
1148 item_edit->popup_height_signal_id = 0;
1149 }
1150 }
1151
1152 item_edit->is_popup = popup_item != NULL;
1153
1154 item_edit->popup_item = popup_item;
1155 item_edit->popup_get_height = popup_get_height;
1156 item_edit->popup_autosize = popup_autosize;
1157 item_edit->popup_set_focus = popup_set_focus;
1158 item_edit->popup_post_show = popup_post_show;
1159 item_edit->popup_get_width = popup_get_width;
1160 item_edit->popup_user_data = popup_user_data;
1161
1162 if (item_edit->is_popup)
1163 connect_popup_toggle_signals (item_edit);
1164 else
1165 {
1166 disconnect_popup_toggle_signals (item_edit);
1167
1168 gnc_item_edit_hide_popup (item_edit);
1169 gtk_widget_hide (item_edit->popup_toggle.ebox);
1170 }
1171}
1172
1173gboolean
1174gnc_item_edit_get_has_selection (GncItemEdit *item_edit)
1175{
1176 GtkEditable *editable;
1177
1178 g_return_val_if_fail ((item_edit != NULL), FALSE);
1179 g_return_val_if_fail (GNC_IS_ITEM_EDIT(item_edit), FALSE);
1180
1181 editable = GTK_EDITABLE(item_edit->editor);
1182 return gtk_editable_get_selection_bounds (editable, NULL, NULL);
1183}
1184
utility functions for the GnuCash UI
Convenience wrapper around GdkRGBA for use in Register Gnome classes.
Public declarations for GnucashCursor class.
Public declarations for GncItemEdit class.
Private declarations for GnucashSheet class.
Public declarations of GnucashRegister class.
Styling functions for RegisterGnome.
void gnucash_get_style_classes(GnucashSheet *sheet, GtkStyleContext *stylectxt, RegisterColor field_type, gboolean use_neg_class)
Map a cell color type to a css style class.
char * gnc_filter_text_for_control_chars(const char *text)
Returns the incoming text removed of control characters.
#define DEBUG(format, args...)
Print a debugging message.
Definition qoflog.h:264
gint origin_y
x origin of block