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