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 errordomain WidgetError
25  {
26    ICON_NOT_FOUND,
27    UNKNOWN
28  }
29
30  public class UIWidgetsConfig : ConfigObject
31  {
32    public bool animation_enabled { get; set; default = true; }
33    public bool extended_info_enabled { get; set; default = true; }
34  }
35
36  public class CloneWidget : Gtk.Widget
37  {
38    private Gtk.Widget clone;
39    public CloneWidget (Gtk.Widget to_clone)
40    {
41      this.clone = to_clone;
42      this.set_has_window (false);
43    }
44    public override void get_preferred_width (out int min_width, out int nat_width)
45    {
46      clone.get_preferred_width (out min_width, out nat_width);
47    }
48    public override void get_preferred_height (out int min_height, out int nat_height)
49    {
50      clone.get_preferred_height (out min_height, out nat_height);
51    }
52  }
53
54  public class SmartLabel : Gtk.Misc
55  {
56    public const string[] size_to_string = {
57      "xx-small",
58      "x-small",
59      "small",
60      "medium",
61      "large",
62      "x-large",
63      "xx-large"
64    };
65
66    public static Size string_to_size (string sizename)
67    {
68      Size s = Size.MEDIUM;
69      for (uint i = 0; i < size_to_string.length; i++)
70      {
71        if (size_to_string[i] == sizename) return (Size)i;
72      }
73      return s;
74    }
75
76    protected const double[] size_to_scale = {
77      Pango.Scale.XX_SMALL,
78      Pango.Scale.X_SMALL,
79      Pango.Scale.SMALL,
80      Pango.Scale.MEDIUM,
81      Pango.Scale.LARGE,
82      Pango.Scale.X_LARGE,
83      Pango.Scale.XX_LARGE
84    };
85
86    public enum Size
87    {
88      XX_SMALL,
89      X_SMALL,
90      SMALL,
91      MEDIUM,
92      LARGE,
93      X_LARGE,
94      XX_LARGE
95    }
96
97    public bool natural_requisition {
98      get; set; default = false;
99    }
100
101    public Size size {
102      get; set; default = Size.MEDIUM;
103    }
104
105    public Size min_size {
106      get; set; default = Size.MEDIUM;
107    }
108
109    private string text = "";
110
111    private Size real_size = Size.MEDIUM;
112    private Gtk.Requisition last_req;
113    private Pango.Layout layout;
114    private Utils.ColorHelper ch;
115    private Pango.EllipsizeMode ellipsize = Pango.EllipsizeMode.NONE;
116
117    private uint tid = 0;
118    private const int INITIAL_TIMEOUT = 1750;
119    private const int SPACING = 50;
120    private int offset = 0;
121    private bool animate = false;
122
123    construct
124    {
125      layout = this.create_pango_layout ("");
126      ch = Utils.ColorHelper.get_default ();
127      last_req = {};
128      this.set_has_window (false);
129      this.notify["size"].connect (sizes_changed);
130      this.notify["min-size"].connect (sizes_changed);
131      this.xalign = 0.0f;
132      this.yalign = 1.0f;
133
134      //do not remove this, it's important to create the first scale attr
135      this.set_text ("");
136    }
137
138    private void sizes_changed ()
139    {
140      if (min_size > size) this._min_size = this._size;
141      this.real_size = size;
142      queue_resize ();
143    }
144
145    public void set_animation_enabled (bool b)
146    {
147      this.animate = b;
148
149      if (b)
150      {
151        this.ellipsize = Pango.EllipsizeMode.NONE;
152        sizes_changed ();
153      }
154      else
155      {
156        if (tid != 0) stop_animation ();
157        sizes_changed ();
158      }
159    }
160
161    public void set_text (string s)
162    {
163      string m = Markup.escape_text (s);
164      if (m == text) return;
165      text = m;
166      text_updated ();
167    }
168
169    public void set_markup (string m)
170    {
171      if (m == text) return;
172      text = m;
173      text_updated ();
174    }
175
176    private void stop_animation ()
177    {
178      Source.remove (tid);
179      tid = 0;
180      offset = 0;
181    }
182
183    int _anim_width = 0;
184    private void start_animation ()
185    {
186      if (tid != 0) return;
187
188      tid = Timeout.add (40, () => {
189        offset = (offset - 1) % (_anim_width);
190        queue_draw ();
191        return true;
192      });
193    }
194
195    public void set_ellipsize (Pango.EllipsizeMode mode)
196    {
197      if (animate) this.ellipsize = Pango.EllipsizeMode.NONE;
198      else this.ellipsize = mode;
199    }
200
201    private void text_updated ()
202    {
203      real_size = _size;
204      queue_resize ();
205      if (tid != 0) stop_animation ();
206    }
207
208    public override void size_allocate (Gtk.Allocation allocation)
209    {
210      base.size_allocate (allocation);
211
212      /* size_allocate is called after size_request */
213      /* so last_req is filled with the standard requisition */
214
215      if (allocation.width >= last_req.width || ((!animate) && real_size == _min_size))
216      {
217        /* That's good, we have enough space for default size */
218        if (tid != 0) stop_animation ();
219        return;
220      }
221      /* Mh, bad, let's start shrinking */
222      Gtk.Requisition req;
223
224      var attrs = layout.get_attributes ();
225      var iter = attrs.get_iterator (); //the first iterator is a scale
226      unowned Pango.Attribute? attr = iter.get (Pango.AttrType.SCALE);
227      unowned Pango.AttrFloat a = (Pango.AttrFloat) attr;
228
229      bool needs_animation = true;
230      while (real_size > _min_size)
231      {
232        real_size = real_size - 1;
233        a.value = size_to_scale[real_size];
234        layout.context_changed ();
235        requisition_for_size (out req, null, real_size, true);
236
237        if (allocation.width >= req.width)
238        {
239          needs_animation = false;
240          break;
241        }
242      }
243
244      if (animate && needs_animation)
245      {
246        if (tid == 0)
247        {
248          tid = Timeout.add (INITIAL_TIMEOUT, () => {
249            tid = 0;
250            start_animation ();
251            return false;
252          });
253        }
254      }
255      else
256      {
257        if (tid != 0) stop_animation ();
258      }
259    }
260
261    public override bool draw (Cairo.Context ctx)
262    {
263      Gtk.Allocation allocation;
264      get_allocation (out allocation);
265
266      int w = allocation.width - this.xpad * 2;
267      int h = allocation.height - this.ypad * 2;
268      ctx.translate (this.xpad, this.ypad);
269      ctx.rectangle (0, 0, w, h);
270      ctx.clip ();
271
272      bool rtl = this.get_direction () == Gtk.TextDirection.RTL;
273
274      int x, y, width, height;
275
276      if (animate && tid != 0)
277      {
278        ctx.translate (offset, 0);
279      }
280      else
281      {
282        if (ellipsize != Pango.EllipsizeMode.NONE)
283        {
284          layout.set_width (w * Pango.SCALE);
285          layout.set_ellipsize (ellipsize);
286        }
287      }
288
289      Gui.Utils.get_draw_position (out x, out y, out width, out height, layout, rtl,
290                                   w, h, this.xalign, this.yalign);
291      ctx.translate (x, y);
292
293      ctx.set_operator (Cairo.Operator.OVER);
294      ch.set_source_rgba (ctx, 1.0, StyleType.FG, this.get_state_flags ());
295
296      Pango.cairo_show_layout (ctx, layout);
297
298      width += SPACING;
299      _anim_width = width;
300      int rotate = (offset + width);
301      if (rtl) rotate = offset;
302      if (animate && tid != 0 && rotate < w)
303      {
304        ctx.translate (width, 0);
305        Pango.cairo_show_layout (ctx, layout);
306      }
307
308      return true;
309    }
310
311    protected void requisition_for_size (out Gtk.Requisition req, out int char_width, Size s, bool return_only_width = false)
312    {
313      req = { this.xpad * 2, this.ypad * 2 };
314
315      Pango.Rectangle logical_rect;
316      layout.set_width (-1);
317      layout.set_ellipsize (Pango.EllipsizeMode.NONE);
318      layout.get_extents (null, out logical_rect);
319
320      req.width += logical_rect.width / Pango.SCALE;
321      if (return_only_width)
322      {
323        char_width = 0;
324        return;
325      }
326
327      Pango.Context ctx = layout.get_context ();
328      Pango.FontDescription fdesc = new Pango.FontDescription ();
329      fdesc.merge_static (this.style.font_desc, true);
330
331      fdesc.set_size ((int)(size_to_scale[s] * (double)fdesc.get_size()));
332      var metrics = ctx.get_metrics (fdesc, ctx.get_language ());
333
334      req.height += (metrics.get_ascent () + metrics.get_descent ()) / Pango.SCALE;
335      char_width = int.max (metrics.get_approximate_char_width (), metrics.get_approximate_digit_width ()) / Pango.SCALE;
336    }
337
338    public void size_request (out Gtk.Requisition req)
339    {
340      layout.set_markup ("<span size=\"%s\">%s</span>".printf (size_to_string[_size], this.text), -1);
341      int char_width;
342      this.requisition_for_size (out req, out char_width, this._size);
343      last_req.width = req.width;
344      last_req.height = req.height;
345      if (!this.natural_requisition && (this.ellipsize != Pango.EllipsizeMode.NONE || animate))
346        req.width = char_width * 3;
347    }
348
349    public override void get_preferred_width (out int min_width, out int nat_width)
350    {
351      Gtk.Requisition req;
352      this.size_request (out req);
353      min_width = nat_width = req.width;
354    }
355
356    public override void get_preferred_height (out int min_height, out int nat_height)
357    {
358      Gtk.Requisition req;
359      this.size_request (out req);
360      min_height = nat_height = req.height;
361    }
362  }
363
364  public class SchemaContainer : Gtk.Container
365  {
366    public class Schema : GLib.Object
367    {
368      private Gtk.Allocation[] _positions = {};
369      public Gtk.Allocation[] positions { get { return _positions; } }
370      public Schema ()
371      {
372
373      }
374      public void add_allocation (Gtk.Allocation alloc)
375      {
376        alloc.x = int.max (0, alloc.x);
377        alloc.y = int.max (0, alloc.y);
378        alloc.width = int.max (0, alloc.width);
379        alloc.height = int.max (0, alloc.height);
380
381        this._positions += alloc;
382      }
383    }
384
385    protected Gee.List<Schema> schemas;
386    protected Gee.List<Gtk.Widget> children;
387    private int active_schema = 0;
388    private int[] render_order = null;
389
390    public int xpad { get; set; default = 0; }
391    public int ypad { get; set; default = 0; }
392
393    public int scale_size {
394      get; set; default = 128;
395    }
396
397    public bool fixed_height {
398      get; set; default = false;
399    }
400
401    public bool fixed_width {
402      get; set; default = false;
403    }
404
405    public SchemaContainer (int scale_size)
406    {
407      this.scale_size = scale_size;
408      schemas = new Gee.ArrayList<Schema> ();
409      children = new Gee.ArrayList<Gtk.Widget> ();
410      set_has_window (false);
411      set_redraw_on_allocate (false);
412      this.notify["scale-size"].connect (this.queue_resize);
413      this.notify["fixed-width"].connect (this.queue_resize);
414      this.notify["fixed-height"].connect (this.queue_resize);
415      this.notify["xpad"].connect (this.queue_resize);
416      this.notify["ypad"].connect (this.queue_resize);
417    }
418
419    public void set_render_order (int[]? order)
420    {
421      this.render_order = order;
422      this.queue_draw ();
423    }
424
425    public new void set_size_request (int width, int height)
426    {
427      if (width <= 0) width = 1;
428      if (height <= 0) height = 1;
429
430      base.set_size_request (width, height);
431    }
432
433    public void add_schema (Schema s)
434    {
435      schemas.add (s);
436      if (schemas.size == 1) select_schema (0);
437    }
438
439    public void select_schema (int i)
440    {
441      if (i < 0) return;
442      if (i >= schemas.size) return;
443      active_schema = i;
444      if (!this.get_realized ()) return;
445      Gtk.Allocation alloc;
446      this.get_allocation (out alloc);
447      size_allocate ({
448        alloc.x, alloc.y,
449        alloc.width, alloc.height
450      });
451      queue_resize ();
452    }
453
454
455
456    public override void forall_internal (bool b, Gtk.Callback callback)
457    {
458      if (render_order == null)
459      {
460        foreach (var child in children)
461        {
462          callback (child);
463        }
464      }
465      else
466      {
467        for (int i = 0; i < render_order.length; i++)
468          callback (children.get (render_order[i]));
469      }
470    }
471
472    public override void add (Gtk.Widget widget)
473    {
474      this.children.add (widget);
475      widget.set_parent (this);
476    }
477
478    public override void remove (Gtk.Widget widget)
479    {
480      // cannot remove for now :P TODO
481    }
482
483    public void size_request (out Gtk.Requisition req)
484    {
485      req = {};
486      int i = 0;
487      Gtk.Allocation[] alloc = schemas.get (active_schema).positions;
488
489      foreach (Gtk.Widget child in children)
490      {
491        if (alloc.length > i)
492        {
493          var ca = alloc[i];
494          req = {int.max ((ca.x + ca.width) * _scale_size / 100, req.width),
495            int.max ((ca.y + ca.height) * _scale_size / 100, req.height)};
496          child.visible = true;
497        }
498        else
499        {
500          child.visible = false;
501        }
502        i++;
503      }
504      if (this._fixed_height) req.height = this._scale_size;
505      if (this._fixed_width) req.width = this._scale_size;
506      req.width += _xpad * 2;
507      req.height += _ypad * 2;
508    }
509
510    public override void get_preferred_width (out int min_width, out int nat_width)
511    {
512      Gtk.Requisition req;
513      this.size_request (out req);
514      min_width = nat_width = req.width;
515    }
516
517    public override void get_preferred_height (out int min_height, out int nat_height)
518    {
519      Gtk.Requisition req;
520      this.size_request (out req);
521      min_height = nat_height = req.height;
522    }
523
524    public override void size_allocate (Gtk.Allocation allocation)
525    {
526      base.size_allocate (allocation);
527
528      if (schemas.size <= 0)
529      {
530        foreach (Gtk.Widget child in children)
531          child.hide ();
532        return;
533      }
534      Gtk.Allocation[] alloc = schemas.get (active_schema).positions;
535
536      int i = 0;
537      int x = allocation.x + _xpad;
538      int y = allocation.y + _ypad;
539      bool rtl = this.get_direction () == Gtk.TextDirection.RTL;
540      foreach (Gtk.Widget child in children)
541      {
542        Gtk.Allocation a;
543        if (alloc.length <= i)
544        {
545          a = {};
546        }
547        else
548        {
549          var ca = alloc[i];
550          a = {x + ca.x * this._scale_size / 100, y + ca.y * this._scale_size / 100,
551            ca.width * this._scale_size / 100, ca.height * this._scale_size / 100};
552        }
553        if (rtl) a.x = 2 * allocation.x + allocation.width - a.x - a.width;
554        child.size_allocate (a);
555        i++;
556      }
557    }
558  }
559
560  public class SelectionContainer : Gtk.Container
561  {
562    protected Gee.List<Gtk.Widget> children;
563    private int active_child = 0;
564
565    public SelectionContainer ()
566    {
567      children = new Gee.ArrayList<Gtk.Widget> ();
568      set_has_window (false);
569      set_redraw_on_allocate (false);
570    }
571
572    public void select_child (int i)
573    {
574      if (i < 0) return;
575      if (i >= children.size) return;
576      active_child = i;
577      i = 0;
578      foreach (var child in children)
579      {
580        if (i == active_child)
581        {
582          if (!child.visible) child.show ();
583        }
584        else
585        {
586          if (child.visible) child.hide ();
587        }
588
589        i++;
590      }
591      if (!this.get_realized ()) return;
592      queue_resize ();
593    }
594
595    public override void forall_internal (bool b, Gtk.Callback callback)
596    {
597      foreach (var child in children)
598        callback (child);
599    }
600
601    public override void add (Gtk.Widget widget)
602    {
603      this.children.add (widget);
604      widget.set_parent (this);
605    }
606
607    public override void remove (Gtk.Widget widget)
608    {
609      this.children.remove (widget);
610      widget.unparent ();
611    }
612
613    public override void size_allocate (Gtk.Allocation allocation)
614    {
615      base.size_allocate (allocation);
616      if (active_child >= children.size) return;
617      children.get (active_child).size_allocate (allocation);
618    }
619
620    public override void get_preferred_width (out int min_width, out int nat_width)
621    {
622      if (active_child >= children.size) {
623        min_width = nat_width = 0;
624        return;
625      }
626      children.get (active_child).get_preferred_width (out min_width, out nat_width);
627    }
628
629    public override void get_preferred_height (out int min_height, out int nat_height)
630    {
631      if (active_child >= children.size) {
632        min_height = nat_height = 0;
633        return;
634      }
635      children.get (active_child).get_preferred_height (out min_height, out nat_height);
636    }
637  }
638
639  public class Throbber : Gtk.Spinner
640  {
641    construct
642    {
643      this.notify["active"].connect (this.queue_draw);
644    }
645
646    public override bool draw (Cairo.Context ctx)
647    {
648      if (this.active)
649      {
650        return base.draw (ctx);
651      }
652      return true;
653    }
654  }
655
656  public class SensitiveWidget : Gtk.EventBox
657  {
658    private Gtk.Widget _widget;
659    public Gtk.Widget widget { get { return this._widget; }}
660
661    public SensitiveWidget (Gtk.Widget widget)
662    {
663      this.above_child = false;
664      this.visible_window = false;
665      this.set_has_window (false);
666
667      this._widget = widget;
668      this.add (this._widget);
669      this._widget.show ();
670    }
671
672    public override bool draw (Cairo.Context ctx)
673    {
674      this.propagate_draw (this.get_child (), ctx);
675      return true;
676    }
677  }
678
679  public class NamedIcon : Gtk.Image
680  {
681    public string not_found_name { get; set; default = "unknown"; }
682    private string current;
683    private Gtk.IconSize current_size;
684
685    construct
686    {
687      current = "";
688      current_size = Gtk.IconSize.DIALOG;
689    }
690
691    public void size_request (out Gtk.Requisition req)
692    {
693      req = {
694        this.width_request,
695        this.height_request
696      };
697      if (req.width <= 0 && req.height <= 0)
698      {
699        req.width = xpad * 2;
700        req.height = ypad * 2;
701        if (pixel_size >= 0)
702        {
703          req.width += pixel_size;
704          req.height += pixel_size;
705        }
706      }
707    }
708
709    public override void get_preferred_width (out int min_width, out int nat_width)
710    {
711      Gtk.Requisition req;
712      this.size_request (out req);
713      min_width = nat_width = req.width;
714    }
715
716    public override void get_preferred_height (out int min_height, out int nat_height)
717    {
718      Gtk.Requisition req;
719      this.size_request (out req);
720      min_height = nat_height = req.height;
721    }
722
723    public override bool draw (Cairo.Context ctx)
724    {
725      if (current == null || current == "") return true;
726      ctx.set_operator (Cairo.Operator.OVER);
727
728      Gtk.Allocation allocation;
729      get_allocation (out allocation);
730
731      int w = allocation.width - this.xpad * 2;
732      int h = allocation.height - this.ypad * 2;
733      ctx.translate (this.xpad, this.ypad);
734      ctx.rectangle (0, 0, w, h);
735      ctx.clip ();
736
737      Gdk.Pixbuf icon_pixbuf = IconCacheService.get_default ().get_icon (
738            current, pixel_size <= 0 ? int.min (allocation.width, allocation.height) : pixel_size);
739
740      if (icon_pixbuf == null) return true;
741
742      Gdk.cairo_set_source_pixbuf (ctx, icon_pixbuf,
743          (w - icon_pixbuf.get_width ()) / 2,
744          (h - icon_pixbuf.get_height ()) / 2);
745      ctx.paint ();
746
747      return true;
748    }
749    public new void clear ()
750    {
751      current = "";
752      this.queue_draw ();
753    }
754    public void set_icon_name (string? name, Gtk.IconSize size = Gtk.IconSize.DND)
755    {
756      if (name == null)
757        name = "";
758      if (name == current && name != "")
759        return;
760      else
761      {
762        if (name == "")
763        {
764          name = not_found_name;
765        }
766        current = name;
767        current_size = size;
768        this.queue_draw ();
769      }
770    }
771  }
772
773  public class FakeInput : Gtk.Alignment
774  {
775    public bool draw_input { get; set; default = true; }
776    public double input_alpha { get; set; default = 1.0; }
777    public double border_radius { get; set; default = 3.0; }
778    public double shadow_height { get; set; default = 3; }
779    public double focus_height { get; set; default = 3; }
780
781    private Utils.ColorHelper ch;
782    public Gtk.Widget? focus_widget
783    {
784      get { return _focus_widget; }
785      set {
786        if (value == _focus_widget)
787          return;
788        this.queue_draw ();
789        if (_focus_widget != null)
790          _focus_widget.queue_draw ();
791        _focus_widget = value;
792        if (_focus_widget != null)
793          _focus_widget.queue_draw ();
794      }
795    }
796    private Gtk.Widget? _focus_widget;
797    construct
798    {
799      _focus_widget = null;
800      ch = Utils.ColorHelper.get_default ();
801      this.notify["draw-input"].connect (this.queue_draw);
802      this.notify["input-alpha"].connect (this.queue_draw);
803      this.notify["border-radius"].connect (this.queue_draw);
804      this.notify["shadow-pct"].connect (this.queue_draw);
805      this.notify["focus-height"].connect (this.queue_draw);
806    }
807
808    public override bool draw (Cairo.Context ctx)
809    {
810      if (draw_input)
811      {
812        Gtk.Allocation allocation;
813        get_allocation (out allocation);
814
815        ctx.save ();
816        ctx.translate (1.5, 1.5);
817        ctx.set_operator (Cairo.Operator.OVER);
818        ctx.set_line_width (1.25);
819
820        Gdk.cairo_rectangle (ctx, {0, 0, allocation.width, allocation.height});
821        ctx.clip ();
822        ctx.save ();
823
824        double x = this.left_padding,
825               y = this.top_padding,
826               w = allocation.width - this.left_padding - this.right_padding - 3.0,
827               h = allocation.height - this.top_padding - this.bottom_padding - 3.0;
828        Utils.cairo_rounded_rect (ctx, x, y, w, h, border_radius);
829        if (!ch.is_dark_color (StyleType.FG, Gtk.StateFlags.NORMAL))
830          ch.set_source_rgba (ctx, input_alpha, StyleType.BG, Gtk.StateFlags.NORMAL, Mod.DARKER);
831        else
832          ch.set_source_rgba (ctx, input_alpha, StyleType.FG, Gtk.StateFlags.NORMAL, Mod.INVERTED);
833        ctx.fill_preserve ();
834        var pat = new Cairo.Pattern.linear (0, y, 0, y + shadow_height);
835        ch.add_color_stop_rgba (pat, 0, 0.6 * input_alpha, StyleType.FG, Gtk.StateFlags.NORMAL);
836        ch.add_color_stop_rgba (pat, 0.3, 0.25 * input_alpha, StyleType.FG, Gtk.StateFlags.NORMAL);
837        ch.add_color_stop_rgba (pat, 1.0, 0, StyleType.FG, Gtk.StateFlags.NORMAL);
838        ctx.set_source (pat);
839        ctx.fill ();
840        if (_focus_widget != null)
841        {
842          /*
843                     ____            y1
844                  .-'    '-.
845               .-'          '-.
846            .-'                '-.
847           x1         x2         x3  y2
848          */
849          Gtk.Allocation focus_allocation;
850          _focus_widget.get_allocation (out focus_allocation);
851          double x1 = double.max (focus_allocation.x, x),
852                 x3 = double.min (focus_allocation.x + focus_allocation.width,
853                           x + w);
854          double x2 = (x1 + x3) / 2.0;
855          double y2 = y + h;
856          double y1 = y + h - focus_height;
857          ctx.new_path ();
858          ctx.move_to (x1, y2);
859          if (x1 < x + 1)
860          {
861            ctx.line_to (x1, y1);
862            ctx.line_to (x2, y1);
863          }
864          else
865          {
866            ctx.curve_to (x1, y2, x1, y1, x2, y1);
867          }
868          if (x3 > x + w - 1)
869          {
870            ctx.line_to (x3, y1);
871            ctx.line_to (x3, y2);
872          }
873          else
874          {
875            ctx.curve_to (x3, y1, x3, y2, x3, y2);
876          }
877          ctx.close_path ();
878          ctx.clip ();
879          pat = new Cairo.Pattern.linear (0, y2, 0, y1);
880          ch.add_color_stop_rgba (pat, 0, 1.0 * input_alpha, StyleType.BG, Gtk.StateFlags.SELECTED);
881          ch.add_color_stop_rgba (pat, 1, 0, StyleType.BG, Gtk.StateFlags.SELECTED);
882          ctx.set_source (pat);
883          ctx.paint ();
884        }
885        ctx.restore ();
886        Utils.cairo_rounded_rect (ctx, x, y, w, h, border_radius);
887        ch.set_source_rgba (ctx, 0.6 * input_alpha, StyleType.FG, Gtk.StateFlags.NORMAL);
888        ctx.stroke ();
889        ctx.restore ();
890      }
891      return base.draw (ctx);
892    }
893  }
894
895  public class MenuThrobber : MenuButton
896  {
897    public bool active { get; set; default = false; }
898    private float progress = 0.0f;
899    private uint timer_src_id = 0;
900
901    construct
902    {
903      this.notify["active"].connect (this.active_toggled);
904    }
905
906    private void active_toggled ()
907    {
908      if (active && timer_src_id == 0)
909      {
910        timer_src_id = Timeout.add (1000 / 30, () => {
911          progress += 1.0f / 30;
912          if (progress > 1.0f) progress = 0.0f;
913          queue_draw ();
914          return true;
915        });
916      }
917      else if (!active && timer_src_id != 0)
918      {
919        Source.remove (timer_src_id);
920        timer_src_id = 0;
921        progress = 0.0f;
922      }
923      queue_draw ();
924    }
925
926    public override void get_preferred_width (out int min_width, out int nat_width)
927    {
928      min_width = nat_width = 11;
929    }
930
931    public override void get_preferred_height (out int min_height, out int nat_height)
932    {
933      min_height = nat_height = 11;
934    }
935
936    public override void size_allocate (Gtk.Allocation allocation)
937    {
938      Gtk.Allocation alloc = {allocation.x, allocation.y, allocation.width, allocation.height};
939      set_allocation (alloc);
940    }
941
942    public override bool draw (Cairo.Context ctx)
943    {
944      base.draw (ctx);
945
946      if (!active) return true;
947
948      Gtk.Allocation allocation;
949      get_allocation (out allocation);
950
951      double r = 0.0, g = 0.0, b = 0.0;
952      double SIZE = 0.5;
953      double size = button_scale * int.min (allocation.width, allocation.height) - SIZE * 2;
954      size *= 0.5;
955      double xc = allocation.width - SIZE * 2 - size;
956      double yc = size;
957      double arc_start = Math.PI * 2 * progress;
958      double arc_end = arc_start + Math.PI / 2.0;
959
960      ch.get_rgb (out r, out g, out b, StyleType.FG, Gtk.StateFlags.NORMAL, Mod.LIGHTER);
961      ctx.set_source_rgb (r, g, b);
962      ctx.arc_negative (xc, yc, size * 0.5, arc_end, arc_start);
963      ctx.arc (xc, yc, size, arc_start, arc_end);
964      ctx.fill ();
965
966      return true;
967    }
968  }
969
970  public class FakeButton : Gtk.EventBox
971  {
972    construct
973    {
974      this.set_events (Gdk.EventMask.BUTTON_RELEASE_MASK |
975                       Gdk.EventMask.ENTER_NOTIFY_MASK |
976                       Gdk.EventMask.LEAVE_NOTIFY_MASK);
977      this.visible_window = false;
978    }
979    public override bool enter_notify_event (Gdk.EventCrossing event)
980    {
981      enter ();
982      return true;
983    }
984    public override bool leave_notify_event (Gdk.EventCrossing event)
985    {
986      leave ();
987      return true;
988    }
989    public override bool button_release_event (Gdk.EventButton event)
990    {
991      released ();
992      return true;
993    }
994    public virtual signal void leave () {}
995    public virtual signal void enter () {}
996    public virtual signal void released () {}
997  }
998
999  public class MenuButton : FakeButton
1000  {
1001    public double button_scale { get; set; default = 1.0; }
1002    private bool entered;
1003    protected Utils.ColorHelper ch;
1004    private Gtk.Menu menu;
1005    public MenuButton ()
1006    {
1007      ch = Utils.ColorHelper.get_default ();
1008      entered = false;
1009      menu = new Gtk.Menu ();
1010      Gtk.MenuItem item = null;
1011
1012      item = new Gtk.ImageMenuItem.from_stock (Gtk.Stock.PREFERENCES, null);
1013      item.activate.connect (() => {
1014        settings_clicked ();
1015      });
1016      menu.append (item);
1017
1018      item = new Gtk.ImageMenuItem.from_stock (Gtk.Stock.ABOUT, null);
1019      item.activate.connect (() => {
1020        var about = new SynapseAboutDialog ();
1021        about.run ();
1022        about.destroy ();
1023      });
1024      menu.append (item);
1025
1026      item = new Gtk.SeparatorMenuItem ();
1027      menu.append (item);
1028
1029      item = new Gtk.ImageMenuItem.from_stock (Gtk.Stock.QUIT, null);
1030      item.activate.connect (Gtk.main_quit);
1031      menu.append (item);
1032
1033      menu.show_all ();
1034    }
1035
1036    public unowned Gtk.Menu get_menu ()
1037    {
1038      return menu;
1039    }
1040
1041    public override void enter ()
1042    {
1043      entered = true;
1044      this.queue_draw ();
1045    }
1046    public override void leave ()
1047    {
1048      entered = false;
1049      this.queue_draw ();
1050    }
1051    public bool is_menu_visible ()
1052    {
1053      return menu.visible;
1054    }
1055    public override void released ()
1056    {
1057      menu.popup (null, null, null, 1, 0);
1058    }
1059    public signal void settings_clicked ();
1060    public override void size_allocate (Gtk.Allocation allocation)
1061    {
1062      Gtk.Allocation alloc = {allocation.x, allocation.y, allocation.width, allocation.height};
1063      set_allocation (alloc);
1064    }
1065
1066    public override void get_preferred_width (out int min_width, out int nat_width)
1067    {
1068      min_width = nat_width = 11;
1069    }
1070
1071    public override void get_preferred_height (out int min_height, out int nat_height)
1072    {
1073      min_height = nat_height = 11;
1074    }
1075
1076    public override bool draw (Cairo.Context ctx)
1077    {
1078      Gtk.Allocation allocation;
1079      get_allocation (out allocation);
1080
1081      double SIZE = 0.5;
1082      ctx.translate (SIZE, SIZE);
1083      ctx.set_operator (Cairo.Operator.OVER);
1084
1085      double r = 0.0, g = 0.0, b = 0.0;
1086      double size = button_scale * int.min (allocation.width, allocation.height) - SIZE * 2;
1087
1088      Cairo.Pattern pat = new Cairo.Pattern.linear (0, 0, 0, allocation.height);
1089      if (entered || (this.get_state_flags () & Gtk.StateFlags.SELECTED) != 0)
1090      {
1091        ch.get_rgb (out r, out g, out b, StyleType.BG, Gtk.StateFlags.SELECTED);
1092      }
1093      else
1094      {
1095        ch.get_rgb (out r, out g, out b, StyleType.BG, Gtk.StateFlags.NORMAL);
1096      }
1097      pat.add_color_stop_rgb (0.0,
1098                              double.max(r - 0.15, 0),
1099                              double.max(g - 0.15, 0),
1100                              double.max(b - 0.15, 0));
1101      if (entered)
1102      {
1103        ch.get_rgb (out r, out g, out b, StyleType.BG, Gtk.StateFlags.NORMAL);
1104      }
1105      pat.add_color_stop_rgb (1.0,
1106                              double.min(r + 0.15, 1),
1107                              double.min(g + 0.15, 1),
1108                              double.min(b + 0.15, 1));
1109
1110      size *= 0.5;
1111      double xc = allocation.width - SIZE * 2 - size;
1112      double yc = size;
1113      ctx.set_source (pat);
1114      ctx.arc (xc, yc, size, 0.0, Math.PI * 2);
1115      ctx.fill ();
1116
1117      if (entered)
1118      {
1119        ch.get_rgb (out r, out g, out b, StyleType.FG, Gtk.StateFlags.NORMAL);
1120      }
1121      else
1122      {
1123        ch.get_rgb (out r, out g, out b, StyleType.BG, Gtk.StateFlags.NORMAL);
1124      }
1125
1126      ctx.set_source_rgb (r, g, b);
1127      ctx.arc (xc, yc, size * 0.5, 0.0, Math.PI * 2);
1128      ctx.fill ();
1129
1130      return true;
1131    }
1132  }
1133
1134  public class SynapseAboutDialog : Gtk.AboutDialog
1135  {
1136    public SynapseAboutDialog ()
1137    {
1138      string[] devs = {"Michal Hruby <michal.mhr@gmail.com>",
1139                       "Alberto Aldegheri <albyrock87+dev@gmail.com>",
1140                       "Tom Beckmann <tom@elementaryos.org>",
1141                       "Rico Tzschichholz <ricotz@ubuntu.com>"};
1142      string[] artists = devs;
1143      artists += "Ian Cylkowski <designbyizo@gmail.com>";
1144      GLib.Object (artists : artists,
1145                   authors : devs,
1146                   copyright : "Copyright (C) 2010-2018 " + string.joinv ("\n", devs),
1147                   program_name: "Synapse",
1148                   logo_icon_name : "synapse",
1149                   version: Config.VERSION + "\n" + Config.RELEASE_NAME,
1150                   website: "http://launchpad.net/synapse-project");
1151    }
1152  }
1153
1154  public class HTextSelector : Gtk.EventBox
1155  {
1156    private const int ARROW_SIZE = 7;
1157    public string selected_markup { get; set; default = "<span size=\"medium\"><b>%s</b></span>"; }
1158    public string unselected_markup { get; set; default = "<span size=\"small\">%s</span>"; }
1159    public int padding { get; set; default = 18; }
1160    public bool show_arrows { get; set; default = true; }
1161    public bool animation_enabled { get; set; default = true; }
1162    private class PangoReadyText
1163    {
1164      public string text { get; set; default = ""; }
1165      public int offset { get; set; default = 0; }
1166      public int width { get; set; default = 0; }
1167      public int height { get; set; default = 0; }
1168    }
1169    private int _selected;
1170    public int selected { get { return _selected; } set {
1171      if (value == _selected ||
1172          value < 0 ||
1173          value >= texts.size)
1174        return;
1175      _selected = value;
1176      update_all_sizes ();
1177      update_cached_surface ();
1178      queue_draw ();
1179      if (!animation_enabled)
1180      {
1181        update_current_offset ();
1182        return;
1183      }
1184      if (tid == 0)
1185      {
1186        tid = Timeout.add (30, () => {
1187          return update_current_offset ();
1188        });
1189      }
1190    }}
1191    private Gee.List<PangoReadyText> texts;
1192    private Cairo.Surface cached_surface;
1193    private int wmax;
1194    private int hmax;
1195    private int current_offset;
1196    private Utils.ColorHelper ch;
1197    private Gtk.Label label;
1198
1199    public HTextSelector ()
1200    {
1201      this.above_child = false;
1202      this.set_has_window (false);
1203      this.visible_window = false;
1204      this.label = new Gtk.Label ("");
1205      this.label.show ();
1206      this.add (label);
1207      this.set_events (Gdk.EventMask.BUTTON_PRESS_MASK |
1208                       Gdk.EventMask.SCROLL_MASK);
1209      ch = Utils.ColorHelper.get_default ();
1210      cached_surface = null;
1211      tid = 0;
1212      wmax = hmax = current_offset = 0;
1213      texts = new Gee.ArrayList<PangoReadyText> ();
1214      this.label.style_updated.connect (() => {
1215        update_all_sizes ();
1216        update_cached_surface ();
1217        queue_resize ();
1218        queue_draw ();
1219      });
1220      this.size_allocate.connect (() => {
1221        if (tid == 0)
1222          tid = Timeout.add (30, () => {
1223            return update_current_offset ();
1224          });
1225      });
1226      this.notify["sensitive"].connect (() => {
1227        update_cached_surface ();
1228        queue_draw ();
1229      });
1230      this.realize.connect (this._global_update);
1231      this.notify["selected-markup"].connect (_global_update);
1232      this.notify["unselected-markup"].connect (_global_update);
1233      _selected = 0;
1234
1235      var config = (UIWidgetsConfig) ConfigService.get_default ().get_config ("ui", "widgets", typeof (UIWidgetsConfig));
1236      animation_enabled = config.animation_enabled;
1237    }
1238    public override bool button_press_event (Gdk.EventButton event)
1239    {
1240      int x = (int)event.x;
1241      x -= current_offset;
1242      if (x < 0)
1243      {
1244        this.selected = 0;
1245        selection_changed ();
1246        return false;
1247      }
1248      int i = 0;
1249      while (i < texts.size && texts.get (i).offset < x) i++;
1250      this.selected = i - 1;
1251      selection_changed ();
1252      return false;
1253    }
1254
1255    public override bool scroll_event (Gdk.EventScroll event)
1256    {
1257      if (event.direction == Gdk.ScrollDirection.UP)
1258        select_prev ();
1259      else
1260        select_next ();
1261      selection_changed ();
1262      return true;
1263    }
1264
1265    public signal void selection_changed ();
1266
1267    public void add_text (string txt)
1268    {
1269      texts.add (new PangoReadyText(){
1270        text = txt,
1271        offset = 0,
1272        width = 0,
1273        height = 0
1274      });
1275      _global_update ();
1276    }
1277
1278    public void remove_text (int i)
1279    {
1280      return_if_fail (i > 0 && i < texts.size);
1281      return_if_fail (texts.size == 1);
1282      texts.remove_at (i);
1283      _global_update ();
1284      if (selected >= texts.size) selected = texts.size - 1;
1285    }
1286    private void _global_update ()
1287    {
1288      update_all_sizes ();
1289      update_cached_surface ();
1290      queue_resize ();
1291      queue_draw ();
1292    }
1293    private void update_all_sizes ()
1294    {
1295      // also updates offsets
1296      int w = 0, h = 0;
1297      wmax = hmax = 0;
1298      string s;
1299      PangoReadyText txt = null;
1300      int lastx = 0;
1301      unowned Pango.Layout layout = this.label.get_layout ();
1302      for (int i = 0; i < texts.size; i++)
1303      {
1304        txt = texts.get (i);
1305        if (txt == null) continue;
1306        s = Markup.printf_escaped (i == _selected ? selected_markup : unselected_markup, txt.text);
1307        layout.set_markup (s, -1);
1308        layout.get_pixel_size (out w, out h);
1309        txt.width = w;
1310        txt.height = h;
1311        txt.offset = lastx;
1312        lastx += w + padding;
1313        wmax = int.max (wmax , txt.width);
1314        hmax = int.max (hmax, txt.height);
1315      }
1316    }
1317    public override void get_preferred_width (out int min_width, out int nat_width)
1318    {
1319      min_width = nat_width = wmax * 3; //triple for fading
1320    }
1321    public override void get_preferred_height (out int min_height, out int nat_height)
1322    {
1323      min_height = nat_height = hmax;
1324    }
1325    public void select_prev ()
1326    {
1327      selected = selected - 1;
1328    }
1329    public void select_next ()
1330    {
1331      selected = selected + 1;
1332    }
1333    private void update_cached_surface ()
1334    {
1335      if (!this.get_realized ()) return;
1336      int w = 0, h = 0;
1337      PangoReadyText txt;
1338      txt = texts.last ();
1339      w = txt.offset + txt.width;
1340      h = hmax * 3; //triple h for nice vertical placement
1341      var window_context = Gdk.cairo_create (this.get_window ());
1342      this.cached_surface = new Cairo.Surface.similar (window_context.get_target (), Cairo.Content.COLOR_ALPHA, w, h);
1343      var ctx = new Cairo.Context (this.cached_surface);
1344
1345      unowned Pango.Layout layout = this.label.get_layout ();
1346      Pango.cairo_update_context (ctx, layout.get_context ());
1347      ch.set_source_rgba (ctx, 1.0, StyleType.FG, this.get_state_flags ());
1348      ctx.set_operator (Cairo.Operator.OVER);
1349      string s;
1350      for (int i = 0; i < texts.size; i++)
1351      {
1352        txt = texts.get (i);
1353        if (txt == null)
1354          continue;
1355        ctx.save ();
1356        ctx.translate (txt.offset, (h - txt.height) / 2);
1357        s = Markup.printf_escaped (i == _selected ? selected_markup : unselected_markup, txt.text);
1358        layout.set_markup (s, -1);
1359        Pango.cairo_show_layout (ctx, layout);
1360        ctx.restore ();
1361      }
1362      /* Arrows */
1363      if (!this.show_arrows)
1364        return;
1365      if ((this.get_state_flags () & Gtk.StateFlags.SELECTED) != 0)
1366        ch.set_source_rgba (ctx, 1.0, StyleType.BG, Gtk.StateFlags.NORMAL);
1367      else
1368        ch.set_source_rgba (ctx, 1.0, StyleType.BG, Gtk.StateFlags.SELECTED);
1369      txt = texts.get (_selected);
1370      double asize = double.min (ARROW_SIZE, h);
1371      double px, py = h / 2;
1372      double f = 2; //curvature
1373      f = asize - f;
1374      if (_selected < texts.size - 1)
1375      {
1376        px = txt.offset + txt.width + asize + (padding-asize) / 2;
1377        ctx.move_to (px, py);
1378        ctx.rel_line_to (-asize, -asize/2);
1379        ctx.curve_to (px - f, py, px - f, py, px - asize, py + asize / 2);
1380        ctx.line_to (px, py);
1381        ctx.fill ();
1382      }
1383      if (_selected > 0)
1384      {
1385        px = txt.offset - asize - (padding-asize) / 2;
1386        ctx.move_to (px, py);
1387        ctx.rel_line_to (asize, -asize/2);
1388        ctx.curve_to (px + f, py, px + f, py, px + asize, py + asize / 2);
1389        ctx.line_to (px, py);
1390        ctx.fill ();
1391      }
1392    }
1393    private uint tid;
1394    private bool update_current_offset ()
1395    {
1396      double draw_offset = 0; //target offset
1397      PangoReadyText txt = texts.get (_selected);
1398      draw_offset = this.get_allocated_width () / 2 - txt.offset - txt.width / 2;
1399      int target = (int)Math.round (draw_offset);
1400      if (!animation_enabled)
1401      {
1402        current_offset = target;
1403        queue_draw ();
1404        return false;
1405      }
1406      if (target == current_offset)
1407      {
1408        tid = 0;
1409        return false; // stop animation
1410      }
1411      int inc = int.max (1, (int) Math.fabs ((target - current_offset) / 6));
1412      current_offset += target > current_offset ? inc : - inc;
1413      queue_draw ();
1414      return true;
1415    }
1416    protected override bool draw (Cairo.Context ctx)
1417    {
1418      if (texts.size == 0 || this.cached_surface == null)
1419        return true;
1420
1421      Gtk.Allocation allocation;
1422      get_allocation (out allocation);
1423
1424      double w = allocation.width;
1425      double h = allocation.height;
1426
1427      ctx.set_operator (Cairo.Operator.OVER);
1428      double x, y;
1429      x = current_offset;
1430      y = Math.round ((h - (3 * hmax)) / 2 );
1431      ctx.set_source_surface (this.cached_surface, x, y);
1432      ctx.rectangle (0, 0, w, h);
1433      ctx.clip ();
1434      var pat = new Cairo.Pattern.linear (0, 0, w, h);
1435      double fadepct = wmax / (double)w;
1436      if (w / 3 < wmax)
1437        fadepct = (w - wmax) / 2 / (double)w;
1438      pat.add_color_stop_rgba (0, 1, 1, 1, 0);
1439      pat.add_color_stop_rgba (fadepct, 1, 1, 1, 1);
1440      pat.add_color_stop_rgba (1 - fadepct, 1, 1, 1, 1);
1441      pat.add_color_stop_rgba (1, 1, 1, 1, 0);
1442      ctx.mask (pat);
1443      return true;
1444    }
1445  }
1446}
1447