1 | /* GtkCellRendererCombo |
2 | * Copyright (C) 2004 Lorenzo Gil Sanchez |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2 of the License, or (at your option) any later version. |
8 | * |
9 | * This library 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 GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | */ |
17 | |
18 | #include "config.h" |
19 | #include <string.h> |
20 | |
21 | #include "gtkintl.h" |
22 | #include "gtkentry.h" |
23 | #include "gtkcelllayout.h" |
24 | #include "gtkcellrenderercombo.h" |
25 | #include "gtkcellrenderertext.h" |
26 | #include "gtkcombobox.h" |
27 | #include "gtkmarshalers.h" |
28 | #include "gtkprivate.h" |
29 | |
30 | |
31 | /** |
32 | * GtkCellRendererCombo: |
33 | * |
34 | * Renders a combobox in a cell |
35 | * |
36 | * `GtkCellRendererCombo` renders text in a cell like `GtkCellRendererText` from |
37 | * which it is derived. But while `GtkCellRendererText` offers a simple entry to |
38 | * edit the text, `GtkCellRendererCombo` offers a `GtkComboBox` |
39 | * widget to edit the text. The values to display in the combo box are taken from |
40 | * the tree model specified in the `GtkCellRendererCombo`:model property. |
41 | * |
42 | * The combo cell renderer takes care of adding a text cell renderer to the combo |
43 | * box and sets it to display the column specified by its |
44 | * `GtkCellRendererCombo`:text-column property. Further properties of the combo box |
45 | * can be set in a handler for the `GtkCellRenderer::editing-started` signal. |
46 | */ |
47 | |
48 | typedef struct _GtkCellRendererComboPrivate GtkCellRendererComboPrivate; |
49 | typedef struct _GtkCellRendererComboClass GtkCellRendererComboClass; |
50 | |
51 | struct _GtkCellRendererCombo |
52 | { |
53 | GtkCellRendererText parent; |
54 | }; |
55 | |
56 | struct _GtkCellRendererComboClass |
57 | { |
58 | GtkCellRendererTextClass parent; |
59 | }; |
60 | |
61 | |
62 | struct _GtkCellRendererComboPrivate |
63 | { |
64 | GtkTreeModel *model; |
65 | |
66 | GtkWidget *combo; |
67 | |
68 | gboolean has_entry; |
69 | |
70 | int text_column; |
71 | |
72 | gulong focus_out_id; |
73 | }; |
74 | |
75 | |
76 | static void gtk_cell_renderer_combo_finalize (GObject *object); |
77 | static void gtk_cell_renderer_combo_get_property (GObject *object, |
78 | guint prop_id, |
79 | GValue *value, |
80 | GParamSpec *pspec); |
81 | |
82 | static void gtk_cell_renderer_combo_set_property (GObject *object, |
83 | guint prop_id, |
84 | const GValue *value, |
85 | GParamSpec *pspec); |
86 | |
87 | static GtkCellEditable *gtk_cell_renderer_combo_start_editing (GtkCellRenderer *cell, |
88 | GdkEvent *event, |
89 | GtkWidget *widget, |
90 | const char *path, |
91 | const GdkRectangle *background_area, |
92 | const GdkRectangle *cell_area, |
93 | GtkCellRendererState flags); |
94 | |
95 | enum { |
96 | PROP_0, |
97 | PROP_MODEL, |
98 | PROP_TEXT_COLUMN, |
99 | PROP_HAS_ENTRY |
100 | }; |
101 | |
102 | enum { |
103 | CHANGED, |
104 | LAST_SIGNAL |
105 | }; |
106 | |
107 | static guint cell_renderer_combo_signals[LAST_SIGNAL] = { 0, }; |
108 | |
109 | #define GTK_CELL_RENDERER_COMBO_PATH "gtk-cell-renderer-combo-path" |
110 | |
111 | G_DEFINE_TYPE_WITH_PRIVATE (GtkCellRendererCombo, gtk_cell_renderer_combo, GTK_TYPE_CELL_RENDERER_TEXT) |
112 | |
113 | static void |
114 | gtk_cell_renderer_combo_class_init (GtkCellRendererComboClass *klass) |
115 | { |
116 | GObjectClass *object_class = G_OBJECT_CLASS (klass); |
117 | GtkCellRendererClass *cell_class = GTK_CELL_RENDERER_CLASS (klass); |
118 | |
119 | object_class->finalize = gtk_cell_renderer_combo_finalize; |
120 | object_class->get_property = gtk_cell_renderer_combo_get_property; |
121 | object_class->set_property = gtk_cell_renderer_combo_set_property; |
122 | |
123 | cell_class->start_editing = gtk_cell_renderer_combo_start_editing; |
124 | |
125 | /** |
126 | * GtkCellRendererCombo:model: |
127 | * |
128 | * Holds a tree model containing the possible values for the combo box. |
129 | * Use the text_column property to specify the column holding the values. |
130 | */ |
131 | g_object_class_install_property (oclass: object_class, |
132 | property_id: PROP_MODEL, |
133 | pspec: g_param_spec_object (name: "model" , |
134 | P_("Model" ), |
135 | P_("The model containing the possible values for the combo box" ), |
136 | GTK_TYPE_TREE_MODEL, |
137 | GTK_PARAM_READWRITE)); |
138 | |
139 | /** |
140 | * GtkCellRendererCombo:text-column: |
141 | * |
142 | * Specifies the model column which holds the possible values for the |
143 | * combo box. |
144 | * |
145 | * Note that this refers to the model specified in the model property, |
146 | * not the model backing the tree view to which |
147 | * this cell renderer is attached. |
148 | * |
149 | * `GtkCellRendererCombo` automatically adds a text cell renderer for |
150 | * this column to its combo box. |
151 | */ |
152 | g_object_class_install_property (oclass: object_class, |
153 | property_id: PROP_TEXT_COLUMN, |
154 | pspec: g_param_spec_int (name: "text-column" , |
155 | P_("Text Column" ), |
156 | P_("A column in the data source model to get the strings from" ), |
157 | minimum: -1, |
158 | G_MAXINT, |
159 | default_value: -1, |
160 | GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY)); |
161 | |
162 | /** |
163 | * GtkCellRendererCombo:has-entry: |
164 | * |
165 | * If %TRUE, the cell renderer will include an entry and allow to enter |
166 | * values other than the ones in the popup list. |
167 | */ |
168 | g_object_class_install_property (oclass: object_class, |
169 | property_id: PROP_HAS_ENTRY, |
170 | pspec: g_param_spec_boolean (name: "has-entry" , |
171 | P_("Has Entry" ), |
172 | P_("If FALSE, don’t allow to enter strings other than the chosen ones" ), |
173 | TRUE, |
174 | GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY)); |
175 | |
176 | |
177 | /** |
178 | * GtkCellRendererCombo::changed: |
179 | * @combo: the object on which the signal is emitted |
180 | * @path_string: a string of the path identifying the edited cell |
181 | * (relative to the tree view model) |
182 | * @new_iter: the new iter selected in the combo box |
183 | * (relative to the combo box model) |
184 | * |
185 | * This signal is emitted each time after the user selected an item in |
186 | * the combo box, either by using the mouse or the arrow keys. Contrary |
187 | * to GtkComboBox, GtkCellRendererCombo::changed is not emitted for |
188 | * changes made to a selected item in the entry. The argument @new_iter |
189 | * corresponds to the newly selected item in the combo box and it is relative |
190 | * to the GtkTreeModel set via the model property on GtkCellRendererCombo. |
191 | * |
192 | * Note that as soon as you change the model displayed in the tree view, |
193 | * the tree view will immediately cease the editing operating. This |
194 | * means that you most probably want to refrain from changing the model |
195 | * until the combo cell renderer emits the edited or editing_canceled signal. |
196 | */ |
197 | cell_renderer_combo_signals[CHANGED] = |
198 | g_signal_new (I_("changed" ), |
199 | G_TYPE_FROM_CLASS (object_class), |
200 | signal_flags: G_SIGNAL_RUN_LAST, |
201 | class_offset: 0, |
202 | NULL, NULL, |
203 | c_marshaller: _gtk_marshal_VOID__STRING_BOXED, |
204 | G_TYPE_NONE, n_params: 2, |
205 | G_TYPE_STRING, |
206 | GTK_TYPE_TREE_ITER); |
207 | g_signal_set_va_marshaller (signal_id: cell_renderer_combo_signals[CHANGED], |
208 | G_TYPE_FROM_CLASS (object_class), |
209 | va_marshaller: _gtk_marshal_VOID__STRING_BOXEDv); |
210 | } |
211 | |
212 | static void |
213 | gtk_cell_renderer_combo_init (GtkCellRendererCombo *self) |
214 | { |
215 | GtkCellRendererComboPrivate *priv = gtk_cell_renderer_combo_get_instance_private (self); |
216 | |
217 | priv->model = NULL; |
218 | priv->text_column = -1; |
219 | priv->has_entry = TRUE; |
220 | priv->focus_out_id = 0; |
221 | } |
222 | |
223 | /** |
224 | * gtk_cell_renderer_combo_new: |
225 | * |
226 | * Creates a new `GtkCellRendererCombo`. |
227 | * Adjust how text is drawn using object properties. |
228 | * Object properties can be set globally (with g_object_set()). |
229 | * Also, with `GtkTreeViewColumn`, you can bind a property to a value |
230 | * in a `GtkTreeModel`. For example, you can bind the “text” property |
231 | * on the cell renderer to a string value in the model, thus rendering |
232 | * a different string in each row of the `GtkTreeView`. |
233 | * |
234 | * Returns: the new cell renderer |
235 | */ |
236 | GtkCellRenderer * |
237 | gtk_cell_renderer_combo_new (void) |
238 | { |
239 | return g_object_new (GTK_TYPE_CELL_RENDERER_COMBO, NULL); |
240 | } |
241 | |
242 | static void |
243 | gtk_cell_renderer_combo_finalize (GObject *object) |
244 | { |
245 | GtkCellRendererCombo *cell = GTK_CELL_RENDERER_COMBO (object); |
246 | GtkCellRendererComboPrivate *priv = gtk_cell_renderer_combo_get_instance_private (self: cell); |
247 | |
248 | if (priv->model) |
249 | { |
250 | g_object_unref (object: priv->model); |
251 | priv->model = NULL; |
252 | } |
253 | |
254 | G_OBJECT_CLASS (gtk_cell_renderer_combo_parent_class)->finalize (object); |
255 | } |
256 | |
257 | static void |
258 | gtk_cell_renderer_combo_get_property (GObject *object, |
259 | guint prop_id, |
260 | GValue *value, |
261 | GParamSpec *pspec) |
262 | { |
263 | GtkCellRendererCombo *cell = GTK_CELL_RENDERER_COMBO (object); |
264 | GtkCellRendererComboPrivate *priv = gtk_cell_renderer_combo_get_instance_private (self: cell); |
265 | |
266 | switch (prop_id) |
267 | { |
268 | case PROP_MODEL: |
269 | g_value_set_object (value, v_object: priv->model); |
270 | break; |
271 | case PROP_TEXT_COLUMN: |
272 | g_value_set_int (value, v_int: priv->text_column); |
273 | break; |
274 | case PROP_HAS_ENTRY: |
275 | g_value_set_boolean (value, v_boolean: priv->has_entry); |
276 | break; |
277 | default: |
278 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
279 | break; |
280 | } |
281 | } |
282 | |
283 | static void |
284 | gtk_cell_renderer_combo_set_property (GObject *object, |
285 | guint prop_id, |
286 | const GValue *value, |
287 | GParamSpec *pspec) |
288 | { |
289 | GtkCellRendererCombo *cell = GTK_CELL_RENDERER_COMBO (object); |
290 | GtkCellRendererComboPrivate *priv = gtk_cell_renderer_combo_get_instance_private (self: cell); |
291 | |
292 | switch (prop_id) |
293 | { |
294 | case PROP_MODEL: |
295 | { |
296 | if (priv->model) |
297 | g_object_unref (object: priv->model); |
298 | priv->model = GTK_TREE_MODEL (g_value_get_object (value)); |
299 | if (priv->model) |
300 | g_object_ref (priv->model); |
301 | break; |
302 | } |
303 | case PROP_TEXT_COLUMN: |
304 | if (priv->text_column != g_value_get_int (value)) |
305 | { |
306 | priv->text_column = g_value_get_int (value); |
307 | g_object_notify_by_pspec (object, pspec); |
308 | } |
309 | break; |
310 | case PROP_HAS_ENTRY: |
311 | if (priv->has_entry != g_value_get_boolean (value)) |
312 | { |
313 | priv->has_entry = g_value_get_boolean (value); |
314 | g_object_notify_by_pspec (object, pspec); |
315 | } |
316 | break; |
317 | default: |
318 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
319 | break; |
320 | } |
321 | } |
322 | |
323 | static void |
324 | gtk_cell_renderer_combo_changed (GtkComboBox *combo, |
325 | gpointer data) |
326 | { |
327 | GtkTreeIter iter; |
328 | GtkCellRendererCombo *cell; |
329 | |
330 | cell = GTK_CELL_RENDERER_COMBO (data); |
331 | |
332 | if (gtk_combo_box_get_active_iter (combo_box: combo, iter: &iter)) |
333 | { |
334 | const char *path; |
335 | |
336 | path = g_object_get_data (G_OBJECT (combo), GTK_CELL_RENDERER_COMBO_PATH); |
337 | g_signal_emit (instance: cell, signal_id: cell_renderer_combo_signals[CHANGED], detail: 0, |
338 | path, &iter); |
339 | } |
340 | } |
341 | |
342 | static void |
343 | gtk_cell_renderer_combo_editing_done (GtkCellEditable *combo, |
344 | gpointer data) |
345 | { |
346 | GtkCellRendererCombo *cell = GTK_CELL_RENDERER_COMBO (data); |
347 | GtkCellRendererComboPrivate *priv = gtk_cell_renderer_combo_get_instance_private (self: cell); |
348 | const char *path; |
349 | char *new_text = NULL; |
350 | GtkTreeModel *model; |
351 | GtkTreeIter iter; |
352 | GtkEntry *entry; |
353 | gboolean canceled; |
354 | |
355 | if (priv->focus_out_id > 0) |
356 | { |
357 | g_signal_handler_disconnect (instance: combo, handler_id: priv->focus_out_id); |
358 | priv->focus_out_id = 0; |
359 | } |
360 | |
361 | g_object_get (object: combo, |
362 | first_property_name: "editing-canceled" , &canceled, |
363 | NULL); |
364 | gtk_cell_renderer_stop_editing (GTK_CELL_RENDERER (data), canceled); |
365 | if (canceled) |
366 | { |
367 | priv->combo = NULL; |
368 | return; |
369 | } |
370 | |
371 | if (gtk_combo_box_get_has_entry (GTK_COMBO_BOX (combo))) |
372 | { |
373 | entry = GTK_ENTRY (gtk_combo_box_get_child (GTK_COMBO_BOX (combo))); |
374 | new_text = g_strdup (str: gtk_editable_get_text (GTK_EDITABLE (entry))); |
375 | } |
376 | else |
377 | { |
378 | model = gtk_combo_box_get_model (GTK_COMBO_BOX (combo)); |
379 | |
380 | if (model |
381 | && gtk_combo_box_get_active_iter (GTK_COMBO_BOX (combo), iter: &iter)) |
382 | gtk_tree_model_get (tree_model: model, iter: &iter, priv->text_column, &new_text, -1); |
383 | } |
384 | |
385 | path = g_object_get_data (G_OBJECT (combo), GTK_CELL_RENDERER_COMBO_PATH); |
386 | g_signal_emit_by_name (instance: cell, detailed_signal: "edited" , path, new_text); |
387 | |
388 | priv->combo = NULL; |
389 | |
390 | g_free (mem: new_text); |
391 | } |
392 | |
393 | static void |
394 | gtk_cell_renderer_combo_focus_change (GtkWidget *widget, |
395 | GParamSpec *pspec, |
396 | gpointer data) |
397 | { |
398 | if (!gtk_widget_has_focus (widget)) |
399 | gtk_cell_renderer_combo_editing_done (GTK_CELL_EDITABLE (widget), data); |
400 | } |
401 | |
402 | typedef struct |
403 | { |
404 | GtkCellRendererCombo *cell; |
405 | gboolean found; |
406 | GtkTreeIter iter; |
407 | } SearchData; |
408 | |
409 | static gboolean |
410 | find_text (GtkTreeModel *model, |
411 | GtkTreePath *path, |
412 | GtkTreeIter *iter, |
413 | gpointer data) |
414 | { |
415 | SearchData *search_data = (SearchData *)data; |
416 | GtkCellRendererComboPrivate *priv = gtk_cell_renderer_combo_get_instance_private (self: search_data->cell); |
417 | char *text, *cell_text; |
418 | |
419 | gtk_tree_model_get (tree_model: model, iter, priv->text_column, &text, -1); |
420 | g_object_get (GTK_CELL_RENDERER_TEXT (search_data->cell), |
421 | first_property_name: "text" , &cell_text, |
422 | NULL); |
423 | if (text && cell_text && g_strcmp0 (str1: text, str2: cell_text) == 0) |
424 | { |
425 | search_data->iter = *iter; |
426 | search_data->found = TRUE; |
427 | } |
428 | |
429 | g_free (mem: cell_text); |
430 | g_free (mem: text); |
431 | |
432 | return search_data->found; |
433 | } |
434 | |
435 | static GtkCellEditable * |
436 | gtk_cell_renderer_combo_start_editing (GtkCellRenderer *cell, |
437 | GdkEvent *event, |
438 | GtkWidget *widget, |
439 | const char *path, |
440 | const GdkRectangle *background_area, |
441 | const GdkRectangle *cell_area, |
442 | GtkCellRendererState flags) |
443 | { |
444 | GtkCellRendererCombo *cell_combo = GTK_CELL_RENDERER_COMBO (cell); |
445 | GtkCellRendererComboPrivate *priv = gtk_cell_renderer_combo_get_instance_private (self: cell_combo); |
446 | GtkWidget *combo; |
447 | SearchData search_data; |
448 | gboolean editable; |
449 | char *text; |
450 | |
451 | g_object_get (object: cell, first_property_name: "editable" , &editable, NULL); |
452 | if (editable == FALSE) |
453 | return NULL; |
454 | |
455 | if (priv->text_column < 0) |
456 | return NULL; |
457 | |
458 | if (priv->has_entry) |
459 | { |
460 | combo = g_object_new (GTK_TYPE_COMBO_BOX, first_property_name: "has-entry" , TRUE, NULL); |
461 | |
462 | if (priv->model) |
463 | gtk_combo_box_set_model (GTK_COMBO_BOX (combo), model: priv->model); |
464 | gtk_combo_box_set_entry_text_column (GTK_COMBO_BOX (combo), |
465 | text_column: priv->text_column); |
466 | |
467 | g_object_get (object: cell, first_property_name: "text" , &text, NULL); |
468 | if (text) |
469 | gtk_editable_set_text (GTK_EDITABLE (gtk_combo_box_get_child (GTK_COMBO_BOX (combo))), text); |
470 | g_free (mem: text); |
471 | } |
472 | else |
473 | { |
474 | cell = gtk_cell_renderer_text_new (); |
475 | |
476 | combo = gtk_combo_box_new (); |
477 | if (priv->model) |
478 | gtk_combo_box_set_model (GTK_COMBO_BOX (combo), model: priv->model); |
479 | |
480 | gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), cell, TRUE); |
481 | gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), |
482 | cell, "text" , priv->text_column, |
483 | NULL); |
484 | |
485 | /* determine the current value */ |
486 | if (priv->model) |
487 | { |
488 | search_data.cell = cell_combo; |
489 | search_data.found = FALSE; |
490 | gtk_tree_model_foreach (model: priv->model, func: find_text, user_data: &search_data); |
491 | if (search_data.found) |
492 | gtk_combo_box_set_active_iter (GTK_COMBO_BOX (combo), |
493 | iter: &(search_data.iter)); |
494 | } |
495 | } |
496 | |
497 | g_object_set (object: combo, first_property_name: "has-frame" , FALSE, NULL); |
498 | g_object_set_data_full (G_OBJECT (combo), |
499 | I_(GTK_CELL_RENDERER_COMBO_PATH), |
500 | data: g_strdup (str: path), destroy: g_free); |
501 | |
502 | gtk_widget_show (widget: combo); |
503 | |
504 | g_signal_connect (GTK_CELL_EDITABLE (combo), "editing-done" , |
505 | G_CALLBACK (gtk_cell_renderer_combo_editing_done), |
506 | cell_combo); |
507 | g_signal_connect (GTK_CELL_EDITABLE (combo), "changed" , |
508 | G_CALLBACK (gtk_cell_renderer_combo_changed), |
509 | cell_combo); |
510 | priv->focus_out_id = g_signal_connect (combo, "notify::has-focus" , |
511 | G_CALLBACK (gtk_cell_renderer_combo_focus_change), |
512 | cell_combo); |
513 | |
514 | priv->combo = combo; |
515 | |
516 | return GTK_CELL_EDITABLE (combo); |
517 | } |
518 | |