1/* 2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> 3 * Copyright (C) 2010 Alberto Aldegheri <albyrock87+dev@gmail.com> 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 * Authored by Alberto Aldegheri <albyrock87+dev@gmail.com> 19 * 20 */ 21 22namespace Synapse.Gui 23{ 24 public class MatchViewRenderer : MatchListView.MatchViewRendererBase 25 { 26 // the size of Match and Action icons 27 public int icon_size { get; set; default = 32; } 28 // top and bottom row's padding 29 public int cell_vpadding { get; set; default = 2; } 30 // left and right padding on each component of the row 31 public int cell_hpadding { get; set; default = 3; } 32 //hilight matched text into selected match's title 33 public bool hilight_on_selected { get; set; default = false; } 34 //shows the pattern after the title if hilight doesn't match the title 35 public bool show_pattern_in_hilight { get; set; default = false; } 36 //shows extended info when present (ie "xx minutes ago") 37 public bool show_extended_info { get; set; default = true; } 38 //hides extended info on selected row if present 39 public bool hide_extended_on_selected { get; set; default = false; } 40 //overlay action icon to the text, or reserve space for action icon shrinking labels 41 public bool overlay_action { get; set; default = false; } 42 //the string pattern to use in the hilight 43 public new string pattern { get; set; default = ""; } 44 //the Action match to use to retrive the action icon to show 45 public Match action { get; set; default = null; } 46 //the markup of the title 47 public string title_markup { get; set; default = "<span size=\"medium\"><b>%s</b></span>"; } 48 //the markup of the description 49 public string description_markup { get; set; default = "<span size=\"small\">%s</span>"; } 50 //the markup of the extended info **extend info is already inserted into description markup** 51 public string extended_info_markup { get; set; default = "%s"; } 52 53 private int text_height = 1; 54 55 construct 56 { 57 this.get_layout ().set_ellipsize (Pango.EllipsizeMode.END); 58 59 this.notify["icon-size"].connect (size_changed); 60 this.notify["cell-vpadding"].connect (size_changed); 61 this.notify["cell-hpadding"].connect (size_changed); 62 this.notify["title-markup"].connect (size_changed); 63 this.notify["description-markup"].connect (size_changed); 64 } 65 66 public override void render_match (Cairo.Context ctx, Match m, int width, int height, bool use_base = false, double selected_pct = 1.0) 67 { 68 /* _____ ____________________________ _____ 69 | | | | | | 70 | | |____________________________| | | 71 |_____| |____|__________________|____| |_____| 72 */ 73 ctx.set_operator (Cairo.Operator.OVER); 74 //rtl = Gtk.TextDirection.RTL; // <-- uncomment to test RTL 75 bool has_action = false; 76 Gtk.StateFlags state = Gtk.StateFlags.NORMAL; 77 if (selected_pct > 1.0) 78 { 79 state = Gtk.StateFlags.SELECTED; 80 selected_pct = selected_pct - 1.0; 81 } 82 if (state == Gtk.StateFlags.SELECTED && action != null) has_action = true; 83 84 int x = 0, y = 0; 85 int text_width = width - cell_hpadding * 4 - icon_size; 86 if (has_action && !overlay_action) text_width = text_width - cell_hpadding * 2 - icon_size; 87 88 if (rtl != Gtk.TextDirection.RTL) 89 { 90 /* Match Icon */ 91 x = cell_hpadding; 92 y = (height - icon_size) / 2; 93 draw_icon (ctx, m, x, y); 94 95 /* Title and description */ 96 x += icon_size + cell_hpadding * 2; 97 y = (height - text_height) / 2; 98 draw_text (ctx, m, x, y, text_width, state, use_base, selected_pct); 99 100 /* Action Icon */ 101 if (has_action) 102 { 103 y = (height - icon_size) / 2; 104 draw_action (ctx, width - cell_hpadding - icon_size, y, selected_pct); 105 } 106 } 107 else 108 { 109 /* Match Icon */ 110 x = width - cell_hpadding - icon_size; 111 y = (height - icon_size) / 2; 112 draw_icon (ctx, m, x, y); 113 114 /* Title and description */ 115 x = x - cell_hpadding * 2 - text_width; 116 y = (height - text_height) / 2; 117 draw_text (ctx, m, x, y, text_width, state, use_base, selected_pct); 118 119 /* Action Icon */ 120 if (has_action) 121 { 122 y = (height - icon_size) / 2; 123 draw_action (ctx, cell_hpadding, y, selected_pct); 124 } 125 } 126 } 127 protected override int calculate_row_height () 128 { 129 string s = "%s\n%s".printf (title_markup, description_markup); 130 Markup.printf_escaped (s, " 北", " 北"); 131 unowned Pango.Layout layout = this.get_layout (); 132 layout.set_markup (s, -1); 133 int width = 0, height = 0; 134 layout.get_pixel_size (out width, out height); 135 this.text_height = height; 136 height = int.max (this.icon_size, height) + cell_vpadding * 2; 137 return height; 138 } 139 140 private void size_changed () 141 { 142 calculate_row_height (); 143 this.queue_resize (); 144 } 145 146 private void draw_icon (Cairo.Context ctx, Match m, int x, int y) 147 { 148 ctx.save (); 149 ctx.translate (x, y); 150 draw_icon_in_position (ctx, m.icon_name, icon_size); 151 ctx.restore (); 152 } 153 private void draw_action (Cairo.Context ctx, int x, int y, double selected_fill_pct) 154 { 155 if (selected_fill_pct < 0.9) return; 156 if (selected_fill_pct < 1.0) selected_fill_pct /= 3.0; 157 ctx.save (); 158 ctx.translate (x, y); 159 draw_icon_in_position (ctx, action.icon_name, icon_size, selected_fill_pct); 160 ctx.restore (); 161 } 162 163 private void draw_text (Cairo.Context ctx, Match m, int x, int y, int width, Gtk.StateFlags state, bool use_base, double selected_fill_pct) 164 { 165 ctx.save (); 166 ctx.translate (x, y); 167 ctx.rectangle (0, 0, width, text_height); 168 ctx.clip (); 169 170 bool selected = (state == Gtk.StateFlags.SELECTED); 171 172 var styletype = StyleType.FG; 173 if (use_base || selected) styletype = StyleType.TEXT; 174 175 if (selected && selected_fill_pct < 1.0) 176 { 177 double r = 0, g = 0, b = 0; 178 ch.get_rgb_from_mix (styletype, Gtk.StateFlags.NORMAL, Mod.NORMAL, 179 styletype, Gtk.StateFlags.SELECTED, Mod.NORMAL, 180 selected_fill_pct, out r, out g, out b); 181 ctx.set_source_rgba (r, g, b, 1.0); 182 } 183 else 184 { 185 ch.set_source_rgba (ctx, 1.0, styletype, state, Mod.NORMAL); 186 } 187 188 string s = ""; 189 /* ----------------------- draw title --------------------- */ 190 if (hilight_on_selected && selected && selected_fill_pct == 1.0) 191 { 192 s = title_markup.printf (Utils.markup_string_with_search (m.title, pattern, "", show_pattern_in_hilight)); 193 } 194 else 195 { 196 s = Markup.printf_escaped (title_markup, m.title); 197 } 198 unowned Pango.Layout layout = this.get_layout (); 199 layout.set_markup (s, -1); 200 layout.set_ellipsize (Pango.EllipsizeMode.END); 201 layout.set_width (Pango.SCALE * width); 202 Pango.cairo_show_layout (ctx, layout); 203 204 bool has_extended_info = show_extended_info && (m is ExtendedInfo); 205 if (hide_extended_on_selected && selected) has_extended_info = false; 206 int width_for_description = width - cell_hpadding; 207 208 /* ----------------- draw extended info ------------------- */ 209 if (has_extended_info) 210 { 211 ctx.save (); 212 s = Markup.printf_escaped (extended_info_markup, ((ExtendedInfo) m).extended_info ?? ""); 213 s = description_markup.printf (s); 214 layout.set_markup (s, -1); 215 layout.set_width (Pango.SCALE * width_for_description); 216 int w = 0, h = 0; 217 layout.get_pixel_size (out w, out h); 218 w += _cell_hpadding * 2; 219 220 width_for_description -= w; 221 222 if (rtl == Gtk.TextDirection.RTL) 223 ctx.translate (- width + w, text_height - h); 224 else 225 ctx.translate (width - w, text_height - h); 226 Pango.cairo_show_layout (ctx, layout); 227 ctx.restore (); 228 } 229 230 /* ------------------ draw description --------------------- */ 231 s = Markup.printf_escaped (description_markup, Utils.get_printable_description (m)); 232 233 layout.set_markup (s, -1); 234 layout.set_width (Pango.SCALE * width_for_description); 235 int w = 0, h = 0; 236 layout.get_pixel_size (out w, out h); 237 238 if (rtl == Gtk.TextDirection.RTL) 239 ctx.translate (width - width_for_description, text_height - h); 240 else 241 ctx.translate (0, text_height - h); 242 243 Pango.cairo_show_layout (ctx, layout); 244 ctx.restore (); 245 } 246 } 247 248 public class MatchListView : Gtk.EventBox 249 { 250 /* Animation stuffs */ 251 private uint tid; 252 private const int ANIM_TIMEOUT = 1000 / 25; 253 private const int ANIM_STEPS = 180 / ANIM_TIMEOUT; 254 public bool animation_enabled { 255 get; set; default = true; 256 } 257 258 private int offset; //current offset 259 private int toffset; //target offset 260 261 private int soffset; //current selection offset 262 private int tsoffset; //target selection offset 263 264 private int astep; // animation step for offset 265 private int sstep; // animation step for selection 266 267 private int row_height; //fixed row height (usually icon_size + 4) 268 269 /* _________________ -> 0 coord for offset 270 | | 271 ___|________________|___ -> offset -> 0 coord for soffset 272 | | 273 |#######################| => selection -> soffset 274 | | => visible area 275 |_______________________| 276 | | 277 | | 278 | | 279 |________________| 280 */ 281 282 private Gee.List<Match> items; 283 private int goto_index; 284 private int select_index; 285 286 public void set_indexes (int targetted, int selected) 287 { 288 bool b = _select(selected); 289 b = _goto(targetted) || b; 290 if (b) this.update_target_offsets (); 291 } 292 293 public int selected_index { 294 get { 295 return this.select_index; 296 } 297 } 298 299 public int targetted_index { 300 get { 301 return this.goto_index; 302 } 303 } 304 305 public bool selection_enabled 306 { 307 get; set; default = true; 308 } 309 310 public bool use_base_colors 311 { 312 get; set; default = true; 313 } 314 315 public int min_visible_rows 316 { 317 get; set; default = 5; 318 } 319 320 private bool inhibit_move; 321 322 public enum Behavior 323 { 324 TOP, 325 CENTER, 326 BOTTOM, 327 TOP_FORCED, 328 CENTER_FORCED, 329 BOTTOM_FORCED 330 } 331 public Behavior behavior 332 { 333 get; 334 set; 335 default = Behavior.CENTER; 336 } 337 338 public class MatchViewRendererBase : Gtk.Label 339 { 340 /* Methods to override here */ 341 342 /* render_match: 343 use_base : if true use gtk.BASE/TEXT, else use gtk.BG/FG 344 selected_pct : [1.0 - 2.0] -> 345 if > 1.0 then 346 selected = true 347 pct = selected_pct - 1.0 : how much is the selected row near the target position 348 */ 349 public virtual void render_match (Cairo.Context ctx, Match m, int width, int height, bool use_base = false, double selected_pct = 1.0) 350 { 351 ctx.translate (2, 2); 352 this.draw_icon_in_position (ctx, m.icon_name, 32); 353 } 354 protected virtual int calculate_row_height () 355 { 356 return 36; 357 } 358 /* End Methods to override - do not edit below */ 359 360 protected Utils.ColorHelper ch; 361 362 protected Gtk.TextDirection rtl; 363 364 protected void draw_icon_in_position (Cairo.Context ctx, string? name, int pixel_size, double with_alpha = 1.0) 365 { 366 ctx.rectangle (0, 0, pixel_size, pixel_size); 367 ctx.clip (); 368 if (name == null || name == "") name = "unknown"; 369 370 var icon_pixbuf = IconCacheService.get_default ().get_icon (name, pixel_size); 371 if (icon_pixbuf == null) return; 372 373 Gdk.cairo_set_source_pixbuf (ctx, icon_pixbuf, 0, 0); 374 if (with_alpha == 1.0) 375 ctx.paint (); 376 else 377 ctx.paint_with_alpha (with_alpha); 378 } 379 380 construct 381 { 382 rtl = Gtk.TextDirection.LTR; 383 ch = Utils.ColorHelper.get_default (); 384 385 style_updated.connect (this.on_style_updated); 386 on_style_updated (); 387 } 388 389 private int row_height_cached = 36; 390 public int get_row_height_request () 391 { 392 return this.row_height_cached; 393 } 394 395 public void on_style_updated () 396 { 397 // calculate here the new row height 398 this.rtl = this.get_direction (); 399 unowned Pango.Layout layout = this.get_layout (); 400 Utils.update_layout_rtl (layout, rtl); 401 layout.set_ellipsize (Pango.EllipsizeMode.END); 402 this.row_height_cached = calculate_row_height (); 403 this.queue_resize (); // queue_resize, so MatchListView will query for new row_height_request 404 } 405 406 public override bool draw (Cairo.Context ctx) 407 { 408 //Transparent. 409 return true; 410 } 411 412 } 413 414 protected MatchViewRendererBase renderer; 415 private Utils.ColorHelper ch; 416 417 public MatchViewRendererBase get_renderer () 418 { 419 return renderer; 420 } 421 422 public MatchListView (MatchViewRendererBase mr) 423 { 424 ch = Utils.ColorHelper.get_default (); 425 inhibit_move = false; 426 renderer = mr; 427 // Add the renderer to screen as a child, this way it will receive all events. 428 mr.show (); 429 mr.set_parent (this); 430 // Add our window to get mouse events 431 this.above_child = false; 432 this.visible_window = false; 433 this.set_has_window (false); 434 this.set_events (Gdk.EventMask.BUTTON_PRESS_MASK | 435 Gdk.EventMask.SCROLL_MASK); 436 437 // D&D 438 Gtk.drag_source_set (this, Gdk.ModifierType.BUTTON1_MASK, {}, 439 Gdk.DragAction.ASK | 440 Gdk.DragAction.COPY | 441 Gdk.DragAction.MOVE | 442 Gdk.DragAction.LINK); 443 444 //this.above_child = true; 445 this.visible_window = false; 446 this.offset = this.toffset = 0; 447 this.soffset = this.tsoffset = 0; 448 this.astep = this.sstep = 1; 449 this.goto_index = 0; 450 this.select_index = -1; 451 this.row_height = renderer.get_row_height_request (); 452 this.tid = 0; 453 454 this.items = null; 455 456 this.size_allocate.connect (this.update_target_offsets); 457 this.notify["behavior"].connect (this.update_target_offsets); 458 this.notify["min-visible-rows"].connect (this.queue_resize); 459 this.notify["animation-enabled"].connect (() => { 460 this.update_current_offsets (); 461 }); 462 } 463 464 public override void forall_internal (bool b, Gtk.Callback callback) 465 { 466 if (b) callback (this.renderer); 467 } 468 469 public override void size_allocate (Gtk.Allocation allocation) 470 { 471 base.size_allocate (allocation); 472 renderer.size_allocate ({ 0, 0, 0, 0 }); 473 } 474 public override void get_preferred_height (out int min_height, out int nat_height) 475 { 476 base.get_preferred_height (out min_height, out nat_height); 477 int tmp = this.renderer.get_row_height_request (); 478 if (tmp != this.row_height) 479 { 480 this.row_height = tmp; 481 this.update_target_offsets (); 482 this.queue_draw (); 483 } 484 min_height = nat_height = this.row_height * this.min_visible_rows; 485 } 486 public override void get_preferred_width (out int min_width, out int nat_width) 487 { 488 base.get_preferred_width (out min_width, out nat_width); 489 min_width = nat_width = 1; 490 } 491 492 private bool _select (int i) 493 { 494 if (i == this.select_index || 495 this.items == null || 496 i < -1 || 497 i >= this.items.size) return false; 498 this.select_index = i; 499 return true; 500 } 501 502 private bool _goto (int i) 503 { 504 if (i == this.goto_index || 505 this.items == null || 506 i < 0 || 507 i >= this.items.size) return false; 508 this.goto_index = i; 509 return true; 510 } 511 512 private bool update_current_offsets () 513 { 514 if (! (animation_enabled && this.get_realized ()) ) 515 { 516 this.tid = 0; 517 this.offset = this.toffset; 518 this.soffset = this.tsoffset; 519 this.queue_draw (); 520 return false; 521 } 522 if (inhibit_move) return false; 523 bool needs_animation = false; 524 /* Offset animation */ 525 if (this.offset != this.toffset) 526 { 527 needs_animation = true; 528 int diff = (int)Math.fabs (this.offset - this.toffset); 529 if (this.astep > 0) 530 { 531 if (diff < this.astep) 532 { 533 this.offset = this.toffset; 534 } 535 else 536 { 537 this.offset += this.offset < this.toffset ? this.astep : - this.astep; 538 } 539 } 540 else 541 { 542 diff = int.max (1, diff >> 2); 543 this.offset += this.toffset > this.offset ? diff : - diff; 544 } 545 } 546 /* Selection animation */ 547 if (this.soffset != this.tsoffset && this.selection_enabled) 548 { 549 needs_animation = true; 550 int diff = (int)Math.fabs (this.soffset - this.tsoffset); 551 if (diff < this.sstep) 552 { 553 this.soffset = this.tsoffset; 554 } 555 else 556 { 557 this.soffset += this.soffset < this.tsoffset ? this.sstep : - this.sstep; 558 } 559 } 560 if (needs_animation) 561 { 562 if (tid == 0) tid = Timeout.add (ANIM_TIMEOUT, this.update_current_offsets); 563 this.queue_draw (); 564 return true; 565 } 566 567 tid = 0; 568 return false; 569 } 570 571 private void update_target_offsets () 572 { 573 Gtk.Allocation allocation; 574 get_allocation (out allocation); 575 576 int visible_items = allocation.height / this.row_height; 577 578 switch (this.behavior) 579 { 580 case Behavior.TOP_FORCED: 581 // Item has to stay on top 582 this.toffset = this.row_height * this.goto_index; 583 break; 584 default: 585 case Behavior.CENTER: 586 if (this.goto_index <= (visible_items / 2) || this.items.size <= visible_items) 587 { 588 this.toffset = 0; 589 } 590 else if (this.goto_index >= ( this.items.size - 1 - (visible_items / 2) )) 591 { 592 this.toffset = this.row_height * this.items.size - allocation.height; 593 } 594 else 595 { 596 this.toffset = this.row_height * this.goto_index - allocation.height / 2 + this.row_height / 2; 597 } 598 break; 599 } 600 // update also selection 601 this.tsoffset = this.select_index * this.row_height - this.toffset; 602 int diff = (int) Math.fabs (this.toffset - this.offset); 603 if (diff < this.row_height * 3) 604 this.astep = int.max (1, diff / ANIM_STEPS ); 605 else // use special animation if the diff is too much 606 this.astep = -1; 607 this.sstep = int.max (1, (int) (Math.fabs (this.tsoffset - this.soffset) / ANIM_STEPS )); 608 609 update_current_offsets (); 610 } 611 612 public virtual void set_list (Gee.List<Match>? list, int targetted_index = 0, int selected_index = -1) 613 { 614 this.items = list; 615 this.select_index = selected_index; 616 this.goto_index = targetted_index; 617 int maxoffset = list == null ? 0 : this.row_height * list.size; 618 if (this.offset > maxoffset) this.offset = maxoffset; 619 inhibit_move = false; 620 this.update_target_offsets (); 621 this.queue_draw (); 622 } 623 624 public int get_list_size () 625 { 626 return this.items == null ? 0 : this.items.size; 627 } 628 629 public override bool draw (Cairo.Context ctx) 630 { 631 Gtk.Allocation allocation; 632 get_allocation (out allocation); 633 634 /* Clip */ 635 ctx.rectangle (0, 0, allocation.width, allocation.height); 636 ctx.clip (); 637 ctx.set_operator (Cairo.Operator.OVER); 638 639 if (this.use_base_colors) 640 { 641 ch.set_source_rgba (ctx, 1.0, StyleType.BASE, Gtk.StateFlags.NORMAL, Mod.NORMAL); 642 ctx.paint (); 643 } 644 645 if (this.items == null || this.items.size == 0) return true; 646 647 ctx.set_font_options (this.get_screen().get_font_options()); 648 649 int visible_items = allocation.height / this.row_height + 2; 650 int i = get_item_at_pos (0); 651 visible_items += i; 652 653 int ypos = 0; 654 655 if (this.select_index >= 0 && this.selection_enabled) 656 { 657 if (this.soffset > (-this.row_height) && this.soffset < allocation.height) 658 { 659 ypos = int.max (this.soffset, 0); 660 unowned Gtk.StyleContext context = get_style_context (); 661 context.save (); 662 context.add_class("view"); 663 context.set_state (Gtk.StateFlags.SELECTED); 664 context.render_background (ctx, 0, ypos, 665 allocation.width, this.row_height); 666 context.render_frame (ctx, 0, ypos, 667 allocation.width, this.row_height); 668 context.restore (); 669 } 670 } 671 double pct = 1.0; 672 for (; i < visible_items && i < this.items.size; ++i) 673 { 674 ypos = i * this.row_height - this.offset; 675 if (ypos > allocation.height) break; 676 ctx.save (); 677 ctx.translate (0, ypos); 678 ctx.rectangle (0, 0, allocation.width, this.row_height); 679 ctx.clip (); 680 pct = 1.0; 681 if (this.selection_enabled && i == select_index) 682 { 683 // set pct as 1.0 + [0.0 - 1.0] where second operator is the "near-factor" 684 pct = (Math.fabs (this.toffset - this.offset) / this.row_height); 685 if (pct == 0.0) pct = (Math.fabs (this.tsoffset - this.soffset) / this.row_height); 686 pct = 2.0 - double.min (1.0 , pct); 687 } 688 renderer.render_match (ctx, this.items.get (i), allocation.width, this.row_height, this.use_base_colors, pct); 689 ctx.restore (); 690 } 691 692 return true; 693 } 694 695 private int get_item_at_pos (int y) 696 { 697 return (this.offset + y) / this.row_height; 698 } 699 700 public override bool scroll_event (Gdk.EventScroll event) 701 { 702 if (this.items == null) return true; 703 inhibit_move = false; 704 int k = 1; 705 if (event.direction == Gdk.ScrollDirection.UP) k = this.goto_index == 0 ? 0 : -1; 706 707 this.set_indexes (this.goto_index + k, this.goto_index + k); 708 this.selected_index_changed (this.select_index); 709 return true; 710 } 711 712 // Fired when user changes selection interacting with the list 713 public signal void selected_index_changed (int new_index); 714 public signal void fire_item (); 715 public signal void fire_item_context_switch (); 716 717 private int dragdrop_target_item = 0; 718 private string dragdrop_name = ""; 719 private string dragdrop_uri = ""; 720 public override bool button_press_event (Gdk.EventButton event) 721 { 722 if (this.tid != 0) return true; 723 this.dragdrop_target_item = get_item_at_pos ((int)event.y); 724 var tl = new Gtk.TargetList ({}); 725 726 if (this.items == null || this.items.size <= this.dragdrop_target_item) 727 { 728 dragdrop_name = ""; 729 dragdrop_uri = ""; 730 Gtk.drag_source_set_target_list (this, tl); 731 Gtk.drag_source_set_icon_stock (this, Gtk.Stock.MISSING_IMAGE); 732 return true; 733 } 734 735 if (this.selection_enabled) 736 { 737 if ((event.type == Gdk.EventType.BUTTON_PRESS || event.type == Gdk.EventType.2BUTTON_PRESS) 738 && this.select_index == this.dragdrop_target_item) 739 { 740 this.set_indexes (this.dragdrop_target_item, this.dragdrop_target_item); 741 if (event.type == Gdk.EventType.2BUTTON_PRESS) 742 { 743 this.fire_item (); 744 } 745 else if (event.type == Gdk.EventType.BUTTON_PRESS && event.button == 3) 746 { 747 this.fire_item_context_switch (); 748 } 749 return true; //Fire item! So we don't need to drag things! 750 } 751 else 752 { 753 this.inhibit_move = true; 754 this.set_indexes (this.dragdrop_target_item, this.dragdrop_target_item); 755 this.selected_index_changed (this.select_index); 756 Timeout.add (Gtk.Settings.get_default ().gtk_double_click_time ,() => { 757 if (inhibit_move) 758 { 759 inhibit_move = false; 760 update_current_offsets (); 761 } 762 return false; 763 }); 764 } 765 } 766 767 UriMatch? um = items.get (this.dragdrop_target_item) as UriMatch; 768 if (um == null) 769 { 770 Gtk.drag_source_set_target_list (this, tl); 771 Gtk.drag_source_set_icon_stock (this, Gtk.Stock.MISSING_IMAGE); 772 dragdrop_name = ""; 773 dragdrop_uri = ""; 774 return true; 775 } 776 777 tl.add_text_targets (0); 778 tl.add_uri_targets (1); 779 dragdrop_name = um.title; 780 dragdrop_uri = um.uri; 781 Gtk.drag_source_set_target_list (this, tl); 782 783 try { 784 var icon = GLib.Icon.new_for_string (um.icon_name); 785 if (icon == null) return true; 786 787 Gtk.IconInfo iconinfo = Gtk.IconTheme.get_default ().lookup_by_gicon (icon, 48, Gtk.IconLookupFlags.FORCE_SIZE); 788 if (iconinfo == null) return true; 789 790 Gdk.Pixbuf icon_pixbuf = iconinfo.load_icon (); 791 if (icon_pixbuf == null) return true; 792 793 Gtk.drag_source_set_icon_pixbuf (this, icon_pixbuf); 794 } 795 catch (GLib.Error err) {} 796 return true; 797 } 798 799 public override void drag_data_get (Gdk.DragContext context, Gtk.SelectionData selection_data, uint info, uint time_) 800 { 801 /* Called at drop time */ 802 selection_data.set_text (dragdrop_name, -1); 803 selection_data.set_uris ({dragdrop_uri}); 804 } 805 } 806 807 public class ResultBox : Gtk.EventBox 808 { 809 private const int VISIBLE_RESULTS = 5; 810 private const int ICON_SIZE = 36; 811 private int mwidth; 812 private int nrows; 813 814 private Gtk.Box vbox; 815 private Gtk.Box status_box; 816 817 private Utils.ColorHelper ch; 818 819 public bool use_base_colors 820 { 821 get; set; default = true; 822 } 823 824 public bool show_no_results 825 { 826 get; set; default = true; 827 } 828 829 public ResultBox (int width, int nrows = 5) 830 { 831 this.mwidth = width; 832 this.nrows = nrows; 833 this.set_has_window (false); 834 this.above_child = false; 835 this.visible_window = false; 836 ch = Utils.ColorHelper.get_default (); 837 build_ui(); 838 this.notify["use-base-colors"].connect (() => { 839 view.use_base_colors = use_base_colors; 840 if (use_base_colors) 841 { 842 set_state (Gtk.StateFlags.SELECTED); 843 } 844 else 845 { 846 set_state (Gtk.StateFlags.NORMAL); 847 } 848 this.queue_draw (); 849 }); 850 } 851 852 private MatchListView view; 853 private MatchViewRenderer rend; 854 private Gtk.Label status; 855 856 public new void set_state (Gtk.StateFlags state) 857 { 858 base.set_state_flags (state, false); 859 status.set_state_flags (Gtk.StateFlags.NORMAL, true); 860 } 861 862 public override bool draw (Cairo.Context ctx) 863 { 864 if (_use_base_colors) 865 { 866 Gtk.Allocation allocation, status_allocation; 867 this.get_allocation (out allocation); 868 status.get_allocation (out status_allocation); 869 870 /* Clip */ 871 ctx.rectangle (0, 0, allocation.width, allocation.height); 872 ctx.clip (); 873 874 ctx.set_operator (Cairo.Operator.OVER); 875 /* Prepare bg's colors using GtkStyleContext */ 876 Cairo.Pattern pat = new Cairo.Pattern.linear(0, 0, 0, status.get_allocated_height ()); 877 878 Gtk.StateFlags t = this.get_state_flags (); 879 ch.add_color_stop_rgba (pat, 0.0, 0.95, StyleType.BG, t); 880 ch.add_color_stop_rgba (pat, 1.0, 0.95, StyleType.BG, t, Mod.DARKER); 881 /* Prepare and draw top bg's rect */ 882 ctx.set_source (pat); 883 ctx.paint (); 884 } 885 /* Propagate Draw */ 886 this.propagate_draw (this.get_child (), ctx); 887 888 return true; 889 } 890 891 public MatchListView get_match_list_view () 892 { 893 return this.view; 894 } 895 896 public override void get_preferred_width (out int min_width, out int nat_width) 897 { 898 vbox.get_preferred_width (out min_width, out nat_width); 899 min_width = nat_width = int.max (min_width, this.mwidth); 900 } 901 902 public override void get_preferred_height (out int min_height, out int nat_height) 903 { 904 vbox.get_preferred_height (out min_height, out nat_height); 905 } 906 907 private void build_ui() 908 { 909 rend = new MatchViewRenderer (); 910 view = new MatchListView (rend); 911 view.min_visible_rows = this.nrows; 912 913 vbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); 914 vbox.border_width = 0; 915 this.add (vbox); 916 vbox.pack_start (view); 917 status_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); 918 status_box.set_size_request (-1, 15); 919 vbox.pack_start (status_box, false); 920 status = new Gtk.Label (null); 921 status.set_alignment (0, 0); 922 status.set_markup (Markup.printf_escaped ("<b>%s</b>", _("No results."))); 923 status_box.pack_start (status, false, false, 10); 924 status_box.pack_start (new Gtk.Label (null), true, false); 925 } 926 927 public void update_matches (Gee.List<Synapse.Match>? rs) 928 { 929 view.set_list (rs); 930 if (rs==null || rs.size == 0) 931 { 932 status.set_markup (Markup.printf_escaped ("<b>%s</b>", _("No results."))); 933 status.visible = _show_no_results; 934 } 935 else 936 { 937 status.set_markup (Markup.printf_escaped (_("<b>1 of %d</b>"), view.get_list_size ())); 938 status.visible = true; 939 } 940 } 941 942 public void move_selection_to_index (int i) 943 { 944 view.set_indexes (i, i); 945 status.set_markup (Markup.printf_escaped (_("<b>%d of %d</b>"), i + 1, view.get_list_size ())); 946 } 947 } 948 949 public class SpecificMatchList : MatchListView 950 { 951 protected IController controller; 952 protected Model model; 953 protected SearchingFor sf; 954 955 private class LabelMatch : Match 956 { 957 public LabelMatch (string title, string description, string icon_name) 958 { 959 GLib.Object (title: title, 960 description: description, 961 icon_name: icon_name, 962 has_thumbnail: false); 963 } 964 } 965 966 private static bool lists_initialized = false; 967 private static Gee.List<Match>? tts = null; 968 private static Gee.List<Match>? nores = null; 969 private static Gee.List<Match>? noact = null; 970 971 972 private bool has_results = false; 973 private MatchViewRenderer rend; 974 public MatchViewRenderer get_match_renderer () { return rend; } 975 976 public SpecificMatchList (IController controller, Model model, SearchingFor sf) 977 { 978 base (new MatchViewRenderer ()); 979 this.rend = renderer as MatchViewRenderer; 980 this.rend.hilight_on_selected = true; 981 this.sf = sf; 982 this.controller = controller; 983 this.model = model; 984 if (!lists_initialized) 985 { 986 lists_initialized = true; 987 988 tts = new Gee.ArrayList<Match>(); 989 nores = new Gee.ArrayList<Match>(); 990 noact = new Gee.ArrayList<Match>(); 991 Match m = null; 992 m = new LabelMatch (IController.NO_RESULTS, 993 "", 994 "missing-image"); 995 nores.add (m); 996 m = new LabelMatch (IController.NO_RECENT_ACTIVITIES, 997 "", 998 "missing-image"); 999 noact.add (m); 1000 m = new LabelMatch (IController.TYPE_TO_SEARCH, 1001 IController.DOWN_TO_SEE_RECENT, 1002 "search"); 1003 tts.add (m); 1004 this.controller.handle_recent_activities.connect ((b) => { 1005 m.description = IController.DOWN_TO_SEE_RECENT; 1006 queue_draw (); 1007 }); 1008 } 1009 } 1010 1011 public void update_searching_for () 1012 { 1013 if (controller.is_in_initial_state ()) 1014 { 1015 this.selection_enabled = false; 1016 this.min_visible_rows = sf == SearchingFor.SOURCES ? 1 : 0; 1017 } 1018 else if (model.searching_for == sf) 1019 { 1020 this.min_visible_rows = 5; 1021 this.selection_enabled = has_results; 1022 } 1023 else 1024 { 1025 this.selection_enabled = false; 1026 this.min_visible_rows = sf != SearchingFor.TARGETS ? 1 : has_results ? 1 : 0; 1027 } 1028 } 1029 1030 public override void set_list (Gee.List<Match>? list, int targetted_index = 0, int selected_index = -1) 1031 { 1032 if (list == null || list.size == 0) 1033 { 1034 has_results = false; 1035 switch (this.sf) 1036 { 1037 case SearchingFor.SOURCES: 1038 if (controller.is_in_initial_state ()) base.set_list (tts); 1039 else if (controller.searched_for_recent ()) base.set_list (noact); 1040 else base.set_list (nores); 1041 break; 1042 case SearchingFor.ACTIONS: 1043 if (model.focus[SearchingFor.SOURCES].value != null) 1044 base.set_list (nores); 1045 else 1046 base.set_list (null); 1047 break; 1048 default: //TARGETS 1049 if (!model.needs_target ()) base.set_list (null); 1050 else if (controller.searched_for_recent ()) base.set_list (noact); 1051 else base.set_list (nores); 1052 break; 1053 } 1054 this.rend.pattern = ""; 1055 } 1056 else 1057 { 1058 has_results = true; 1059 base.set_list (list, targetted_index, selected_index); 1060 this.rend.pattern = model.query[sf]; 1061 } 1062 update_searching_for (); 1063 } 1064 } 1065} 1066