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