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, " &#21271;", " &#21271;");
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