.
1 /*
2 Copyright (C) 2012 2014 2015 Johan Mattsson
3
4 This library is free software; you can redistribute it and/or modify
5 it under the terms of the GNU Lesser General Public License as
6 published by the Free Software Foundation; either version 3 of the
7 License, or (at your option) any later version.
8
9 This library is distributed in the hope that it will be useful, but
10 WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 Lesser General Public License for more details.
13 */
14
15 using Cairo;
16
17 namespace BirdFont {
18
19 public class TabBar : GLib.Object {
20
21 public int width = 0;
22 public int height = 0;
23
24 public Gee.ArrayList<Tab> tabs;
25
26 static const int NO_TAB = -1;
27 static const int NEXT_TAB = -2;
28 static const int PREVIOUS_TAB = -3;
29 static const int PROGRESS_WHEEL = -3;
30 static const int SHOW_MENU = -4;
31 static const int STOP_BUTTON = -5;
32
33 int first_tab = 0;
34 int selected = 0;
35 int over = NO_TAB;
36 int over_close_tab = NO_TAB;
37
38 public signal void signal_tab_selected (Tab selected_tab);
39 public signal void redraw_tab_bar (int x, int y, int w, int h);
40
41 Tab? previous_tab = null;
42 Tab? current_tab = null;
43
44 double scale = 1; // scale images in 320 dpi
45
46 bool processing = false;
47 bool stop_button = false;
48 double wheel_rotation = 0;
49
50 double background_r = 51 /255.0;
51 double background_g = 54 /255.0;
52 double background_b = 59 /255.0;
53
54 Text menu_icon;
55 Text progress_icon;
56 Text stop_icon;
57 Text left_arrow;
58 Text right_arrow;
59
60 public TabBar () {
61 tabs = new Gee.ArrayList<Tab> ();
62
63 menu_icon = new Text ("menu_icon");
64 menu_icon.load_font (Theme.get_icon_file ());
65
66 progress_icon = new Text ("progress");
67 progress_icon.load_font (Theme.get_icon_file ());
68
69 stop_icon = new Text ("stop");
70 stop_icon.load_font (Theme.get_icon_file ());
71
72 left_arrow = new Text ("left_arrow");
73 left_arrow.load_font (Theme.get_icon_file ());
74
75 right_arrow = new Text ("right_arrow");
76 right_arrow.load_font (Theme.get_icon_file ());
77
78 start_wheel ();
79 }
80
81 public void redraw (int x, int y, int w, int h) {
82 redraw_tab_bar (x, y, w, h);
83 }
84
85 public void set_background_color (double r, double g, double b) {
86 background_r = r;
87 background_g = g;
88 background_b = r;
89 }
90
91 public void motion (double x, double y) {
92 MainWindow.set_cursor (NativeWindow.VISIBLE);
93 motion_event (x, y, out over, out over_close_tab);
94 }
95
96 private void motion_event (double x, double y, out int over, out int over_close_tab) {
97 int i = 0;
98 double offset = 0;
99 bool close_y, close_x;
100
101 if (x < 24 && has_scroll ()) {
102 over_close_tab = NO_TAB;
103 over = PREVIOUS_TAB;
104 return;
105 }
106
107 if (!has_progress_wheel ()) {
108 if (x > width - 25) {
109 over_close_tab = NO_TAB;
110 over = SHOW_MENU;
111 return;
112 }
113 } else if (!has_scroll () && cancelable_task ()) {
114 if (x > width - 19 && 10 <= y < height - 10) {
115 over_close_tab = NO_TAB;
116 over = STOP_BUTTON;
117 stop_button = true;
118 redraw_tab_bar (0, 0, width, height);
119 } else {
120 stop_button = false;
121 }
122 } else if (has_scroll () && cancelable_task ()) {
123 if (x > width - 19 && 10 <= y < height - 10) {
124 over_close_tab = NO_TAB;
125 over = STOP_BUTTON;
126 stop_button = true;
127 redraw_tab_bar (0, 0, width, height);
128 return;
129 } else if (x > width - 2 * 19) {
130 over_close_tab = NO_TAB;
131 over = NEXT_TAB;
132 }
133 stop_button = false;
134 } else if (!has_scroll () && has_progress_wheel ()) {
135 if (x > width - 19) {
136 over_close_tab = NO_TAB;
137 over = PROGRESS_WHEEL;
138 }
139 } else if (has_scroll () && !has_progress_wheel ()) {
140 if (x > width - 19) {
141 over_close_tab = NO_TAB;
142 over = NEXT_TAB;
143 }
144 }
145
146 if (has_scroll ()) {
147 offset += 25;
148 }
149
150 foreach (Tab t in tabs) {
151 if (i < first_tab) {
152 i++;
153 continue;
154 }
155
156 if (offset < x < offset + t.get_width ()) {
157 over = i;
158
159 close_y = height / 2.0 - 4 < y < height / 2.0 + 4;
160 close_x = x > offset + t.get_width () - 16;
161
162 if (close_y && close_x) {
163 over_close_tab = i;
164 } else {
165 over_close_tab = NO_TAB;
166 }
167
168 return;
169 }
170
171 offset += t.get_width ();
172 i++;
173 }
174
175 over_close_tab = NO_TAB;
176 over = NO_TAB;
177 }
178
179 bool cancelable_task () {
180 return MainWindow.blocking_background_task.is_cancellable ();
181 }
182
183 /** Select tab for a glyph by charcode or name.
184 * @return true if the tab was found
185 */
186 public bool select_char (string s) {
187 int i = 0;
188
189 if (MenuTab.has_suppress_event ()) {
190 warn_if_test ("Event suppressed");
191 return false;
192 }
193
194 foreach (Tab t in tabs) {
195 if (t.get_display ().get_name () == s) {
196 select_tab (i);
197 return true;
198 }
199 i++;
200 }
201
202 return false;
203 }
204
205 public bool select_tab_name (string s) {
206 if (MenuTab.has_suppress_event ()) {
207 warn_if_test ("Event suppressed");
208 return false;
209 }
210
211 return select_char (s);
212 }
213
214 public void select_overview () {
215 if (MenuTab.has_suppress_event ()) {
216 warn_if_test ("Event suppressed");
217 return;
218 }
219
220 select_tab_name ("Overview");
221 }
222
223 private void select_previous_tab () {
224 Tab t;
225 bool open;
226
227 if (MenuTab.has_suppress_event ()) {
228 warn_if_test ("Event suppressed");
229 return;
230 }
231
232 if (previous_tab == null) {
233 return;
234 }
235
236 t = (!) previous_tab;
237 open = selected_open_tab (t);
238
239 if (!open) {
240 select_tab ((int) tabs.size - 1);
241 }
242 }
243
244 public void close_display (FontDisplay f) {
245 int i = -1;
246
247 if (MenuTab.has_suppress_event ()) {
248 warn_if_test ("Event suppressed");
249 return;
250 }
251
252 if (tabs.size >= 1) {
253 foreach (Tab t in tabs) {
254 ++i;
255
256 if (t.get_display () == f) {
257 close_tab (i) ;
258 return;
259 }
260 }
261 }
262
263 return_if_fail (i != -1);
264 }
265
266 public void close_all_tabs () {
267 if (MenuTab.has_suppress_event ()) {
268 warn_if_test ("Event suppressed");
269 return;
270 }
271
272 for (int i = 0; i < get_length (); i++) {
273 if (close_tab (i, false, true)) {
274 close_all_tabs ();
275 }
276 }
277 }
278
279 public bool close_tab (int index, bool background_tab = false, bool select_new_tab = true) {
280 Tab t;
281 EmptyTab empty_tab_canvas;
282 Tab empty_tab;
283 GlyphCollection gc;
284
285 if (MenuTab.has_suppress_event ()) {
286 warn_if_test ("Event suppressed");
287 return false;
288 }
289
290 if (!(0 <= index < tabs.size)) {
291 return false;
292 }
293
294 if (tabs.size == 1) {
295 empty_tab_canvas = new EmptyTab ("", "");
296 gc = new GlyphCollection.with_glyph('\0', "");
297 GlyphCanvas.set_display (empty_tab_canvas);
298 MainWindow.get_glyph_canvas ().set_current_glyph_collection (gc);
299 empty_tab = new Tab (empty_tab_canvas, 0, false);
300 signal_tab_selected (empty_tab);
301 }
302
303 t = tabs.get (index);
304
305 if (first_tab > 0) {
306 first_tab--;
307 }
308
309 if (t.has_close_button ()) {
310 t.get_display ().close ();
311
312 tabs.remove_at (index);
313
314 if (!background_tab && select_new_tab) {
315 select_previous_tab ();
316 }
317
318 return true;
319 }
320
321 if (select_new_tab) {
322 select_tab (index);
323 }
324
325 return false;
326 }
327
328 public bool close_by_name (string name, bool background_tab = false) {
329 int i = 0;
330
331 foreach (Tab tab in tabs) {
332 if (tab.get_display ().get_name () == name) {
333 bool closed = close_tab (i, background_tab);
334 redraw_tab_bar (0, 0, width, height);
335 return closed;
336 }
337
338 i++;
339 }
340
341 return false;
342 }
343
344 public void close_background_tab_by_name (string name) {
345 close_by_name (name, true);
346 }
347
348 /** Select a tab and return true if it is open. */
349 public bool selected_open_tab (Tab t) {
350 int i = 0;
351
352 if (MenuTab.has_suppress_event ()) {
353 warn_if_test ("Event suppressed");
354 return false;
355 }
356
357 foreach (var n in tabs) {
358 if (n == t) {
359 select_tab (i);
360 return true;
361 }
362
363 i++;
364 }
365
366 return false;
367 }
368
369 public Tab? get_nth (int i) {
370 if (!(0 <= i < get_length ())) {
371 return null;
372 }
373
374 return tabs.get (i);
375 }
376
377 public Tab? get_tab (string name) {
378 foreach (var n in tabs) {
379 if (n.get_display ().get_name () == name) {
380 return n;
381 }
382 }
383
384 return null;
385 }
386
387 public bool selected_open_tab_by_name (string t) {
388 int i = 0;
389
390 if (MenuTab.has_suppress_event ()) {
391 warn_if_test ("Event suppressed");
392 return false;
393 }
394
395 foreach (var n in tabs) {
396 if (n.get_display ().get_name () == t) {
397 select_tab (i);
398 return true;
399 }
400
401 i++;
402 }
403
404 return false;
405 }
406
407 public Tab get_selected_tab () {
408 int s = get_selected ();
409 if (0 <= s < tabs.size) {
410 return tabs.get (get_selected ());
411 }
412
413 warning ("No tab selected.");
414 return new Tab (new EmptyTab ("Error", "Error"), 30, false);
415 }
416
417 public uint get_length () {
418 return tabs.size;
419 }
420
421 public int get_selected () {
422 return selected;
423 }
424
425 public void select_tab (int index, bool signal_selected = true) {
426 Tab t;
427
428 if (MenuTab.has_suppress_event ()) {
429 warn_if_test ("Event suppressed");
430 return;
431 }
432
433 // always close any pending text input if the user switches tab
434 TabContent.hide_text_input ();
435
436 if (index == SHOW_MENU) {
437 MainWindow.get_menu ().show_menu = !MainWindow.get_menu ().show_menu;
438 GlyphCanvas.redraw ();
439 return;
440 }
441
442 if (index == NEXT_TAB) {
443 selected++;
444
445 if (selected >= tabs.size) {
446 selected = (int) tabs.size - 1;
447 }
448
449 scroll_to_tab (selected);
450 return;
451 }
452
453 if (index == PREVIOUS_TAB) {
454
455 if (selected > 0) {
456 selected--;
457 }
458
459 scroll_to_tab (selected);
460 return;
461 }
462
463 if (!(0 <= index < tabs.size)) {
464 return;
465 }
466
467 selected = index;
468 t = tabs.get (index);
469 previous_tab = current_tab;
470 current_tab = t;
471 scroll_to_tab (selected, signal_selected);
472 }
473
474 private bool has_scroll () {
475 int i = 0;
476 double offset = 19;
477 double end = (has_progress_wheel ()) ? width - 28 : width - 19;
478
479 if (first_tab > 0) {
480 return true;
481 }
482
483 foreach (Tab t in tabs) {
484 if (i < first_tab) {
485 i++;
486 continue;
487 }
488
489 if (offset + t.get_width () + 3 > end) {
490 return true;
491 }
492
493 offset += t.get_width ();
494 i++;
495 }
496
497 return false;
498 }
499
500 private void signal_selected (int index) {
501 Tab t;
502
503 t = tabs.get (index);
504
505 GlyphCanvas.set_display (t.get_display ());
506
507 MainWindow.get_glyph_canvas ()
508 .set_current_glyph_collection (t.get_glyph_collection ());
509
510 signal_tab_selected (t);
511 }
512
513 private void scroll_to_tab (int index, bool send_signal_selected = true) {
514 double offset = 19;
515 int i = 0;
516 double end = (has_progress_wheel ()) ? width - 68 : width - 40;
517
518 if (index < first_tab) {
519 first_tab = index;
520
521 if (send_signal_selected) {
522 signal_selected (index);
523 }
524 return;
525 }
526
527 foreach (Tab t in tabs) {
528 if (i < first_tab) {
529 i++;
530 continue;
531 }
532
533 // out of view
534 if (offset + t.get_width () + 3 > end) {
535 first_tab++;
536 scroll_to_tab (index);
537 return;
538 }
539
540 // in view
541 if (i == index) {
542
543 if (send_signal_selected) {
544 signal_selected (index);
545 }
546
547 return;
548 }
549
550 offset += t.get_width ();
551 i++;
552 }
553
554 warning ("");
555 }
556
557 public void select_tab_click (double x, double y, int width, int height) {
558 int over, close;
559
560 if (MainWindow.get_menu ().show_menu) {
561 MainWindow.get_menu ().show_menu = false;
562 GlyphCanvas.redraw ();
563 }
564
565 this.width = width;
566 this.height = height;
567 this.scale = height / 117.0;
568
569 motion_event (x, y, out over, out close);
570
571 if (stop_button) {
572 MainWindow.abort_task ();
573 } else if (over_close_tab >= 0) {
574 close_tab (over_close_tab);
575 } else {
576 select_tab (over);
577 }
578 }
579
580 public void add_tab (FontDisplay display_item, bool signal_selected = true, GlyphCollection? gc = null) {
581 double tab_width = -1;
582 bool always_open = false;
583 int position = (tabs.size == 0) ? 0 : selected + 1;
584 Tab tab;
585
586 if (MenuTab.has_suppress_event ()) {
587 warn_if_test ("Event suppressed");
588 return;
589 }
590
591 if (tab_width < 0) {
592 tab_width = 9 * display_item.get_label ().char_count ();
593 tab_width += 36;
594 }
595
596 tab = new Tab (display_item, tab_width, always_open);
597 tabs.insert (position, tab);
598
599 if (gc != null) {
600 tab.set_glyph_collection ((!) gc);
601 }
602
603 GlyphCanvas.set_display (tab.get_display ());
604
605 MainWindow.get_glyph_canvas ()
606 .set_current_glyph_collection (tab.get_glyph_collection ());
607
608 select_tab (position, signal_selected);
609 }
610
611 /** Returns true if the new item was added to the bar. */
612 public bool add_unique_tab (FontDisplay display_item, bool signal_selected = true) {
613 bool i;
614
615 if (MenuTab.has_suppress_event ()) {
616 warn_if_test ("Event suppressed");
617 return false;
618 }
619
620 i = select_tab_name (display_item.get_name ());
621
622 if (!i) {
623 add_tab (display_item, signal_selected);
624 return true;
625 }
626
627 return false;
628 }
629
630 public void draw (Context cr, int width, int height) {
631 double next_tab_x;
632 double w, h;
633
634 this.width = width;
635 this.height = height;
636 this.scale = height / 117.0;
637
638 cr.save ();
639 cr.set_line_width (0);
640 Theme.color (cr, "Default Background");
641 cr.rectangle (0, 0, width, height);
642 cr.fill ();
643 cr.restore ();
644
645 cr.save ();
646 cr.scale (scale, scale);
647
648 w = width / scale;
649 h = height / scale;
650
651 if (has_scroll () && !has_progress_wheel ()) {
652 // left arrow
653 Theme.text_color (left_arrow, "Text Tab Bar");
654 left_arrow.set_font_size (40 / scale);
655 left_arrow.widget_x = 2 / scale;
656 left_arrow.widget_y = h / 2.0 - (40 / scale ) / 2;
657 left_arrow.draw (cr);
658
659 // right arrow
660 Theme.text_color (right_arrow, "Text Tab Bar");
661 next_tab_x = (has_progress_wheel ()) ? w - (2 * 19 + 3) / scale : w - 19 / scale;
662 next_tab_x-= 32 / scale;
663
664 right_arrow.set_font_size (40 / scale);
665 right_arrow.widget_x = next_tab_x;
666 right_arrow.widget_y = h / 2.0 - (40 / scale ) / 2;
667 right_arrow.draw (cr);
668 }
669
670 if (has_progress_wheel ()) {
671 double progress_size = 40 / scale;
672 Text wheel = has_stop_button () ? stop_icon : progress_icon;
673
674 if (!has_stop_button ()) {
675 Theme.text_color (wheel, "Text Tab Bar");
676 } else {
677 Theme.text_color (wheel, "Highlighted 1");
678 }
679
680 wheel.set_font_size (progress_size);
681
682 double middley = h / 2;
683 double middlex = w - (wheel.get_sidebearing_extent () / 2) / scale;
684
685 wheel.widget_x = middlex;
686 wheel.widget_y = middley;
687
688 cr.save ();
689 if (!has_stop_button ()) {
690 cr.translate (middlex, middley);
691 cr.rotate (wheel_rotation);
692 cr.translate (-middlex, -middley);
693 }
694
695 wheel.draw_at_baseline (cr, wheel.widget_x, wheel.widget_y);
696 cr.restore ();
697 } else {
698 // menu icon
699 if (MainWindow.get_menu ().show_menu) {
700 Theme.color (cr, "Menu Background");
701 cr.rectangle (w - 40 / scale, 0, 40 / scale, h);
702 cr.fill ();
703 }
704
705 if (MainWindow.get_menu ().show_menu) {
706 Theme.text_color (menu_icon, "Foreground Inverted");
707 } else {
708 Theme.text_color (menu_icon, "Highlighted 1");
709 }
710
711 menu_icon.set_font_size (40 / scale);
712 menu_icon.widget_x = (int) (w - 27 / scale);
713 menu_icon.widget_y = (int) (((h - menu_icon.get_height ()) / 2) / scale);
714 menu_icon.draw (cr);
715 }
716
717 draw_tabs (cr);
718 cr.restore ();
719 }
720
721 private void draw_tabs (Context cr) {
722 double text_height, text_width, center_x, center_y;
723 double close_opacity;
724 double offset;
725 double tab_width;
726 double tabs_end = width / scale;
727 double h = height / scale;
728 double tab_height;
729 Tab t;
730 Text label;
731
732 if (has_progress_wheel ()) {
733 tabs_end -= 19 / scale;
734 }
735
736 if (has_scroll ()) {
737 tabs_end -= 60 / scale;
738 offset = 24 / scale;
739 } else {
740 offset = 0;
741 }
742
743 tab_height = this.height / scale;
744
745 for (int tab_index = first_tab; tab_index < tabs.size; tab_index++) {
746 t = tabs.get (tab_index);
747
748 cr.save ();
749 cr.translate (offset, 0);
750
751 tab_width = t.get_width () / scale;
752
753 if (offset + tab_width > tabs_end) {
754 cr.restore ();
755 break;
756 }
757
758 // background
759 if (tab_index == selected) {
760 cr.save ();
761 Theme.color (cr, "Highlighted 1");
762 cr.rectangle (0, 0, tab_width, h);
763 cr.fill ();
764 cr.restore ();
765 } else if (tab_index == over) {
766 cr.save ();
767 Theme.color (cr, "Default Background");
768 cr.rectangle (0, 0, tab_width, h);
769 cr.fill ();
770 cr.restore ();
771 } else {
772 cr.save ();
773 Theme.color (cr, "Default Background");
774 cr.rectangle (0, 0, tab_width, h);
775 cr.fill ();
776 cr.restore ();
777 }
778
779 // close (x)
780 if (t.has_close_button ()) {
781 cr.save ();
782 cr.new_path ();
783 cr.set_line_width (1 / scale);
784
785 close_opacity = (over_close_tab == tab_index) ? 1 : 0.2;
786
787 if (tab_index == selected) {
788 Theme.color_opacity (cr, "Selected Tab Foreground", close_opacity);
789 } else {
790 Theme.color_opacity (cr, "Text Foreground", close_opacity);
791 }
792
793 cr.move_to (tab_width - 7 / scale, h / 2.0 - 2.5 / scale);
794 cr.line_to (tab_width - 12 / scale, h / 2.0 + 2.5 / scale);
795
796 cr.move_to (tab_width - 12 / scale, h / 2.0 - 2.5 / scale);
797 cr.line_to (tab_width - 7 / scale, h / 2.0 + 2.5 / scale);
798
799 cr.stroke ();
800 cr.restore ();
801 }
802
803 // tab label
804 label = new Text ();
805 label.set_text (t.get_label ());
806 text_height = (int) (16 / scale);
807 label.set_font_size (text_height);
808 text_width = label.get_extent ();
809 center_x = tab_width / 2.0 - text_width / 2.0;
810 center_y = (int) (tab_height / 2.0 + 4 / scale);
811
812 if (tab_index == selected) {
813 Theme.text_color (label, "Selected Tab Foreground");
814 } else {
815 Theme.text_color (label, "Text Tab Bar");
816 }
817
818 label.set_font_size (text_height);
819 label.draw_at_baseline (cr, center_x, center_y);
820
821 // edges
822 if (tab_index != selected) { // don't draw edges for the selected tab
823 if (tab_index + 1 != selected) {
824 cr.save ();
825 Theme.color (cr, "Tab Separator");
826 cr.rectangle (tab_width - 1 / scale, 0, 1 / scale, h);
827 cr.fill ();
828 cr.restore ();
829 }
830
831 if (tab_index == first_tab) {
832 cr.save ();
833 Theme.color (cr, "Tab Separator");
834 cr.rectangle (0, 0, 1 / scale, h);
835 cr.fill ();
836 cr.restore ();
837 }
838 }
839
840 cr.restore ();
841
842 offset += tab_width;
843 }
844 }
845
846 public void add_empty_tab (string name, string label) {
847 add_tab (new EmptyTab (name, label));
848 }
849
850 bool has_stop_button () {
851 return processing
852 && stop_button;
853 }
854
855 bool has_progress_wheel () {
856 return processing;
857 }
858
859 public void set_progress (bool running) {
860 TimeoutSource timer;
861
862 if (unlikely (processing == running)) {
863 warning (@"Progress is already set to $running");
864 return;
865 }
866
867 processing = running;
868
869 if (!processing) {
870 stop_button = false;
871 }
872
873 if (processing) {
874 timer = new TimeoutSource (250);
875 timer.set_callback (() => {
876 wheel_rotation += 0.008 * 2 * Math.PI;
877
878 if (wheel_rotation > 2 * Math.PI) {
879 wheel_rotation -= 2 * Math.PI;
880 }
881
882 redraw_tab_bar (width - 40, 0, 40, height);
883
884 return processing;
885 });
886 timer.attach (null);
887 }
888 }
889
890 public static void start_wheel () {
891 TabBar t;
892 if (!is_null (MainWindow.get_tab_bar ())) {
893 t = MainWindow.get_tab_bar ();
894 t.set_progress (true);
895 }
896 }
897
898 public static void stop_wheel () {
899 if (!is_null (MainWindow.get_tab_bar ())) {
900 MainWindow.get_tab_bar ().set_progress (false);
901 }
902 }
903 }
904
905 }
906