1 /*
2 * list.c
3 * Copyright 2011-2013 John Lindgren and Michał Lipski
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice,
9 * this list of conditions, and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions, and the following disclaimer in the documentation
13 * provided with the distribution.
14 *
15 * This software is provided "as is" and without any warranty, express or
16 * implied. In no event shall the authors be liable for any damages arising from
17 * the use of this software.
18 */
19
20 #include <stddef.h>
21 #include <gtk/gtk.h>
22
23 #include <libaudcore/hook.h>
24 #include <libaudcore/objects.h>
25
26 #include "libaudgui-gtk.h"
27 #include "list.h"
28
29 enum {
30 HIGHLIGHT_COLUMN,
31 RESERVED_COLUMNS
32 };
33
34 #define MODEL_HAS_CB(m, cb) \
35 ((m)->cbs_size > (int) offsetof (AudguiListCallbacks, cb) && (m)->cbs->cb)
36 #define PATH_IS_SELECTED(w, p) (gtk_tree_selection_path_is_selected \
37 (gtk_tree_view_get_selection ((GtkTreeView *) (w)), (p)))
38
39 struct ListModel {
40 GObject parent;
41 const AudguiListCallbacks * cbs;
42 int cbs_size;
43 void * user;
44 int charwidth;
45 int rows, highlight;
46 int columns;
47 GList * column_types;
48 bool resizable;
49 bool frozen, blocked;
50 bool dragging;
51 int clicked_row, receive_row;
52 int scroll_speed;
53 };
54
55 /* ==== MODEL ==== */
56
list_model_get_flags(GtkTreeModel * model)57 static GtkTreeModelFlags list_model_get_flags (GtkTreeModel * model)
58 {
59 return GTK_TREE_MODEL_LIST_ONLY;
60 }
61
list_model_get_n_columns(GtkTreeModel * model)62 static int list_model_get_n_columns (GtkTreeModel * model)
63 {
64 return ((ListModel *) model)->columns;
65 }
66
list_model_get_column_type(GtkTreeModel * _model,int column)67 static GType list_model_get_column_type (GtkTreeModel * _model, int column)
68 {
69 ListModel * model = (ListModel *) _model;
70 g_return_val_if_fail (column >= 0 && column < model->columns, G_TYPE_INVALID);
71
72 if (column == HIGHLIGHT_COLUMN)
73 return PANGO_TYPE_WEIGHT;
74
75 return GPOINTER_TO_INT (g_list_nth_data (model->column_types, column -
76 RESERVED_COLUMNS));
77 }
78
list_model_get_iter(GtkTreeModel * model,GtkTreeIter * iter,GtkTreePath * path)79 static gboolean list_model_get_iter (GtkTreeModel * model, GtkTreeIter * iter,
80 GtkTreePath * path)
81 {
82 int row = gtk_tree_path_get_indices (path)[0];
83 if (row < 0 || row >= ((ListModel *) model)->rows)
84 return false;
85 iter->user_data = GINT_TO_POINTER (row);
86 return true;
87 }
88
list_model_get_path(GtkTreeModel * model,GtkTreeIter * iter)89 static GtkTreePath * list_model_get_path (GtkTreeModel * model,
90 GtkTreeIter * iter)
91 {
92 int row = GPOINTER_TO_INT (iter->user_data);
93 g_return_val_if_fail (row >= 0 && row < ((ListModel *) model)->rows, nullptr);
94 return gtk_tree_path_new_from_indices (row, -1);
95 }
96
list_model_get_value(GtkTreeModel * _model,GtkTreeIter * iter,int column,GValue * value)97 static void list_model_get_value (GtkTreeModel * _model, GtkTreeIter * iter,
98 int column, GValue * value)
99 {
100 ListModel * model = (ListModel *) _model;
101 int row = GPOINTER_TO_INT (iter->user_data);
102 g_return_if_fail (column >= 0 && column < model->columns);
103 g_return_if_fail (row >= 0 && row < model->rows);
104
105 if (column == HIGHLIGHT_COLUMN)
106 {
107 g_value_init (value, PANGO_TYPE_WEIGHT);
108 g_value_set_enum (value, row == model->highlight ? PANGO_WEIGHT_BOLD :
109 PANGO_WEIGHT_NORMAL);
110 return;
111 }
112
113 g_value_init (value, GPOINTER_TO_INT (g_list_nth_data (model->column_types,
114 column - RESERVED_COLUMNS)));
115 model->cbs->get_value (model->user, row, column - RESERVED_COLUMNS, value);
116 }
117
list_model_iter_next(GtkTreeModel * _model,GtkTreeIter * iter)118 static gboolean list_model_iter_next (GtkTreeModel * _model, GtkTreeIter * iter)
119 {
120 ListModel * model = (ListModel *) _model;
121 int row = GPOINTER_TO_INT (iter->user_data);
122 g_return_val_if_fail (row >= 0 && row < model->rows, false);
123 if (row + 1 >= model->rows)
124 return false;
125 iter->user_data = GINT_TO_POINTER (row + 1);
126 return true;
127 }
128
list_model_iter_children(GtkTreeModel * model,GtkTreeIter * iter,GtkTreeIter * parent)129 static gboolean list_model_iter_children (GtkTreeModel * model,
130 GtkTreeIter * iter, GtkTreeIter * parent)
131 {
132 if (parent || ((ListModel *) model)->rows < 1)
133 return false;
134 iter->user_data = GINT_TO_POINTER (0);
135 return true;
136 }
137
list_model_iter_has_child(GtkTreeModel * model,GtkTreeIter * iter)138 static gboolean list_model_iter_has_child (GtkTreeModel * model,
139 GtkTreeIter * iter)
140 {
141 return false;
142 }
143
list_model_iter_n_children(GtkTreeModel * model,GtkTreeIter * iter)144 static int list_model_iter_n_children (GtkTreeModel * model, GtkTreeIter * iter)
145 {
146 return iter ? 0 : ((ListModel *) model)->rows;
147 }
148
list_model_iter_nth_child(GtkTreeModel * model,GtkTreeIter * iter,GtkTreeIter * parent,int n)149 static gboolean list_model_iter_nth_child (GtkTreeModel * model,
150 GtkTreeIter * iter, GtkTreeIter * parent, int n)
151 {
152 if (parent || n < 0 || n >= ((ListModel *) model)->rows)
153 return false;
154 iter->user_data = GINT_TO_POINTER (n);
155 return true;
156 }
157
list_model_iter_parent(GtkTreeModel * model,GtkTreeIter * iter,GtkTreeIter * child)158 static gboolean list_model_iter_parent (GtkTreeModel * model,
159 GtkTreeIter * iter, GtkTreeIter * child)
160 {
161 return false;
162 }
163
iface_init(GtkTreeModelIface * iface)164 static void iface_init (GtkTreeModelIface * iface)
165 {
166 iface->get_flags = list_model_get_flags;
167 iface->get_n_columns = list_model_get_n_columns;
168 iface->get_column_type = list_model_get_column_type;
169 iface->get_iter = list_model_get_iter;
170 iface->get_path = list_model_get_path;
171 iface->get_value = list_model_get_value;
172 iface->iter_next = list_model_iter_next;
173 iface->iter_children = list_model_iter_children;
174 iface->iter_has_child = list_model_iter_has_child;
175 iface->iter_n_children = list_model_iter_n_children;
176 iface->iter_nth_child = list_model_iter_nth_child;
177 iface->iter_parent = list_model_iter_parent;
178 }
179
180 static const GInterfaceInfo iface_info = {
181 (GInterfaceInitFunc) iface_init
182 };
183
list_model_get_type()184 static GType list_model_get_type ()
185 {
186 static GType type = G_TYPE_INVALID;
187 if (type == G_TYPE_INVALID)
188 {
189 type = g_type_register_static_simple (G_TYPE_OBJECT, "AudguiListModel",
190 sizeof (GObjectClass), nullptr, sizeof (ListModel), nullptr, (GTypeFlags) 0);
191 g_type_add_interface_static (type, GTK_TYPE_TREE_MODEL, & iface_info);
192 }
193 return type;
194 }
195
196 /* ==== CALLBACKS ==== */
197
select_allow_cb(GtkTreeSelection * sel,GtkTreeModel * model,GtkTreePath * path,gboolean was,void * user)198 static gboolean select_allow_cb (GtkTreeSelection * sel, GtkTreeModel * model,
199 GtkTreePath * path, gboolean was, void * user)
200 {
201 return ! ((ListModel *) model)->frozen;
202 }
203
select_row_cb(GtkTreeModel * _model,GtkTreePath * path,GtkTreeIter * iter,void * user)204 static void select_row_cb (GtkTreeModel * _model, GtkTreePath * path,
205 GtkTreeIter * iter, void * user)
206 {
207 ListModel * model = (ListModel *) _model;
208 int row = gtk_tree_path_get_indices (path)[0];
209 g_return_if_fail (row >= 0 && row < model->rows);
210 model->cbs->set_selected (model->user, row, true);
211 }
212
select_cb(GtkTreeSelection * sel,ListModel * model)213 static void select_cb (GtkTreeSelection * sel, ListModel * model)
214 {
215 if (model->blocked)
216 return;
217 model->cbs->select_all (model->user, false);
218 gtk_tree_selection_selected_foreach (sel, select_row_cb, nullptr);
219 }
220
focus_cb(GtkTreeView * tree,ListModel * model)221 static void focus_cb (GtkTreeView * tree, ListModel * model)
222 {
223 if (! model->blocked)
224 model->cbs->focus_change (model->user,
225 audgui_list_get_focus ((GtkWidget *) tree));
226 }
227
activate_cb(GtkTreeView * tree,GtkTreePath * path,GtkTreeViewColumn * col,ListModel * model)228 static void activate_cb (GtkTreeView * tree, GtkTreePath * path,
229 GtkTreeViewColumn * col, ListModel * model)
230 {
231 int row = gtk_tree_path_get_indices (path)[0];
232 g_return_if_fail (row >= 0 && row < model->rows);
233 model->cbs->activate_row (model->user, row);
234 }
235
button_press_cb(GtkWidget * widget,GdkEventButton * event,ListModel * model)236 static gboolean button_press_cb (GtkWidget * widget, GdkEventButton * event,
237 ListModel * model)
238 {
239 GtkTreePath * path = nullptr;
240 gtk_tree_view_get_path_at_pos ((GtkTreeView *) widget, event->x, event->y,
241 & path, nullptr, nullptr, nullptr);
242
243 if (event->type == GDK_BUTTON_PRESS && event->button == 3
244 && MODEL_HAS_CB (model, right_click))
245 {
246 /* Only allow GTK to select this row if it is not already selected. We
247 * don't want to clear a multiple selection. */
248 if (path)
249 {
250 if (PATH_IS_SELECTED (widget, path))
251 model->frozen = true;
252 gtk_tree_view_set_cursor ((GtkTreeView *) widget, path, nullptr, false);
253 model->frozen = false;
254 }
255
256 model->cbs->right_click (model->user, event);
257
258 if (path)
259 gtk_tree_path_free (path);
260 return true;
261 }
262
263 /* Only allow GTK to select this row if it is not already selected. If we
264 * are going to be dragging, we don't want to clear a multiple selection.
265 * If this is just a simple click, we will clear the multiple selection in
266 * button_release_cb. */
267 if (event->type == GDK_BUTTON_PRESS && event->button == 1 && ! (event->state
268 & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) && path && PATH_IS_SELECTED (widget,
269 path))
270 model->frozen = true;
271
272 if (path)
273 model->clicked_row = gtk_tree_path_get_indices (path)[0];
274 else
275 model->clicked_row = -1;
276
277 if (path)
278 gtk_tree_path_free (path);
279 return false;
280 }
281
button_release_cb(GtkWidget * widget,GdkEventButton * event,ListModel * model)282 static gboolean button_release_cb (GtkWidget * widget, GdkEventButton * event,
283 ListModel * model)
284 {
285 /* If button_press_cb set "frozen", and we were not dragging, we need to
286 * clear a multiple selection. */
287 if (model->frozen && model->clicked_row >= 0 && model->clicked_row <
288 model->rows)
289 {
290 model->frozen = false;
291 GtkTreePath * path = gtk_tree_path_new_from_indices (model->clicked_row, -1);
292 gtk_tree_view_set_cursor ((GtkTreeView *) widget, path, nullptr, false);
293 gtk_tree_path_free (path);
294 }
295
296 return false;
297 }
298
key_press_cb(GtkWidget * widget,GdkEventKey * event,ListModel * model)299 static gboolean key_press_cb (GtkWidget * widget, GdkEventKey * event, ListModel * model)
300 {
301 /* GTK thinks the spacebar should activate a row; I (jlindgren) disagree */
302 if (event->keyval == ' ' && ! (event->state & GDK_CONTROL_MASK))
303 return true;
304
305 return false;
306 }
307
motion_notify_cb(GtkWidget * widget,GdkEventMotion * event,ListModel * model)308 static gboolean motion_notify_cb (GtkWidget * widget, GdkEventMotion * event, ListModel * model)
309 {
310 if (MODEL_HAS_CB (model, mouse_motion))
311 {
312 int x, y;
313 gtk_tree_view_convert_bin_window_to_widget_coords ((GtkTreeView *)
314 widget, event->x, event->y, & x, & y);
315
316 int row = audgui_list_row_at_point (widget, x, y);
317 model->cbs->mouse_motion (model->user, event, row);
318 }
319
320 return false;
321 }
322
leave_notify_cb(GtkWidget * widget,GdkEventMotion * event,ListModel * model)323 static gboolean leave_notify_cb (GtkWidget * widget, GdkEventMotion * event, ListModel * model)
324 {
325 if (MODEL_HAS_CB (model, mouse_leave))
326 {
327 int x, y;
328 gtk_tree_view_convert_bin_window_to_widget_coords ((GtkTreeView *)
329 widget, event->x, event->y, & x, & y);
330
331 int row = audgui_list_row_at_point (widget, x, y);
332 model->cbs->mouse_leave (model->user, event, row);
333 }
334
335 return false;
336 }
337
338 /* ==== DRAG AND DROP ==== */
339
drag_begin(GtkWidget * widget,GdkDragContext * context,ListModel * model)340 static void drag_begin (GtkWidget * widget, GdkDragContext * context,
341 ListModel * model)
342 {
343 g_signal_stop_emission_by_name (widget, "drag-begin");
344
345 model->dragging = true;
346
347 /* If button_press_cb preserved a multiple selection, tell button_release_cb
348 * not to clear it. */
349 model->frozen = false;
350 }
351
drag_end(GtkWidget * widget,GdkDragContext * context,ListModel * model)352 static void drag_end (GtkWidget * widget, GdkDragContext * context,
353 ListModel * model)
354 {
355 g_signal_stop_emission_by_name (widget, "drag-end");
356
357 model->dragging = false;
358 model->clicked_row = -1;
359 }
360
drag_data_get(GtkWidget * widget,GdkDragContext * context,GtkSelectionData * sel,unsigned info,unsigned time,ListModel * model)361 static void drag_data_get (GtkWidget * widget, GdkDragContext * context,
362 GtkSelectionData * sel, unsigned info, unsigned time, ListModel * model)
363 {
364 g_signal_stop_emission_by_name (widget, "drag-data-get");
365
366 Index<char> data = model->cbs->get_data (model->user);
367 gtk_selection_data_set (sel, gdk_atom_intern (model->cbs->data_type, false),
368 8, (const unsigned char *) data.begin (), data.len ());
369 }
370
get_scroll_pos(GtkAdjustment * adj,int & pos,int & end)371 static void get_scroll_pos (GtkAdjustment * adj, int & pos, int & end)
372 {
373 pos = gtk_adjustment_get_value (adj);
374 end = gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj);
375 }
376
can_scroll(int pos,int end,int speed)377 static bool can_scroll (int pos, int end, int speed)
378 {
379 if (speed > 0)
380 return pos < end;
381 if (speed < 0)
382 return pos > 0;
383
384 return false;
385 }
386
autoscroll(void * widget)387 static void autoscroll (void * widget)
388 {
389 ListModel * model = (ListModel *) gtk_tree_view_get_model
390 ((GtkTreeView *) widget);
391
392 GtkAdjustment * adj = gtk_tree_view_get_vadjustment ((GtkTreeView *) widget);
393 g_return_if_fail (adj);
394
395 int pos, end;
396 get_scroll_pos (adj, pos, end);
397 pos = aud::clamp (pos + model->scroll_speed, 0, end);
398 gtk_adjustment_set_value (adj, pos);
399
400 if (! can_scroll (pos, end, model->scroll_speed))
401 {
402 model->scroll_speed = 0;
403 timer_remove (TimerRate::Hz30, autoscroll, widget);
404 }
405 }
406
start_autoscroll(ListModel * model,GtkWidget * widget,int speed)407 static void start_autoscroll (ListModel * model, GtkWidget * widget, int speed)
408 {
409 GtkAdjustment * adj = gtk_tree_view_get_vadjustment ((GtkTreeView *) widget);
410 g_return_if_fail (adj);
411
412 int pos, end;
413 get_scroll_pos (adj, pos, end);
414
415 if (can_scroll (pos, end, speed))
416 {
417 model->scroll_speed = speed;
418 timer_add (TimerRate::Hz30, autoscroll, widget);
419 }
420 }
421
stop_autoscroll(ListModel * model,GtkWidget * widget)422 static void stop_autoscroll (ListModel * model, GtkWidget * widget)
423 {
424 model->scroll_speed = 0;
425 timer_remove (TimerRate::Hz30, autoscroll, widget);
426 }
427
drag_motion(GtkWidget * widget,GdkDragContext * context,int x,int y,unsigned time,ListModel * model)428 static gboolean drag_motion (GtkWidget * widget, GdkDragContext * context,
429 int x, int y, unsigned time, ListModel * model)
430 {
431 g_signal_stop_emission_by_name (widget, "drag-motion");
432
433 if (model->dragging && MODEL_HAS_CB (model, shift_rows))
434 gdk_drag_status (context, GDK_ACTION_MOVE, time); /* dragging within same list */
435 else if (MODEL_HAS_CB (model, data_type) && MODEL_HAS_CB (model, receive_data))
436 gdk_drag_status (context, GDK_ACTION_COPY, time); /* cross-widget dragging */
437 else
438 return false;
439
440 if (model->rows > 0)
441 {
442 int row = audgui_list_row_at_point_rounded (widget, x, y);
443 if (row == model->rows)
444 {
445 GtkTreePath * path = gtk_tree_path_new_from_indices (row - 1, -1);
446 gtk_tree_view_set_drag_dest_row ((GtkTreeView *) widget, path,
447 GTK_TREE_VIEW_DROP_AFTER);
448 gtk_tree_path_free (path);
449 }
450 else
451 {
452 GtkTreePath * path = gtk_tree_path_new_from_indices (row, -1);
453 gtk_tree_view_set_drag_dest_row ((GtkTreeView *) widget, path,
454 GTK_TREE_VIEW_DROP_BEFORE);
455 gtk_tree_path_free (path);
456 }
457 }
458
459 gtk_tree_view_convert_widget_to_bin_window_coords ((GtkTreeView *) widget,
460 x, y, & x, & y);
461
462 int height = gdk_window_get_height (gtk_tree_view_get_bin_window ((GtkTreeView *) widget));
463 int hotspot = aud::min (height / 4, audgui_get_dpi () / 2);
464
465 if (y >= 0 && y < hotspot)
466 start_autoscroll (model, widget, y - hotspot);
467 else if (y >= height - hotspot && y < height)
468 start_autoscroll (model, widget, y - (height - hotspot));
469 else
470 stop_autoscroll (model, widget);
471
472 return true;
473 }
474
drag_leave(GtkWidget * widget,GdkDragContext * context,unsigned time,ListModel * model)475 static void drag_leave (GtkWidget * widget, GdkDragContext * context,
476 unsigned time, ListModel * model)
477 {
478 g_signal_stop_emission_by_name (widget, "drag-leave");
479
480 gtk_tree_view_set_drag_dest_row ((GtkTreeView *) widget, nullptr, (GtkTreeViewDropPosition) 0);
481 stop_autoscroll (model, widget);
482 }
483
drag_drop(GtkWidget * widget,GdkDragContext * context,int x,int y,unsigned time,ListModel * model)484 static gboolean drag_drop (GtkWidget * widget, GdkDragContext * context, int x,
485 int y, unsigned time, ListModel * model)
486 {
487 g_signal_stop_emission_by_name (widget, "drag-drop");
488
489 gboolean success = true;
490 int row = audgui_list_row_at_point_rounded (widget, x, y);
491
492 if (model->dragging && MODEL_HAS_CB (model, shift_rows))
493 {
494 /* dragging within same list */
495 if (model->clicked_row >= 0 && model->clicked_row < model->rows)
496 model->cbs->shift_rows (model->user, model->clicked_row, row);
497 else
498 success = false;
499 }
500 else if (MODEL_HAS_CB (model, data_type) && MODEL_HAS_CB (model, receive_data))
501 {
502 /* cross-widget dragging */
503 model->receive_row = row;
504 gtk_drag_get_data (widget, context, gdk_atom_intern
505 (model->cbs->data_type, false), time);
506 }
507 else
508 success = false;
509
510 gtk_drag_finish (context, success, false, time);
511 gtk_tree_view_set_drag_dest_row ((GtkTreeView *) widget, nullptr, (GtkTreeViewDropPosition) 0);
512 stop_autoscroll (model, widget);
513 return true;
514 }
515
drag_data_received(GtkWidget * widget,GdkDragContext * context,int x,int y,GtkSelectionData * sel,unsigned info,unsigned time,ListModel * model)516 static void drag_data_received (GtkWidget * widget, GdkDragContext * context, int x,
517 int y, GtkSelectionData * sel, unsigned info, unsigned time, ListModel * model)
518 {
519 g_signal_stop_emission_by_name (widget, "drag-data-received");
520
521 g_return_if_fail (model->receive_row >= 0 && model->receive_row <=
522 model->rows);
523
524 auto data = (const char *) gtk_selection_data_get_data (sel);
525 int length = gtk_selection_data_get_length (sel);
526
527 if (data && length)
528 model->cbs->receive_data (model->user, model->receive_row, data, length);
529
530 model->receive_row = -1;
531 }
532
533 /* ==== PUBLIC FUNCS ==== */
534
destroy_cb(GtkWidget * list,ListModel * model)535 static void destroy_cb (GtkWidget * list, ListModel * model)
536 {
537 stop_autoscroll (model, list);
538 g_list_free (model->column_types);
539 g_object_unref (model);
540 }
541
update_selection(GtkWidget * list,ListModel * model,int at,int rows)542 static void update_selection (GtkWidget * list, ListModel * model, int at,
543 int rows)
544 {
545 model->blocked = true;
546 GtkTreeSelection * sel = gtk_tree_view_get_selection ((GtkTreeView *) list);
547
548 for (int i = at; i < at + rows; i ++)
549 {
550 GtkTreeIter iter = {0, GINT_TO_POINTER (i)};
551 if (model->cbs->get_selected (model->user, i))
552 gtk_tree_selection_select_iter (sel, & iter);
553 else
554 gtk_tree_selection_unselect_iter (sel, & iter);
555 }
556
557 model->blocked = false;
558 }
559
audgui_list_new_real(const AudguiListCallbacks * cbs,int cbs_size,void * user,int rows)560 EXPORT GtkWidget * audgui_list_new_real (const AudguiListCallbacks * cbs, int cbs_size,
561 void * user, int rows)
562 {
563 g_return_val_if_fail (cbs->get_value, nullptr);
564
565 ListModel * model = (ListModel *) g_object_new (list_model_get_type (), nullptr);
566 model->cbs = cbs;
567 model->cbs_size = cbs_size;
568 model->user = user;
569 model->rows = rows;
570 model->highlight = -1;
571 model->columns = RESERVED_COLUMNS;
572 model->column_types = nullptr;
573 model->resizable = true;
574 model->frozen = false;
575 model->blocked = false;
576 model->dragging = false;
577 model->clicked_row = -1;
578 model->receive_row = -1;
579 model->scroll_speed = 0;
580
581 GtkWidget * list = gtk_tree_view_new_with_model ((GtkTreeModel *) model);
582 gtk_tree_view_set_fixed_height_mode ((GtkTreeView *) list, true);
583 g_signal_connect (list, "destroy", (GCallback) destroy_cb, model);
584
585 model->charwidth = audgui_get_digit_width (list);
586
587 if (MODEL_HAS_CB (model, get_selected) && MODEL_HAS_CB (model, set_selected)
588 && MODEL_HAS_CB (model, select_all))
589 {
590 GtkTreeSelection * sel = gtk_tree_view_get_selection
591 ((GtkTreeView *) list);
592 gtk_tree_selection_set_mode (sel, GTK_SELECTION_MULTIPLE);
593 gtk_tree_selection_set_select_function (sel, select_allow_cb, nullptr, nullptr);
594 g_signal_connect (sel, "changed", (GCallback) select_cb, model);
595
596 update_selection (list, model, 0, rows);
597 }
598
599 if (MODEL_HAS_CB (model, focus_change))
600 g_signal_connect (list, "cursor-changed", (GCallback) focus_cb, model);
601
602 if (MODEL_HAS_CB (model, activate_row))
603 g_signal_connect (list, "row-activated", (GCallback) activate_cb, model);
604
605 g_signal_connect (list, "button-press-event", (GCallback) button_press_cb, model);
606 g_signal_connect (list, "button-release-event", (GCallback) button_release_cb, model);
607 g_signal_connect (list, "key-press-event", (GCallback) key_press_cb, model);
608 g_signal_connect (list, "motion-notify-event", (GCallback) motion_notify_cb, model);
609 g_signal_connect (list, "leave-notify-event", (GCallback) leave_notify_cb, model);
610
611 gboolean supports_drag = false;
612
613 if (MODEL_HAS_CB (model, data_type) &&
614 (MODEL_HAS_CB (model, get_data) || MODEL_HAS_CB (model, receive_data)))
615 {
616 const GtkTargetEntry target = {(char *) cbs->data_type, 0, 0};
617
618 if (MODEL_HAS_CB (model, get_data))
619 {
620 gtk_drag_source_set (list, GDK_BUTTON1_MASK, & target, 1, GDK_ACTION_COPY);
621 g_signal_connect (list, "drag-data-get", (GCallback) drag_data_get, model);
622 }
623
624 if (MODEL_HAS_CB (model, receive_data))
625 {
626 gtk_drag_dest_set (list, (GtkDestDefaults) 0, & target, 1, GDK_ACTION_COPY);
627 g_signal_connect (list, "drag-data-received", (GCallback) drag_data_received, model);
628 }
629
630 supports_drag = true;
631 }
632 else if (MODEL_HAS_CB (model, shift_rows))
633 {
634 gtk_drag_source_set (list, GDK_BUTTON1_MASK, nullptr, 0, GDK_ACTION_COPY);
635 gtk_drag_dest_set (list, (GtkDestDefaults) 0, nullptr, 0, GDK_ACTION_COPY);
636 supports_drag = true;
637 }
638
639 if (supports_drag)
640 {
641 g_signal_connect (list, "drag-begin", (GCallback) drag_begin, model);
642 g_signal_connect (list, "drag-end", (GCallback) drag_end, model);
643 g_signal_connect (list, "drag-motion", (GCallback) drag_motion, model);
644 g_signal_connect (list, "drag-leave", (GCallback) drag_leave, model);
645 g_signal_connect (list, "drag-drop", (GCallback) drag_drop, model);
646 }
647
648 return list;
649 }
650
audgui_list_get_user(GtkWidget * list)651 EXPORT void * audgui_list_get_user (GtkWidget * list)
652 {
653 ListModel * model = (ListModel *) gtk_tree_view_get_model
654 ((GtkTreeView *) list);
655 return model->user;
656 }
657
audgui_list_add_column(GtkWidget * list,const char * title,int column,GType type,int width,bool use_markup)658 EXPORT void audgui_list_add_column (GtkWidget * list, const char * title,
659 int column, GType type, int width, bool use_markup)
660 {
661 ListModel * model = (ListModel *) gtk_tree_view_get_model
662 ((GtkTreeView *) list);
663 g_return_if_fail (RESERVED_COLUMNS + column == model->columns);
664
665 model->columns ++;
666 model->column_types = g_list_append (model->column_types, GINT_TO_POINTER
667 (type));
668
669 GtkCellRenderer * renderer = gtk_cell_renderer_text_new ();
670
671 GtkTreeViewColumn * tree_column = use_markup ?
672 gtk_tree_view_column_new_with_attributes
673 (title, renderer, "markup", RESERVED_COLUMNS + column, nullptr) :
674 gtk_tree_view_column_new_with_attributes
675 (title, renderer, "text", RESERVED_COLUMNS + column, "weight", HIGHLIGHT_COLUMN, nullptr);
676
677 gtk_tree_view_column_set_sizing (tree_column, GTK_TREE_VIEW_COLUMN_FIXED);
678
679 int pad1, pad2, pad3;
680 gtk_widget_style_get (list, "horizontal-separator", & pad1, "focus-line-width", & pad2, nullptr);
681 gtk_cell_renderer_get_padding (renderer, & pad3, nullptr);
682 int padding = pad1 + 2 * pad2 + 2 * pad3;
683
684 if (width < 0)
685 {
686 gtk_tree_view_column_set_expand (tree_column, true);
687 model->resizable = false; // columns to the right will not be resizable
688 }
689 else
690 {
691 gtk_tree_view_column_set_resizable (tree_column, model->resizable);
692 gtk_tree_view_column_set_min_width (tree_column,
693 width * model->charwidth + model->charwidth / 2 + padding);
694 }
695
696 if (width >= 0 && width < 10)
697 g_object_set ((GObject *) renderer, "xalign", (float) 1, nullptr);
698 else
699 g_object_set ((GObject *) renderer, "ellipsize-set", true, "ellipsize",
700 PANGO_ELLIPSIZE_END, nullptr);
701
702 gtk_tree_view_append_column ((GtkTreeView *) list, tree_column);
703 }
704
audgui_list_row_count(GtkWidget * list)705 EXPORT int audgui_list_row_count (GtkWidget * list)
706 {
707 return ((ListModel *) gtk_tree_view_get_model ((GtkTreeView *) list))->rows;
708 }
709
audgui_list_insert_rows(GtkWidget * list,int at,int rows)710 EXPORT void audgui_list_insert_rows (GtkWidget * list, int at, int rows)
711 {
712 ListModel * model = (ListModel *) gtk_tree_view_get_model
713 ((GtkTreeView *) list);
714 g_return_if_fail (at >= 0 && at <= model->rows && rows >= 0);
715
716 model->rows += rows;
717 if (model->highlight >= at)
718 model->highlight += rows;
719
720 GtkTreeIter iter = {0, GINT_TO_POINTER (at)};
721 GtkTreePath * path = gtk_tree_path_new_from_indices (at, -1);
722
723 for (int i = rows; i --; )
724 gtk_tree_model_row_inserted ((GtkTreeModel *) model, path, & iter);
725
726 gtk_tree_path_free (path);
727
728 if (model->cbs->get_selected)
729 update_selection (list, model, at, rows);
730 }
731
audgui_list_update_rows(GtkWidget * list,int at,int rows)732 EXPORT void audgui_list_update_rows (GtkWidget * list, int at, int rows)
733 {
734 ListModel * model = (ListModel *) gtk_tree_view_get_model
735 ((GtkTreeView *) list);
736 g_return_if_fail (at >= 0 && rows >= 0 && at + rows <= model->rows);
737
738 GtkTreeIter iter = {0, GINT_TO_POINTER (at)};
739 GtkTreePath * path = gtk_tree_path_new_from_indices (at, -1);
740
741 while (rows --)
742 {
743 gtk_tree_model_row_changed ((GtkTreeModel *) model, path, & iter);
744 iter.user_data = GINT_TO_POINTER (GPOINTER_TO_INT (iter.user_data) + 1);
745 gtk_tree_path_next (path);
746 }
747
748 gtk_tree_path_free (path);
749 }
750
audgui_list_delete_rows(GtkWidget * list,int at,int rows)751 EXPORT void audgui_list_delete_rows (GtkWidget * list, int at, int rows)
752 {
753 ListModel * model = (ListModel *) gtk_tree_view_get_model
754 ((GtkTreeView *) list);
755 g_return_if_fail (at >= 0 && rows >= 0 && at + rows <= model->rows);
756
757 model->rows -= rows;
758 if (model->highlight >= at + rows)
759 model->highlight -= rows;
760 else if (model->highlight >= at)
761 model->highlight = -1;
762
763 model->frozen = true;
764 model->blocked = true;
765
766 int focus = audgui_list_get_focus (list);
767
768 // first delete rows after cursor so it does not get moved to one of them
769 if (focus >= at && focus + 1 < at + rows)
770 {
771 GtkTreePath * path = gtk_tree_path_new_from_indices (focus + 1, -1);
772
773 while (focus + 1 < at + rows)
774 {
775 gtk_tree_model_row_deleted ((GtkTreeModel *) model, path);
776 rows --;
777 }
778
779 gtk_tree_path_free (path);
780 }
781
782 // now delete rows preceding cursor and finally cursor row itself
783 GtkTreePath * path = gtk_tree_path_new_from_indices (at, -1);
784
785 while (rows --)
786 gtk_tree_model_row_deleted ((GtkTreeModel *) model, path);
787
788 gtk_tree_path_free (path);
789
790 model->frozen = false;
791 model->blocked = false;
792 }
793
audgui_list_update_selection(GtkWidget * list,int at,int rows)794 EXPORT void audgui_list_update_selection (GtkWidget * list, int at, int rows)
795 {
796 ListModel * model = (ListModel *) gtk_tree_view_get_model
797 ((GtkTreeView *) list);
798 g_return_if_fail (model->cbs->get_selected);
799 g_return_if_fail (at >= 0 && rows >= 0 && at + rows <= model->rows);
800 update_selection (list, model, at, rows);
801 }
802
audgui_list_get_highlight(GtkWidget * list)803 EXPORT int audgui_list_get_highlight (GtkWidget * list)
804 {
805 ListModel * model = (ListModel *) gtk_tree_view_get_model
806 ((GtkTreeView *) list);
807 return model->highlight;
808 }
809
audgui_list_set_highlight(GtkWidget * list,int row)810 EXPORT void audgui_list_set_highlight (GtkWidget * list, int row)
811 {
812 ListModel * model = (ListModel *) gtk_tree_view_get_model
813 ((GtkTreeView *) list);
814 g_return_if_fail (row >= -1 && row < model->rows);
815
816 int old = model->highlight;
817 if (row == old)
818 return;
819 model->highlight = row;
820
821 if (old >= 0)
822 audgui_list_update_rows (list, old, 1);
823 if (row >= 0)
824 audgui_list_update_rows (list, row, 1);
825 }
826
audgui_list_get_focus(GtkWidget * list)827 EXPORT int audgui_list_get_focus (GtkWidget * list)
828 {
829 GtkTreePath * path = nullptr;
830 gtk_tree_view_get_cursor ((GtkTreeView *) list, & path, nullptr);
831
832 if (! path)
833 return -1;
834
835 int row = gtk_tree_path_get_indices (path)[0];
836
837 gtk_tree_path_free (path);
838 return row;
839 }
840
audgui_list_set_focus(GtkWidget * list,int row)841 EXPORT void audgui_list_set_focus (GtkWidget * list, int row)
842 {
843 ListModel * model = (ListModel *) gtk_tree_view_get_model
844 ((GtkTreeView *) list);
845 g_return_if_fail (row >= -1 && row < model->rows);
846
847 if (row < 0 || row == audgui_list_get_focus (list))
848 return;
849
850 model->frozen = true;
851 model->blocked = true;
852
853 GtkTreePath * path = gtk_tree_path_new_from_indices (row, -1);
854 gtk_tree_view_set_cursor ((GtkTreeView *) list, path, nullptr, false);
855 gtk_tree_view_scroll_to_cell ((GtkTreeView *) list, path, nullptr, false, 0, 0);
856 gtk_tree_path_free (path);
857
858 model->frozen = false;
859 model->blocked = false;
860 }
861
audgui_list_row_at_point(GtkWidget * list,int x,int y)862 EXPORT int audgui_list_row_at_point (GtkWidget * list, int x, int y)
863 {
864 ListModel * model = (ListModel *) gtk_tree_view_get_model ((GtkTreeView *) list);
865
866 GtkTreePath * path = nullptr;
867 gtk_tree_view_convert_widget_to_bin_window_coords ((GtkTreeView *) list, x, y, & x, & y);
868 gtk_tree_view_get_path_at_pos ((GtkTreeView *) list, x, y, & path, nullptr, nullptr, nullptr);
869
870 if (! path)
871 return -1;
872
873 int row = gtk_tree_path_get_indices (path)[0];
874 g_return_val_if_fail (row >= 0 && row < model->rows, -1);
875
876 gtk_tree_path_free (path);
877 return row;
878 }
879
880 /* note that this variant always returns a valid row (or row + 1) */
audgui_list_row_at_point_rounded(GtkWidget * list,int x,int y)881 EXPORT int audgui_list_row_at_point_rounded (GtkWidget * list, int x, int y)
882 {
883 ListModel * model = (ListModel *) gtk_tree_view_get_model ((GtkTreeView *) list);
884
885 gtk_tree_view_convert_widget_to_bin_window_coords ((GtkTreeView *) list, x, y, & x, & y);
886
887 /* bound the mouse cursor within the bin window to get the nearest row */
888 GdkWindow * bin = gtk_tree_view_get_bin_window ((GtkTreeView *) list);
889 x = aud::clamp (x, 0, gdk_window_get_width (bin) - 1);
890 y = aud::clamp (y, 0, gdk_window_get_height (bin) - 1);
891
892 GtkTreePath * path = nullptr;
893 gtk_tree_view_get_path_at_pos ((GtkTreeView *) list, x, y, & path, nullptr, nullptr, nullptr);
894
895 if (! path)
896 return model->rows;
897
898 int row = gtk_tree_path_get_indices (path)[0];
899 g_return_val_if_fail (row >= 0 && row < model->rows, -1);
900
901 GdkRectangle rect;
902 gtk_tree_view_get_background_area ((GtkTreeView *) list, path, nullptr, & rect);
903 if (y > rect.y + rect.height / 2)
904 row ++;
905
906 gtk_tree_path_free (path);
907 return row;
908 }
909