1/*
2* Copyright (c) 2017-2020 Alecaddd (https://alecaddd.com)
3*
4* This program is free software; you can redistribute it and/or
5* modify it under the terms of the GNU 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 program is distributed in the hope that it will be useful,
10* but WITHOUT ANY WARRANTY; without even the implied warranty of
11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12* General Public License for more details.
13*
14* You should have received a copy of the GNU General Public
15* License along with this program; if not, write to the
16* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17* Boston, MA 02110-1301 USA
18*
19* Authored by: Alessandro "Alecaddd" Castellani <castellani.ale@gmail.com>
20*/
21
22public class Sequeler.Partials.LibraryItem : Gtk.ListBoxRow {
23    public Gee.HashMap<string, string> data { get; set; }
24    public Gtk.Label title;
25    public Gdk.RGBA color;
26
27    public Gtk.Revealer main_revealer;
28    private Gtk.Revealer motion_revealer;
29    public Gtk.ModelButton connect_button;
30    public Gtk.Spinner spinner;
31
32    public Gtk.ScrolledWindow scrolled { get; set; }
33    private bool scroll_up = false;
34    private bool scrolling = false;
35    private bool should_scroll = false;
36    public Gtk.Adjustment vadjustment;
37
38    private const int SCROLL_STEP_SIZE = 5;
39    private const int SCROLL_DISTANCE = 30;
40    private const int SCROLL_DELAY = 50;
41
42    public signal void edit_dialog (Gee.HashMap data);
43    public signal void duplicate_connection (Gee.HashMap data);
44    public signal void confirm_delete (
45        Gtk.ListBoxRow item,
46        Gee.HashMap data
47    );
48    public signal void connect_to (
49        Gee.HashMap data,
50        Gtk.Spinner spinner,
51        Gtk.ModelButton button
52    );
53
54    // Datatype restrictions on DnD (Gtk.TargetFlags).
55    const Gtk.TargetEntry[] TARGET_ENTRIES_LABEL = {
56        { "LIBRARYITEM", Gtk.TargetFlags.SAME_APP, 0 }
57    };
58
59    public LibraryItem (Gee.HashMap<string, string> data) {
60        Object (
61            data: data
62        );
63
64        get_style_context ().add_class ("library-box");
65        expand = true;
66
67        var box = new Gtk.Grid ();
68        box.get_style_context ().add_class ("library-inner-box");
69        box.margin = 3;
70
71        var color_box = new Gtk.Grid ();
72        color_box.get_style_context ().add_class ("library-colorbox");
73        color_box.set_size_request (12, 12);
74        color_box.margin = 9;
75
76        color = Gdk.RGBA ();
77        color.parse (data["color"]);
78        try {
79            var style = new Gtk.CssProvider ();
80            style.load_from_data (
81                "* {background-color: %s;}".printf (color.to_string ()),
82                -1
83            );
84            color_box.get_style_context ().add_provider (
85                style,
86                Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
87            );
88        } catch (Error e) {
89            debug (
90                "Internal error loading session chooser style: %s",
91                e.message
92            );
93        }
94
95        title = new Gtk.Label (data["title"]);
96        title.get_style_context ().add_class ("text-bold");
97        title.halign = Gtk.Align.START;
98        title.ellipsize = Pango.EllipsizeMode.END;
99        title.margin_end = 9;
100        title.set_line_wrap (true);
101        title.hexpand = true;
102
103        box.attach (color_box, 0, 0, 1, 1);
104        box.attach (title, 1, 0, 1, 1);
105
106        connect_button = new Gtk.ModelButton ();
107        connect_button.text = _("Connect");
108
109        var edit_button = new Gtk.ModelButton ();
110        edit_button.text = _("Edit Connection");
111
112        var duplicate_button = new Gtk.ModelButton ();
113        duplicate_button.text = _("Duplicate Connection");
114
115        var delete_button = new Gtk.ModelButton ();
116        delete_button.text = _("Delete Connection");
117
118        var open_menu = new Gtk.MenuButton ();
119        open_menu.set_image (
120            new Gtk.Image.from_icon_name (
121                "view-more-symbolic",
122                Gtk.IconSize.SMALL_TOOLBAR
123            )
124        );
125        open_menu.get_style_context ().add_class ("library-btn");
126        open_menu.tooltip_text = _("Options");
127
128        var menu_separator = new Gtk.Separator (Gtk.Orientation.HORIZONTAL);
129        menu_separator.margin_top = 6;
130        menu_separator.margin_bottom = 6;
131
132        var menu_grid = new Gtk.Grid ();
133        menu_grid.expand = true;
134        menu_grid.margin_top = 3;
135        menu_grid.margin_bottom = 3;
136        menu_grid.orientation = Gtk.Orientation.VERTICAL;
137
138        menu_grid.attach (connect_button, 0, 1, 1, 1);
139        menu_grid.attach (edit_button, 0, 2, 1, 1);
140        menu_grid.attach (duplicate_button, 0, 3, 1, 1);
141        menu_grid.attach (menu_separator, 0, 4, 1, 1);
142        menu_grid.attach (delete_button, 0, 5, 1, 1);
143        menu_grid.show_all ();
144
145        var menu_popover = new Gtk.Popover (null);
146        menu_popover.add (menu_grid);
147
148        open_menu.popover = menu_popover;
149        open_menu.relief = Gtk.ReliefStyle.NONE;
150        open_menu.valign = Gtk.Align.CENTER;
151
152        spinner = new Gtk.Spinner ();
153
154        box.attach (spinner, 2, 0, 1, 1);
155        box.attach (open_menu, 3, 0, 1, 1);
156
157        var motion_grid = new Gtk.Grid ();
158        motion_grid.margin = 6;
159        motion_grid.get_style_context ().add_class ("grid-motion");
160        motion_grid.height_request = 18;
161
162        motion_revealer = new Gtk.Revealer ();
163        motion_revealer.transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN;
164        motion_revealer.add (motion_grid);
165
166        box.attach (motion_revealer, 0, 1, 4, 1);
167
168        var event_box = new Gtk.EventBox ();
169        event_box.add (box);
170
171        main_revealer = new Gtk.Revealer ();
172        main_revealer.reveal_child = true;
173        main_revealer.transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN;
174        main_revealer.add (event_box);
175
176        add (main_revealer);
177
178        delete_button.clicked.connect (() => {
179            confirm_delete (this, data);
180        });
181
182        edit_button.clicked.connect (() => {
183            edit_dialog (data);
184        });
185
186        duplicate_button.clicked.connect (() => {
187            duplicate_connection (data);
188        });
189
190        connect_button.clicked.connect (() => {
191            spinner.start ();
192            connect_button.sensitive = false;
193            connect_to (data, spinner, connect_button);
194        });
195
196        event_box.enter_notify_event.connect (event => {
197            box.set_state_flags (Gtk.StateFlags.PRELIGHT, true);
198            return false;
199        });
200
201        event_box.leave_notify_event.connect (event => {
202            if (event.detail != Gdk.NotifyType.INFERIOR) {
203                box.set_state_flags (Gtk.StateFlags.NORMAL, true);
204            }
205            return false;
206        });
207
208        open_menu.clicked.connect (event => {
209            box.set_state_flags (Gtk.StateFlags.PRELIGHT, true);
210        });
211
212        menu_popover.closed.connect (event => {
213            box.set_state_flags (Gtk.StateFlags.NORMAL, true);
214        });
215
216        build_drag_and_drop ();
217    }
218
219    private void build_drag_and_drop () {
220        // Make this a draggable widget
221        Gtk.drag_source_set (
222            this,
223            Gdk.ModifierType.BUTTON1_MASK,
224            TARGET_ENTRIES_LABEL,
225            Gdk.DragAction.MOVE
226        );
227
228        drag_begin.connect (on_drag_begin);
229        drag_data_get.connect (on_drag_data_get);
230
231        // Make this widget a DnD destination.
232        Gtk.drag_dest_set (
233            this,
234            Gtk.DestDefaults.MOTION,
235            TARGET_ENTRIES_LABEL,
236            Gdk.DragAction.MOVE
237        );
238
239        drag_motion.connect (on_drag_motion);
240        drag_leave.connect (on_drag_leave);
241        drag_end.connect (clear_indicator);
242    }
243
244    private void on_drag_begin (Gtk.Widget widget, Gdk.DragContext context) {
245        var row = (Partials.LibraryItem) widget;
246
247        Gtk.Allocation alloc;
248        row.get_allocation (out alloc);
249
250        var surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, alloc.width, alloc.height);
251        var cr = new Cairo.Context (surface);
252        cr.set_source_rgba (0, 0, 0, 0.3);
253        cr.set_line_width (1);
254
255        cr.move_to (0, 0);
256        cr.line_to (alloc.width, 0);
257        cr.line_to (alloc.width, alloc.height);
258        cr.line_to (0, alloc.height);
259        cr.line_to (0, 0);
260        cr.stroke ();
261
262        cr.set_source_rgba (255, 255, 255, 0.5);
263        cr.rectangle (0, 0, alloc.width, alloc.height);
264        cr.fill ();
265
266        row.draw (cr);
267        Gtk.drag_set_icon_surface (context, surface);
268        main_revealer.reveal_child = false;
269    }
270
271    private void on_drag_data_get (Gtk.Widget widget, Gdk.DragContext context,
272        Gtk.SelectionData selection_data, uint target_type, uint time) {
273        uchar[] data = new uchar[(sizeof (Partials.LibraryItem))];
274        ((Gtk.Widget[])data)[0] = widget;
275
276        selection_data.set (
277            Gdk.Atom.intern_static_string ("LIBRARYITEM"), 32, data
278        );
279    }
280
281    public void clear_indicator (Gdk.DragContext context) {
282        main_revealer.reveal_child = true;
283    }
284
285    public bool on_drag_motion (Gdk.DragContext context, int x, int y, uint time) {
286        motion_revealer.reveal_child = true;
287
288        int index = get_index ();
289        Gtk.Allocation alloc;
290        get_allocation (out alloc);
291
292        int real_y = (index * alloc.height) - alloc.height + y;
293        check_scroll (real_y);
294
295        if (should_scroll && !scrolling) {
296            scrolling = true;
297            Timeout.add (SCROLL_DELAY, scroll);
298        }
299
300        return true;
301    }
302
303    private void check_scroll (int y) {
304        vadjustment = scrolled.vadjustment;
305
306        if (vadjustment == null) {
307            return;
308        }
309
310        double vadjustment_min = vadjustment.value;
311        double vadjustment_max = vadjustment.page_size + vadjustment_min;
312        double show_min = double.max (0, y - SCROLL_DISTANCE);
313        double show_max = double.min (vadjustment.upper, y + SCROLL_DISTANCE);
314
315        if (vadjustment_min > show_min) {
316            should_scroll = true;
317            scroll_up = true;
318        } else if (vadjustment_max < show_max) {
319            should_scroll = true;
320            scroll_up = false;
321        } else {
322            should_scroll = false;
323        }
324    }
325
326    private bool scroll () {
327        if (should_scroll) {
328            if (scroll_up) {
329                vadjustment.value -= SCROLL_STEP_SIZE;
330            } else {
331                vadjustment.value += SCROLL_STEP_SIZE;
332            }
333        } else {
334            scrolling = false;
335        }
336
337        return should_scroll;
338    }
339
340    public void on_drag_leave (Gdk.DragContext context, uint time) {
341        motion_revealer.reveal_child = false;
342        should_scroll = false;
343    }
344}
345