1
1
/*
2
- * Copyright (c) 2022-2023 Fyra Labs
2
+ * Copyright (c) 2022-2024 Fyra Labs
3
3
* Copyright (c) 2014–2021 elementary, Inc. (https://elementary.io)
4
4
*
5
5
* This program is free software: you can redistribute it and/or modify
@@ -53,6 +53,8 @@ public class He.TimePicker : Gtk.Entry {
53
53
}
54
54
55
55
update_text (true );
56
+ clock. hour = _time. get_hour ();
57
+ clock. minute = _time. get_minute ();
56
58
changing_time = false ;
57
59
}
58
60
}
@@ -65,6 +67,7 @@ public class He.TimePicker : Gtk.Entry {
65
67
private Gtk . SpinButton minutes_spinbutton;
66
68
private Gtk . ToggleButton am_togglebutton;
67
69
private Gtk . ToggleButton pm_togglebutton;
70
+ private ClockWidget clock;
68
71
69
72
/**
70
73
* Creates a new TimePicker widget with the given format strings.
@@ -108,8 +111,10 @@ public class He.TimePicker : Gtk.Entry {
108
111
109
112
if (is_clock_format_12h ()) {
110
113
hours_spinbutton = new Gtk .SpinButton .with_range (1 , 12 , 1 );
114
+ clock. is_military_mode = false ;
111
115
} else {
112
116
hours_spinbutton = new Gtk .SpinButton .with_range (0 , 23 , 1 );
117
+ clock. is_military_mode = true ;
113
118
}
114
119
115
120
hours_spinbutton. orientation = Gtk . Orientation . VERTICAL ;
@@ -138,16 +143,39 @@ public class He.TimePicker : Gtk.Entry {
138
143
var separation_label = new Gtk .Label (" :" );
139
144
separation_label. add_css_class (" display" );
140
145
141
- var pop_grid = new Gtk .Box (Gtk . Orientation . HORIZONTAL , 6 ) {
146
+ var pop_grid_top = new Gtk .Box (Gtk . Orientation . HORIZONTAL , 6 ) {
142
147
margin_top = 12 ,
143
- margin_bottom = 12 ,
144
148
margin_start = 12 ,
145
149
margin_end = 12 ,
146
150
};
147
- pop_grid. append (hours_spinbutton);
148
- pop_grid. append (separation_label);
149
- pop_grid. append (minutes_spinbutton);
150
- pop_grid. append (am_pm_box);
151
+ pop_grid_top. append (hours_spinbutton);
152
+ pop_grid_top. append (separation_label);
153
+ pop_grid_top. append (minutes_spinbutton);
154
+ pop_grid_top. append (am_pm_box);
155
+
156
+ clock = new ClockWidget ();
157
+ clock. time_selected. connect ((hour, minute) = > {
158
+ hours_spinbutton. set_text (hour. to_string ());
159
+ if (minute < 10 ) {
160
+ minutes_spinbutton. set_text (" 0" + minute. to_string ());
161
+ } else {
162
+ minutes_spinbutton. set_text (minute. to_string ());
163
+ }
164
+
165
+ time_changed ();
166
+ update_time (true );
167
+ update_time (false );
168
+ });
169
+
170
+ var pop_grid_middle = new Gtk .Box (Gtk . Orientation . HORIZONTAL , 6 ) {
171
+ margin_start = 12 ,
172
+ margin_end = 12 ,
173
+ };
174
+ pop_grid_middle. append (clock);
175
+
176
+ var pop_grid = new Gtk .Box (Gtk . Orientation . VERTICAL , 24 );
177
+ pop_grid. append (pop_grid_top);
178
+ pop_grid. append (pop_grid_middle);
151
179
152
180
popover = new Gtk .Popover () {
153
181
autohide = true ,
@@ -160,8 +188,7 @@ public class He.TimePicker : Gtk.Entry {
160
188
161
189
var focus_controller = new Gtk .EventControllerFocus ();
162
190
var scroll_controller = new Gtk .EventControllerScroll (
163
- Gtk . EventControllerScrollFlags . BOTH_AXES
164
- | Gtk . EventControllerScrollFlags . DISCRETE
191
+ Gtk . EventControllerScrollFlags . BOTH_AXES | Gtk . EventControllerScrollFlags . DISCRETE
165
192
);
166
193
167
194
add_controller (focus_controller);
@@ -272,11 +299,13 @@ public class He.TimePicker : Gtk.Entry {
272
299
273
300
// Make sure that bounds are set correctly
274
301
hours_spinbutton. set_range (1 , 12 );
302
+ clock. is_military_mode = false ;
275
303
} else {
276
304
am_pm_box. hide ();
277
305
hours_spinbutton. set_value (time. get_hour ());
278
306
279
307
hours_spinbutton. set_range (0 , 23 );
308
+ clock. is_military_mode = true ;
280
309
}
281
310
282
311
minutes_spinbutton. set_value (time. get_minute ());
@@ -390,4 +419,270 @@ public class He.TimePicker : Gtk.Entry {
390
419
time_changed ();
391
420
}
392
421
}
393
- }
422
+
423
+ private class ClockWidget : Gtk .Widget {
424
+ private const double SIZE = 256.0 ;
425
+ private const double CENTER = SIZE / 2 ;
426
+ private const double RADIUS = (SIZE / 2 ) - 10.0 ;
427
+ private const double SELECTION_CIRCLE_RADIUS = 24.0 ;
428
+ private const double INNER_RADIUS = RADIUS - SELECTION_CIRCLE_RADIUS - 30.0 ;
429
+ private const double OUTER_RADIUS = RADIUS - SELECTION_CIRCLE_RADIUS ;
430
+ private const double HAND_LINE_WIDTH = 2.0 ;
431
+ private const double HAND_CENTER_WIDTH = 4.0 ;
432
+ private const string FONT_FAMILY = " Manrope" ;
433
+ private const int HOUR_FONT_SIZE = 20 ;
434
+ private const int MINUTE_FONT_SIZE = 18 ;
435
+ private const int HALF_DAY = 12 ;
436
+ private const int FULL_DAY = 24 ;
437
+ private const int MINUTES = 60 ;
438
+
439
+ public bool selecting_hour { get ; set ; default = true ;}
440
+ public bool is_military_mode { get ; set ; default = false ;}
441
+
442
+ private int selected_hour = 0 ;
443
+ private int selected_minute = 0 ;
444
+ private double last_angle = 0.0 ;
445
+
446
+ private Gdk . RGBA _accent_color = { 1 , 1 , 1 , 1 };
447
+ public Gdk . RGBA accent_color {
448
+ get { return _accent_color; }
449
+ set { _accent_color = value ; queue_draw (); }
450
+ }
451
+
452
+ public int hour {
453
+ get { return selected_hour; }
454
+ set {
455
+ selected_hour = ((int )(value * (is_military_mode ? FULL_DAY : HALF_DAY ) / (2 * Math . PI )) + 3 ) % (is_military_mode ? FULL_DAY : HALF_DAY );
456
+ if (selected_hour == 0 ) selected_hour = (is_military_mode ? FULL_DAY : HALF_DAY );
457
+ queue_draw ();
458
+ }
459
+ }
460
+
461
+ public int minute {
462
+ get { return selected_minute; }
463
+ set {
464
+ selected_minute = (int )Math . round ((value * 30 / Math . PI ) + 15 ) % MINUTES ;
465
+ queue_draw ();
466
+ }
467
+ }
468
+
469
+ public signal void time_selected (int hour , int minute );
470
+
471
+ construct {
472
+ var click_gesture = new Gtk .GestureClick ();
473
+ click_gesture. pressed. connect ((n_press, x, y) = > {
474
+ last_angle = get_angle_from_coords (x, y);
475
+ });
476
+ click_gesture. released. connect (() = > {
477
+ if (selecting_hour) {
478
+ selecting_hour = false ;
479
+ queue_draw ();
480
+ } else {
481
+ emit_time_selected ();
482
+ reset_to_hour_selection ();
483
+ }
484
+ });
485
+ add_controller (click_gesture);
486
+
487
+ var drag_gesture = new Gtk .GestureDrag ();
488
+ drag_gesture. drag_update. connect ((offset_x, offset_y) = > {
489
+ double x, y;
490
+ drag_gesture. get_bounding_box_center (out x, out y);
491
+ update_selection (x, y);
492
+ });
493
+ add_controller (drag_gesture);
494
+
495
+ hexpand = true ;
496
+ vexpand = true ;
497
+ width_request = (int )SIZE ;
498
+ height_request = (int )SIZE ;
499
+ }
500
+
501
+ protected override void dispose () {
502
+ reset_to_hour_selection ();
503
+ base . dispose ();
504
+ }
505
+
506
+ protected override void snapshot (Gtk .Snapshot snapshot ) {
507
+ var rect = Graphene . Rect ();
508
+ rect. init (0 , 0 , (float )SIZE , (float )SIZE );
509
+ var cr = snapshot. append_cairo (rect);
510
+
511
+ // Font used
512
+ cr. select_font_face (FONT_FAMILY , Cairo . FontSlant . NORMAL , Cairo . FontWeight . NORMAL );
513
+
514
+ // Draw clock face
515
+ cr. set_source_rgba ((0.15 * 1 ), (0.15 * 1 ), (0.15 * 1 ), 0.08 );
516
+ cr. arc (CENTER , CENTER , RADIUS , 0 , 2 * Math . PI );
517
+ cr. fill ();
518
+
519
+ // Draw selection hand
520
+ double hand_angle = selecting_hour ? get_hour_angle () : get_minute_angle ();
521
+ double hand_radius = get_hand_radius (RADIUS );
522
+ double hand_x = CENTER + hand_radius * Math . cos (hand_angle);
523
+ double hand_y = CENTER + hand_radius * Math . sin (hand_angle);
524
+ cr. set_source_rgba (accent_color. red, accent_color. green, accent_color. blue, 1 );
525
+ cr. arc (CENTER , CENTER , HAND_CENTER_WIDTH , 0 , 2 * Math . PI );
526
+ cr. fill_preserve ();
527
+ cr. set_source_rgba (accent_color. red, accent_color. green, accent_color. blue, 1 );
528
+ cr. set_line_width (HAND_LINE_WIDTH );
529
+ cr. move_to (CENTER , CENTER );
530
+ cr. line_to (hand_x, hand_y);
531
+ cr. stroke ();
532
+ cr. set_source_rgba (accent_color. red, accent_color. green, accent_color. blue, 1 );
533
+ cr. arc (hand_x, hand_y, SELECTION_CIRCLE_RADIUS , 0 , 2 * Math . PI );
534
+ cr. fill ();
535
+
536
+ // Draw minutes
537
+ if (! selecting_hour) {
538
+ for (int i = 0 ; i < MINUTES ; i + = 5 ) {
539
+ double minute_angle = (i - 15 ) * (2 * Math . PI ) / MINUTES ;
540
+ double minute_x = CENTER + OUTER_RADIUS * Math . cos (minute_angle);
541
+ double minute_y = CENTER + OUTER_RADIUS * Math . sin (minute_angle);
542
+
543
+ double selection_angle = get_minute_angle ();
544
+ if (selection_angle < - Math . PI / 2 ) {
545
+ selection_angle + = 2 * Math . PI ;
546
+ }
547
+ double label_angle = minute_angle;
548
+ if (label_angle < - Math . PI / 2 ) {
549
+ label_angle + = 2 * Math . PI ;
550
+ }
551
+
552
+ bool is_selected = Math . fabs (selection_angle - label_angle) < Math . PI / MINUTES ;
553
+
554
+ if (is_selected) {
555
+ cr. set_source_rgba ((0.32 * 1 ), (0.32 * 1 ), (0.32 * 1 ), 1 );
556
+ } else {
557
+ cr. set_source_rgba ((0.15 * 1 ), (0.15 * 1 ), (0.15 * 1 ), 1 );
558
+ }
559
+
560
+ cr. set_font_size (MINUTE_FONT_SIZE );
561
+
562
+ if (i >= 6 ) {
563
+ cr. move_to (minute_x - 18 / 2.0 , minute_y + 16 / 2.0 );
564
+ } else if (i == 1 ) {
565
+ cr. move_to (minute_x - 6 / 2.0 , minute_y + 16 / 2.0 );
566
+ } else {
567
+ cr. move_to (minute_x - 10 / 2.0 , minute_y + 16 / 2.0 );
568
+ }
569
+ cr. show_text (i. to_string ());
570
+ }
571
+ }
572
+
573
+ // Draw hours
574
+ if (selecting_hour) {
575
+ int hour_count = is_military_mode ? FULL_DAY : HALF_DAY ;
576
+
577
+ for (int i = 1 ; i <= hour_count; i++ ) {
578
+ double hour_angle = ((i - 3 ) * Math . PI ) / (HALF_DAY / 2 );
579
+ double label_radius = (i > HALF_DAY && is_military_mode) ? INNER_RADIUS : OUTER_RADIUS ;
580
+ double label_x = CENTER + label_radius * Math . cos (hour_angle);
581
+ double label_y = CENTER + label_radius * Math . sin (hour_angle);
582
+
583
+ double selection_angle = get_hour_angle ();
584
+ if (selection_angle < - Math . PI / 2 ) {
585
+ selection_angle + = 2 * Math . PI ;
586
+ }
587
+ double label_angle = hour_angle;
588
+ if (label_angle < - Math . PI / 2 ) {
589
+ label_angle + = 2 * Math . PI ;
590
+ }
591
+
592
+ bool is_selected = Math . fabs (selection_angle - label_angle) < Math . PI / HALF_DAY ;
593
+
594
+ if (is_selected) {
595
+ cr. set_source_rgba ((0.32 * 1 ), (0.32 * 1 ), (0.32 * 1 ), 1 );
596
+ } else {
597
+ cr. set_source_rgba ((0.15 * 1 ), (0.15 * 1 ), (0.15 * 1 ), 1 );
598
+ }
599
+
600
+ if (i >= 10 ) {
601
+ cr. move_to (label_x - 18 / 2.0 , label_y + 16 / 2.0 );
602
+ } else if (i == 1 ) {
603
+ cr. move_to (label_x - 6 / 2.0 , label_y + 16 / 2.0 );
604
+ } else {
605
+ cr. move_to (label_x - 10 / 2.0 , label_y + 16 / 2.0 );
606
+ }
607
+
608
+ if (i <= HALF_DAY ) {
609
+ cr. set_font_size (HOUR_FONT_SIZE );
610
+ } else {
611
+ cr. set_font_size (MINUTE_FONT_SIZE );
612
+ }
613
+
614
+ // If i is 1 to 12, display 1 to 12
615
+ // If i is 13 to 23, display 13 to 23
616
+ // If i is 24, display 0
617
+ cr. show_text ((i > HALF_DAY ? (i == 24 ? 0 : i) : i). to_string ());
618
+ }
619
+ }
620
+ }
621
+
622
+ private void emit_time_selected () {
623
+ time_selected (selected_hour, selected_minute);
624
+ }
625
+
626
+ private void reset_to_hour_selection () {
627
+ selecting_hour = true ;
628
+ queue_draw ();
629
+ }
630
+
631
+ private double get_hour_angle () {
632
+ double hour = selected_hour % HALF_DAY ;
633
+ if (hour == 0 ) hour = HALF_DAY ;
634
+ double angle = (hour * (2 * Math . PI / HALF_DAY )) - Math . PI / 2 ;
635
+ return angle;
636
+ }
637
+
638
+ private double get_minute_angle () {
639
+ return (selected_minute % MINUTES ) * (2 * Math . PI / MINUTES ) - Math . PI / 2 ;
640
+ }
641
+
642
+ private double get_hand_radius (double base_radius ) {
643
+ if (selecting_hour && is_military_mode && selected_hour > HALF_DAY ) {
644
+ return INNER_RADIUS ;
645
+ } else {
646
+ return OUTER_RADIUS ;
647
+ }
648
+ }
649
+
650
+ private void update_selection (double x , double y ) {
651
+ double dx = x - CENTER ;
652
+ double dy = y - CENTER ;
653
+
654
+ double distance = Math . sqrt (dx * dx + dy * dy);
655
+ double angle = Math . atan2 (dy, dx) + Math . PI / 2 ;
656
+ if (angle < 0 ) {
657
+ angle + = 2 * Math . PI ;
658
+ }
659
+
660
+ if (selecting_hour) {
661
+ int temp_hour = (int )Math . round ((angle * HALF_DAY / (2 * Math . PI )));
662
+ if (temp_hour <= 0 ) temp_hour + = HALF_DAY ;
663
+ if (is_military_mode) {
664
+ if (distance < INNER_RADIUS ) {
665
+ temp_hour + = HALF_DAY ;
666
+ }
667
+ }
668
+ selected_hour = temp_hour;
669
+ } else {
670
+ selected_minute = (int )Math . round ((angle * MINUTES / (2 * Math . PI ))) % MINUTES ;
671
+ if (selected_minute < 0 ) selected_minute + = MINUTES ;
672
+ }
673
+
674
+ last_angle = angle;
675
+
676
+ queue_draw ();
677
+ }
678
+
679
+ private double get_angle_from_coords (double x , double y ) {
680
+ double angle = Math . atan2 (y - CENTER , x - CENTER );
681
+ if (angle < - Math . PI / 2 ) {
682
+ angle + = 2 * Math . PI ;
683
+ }
684
+
685
+ return angle;
686
+ }
687
+ }
688
+ }
0 commit comments