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