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