diff --git a/CHANGELOG.md b/CHANGELOG.md index a29239f..c00dcc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ Changes to this project will be logged in this file. This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## 2.3.0 + +Robodash 2.3.0 improves the selector UI. + +### Added + +- An indication for what auton in the selector list is currently selected +- `color_hue` parameter to autons, which places a color chip next to the auton's name in the list. +- Page up / page down buttons to the selector if it's scrollable, making scrolling easier when there's many autons. +- Up / down buttons to the selector, providing an alternate way to select autons in the list. +- `rd::Selector::next_auton` and `rd::Selector::prev_auton` functions for user-defined methods to control the selector, enabling hardware buttons or dials. + ## 2.2.0 Robodash 2.2.0 provides selector enhancements. diff --git a/LICENCE b/LICENCE index ab87bd9..325112b 100644 --- a/LICENCE +++ b/LICENCE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Thurston A Yates +Copyright (c) 2025 Thurston A Yates Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 2c9b858..e441bdb 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ EXCLUDE_COLD_LIBRARIES:= # Set this to 1 to add additional rules to compile your project as a PROS library template IS_LIBRARY:=1 LIBNAME:=robodash -VERSION:=2.2.0 +VERSION:=2.3.0 # EXCLUDE_SRC_FROM_LIB= $(SRCDIR)/unpublishedfile.c # this line excludes opcontrol.c and similar files diff --git a/docs/Doxyfile b/docs/Doxyfile index 0bac641..726323f 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = Robodash # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2.2.0 +PROJECT_NUMBER = 2.3.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/docs/source/conf.py b/docs/source/conf.py index a3f7697..428abf4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,7 +7,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = "Robodash" -copyright = "2024, Thurston A Yates" +copyright = "2025, Thurston A Yates" author = "Thurston A Yates" # -- General configuration --------------------------------------------------- diff --git a/docs/source/img/alert.png b/docs/source/img/alert.png index 1f941ce..f35dbc0 100644 Binary files a/docs/source/img/alert.png and b/docs/source/img/alert.png differ diff --git a/docs/source/img/alert_button.png b/docs/source/img/alert_button.png index 610644f..a4d70d0 100644 Binary files a/docs/source/img/alert_button.png and b/docs/source/img/alert_button.png differ diff --git a/docs/source/img/selector.png b/docs/source/img/selector.png index 374fcbb..5fc6b05 100644 Binary files a/docs/source/img/selector.png and b/docs/source/img/selector.png differ diff --git a/docs/source/img/view_button.png b/docs/source/img/view_button.png index 4dc9357..1d2846b 100644 Binary files a/docs/source/img/view_button.png and b/docs/source/img/view_button.png differ diff --git a/docs/source/img/view_selector.png b/docs/source/img/view_selector.png index 3e1570a..7f7d1d0 100644 Binary files a/docs/source/img/view_selector.png and b/docs/source/img/view_selector.png differ diff --git a/include/robodash/api.h b/include/robodash/api.h index aaffe66..ccf5ee5 100644 --- a/include/robodash/api.h +++ b/include/robodash/api.h @@ -9,7 +9,7 @@ #define ROBODASH #define RD_VERSION_MAJOR 2 -#define RD_VERSION_MINOR 2 +#define RD_VERSION_MINOR 3 #define RD_VERSION_PATCH 0 #include "liblvgl/lvgl.h" diff --git a/include/robodash/impl/styles.h b/include/robodash/impl/styles.h index ebc3ad5..f317f81 100644 --- a/include/robodash/impl/styles.h +++ b/include/robodash/impl/styles.h @@ -49,6 +49,7 @@ extern void _init_style_misc(); extern lv_style_t style_list; extern lv_style_t style_list_btn; extern lv_style_t style_list_btn_pr; +extern lv_style_t style_list_btn_ch; extern void _init_style_list(); diff --git a/include/robodash/views/selector.hpp b/include/robodash/views/selector.hpp index 56d88c4..dfa8728 100644 --- a/include/robodash/views/selector.hpp +++ b/include/robodash/views/selector.hpp @@ -38,6 +38,7 @@ class Selector { std::string name; routine_action_t action; std::string img = ""; + int color_hue = -1; } routine_t; typedef std::function)> select_action_t; @@ -74,6 +75,22 @@ class Selector { */ void on_select(select_action_t callback); + /** + * @brief Select the next auton in the list + * @param wrap_around Whether to wrap around to the beginning once the last auton is reached + * + * Selects the next auton in the list for use with physical buttons such as limit switches. + */ + void next_auton(bool wrap_around = true); + + /** + * @brief Select the previous auton in the list + * @param wrap_around Whether to wrap around to the end once the first auton is reached + * + * Selects the previous auton in the list for use with physical buttons such as limit switches. + */ + void prev_auton(bool wrap_around = true); + /** * @brief Set this view to the active view */ @@ -84,7 +101,8 @@ class Selector { private: rd_view_t *view; - lv_obj_t *select_cont; + lv_obj_t *routine_list; + lv_obj_t *selected_cont; lv_obj_t *selected_label; lv_obj_t *selected_img; @@ -99,6 +117,10 @@ class Selector { void run_callbacks(); static void select_cb(lv_event_t *event); + static void up_cb(lv_event_t *event); + static void down_cb(lv_event_t *event); + static void pg_up_cb(lv_event_t *event); + static void pg_down_cb(lv_event_t *event); }; } // namespace rd \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 4cb0f9d..81e668f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,14 +6,16 @@ void best_auton() { std::cout << "Running best auton" << std::endl; } void simple_auton() { std::cout << "Running simple auton " << std::endl; } void good_auton() { std::cout << "Running good auton" << std::endl; } +void skills() { std::cout << "Running skills" << std::endl; } // ================================= Views ================================= // // Create robodash selector rd::Selector selector({ - {"Best auton", &best_auton}, - {"Simple auton", &simple_auton}, - {"Good auton", &good_auton}, + {"Best auton", &best_auton, "", 0}, + {"Simple auton", &simple_auton, "", 220}, + {"Good auton", &good_auton, "", 100}, + {"Skills", &skills}, }); // Create robodash console diff --git a/src/robodash/styles/colors.c b/src/robodash/styles/colors.c index 7de7991..9d639aa 100644 --- a/src/robodash/styles/colors.c +++ b/src/robodash/styles/colors.c @@ -1,3 +1,4 @@ +#include "liblvgl/misc/lv_color.h" #include "robodash/apix.h" // ================================= Colors ================================= // diff --git a/src/robodash/styles/list.c b/src/robodash/styles/list.c index 81f30de..1146ca7 100644 --- a/src/robodash/styles/list.c +++ b/src/robodash/styles/list.c @@ -5,6 +5,7 @@ lv_style_t style_list; lv_style_t style_list_btn; lv_style_t style_list_btn_pr; +lv_style_t style_list_btn_ch; void _init_style_list() { // List @@ -13,9 +14,7 @@ void _init_style_list() { lv_style_set_border_width(&style_list, 1); lv_style_set_border_opa(&style_list, LV_OPA_COVER); lv_style_set_bg_color(&style_list, color_bg); - lv_style_set_pad_ver(&style_list, 0); - lv_style_set_pad_hor(&style_list, 8); - lv_style_set_pad_gap(&style_list, 0); + lv_style_set_pad_hor(&style_list, 0); // List button lv_style_init(&style_list_btn); @@ -31,4 +30,9 @@ void _init_style_list() { // List button pressed lv_style_init(&style_list_btn_pr); lv_style_set_bg_color(&style_list_btn_pr, color_shade); -} \ No newline at end of file + + // List button checked + lv_style_init(&style_list_btn_ch); + lv_style_set_bg_color(&style_list_btn_ch, lv_color_darken(color_shade, 64)); + lv_style_set_transform_width(&style_list_btn_ch, 0); +} diff --git a/src/robodash/views/selector.cpp b/src/robodash/views/selector.cpp index cdc8e0a..54f984e 100644 --- a/src/robodash/views/selector.cpp +++ b/src/robodash/views/selector.cpp @@ -73,27 +73,16 @@ void rd::Selector::sd_load() { // None selected or not our selector if (strcmp(saved_name, "") == 0 || saved_selector != this->name) { - selected_routine = nullptr; return; } - for (rd::Selector::routine_t &r : routines) { - if (strcmp(r.name.c_str(), saved_name) != 0) continue; - selected_routine = &r; - run_callbacks(); - } - - if (selected_routine != nullptr) { - // Update routine label - char label_str[strlen(saved_name) + 20]; - sprintf(label_str, "Selected routine:\n%s", selected_routine->name.c_str()); - lv_label_set_text(selected_label, label_str); - lv_obj_align(selected_label, LV_ALIGN_CENTER, 120, 0); - - if (selected_routine->img.empty() || !pros::usd::is_installed()) return; - - lv_img_set_src(this->selected_img, selected_routine->img.c_str()); - lv_obj_clear_flag(this->selected_img, LV_OBJ_FLAG_HIDDEN); + // Press button for selected auton + for (int id = 0; id < lv_obj_get_child_cnt(routine_list); id++) { + lv_obj_t *list_child = lv_obj_get_child(routine_list, id); + if (list_child == nullptr) continue; + if (strcmp(lv_list_get_btn_text(routine_list, list_child), saved_name) != 0) continue; + lv_event_send(list_child, LV_EVENT_CLICKED, selected_routine); + break; } } @@ -110,6 +99,13 @@ void rd::Selector::select_cb(lv_event_t *event) { selector->run_callbacks(); + // Clear other checked buttons, make this auton's button the checked one + for (int id = 0; id < lv_obj_get_child_cnt(selector->routine_list); id++) { + lv_obj_t *list_child = lv_obj_get_child(selector->routine_list, id); + lv_obj_clear_state(list_child, LV_STATE_CHECKED); + } + lv_obj_add_state(obj, LV_STATE_CHECKED); + if (routine == nullptr) { lv_label_set_text(selector->selected_label, "No routine\nselected"); lv_obj_add_flag(selector->selected_img, LV_OBJ_FLAG_HIDDEN); @@ -132,6 +128,30 @@ void rd::Selector::select_cb(lv_event_t *event) { lv_obj_clear_flag(selector->selected_img, LV_OBJ_FLAG_HIDDEN); } +void rd::Selector::pg_up_cb(lv_event_t *event) { + rd::Selector *selector = (rd::Selector *)lv_obj_get_user_data(lv_event_get_target(event)); + lv_coord_t scroll_y = lv_obj_get_height(selector->routine_list); + lv_obj_scroll_by_bounded(selector->routine_list, 0, scroll_y, LV_ANIM_ON); +} + +void rd::Selector::pg_down_cb(lv_event_t *event) { + rd::Selector *selector = (rd::Selector *)lv_obj_get_user_data(lv_event_get_target(event)); + lv_coord_t scroll_y = lv_obj_get_height(selector->routine_list) * -1; + lv_obj_scroll_by_bounded(selector->routine_list, 0, scroll_y, LV_ANIM_ON); +} + +void rd::Selector::up_cb(lv_event_t *event) { + rd::Selector *selector = (rd::Selector *)lv_obj_get_user_data(lv_event_get_target(event)); + if (!selector) return; + selector->prev_auton(); +} + +void rd::Selector::down_cb(lv_event_t *event) { + rd::Selector *selector = (rd::Selector *)lv_obj_get_user_data(lv_event_get_target(event)); + if (!selector) return; + selector->next_auton(); +} + // ============================== Constructor ============================== // rd::Selector::Selector(std::vector autons) : Selector("Auton Selector", autons) {} @@ -146,12 +166,12 @@ rd::Selector::Selector(std::string name, std::vector new_routines) { lv_obj_set_style_bg_color(view->obj, color_bg, 0); - lv_obj_t *routine_list = lv_list_create(view->obj); + routine_list = lv_list_create(view->obj); lv_obj_set_size(routine_list, 228, 192); lv_obj_align(routine_list, LV_ALIGN_TOP_LEFT, 8, 40); lv_obj_add_style(routine_list, &style_list, 0); - lv_obj_t *selected_cont = lv_obj_create(view->obj); + selected_cont = lv_obj_create(view->obj); lv_obj_add_style(selected_cont, &style_transp, 0); lv_obj_set_layout(selected_cont, LV_LAYOUT_FLEX); lv_obj_set_size(selected_cont, 240, 240); @@ -161,20 +181,92 @@ rd::Selector::Selector(std::string name, std::vector new_routines) { ); lv_obj_set_flex_flow(selected_cont, LV_FLEX_FLOW_COLUMN); - selected_img = lv_img_create(selected_cont); - lv_obj_set_size(selected_img, 168, 168); - lv_obj_add_flag(selected_img, LV_OBJ_FLAG_HIDDEN); - selected_label = lv_label_create(selected_cont); lv_label_set_text(selected_label, "No routine\nselected"); lv_obj_add_style(selected_label, &style_text_centered, 0); lv_obj_add_style(selected_label, &style_text_medium, 0); + selected_img = lv_img_create(selected_cont); + lv_obj_set_size(selected_img, 168, 168); + lv_obj_add_flag(selected_img, LV_OBJ_FLAG_HIDDEN); + + // Routine list button cluster + lv_obj_t *list_btns = lv_obj_create(view->obj); + lv_obj_add_style(list_btns, &style_transp, 0); + lv_obj_set_size(list_btns, 32, 192); + lv_obj_align(list_btns, LV_ALIGN_TOP_LEFT, 236, 40); + lv_obj_clear_flag(list_btns, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_layout(list_btns, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(list_btns, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align( + list_btns, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER + ); + + // Up page button + lv_obj_t *pg_up_btn = lv_btn_create(list_btns); + lv_obj_add_style(pg_up_btn, &style_transp, 0); + lv_obj_set_size(pg_up_btn, 32, 32); + lv_obj_add_event_cb(pg_up_btn, &pg_up_cb, LV_EVENT_CLICKED, NULL); + lv_obj_set_user_data(pg_up_btn, this); + lv_obj_add_flag(pg_up_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_style_text_opa(pg_up_btn, 128, LV_STATE_PRESSED); + lv_obj_set_flex_grow(pg_up_btn, 1); + + lv_obj_t *pg_up_img = lv_img_create(pg_up_btn); + lv_obj_align(pg_up_img, LV_ALIGN_CENTER, 0, 0); + lv_img_set_src(pg_up_img, LV_SYMBOL_UP "\n" LV_SYMBOL_UP); + lv_obj_set_style_text_line_space(pg_up_img, -10, LV_PART_MAIN); + + // Up button + lv_obj_t *up_btn = lv_btn_create(list_btns); + lv_obj_add_style(up_btn, &style_transp, 0); + lv_obj_set_size(up_btn, 32, 32); + lv_obj_add_event_cb(up_btn, &up_cb, LV_EVENT_CLICKED, NULL); + lv_obj_set_user_data(up_btn, this); + lv_obj_set_style_text_opa(up_btn, 128, LV_STATE_PRESSED); + lv_obj_set_flex_grow(up_btn, 1); + + lv_obj_t *up_img = lv_img_create(up_btn); + lv_obj_align(up_img, LV_ALIGN_CENTER, 0, 0); + lv_img_set_src(up_img, LV_SYMBOL_UP); + + // Down button + lv_obj_t *down_btn = lv_btn_create(list_btns); + lv_obj_add_style(down_btn, &style_transp, 0); + lv_obj_set_size(down_btn, 32, 32); + lv_obj_add_event_cb(down_btn, &down_cb, LV_EVENT_CLICKED, NULL); + lv_obj_set_user_data(down_btn, this); + lv_obj_set_style_text_opa(down_btn, 128, LV_STATE_PRESSED); + lv_obj_set_flex_grow(down_btn, 1); + + lv_obj_t *down_img = lv_img_create(down_btn); + lv_obj_align(down_img, LV_ALIGN_CENTER, 0, 0); + lv_img_set_src(down_img, LV_SYMBOL_DOWN); + + // Down page button + lv_obj_t *pg_down_btn = lv_btn_create(list_btns); + lv_obj_add_style(pg_down_btn, &style_transp, 0); + lv_obj_set_size(pg_down_btn, 32, 32); + lv_obj_add_event_cb(pg_down_btn, &pg_down_cb, LV_EVENT_CLICKED, NULL); + lv_obj_set_user_data(pg_down_btn, this); + lv_obj_add_flag(pg_down_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_style_text_opa(pg_down_btn, 128, LV_STATE_PRESSED); + lv_obj_set_flex_grow(pg_down_btn, 1); + + lv_obj_t *pg_down_img = lv_img_create(pg_down_btn); + lv_obj_align(pg_down_img, LV_ALIGN_CENTER, 0, 0); + lv_img_set_src(pg_down_img, LV_SYMBOL_DOWN "\n" LV_SYMBOL_DOWN); + lv_obj_set_style_text_line_space(pg_down_img, -10, LV_PART_MAIN); + + // Nothing auton lv_obj_t *nothing_btn = lv_list_add_btn(routine_list, NULL, "Nothing"); - lv_obj_add_event_cb(nothing_btn, &select_cb, LV_EVENT_PRESSED, nullptr); + lv_obj_add_event_cb(nothing_btn, &select_cb, LV_EVENT_CLICKED, nullptr); lv_obj_set_user_data(nothing_btn, this); lv_obj_add_style(nothing_btn, &style_list_btn, 0); lv_obj_add_style(nothing_btn, &style_list_btn_pr, LV_STATE_PRESSED); + lv_obj_add_style(nothing_btn, &style_list_btn_ch, LV_STATE_CHECKED); + lv_obj_set_style_transform_width(nothing_btn, -8, 0); + lv_obj_add_state(nothing_btn, LV_STATE_CHECKED); lv_obj_t *title = lv_label_create(view->obj); lv_label_set_text(title, "Select autonomous routine"); @@ -182,11 +274,10 @@ rd::Selector::Selector(std::string name, std::vector new_routines) { lv_obj_align(title, LV_ALIGN_TOP_LEFT, 8, 12); if (pros::usd::is_installed()) { - lv_obj_t *save_icon = lv_label_create(view->obj); + lv_obj_t *save_icon = lv_label_create(list_btns); lv_obj_add_style(save_icon, &style_text_medium, 0); lv_obj_add_style(save_icon, &style_text_centered, 0); lv_label_set_text(save_icon, LV_SYMBOL_SD_CARD "\nSD"); - lv_obj_align(save_icon, LV_ALIGN_BOTTOM_MID, 16, -8); } // ----------------------------- Add autons ----------------------------- // @@ -201,10 +292,31 @@ rd::Selector::Selector(std::string name, std::vector new_routines) { for (routine_t &routine : routines) { lv_obj_t *new_btn = lv_list_add_btn(routine_list, NULL, routine.name.c_str()); + lv_obj_add_style(new_btn, &style_list_btn, 0); lv_obj_add_style(new_btn, &style_list_btn_pr, LV_STATE_PRESSED); + lv_obj_add_style(new_btn, &style_list_btn_ch, LV_STATE_CHECKED); + lv_obj_set_style_transform_width(new_btn, -8, 0); lv_obj_set_user_data(new_btn, this); - lv_obj_add_event_cb(new_btn, &select_cb, LV_EVENT_PRESSED, &routine); + lv_obj_add_event_cb(new_btn, &select_cb, LV_EVENT_CLICKED, &routine); + + if (routine.color_hue > -1) { + lv_obj_t *color_chip = lv_obj_create(new_btn); + lv_obj_set_size(color_chip, 16, 16); + lv_obj_set_style_bg_color( + color_chip, lv_color_hsv_to_rgb(routine.color_hue, 75, 80), 0 + ); + lv_obj_set_style_border_opa(color_chip, LV_OPA_0, 0); + lv_obj_set_style_radius(color_chip, 4, 0); + lv_obj_align(color_chip, LV_ALIGN_RIGHT_MID, -4, 8); + lv_obj_clear_flag(color_chip, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(color_chip, LV_OBJ_FLAG_CLICKABLE); + } + } + + if (routines.size() > 3) { + lv_obj_clear_flag(pg_down_btn, LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(pg_up_btn, LV_OBJ_FLAG_HIDDEN); } if (pros::usd::is_installed()) sd_load(); @@ -212,6 +324,50 @@ rd::Selector::Selector(std::string name, std::vector new_routines) { // ============================= Other Methods ============================= // +void rd::Selector::next_auton(bool wrap_around) { + for (int id = 0; id < lv_obj_get_child_cnt(routine_list); id++) { + lv_obj_t *list_child = lv_obj_get_child(routine_list, id); + if (!lv_obj_has_state(list_child, LV_STATE_CHECKED)) continue; + + if (id == lv_obj_get_child_cnt(routine_list) - 1) { + if (!wrap_around) return; + // nullptr because the "Nothing" button is always first, and doesnt have user data + lv_event_send(lv_obj_get_child(routine_list, 0), LV_EVENT_CLICKED, nullptr); + } else { + lv_obj_t *next_child = lv_obj_get_child(routine_list, id + 1); + if (next_child == nullptr) return; + lv_event_send(next_child, LV_EVENT_CLICKED, &routines[id + 1]); + } + + return; + } +} + +void rd::Selector::prev_auton(bool wrap_around) { + lv_obj_t *prev_child = nullptr; + int child_count = lv_obj_get_child_cnt(routine_list); + for (int id = 0; id < child_count; id++) { + lv_obj_t *list_child = lv_obj_get_child(routine_list, id); + if (!lv_obj_has_state(list_child, LV_STATE_CHECKED)) { + prev_child = list_child; + continue; + }; + + if (id == 0) { + if (!wrap_around) return; + lv_event_send( + lv_obj_get_child(routine_list, child_count - 1), LV_EVENT_CLICKED, + &routines[child_count - 1] + ); + } else { + if (prev_child == nullptr) return; + lv_event_send(prev_child, LV_EVENT_CLICKED, &routines[id - 1]); + } + + return; + } +} + void rd::Selector::run_callbacks() { for (select_action_t callback : this->select_callbacks) { if (this->selected_routine == nullptr) {