Skip to content

Commit

Permalink
TimePicker - new Clock widget to scrub to the desired time
Browse files Browse the repository at this point in the history
  • Loading branch information
lainsce committed Jun 28, 2024
1 parent 158084b commit 8bc779e
Showing 1 changed file with 305 additions and 10 deletions.
315 changes: 305 additions & 10 deletions lib/Widgets/TimePicker.vala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022-2023 Fyra Labs
* Copyright (c) 2022-2024 Fyra Labs
* Copyright (c) 2014–2021 elementary, Inc. (https://elementary.io)
*
* This program is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -53,6 +53,8 @@ public class He.TimePicker : Gtk.Entry {
}

update_text (true);
clock.hour = _time.get_hour ();
clock.minute = _time.get_minute ();
changing_time = false;
}
}
Expand All @@ -65,6 +67,7 @@ public class He.TimePicker : Gtk.Entry {
private Gtk.SpinButton minutes_spinbutton;
private Gtk.ToggleButton am_togglebutton;
private Gtk.ToggleButton pm_togglebutton;
private ClockWidget clock;

/**
* Creates a new TimePicker widget with the given format strings.
Expand Down Expand Up @@ -108,8 +111,10 @@ public class He.TimePicker : Gtk.Entry {

if (is_clock_format_12h ()) {
hours_spinbutton = new Gtk.SpinButton.with_range (1, 12, 1);
clock.is_military_mode = false;
} else {
hours_spinbutton = new Gtk.SpinButton.with_range (0, 23, 1);
clock.is_military_mode = true;
}

hours_spinbutton.orientation = Gtk.Orientation.VERTICAL;
Expand Down Expand Up @@ -138,16 +143,39 @@ public class He.TimePicker : Gtk.Entry {
var separation_label = new Gtk.Label (":");
separation_label.add_css_class ("display");

var pop_grid = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6) {
var pop_grid_top = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6) {
margin_top = 12,
margin_bottom = 12,
margin_start = 12,
margin_end = 12,
};
pop_grid.append (hours_spinbutton);
pop_grid.append (separation_label);
pop_grid.append (minutes_spinbutton);
pop_grid.append (am_pm_box);
pop_grid_top.append (hours_spinbutton);
pop_grid_top.append (separation_label);
pop_grid_top.append (minutes_spinbutton);
pop_grid_top.append (am_pm_box);

clock = new ClockWidget ();
clock.time_selected.connect ((hour, minute) => {
hours_spinbutton.set_text (hour.to_string ());
if (minute < 10) {
minutes_spinbutton.set_text ("0" + minute.to_string ());
} else {
minutes_spinbutton.set_text (minute.to_string ());
}

time_changed ();
update_time (true);
update_time (false);
});

var pop_grid_middle = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6) {
margin_start = 12,
margin_end = 12,
};
pop_grid_middle.append (clock);

var pop_grid = new Gtk.Box (Gtk.Orientation.VERTICAL, 24);
pop_grid.append (pop_grid_top);
pop_grid.append (pop_grid_middle);

popover = new Gtk.Popover () {
autohide = true,
Expand All @@ -160,8 +188,7 @@ public class He.TimePicker : Gtk.Entry {

var focus_controller = new Gtk.EventControllerFocus ();
var scroll_controller = new Gtk.EventControllerScroll (
Gtk.EventControllerScrollFlags.BOTH_AXES
| Gtk.EventControllerScrollFlags.DISCRETE
Gtk.EventControllerScrollFlags.BOTH_AXES | Gtk.EventControllerScrollFlags.DISCRETE
);

add_controller (focus_controller);
Expand Down Expand Up @@ -272,11 +299,13 @@ public class He.TimePicker : Gtk.Entry {

// Make sure that bounds are set correctly
hours_spinbutton.set_range (1, 12);
clock.is_military_mode = false;
} else {
am_pm_box.hide ();
hours_spinbutton.set_value (time.get_hour ());

hours_spinbutton.set_range (0, 23);
clock.is_military_mode = true;
}

minutes_spinbutton.set_value (time.get_minute ());
Expand Down Expand Up @@ -390,4 +419,270 @@ public class He.TimePicker : Gtk.Entry {
time_changed ();
}
}
}

private class ClockWidget : Gtk.Widget {
private const double SIZE = 256.0;
private const double CENTER = SIZE / 2;
private const double RADIUS = (SIZE / 2) - 10.0;
private const double SELECTION_CIRCLE_RADIUS = 24.0;
private const double INNER_RADIUS = RADIUS - SELECTION_CIRCLE_RADIUS - 30.0;
private const double OUTER_RADIUS = RADIUS - SELECTION_CIRCLE_RADIUS;
private const double HAND_LINE_WIDTH = 2.0;
private const double HAND_CENTER_WIDTH = 4.0;
private const string FONT_FAMILY = "Manrope";
private const int HOUR_FONT_SIZE = 20;
private const int MINUTE_FONT_SIZE = 18;
private const int HALF_DAY = 12;
private const int FULL_DAY = 24;
private const int MINUTES = 60;

public bool selecting_hour { get; set; default = true;}
public bool is_military_mode { get; set; default = false;}

private int selected_hour = 0;
private int selected_minute = 0;
private double last_angle = 0.0;

private Gdk.RGBA _accent_color = { 1, 1, 1, 1 };
public Gdk.RGBA accent_color {
get { return _accent_color; }
set { _accent_color = value; queue_draw (); }
}

public int hour {
get { return selected_hour; }
set {
selected_hour = ((int)(value * (is_military_mode ? FULL_DAY : HALF_DAY) / (2 * Math.PI)) + 3) % (is_military_mode ? FULL_DAY : HALF_DAY);
if (selected_hour == 0) selected_hour = (is_military_mode ? FULL_DAY : HALF_DAY);
queue_draw ();
}
}

public int minute {
get { return selected_minute; }
set {
selected_minute = (int)Math.round ((value * 30 / Math.PI) + 15) % MINUTES;
queue_draw ();
}
}

public signal void time_selected (int hour, int minute);

construct {
var click_gesture = new Gtk.GestureClick ();
click_gesture.pressed.connect ((n_press, x, y) => {
last_angle = get_angle_from_coords (x, y);
});
click_gesture.released.connect (() => {
if (selecting_hour) {
selecting_hour = false;
queue_draw ();
} else {
emit_time_selected ();
reset_to_hour_selection ();
}
});
add_controller (click_gesture);

var drag_gesture = new Gtk.GestureDrag ();
drag_gesture.drag_update.connect ((offset_x, offset_y) => {
double x, y;
drag_gesture.get_bounding_box_center (out x, out y);
update_selection (x, y);
});
add_controller (drag_gesture);

hexpand = true;
vexpand = true;
width_request = (int)SIZE;
height_request = (int)SIZE;
}

protected override void dispose () {
reset_to_hour_selection ();
base.dispose ();
}

protected override void snapshot (Gtk.Snapshot snapshot) {
var rect = Graphene.Rect ();
rect.init (0, 0, (float)SIZE, (float)SIZE);
var cr = snapshot.append_cairo (rect);

// Font used
cr.select_font_face (FONT_FAMILY, Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);

// Draw clock face
cr.set_source_rgba ((0.15 * 1), (0.15 * 1), (0.15 * 1), 0.08);
cr.arc (CENTER, CENTER, RADIUS, 0, 2 * Math.PI);
cr.fill ();

// Draw selection hand
double hand_angle = selecting_hour ? get_hour_angle () : get_minute_angle ();
double hand_radius = get_hand_radius (RADIUS);
double hand_x = CENTER + hand_radius * Math.cos (hand_angle);
double hand_y = CENTER + hand_radius * Math.sin (hand_angle);
cr.set_source_rgba (accent_color.red, accent_color.green, accent_color.blue, 1);
cr.arc (CENTER, CENTER, HAND_CENTER_WIDTH, 0, 2 * Math.PI);
cr.fill_preserve ();
cr.set_source_rgba (accent_color.red, accent_color.green, accent_color.blue, 1);
cr.set_line_width (HAND_LINE_WIDTH);
cr.move_to (CENTER, CENTER);
cr.line_to (hand_x, hand_y);
cr.stroke ();
cr.set_source_rgba (accent_color.red, accent_color.green, accent_color.blue, 1);
cr.arc (hand_x, hand_y, SELECTION_CIRCLE_RADIUS, 0, 2 * Math.PI);
cr.fill ();

// Draw minutes
if (!selecting_hour) {
for (int i = 0; i < MINUTES; i += 5) {
double minute_angle = (i - 15) * (2 * Math.PI) / MINUTES;
double minute_x = CENTER + OUTER_RADIUS * Math.cos (minute_angle);
double minute_y = CENTER + OUTER_RADIUS * Math.sin (minute_angle);

double selection_angle = get_minute_angle ();
if (selection_angle < -Math.PI / 2) {
selection_angle += 2 * Math.PI;
}
double label_angle = minute_angle;
if (label_angle < -Math.PI / 2) {
label_angle += 2 * Math.PI;
}

bool is_selected = Math.fabs (selection_angle - label_angle) < Math.PI / MINUTES;

if (is_selected) {
cr.set_source_rgba ((0.32 * 1), (0.32 * 1), (0.32 * 1), 1);
} else {
cr.set_source_rgba ((0.15 * 1), (0.15 * 1), (0.15 * 1), 1);
}

cr.set_font_size (MINUTE_FONT_SIZE);

if (i >= 6) {
cr.move_to (minute_x - 18 / 2.0, minute_y + 16 / 2.0);
} else if (i == 1) {
cr.move_to (minute_x - 6 / 2.0, minute_y + 16 / 2.0);
} else {
cr.move_to (minute_x - 10 / 2.0, minute_y + 16 / 2.0);
}
cr.show_text (i.to_string ());
}
}

// Draw hours
if (selecting_hour) {
int hour_count = is_military_mode ? FULL_DAY : HALF_DAY;

for (int i = 1; i <= hour_count; i++) {
double hour_angle = ((i - 3) * Math.PI) / (HALF_DAY / 2);
double label_radius = (i > HALF_DAY && is_military_mode) ? INNER_RADIUS : OUTER_RADIUS;
double label_x = CENTER + label_radius * Math.cos (hour_angle);
double label_y = CENTER + label_radius * Math.sin (hour_angle);

double selection_angle = get_hour_angle ();
if (selection_angle < -Math.PI / 2) {
selection_angle += 2 * Math.PI;
}
double label_angle = hour_angle;
if (label_angle < -Math.PI / 2) {
label_angle += 2 * Math.PI;
}

bool is_selected = Math.fabs (selection_angle - label_angle) < Math.PI / HALF_DAY;

if (is_selected) {
cr.set_source_rgba ((0.32 * 1), (0.32 * 1), (0.32 * 1), 1);
} else {
cr.set_source_rgba ((0.15 * 1), (0.15 * 1), (0.15 * 1), 1);
}

if (i >= 10) {
cr.move_to (label_x - 18 / 2.0, label_y + 16 / 2.0);
} else if (i == 1) {
cr.move_to (label_x - 6 / 2.0, label_y + 16 / 2.0);
} else {
cr.move_to (label_x - 10 / 2.0, label_y + 16 / 2.0);
}

if (i <= HALF_DAY) {
cr.set_font_size (HOUR_FONT_SIZE);
} else {
cr.set_font_size (MINUTE_FONT_SIZE);
}

// If i is 1 to 12, display 1 to 12
// If i is 13 to 23, display 13 to 23
// If i is 24, display 0
cr.show_text ((i > HALF_DAY ? (i == 24 ? 0 : i) : i).to_string ());
}
}
}

private void emit_time_selected () {
time_selected (selected_hour, selected_minute);
}

private void reset_to_hour_selection () {
selecting_hour = true;
queue_draw ();
}

private double get_hour_angle () {
double hour = selected_hour % HALF_DAY;
if (hour == 0) hour = HALF_DAY;
double angle = (hour * (2 * Math.PI / HALF_DAY)) - Math.PI / 2;
return angle;
}

private double get_minute_angle () {
return (selected_minute % MINUTES) * (2 * Math.PI / MINUTES) - Math.PI / 2;
}

private double get_hand_radius (double base_radius) {
if (selecting_hour && is_military_mode && selected_hour > HALF_DAY) {
return INNER_RADIUS;
} else {
return OUTER_RADIUS;
}
}

private void update_selection (double x, double y) {
double dx = x - CENTER;
double dy = y - CENTER;

double distance = Math.sqrt (dx * dx + dy * dy);
double angle = Math.atan2 (dy, dx) + Math.PI / 2;
if (angle < 0) {
angle += 2 * Math.PI;
}

if (selecting_hour) {
int temp_hour = (int)Math.round ((angle * HALF_DAY / (2 * Math.PI)));
if (temp_hour <= 0) temp_hour += HALF_DAY;
if (is_military_mode) {
if (distance < INNER_RADIUS) {
temp_hour += HALF_DAY;
}
}
selected_hour = temp_hour;
} else {
selected_minute = (int)Math.round ((angle * MINUTES / (2 * Math.PI))) % MINUTES;
if (selected_minute < 0) selected_minute += MINUTES;
}

last_angle = angle;

queue_draw ();
}

private double get_angle_from_coords (double x, double y) {
double angle = Math.atan2 (y - CENTER, x - CENTER);
if (angle < -Math.PI / 2) {
angle += 2 * Math.PI;
}

return angle;
}
}
}

0 comments on commit 8bc779e

Please sign in to comment.