Filter the historical list according to a date picker #194
|
@ -14,7 +14,7 @@ General Public License for more details.
|
||||||
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::{components::TextEntry, types::ParseError};
|
use crate::{components::{i32_field, TextEntry, month_field}, types::ParseError};
|
||||||
use chrono::{Datelike, Local};
|
use chrono::{Datelike, Local};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
@ -28,7 +28,7 @@ enum Part {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DateFieldPrivate {
|
pub struct DateFieldPrivate {
|
||||||
value: Rc<RefCell<chrono::NaiveDate>>,
|
date: Rc<RefCell<chrono::NaiveDate>>,
|
||||||
year: TextEntry<i32>,
|
year: TextEntry<i32>,
|
||||||
month: TextEntry<u32>,
|
month: TextEntry<u32>,
|
||||||
day: TextEntry<u32>,
|
day: TextEntry<u32>,
|
||||||
|
@ -41,36 +41,69 @@ impl ObjectSubclass for DateFieldPrivate {
|
||||||
type ParentType = gtk::Box;
|
type ParentType = gtk::Box;
|
||||||
|
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let date = Local::now().date_naive();
|
let date = Rc::new(RefCell::new(Local::now().date_naive()));
|
||||||
let year = TextEntry::new(
|
|
||||||
"year",
|
let year = i32_field(date.borrow().year(),
|
||||||
Some(date.year()),
|
{
|
||||||
|v| format!("{}", v),
|
let date = date.clone();
|
||||||
|v| v.parse::<i32>().map_err(|_| ParseError),
|
move |value| {
|
||||||
|_| {},
|
if let Some(year) = value {
|
||||||
|
let mut date = date.borrow_mut();
|
||||||
|
if let Some(new_date) = date.with_year(year) {
|
||||||
|
*date = new_date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
let month = TextEntry::new(
|
year.widget.set_max_length(4);
|
||||||
"month",
|
year.add_css_class("date-field__year");
|
||||||
Some(date.month()),
|
|
||||||
|v| format!("{}", v),
|
let month = month_field(
|
||||||
|v| v.parse::<u32>().map_err(|_| ParseError),
|
date.borrow().month(),
|
||||||
|_| {},
|
{
|
||||||
|
let date = date.clone();
|
||||||
|
move |value| {
|
||||||
|
if let Some(month) = value {
|
||||||
|
let mut date = date.borrow_mut();
|
||||||
|
if let Some(new_date) = date.with_month(month) {
|
||||||
|
*date = new_date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
month.add_css_class("date-field__month");
|
||||||
|
|
||||||
|
/* Modify this so that it enforces the number of days per month */
|
||||||
let day = TextEntry::new(
|
let day = TextEntry::new(
|
||||||
"day",
|
"day",
|
||||||
Some(date.day()),
|
Some(date.borrow().day()),
|
||||||
|v| format!("{}", v),
|
|v| format!("{}", v),
|
||||||
|v| v.parse::<u32>().map_err(|_| ParseError),
|
|v| v.parse::<u32>().map_err(|_| ParseError),
|
||||||
|_| {},
|
{
|
||||||
|
let date = date.clone();
|
||||||
|
move |value| {
|
||||||
|
if let Some(day) = value {
|
||||||
|
let mut date = date.borrow_mut();
|
||||||
|
if let Some(new_date) = date.with_day(day) {
|
||||||
|
*date = new_date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
);
|
);
|
||||||
|
day.add_css_class("date-field__day");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
value: Rc::new(RefCell::new(date.clone())),
|
date,
|
||||||
year,
|
year,
|
||||||
month,
|
month,
|
||||||
day,
|
day,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectImpl for DateFieldPrivate {}
|
impl ObjectImpl for DateFieldPrivate {}
|
||||||
|
@ -97,17 +130,48 @@ impl DateField {
|
||||||
s.imp().month.set_value(Some(date.month()));
|
s.imp().month.set_value(Some(date.month()));
|
||||||
s.imp().day.set_value(Some(date.day()));
|
s.imp().day.set_value(Some(date.day()));
|
||||||
|
|
||||||
*s.imp().value.borrow_mut() = date;
|
*s.imp().date.borrow_mut() = date;
|
||||||
|
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* As soon as the field gets focus, highlight the first element
|
|
||||||
|
pub fn date(&self) -> chrono::NaiveDate {
|
||||||
|
self.imp().date.borrow().clone()
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
*/
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::gtk_init::gtk_init;
|
use crate::gtk_init::gtk_init;
|
||||||
|
|
||||||
|
// Enabling this test pushes tests on the TextField into an infinite loop. That likely indicates a bad interaction within the TextField itself, and that is going to need to be fixed.
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn it_allows_valid_dates() {
|
||||||
|
let reference = chrono::NaiveDate::from_ymd_opt(2006, 01, 02).unwrap();
|
||||||
|
let field = DateField::new(reference);
|
||||||
|
field.imp().year.set_value(Some(2023));
|
||||||
|
field.imp().month.set_value(Some(10));
|
||||||
|
field.imp().day.set_value(Some(13));
|
||||||
|
assert!(field.is_valid());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn it_disallows_out_of_range_months() {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn it_allows_days_within_range_for_month() {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ mod steps;
|
||||||
pub use steps::{steps_editor, Steps};
|
pub use steps::{steps_editor, Steps};
|
||||||
|
|
||||||
mod text_entry;
|
mod text_entry;
|
||||||
pub use text_entry::{distance_field, duration_field, time_field, weight_field, TextEntry};
|
pub use text_entry::{distance_field, duration_field, time_field, weight_field, i32_field, month_field, TextEntry};
|
||||||
|
|
||||||
mod time_distance;
|
mod time_distance;
|
||||||
pub use time_distance::{time_distance_detail, time_distance_summary};
|
pub use time_distance::{time_distance_detail, time_distance_summary};
|
||||||
|
|
|
@ -23,11 +23,12 @@ use std::{cell::RefCell, rc::Rc};
|
||||||
pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
|
pub type Parser<T> = dyn Fn(&str) -> Result<T, ParseError>;
|
||||||
pub type OnUpdate<T> = dyn Fn(Option<T>);
|
pub type OnUpdate<T> = dyn Fn(Option<T>);
|
||||||
|
|
||||||
|
// TextEntry is not a proper widget because I was never able to figure out how to do a type parameterization on a GTK widget.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TextEntry<T: Clone + std::fmt::Debug> {
|
pub struct TextEntry<T: Clone + std::fmt::Debug> {
|
||||||
value: Rc<RefCell<Option<T>>>,
|
value: Rc<RefCell<Option<T>>>,
|
||||||
|
|
||||||
widget: gtk::Entry,
|
pub widget: gtk::Entry,
|
||||||
renderer: Rc<dyn Fn(&T) -> String>,
|
renderer: Rc<dyn Fn(&T) -> String>,
|
||||||
parser: Rc<Parser<T>>,
|
parser: Rc<Parser<T>>,
|
||||||
on_update: Rc<OnUpdate<T>>,
|
on_update: Rc<OnUpdate<T>>,
|
||||||
|
@ -82,7 +83,6 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
|
||||||
if let Some(ref v) = val {
|
if let Some(ref v) = val {
|
||||||
self.widget.set_text(&(self.renderer)(v));
|
self.widget.set_text(&(self.renderer)(v));
|
||||||
}
|
}
|
||||||
*self.value.borrow_mut() = val;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_text_change(&self, buffer: >k::EntryBuffer) {
|
fn handle_text_change(&self, buffer: >k::EntryBuffer) {
|
||||||
|
@ -105,6 +105,14 @@ impl<T: Clone + std::fmt::Debug + 'static> TextEntry<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_css_class(&self, class_: &str) {
|
||||||
|
self.widget.add_css_class(class_);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_css_class(&self, class_: &str) {
|
||||||
|
self.widget.remove_css_class(class_);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn widget(&self) -> gtk::Widget {
|
pub fn widget(&self) -> gtk::Widget {
|
||||||
self.widget.clone().upcast::<gtk::Widget>()
|
self.widget.clone().upcast::<gtk::Widget>()
|
||||||
}
|
}
|
||||||
|
@ -181,6 +189,45 @@ where
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn i32_field<OnUpdate>(
|
||||||
|
value: i32,
|
||||||
|
on_update: OnUpdate,
|
||||||
|
) -> TextEntry<i32>
|
||||||
|
where
|
||||||
|
OnUpdate: Fn(Option<i32>) + 'static,
|
||||||
|
{
|
||||||
|
TextEntry::new(
|
||||||
|
"0",
|
||||||
|
Some(value),
|
||||||
|
|val| format!("{}", val),
|
||||||
|
|v|
|
||||||
|
v.parse::<i32>().map_err(|_| ParseError),
|
||||||
|
on_update,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn month_field<OnUpdate>(
|
||||||
|
value: u32,
|
||||||
|
on_update: OnUpdate,
|
||||||
|
) -> TextEntry<u32>
|
||||||
|
where
|
||||||
|
OnUpdate: Fn(Option<u32>) + 'static,
|
||||||
|
{
|
||||||
|
TextEntry::new(
|
||||||
|
"0",
|
||||||
|
Some(value),
|
||||||
|
|val| format!("{}", val),
|
||||||
|
|v| {
|
||||||
|
let val = v.parse::<u32>().map_err(|_| ParseError)?;
|
||||||
|
if val == 0 || val > 12 {
|
||||||
|
return Err(ParseError);
|
||||||
|
}
|
||||||
|
Ok(val)
|
||||||
|
},
|
||||||
|
on_update,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -109,12 +109,29 @@ impl HistoricalView {
|
||||||
|
|
||||||
*s.imp().app.borrow_mut() = Some(app);
|
*s.imp().app.borrow_mut() = Some(app);
|
||||||
|
|
||||||
|
let start_date = DateField::new(interval.start);
|
||||||
|
let end_date = DateField::new(interval.end);
|
||||||
|
let search_button = gtk::Button::with_label("Search");
|
||||||
|
search_button.connect_clicked({
|
||||||
|
let s = s.clone();
|
||||||
|
let start_date = start_date.clone();
|
||||||
|
let end_date = end_date.clone();
|
||||||
|
move |_| {
|
||||||
|
let interval = DayInterval {
|
||||||
|
start: start_date.date(),
|
||||||
|
end: end_date.date(),
|
||||||
|
};
|
||||||
|
s.set_interval(interval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let date_row = gtk::Box::builder()
|
let date_row = gtk::Box::builder()
|
||||||
.orientation(gtk::Orientation::Horizontal)
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
.build();
|
.build();
|
||||||
date_row.append(&DateField::new(interval.start));
|
date_row.append(&start_date);
|
||||||
date_row.append(>k::Label::new(Some("to")));
|
date_row.append(>k::Label::new(Some("to")));
|
||||||
date_row.append(&DateField::new(interval.end));
|
date_row.append(&end_date);
|
||||||
|
date_row.append(&search_button);
|
||||||
|
|
||||||
let quick_picker = gtk::Box::builder()
|
let quick_picker = gtk::Box::builder()
|
||||||
.orientation(gtk::Orientation::Horizontal)
|
.orientation(gtk::Orientation::Horizontal)
|
||||||
|
|
Loading…
Reference in New Issue