GnuCash  5.6-150-g038405b370+
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 */
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 
65 static void gnc_item_edit_destroying (GtkWidget *this, gpointer data);
66 
67 G_DEFINE_TYPE (GncItemEdit, gnc_item_edit, GTK_TYPE_BOX)
68 
69 G_DEFINE_TYPE (GncItemEditTb, gnc_item_edit_tb, GTK_TYPE_TOGGLE_BUTTON)
70 
71 static void
72 gnc_item_edit_tb_init (GncItemEditTb *item_edit_tb)
73 {
74  item_edit_tb->sheet = NULL;
75 }
76 
77 static void
78 gnc_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 
96 static void
97 gnc_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 
115 static void
116 gnc_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 
137 static void
138 gnc_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 
149 static void
150 gnc_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 
176 GtkWidget *
177 gnc_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 
190 static gboolean
191 tb_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  */
206 void
207 gnc_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 
240 static gboolean
241 gnc_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 
260 void
261 gnc_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 
274 void
275 gnc_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  */
294 static void
295 gnc_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 
330 void
331 gnc_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 
374 void
375 gnc_item_edit_cut_clipboard (GncItemEdit *item_edit)
376 {
377  gtk_editable_cut_clipboard (GTK_EDITABLE(item_edit->editor));
378 }
379 
380 void
381 gnc_item_edit_copy_clipboard (GncItemEdit *item_edit)
382 {
383  gtk_editable_copy_clipboard (GTK_EDITABLE(item_edit->editor));
384 }
385 
386 void
387 gnc_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 
429 static gboolean
430 key_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 
442 static void
443 gnc_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 
482 static void
483 block_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 
497 static void
498 unblock_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 
512 static gboolean
513 draw_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  */
538 static void
539 preedit_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 
546 static gboolean
547 draw_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 
608 static gboolean
609 draw_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 
635 static void
636 connect_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 
663 static void
664 disconnect_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  */
683 static void
684 gnc_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 
702 static void
703 gnc_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 
720 static void
721 gnc_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 
731 static void
732 gnc_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  */
744 static void
745 gnc_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 
771 gint
772 gnc_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 
793 gint
794 gnc_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 
817 gint
818 gnc_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 
837 static gboolean
838 button_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  return FALSE;
856 }
857 
858 GtkWidget *
859 gnc_item_edit_new (GnucashSheet *sheet)
860 {
861  GtkStyleContext *stylectxt;
862  GtkBorder padding;
863  GtkBorder margin;
864  GtkBorder border;
865  GncItemEdit *item_edit = g_object_new (GNC_TYPE_ITEM_EDIT,
866  "sheet", sheet,
867  "spacing", 0,
868  "homogeneous", FALSE,
869  NULL);
870  gtk_layout_put (GTK_LAYOUT(sheet), GTK_WIDGET(item_edit), 0, 0);
871 
872  /* Create the text entry */
873  item_edit->editor = gtk_entry_new ();
874  sheet->entry = item_edit->editor;
875  gtk_entry_set_width_chars (GTK_ENTRY(item_edit->editor), 1);
876  gtk_box_pack_start (GTK_BOX(item_edit), item_edit->editor, TRUE, TRUE, 0);
877 
878  // Get the CSS space settings for the entry
879  stylectxt = gtk_widget_get_style_context (GTK_WIDGET(item_edit->editor));
880  gtk_style_context_add_class (stylectxt, "gnc-class-register-foreground");
881  gtk_style_context_get_padding (stylectxt, GTK_STATE_FLAG_NORMAL, &padding);
882  gtk_style_context_get_margin (stylectxt, GTK_STATE_FLAG_NORMAL, &margin);
883  gtk_style_context_get_border (stylectxt, GTK_STATE_FLAG_NORMAL, &border);
884 
885  item_edit->padding = padding;
886  item_edit->margin = margin;
887  item_edit->border = border;
888 
889  // Make sure the Entry can not have focus and no frame
890  gtk_widget_set_can_focus (GTK_WIDGET(item_edit->editor), FALSE);
891  gtk_entry_set_has_frame (GTK_ENTRY(item_edit->editor), FALSE);
892 
893  // Connect to the draw signal so we can draw a cursor
894  g_signal_connect_after (item_edit->editor, "draw",
895  G_CALLBACK(draw_text_cursor_cb), item_edit);
896 
897  g_signal_connect (item_edit->editor, "preedit-changed",
898  G_CALLBACK(preedit_changed_cb), item_edit);
899 
900  // Fill in the background so the underlying sheet cell can not be seen
901  g_signal_connect (item_edit, "draw",
902  G_CALLBACK(draw_background_cb), item_edit);
903 
904  // This call back intercepts the mouse button event so the main
905  // register popup menu can be displayed instead of the entry one.
906  g_signal_connect (item_edit->editor, "button-press-event",
907  G_CALLBACK(button_press_cb), item_edit);
908 
909  /* Create the popup button
910  It will only be displayed when the cell being edited provides
911  a popup item (like a calendar or account list) */
912  item_edit->popup_toggle.tbutton = gnc_item_edit_tb_new (sheet);
913  gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON(item_edit->popup_toggle.tbutton), FALSE);
914 
915  /* Wrap the popup button in an event box to give it its own gdkwindow.
916  * Without one the button would disappear behind the grid object. */
917  item_edit->popup_toggle.ebox = gtk_event_box_new ();
918  g_object_ref (item_edit->popup_toggle.ebox);
919  gtk_container_add (GTK_CONTAINER(item_edit->popup_toggle.ebox),
920  item_edit->popup_toggle.tbutton);
921 
922  // This call back intercepts the right mouse button event to stop the
923  // gnucash_sheet_button_press_event from running.
924  g_signal_connect (item_edit->popup_toggle.ebox, "button-press-event",
925  G_CALLBACK(tb_button_press_cb), NULL);
926 
927  gtk_box_pack_start (GTK_BOX(item_edit), item_edit->popup_toggle.ebox, FALSE, FALSE, 0);
928  gtk_widget_show_all (GTK_WIDGET(item_edit));
929  g_signal_connect (G_OBJECT(item_edit), "destroy",
930  G_CALLBACK(gnc_item_edit_destroying), NULL);
931  return GTK_WIDGET(item_edit);
932 }
933 
934 static void
935 gnc_item_edit_destroying (GtkWidget *item_edit, gpointer data)
936 {
937  if (GNC_ITEM_EDIT(item_edit)->popup_height_signal_id > 0)
938  g_signal_handler_disconnect (GNC_ITEM_EDIT(item_edit)->popup_item,
939  GNC_ITEM_EDIT(item_edit)->popup_height_signal_id);
940 
941  while (g_idle_remove_by_data ((gpointer)item_edit))
942  continue;
943 }
944 
945 static void
946 check_popup_height_is_true (GtkWidget *widget,
947  GdkRectangle *allocation,
948  gpointer user_data)
949 {
950  GncItemEdit *item_edit = GNC_ITEM_EDIT(user_data);
951 
952  // the popup returned height value on first pop sometimes does not reflect the true height
953  // but the minimum height so just to be sure check this value against the allocated one.
954  if (allocation->height != item_edit->popup_returned_height)
955  {
956  item_edit->popup_allocation_height = allocation->height;
957  gtk_container_remove (GTK_CONTAINER(item_edit->sheet), item_edit->popup_item);
958 
959  g_idle_add_full (G_PRIORITY_HIGH_IDLE,
960  (GSourceFunc)gnc_item_edit_update, item_edit, NULL);
961  }
962 }
963 
964 void
965 gnc_item_edit_show_popup (GncItemEdit *item_edit)
966 {
967  GtkToggleButton *toggle;
968  GtkAdjustment *vadj, *hadj;
969  GtkAllocation alloc;
970  GnucashSheet *sheet;
971  gint x = 0, y = 0, w = 0, h = 0;
972  gint y_offset, x_offset;
973  gint popup_x, popup_y;
974  gint popup_w = -1, popup_h = -1;
975  gint popup_max_width, popup_max_height;
976  gint view_height;
977  gint down_height, up_height;
978  gint sheet_width;
979 
980  g_return_if_fail (item_edit != NULL);
981  g_return_if_fail (GNC_IS_ITEM_EDIT(item_edit));
982 
983  if (!item_edit->is_popup)
984  return;
985 
986  sheet = item_edit->sheet;
987 
988  sheet_width = sheet->width;
989 
990  gtk_widget_get_allocation (GTK_WIDGET(sheet), &alloc);
991  view_height = alloc.height;
992 
993  vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE(sheet));
994  hadj = gtk_scrollable_get_hadjustment (GTK_SCROLLABLE(sheet));
995 
996  y_offset = gtk_adjustment_get_value (vadj);
997  x_offset = gtk_adjustment_get_value (hadj);
998  gnc_item_edit_get_pixel_coords (item_edit, &x, &y, &w, &h);
999 
1000  popup_x = x;
1001 
1002  up_height = y - y_offset;
1003  down_height = view_height - (up_height + h);
1004 
1005  popup_max_height = MAX(up_height, down_height);
1006  popup_max_width = sheet_width - popup_x + x_offset; // always pops to the right
1007 
1008  if (item_edit->popup_get_height)
1009  popup_h = item_edit->popup_get_height
1010  (item_edit->popup_item, popup_max_height, h,
1011  item_edit->popup_user_data);
1012 
1013  if (item_edit->popup_autosize)
1014  popup_w =
1015  item_edit->popup_autosize (item_edit->popup_item,
1016  popup_max_width,
1017  item_edit->popup_user_data);
1018  else
1019  popup_w = 0;
1020 
1021  // Adjust the popup_y point based on popping above or below
1022  if (up_height > down_height)
1023  popup_y = y - popup_h - 1;
1024  else
1025  popup_y = y + h;
1026 
1027  if (!gtk_widget_get_parent (item_edit->popup_item))
1028  gtk_layout_put (GTK_LAYOUT(sheet), item_edit->popup_item, popup_x, popup_y);
1029 
1030  // Lets check popup height is the true height
1031  item_edit->popup_returned_height = popup_h;
1032 
1033  gtk_widget_get_allocation (GTK_WIDGET(item_edit), &alloc);
1034 
1035  // the calendar will be 0
1036  if ((popup_w != 0) && (popup_w < alloc.width))
1037  popup_w = alloc.width;
1038 
1039  if (popup_h == popup_max_height)
1040  gtk_widget_set_size_request (item_edit->popup_item, popup_w - 1, popup_h);
1041  else
1042  gtk_widget_set_size_request (item_edit->popup_item, popup_w - 1, -1);
1043 
1044  toggle = GTK_TOGGLE_BUTTON(item_edit->popup_toggle.tbutton);
1045 
1046  if (!gtk_toggle_button_get_active (toggle))
1047  {
1048  block_toggle_signals (item_edit);
1049  gtk_toggle_button_set_active (toggle, TRUE);
1050  unblock_toggle_signals (item_edit);
1051  }
1052 
1053  // set the popup arrow direction up
1054  item_edit->popup_toggle.arrow_down = FALSE;
1055  item_edit->show_popup = TRUE;
1056 
1057  if (item_edit->popup_set_focus)
1058  item_edit->popup_set_focus (item_edit->popup_item,
1059  item_edit->popup_user_data);
1060 
1061  if (item_edit->popup_post_show)
1062  item_edit->popup_post_show (item_edit->popup_item,
1063  item_edit->popup_user_data);
1064 
1065  if (item_edit->popup_get_width)
1066  {
1067  int popup_width;
1068 
1069  popup_width = item_edit->popup_get_width
1070  (item_edit->popup_item,
1071  item_edit->popup_user_data);
1072 
1073  if (popup_width > popup_w)
1074  popup_width = popup_w;
1075 
1076  if (popup_width > popup_max_width)
1077  {
1078  popup_x -= popup_width - popup_max_width;
1079  popup_x = MAX(0, popup_x);
1080  }
1081  else
1082  popup_x = x;
1083 
1084  gtk_layout_move (GTK_LAYOUT(sheet), item_edit->popup_item, popup_x, popup_y);
1085  }
1086 }
1087 
1088 
1089 void
1090 gnc_item_edit_hide_popup (GncItemEdit *item_edit)
1091 {
1092  g_return_if_fail (item_edit != NULL);
1093  g_return_if_fail (GNC_IS_ITEM_EDIT(item_edit));
1094 
1095  if (!item_edit->is_popup)
1096  return;
1097 
1098  if (gtk_widget_get_parent (GTK_WIDGET(item_edit->popup_item)) != GTK_WIDGET(item_edit->sheet))
1099  return;
1100 
1101  gtk_container_remove (GTK_CONTAINER(item_edit->sheet), item_edit->popup_item);
1102 
1103  // set the popup arrow direction down
1104  item_edit->popup_toggle.arrow_down = TRUE;
1105 
1106  gtk_toggle_button_set_active
1107  (GTK_TOGGLE_BUTTON(item_edit->popup_toggle.tbutton), FALSE);
1108 
1109  item_edit->popup_allocation_height = -1;
1110 
1111  gtk_widget_grab_focus (GTK_WIDGET(item_edit->sheet));
1112 }
1113 
1114 
1115 void
1116 gnc_item_edit_set_popup (GncItemEdit *item_edit,
1117  GtkWidget *popup_item,
1118  PopupGetHeight popup_get_height,
1119  PopupAutosize popup_autosize,
1120  PopupSetFocus popup_set_focus,
1121  PopupPostShow popup_post_show,
1122  PopupGetWidth popup_get_width,
1123  gpointer popup_user_data)
1124 {
1125  g_return_if_fail (GNC_IS_ITEM_EDIT(item_edit));
1126 
1127  if (item_edit->is_popup)
1128  gnc_item_edit_hide_popup (item_edit);
1129 
1130  /* setup size-allocate callback for popup_item height, done here as
1131  item_edit is constant and popup_item changes per cell */
1132  if (popup_item)
1133  {
1134  item_edit->popup_height_signal_id = g_signal_connect_after (
1135  popup_item, "size-allocate",
1136  G_CALLBACK(check_popup_height_is_true),
1137  item_edit);
1138  }
1139  else
1140  {
1141  if (GNC_ITEM_EDIT(item_edit)->popup_height_signal_id > 0)
1142  {
1143  g_signal_handler_disconnect (item_edit->popup_item, item_edit->popup_height_signal_id);
1144  item_edit->popup_height_signal_id = 0;
1145  }
1146  }
1147 
1148  item_edit->is_popup = popup_item != NULL;
1149 
1150  item_edit->popup_item = popup_item;
1151  item_edit->popup_get_height = popup_get_height;
1152  item_edit->popup_autosize = popup_autosize;
1153  item_edit->popup_set_focus = popup_set_focus;
1154  item_edit->popup_post_show = popup_post_show;
1155  item_edit->popup_get_width = popup_get_width;
1156  item_edit->popup_user_data = popup_user_data;
1157 
1158  if (item_edit->is_popup)
1159  connect_popup_toggle_signals (item_edit);
1160  else
1161  {
1162  disconnect_popup_toggle_signals (item_edit);
1163 
1164  gnc_item_edit_hide_popup (item_edit);
1165  gtk_widget_hide (item_edit->popup_toggle.ebox);
1166  }
1167 }
1168 
1169 gboolean
1170 gnc_item_edit_get_has_selection (GncItemEdit *item_edit)
1171 {
1172  GtkEditable *editable;
1173 
1174  g_return_val_if_fail ((item_edit != NULL), FALSE);
1175  g_return_val_if_fail (GNC_IS_ITEM_EDIT(item_edit), FALSE);
1176 
1177  editable = GTK_EDITABLE(item_edit->editor);
1178  return gtk_editable_get_selection_bounds (editable, NULL, NULL);
1179 }
1180 
utility functions for the GnuCash UI
#define G_LOG_DOMAIN
Functions providing the SX List as a plugin page.
#define DEBUG(format, args...)
Print a debugging message.
Definition: qoflog.h:264
Convenience wrapper around GdkRGBA for use in Register Gnome classes.
Public declarations for GnucashCursor class.
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.
Public declarations of GnucashRegister class.
Private declarations for GnucashSheet class.
char * gnc_filter_text_for_control_chars(const char *text)
Returns the incoming text removed of control characters.
Public declarations for GncItemEdit class.
Styling functions for RegisterGnome.
gint origin_y
x origin of block
Definition: gnucash-sheet.h:57