using Gtk; class DragListBox : ListBox { private ListBoxRow? hover_row; private ListBoxRow? drag_row; private bool top = false; private int hover_top; private int hover_bottom; private bool should_scroll = false; private bool scrolling = false; private bool scroll_up; private const int SCROLL_STEP_SIZE = 8; private const int SCROLL_DISTANCE = 30; private const int SCROLL_DELAY = 50; public Adjustment? vadjustment { public set { _vadjustment = value; if (_vadjustment == null) { should_scroll = false; } } public get { return _vadjustment; } } private Adjustment? _vadjustment; const TargetEntry[] entries = { { "GTK_LIST_BOX_ROW", Gtk.TargetFlags.SAME_APP, 0} }; public DragListBox () { drag_dest_set (this, Gtk.DestDefaults.ALL, entries, Gdk.DragAction.MOVE); } void row_drag_begin (Widget widget, Gdk.DragContext context) { ListBoxRow row; Allocation alloc; Cairo.Surface surface; Cairo.Context cr; int x, y; DragListBox parent; row = (ListBoxRow) widget.get_ancestor (typeof (ListBoxRow)); row.get_allocation (out alloc); surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, alloc.width, alloc.height); cr = new Cairo.Context (surface); parent = row.get_parent () as DragListBox; if (parent != null) parent.drag_row = row; row.get_style_context ().add_class ("drag-icon"); row.draw (cr); row.get_style_context ().remove_class ("drag-icon"); widget.translate_coordinates (row, 0, 0, out x, out y); surface.set_device_offset (-x, -y); drag_set_icon_surface (context, surface); } public override bool drag_motion ( Gdk.DragContext context, int x, int y, uint time_ ) { if (y > hover_top || y < hover_bottom) { Allocation alloc; var row = get_row_at_y (y); bool old_top = top; row.get_allocation (out alloc); int hover_row_y = alloc.y; int hover_row_height = alloc.height; if (row != drag_row) { if (y < hover_row_y + hover_row_height/2) { hover_top = hover_row_y; hover_bottom = hover_top + hover_row_height/2; row.get_style_context ().add_class ("drag-hover-top"); row.get_style_context ().remove_class ("drag-hover-bottom"); top = true; } else { hover_top = hover_row_y + hover_row_height/2; hover_bottom = hover_row_y + hover_row_height; row.get_style_context ().add_class ("drag-hover-bottom"); row.get_style_context ().remove_class ("drag-hover-top"); top = false; } } if (hover_row != null && hover_row != row) { if (old_top) hover_row.get_style_context ().remove_class ("drag-hover-top"); else hover_row.get_style_context ().remove_class ("drag-hover-bottom"); } hover_row = row; } check_scroll (y); if(should_scroll && !scrolling) { scrolling = true; Timeout.add (SCROLL_DELAY, scroll); } return true; } public override void drag_leave (Gdk.DragContext context, uint time_) { should_scroll = false; } void check_scroll (int y) { if (vadjustment == null) { return; } double vadjustment_min = vadjustment.value; double vadjustment_max = vadjustment.page_size + vadjustment_min; double show_min = double.max(0, y - SCROLL_DISTANCE); double show_max = double.min(vadjustment.upper, y + SCROLL_DISTANCE); if(vadjustment_min > show_min) { should_scroll = true; scroll_up = true; } else if (vadjustment_max < show_max){ should_scroll = true; scroll_up = false; } else { should_scroll = false; } } bool scroll () { if (should_scroll) { if(scroll_up) { vadjustment.value -= SCROLL_STEP_SIZE; } else { vadjustment.value += SCROLL_STEP_SIZE; } } else { scrolling = false; } return should_scroll; } void row_drag_data_get ( Widget widget, Gdk.DragContext context, SelectionData selection_data, uint info, uint time_ ) { uchar[] data = new uchar[(sizeof (Widget))]; ((Widget[])data)[0] = widget; selection_data.set ( Gdk.Atom.intern_static_string ("GTK_LIST_BOX_ROW"), 32, data ); } public override void drag_data_received ( Gdk.DragContext context, int x, int y, SelectionData selection_data, uint info, uint time_ ) { Widget handle; ListBoxRow row; int index = 0; if (hover_row != null) { if (top) { index = hover_row.get_index () - 1; hover_row.get_style_context ().remove_class ("drag-hover-top"); } else { index = hover_row.get_index (); hover_row.get_style_context ().remove_class ("drag-hover-bottom"); } handle = ((Widget[])selection_data.get_data ())[0]; row = (ListBoxRow) handle.get_ancestor (typeof (ListBoxRow)); if (row != hover_row) { row.get_parent ().remove (row); insert (row, index); } } drag_row = null; } public ListBoxRow create_row (string text) { ListBoxRow row; EventBox handle; Box box; Label label; Image image; row = new ListBoxRow (); box = new Box (Orientation.HORIZONTAL, 10); box.margin_start = 10; box.margin_end = 10; row.add (box); handle = new EventBox (); image = new Image.from_icon_name ("view-list-symbolic", IconSize.MENU); handle.add (image); box.add (handle); label = new Gtk.Label (text); box.pack_end (label, true); drag_source_set ( handle, Gdk.ModifierType.BUTTON1_MASK, entries, Gdk.DragAction.MOVE ); handle.drag_begin.connect (row_drag_begin); handle.drag_data_get.connect (row_drag_data_get); return row; } } const string css = ".drag-icon { " + " background: white; " + " border: 1px solid black; " + "}" + ".drag-hover-top {" + " background: linear-gradient(to bottom, rgba(0,0,0,0.65) 0%,rgba(0,0,0,0) 35%); " + "}" + ".drag-hover-bottom {" + " background: linear-gradient(to bottom, rgba(0,0,0,0) 65%,rgba(0,0,0,0.65) 100%); " + "}"; int main (string[] args) { Window window; DragListBox list; ScrolledWindow sw; ListBoxRow row; string text; CssProvider provider; Gtk.init (ref args); provider = new CssProvider (); try { provider.load_from_data (css); } catch (Error e) { warning (e.message); } Gtk.StyleContext.add_provider_for_screen ( Gdk.Screen.get_default (), provider, STYLE_PROVIDER_PRIORITY_APPLICATION ); window = new Window (); window.set_default_size (-1, 300); window.destroy.connect (Gtk.main_quit); sw = new ScrolledWindow (null, null); sw.hexpand = true; sw.set_policy (PolicyType.NEVER, PolicyType.ALWAYS); window.add (sw); list = new DragListBox (); list.set_selection_mode (SelectionMode.NONE); sw.add (list); for (int i = 0; i < 20; i++) { text = "Row %d".printf (i); row = list.create_row (text); list.insert (row, -1); } list.vadjustment = sw.vadjustment; window.show_all (); Gtk.main (); return 0; }