mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Setup improvements
This commit is contained in:
parent
bb68fba13d
commit
0ab30d4b6e
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
## Higher Priority
|
## Higher Priority
|
||||||
|
|
||||||
|
- Remove screenshot app and replace with tt.py + screenshot.py: "tt screenshot [filepath]". if filepath isn't specified, save as screenshot-[yyyy-mm-dd].png
|
||||||
- BlueTooth as radio backend for MeshCore/Meshtastic?
|
- BlueTooth as radio backend for MeshCore/Meshtastic?
|
||||||
- ESP-NOW as radio backend. Use it for MeshCore/Meshtastic?
|
- ESP-NOW as radio backend. Use it for MeshCore/Meshtastic?
|
||||||
- Find out why the first build of an app with a clean build cache folder always fails
|
- Find out why the first build of an app with a clean build cache folder always fails
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#include <tactility/concurrent/task_event_group.h>
|
#include <tactility/concurrent/task_event_group.h>
|
||||||
@ -20,6 +21,9 @@ extern "C" {
|
|||||||
|
|
||||||
#define LOCATION_PROVIDER_ID_MAX_LENGTH 15
|
#define LOCATION_PROVIDER_ID_MAX_LENGTH 15
|
||||||
|
|
||||||
|
/** Maximum length of an IANA time zone name from location_lookup_time_zone(), excluding the NUL terminator */
|
||||||
|
#define LOCATION_TIME_ZONE_MAX_LENGTH 63
|
||||||
|
|
||||||
/** Horizontal accuracy in meters of a fix with LOCATION_PRECISION_APPROXIMATE */
|
/** Horizontal accuracy in meters of a fix with LOCATION_PRECISION_APPROXIMATE */
|
||||||
#define LOCATION_APPROXIMATE_ACCURACY_METERS 3000.0f
|
#define LOCATION_APPROXIMATE_ACCURACY_METERS 3000.0f
|
||||||
|
|
||||||
@ -115,6 +119,18 @@ error_t location_unsubscribe(struct LocationSubscription* sub);
|
|||||||
/** @return true when location services are enabled and there is at least one subscription */
|
/** @return true when location services are enabled and there is at least one subscription */
|
||||||
bool location_is_in_use(void);
|
bool location_is_in_use(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up the IANA time zone name (e.g. "Europe/Brussels") of the device's public IP address with ip-api.com.
|
||||||
|
* Blocks for up to about 10 seconds, so call it from an app's own task.
|
||||||
|
* @param[out] out_name the time zone name
|
||||||
|
* @param[in] out_size the size of @a out_name, at least LOCATION_TIME_ZONE_MAX_LENGTH + 1 for any name
|
||||||
|
* @retval ERROR_INVALID_STATE location services are disabled
|
||||||
|
* @retval ERROR_RESOURCE the request failed, e.g. without network
|
||||||
|
* @retval ERROR_NOT_FOUND ip-api.com returned no time zone
|
||||||
|
* @retval ERROR_BUFFER_OVERFLOW the name doesn't fit in @a out_name
|
||||||
|
*/
|
||||||
|
error_t location_lookup_time_zone(char* out_name, size_t out_size);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -31,10 +31,12 @@ struct IpApiResponse {
|
|||||||
char message[IP_API_MESSAGE_MAX_LENGTH + 1];
|
char message[IP_API_MESSAGE_MAX_LENGTH + 1];
|
||||||
double latitude;
|
double latitude;
|
||||||
double longitude;
|
double longitude;
|
||||||
|
/** IANA time zone name, empty when unknown */
|
||||||
|
char time_zone[LOCATION_TIME_ZONE_MAX_LENGTH + 1];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse an HTTP response of the newline separated ip-api endpoint, requested with fields "status,message,lat,lon"
|
* Parse an HTTP response of the newline separated ip-api endpoint, requested with fields "status,message,lat,lon,timezone"
|
||||||
* @param[in] response the NUL-terminated response, including the status line and headers
|
* @param[in] response the NUL-terminated response, including the status line and headers
|
||||||
* @return false when the response is malformed
|
* @return false when the response is malformed
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -32,7 +32,7 @@ namespace {
|
|||||||
|
|
||||||
// The free ip-api endpoint only supports plain HTTP
|
// The free ip-api endpoint only supports plain HTTP
|
||||||
constexpr auto* API_HOST = "ip-api.com";
|
constexpr auto* API_HOST = "ip-api.com";
|
||||||
constexpr auto* API_PATH = "/line/?fields=status,message,lat,lon";
|
constexpr auto* API_PATH = "/line/?fields=status,message,lat,lon,timezone";
|
||||||
constexpr uint64_t RETRY_INTERVAL_MS = 60 * 1000;
|
constexpr uint64_t RETRY_INTERVAL_MS = 60 * 1000;
|
||||||
// Bounds the request rate when the network connects repeatedly
|
// Bounds the request rate when the network connects repeatedly
|
||||||
constexpr uint64_t MIN_INTERVAL_MS = 10 * 1000;
|
constexpr uint64_t MIN_INTERVAL_MS = 10 * 1000;
|
||||||
@ -337,6 +337,12 @@ bool waitStopped(void* context, TickType_t timeout) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The characters of IANA time zone names, e.g. "America/Port-au-Prince" or "Etc/GMT+5" */
|
||||||
|
bool isTimeZoneCharacter(char character) {
|
||||||
|
return (character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') ||
|
||||||
|
character == '_' || character == '+' || character == '-' || character == '/';
|
||||||
|
}
|
||||||
|
|
||||||
std::string_view nextLine(std::string_view& text) {
|
std::string_view nextLine(std::string_view& text) {
|
||||||
const size_t end = text.find('\n');
|
const size_t end = text.find('\n');
|
||||||
std::string_view line = text.substr(0, end);
|
std::string_view line = text.substr(0, end);
|
||||||
@ -369,6 +375,27 @@ void ip_provider_untrack_network(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error_t location_lookup_time_zone(char* out_name, size_t out_size) {
|
||||||
|
if (!location_is_enabled()) {
|
||||||
|
return ERROR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
tt::std::string response(RESPONSE_MAX_LENGTH, '\0');
|
||||||
|
IpApiResponse parsed;
|
||||||
|
if (!httpGet(API_HOST, API_PATH, response.data(), response.size()) || !ip_api_parse_response(response.c_str(), &parsed) ||
|
||||||
|
parsed.status_code != 200 || !parsed.success) {
|
||||||
|
return ERROR_RESOURCE;
|
||||||
|
}
|
||||||
|
const size_t length = strlen(parsed.time_zone);
|
||||||
|
if (length == 0) {
|
||||||
|
return ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (length >= out_size) {
|
||||||
|
return ERROR_BUFFER_OVERFLOW;
|
||||||
|
}
|
||||||
|
memcpy(out_name, parsed.time_zone, length + 1);
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
bool ip_api_parse_response(const char* response, IpApiResponse* out) {
|
bool ip_api_parse_response(const char* response, IpApiResponse* out) {
|
||||||
*out = IpApiResponse {
|
*out = IpApiResponse {
|
||||||
.status_code = 0,
|
.status_code = 0,
|
||||||
@ -378,6 +405,7 @@ bool ip_api_parse_response(const char* response, IpApiResponse* out) {
|
|||||||
.message = {},
|
.message = {},
|
||||||
.latitude = NAN,
|
.latitude = NAN,
|
||||||
.longitude = NAN,
|
.longitude = NAN,
|
||||||
|
.time_zone = {},
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string_view text = response;
|
std::string_view text = response;
|
||||||
@ -427,6 +455,12 @@ bool ip_api_parse_response(const char* response, IpApiResponse* out) {
|
|||||||
std::fabs(out->latitude) > 90.0 || std::fabs(out->longitude) > 180.0) {
|
std::fabs(out->latitude) > 90.0 || std::fabs(out->longitude) > 180.0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const std::string_view timeZone = nextLine(text);
|
||||||
|
if (timeZone.size() > LOCATION_TIME_ZONE_MAX_LENGTH || !std::ranges::all_of(timeZone, isTimeZoneCharacter)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(out->time_zone, timeZone.data(), timeZone.size());
|
||||||
|
out->time_zone[timeZone.size()] = '\0';
|
||||||
out->success = true;
|
out->success = true;
|
||||||
} else if (status == "fail") {
|
} else if (status == "fail") {
|
||||||
const std::string_view message = nextLine(text).substr(0, IP_API_MESSAGE_MAX_LENGTH);
|
const std::string_view message = nextLine(text).substr(0, IP_API_MESSAGE_MAX_LENGTH);
|
||||||
|
|||||||
@ -51,6 +51,7 @@ static const ModuleSymbol location_module_symbols[] = {
|
|||||||
DEFINE_MODULE_SYMBOL(location_subscribe),
|
DEFINE_MODULE_SYMBOL(location_subscribe),
|
||||||
DEFINE_MODULE_SYMBOL(location_unsubscribe),
|
DEFINE_MODULE_SYMBOL(location_unsubscribe),
|
||||||
DEFINE_MODULE_SYMBOL(location_is_in_use),
|
DEFINE_MODULE_SYMBOL(location_is_in_use),
|
||||||
|
DEFINE_MODULE_SYMBOL(location_lookup_time_zone),
|
||||||
MODULE_SYMBOL_TERMINATOR
|
MODULE_SYMBOL_TERMINATOR
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
#include <location/ip_provider.h>
|
#include <location/ip_provider.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
TEST_CASE("ip_api_parse_response parses a successful lookup") {
|
TEST_CASE("ip_api_parse_response parses a successful lookup") {
|
||||||
const char* response =
|
const char* response =
|
||||||
@ -13,17 +14,34 @@ TEST_CASE("ip_api_parse_response parses a successful lookup") {
|
|||||||
"\r\n"
|
"\r\n"
|
||||||
"success\n"
|
"success\n"
|
||||||
"39.03\n"
|
"39.03\n"
|
||||||
"-77.5\n";
|
"-77.5\n"
|
||||||
|
"America/New_York\n";
|
||||||
IpApiResponse parsed;
|
IpApiResponse parsed;
|
||||||
REQUIRE(ip_api_parse_response(response, &parsed));
|
REQUIRE(ip_api_parse_response(response, &parsed));
|
||||||
CHECK(parsed.status_code == 200);
|
CHECK(parsed.status_code == 200);
|
||||||
CHECK(parsed.success);
|
CHECK(parsed.success);
|
||||||
CHECK(parsed.latitude == doctest::Approx(39.03));
|
CHECK(parsed.latitude == doctest::Approx(39.03));
|
||||||
CHECK(parsed.longitude == doctest::Approx(-77.5));
|
CHECK(parsed.longitude == doctest::Approx(-77.5));
|
||||||
|
CHECK(strcmp(parsed.time_zone, "America/New_York") == 0);
|
||||||
CHECK(parsed.rate_remaining == 44);
|
CHECK(parsed.rate_remaining == 44);
|
||||||
CHECK(parsed.rate_reset_seconds == 60);
|
CHECK(parsed.rate_reset_seconds == 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ip_api_parse_response accepts a lookup without a time zone") {
|
||||||
|
const char* response = "HTTP/1.1 200 OK\r\n\r\nsuccess\n52.37\n4.89\n";
|
||||||
|
IpApiResponse parsed;
|
||||||
|
REQUIRE(ip_api_parse_response(response, &parsed));
|
||||||
|
CHECK(parsed.success);
|
||||||
|
CHECK(strcmp(parsed.time_zone, "") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ip_api_parse_response rejects an invalid time zone") {
|
||||||
|
IpApiResponse parsed;
|
||||||
|
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nsuccess\n52.37\n4.89\n../etc/passwd x\n", &parsed));
|
||||||
|
const std::string tooLong = "HTTP/1.1 200 OK\r\n\r\nsuccess\n52.37\n4.89\n" + std::string(LOCATION_TIME_ZONE_MAX_LENGTH + 1, 'A') + "\n";
|
||||||
|
CHECK_FALSE(ip_api_parse_response(tooLong.c_str(), &parsed));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("ip_api_parse_response parses a failed lookup") {
|
TEST_CASE("ip_api_parse_response parses a failed lookup") {
|
||||||
const char* response = "HTTP/1.1 200 OK\r\nx-rl: 0\r\nx-ttl: 12\r\n\r\nfail\nprivate range\n";
|
const char* response = "HTTP/1.1 200 OK\r\nx-rl: 0\r\nx-ttl: 12\r\n\r\nfail\nprivate range\n";
|
||||||
IpApiResponse parsed;
|
IpApiResponse parsed;
|
||||||
|
|||||||
36
Modules/lvgl-module/include/lvgl/widgets/step_indicator.h
Normal file
36
Modules/lvgl-module/include/lvgl/widgets/step_indicator.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <lvgl.h>
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates a step indicator: a horizontal row of line segments, one per step, that shows the progress through a number of steps.
|
||||||
|
* The segments share the indicator's width. The theme determines their thickness, the gap between them, and the colours of finished and unfinished steps.
|
||||||
|
* @param[in] parent the parent object
|
||||||
|
* @return the created step indicator
|
||||||
|
*/
|
||||||
|
lv_obj_t* lvgl_step_indicator_create(lv_obj_t* parent);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the number of steps, which is the number of segments.
|
||||||
|
* @param[in] obj the step indicator
|
||||||
|
* @param[in] count the number of steps
|
||||||
|
*/
|
||||||
|
void lvgl_step_indicator_set_step_count(lv_obj_t* obj, uint32_t count);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets how many steps are finished: the segments of the first @p finished_count steps show as finished.
|
||||||
|
* @param[in] obj the step indicator
|
||||||
|
* @param[in] finished_count the number of finished steps
|
||||||
|
*/
|
||||||
|
void lvgl_step_indicator_set_finished_count(lv_obj_t* obj, uint32_t finished_count);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@ -22,6 +22,7 @@
|
|||||||
#include <lvgl/widgets/icon_button.h>
|
#include <lvgl/widgets/icon_button.h>
|
||||||
#include <lvgl/widgets/list.h>
|
#include <lvgl/widgets/list.h>
|
||||||
#include <lvgl/widgets/page_indicator.h>
|
#include <lvgl/widgets/page_indicator.h>
|
||||||
|
#include <lvgl/widgets/step_indicator.h>
|
||||||
#include <lvgl/widgets/spinner.h>
|
#include <lvgl/widgets/spinner.h>
|
||||||
#include <lvgl/widgets/statusbar.h>
|
#include <lvgl/widgets/statusbar.h>
|
||||||
#include <lvgl/widgets/text_button.h>
|
#include <lvgl/widgets/text_button.h>
|
||||||
@ -130,6 +131,10 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
|||||||
DEFINE_MODULE_SYMBOL(lvgl_page_indicator_create),
|
DEFINE_MODULE_SYMBOL(lvgl_page_indicator_create),
|
||||||
DEFINE_MODULE_SYMBOL(lvgl_page_indicator_set_page_count),
|
DEFINE_MODULE_SYMBOL(lvgl_page_indicator_set_page_count),
|
||||||
DEFINE_MODULE_SYMBOL(lvgl_page_indicator_set_page),
|
DEFINE_MODULE_SYMBOL(lvgl_page_indicator_set_page),
|
||||||
|
// lvgl_step_indicator
|
||||||
|
DEFINE_MODULE_SYMBOL(lvgl_step_indicator_create),
|
||||||
|
DEFINE_MODULE_SYMBOL(lvgl_step_indicator_set_step_count),
|
||||||
|
DEFINE_MODULE_SYMBOL(lvgl_step_indicator_set_finished_count),
|
||||||
// lvgl_card
|
// lvgl_card
|
||||||
DEFINE_MODULE_SYMBOL(lvgl_card_create),
|
DEFINE_MODULE_SYMBOL(lvgl_card_create),
|
||||||
// lvgl_chip
|
// lvgl_chip
|
||||||
|
|||||||
@ -19,6 +19,8 @@ extern const lv_obj_class_t lvgl_chip_class;
|
|||||||
extern const lv_obj_class_t lvgl_badge_class;
|
extern const lv_obj_class_t lvgl_badge_class;
|
||||||
extern const lv_obj_class_t lvgl_page_indicator_class;
|
extern const lv_obj_class_t lvgl_page_indicator_class;
|
||||||
extern const lv_obj_class_t lvgl_page_indicator_dot_class;
|
extern const lv_obj_class_t lvgl_page_indicator_dot_class;
|
||||||
|
extern const lv_obj_class_t lvgl_step_indicator_class;
|
||||||
|
extern const lv_obj_class_t lvgl_step_indicator_segment_class;
|
||||||
extern const lv_obj_class_t lvgl_list_class;
|
extern const lv_obj_class_t lvgl_list_class;
|
||||||
extern const lv_obj_class_t lvgl_list_text_class;
|
extern const lv_obj_class_t lvgl_list_text_class;
|
||||||
extern const lv_obj_class_t lvgl_list_button_class;
|
extern const lv_obj_class_t lvgl_list_button_class;
|
||||||
@ -150,6 +152,11 @@ extern const lv_obj_class_t lvgl_text_button_class;
|
|||||||
#define PAGE_INDICATOR_COLOR COLOR_ON_SURFACE_VARIANT
|
#define PAGE_INDICATOR_COLOR COLOR_ON_SURFACE_VARIANT
|
||||||
#define PAGE_INDICATOR_DOT_SIZE (theme->config.is_compact ? 6 : LV_DPX_CALC(theme->disp_dpi, 8))
|
#define PAGE_INDICATOR_DOT_SIZE (theme->config.is_compact ? 6 : LV_DPX_CALC(theme->disp_dpi, 8))
|
||||||
#define PAGE_INDICATOR_GAP (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
#define PAGE_INDICATOR_GAP (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||||
|
#define STEP_INDICATOR_FINISHED_COLOR COLOR_SECONDARY
|
||||||
|
#define STEP_INDICATOR_UNFINISHED_COLOR (lv_color_mix(lv_color_black(), COLOR_SECONDARY, LV_OPA_50))
|
||||||
|
#define STEP_INDICATOR_THICKNESS (LV_DPX_CALC(theme->disp_dpi, 6))
|
||||||
|
#define STEP_INDICATOR_MARGIN (LV_DPX_CALC(theme->disp_dpi, 6))
|
||||||
|
#define STEP_INDICATOR_GAP STEP_INDICATOR_MARGIN
|
||||||
#define BADGE_COLOR COLOR_SECONDARY
|
#define BADGE_COLOR COLOR_SECONDARY
|
||||||
#define BADGE_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
#define BADGE_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||||
#define SCROLLBAR_COLOR COLOR_OUTLINE
|
#define SCROLLBAR_COLOR COLOR_OUTLINE
|
||||||
@ -266,7 +273,7 @@ extern const lv_obj_class_t lvgl_text_button_class;
|
|||||||
#define KEY_CHECKED_TEXT_COLOR COLOR_ON_SECONDARY_CONTAINER
|
#define KEY_CHECKED_TEXT_COLOR COLOR_ON_SECONDARY_CONTAINER
|
||||||
#define KEY_SELECTED_COLOR COLOR_PRIMARY
|
#define KEY_SELECTED_COLOR COLOR_PRIMARY
|
||||||
#define KEY_SELECTED_TEXT_COLOR COLOR_ON_PRIMARY
|
#define KEY_SELECTED_TEXT_COLOR COLOR_ON_PRIMARY
|
||||||
#define LIST_COLOR COLOR_SURFACE
|
#define LIST_COLOR CARD_COLOR
|
||||||
#define LIST_PAD DENSITY_LIST_PAD
|
#define LIST_PAD DENSITY_LIST_PAD
|
||||||
#define LIST_ITEM_COLOR COLOR_SURFACE
|
#define LIST_ITEM_COLOR COLOR_SURFACE
|
||||||
#define LIST_ITEM_OPACITY LV_OPA_TRANSP
|
#define LIST_ITEM_OPACITY LV_OPA_TRANSP
|
||||||
@ -276,7 +283,9 @@ extern const lv_obj_class_t lvgl_text_button_class;
|
|||||||
#define LIST_ITEM_GAP (theme->config.is_compact ? 3 : SPACE_4)
|
#define LIST_ITEM_GAP (theme->config.is_compact ? 3 : SPACE_4)
|
||||||
#define LIST_ITEM_FOCUSED_COLOR COLOR_SECONDARY_CONTAINER
|
#define LIST_ITEM_FOCUSED_COLOR COLOR_SECONDARY_CONTAINER
|
||||||
#define LIST_ITEM_FOCUSED_TEXT_COLOR COLOR_ON_SECONDARY_CONTAINER
|
#define LIST_ITEM_FOCUSED_TEXT_COLOR COLOR_ON_SECONDARY_CONTAINER
|
||||||
#define LIST_DIVIDER_COLOR COLOR_OUTLINE_VARIANT
|
#define LIST_ITEM_SELECTED_COLOR COLOR_SECONDARY_CONTAINER
|
||||||
|
#define LIST_ITEM_SELECTED_TEXT_COLOR COLOR_ON_SECONDARY_CONTAINER
|
||||||
|
#define LIST_DIVIDER_COLOR (lv_color_mix(COLOR_SECONDARY, LIST_COLOR, 89))
|
||||||
#define LIST_DIVIDER_WIDTH DENSITY_BORDER
|
#define LIST_DIVIDER_WIDTH DENSITY_BORDER
|
||||||
#define LIST_SUBHEADER_TEXT_COLOR COLOR_ON_SURFACE_VARIANT
|
#define LIST_SUBHEADER_TEXT_COLOR COLOR_ON_SURFACE_VARIANT
|
||||||
#define TABS_COLOR CARD_COLOR
|
#define TABS_COLOR CARD_COLOR
|
||||||
@ -357,6 +366,9 @@ typedef struct {
|
|||||||
#endif
|
#endif
|
||||||
lv_style_t page_indicator_dot;
|
lv_style_t page_indicator_dot;
|
||||||
lv_style_t page_indicator_dot_current;
|
lv_style_t page_indicator_dot_current;
|
||||||
|
lv_style_t step_indicator;
|
||||||
|
lv_style_t step_indicator_segment;
|
||||||
|
lv_style_t step_indicator_segment_finished;
|
||||||
lv_style_t badge;
|
lv_style_t badge;
|
||||||
lv_style_t chip;
|
lv_style_t chip;
|
||||||
lv_style_t chip_selected;
|
lv_style_t chip_selected;
|
||||||
@ -500,6 +512,7 @@ typedef struct {
|
|||||||
lv_style_t list;
|
lv_style_t list;
|
||||||
lv_style_t list_item;
|
lv_style_t list_item;
|
||||||
lv_style_t list_item_focused;
|
lv_style_t list_item_focused;
|
||||||
|
lv_style_t list_item_selected;
|
||||||
lv_style_t list_subheader;
|
lv_style_t list_subheader;
|
||||||
#if LV_USE_TABVIEW
|
#if LV_USE_TABVIEW
|
||||||
lv_style_t tabs;
|
lv_style_t tabs;
|
||||||
@ -798,6 +811,22 @@ static void style_init(my_theme_t * theme)
|
|||||||
style_init_reset(&theme->styles.page_indicator_dot_current);
|
style_init_reset(&theme->styles.page_indicator_dot_current);
|
||||||
lv_style_set_bg_opa(&theme->styles.page_indicator_dot_current, LV_OPA_COVER);
|
lv_style_set_bg_opa(&theme->styles.page_indicator_dot_current, LV_OPA_COVER);
|
||||||
|
|
||||||
|
style_init_reset(&theme->styles.step_indicator);
|
||||||
|
lv_style_set_bg_opa(&theme->styles.step_indicator, LV_OPA_TRANSP);
|
||||||
|
lv_style_set_border_width(&theme->styles.step_indicator, 0);
|
||||||
|
lv_style_set_pad_all(&theme->styles.step_indicator, STEP_INDICATOR_MARGIN);
|
||||||
|
lv_style_set_pad_gap(&theme->styles.step_indicator, STEP_INDICATOR_GAP);
|
||||||
|
|
||||||
|
style_init_reset(&theme->styles.step_indicator_segment);
|
||||||
|
lv_style_set_height(&theme->styles.step_indicator_segment, STEP_INDICATOR_THICKNESS);
|
||||||
|
lv_style_set_radius(&theme->styles.step_indicator_segment, LV_RADIUS_CIRCLE);
|
||||||
|
lv_style_set_border_width(&theme->styles.step_indicator_segment, 0);
|
||||||
|
lv_style_set_bg_color(&theme->styles.step_indicator_segment, STEP_INDICATOR_UNFINISHED_COLOR);
|
||||||
|
lv_style_set_bg_opa(&theme->styles.step_indicator_segment, LV_OPA_COVER);
|
||||||
|
|
||||||
|
style_init_reset(&theme->styles.step_indicator_segment_finished);
|
||||||
|
lv_style_set_bg_color(&theme->styles.step_indicator_segment_finished, STEP_INDICATOR_FINISHED_COLOR);
|
||||||
|
|
||||||
style_init_reset(&theme->styles.badge);
|
style_init_reset(&theme->styles.badge);
|
||||||
lv_style_set_width(&theme->styles.badge, BADGE_SIZE);
|
lv_style_set_width(&theme->styles.badge, BADGE_SIZE);
|
||||||
lv_style_set_height(&theme->styles.badge, BADGE_SIZE);
|
lv_style_set_height(&theme->styles.badge, BADGE_SIZE);
|
||||||
@ -1210,7 +1239,7 @@ static void style_init(my_theme_t * theme)
|
|||||||
lv_style_set_bg_color(&theme->styles.list, LIST_COLOR);
|
lv_style_set_bg_color(&theme->styles.list, LIST_COLOR);
|
||||||
lv_style_set_bg_opa(&theme->styles.list, LV_OPA_COVER);
|
lv_style_set_bg_opa(&theme->styles.list, LV_OPA_COVER);
|
||||||
lv_style_set_pad_hor(&theme->styles.list, 0);
|
lv_style_set_pad_hor(&theme->styles.list, 0);
|
||||||
lv_style_set_pad_ver(&theme->styles.list, LIST_PAD);
|
lv_style_set_pad_ver(&theme->styles.list, 0);
|
||||||
lv_style_set_pad_gap(&theme->styles.list, 0);
|
lv_style_set_pad_gap(&theme->styles.list, 0);
|
||||||
lv_style_set_clip_corner(&theme->styles.list, true);
|
lv_style_set_clip_corner(&theme->styles.list, true);
|
||||||
|
|
||||||
@ -1230,6 +1259,11 @@ static void style_init(my_theme_t * theme)
|
|||||||
lv_style_set_bg_opa(&theme->styles.list_item_focused, LV_OPA_COVER);
|
lv_style_set_bg_opa(&theme->styles.list_item_focused, LV_OPA_COVER);
|
||||||
lv_style_set_text_color(&theme->styles.list_item_focused, LIST_ITEM_FOCUSED_TEXT_COLOR);
|
lv_style_set_text_color(&theme->styles.list_item_focused, LIST_ITEM_FOCUSED_TEXT_COLOR);
|
||||||
|
|
||||||
|
style_init_reset(&theme->styles.list_item_selected);
|
||||||
|
lv_style_set_bg_color(&theme->styles.list_item_selected, LIST_ITEM_SELECTED_COLOR);
|
||||||
|
lv_style_set_bg_opa(&theme->styles.list_item_selected, LV_OPA_COVER);
|
||||||
|
lv_style_set_text_color(&theme->styles.list_item_selected, LIST_ITEM_SELECTED_TEXT_COLOR);
|
||||||
|
|
||||||
style_init_reset(&theme->styles.list_subheader);
|
style_init_reset(&theme->styles.list_subheader);
|
||||||
lv_style_set_text_color(&theme->styles.list_subheader, LIST_SUBHEADER_TEXT_COLOR);
|
lv_style_set_text_color(&theme->styles.list_subheader, LIST_SUBHEADER_TEXT_COLOR);
|
||||||
lv_style_set_text_font(&theme->styles.list_subheader, theme->config.font_small);
|
lv_style_set_text_font(&theme->styles.list_subheader, theme->config.font_small);
|
||||||
@ -1935,6 +1969,17 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
|||||||
lv_obj_add_style(obj, &theme->styles.page_indicator_dot_current, LV_STATE_CHECKED);
|
lv_obj_add_style(obj, &theme->styles.page_indicator_dot_current, LV_STATE_CHECKED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(lv_obj_check_type(obj, &lvgl_step_indicator_class)) {
|
||||||
|
/* step_indicator */
|
||||||
|
lv_obj_add_style(obj, &theme->styles.step_indicator, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(lv_obj_check_type(obj, &lvgl_step_indicator_segment_class)) {
|
||||||
|
/* step_indicator_segment */
|
||||||
|
lv_obj_add_style(obj, &theme->styles.step_indicator_segment, 0);
|
||||||
|
lv_obj_add_style(obj, &theme->styles.step_indicator_segment_finished, LV_STATE_CHECKED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(lv_obj_check_type(obj, &lvgl_badge_class)) {
|
if(lv_obj_check_type(obj, &lvgl_badge_class)) {
|
||||||
/* badge */
|
/* badge */
|
||||||
lv_obj_add_style(obj, &theme->styles.badge, 0);
|
lv_obj_add_style(obj, &theme->styles.badge, 0);
|
||||||
@ -2150,6 +2195,7 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
|||||||
if(lv_obj_check_type(obj, &lvgl_list_button_class)) {
|
if(lv_obj_check_type(obj, &lvgl_list_button_class)) {
|
||||||
/* flex_list_button */
|
/* flex_list_button */
|
||||||
lv_obj_add_style(obj, &theme->styles.list_item, 0);
|
lv_obj_add_style(obj, &theme->styles.list_item, 0);
|
||||||
|
lv_obj_add_style(obj, &theme->styles.list_item_selected, LV_STATE_CHECKED);
|
||||||
lv_obj_add_style(obj, &theme->styles.transition_exit, 0);
|
lv_obj_add_style(obj, &theme->styles.transition_exit, 0);
|
||||||
lv_obj_add_style(obj, &theme->styles.state_pressed, LV_STATE_PRESSED);
|
lv_obj_add_style(obj, &theme->styles.state_pressed, LV_STATE_PRESSED);
|
||||||
lv_obj_add_style(obj, &theme->styles.transition_enter, LV_STATE_PRESSED);
|
lv_obj_add_style(obj, &theme->styles.transition_enter, LV_STATE_PRESSED);
|
||||||
|
|||||||
@ -19,6 +19,8 @@ extern const lv_obj_class_t lvgl_chip_class;
|
|||||||
extern const lv_obj_class_t lvgl_badge_class;
|
extern const lv_obj_class_t lvgl_badge_class;
|
||||||
extern const lv_obj_class_t lvgl_page_indicator_class;
|
extern const lv_obj_class_t lvgl_page_indicator_class;
|
||||||
extern const lv_obj_class_t lvgl_page_indicator_dot_class;
|
extern const lv_obj_class_t lvgl_page_indicator_dot_class;
|
||||||
|
extern const lv_obj_class_t lvgl_step_indicator_class;
|
||||||
|
extern const lv_obj_class_t lvgl_step_indicator_segment_class;
|
||||||
extern const lv_obj_class_t lvgl_list_class;
|
extern const lv_obj_class_t lvgl_list_class;
|
||||||
extern const lv_obj_class_t lvgl_list_text_class;
|
extern const lv_obj_class_t lvgl_list_text_class;
|
||||||
extern const lv_obj_class_t lvgl_list_button_class;
|
extern const lv_obj_class_t lvgl_list_button_class;
|
||||||
@ -97,6 +99,10 @@ extern const lv_obj_class_t lvgl_text_button_class;
|
|||||||
#define PAGE_INDICATOR_COLOR COLOR_ON_SURFACE
|
#define PAGE_INDICATOR_COLOR COLOR_ON_SURFACE
|
||||||
#define PAGE_INDICATOR_DOT_SIZE (theme->config.is_compact ? 6 : LV_DPX_CALC(theme->disp_dpi, 8))
|
#define PAGE_INDICATOR_DOT_SIZE (theme->config.is_compact ? 6 : LV_DPX_CALC(theme->disp_dpi, 8))
|
||||||
#define PAGE_INDICATOR_GAP (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
#define PAGE_INDICATOR_GAP (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||||
|
#define STEP_INDICATOR_COLOR COLOR_ON_SURFACE
|
||||||
|
#define STEP_INDICATOR_THICKNESS (LV_DPX_CALC(theme->disp_dpi, 6))
|
||||||
|
#define STEP_INDICATOR_MARGIN (LV_DPX_CALC(theme->disp_dpi, 6))
|
||||||
|
#define STEP_INDICATOR_GAP STEP_INDICATOR_MARGIN
|
||||||
#define BADGE_COLOR COLOR_ON_SURFACE
|
#define BADGE_COLOR COLOR_ON_SURFACE
|
||||||
#define BADGE_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
#define BADGE_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||||
#define SCROLLBAR_COLOR COLOR_ON_SURFACE
|
#define SCROLLBAR_COLOR COLOR_ON_SURFACE
|
||||||
@ -233,6 +239,9 @@ typedef struct {
|
|||||||
#endif
|
#endif
|
||||||
lv_style_t page_indicator_dot;
|
lv_style_t page_indicator_dot;
|
||||||
lv_style_t page_indicator_dot_current;
|
lv_style_t page_indicator_dot_current;
|
||||||
|
lv_style_t step_indicator;
|
||||||
|
lv_style_t step_indicator_segment;
|
||||||
|
lv_style_t step_indicator_segment_finished;
|
||||||
lv_style_t badge;
|
lv_style_t badge;
|
||||||
lv_style_t chip;
|
lv_style_t chip;
|
||||||
#if LV_USE_BUTTON || LV_USE_MSGBOX
|
#if LV_USE_BUTTON || LV_USE_MSGBOX
|
||||||
@ -582,6 +591,23 @@ static void style_init(my_theme_t * theme)
|
|||||||
style_init_reset(&theme->styles.page_indicator_dot_current);
|
style_init_reset(&theme->styles.page_indicator_dot_current);
|
||||||
lv_style_set_bg_opa(&theme->styles.page_indicator_dot_current, LV_OPA_COVER);
|
lv_style_set_bg_opa(&theme->styles.page_indicator_dot_current, LV_OPA_COVER);
|
||||||
|
|
||||||
|
style_init_reset(&theme->styles.step_indicator);
|
||||||
|
lv_style_set_bg_opa(&theme->styles.step_indicator, LV_OPA_TRANSP);
|
||||||
|
lv_style_set_border_width(&theme->styles.step_indicator, 0);
|
||||||
|
lv_style_set_pad_all(&theme->styles.step_indicator, STEP_INDICATOR_MARGIN);
|
||||||
|
lv_style_set_pad_gap(&theme->styles.step_indicator, STEP_INDICATOR_GAP);
|
||||||
|
|
||||||
|
style_init_reset(&theme->styles.step_indicator_segment);
|
||||||
|
lv_style_set_height(&theme->styles.step_indicator_segment, STEP_INDICATOR_THICKNESS);
|
||||||
|
lv_style_set_radius(&theme->styles.step_indicator_segment, LV_RADIUS_CIRCLE);
|
||||||
|
lv_style_set_bg_color(&theme->styles.step_indicator_segment, STEP_INDICATOR_COLOR);
|
||||||
|
lv_style_set_bg_opa(&theme->styles.step_indicator_segment, LV_OPA_TRANSP);
|
||||||
|
lv_style_set_border_color(&theme->styles.step_indicator_segment, STEP_INDICATOR_COLOR);
|
||||||
|
lv_style_set_border_width(&theme->styles.step_indicator_segment, 1);
|
||||||
|
|
||||||
|
style_init_reset(&theme->styles.step_indicator_segment_finished);
|
||||||
|
lv_style_set_bg_opa(&theme->styles.step_indicator_segment_finished, LV_OPA_COVER);
|
||||||
|
|
||||||
style_init_reset(&theme->styles.badge);
|
style_init_reset(&theme->styles.badge);
|
||||||
lv_style_set_width(&theme->styles.badge, BADGE_SIZE);
|
lv_style_set_width(&theme->styles.badge, BADGE_SIZE);
|
||||||
lv_style_set_height(&theme->styles.badge, BADGE_SIZE);
|
lv_style_set_height(&theme->styles.badge, BADGE_SIZE);
|
||||||
@ -889,7 +915,7 @@ static void style_init(my_theme_t * theme)
|
|||||||
lv_style_set_border_width(&theme->styles.list, BORDER);
|
lv_style_set_border_width(&theme->styles.list, BORDER);
|
||||||
lv_style_set_radius(&theme->styles.list, SHAPE_CONTAINER);
|
lv_style_set_radius(&theme->styles.list, SHAPE_CONTAINER);
|
||||||
lv_style_set_pad_hor(&theme->styles.list, 0);
|
lv_style_set_pad_hor(&theme->styles.list, 0);
|
||||||
lv_style_set_pad_ver(&theme->styles.list, LIST_PAD);
|
lv_style_set_pad_ver(&theme->styles.list, 0);
|
||||||
lv_style_set_pad_gap(&theme->styles.list, 0);
|
lv_style_set_pad_gap(&theme->styles.list, 0);
|
||||||
lv_style_set_clip_corner(&theme->styles.list, true);
|
lv_style_set_clip_corner(&theme->styles.list, true);
|
||||||
|
|
||||||
@ -1515,6 +1541,17 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
|||||||
lv_obj_add_style(obj, &theme->styles.page_indicator_dot_current, LV_STATE_CHECKED);
|
lv_obj_add_style(obj, &theme->styles.page_indicator_dot_current, LV_STATE_CHECKED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(lv_obj_check_type(obj, &lvgl_step_indicator_class)) {
|
||||||
|
/* step_indicator */
|
||||||
|
lv_obj_add_style(obj, &theme->styles.step_indicator, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(lv_obj_check_type(obj, &lvgl_step_indicator_segment_class)) {
|
||||||
|
/* step_indicator_segment */
|
||||||
|
lv_obj_add_style(obj, &theme->styles.step_indicator_segment, 0);
|
||||||
|
lv_obj_add_style(obj, &theme->styles.step_indicator_segment_finished, LV_STATE_CHECKED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(lv_obj_check_type(obj, &lvgl_badge_class)) {
|
if(lv_obj_check_type(obj, &lvgl_badge_class)) {
|
||||||
/* badge */
|
/* badge */
|
||||||
lv_obj_add_style(obj, &theme->styles.badge, 0);
|
lv_obj_add_style(obj, &theme->styles.badge, 0);
|
||||||
@ -1723,6 +1760,7 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
|||||||
if(lv_obj_check_type(obj, &lvgl_list_button_class)) {
|
if(lv_obj_check_type(obj, &lvgl_list_button_class)) {
|
||||||
/* flex_list_button */
|
/* flex_list_button */
|
||||||
lv_obj_add_style(obj, &theme->styles.list_item, 0);
|
lv_obj_add_style(obj, &theme->styles.list_item, 0);
|
||||||
|
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_CHECKED);
|
||||||
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_PRESSED);
|
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_PRESSED);
|
||||||
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_FOCUS_KEY);
|
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_FOCUS_KEY);
|
||||||
lv_obj_add_style(obj, &theme->styles.disabled, LV_STATE_DISABLED);
|
lv_obj_add_style(obj, &theme->styles.disabled, LV_STATE_DISABLED);
|
||||||
|
|||||||
82
Modules/lvgl-module/source/widgets/step_indicator.cpp
Normal file
82
Modules/lvgl-module/source/widgets/step_indicator.cpp
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#define LV_USE_PRIVATE_API 1 // For the lv_obj_class_t declaration
|
||||||
|
|
||||||
|
#include <lvgl/widgets/step_indicator.h>
|
||||||
|
|
||||||
|
static void step_indicator_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
|
||||||
|
LV_UNUSED(class_p);
|
||||||
|
lv_obj_set_clickable(obj, false);
|
||||||
|
lv_obj_set_scrollable(obj, false);
|
||||||
|
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(obj, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void step_indicator_segment_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
|
||||||
|
LV_UNUSED(class_p);
|
||||||
|
lv_obj_set_clickable(obj, false);
|
||||||
|
lv_obj_set_scrollable(obj, false);
|
||||||
|
// The segments share the indicator's width
|
||||||
|
lv_obj_set_flex_grow(obj, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not static: the theme styles step indicators and their segments by these classes
|
||||||
|
extern "C" const lv_obj_class_t lvgl_step_indicator_class = {
|
||||||
|
.base_class = &lv_obj_class,
|
||||||
|
.constructor_cb = step_indicator_constructor,
|
||||||
|
.destructor_cb = nullptr,
|
||||||
|
.event_cb = nullptr,
|
||||||
|
.user_data = nullptr,
|
||||||
|
.name = "step_indicator",
|
||||||
|
.width_def = LV_PCT(100),
|
||||||
|
.height_def = LV_SIZE_CONTENT,
|
||||||
|
.editable = LV_OBJ_CLASS_EDITABLE_INHERIT,
|
||||||
|
.group_def = LV_OBJ_CLASS_GROUP_DEF_FALSE,
|
||||||
|
.instance_size = 0,
|
||||||
|
.theme_inheritable = LV_OBJ_CLASS_THEME_INHERITABLE_FALSE
|
||||||
|
};
|
||||||
|
|
||||||
|
extern "C" const lv_obj_class_t lvgl_step_indicator_segment_class = {
|
||||||
|
.base_class = &lv_obj_class,
|
||||||
|
.constructor_cb = step_indicator_segment_constructor,
|
||||||
|
.destructor_cb = nullptr,
|
||||||
|
.event_cb = nullptr,
|
||||||
|
.user_data = nullptr,
|
||||||
|
.name = "step_indicator_segment",
|
||||||
|
.width_def = 0,
|
||||||
|
.height_def = 0,
|
||||||
|
.editable = LV_OBJ_CLASS_EDITABLE_INHERIT,
|
||||||
|
.group_def = LV_OBJ_CLASS_GROUP_DEF_FALSE,
|
||||||
|
.instance_size = 0,
|
||||||
|
.theme_inheritable = LV_OBJ_CLASS_THEME_INHERITABLE_FALSE
|
||||||
|
};
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
lv_obj_t* lvgl_step_indicator_create(lv_obj_t* parent) {
|
||||||
|
lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_step_indicator_class, parent);
|
||||||
|
lv_obj_class_init_obj(obj);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
void lvgl_step_indicator_set_step_count(lv_obj_t* obj, uint32_t count) {
|
||||||
|
uint32_t current = lv_obj_get_child_count(obj);
|
||||||
|
while (current > count) {
|
||||||
|
lv_obj_delete(lv_obj_get_child(obj, -1));
|
||||||
|
current--;
|
||||||
|
}
|
||||||
|
while (current < count) {
|
||||||
|
lv_obj_t* segment = lv_obj_class_create_obj(&lvgl_step_indicator_segment_class, obj);
|
||||||
|
lv_obj_class_init_obj(segment);
|
||||||
|
current++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void lvgl_step_indicator_set_finished_count(lv_obj_t* obj, uint32_t finished_count) {
|
||||||
|
const uint32_t count = lv_obj_get_child_count(obj);
|
||||||
|
for (uint32_t i = 0; i < count; i++) {
|
||||||
|
// The theme shows finished steps by the checked state
|
||||||
|
lv_obj_set_state(lv_obj_get_child(obj, static_cast<int32_t>(i)), LV_STATE_CHECKED, i < finished_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -35,6 +35,8 @@
|
|||||||
@widget badge: lvgl_badge_class;
|
@widget badge: lvgl_badge_class;
|
||||||
@widget page_indicator: lvgl_page_indicator_class;
|
@widget page_indicator: lvgl_page_indicator_class;
|
||||||
@widget page_indicator_dot: lvgl_page_indicator_dot_class;
|
@widget page_indicator_dot: lvgl_page_indicator_dot_class;
|
||||||
|
@widget step_indicator: lvgl_step_indicator_class;
|
||||||
|
@widget step_indicator_segment: lvgl_step_indicator_segment_class;
|
||||||
@widget flex_list: lvgl_list_class;
|
@widget flex_list: lvgl_list_class;
|
||||||
@widget flex_list_text: lvgl_list_text_class;
|
@widget flex_list_text: lvgl_list_text_class;
|
||||||
@widget flex_list_button: lvgl_list_button_class;
|
@widget flex_list_button: lvgl_list_button_class;
|
||||||
@ -219,6 +221,13 @@
|
|||||||
@page-indicator-dot-size: if($isCompact, 6, 8dp);
|
@page-indicator-dot-size: if($isCompact, 6, 8dp);
|
||||||
@page-indicator-gap: if($isCompact, 4, 6dp);
|
@page-indicator-gap: if($isCompact, 4, 6dp);
|
||||||
|
|
||||||
|
// Step indicator (lvgl_step_indicator_create): a line segment per step, finished steps in the secondary colour
|
||||||
|
@step-indicator-finished-color: @color-secondary;
|
||||||
|
@step-indicator-unfinished-color: mix(black, @color-secondary, 50%);
|
||||||
|
@step-indicator-thickness: 6dp;
|
||||||
|
@step-indicator-margin: 6dp;
|
||||||
|
@step-indicator-gap: @step-indicator-margin;
|
||||||
|
|
||||||
// Badge (lvgl_badge_create): a small dot that marks an item, e.g. a favourite
|
// Badge (lvgl_badge_create): a small dot that marks an item, e.g. a favourite
|
||||||
@badge-color: @color-secondary;
|
@badge-color: @color-secondary;
|
||||||
@badge-size: if($isCompact, 4, 6dp);
|
@badge-size: if($isCompact, 4, 6dp);
|
||||||
@ -373,7 +382,7 @@
|
|||||||
@key-selected-text-color: @color-on-primary;
|
@key-selected-text-color: @color-on-primary;
|
||||||
|
|
||||||
// List
|
// List
|
||||||
@list-color: @color-surface;
|
@list-color: @card-color;
|
||||||
@list-pad: @density-list-pad;
|
@list-pad: @density-list-pad;
|
||||||
@list-item-color: @color-surface;
|
@list-item-color: @color-surface;
|
||||||
@list-item-opacity: transp;
|
@list-item-opacity: transp;
|
||||||
@ -383,7 +392,10 @@
|
|||||||
@list-item-gap: if($isCompact, 3, @space-4);
|
@list-item-gap: if($isCompact, 3, @space-4);
|
||||||
@list-item-focused-color: @color-secondary-container;
|
@list-item-focused-color: @color-secondary-container;
|
||||||
@list-item-focused-text-color: @color-on-secondary-container;
|
@list-item-focused-text-color: @color-on-secondary-container;
|
||||||
@list-divider-color: @color-outline-variant;
|
@list-item-selected-color: @color-secondary-container;
|
||||||
|
@list-item-selected-text-color: @color-on-secondary-container;
|
||||||
|
// The secondary colour, muted towards the list background
|
||||||
|
@list-divider-color: mix(@color-secondary, @list-color, 35%);
|
||||||
@list-divider-width: @density-border;
|
@list-divider-width: @density-border;
|
||||||
@list-subheader-text-color: @color-on-surface-variant;
|
@list-subheader-text-color: @color-on-surface-variant;
|
||||||
|
|
||||||
@ -614,6 +626,25 @@ page_indicator_dot_current {
|
|||||||
bgOpa: cover;
|
bgOpa: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
step_indicator {
|
||||||
|
bgOpa: transp;
|
||||||
|
borderWidth: 0;
|
||||||
|
padAll: @step-indicator-margin;
|
||||||
|
padGap: @step-indicator-gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
step_indicator_segment {
|
||||||
|
height: @step-indicator-thickness;
|
||||||
|
radius: circle;
|
||||||
|
borderWidth: 0;
|
||||||
|
bgColor: @step-indicator-unfinished-color;
|
||||||
|
bgOpa: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
step_indicator_segment_finished {
|
||||||
|
bgColor: @step-indicator-finished-color;
|
||||||
|
}
|
||||||
|
|
||||||
badge {
|
badge {
|
||||||
width: @badge-size;
|
width: @badge-size;
|
||||||
height: @badge-size;
|
height: @badge-size;
|
||||||
@ -1012,7 +1043,7 @@ list {
|
|||||||
bgColor: @list-color;
|
bgColor: @list-color;
|
||||||
bgOpa: cover;
|
bgOpa: cover;
|
||||||
padHor: 0;
|
padHor: 0;
|
||||||
padVer: @list-pad;
|
padVer: 0;
|
||||||
padGap: 0;
|
padGap: 0;
|
||||||
clipCorner: true;
|
clipCorner: true;
|
||||||
}
|
}
|
||||||
@ -1035,6 +1066,13 @@ list_item_focused {
|
|||||||
textColor: @list-item-focused-text-color;
|
textColor: @list-item-focused-text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A list item that is checked to show it's the selected one
|
||||||
|
list_item_selected {
|
||||||
|
bgColor: @list-item-selected-color;
|
||||||
|
bgOpa: cover;
|
||||||
|
textColor: @list-item-selected-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
list_subheader {
|
list_subheader {
|
||||||
textColor: @list-subheader-text-color;
|
textColor: @list-subheader-text-color;
|
||||||
textFont: $fontSmall;
|
textFont: $fontSmall;
|
||||||
@ -1501,6 +1539,15 @@ apply(page_indicator_dot) {
|
|||||||
page_indicator_dot_current: state.checked;
|
page_indicator_dot_current: state.checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply(step_indicator) {
|
||||||
|
step_indicator: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(step_indicator_segment) {
|
||||||
|
step_indicator_segment: default;
|
||||||
|
step_indicator_segment_finished: state.checked;
|
||||||
|
}
|
||||||
|
|
||||||
apply(badge) {
|
apply(badge) {
|
||||||
badge: default;
|
badge: default;
|
||||||
}
|
}
|
||||||
@ -1680,6 +1727,7 @@ apply(flex_list_text) {
|
|||||||
|
|
||||||
apply(flex_list_button) {
|
apply(flex_list_button) {
|
||||||
list_item: default;
|
list_item: default;
|
||||||
|
list_item_selected: state.checked;
|
||||||
transition_exit: default;
|
transition_exit: default;
|
||||||
state_pressed: state.pressed;
|
state_pressed: state.pressed;
|
||||||
transition_enter: state.pressed;
|
transition_enter: state.pressed;
|
||||||
|
|||||||
@ -33,6 +33,8 @@
|
|||||||
@widget badge: lvgl_badge_class;
|
@widget badge: lvgl_badge_class;
|
||||||
@widget page_indicator: lvgl_page_indicator_class;
|
@widget page_indicator: lvgl_page_indicator_class;
|
||||||
@widget page_indicator_dot: lvgl_page_indicator_dot_class;
|
@widget page_indicator_dot: lvgl_page_indicator_dot_class;
|
||||||
|
@widget step_indicator: lvgl_step_indicator_class;
|
||||||
|
@widget step_indicator_segment: lvgl_step_indicator_segment_class;
|
||||||
@widget flex_list: lvgl_list_class;
|
@widget flex_list: lvgl_list_class;
|
||||||
@widget flex_list_text: lvgl_list_text_class;
|
@widget flex_list_text: lvgl_list_text_class;
|
||||||
@widget flex_list_button: lvgl_list_button_class;
|
@widget flex_list_button: lvgl_list_button_class;
|
||||||
@ -154,6 +156,12 @@
|
|||||||
@page-indicator-dot-size: if($isCompact, 6, 8dp);
|
@page-indicator-dot-size: if($isCompact, 6, 8dp);
|
||||||
@page-indicator-gap: if($isCompact, 4, 6dp);
|
@page-indicator-gap: if($isCompact, 4, 6dp);
|
||||||
|
|
||||||
|
// Step indicator (lvgl_step_indicator_create): a line segment per step, finished steps filled, unfinished steps outlined
|
||||||
|
@step-indicator-color: @color-on-surface;
|
||||||
|
@step-indicator-thickness: 6dp;
|
||||||
|
@step-indicator-margin: 6dp;
|
||||||
|
@step-indicator-gap: @step-indicator-margin;
|
||||||
|
|
||||||
// Badge (lvgl_badge_create): a small dot that marks an item, e.g. a favourite
|
// Badge (lvgl_badge_create): a small dot that marks an item, e.g. a favourite
|
||||||
@badge-color: @color-on-surface;
|
@badge-color: @color-on-surface;
|
||||||
@badge-size: if($isCompact, 4, 6dp);
|
@badge-size: if($isCompact, 4, 6dp);
|
||||||
@ -431,6 +439,26 @@ page_indicator_dot_current {
|
|||||||
bgOpa: cover;
|
bgOpa: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
step_indicator {
|
||||||
|
bgOpa: transp;
|
||||||
|
borderWidth: 0;
|
||||||
|
padAll: @step-indicator-margin;
|
||||||
|
padGap: @step-indicator-gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
step_indicator_segment {
|
||||||
|
height: @step-indicator-thickness;
|
||||||
|
radius: circle;
|
||||||
|
bgColor: @step-indicator-color;
|
||||||
|
bgOpa: transp;
|
||||||
|
borderColor: @step-indicator-color;
|
||||||
|
borderWidth: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
step_indicator_segment_finished {
|
||||||
|
bgOpa: cover;
|
||||||
|
}
|
||||||
|
|
||||||
badge {
|
badge {
|
||||||
width: @badge-size;
|
width: @badge-size;
|
||||||
height: @badge-size;
|
height: @badge-size;
|
||||||
@ -733,7 +761,7 @@ list {
|
|||||||
borderWidth: @border;
|
borderWidth: @border;
|
||||||
radius: @shape-container;
|
radius: @shape-container;
|
||||||
padHor: 0;
|
padHor: 0;
|
||||||
padVer: @list-pad;
|
padVer: 0;
|
||||||
padGap: 0;
|
padGap: 0;
|
||||||
clipCorner: true;
|
clipCorner: true;
|
||||||
}
|
}
|
||||||
@ -1141,6 +1169,15 @@ apply(page_indicator_dot) {
|
|||||||
page_indicator_dot_current: state.checked;
|
page_indicator_dot_current: state.checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply(step_indicator) {
|
||||||
|
step_indicator: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(step_indicator_segment) {
|
||||||
|
step_indicator_segment: default;
|
||||||
|
step_indicator_segment_finished: state.checked;
|
||||||
|
}
|
||||||
|
|
||||||
apply(badge) {
|
apply(badge) {
|
||||||
badge: default;
|
badge: default;
|
||||||
}
|
}
|
||||||
@ -1291,6 +1328,7 @@ apply(flex_list_text) {
|
|||||||
|
|
||||||
apply(flex_list_button) {
|
apply(flex_list_button) {
|
||||||
list_item: default;
|
list_item: default;
|
||||||
|
inverted: state.checked;
|
||||||
inverted: state.pressed;
|
inverted: state.pressed;
|
||||||
inverted: state.focusKey;
|
inverted: state.focusKey;
|
||||||
disabled: state.disabled;
|
disabled: state.disabled;
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <tactility/drivers/radio.h>
|
#include <tactility/drivers/radio.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace tt::settings::radio {
|
namespace tt::settings::radio {
|
||||||
|
|
||||||
/** @return the persisted regulatory region, RADIO_REGION_UNSET when none was chosen */
|
/** @return the persisted regulatory region, RADIO_REGION_UNSET when none was chosen */
|
||||||
@ -19,4 +21,10 @@ const char* getRegionLabel(RadioRegion region);
|
|||||||
/** @return true when the devicetree has a radio, whether it's started or not */
|
/** @return true when the devicetree has a radio, whether it's started or not */
|
||||||
bool hasRadio();
|
bool hasRadio();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param[in] timeZoneName an IANA time zone name, e.g. "Europe/Amsterdam"
|
||||||
|
* @return the region of the most common radio band where that time zone is used, RADIO_REGION_UNSET when unknown
|
||||||
|
*/
|
||||||
|
RadioRegion findRegionForTimeZone(const std::string& timeZoneName);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
81
Tactility/Private/Tactility/app/timezone/TimeZoneView.h
Normal file
81
Tactility/Private/Tactility/app/timezone/TimeZoneView.h
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Tactility/Mutex.h>
|
||||||
|
#include <Tactility/Thread.h>
|
||||||
|
#include <Tactility/Timer.h>
|
||||||
|
|
||||||
|
#include <lvgl.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace tt::app::timezone {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A searchable list of the time zones, with a button that detects the time zone from the location.
|
||||||
|
* Its state survives rebuilding its widgets, e.g. when a window is covered and shown again.
|
||||||
|
*/
|
||||||
|
class TimeZoneView final {
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
/** Called with the LVGL lock held when the user picks a time zone, or when one was detected */
|
||||||
|
using OnSelected = std::function<void(const std::string& name, const std::string& code)>;
|
||||||
|
|
||||||
|
explicit TimeZoneView(OnSelected onSelected);
|
||||||
|
|
||||||
|
/** Waits for a running detection, which can take a few seconds. Destroy it without holding the LVGL lock. */
|
||||||
|
~TimeZoneView();
|
||||||
|
|
||||||
|
TimeZoneView(const TimeZoneView&) = delete;
|
||||||
|
TimeZoneView& operator=(const TimeZoneView&) = delete;
|
||||||
|
|
||||||
|
/** Creates the widgets in @a parent, a flex column, with the LVGL lock held. The list grows to fill it. */
|
||||||
|
void createWidgets(lv_obj_t* parent);
|
||||||
|
|
||||||
|
/** Call with the LVGL lock held when the widgets were deleted */
|
||||||
|
void destroyWidgets();
|
||||||
|
|
||||||
|
/** Shows this time zone as the selected one */
|
||||||
|
void setSelectedName(const std::string& name);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
std::string name;
|
||||||
|
std::string code;
|
||||||
|
};
|
||||||
|
|
||||||
|
OnSelected onSelected;
|
||||||
|
Mutex mutex;
|
||||||
|
// Guarded by mutex
|
||||||
|
std::vector<Entry> entries;
|
||||||
|
std::string filter;
|
||||||
|
std::string selectedName;
|
||||||
|
std::string message;
|
||||||
|
std::unique_ptr<Timer> filterTimer;
|
||||||
|
std::unique_ptr<Thread> detectThread;
|
||||||
|
std::atomic<bool> detecting = false;
|
||||||
|
|
||||||
|
// Valid while the widgets exist, guarded by the LVGL lock
|
||||||
|
lv_obj_t* textarea = nullptr;
|
||||||
|
lv_obj_t* detectButton = nullptr;
|
||||||
|
lv_obj_t* detectSpinner = nullptr;
|
||||||
|
lv_obj_t* messageLabel = nullptr;
|
||||||
|
lv_obj_t* list = nullptr;
|
||||||
|
|
||||||
|
static void onFilterChanged(lv_event_t* event);
|
||||||
|
static void onDetectPressed(lv_event_t* event);
|
||||||
|
static void onEntryPressed(lv_event_t* event);
|
||||||
|
|
||||||
|
void loadEntries();
|
||||||
|
void rebuildList();
|
||||||
|
void showState();
|
||||||
|
void detect();
|
||||||
|
void select(const std::string& name, const std::string& code);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@ -3,11 +3,10 @@
|
|||||||
#include <tactility/paths.h>
|
#include <tactility/paths.h>
|
||||||
|
|
||||||
#include <Tactility/StringUtils.h>
|
#include <Tactility/StringUtils.h>
|
||||||
#include <Tactility/app/timezone/TimeZone.h>
|
#include <Tactility/app/timezone/TimeZoneView.h>
|
||||||
#include <Tactility/app/selectiondialog/SelectionDialog.h>
|
|
||||||
#include <Tactility/app/wifimanage/WifiManage.h>
|
#include <Tactility/app/wifimanage/WifiManage.h>
|
||||||
#include <Tactility/file/File.h>
|
#include <Tactility/file/File.h>
|
||||||
#include <Tactility/settings/RadioSettings.h>
|
#include <Tactility/settings/Time.h>
|
||||||
#include <Tactility/Tactility.h>
|
#include <Tactility/Tactility.h>
|
||||||
|
|
||||||
#include <app/event.h>
|
#include <app/event.h>
|
||||||
@ -31,11 +30,13 @@
|
|||||||
#include <lvgl/fonts.h>
|
#include <lvgl/fonts.h>
|
||||||
#include <lvgl/grid_navigation.h>
|
#include <lvgl/grid_navigation.h>
|
||||||
#include <lvgl/lvgl.h>
|
#include <lvgl/lvgl.h>
|
||||||
#include <lvgl/widgets/page_indicator.h>
|
#include <lvgl/widgets/chip.h>
|
||||||
|
#include <lvgl/widgets/step_indicator.h>
|
||||||
#include <lvgl/widgets/text_button.h>
|
#include <lvgl/widgets/text_button.h>
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
@ -96,8 +97,19 @@ struct StepConfiguration {
|
|||||||
std::function<void()> run;
|
std::function<void()> run;
|
||||||
/** Optional: handles the result of the dialog that run() started */
|
/** Optional: handles the result of the dialog that run() started */
|
||||||
std::function<void(int32_t)> onResult = nullptr;
|
std::function<void(int32_t)> onResult = nullptr;
|
||||||
|
/** What the page shows below its title */
|
||||||
|
enum class Content {
|
||||||
|
Description,
|
||||||
|
/** The description, with the location choices below it */
|
||||||
|
LocationChoice,
|
||||||
|
/** The time zone list instead of the description */
|
||||||
|
TimeZones
|
||||||
|
} content = Content::Description;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** The location choices of the location page, in the order of their chips */
|
||||||
|
constexpr const char* LOCATION_CHOICES[] = { "Precise", "Approximate", "Disabled" };
|
||||||
|
|
||||||
struct Context {
|
struct Context {
|
||||||
uint32_t appInstanceId;
|
uint32_t appInstanceId;
|
||||||
|
|
||||||
@ -112,7 +124,17 @@ struct Context {
|
|||||||
lv_obj_t* backButton = nullptr;
|
lv_obj_t* backButton = nullptr;
|
||||||
lv_obj_t* skipButton = nullptr;
|
lv_obj_t* skipButton = nullptr;
|
||||||
lv_obj_t* continueButton = nullptr;
|
lv_obj_t* continueButton = nullptr;
|
||||||
lv_obj_t* pageIndicator = nullptr;
|
lv_obj_t* stepIndicator = nullptr;
|
||||||
|
lv_obj_t* bottom = nullptr;
|
||||||
|
lv_obj_t* timeZoneContainer = nullptr;
|
||||||
|
lv_obj_t* locationChips = nullptr;
|
||||||
|
// The index in LOCATION_CHOICES picked on the location page, -1 when none. Guarded by the LVGL lock.
|
||||||
|
int32_t locationChoice = -1;
|
||||||
|
|
||||||
|
std::unique_ptr<timezone::TimeZoneView> timeZoneView;
|
||||||
|
// The time zone picked on the time zone page, guarded by the LVGL lock
|
||||||
|
std::string timeZoneName;
|
||||||
|
std::string timeZoneCode;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The welcome page, a page per step, and the completion page */
|
/** The welcome page, a page per step, and the completion page */
|
||||||
@ -133,8 +155,96 @@ uint32_t getPage(const Context* ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** The welcome page counts as the first step, followed by the configuration steps */
|
||||||
|
uint32_t getStepCount(const Context* ctx) {
|
||||||
|
return static_cast<uint32_t>(ctx->steps.size()) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The steps before the current one, or all of them once setup is done */
|
||||||
|
uint32_t getFinishedStepCount(const Context* ctx) {
|
||||||
|
switch (ctx->phase) {
|
||||||
|
case Phase::Welcome:
|
||||||
|
return 0;
|
||||||
|
case Phase::StepIntro:
|
||||||
|
return static_cast<uint32_t>(ctx->stepIndex) + 1;
|
||||||
|
case Phase::Done:
|
||||||
|
return getStepCount(ctx);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
StepConfiguration::Content getContent(const Context* ctx) {
|
||||||
|
return ctx->phase == Phase::StepIntro ? ctx->steps[ctx->stepIndex].content : StepConfiguration::Content::Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shows which location choice is picked */
|
||||||
|
void renderLocationChips(Context* ctx) {
|
||||||
|
const uint32_t count = lv_obj_get_child_count(ctx->locationChips);
|
||||||
|
for (uint32_t i = 0; i < count; i++) {
|
||||||
|
lv_obj_set_state(lv_obj_get_child(ctx->locationChips, static_cast<int32_t>(i)), LV_STATE_CHECKED, static_cast<int32_t>(i) == ctx->locationChoice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The time zone page shows the title at the top and the list below it, the other pages center their text */
|
||||||
|
void renderLayout(Context* ctx) {
|
||||||
|
const auto content = getContent(ctx);
|
||||||
|
const bool showsTimeZones = content == StepConfiguration::Content::TimeZones;
|
||||||
|
const bool showsLocationChoice = content == StepConfiguration::Content::LocationChoice;
|
||||||
|
const bool hasTimeZoneWidgets = lv_obj_get_child_count(ctx->timeZoneContainer) > 0;
|
||||||
|
lv_obj_set_hidden(ctx->descriptionLabel, showsTimeZones);
|
||||||
|
lv_obj_set_hidden(ctx->timeZoneContainer, !showsTimeZones);
|
||||||
|
lv_obj_set_hidden(ctx->locationChips, !showsLocationChoice);
|
||||||
|
if (!showsTimeZones) {
|
||||||
|
if (hasTimeZoneWidgets) {
|
||||||
|
lv_obj_clean(ctx->timeZoneContainer);
|
||||||
|
ctx->timeZoneView->destroyWidgets();
|
||||||
|
}
|
||||||
|
const int titleMargin = lvgl_get_text_font_height(FONT_SIZE_LARGE);
|
||||||
|
if (showsLocationChoice) {
|
||||||
|
// Centered as a group in the space above the buttons
|
||||||
|
const int32_t gap = titleMargin / 2;
|
||||||
|
lv_obj_update_layout(lv_obj_get_parent(ctx->titleLabel));
|
||||||
|
const int32_t height = lv_obj_get_height(ctx->titleLabel) + gap + lv_obj_get_height(ctx->descriptionLabel) + gap + lv_obj_get_height(ctx->locationChips);
|
||||||
|
lv_area_t parent;
|
||||||
|
lv_area_t bottom;
|
||||||
|
lv_obj_get_content_coords(lv_obj_get_parent(ctx->titleLabel), &parent);
|
||||||
|
lv_obj_get_coords(ctx->bottom, &bottom);
|
||||||
|
const int32_t top = LV_MAX((bottom.y1 - parent.y1 - height) / 2, 0);
|
||||||
|
lv_obj_align(ctx->titleLabel, LV_ALIGN_TOP_MID, 0, top);
|
||||||
|
lv_obj_align_to(ctx->descriptionLabel, ctx->titleLabel, LV_ALIGN_OUT_BOTTOM_MID, 0, gap);
|
||||||
|
lv_obj_align_to(ctx->locationChips, ctx->descriptionLabel, LV_ALIGN_OUT_BOTTOM_MID, 0, gap);
|
||||||
|
// Continue confirms the picked choice
|
||||||
|
renderLocationChips(ctx);
|
||||||
|
lv_obj_set_state(ctx->continueButton, LV_STATE_DISABLED, ctx->locationChoice < 0);
|
||||||
|
} else {
|
||||||
|
lv_obj_align(ctx->descriptionLabel, LV_ALIGN_CENTER, 0, 0);
|
||||||
|
lv_obj_align_to(ctx->titleLabel, ctx->descriptionLabel, LV_ALIGN_OUT_TOP_MID, 0, -titleMargin);
|
||||||
|
lv_obj_remove_state(ctx->continueButton, LV_STATE_DISABLED);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_align(ctx->titleLabel, LV_ALIGN_TOP_MID, 0, LV_DPX(8));
|
||||||
|
if (!hasTimeZoneWidgets) {
|
||||||
|
ctx->timeZoneView->createWidgets(ctx->timeZoneContainer);
|
||||||
|
}
|
||||||
|
// Continue confirms the picked time zone
|
||||||
|
lv_obj_set_state(ctx->continueButton, LV_STATE_DISABLED, ctx->timeZoneName.empty());
|
||||||
|
|
||||||
|
// Fills the space between the title and the buttons
|
||||||
|
lv_obj_update_layout(lv_obj_get_parent(ctx->timeZoneContainer));
|
||||||
|
lv_area_t title;
|
||||||
|
lv_area_t bottom;
|
||||||
|
lv_obj_get_coords(ctx->titleLabel, &title);
|
||||||
|
lv_obj_get_coords(ctx->bottom, &bottom);
|
||||||
|
lv_area_t parent;
|
||||||
|
lv_obj_get_content_coords(lv_obj_get_parent(ctx->timeZoneContainer), &parent);
|
||||||
|
lv_obj_set_pos(ctx->timeZoneContainer, 0, title.y2 + LV_DPX(4) - parent.y1);
|
||||||
|
lv_obj_set_size(ctx->timeZoneContainer, LV_PCT(100), bottom.y1 - title.y2 - LV_DPX(8));
|
||||||
|
}
|
||||||
|
|
||||||
void renderCurrent(Context* ctx) {
|
void renderCurrent(Context* ctx) {
|
||||||
lvgl_page_indicator_set_page(ctx->pageIndicator, getPage(ctx));
|
lvgl_step_indicator_set_finished_count(ctx->stepIndicator, getFinishedStepCount(ctx));
|
||||||
lv_obj_set_hidden(ctx->backButton, ctx->phase == Phase::Welcome);
|
lv_obj_set_hidden(ctx->backButton, ctx->phase == Phase::Welcome);
|
||||||
switch (ctx->phase) {
|
switch (ctx->phase) {
|
||||||
case Phase::Welcome: {
|
case Phase::Welcome: {
|
||||||
@ -161,6 +271,7 @@ void renderCurrent(Context* ctx) {
|
|||||||
lv_label_set_text(lv_obj_get_child(ctx->continueButton, 0), "Finish");
|
lv_label_set_text(lv_obj_get_child(ctx->continueButton, 0), "Finish");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
renderLayout(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
void advanceTo(Context* ctx, size_t index) {
|
void advanceTo(Context* ctx, size_t index) {
|
||||||
@ -219,6 +330,13 @@ void onContinueClicked(lv_event_t* event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onLocationChipPressed(lv_event_t* event) {
|
||||||
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||||
|
ctx->locationChoice = static_cast<int32_t>(reinterpret_cast<uintptr_t>(lv_obj_get_user_data(lv_event_get_target_obj(event))));
|
||||||
|
renderLocationChips(ctx);
|
||||||
|
lv_obj_remove_state(ctx->continueButton, LV_STATE_DISABLED);
|
||||||
|
}
|
||||||
|
|
||||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||||
auto* ctx = static_cast<Context*>(userData);
|
auto* ctx = static_cast<Context*>(userData);
|
||||||
|
|
||||||
@ -235,17 +353,45 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
|||||||
lv_label_set_long_mode(ctx->descriptionLabel, LV_LABEL_LONG_WRAP);
|
lv_label_set_long_mode(ctx->descriptionLabel, LV_LABEL_LONG_WRAP);
|
||||||
lv_obj_align(ctx->descriptionLabel, LV_ALIGN_CENTER, 0, 0);
|
lv_obj_align(ctx->descriptionLabel, LV_ALIGN_CENTER, 0, 0);
|
||||||
|
|
||||||
int title_margin = lvgl_get_text_font_height(FONT_SIZE_LARGE);
|
// The location choices: one can be picked, and they wrap to more lines on narrow screens
|
||||||
lv_obj_align_to(ctx->titleLabel, ctx->descriptionLabel, LV_ALIGN_OUT_TOP_MID, 0, -title_margin);
|
ctx->locationChips = lv_obj_create(parent);
|
||||||
|
lv_obj_remove_style_all(ctx->locationChips);
|
||||||
|
lv_obj_set_size(ctx->locationChips, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
// The chip margins space the choices, so they fit on one line on small screens
|
||||||
|
lv_obj_set_flex_flow(ctx->locationChips, LV_FLEX_FLOW_ROW_WRAP);
|
||||||
|
lv_obj_set_flex_align(ctx->locationChips, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
for (size_t i = 0; i < std::size(LOCATION_CHOICES); i++) {
|
||||||
|
auto* chip = lvgl_chip_create(ctx->locationChips);
|
||||||
|
lv_label_set_text(lv_label_create(chip), LOCATION_CHOICES[i]);
|
||||||
|
lv_obj_set_user_data(chip, reinterpret_cast<void*>(static_cast<uintptr_t>(i)));
|
||||||
|
lv_obj_add_event_cb(chip, onLocationChipPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||||
|
}
|
||||||
|
lvgl_grid_navigation_add(ctx->locationChips);
|
||||||
|
lv_obj_set_hidden(ctx->locationChips, true);
|
||||||
|
|
||||||
// Back on the left, the page indicator in the middle, and Skip next to Continue on the right
|
// Filled with the time zone list on the time zone page
|
||||||
auto* buttonRow = lv_obj_create(parent);
|
ctx->timeZoneContainer = lv_obj_create(parent);
|
||||||
|
lv_obj_remove_style_all(ctx->timeZoneContainer);
|
||||||
|
lv_obj_set_flex_flow(ctx->timeZoneContainer, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_hor(ctx->timeZoneContainer, LV_DPX(8), LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_hidden(ctx->timeZoneContainer, true);
|
||||||
|
|
||||||
|
// The buttons, with the progress through the pages below them at the bottom
|
||||||
|
auto* bottom = lv_obj_create(parent);
|
||||||
|
ctx->bottom = bottom;
|
||||||
|
lv_obj_remove_style_all(bottom);
|
||||||
|
lv_obj_set_size(bottom, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_flow(bottom, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_flex_align(bottom, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_align(bottom, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||||
|
|
||||||
|
// Back on the left, and Skip next to Continue on the right
|
||||||
|
auto* buttonRow = lv_obj_create(bottom);
|
||||||
lv_obj_remove_style_all(buttonRow);
|
lv_obj_remove_style_all(buttonRow);
|
||||||
lv_obj_set_size(buttonRow, LV_PCT(100), LV_SIZE_CONTENT);
|
lv_obj_set_size(buttonRow, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
lv_obj_set_style_pad_hor(buttonRow, 8, LV_STATE_DEFAULT);
|
lv_obj_set_style_pad_hor(buttonRow, LV_DPX(8), LV_STATE_DEFAULT);
|
||||||
lv_obj_set_flex_flow(buttonRow, LV_FLEX_FLOW_ROW);
|
lv_obj_set_flex_flow(buttonRow, LV_FLEX_FLOW_ROW);
|
||||||
lv_obj_set_flex_align(buttonRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
lv_obj_set_flex_align(buttonRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
lv_obj_align(buttonRow, LV_ALIGN_BOTTOM_MID, 0, -8);
|
|
||||||
|
|
||||||
ctx->backButton = lvgl_text_button_create(buttonRow);
|
ctx->backButton = lvgl_text_button_create(buttonRow);
|
||||||
lv_label_set_text(lv_label_create(ctx->backButton), "Back");
|
lv_label_set_text(lv_label_create(ctx->backButton), "Back");
|
||||||
@ -264,12 +410,8 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
|||||||
lv_label_set_text(lv_label_create(ctx->continueButton), "Continue");
|
lv_label_set_text(lv_label_create(ctx->continueButton), "Continue");
|
||||||
lv_obj_add_event_cb(ctx->continueButton, onContinueClicked, LV_EVENT_SHORT_CLICKED, ctx);
|
lv_obj_add_event_cb(ctx->continueButton, onContinueClicked, LV_EVENT_SHORT_CLICKED, ctx);
|
||||||
|
|
||||||
// Centered on the row, outside the flex layout. It shows the page number instead of the dots when they don't fit.
|
ctx->stepIndicator = lvgl_step_indicator_create(bottom);
|
||||||
ctx->pageIndicator = lvgl_page_indicator_create(buttonRow);
|
lvgl_step_indicator_set_step_count(ctx->stepIndicator, getStepCount(ctx));
|
||||||
lv_obj_add_flag(ctx->pageIndicator, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
|
||||||
lv_obj_set_style_max_width(ctx->pageIndicator, LV_PCT(25), LV_STATE_DEFAULT);
|
|
||||||
lvgl_page_indicator_set_page_count(ctx->pageIndicator, getPageCount(ctx));
|
|
||||||
lv_obj_align(ctx->pageIndicator, LV_ALIGN_CENTER, 0, 0);
|
|
||||||
|
|
||||||
// The arrow keys move between the buttons, and Continue has the focus first
|
// The arrow keys move between the buttons, and Continue has the focus first
|
||||||
lvgl_grid_navigation_add(buttonRow);
|
lvgl_grid_navigation_add(buttonRow);
|
||||||
@ -288,7 +430,11 @@ void destroyWidgets(void* userData) {
|
|||||||
ctx->backButton = nullptr;
|
ctx->backButton = nullptr;
|
||||||
ctx->skipButton = nullptr;
|
ctx->skipButton = nullptr;
|
||||||
ctx->continueButton = nullptr;
|
ctx->continueButton = nullptr;
|
||||||
ctx->pageIndicator = nullptr;
|
ctx->stepIndicator = nullptr;
|
||||||
|
ctx->bottom = nullptr;
|
||||||
|
ctx->timeZoneContainer = nullptr;
|
||||||
|
ctx->locationChips = nullptr;
|
||||||
|
ctx->timeZoneView->destroyWidgets();
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t appMain(int argc, char* argv[]) {
|
int32_t appMain(int argc, char* argv[]) {
|
||||||
@ -303,11 +449,6 @@ int32_t appMain(int argc, char* argv[]) {
|
|||||||
.run = [&ctx] { ctx.pendingStepDialogId = touchcalibration::start(ctx.appInstanceId); }
|
.run = [&ctx] { ctx.pendingStepDialogId = touchcalibration::start(ctx.appInstanceId); }
|
||||||
},
|
},
|
||||||
#endif
|
#endif
|
||||||
{
|
|
||||||
.title = "Time Zone Setup",
|
|
||||||
.description = "Let's set the time zone.",
|
|
||||||
.run = [&ctx] { ctx.pendingStepDialogId = timezone::start(ctx.appInstanceId, true); }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
.title = "Wi-Fi Setup",
|
.title = "Wi-Fi Setup",
|
||||||
.description = "Let's connect to a Wi-Fi access point.",
|
.description = "Let's connect to a Wi-Fi access point.",
|
||||||
@ -326,40 +467,37 @@ int32_t appMain(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// The radio may only transmit on the frequencies and power of the user's region
|
|
||||||
if (settings::radio::hasRadio()) {
|
|
||||||
ctx.steps.push_back({
|
|
||||||
.title = "Radio Region",
|
|
||||||
.description = "Let's select your region, so the radio uses the frequencies that are allowed there.",
|
|
||||||
.run = [&ctx] {
|
|
||||||
std::vector<std::string> items;
|
|
||||||
for (int region = RADIO_REGION_UNSET + 1; region < RADIO_REGION_COUNT; region++) {
|
|
||||||
items.emplace_back(settings::radio::getRegionLabel(static_cast<RadioRegion>(region)));
|
|
||||||
}
|
|
||||||
ctx.pendingStepDialogId = selectiondialog::start(ctx.appInstanceId, "Region", items);
|
|
||||||
},
|
|
||||||
.onResult = [](int32_t selected) {
|
|
||||||
if (selected >= 0 && selected < RADIO_REGION_COUNT - 1) {
|
|
||||||
settings::radio::saveRegion(static_cast<RadioRegion>(selected + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.steps.push_back({
|
ctx.steps.push_back({
|
||||||
.title = "Location Services",
|
.title = "Location Services",
|
||||||
.description = "Location services let apps use your location. It comes from GPS, or from your IP address via ip-api.com when there is no GPS fix.",
|
.description = "Do you want to allow apps to retrieve your location? It uses GPS and/or contacts ip-api.com",
|
||||||
.run = [&ctx] {
|
.run = [&ctx] {
|
||||||
const std::vector<std::string> items = { "Allow precise location", "Allow approximate location", "Don't allow" };
|
// The order of LOCATION_CHOICES: precise, approximate, disabled
|
||||||
ctx.pendingStepDialogId = selectiondialog::start(ctx.appInstanceId, "Location", items);
|
if (ctx.locationChoice == 0 || ctx.locationChoice == 1) {
|
||||||
},
|
location_set_precision(ctx.locationChoice == 0 ? LOCATION_PRECISION_PRECISE : LOCATION_PRECISION_APPROXIMATE);
|
||||||
.onResult = [](int32_t selected) {
|
|
||||||
if (selected == 0 || selected == 1) {
|
|
||||||
location_set_precision(selected == 0 ? LOCATION_PRECISION_PRECISE : LOCATION_PRECISION_APPROXIMATE);
|
|
||||||
location_set_enabled(true);
|
location_set_enabled(true);
|
||||||
} else if (selected == 2) {
|
} else {
|
||||||
location_set_enabled(false);
|
location_set_enabled(false);
|
||||||
}
|
}
|
||||||
|
advanceTo(&ctx, ctx.stepIndex + 1);
|
||||||
|
},
|
||||||
|
.content = StepConfiguration::Content::LocationChoice
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setting the time zone also sets the radio region, as both depend on where the device is
|
||||||
|
ctx.steps.push_back({
|
||||||
|
.title = "Time Zone",
|
||||||
|
.description = "",
|
||||||
|
.run = [&ctx] {
|
||||||
|
settings::setTimeZone(ctx.timeZoneName, ctx.timeZoneCode);
|
||||||
|
advanceTo(&ctx, ctx.stepIndex + 1);
|
||||||
|
},
|
||||||
|
.content = StepConfiguration::Content::TimeZones
|
||||||
|
});
|
||||||
|
ctx.timeZoneView = std::make_unique<timezone::TimeZoneView>([&ctx](const std::string& name, const std::string& code) {
|
||||||
|
ctx.timeZoneName = name;
|
||||||
|
ctx.timeZoneCode = code;
|
||||||
|
if (ctx.continueButton != nullptr) {
|
||||||
|
lv_obj_remove_state(ctx.continueButton, LV_STATE_DISABLED);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,5 @@
|
|||||||
#include <Tactility/app/timezone/TimeZone.h>
|
#include <Tactility/app/timezone/TimeZone.h>
|
||||||
#include <Tactility/LogMessages.h>
|
#include <Tactility/app/timezone/TimeZoneView.h>
|
||||||
#include <Tactility/MountPoints.h>
|
|
||||||
#include <Tactility/Mutex.h>
|
|
||||||
#include <Tactility/StringUtils.h>
|
|
||||||
#include <Tactility/Timer.h>
|
|
||||||
#include <Tactility/settings/Time.h>
|
#include <Tactility/settings/Time.h>
|
||||||
|
|
||||||
#include <app/event.h>
|
#include <app/event.h>
|
||||||
@ -15,176 +11,34 @@
|
|||||||
#include <lvgl_window_manager/window_manager.h>
|
#include <lvgl_window_manager/window_manager.h>
|
||||||
|
|
||||||
#include <tactility/check.h>
|
#include <tactility/check.h>
|
||||||
#include <tactility/log.h>
|
|
||||||
|
|
||||||
#include <lvgl/lvgl.h>
|
|
||||||
#include <lvgl/icons/shared.h>
|
|
||||||
#include <lvgl/fonts.h>
|
|
||||||
#include <lvgl/widgets/list.h>
|
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <lvgl/widgets/toolbar.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace tt::app::timezone {
|
namespace tt::app::timezone {
|
||||||
|
|
||||||
constexpr auto* TAG = "TimeZone";
|
|
||||||
|
|
||||||
extern const ::AppManifest manifest;
|
extern const ::AppManifest manifest;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
struct TimeZoneEntry {
|
|
||||||
std::string name;
|
|
||||||
std::string code;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Context {
|
struct Context {
|
||||||
uint32_t appInstanceId;
|
uint32_t appInstanceId;
|
||||||
Mutex mutex;
|
|
||||||
std::vector<TimeZoneEntry> entries;
|
|
||||||
std::unique_ptr<Timer> updateTimer;
|
|
||||||
lv_obj_t* listWidget = nullptr;
|
|
||||||
lv_obj_t* filterTextareaWidget = nullptr;
|
|
||||||
bool saveTimeZone = false;
|
bool saveTimeZone = false;
|
||||||
|
std::unique_ptr<TimeZoneView> view;
|
||||||
// The eventual appMain() return value - see AlertDialog.cpp's Context::result for why this
|
// The eventual appMain() return value - see AlertDialog.cpp's Context::result for why this
|
||||||
// is a plain (non-atomic) field safely shared between the LVGL thread (writer, before
|
// is a plain (non-atomic) field safely shared between the LVGL thread (writer, before
|
||||||
// emitting APP_EVENT_CLOSE) and this app's own thread (reader, after waking from it).
|
// emitting APP_EVENT_CLOSE) and this app's own thread (reader, after waking from it).
|
||||||
int32_t result = 1; // Cancelled - safety-net default if closed without picking a time zone
|
int32_t result = 1; // Cancelled - safety-net default if closed without picking a time zone
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// The last picked name/code. Static rather than per-instance: simple, and in practice only one
|
// The last picked name/code. Static rather than per-instance: simple, and in practice only one
|
||||||
// TimeZone dialog is ever open at a time. Written on the LVGL thread (the item-selected
|
// TimeZone dialog is ever open at a time. Written before emitting APP_EVENT_CLOSE; read by the
|
||||||
// callback, before emitting APP_EVENT_CLOSE); read by the parent via getLastName()/getLastCode()
|
// parent via getLastName()/getLastCode() after receiving that event - safe without a lock for
|
||||||
// after receiving that event - safe without a lock for the same reason Context::result is (see
|
// the same reason Context::result is (see AlertDialog.cpp).
|
||||||
// AlertDialog.cpp).
|
|
||||||
std::string lastName;
|
std::string lastName;
|
||||||
std::string lastCode;
|
std::string lastCode;
|
||||||
|
|
||||||
bool parseEntry(const std::string& input, std::string& outName, std::string& outCode) {
|
|
||||||
std::string partial_strip = input.substr(1, input.size() - 3);
|
|
||||||
auto first_end_quote = partial_strip.find('"');
|
|
||||||
if (first_end_quote == std::string::npos) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
outName = partial_strip.substr(0, first_end_quote);
|
|
||||||
outCode = partial_strip.substr(first_end_quote + 3);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onTextareaValueChanged(lv_event_t* e) {
|
|
||||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
|
||||||
if (ctx->mutex.lock(100 / portTICK_PERIOD_MS)) {
|
|
||||||
if (ctx->updateTimer->isRunning()) {
|
|
||||||
ctx->updateTimer->stop();
|
|
||||||
}
|
|
||||||
ctx->updateTimer->start();
|
|
||||||
ctx->mutex.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void createListItem(Context* ctx, lv_obj_t* list, const std::string& title, size_t index) {
|
|
||||||
auto* btn = lvgl_list_add_button(list, nullptr, title.c_str());
|
|
||||||
struct ButtonContext {
|
|
||||||
Context* ctx;
|
|
||||||
size_t index;
|
|
||||||
};
|
|
||||||
auto* buttonCtx = new ButtonContext { ctx, index };
|
|
||||||
lv_obj_add_event_cb(btn, [](lv_event_t* e) {
|
|
||||||
auto* buttonCtx = static_cast<ButtonContext*>(lv_event_get_user_data(e));
|
|
||||||
delete buttonCtx;
|
|
||||||
}, LV_EVENT_DELETE, buttonCtx);
|
|
||||||
lv_obj_add_event_cb(btn, [](lv_event_t* e) {
|
|
||||||
auto* buttonCtx = static_cast<ButtonContext*>(lv_event_get_user_data(e));
|
|
||||||
auto* ctx = buttonCtx->ctx;
|
|
||||||
auto index = buttonCtx->index;
|
|
||||||
LOG_I(TAG, "Selected item at index %d", (int)index);
|
|
||||||
|
|
||||||
auto& entry = ctx->entries[index];
|
|
||||||
|
|
||||||
if (ctx->saveTimeZone) {
|
|
||||||
settings::setTimeZone(entry.name, entry.code);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastName = entry.name;
|
|
||||||
lastCode = entry.code;
|
|
||||||
|
|
||||||
ctx->result = 0; // Ok
|
|
||||||
app_event_emit_close(ctx->appInstanceId);
|
|
||||||
}, LV_EVENT_SHORT_CLICKED, buttonCtx);
|
|
||||||
}
|
|
||||||
|
|
||||||
void readTimeZones(Context* ctx, std::string filter) {
|
|
||||||
auto path = std::string(file::MOUNT_POINT_SYSTEM) + "/timezones.csv";
|
|
||||||
auto* file = fopen(path.c_str(), "rb");
|
|
||||||
if (file == nullptr) {
|
|
||||||
LOG_E(TAG, "Failed to open %s", path.c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
char line[96];
|
|
||||||
std::string name;
|
|
||||||
std::string code;
|
|
||||||
uint32_t count = 0;
|
|
||||||
std::vector<TimeZoneEntry> new_entries;
|
|
||||||
while (fgets(line, 96, file)) {
|
|
||||||
if (parseEntry(line, name, code)) {
|
|
||||||
if (string::lowercase(name).find(filter) != std::string::npos) {
|
|
||||||
count++;
|
|
||||||
new_entries.push_back({.name = name, .code = code});
|
|
||||||
|
|
||||||
// Safety guard
|
|
||||||
if (count > 50) {
|
|
||||||
// TODO: Show warning that we're not displaying a complete list
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_E(TAG, "Parse error at line %llu", count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(file);
|
|
||||||
|
|
||||||
if (ctx->mutex.lock(100 / portTICK_PERIOD_MS)) {
|
|
||||||
ctx->entries = std::move(new_entries);
|
|
||||||
ctx->mutex.unlock();
|
|
||||||
} else {
|
|
||||||
LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_I(TAG, "Processed %llu entries", count);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateList(Context* ctx) {
|
|
||||||
if (lvgl_try_lock(200 / portTICK_PERIOD_MS)) {
|
|
||||||
std::string filter = string::lowercase(std::string(lv_textarea_get_text(ctx->filterTextareaWidget)));
|
|
||||||
lvgl_unlock();
|
|
||||||
readTimeZones(ctx, filter);
|
|
||||||
} else {
|
|
||||||
LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "TimeZone LVGL");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lvgl_try_lock(200 / portTICK_PERIOD_MS)) {
|
|
||||||
if (ctx->mutex.lock(100 / portTICK_PERIOD_MS)) {
|
|
||||||
lv_obj_clean(ctx->listWidget);
|
|
||||||
|
|
||||||
uint32_t index = 0;
|
|
||||||
for (auto& entry : ctx->entries) {
|
|
||||||
createListItem(ctx, ctx->listWidget, entry.name, index);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx->mutex.unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
lvgl_unlock();
|
|
||||||
} else {
|
|
||||||
LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "TimeZone LVGL");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onBackPressed(lv_event_t* event) {
|
void onBackPressed(lv_event_t* event) {
|
||||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||||
app_event_emit_close(ctx->appInstanceId);
|
app_event_emit_close(ctx->appInstanceId);
|
||||||
@ -199,32 +53,20 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
|||||||
auto* toolbar = lvgl_toolbar_create(parent, "Select Time zone");
|
auto* toolbar = lvgl_toolbar_create(parent, "Select Time zone");
|
||||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||||
|
|
||||||
auto* search_wrapper = lv_obj_create(parent);
|
// Margins around the view's card
|
||||||
lv_obj_set_size(search_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
auto* wrapper = lv_obj_create(parent);
|
||||||
lv_obj_set_flex_flow(search_wrapper, LV_FLEX_FLOW_ROW);
|
lv_obj_remove_style_all(wrapper);
|
||||||
lv_obj_set_flex_align(search_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START);
|
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||||
lv_obj_set_style_pad_all(search_wrapper, 0, 0);
|
lv_obj_set_flex_grow(wrapper, 1);
|
||||||
lv_obj_set_style_border_width(search_wrapper, 0, 0);
|
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_all(wrapper, LV_DPX(8), LV_STATE_DEFAULT);
|
||||||
|
|
||||||
auto* icon = lv_image_create(search_wrapper);
|
ctx->view->createWidgets(wrapper);
|
||||||
lv_obj_set_style_margin_left(icon, 8, 0);
|
}
|
||||||
lv_obj_set_style_image_recolor_opa(icon, 255, 0);
|
|
||||||
lv_obj_set_style_image_recolor(icon, lv_theme_get_color_primary(parent), 0);
|
|
||||||
lv_obj_set_style_text_font(icon, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
|
||||||
lv_image_set_src(icon, LVGL_ICON_SHARED_SEARCH);
|
|
||||||
|
|
||||||
auto* textarea = lv_textarea_create(search_wrapper);
|
void destroyWidgets(void* userData) {
|
||||||
lv_textarea_set_placeholder_text(textarea, "e.g. Europe/Amsterdam");
|
auto* ctx = static_cast<Context*>(userData);
|
||||||
lv_textarea_set_one_line(textarea, true);
|
ctx->view->destroyWidgets();
|
||||||
lv_obj_add_event_cb(textarea, onTextareaValueChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
|
||||||
ctx->filterTextareaWidget = textarea;
|
|
||||||
lv_obj_set_flex_grow(textarea, 1);
|
|
||||||
|
|
||||||
auto* list = lvgl_list_create(parent);
|
|
||||||
lv_obj_set_width(list, LV_PCT(100));
|
|
||||||
lv_obj_set_flex_grow(list, 1);
|
|
||||||
lv_obj_set_style_border_width(list, 0, 0);
|
|
||||||
ctx->listWidget = list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t appMain(int argc, char* argv[]) {
|
int32_t appMain(int argc, char* argv[]) {
|
||||||
@ -234,9 +76,15 @@ int32_t appMain(int argc, char* argv[]) {
|
|||||||
Context ctx;
|
Context ctx;
|
||||||
ctx.appInstanceId = appInstanceId;
|
ctx.appInstanceId = appInstanceId;
|
||||||
ctx.saveTimeZone = argc > 0 && argv[0][0] == '1';
|
ctx.saveTimeZone = argc > 0 && argv[0][0] == '1';
|
||||||
|
// Picking a time zone, or detecting one, closes the app with it
|
||||||
ctx.updateTimer = std::make_unique<Timer>(Timer::Type::Once, 500 / portTICK_PERIOD_MS, [&ctx] {
|
ctx.view = std::make_unique<TimeZoneView>([&ctx](const std::string& name, const std::string& code) {
|
||||||
updateList(&ctx);
|
if (ctx.saveTimeZone) {
|
||||||
|
settings::setTimeZone(name, code);
|
||||||
|
}
|
||||||
|
lastName = name;
|
||||||
|
lastCode = code;
|
||||||
|
ctx.result = 0; // Ok
|
||||||
|
app_event_emit_close(ctx.appInstanceId);
|
||||||
});
|
});
|
||||||
|
|
||||||
TaskEventGroup event_group {};
|
TaskEventGroup event_group {};
|
||||||
@ -245,8 +93,7 @@ int32_t appMain(int argc, char* argv[]) {
|
|||||||
AppEventSubscription sub {};
|
AppEventSubscription sub {};
|
||||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||||
|
|
||||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||||
ctx.updateTimer->start();
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||||
@ -262,8 +109,8 @@ int32_t appMain(int argc, char* argv[]) {
|
|||||||
if (shouldClose) break;
|
if (shouldClose) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.updateTimer->stop();
|
|
||||||
window_manager_remove(window);
|
window_manager_remove(window);
|
||||||
|
ctx.view.reset();
|
||||||
check(app_event_unsubscribe(&sub) == ERROR_NONE);
|
check(app_event_unsubscribe(&sub) == ERROR_NONE);
|
||||||
task_event_group_destruct(&event_group);
|
task_event_group_destruct(&event_group);
|
||||||
|
|
||||||
|
|||||||
294
Tactility/Source/app/timezone/TimeZoneView.cpp
Normal file
294
Tactility/Source/app/timezone/TimeZoneView.cpp
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
#include <Tactility/app/timezone/TimeZoneView.h>
|
||||||
|
|
||||||
|
#include <Tactility/MountPoints.h>
|
||||||
|
#include <Tactility/StringUtils.h>
|
||||||
|
|
||||||
|
#include <location/location.h>
|
||||||
|
|
||||||
|
#include <tactility/log.h>
|
||||||
|
|
||||||
|
#include <lvgl/lvgl.h>
|
||||||
|
#include <lvgl/fonts.h>
|
||||||
|
#include <lvgl/icons/shared.h>
|
||||||
|
#include <lvgl/widgets/card.h>
|
||||||
|
#include <lvgl/widgets/icon_button.h>
|
||||||
|
#include <lvgl/widgets/list.h>
|
||||||
|
#include <lvgl/widgets/spinner.h>
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
namespace tt::app::timezone {
|
||||||
|
|
||||||
|
constexpr auto* TAG = "TimeZoneView";
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Bounds the list, so a short filter doesn't create hundreds of widgets
|
||||||
|
constexpr size_t MAX_ENTRIES = 50;
|
||||||
|
constexpr TickType_t FILTER_DELAY = pdMS_TO_TICKS(500);
|
||||||
|
|
||||||
|
std::string getTimeZonesPath() {
|
||||||
|
return std::string(file::MOUNT_POINT_SYSTEM) + "/timezones.csv";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parses a line like "Europe/Amsterdam","CET-1CEST,M3.5.0,M10.5.0/3" */
|
||||||
|
bool parseEntry(const std::string& input, std::string& outName, std::string& outCode) {
|
||||||
|
if (input.size() < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::string partial_strip = input.substr(1, input.size() - 3);
|
||||||
|
auto first_end_quote = partial_strip.find('"');
|
||||||
|
if (first_end_quote == std::string::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outName = partial_strip.substr(0, first_end_quote);
|
||||||
|
outCode = partial_strip.substr(first_end_quote + 3);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Calls @a visitor for each time zone until it returns false */
|
||||||
|
void forEachTimeZone(const std::function<bool(const std::string& name, const std::string& code)>& visitor) {
|
||||||
|
auto* file = fopen(getTimeZonesPath().c_str(), "rb");
|
||||||
|
if (file == nullptr) {
|
||||||
|
LOG_E(TAG, "Failed to open %s", getTimeZonesPath().c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char line[96];
|
||||||
|
std::string name;
|
||||||
|
std::string code;
|
||||||
|
while (fgets(line, sizeof(line), file)) {
|
||||||
|
if (parseEntry(line, name, code) && !visitor(name, code)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TimeZoneView::TimeZoneView(OnSelected onSelected) : onSelected(std::move(onSelected)) {
|
||||||
|
filterTimer = std::make_unique<Timer>(Timer::Type::Once, FILTER_DELAY, [this] {
|
||||||
|
loadEntries();
|
||||||
|
lvgl_lock();
|
||||||
|
rebuildList();
|
||||||
|
lvgl_unlock();
|
||||||
|
});
|
||||||
|
loadEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeZoneView::~TimeZoneView() {
|
||||||
|
filterTimer->stop();
|
||||||
|
if (detectThread != nullptr) {
|
||||||
|
detectThread->join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeZoneView::createWidgets(lv_obj_t* parent) {
|
||||||
|
// The search row, the message and the list share a card. Only the list scrolls, so the card keeps its size.
|
||||||
|
auto* card = lvgl_card_create(parent);
|
||||||
|
lv_obj_set_width(card, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(card, 1);
|
||||||
|
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
auto* searchRow = lv_obj_create(card);
|
||||||
|
lv_obj_remove_style_all(searchRow);
|
||||||
|
lv_obj_set_size(searchRow, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_flow(searchRow, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(searchRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_set_style_pad_column(searchRow, LV_DPX(4), LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
auto* searchIcon = lv_image_create(searchRow);
|
||||||
|
lv_obj_set_style_image_recolor_opa(searchIcon, LV_OPA_COVER, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_image_recolor(searchIcon, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_text_font(searchIcon, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||||
|
lv_image_set_src(searchIcon, LVGL_ICON_SHARED_SEARCH);
|
||||||
|
|
||||||
|
textarea = lv_textarea_create(searchRow);
|
||||||
|
lv_textarea_set_placeholder_text(textarea, "e.g. Europe/Amsterdam");
|
||||||
|
lv_textarea_set_one_line(textarea, true);
|
||||||
|
lv_obj_set_flex_grow(textarea, 1);
|
||||||
|
|
||||||
|
// Detects the time zone from the location, with a spinner in its place while it does
|
||||||
|
detectButton = lvgl_icon_button_create(searchRow);
|
||||||
|
auto* detectLabel = lv_label_create(detectButton);
|
||||||
|
lv_obj_set_style_text_font(detectLabel, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||||
|
lv_label_set_text(detectLabel, LVGL_ICON_SHARED_LOCATION_ON);
|
||||||
|
lv_obj_add_event_cb(detectButton, onDetectPressed, LV_EVENT_SHORT_CLICKED, this);
|
||||||
|
detectSpinner = lvgl_spinner_create(searchRow);
|
||||||
|
|
||||||
|
messageLabel = lv_label_create(card);
|
||||||
|
lv_obj_set_width(messageLabel, LV_PCT(100));
|
||||||
|
lv_label_set_long_mode(messageLabel, LV_LABEL_LONG_WRAP);
|
||||||
|
|
||||||
|
list = lvgl_list_create(card);
|
||||||
|
lv_obj_set_width(list, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(list, 1);
|
||||||
|
lv_obj_set_style_border_width(list, 0, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
mutex.lock();
|
||||||
|
lv_textarea_set_text(textarea, filter.c_str());
|
||||||
|
mutex.unlock();
|
||||||
|
// Added after restoring the text, so restoring it doesn't filter the list again
|
||||||
|
lv_obj_add_event_cb(textarea, onFilterChanged, LV_EVENT_VALUE_CHANGED, this);
|
||||||
|
|
||||||
|
showState();
|
||||||
|
rebuildList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeZoneView::destroyWidgets() {
|
||||||
|
textarea = nullptr;
|
||||||
|
detectButton = nullptr;
|
||||||
|
detectSpinner = nullptr;
|
||||||
|
messageLabel = nullptr;
|
||||||
|
list = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeZoneView::setSelectedName(const std::string& name) {
|
||||||
|
mutex.lock();
|
||||||
|
selectedName = name;
|
||||||
|
mutex.unlock();
|
||||||
|
if (list != nullptr) {
|
||||||
|
rebuildList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeZoneView::onFilterChanged(lv_event_t* event) {
|
||||||
|
auto* view = static_cast<TimeZoneView*>(lv_event_get_user_data(event));
|
||||||
|
view->mutex.lock();
|
||||||
|
view->filter = string::lowercase(std::string(lv_textarea_get_text(view->textarea)));
|
||||||
|
view->mutex.unlock();
|
||||||
|
view->filterTimer->stop();
|
||||||
|
view->filterTimer->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeZoneView::onDetectPressed(lv_event_t* event) {
|
||||||
|
auto* view = static_cast<TimeZoneView*>(lv_event_get_user_data(event));
|
||||||
|
if (view->detecting.exchange(true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
view->mutex.lock();
|
||||||
|
view->message.clear();
|
||||||
|
view->mutex.unlock();
|
||||||
|
view->showState();
|
||||||
|
// The previous detection already ended, as detecting was false
|
||||||
|
if (view->detectThread != nullptr) {
|
||||||
|
view->detectThread->join();
|
||||||
|
}
|
||||||
|
view->detectThread = std::make_unique<Thread>("timezone_detect", 4096, [view] {
|
||||||
|
view->detect();
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
view->detectThread->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeZoneView::onEntryPressed(lv_event_t* event) {
|
||||||
|
auto* view = static_cast<TimeZoneView*>(lv_event_get_user_data(event));
|
||||||
|
const auto index = reinterpret_cast<uintptr_t>(lv_obj_get_user_data(lv_event_get_target_obj(event)));
|
||||||
|
view->mutex.lock();
|
||||||
|
if (index >= view->entries.size()) {
|
||||||
|
view->mutex.unlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Entry entry = view->entries[index];
|
||||||
|
view->mutex.unlock();
|
||||||
|
view->select(entry.name, entry.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeZoneView::loadEntries() {
|
||||||
|
mutex.lock();
|
||||||
|
const std::string currentFilter = filter;
|
||||||
|
mutex.unlock();
|
||||||
|
|
||||||
|
std::vector<Entry> matches;
|
||||||
|
forEachTimeZone([&matches, ¤tFilter](const std::string& name, const std::string& code) {
|
||||||
|
if (string::lowercase(name).find(currentFilter) != std::string::npos) {
|
||||||
|
matches.push_back({ name, code });
|
||||||
|
}
|
||||||
|
return matches.size() < MAX_ENTRIES;
|
||||||
|
});
|
||||||
|
|
||||||
|
mutex.lock();
|
||||||
|
entries = std::move(matches);
|
||||||
|
mutex.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called with the LVGL lock held
|
||||||
|
void TimeZoneView::rebuildList() {
|
||||||
|
if (list == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lv_obj_clean(list);
|
||||||
|
mutex.lock();
|
||||||
|
for (size_t index = 0; index < entries.size(); index++) {
|
||||||
|
auto* button = lvgl_list_add_button(list, nullptr, entries[index].name.c_str());
|
||||||
|
lv_obj_set_user_data(button, reinterpret_cast<void*>(static_cast<uintptr_t>(index)));
|
||||||
|
lv_obj_add_event_cb(button, onEntryPressed, LV_EVENT_SHORT_CLICKED, this);
|
||||||
|
if (entries[index].name == selectedName) {
|
||||||
|
lv_obj_add_state(button, LV_STATE_CHECKED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mutex.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called with the LVGL lock held
|
||||||
|
void TimeZoneView::showState() {
|
||||||
|
if (detectButton == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bool isDetecting = detecting;
|
||||||
|
lv_obj_set_hidden(detectButton, isDetecting);
|
||||||
|
lv_obj_set_hidden(detectSpinner, !isDetecting);
|
||||||
|
mutex.lock();
|
||||||
|
lv_label_set_text(messageLabel, message.c_str());
|
||||||
|
lv_obj_set_hidden(messageLabel, message.empty());
|
||||||
|
mutex.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs on the detection thread, as the lookup blocks
|
||||||
|
void TimeZoneView::detect() {
|
||||||
|
char name[LOCATION_TIME_ZONE_MAX_LENGTH + 1];
|
||||||
|
const error_t error = location_lookup_time_zone(name, sizeof(name));
|
||||||
|
std::string code;
|
||||||
|
std::string failure;
|
||||||
|
if (error == ERROR_NONE) {
|
||||||
|
forEachTimeZone([&name, &code](const std::string& entryName, const std::string& entryCode) {
|
||||||
|
if (entryName == name) {
|
||||||
|
code = entryCode;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (code.empty()) {
|
||||||
|
failure = std::string("The detected time zone ") + name + " isn't in the list.";
|
||||||
|
}
|
||||||
|
} else if (error == ERROR_INVALID_STATE) {
|
||||||
|
failure = "Detecting the time zone needs location services. You can turn them on in Settings.";
|
||||||
|
} else {
|
||||||
|
failure = "The time zone couldn't be detected. Check the internet connection and try again.";
|
||||||
|
}
|
||||||
|
|
||||||
|
lvgl_lock();
|
||||||
|
detecting = false;
|
||||||
|
mutex.lock();
|
||||||
|
message = failure;
|
||||||
|
mutex.unlock();
|
||||||
|
showState();
|
||||||
|
if (failure.empty()) {
|
||||||
|
LOG_I(TAG, "Detected %s", name);
|
||||||
|
// Shows the detected time zone in the list
|
||||||
|
if (textarea != nullptr) {
|
||||||
|
lv_textarea_set_text(textarea, name);
|
||||||
|
}
|
||||||
|
select(name, code);
|
||||||
|
}
|
||||||
|
lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called with the LVGL lock held
|
||||||
|
void TimeZoneView::select(const std::string& name, const std::string& code) {
|
||||||
|
setSelectedName(name);
|
||||||
|
onSelected(name, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -46,4 +46,63 @@ bool hasRadio() {
|
|||||||
return device_exists_of_type(&RADIO_TYPE);
|
return device_exists_of_type(&RADIO_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RadioRegion findRegionForTimeZone(const std::string& timeZoneName) {
|
||||||
|
struct ZoneRegion {
|
||||||
|
const char* zone;
|
||||||
|
RadioRegion region;
|
||||||
|
};
|
||||||
|
// Exact names before the area prefixes below, e.g. the Russian zones in Europe and Asia
|
||||||
|
static constexpr ZoneRegion ZONES[] = {
|
||||||
|
{ "Europe/Moscow", RADIO_REGION_RU_868 },
|
||||||
|
{ "Europe/Kaliningrad", RADIO_REGION_RU_868 },
|
||||||
|
{ "Europe/Samara", RADIO_REGION_RU_868 },
|
||||||
|
{ "Europe/Volgograd", RADIO_REGION_RU_868 },
|
||||||
|
{ "Europe/Kirov", RADIO_REGION_RU_868 },
|
||||||
|
{ "Europe/Astrakhan", RADIO_REGION_RU_868 },
|
||||||
|
{ "Europe/Saratov", RADIO_REGION_RU_868 },
|
||||||
|
{ "Europe/Ulyanovsk", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Yekaterinburg", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Omsk", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Novosibirsk", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Barnaul", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Tomsk", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Novokuznetsk", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Krasnoyarsk", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Irkutsk", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Chita", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Yakutsk", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Khandyga", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Vladivostok", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Ust-Nera", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Magadan", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Sakhalin", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Srednekolymsk", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Kamchatka", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Anadyr", RADIO_REGION_RU_868 },
|
||||||
|
{ "Asia/Shanghai", RADIO_REGION_CN_470 },
|
||||||
|
{ "Asia/Urumqi", RADIO_REGION_CN_470 },
|
||||||
|
{ "Asia/Kolkata", RADIO_REGION_IN_865 },
|
||||||
|
{ "Asia/Calcutta", RADIO_REGION_IN_865 },
|
||||||
|
{ "Asia/Tokyo", RADIO_REGION_JP_920 },
|
||||||
|
{ "Pacific/Auckland", RADIO_REGION_NZ_865 },
|
||||||
|
{ "Pacific/Chatham", RADIO_REGION_NZ_865 },
|
||||||
|
};
|
||||||
|
static constexpr ZoneRegion AREAS[] = {
|
||||||
|
{ "Europe/", RADIO_REGION_EU_868 },
|
||||||
|
{ "America/", RADIO_REGION_US_915 },
|
||||||
|
{ "Australia/", RADIO_REGION_AU_915 },
|
||||||
|
};
|
||||||
|
for (const auto& zone : ZONES) {
|
||||||
|
if (timeZoneName == zone.zone) {
|
||||||
|
return zone.region;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& area : AREAS) {
|
||||||
|
if (timeZoneName.starts_with(area.zone)) {
|
||||||
|
return area.region;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return RADIO_REGION_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
#include <service/paths.h>
|
#include <service/paths.h>
|
||||||
|
|
||||||
|
#include <Tactility/settings/RadioSettings.h>
|
||||||
#include <Tactility/settings/Time.h>
|
#include <Tactility/settings/Time.h>
|
||||||
#include <Tactility/settings/SystemSettings.h>
|
#include <Tactility/settings/SystemSettings.h>
|
||||||
|
|
||||||
#include <tactility/paths.h>
|
#include <tactility/paths.h>
|
||||||
#include <tactility/preferences.h>
|
#include <tactility/preferences.h>
|
||||||
|
#include <tactility/log.h>
|
||||||
#include <tactility/system_event.h>
|
#include <tactility/system_event.h>
|
||||||
|
|
||||||
#include <lvgl/widgets/statusbar.h>
|
#include <lvgl/widgets/statusbar.h>
|
||||||
@ -15,6 +17,8 @@
|
|||||||
|
|
||||||
namespace tt::settings {
|
namespace tt::settings {
|
||||||
|
|
||||||
|
constexpr auto* TAG = "Time";
|
||||||
|
|
||||||
constexpr auto* TIMEZONE_PREFERENCES_KEY_NAME = "tz_name";
|
constexpr auto* TIMEZONE_PREFERENCES_KEY_NAME = "tz_name";
|
||||||
constexpr auto* TIMEZONE_PREFERENCES_KEY_CODE = "tz_code";
|
constexpr auto* TIMEZONE_PREFERENCES_KEY_CODE = "tz_code";
|
||||||
constexpr auto* TIMEZONE_PREFERENCES_KEY_TIME24 = "tz_time24";
|
constexpr auto* TIMEZONE_PREFERENCES_KEY_TIME24 = "tz_time24";
|
||||||
@ -61,6 +65,14 @@ void setTimeZone(const std::string& name, const std::string& code) {
|
|||||||
tzset();
|
tzset();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// The time zone tells where the device is, and so which radio band it may use
|
||||||
|
if (radio::hasRadio()) {
|
||||||
|
const RadioRegion region = radio::findRegionForTimeZone(name);
|
||||||
|
if (region != RADIO_REGION_UNSET && !radio::saveRegion(region)) {
|
||||||
|
LOG_E(TAG, "Failed to save the radio region");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
|
system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user