mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Compare commits
3 Commits
0f12fcce53
...
13d77bc547
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13d77bc547 | ||
|
|
33a830cecf | ||
|
|
24ef79070a |
@ -2,6 +2,7 @@ import os.path
|
||||
from textwrap import dedent
|
||||
from source.models import *
|
||||
from .exception import DevicetreeException
|
||||
from .printing import print_warning
|
||||
|
||||
def write_include(file, include: IncludeC, verbose: bool):
|
||||
if verbose:
|
||||
@ -59,6 +60,35 @@ def find_phandle(devices: list[Device], phandle: str):
|
||||
return f"&{get_device_node_name_safe(device)}"
|
||||
raise DevicetreeException(f"phandle '{phandle}' not found in devicetree")
|
||||
|
||||
def parse_number(text: str):
|
||||
try:
|
||||
return int(text, 0)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def get_device_address(device: Device) -> str:
|
||||
"""The node's unit address (e.g. "display@0"), or else the first value of its "reg" property (e.g. an I2C address)."""
|
||||
reg_property = find_device_property(device, "reg")
|
||||
reg_value = None
|
||||
if reg_property is not None:
|
||||
if reg_property.type == "value":
|
||||
reg_value = reg_property.value
|
||||
elif reg_property.type == "values" and len(reg_property.value) > 0:
|
||||
reg_value = str(reg_property.value[0])
|
||||
if device.node_address is None:
|
||||
return reg_value if reg_value is not None else "0"
|
||||
# Unit addresses are hexadecimal, like in Linux
|
||||
try:
|
||||
unit_address = int(device.node_address, 16)
|
||||
except ValueError:
|
||||
raise DevicetreeException(f"Unit address of {device.node_name}@{device.node_address} isn't a hexadecimal number")
|
||||
# Like Linux's dtc, the unit address should match the first "reg" value
|
||||
if reg_value is not None:
|
||||
reg_number = parse_number(reg_value)
|
||||
if reg_number is not None and unit_address != reg_number:
|
||||
print_warning(f"{device.node_name}@{device.node_address} has a different reg value: {reg_value}")
|
||||
return str(unit_address)
|
||||
|
||||
def property_to_string(property: DeviceProperty, devices: list[Device]) -> str:
|
||||
type = property.type
|
||||
if type == "value" or type == "int":
|
||||
@ -277,7 +307,7 @@ def write_device_structs(file, device: Device, parent_device: Device, bindings:
|
||||
# Write config struct
|
||||
write_config(file, device, bindings, devices, type_name)
|
||||
# Write device struct
|
||||
address_value = device.node_address if device.node_address is not None else "0"
|
||||
address_value = get_device_address(device)
|
||||
file.write(f"static struct Device {node_name}" " = {\n")
|
||||
file.write(f"\t.address = {address_value},\n")
|
||||
file.write(f"\t.name = \"{device.node_name}\",\n") # Use original name
|
||||
|
||||
@ -295,6 +295,86 @@ def test_array_property_defaults_to_null_when_absent():
|
||||
print("PASSED")
|
||||
return True
|
||||
|
||||
def write_reg_config(tmp_dir, node):
|
||||
config_dir = os.path.join(tmp_dir, "reg_data")
|
||||
bindings_dir = os.path.join(config_dir, "bindings")
|
||||
os.makedirs(bindings_dir)
|
||||
|
||||
with open(os.path.join(config_dir, "module.yaml"), "w") as f:
|
||||
f.write("dts: test.dts\nbindings: bindings")
|
||||
|
||||
with open(os.path.join(config_dir, "test.dts"), "w") as f:
|
||||
f.write(f"""/dts-v1/;
|
||||
|
||||
/ {{
|
||||
compatible = "test,root";
|
||||
model = "Test Model";
|
||||
|
||||
{node} {{
|
||||
compatible = "test,reg-device";
|
||||
reg = <0x5D>;
|
||||
}};
|
||||
}};
|
||||
""")
|
||||
|
||||
with open(os.path.join(bindings_dir, "test,root.yaml"), "w") as f:
|
||||
f.write("description: Test root binding\ncompatible: \"test,root\"\nproperties:\n model:\n type: string\n")
|
||||
|
||||
with open(os.path.join(bindings_dir, "test,reg-device.yaml"), "w") as f:
|
||||
f.write("description: Test reg binding\ncompatible: \"test,reg-device\"\nproperties:\n reg:\n type: int\n")
|
||||
|
||||
return config_dir
|
||||
|
||||
def test_reg_is_address_without_unit_address():
|
||||
print("Running test_reg_is_address_without_unit_address...")
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_dir = write_reg_config(tmp_dir, "touch")
|
||||
output_dir = os.path.join(tmp_dir, "output")
|
||||
os.makedirs(output_dir)
|
||||
|
||||
result = run_compiler(config_dir, output_dir)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"FAILED: Compilation should have succeeded: {result.stderr} {result.stdout}")
|
||||
return False
|
||||
|
||||
with open(os.path.join(output_dir, "devicetree.c")) as f:
|
||||
generated = f.read()
|
||||
|
||||
if ".address = 0x5D," not in generated:
|
||||
print(f"FAILED: Expected the reg value as address:\n{generated}")
|
||||
return False
|
||||
|
||||
print("PASSED")
|
||||
return True
|
||||
|
||||
def test_unit_address_mismatch_warns():
|
||||
print("Running test_unit_address_mismatch_warns...")
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
config_dir = write_reg_config(tmp_dir, "touch@10")
|
||||
output_dir = os.path.join(tmp_dir, "output")
|
||||
os.makedirs(output_dir)
|
||||
|
||||
result = run_compiler(config_dir, output_dir)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"FAILED: Compilation should have succeeded: {result.stderr} {result.stdout}")
|
||||
return False
|
||||
|
||||
if "different reg value" not in result.stdout:
|
||||
print(f"FAILED: Expected a warning, got: {result.stdout}")
|
||||
return False
|
||||
|
||||
with open(os.path.join(output_dir, "devicetree.c")) as f:
|
||||
generated = f.read()
|
||||
|
||||
if ".address = 16," not in generated:
|
||||
print(f"FAILED: Expected the unit address as address:\n{generated}")
|
||||
return False
|
||||
|
||||
print("PASSED")
|
||||
return True
|
||||
|
||||
def test_compile_missing_config():
|
||||
print("Running test_compile_missing_config...")
|
||||
with tempfile.TemporaryDirectory() as output_dir:
|
||||
@ -322,7 +402,9 @@ if __name__ == "__main__":
|
||||
test_minmax_out_of_range_default_fails,
|
||||
test_minmax_symbolic_value_skips_validation,
|
||||
test_array_property_generates_static_array_and_length,
|
||||
test_array_property_defaults_to_null_when_absent
|
||||
test_array_property_defaults_to_null_when_absent,
|
||||
test_reg_is_address_without_unit_address,
|
||||
test_unit_address_mismatch_warns
|
||||
]
|
||||
|
||||
failed = 0
|
||||
|
||||
@ -104,9 +104,10 @@ def main():
|
||||
# lvgl (basics)
|
||||
{'src': 'build/esp-idf/lvgl__lvgl/liblvgl__lvgl.a', 'dst': 'Libraries/lvgl/binary/liblvgl.a'},
|
||||
{'src': 'Libraries/lvgl/lvgl.h', 'dst': 'Libraries/lvgl/include/'},
|
||||
{'src': 'Libraries/lvgl/lv_version.h', 'dst': 'Libraries/lvgl/include/'},
|
||||
{'src': 'Libraries/lvgl/lvgl_private.h', 'dst': 'Libraries/lvgl/include/'},
|
||||
{'src': 'Libraries/lvgl/include/**/*.h', 'dst': 'Libraries/lvgl/include/include/'},
|
||||
{'src': 'Libraries/lvgl/LICENCE*.*', 'dst': 'Libraries/lvgl/'},
|
||||
{'src': 'Libraries/lvgl/src/lv_conf_kconfig.h', 'dst': 'Libraries/lvgl/include/lv_conf.h'},
|
||||
{'src': 'Libraries/lvgl/include/lvgl/config/lv_conf_kconfig.h', 'dst': 'Libraries/lvgl/include/lv_conf.h'},
|
||||
{'src': 'Libraries/lvgl/src/**/*.h', 'dst': 'Libraries/lvgl/include/src/'},
|
||||
# elf_loader
|
||||
{'src': 'managed_components/espressif__elf_loader/*.cmake', 'dst': 'Libraries/elf_loader/'},
|
||||
|
||||
@ -69,7 +69,8 @@ def main():
|
||||
# lvgl (basics)
|
||||
{'src': 'buildsim/Libraries/lvgl/lib/liblvgl.a', 'dst': 'Libraries/lvgl/binary/liblvgl.a'},
|
||||
{'src': 'Libraries/lvgl/lvgl.h', 'dst': 'Libraries/lvgl/include/'},
|
||||
{'src': 'Libraries/lvgl/lv_version.h', 'dst': 'Libraries/lvgl/include/'},
|
||||
{'src': 'Libraries/lvgl/lvgl_private.h', 'dst': 'Libraries/lvgl/include/'},
|
||||
{'src': 'Libraries/lvgl/include/**/*.h', 'dst': 'Libraries/lvgl/include/include/'},
|
||||
{'src': 'Libraries/lvgl/LICENCE*.*', 'dst': 'Libraries/lvgl/'},
|
||||
{'src': 'lv_conf.h', 'dst': 'Libraries/lvgl/include/'},
|
||||
{'src': 'Libraries/lvgl/src/**/*.h', 'dst': 'Libraries/lvgl/include/src/'},
|
||||
|
||||
@ -31,7 +31,6 @@ CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
|
||||
# size, and a smaller buffer fails mid-transfer with MBEDTLS_ERR_SSL_INVALID_RECORD (-0x7200).
|
||||
CONFIG_MBEDTLS_DYNAMIC_BUFFER=y
|
||||
# LVGL
|
||||
CONFIG_LV_USE_USER_DATA=y
|
||||
CONFIG_LV_USE_FS_STDIO=y
|
||||
CONFIG_LV_FS_STDIO_LETTER=65
|
||||
CONFIG_LV_FS_STDIO_PATH=""
|
||||
|
||||
@ -215,7 +215,8 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
|
||||
set(CONFIG_LV_BUILD_DEMOS OFF CACHE BOOL "" FORCE)
|
||||
set(CONFIG_LV_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(Libraries/lvgl) # Added as idf component for ESP and as library for other targets
|
||||
target_link_libraries(lvgl PRIVATE SDL2-static)
|
||||
# PUBLIC: lvgl.h includes the SDL driver headers when LV_USE_PRIVATE_API is set
|
||||
target_link_libraries(lvgl PUBLIC SDL2-static)
|
||||
|
||||
# Tests
|
||||
add_subdirectory(Tests)
|
||||
|
||||
@ -113,7 +113,7 @@ void tab5_create_devices_v1(Device* i2c0) {
|
||||
.dsi_bus_id = 0,
|
||||
.num_data_lanes = 2,
|
||||
.lane_bit_rate_mbps = 1000,
|
||||
.dpi_clock_freq_mhz = 60,
|
||||
.dpi_clock_freq_mhz = 80,
|
||||
.hsync_pulse_width = 40,
|
||||
.hsync_back_porch = 140,
|
||||
.hsync_front_porch = 40,
|
||||
|
||||
@ -8,11 +8,7 @@
|
||||
## Higher Priority
|
||||
|
||||
- Update to LVGL 9.6.x (ESP-IDF and POSIX)
|
||||
- Terminal app: on devices with pointer device: LVGL rendering mode should hide the toolbar by default, but tapping the terminal should toggle the visibility of the toolbar.
|
||||
- Terminal app: should always run in non-LVGL mode if no PSRAM is present.
|
||||
- Crash app should work without LVGL, like Boot app. It should wait for any input (keyboard or pointer) to continue.
|
||||
- NimBLE looses pairing key after reboot
|
||||
- CrashDiagnostics shouldn't show a QR when there's no callstack
|
||||
- Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp
|
||||
- Add bold fonts for e-ink readability improvement
|
||||
- Improve Setup: Add keyboard/keypad/encoder navigation explanation.
|
||||
@ -48,7 +44,6 @@
|
||||
- Consider implementing LVGL gridnav in apps https://lvgl.io/docs/open/9.3/details/auxiliary-modules/gridnav.html
|
||||
- Make USB host driver disabled by default, so it doesn't consume memory
|
||||
- TactilityTool: Make API compatibility table (and check for compatibility in the tool itself)
|
||||
- Bug: Crash handling app cannot be exited with an EncoderDevice. (current work-around is to manually reset the device)
|
||||
- Refactor HttpServer into C code and move implementation to http-module
|
||||
- Use GPS time to set/update the current time
|
||||
- Make a URL handler. Use it for handling local files. Match file types with apps.
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit e9db313f8ccc07e27f6afd0f34fe3cf6f20596f0
|
||||
Subproject commit 60b614c23c816ca5edc5f2840c9945eff7da0ad4
|
||||
@ -19,6 +19,7 @@ ESP_PLATFORM` in `source/module.cpp`; symbols outside that gate (e.g. `__cxa_pur
|
||||
|
||||
- `operator new(unsigned int)` / `operator delete(void*, unsigned int)` (`_Znwj` / `_ZdlPvj`)
|
||||
- `operator new[](unsigned int)` / `operator delete[](void*, unsigned int)` (`_Znaj` / `_ZdaPvj`)
|
||||
- `operator new[](unsigned int, std::nothrow_t const&)` (`_ZnajRKSt9nothrow_t`)
|
||||
- `operator delete(void*)` / `operator delete[](void*)` (`_ZdlPv` / `_ZdaPv`) - the unsized forms,
|
||||
used instead of the above when the compiler determines no size is needed
|
||||
- `std::nothrow`
|
||||
|
||||
@ -31,6 +31,7 @@ extern "C" {
|
||||
extern void* _Znwj(uint32_t size); // operator new(unsigned int)
|
||||
extern void _ZdlPvj(void* p, uint64_t size); // operator delete(void*, unsigned int)
|
||||
extern void* _Znaj(uint32_t size); // operator new[](unsigned int)
|
||||
extern void* _ZnajRKSt9nothrow_t(uint32_t size, const std::nothrow_t& tag); // operator new[](unsigned int, std::nothrow_t const&)
|
||||
extern void _ZdaPvj(void* p, uint64_t size); // operator delete[](void*, unsigned int)
|
||||
// Unsized forms: the compiler picks these over the sized ones above depending on context
|
||||
// (e.g. trivially-destructible types needing no array cookie), so both must be exported.
|
||||
@ -572,6 +573,7 @@ static const ModuleSymbol SYMBOLS[] = {
|
||||
DEFINE_MODULE_SYMBOL(_Znwj), // operator new(unsigned int)
|
||||
DEFINE_MODULE_SYMBOL(_ZdlPvj), // operator delete(void*, unsigned int)
|
||||
DEFINE_MODULE_SYMBOL(_Znaj), // operator new[](unsigned int)
|
||||
DEFINE_MODULE_SYMBOL(_ZnajRKSt9nothrow_t), // operator new[](unsigned int, std::nothrow_t const&)
|
||||
DEFINE_MODULE_SYMBOL(_ZdaPvj), // operator delete[](void*, unsigned int)
|
||||
DEFINE_MODULE_SYMBOL(_ZdlPv), // operator delete(void*)
|
||||
DEFINE_MODULE_SYMBOL(_ZdaPv), // operator delete[](void*)
|
||||
|
||||
@ -149,8 +149,10 @@ shared_symbol_code_point_names = [
|
||||
"download",
|
||||
"forum", # Chat app
|
||||
"gamepad",
|
||||
"hard_disk", # System Info
|
||||
"help", # Diceware help
|
||||
"hub", # App Hub
|
||||
"info", # System Info
|
||||
"image", # Screenshot app
|
||||
"keyboard_arrow_up",
|
||||
"lightbulb",
|
||||
@ -158,6 +160,7 @@ shared_symbol_code_point_names = [
|
||||
"lists", # Chat app toolbar
|
||||
"mail",
|
||||
"menu",
|
||||
"memory", # System Info
|
||||
"mop",
|
||||
"more_vert",
|
||||
"music_note",
|
||||
@ -166,6 +169,7 @@ shared_symbol_code_point_names = [
|
||||
"power_settings_new", # Power off for T-Lora Pager
|
||||
"refresh", # e.g. App Hub reload button
|
||||
"search",
|
||||
"select_window_2", # System Info
|
||||
"settings",
|
||||
"toolbar", # Apps without custom icon
|
||||
"navigation", # GPS (settings) app
|
||||
@ -177,6 +181,7 @@ shared_symbol_code_point_names = [
|
||||
"logo_dev",
|
||||
"camera",
|
||||
"lightstrip",
|
||||
"phone_android", # Display settings: orientation
|
||||
]
|
||||
|
||||
# Get more from https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded
|
||||
|
||||
@ -9,7 +9,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/** Changes whenever the icon TTF subset, its codepoints file or the icon name lists change */
|
||||
#define LVGL_ICON_FONT_VERSION 0x1B608834u
|
||||
#define LVGL_ICON_FONT_VERSION 0xC28E1A92u
|
||||
|
||||
// Icon names per icon font, resolved through the .codepoints file next to the TTF
|
||||
extern const char* const lvgl_icon_shared_names[];
|
||||
|
||||
@ -22,8 +22,10 @@
|
||||
#define LVGL_ICON_SHARED_DOWNLOAD "\xEF\x82\x90"
|
||||
#define LVGL_ICON_SHARED_FORUM "\xEE\xA2\xAF"
|
||||
#define LVGL_ICON_SHARED_GAMEPAD "\xEE\x8C\x8F"
|
||||
#define LVGL_ICON_SHARED_HARD_DISK "\xEF\x8F\x9A"
|
||||
#define LVGL_ICON_SHARED_HELP "\xEE\xA3\xBD"
|
||||
#define LVGL_ICON_SHARED_HUB "\xEE\xA7\xB4"
|
||||
#define LVGL_ICON_SHARED_INFO "\xEE\xA2\x8E"
|
||||
#define LVGL_ICON_SHARED_IMAGE "\xEE\x8F\xB4"
|
||||
#define LVGL_ICON_SHARED_KEYBOARD_ARROW_UP "\xEE\x8C\x96"
|
||||
#define LVGL_ICON_SHARED_LIGHTBULB "\xEE\xA4\x8F"
|
||||
@ -31,6 +33,7 @@
|
||||
#define LVGL_ICON_SHARED_LISTS "\xEE\xA6\xB9"
|
||||
#define LVGL_ICON_SHARED_MAIL "\xEE\x85\x99"
|
||||
#define LVGL_ICON_SHARED_MENU "\xEE\x97\x92"
|
||||
#define LVGL_ICON_SHARED_MEMORY "\xEE\x8C\xA2"
|
||||
#define LVGL_ICON_SHARED_MOP "\xEE\x8A\x8D"
|
||||
#define LVGL_ICON_SHARED_MORE_VERT "\xEE\x97\x94"
|
||||
#define LVGL_ICON_SHARED_MUSIC_NOTE "\xEE\x90\x85"
|
||||
@ -39,6 +42,7 @@
|
||||
#define LVGL_ICON_SHARED_POWER_SETTINGS_NEW "\xEF\xA3\x87"
|
||||
#define LVGL_ICON_SHARED_REFRESH "\xEE\x97\x95"
|
||||
#define LVGL_ICON_SHARED_SEARCH "\xEE\xBD\xBA"
|
||||
#define LVGL_ICON_SHARED_SELECT_WINDOW_2 "\xEF\x93\x88"
|
||||
#define LVGL_ICON_SHARED_SETTINGS "\xEE\xA2\xB8"
|
||||
#define LVGL_ICON_SHARED_TOOLBAR "\xEE\xA7\xB7"
|
||||
#define LVGL_ICON_SHARED_NAVIGATION "\xEE\x95\x9D"
|
||||
@ -50,3 +54,4 @@
|
||||
#define LVGL_ICON_SHARED_LOGO_DEV "\xEE\xAB\x96"
|
||||
#define LVGL_ICON_SHARED_CAMERA "\xEE\x8E\xAF"
|
||||
#define LVGL_ICON_SHARED_LIGHTSTRIP "\xF3\xBF\xBD\xB5"
|
||||
#define LVGL_ICON_SHARED_PHONE_ANDROID "\xEF\x8B\x9B"
|
||||
|
||||
@ -49,11 +49,11 @@ void lvgl_unlock(void);
|
||||
bool lvgl_is_running(void);
|
||||
|
||||
/**
|
||||
* @brief Gets the desired UI density for the target hardware.
|
||||
* The density is defined in the `device.properties` of a hardware device.
|
||||
* This setting is read by CMakeLists.txt and passed as a target compile definition of the LVGL module.
|
||||
* @brief Gets the UI density of the theme settings.
|
||||
* @deprecated Use lvgl_theme_is_compact() from <lvgl/theme.h>
|
||||
* @return the UI density
|
||||
*/
|
||||
__attribute__((deprecated("Use lvgl_theme_is_compact()")))
|
||||
enum UiDensity lvgl_get_ui_density(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@ -17,6 +17,11 @@ typedef struct {
|
||||
lv_color_t color_primary;
|
||||
lv_color_t color_secondary;
|
||||
lv_color_t color_error;
|
||||
/** Smaller spacing and controls, for small screens (see lvgl_theme_is_compact()) */
|
||||
bool is_compact;
|
||||
/** Use color_surface as the surface (screen background) colour, instead of the theme's own. Not used by the monochrome theme. */
|
||||
bool surface_override;
|
||||
lv_color_t color_surface;
|
||||
} LvglThemeSettings;
|
||||
|
||||
/**
|
||||
@ -37,6 +42,13 @@ void lvgl_theme_get_default_settings(LvglThemeSettings* settings);
|
||||
*/
|
||||
bool lvgl_theme_is_mono(void);
|
||||
|
||||
/**
|
||||
* @brief Checks whether the theme settings use the compact UI density: smaller spacing and controls, for small screens.
|
||||
* The device's default density is defined in its `device.properties`.
|
||||
* @return true when the UI is compact
|
||||
*/
|
||||
bool lvgl_theme_is_compact(void);
|
||||
|
||||
/**
|
||||
* @brief Sets the theme settings. They are used for displays that are added afterwards, e.g. when LVGL (re)starts.
|
||||
* @param[in] settings the theme settings
|
||||
|
||||
36
Modules/lvgl-module/include/lvgl/widgets/list.h
Normal file
36
Modules/lvgl-module/include/lvgl/widgets/list.h
Normal file
@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Creates a list: a flex column of full-width text rows and buttons.
|
||||
* @param[in] parent the parent object for the new list
|
||||
* @return the created list
|
||||
*/
|
||||
lv_obj_t* lvgl_list_create(lv_obj_t* parent);
|
||||
|
||||
/**
|
||||
* @brief Adds a full-width text row (a section header) to a list.
|
||||
* @param[in] list the list created with lvgl_list_create()
|
||||
* @param[in] text the text of the row, or NULL to leave it empty
|
||||
* @return the created label
|
||||
*/
|
||||
lv_obj_t* lvgl_list_add_text(lv_obj_t* list, const char* text);
|
||||
|
||||
/**
|
||||
* @brief Adds a full-width button with an optional icon and text to a list.
|
||||
* @param[in] list the list created with lvgl_list_create()
|
||||
* @param[in] icon an image source (e.g. a symbol), or NULL for no icon
|
||||
* @param[in] text the button text, or NULL for no text
|
||||
* @return the created button
|
||||
*/
|
||||
lv_obj_t* lvgl_list_add_button(lv_obj_t* list, const void* icon, const char* text);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -247,7 +247,7 @@ static void textarea_hide_keyboard(lv_event_t* event) {
|
||||
|
||||
void lvgl_software_keyboard_construct(LvglSoftwareKeyboard* keyboard, lv_obj_t* parent) {
|
||||
keyboard->object = lv_keyboard_create(parent);
|
||||
lv_obj_add_flag(keyboard->object, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(keyboard->object, true);
|
||||
last_software_keyboard = *keyboard;
|
||||
}
|
||||
|
||||
@ -262,13 +262,13 @@ void lvgl_software_keyboard_destruct(LvglSoftwareKeyboard* keyboard) {
|
||||
|
||||
void lvgl_software_keyboard_show(LvglSoftwareKeyboard* keyboard, lv_obj_t* textarea) {
|
||||
assert(keyboard->object != nullptr);
|
||||
lv_obj_clear_flag(keyboard->object, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(keyboard->object, false);
|
||||
lv_keyboard_set_textarea(keyboard->object, textarea);
|
||||
}
|
||||
|
||||
void lvgl_software_keyboard_hide(LvglSoftwareKeyboard* keyboard) {
|
||||
assert(keyboard->object != nullptr);
|
||||
lv_obj_add_flag(keyboard->object, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(keyboard->object, true);
|
||||
}
|
||||
|
||||
bool lvgl_software_keyboard_is_enabled() {
|
||||
|
||||
@ -60,15 +60,15 @@ static void show_cursor(LvglTrackballCtx* ctx, lv_indev_t* indev) {
|
||||
if (ctx->cursor == nullptr) {
|
||||
return;
|
||||
}
|
||||
lv_obj_remove_flag(ctx->cursor, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_clickable(ctx->cursor, false);
|
||||
lv_indev_set_cursor(indev, ctx->cursor);
|
||||
}
|
||||
|
||||
lv_image_set_src(ctx->cursor, ctx->cursor_image_src);
|
||||
if (ctx->settings.enabled) {
|
||||
lv_obj_remove_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->cursor, false);
|
||||
} else {
|
||||
lv_obj_add_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->cursor, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ static void hide_cursor(LvglTrackballCtx* ctx) {
|
||||
if (ctx->cursor == nullptr) {
|
||||
return;
|
||||
}
|
||||
lv_obj_add_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->cursor, true);
|
||||
}
|
||||
|
||||
// Reports one key event per read: the button as the enter key, and movement as arrow key presses along its strongest axis.
|
||||
@ -280,9 +280,9 @@ error_t lvgl_trackball_set_settings(lv_indev_t* indev, const struct LvglTrackbal
|
||||
// hide_cursor() just hid above (enabled is independent of mode, and defaults to true).
|
||||
if (ctx->cursor != nullptr && ctx->settings.mode == LVGL_TRACKBALL_MODE_POINTER) {
|
||||
if (ctx->settings.enabled) {
|
||||
lv_obj_remove_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->cursor, false);
|
||||
} else {
|
||||
lv_obj_add_flag(ctx->cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->cursor, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl/grid_navigation.h>
|
||||
|
||||
// Marks containers with grid navigation, so the keyboard sends them up and down keys
|
||||
constexpr lv_obj_flag_t GRID_NAVIGATION_FLAG = LV_OBJ_FLAG_USER_1;
|
||||
|
||||
static void remove_from_group(lv_obj_t* child) {
|
||||
if (lv_obj_get_group(child) != nullptr) {
|
||||
lv_group_remove_obj(child);
|
||||
@ -19,15 +16,115 @@ static void on_child_created(lv_event_t* event) {
|
||||
}
|
||||
}
|
||||
|
||||
// Matches which children grid navigation can focus
|
||||
static bool is_focusable(lv_obj_t* obj) {
|
||||
return !lv_obj_is_hidden(obj) && lv_obj_is_clickable(obj) && lv_obj_is_click_focusable(obj);
|
||||
}
|
||||
|
||||
// Finds the next focusable child after (or before) the given index
|
||||
static lv_obj_t* find_focusable_child(lv_obj_t* container, int32_t index, bool next) {
|
||||
const auto child_count = static_cast<int32_t>(lv_obj_get_child_count(container));
|
||||
for (index += next ? 1 : -1; index >= 0 && index < child_count; index += next ? 1 : -1) {
|
||||
auto* child = lv_obj_get_child(container, index);
|
||||
if (is_focusable(child)) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static int32_t get_center_x(lv_obj_t* obj) {
|
||||
lv_area_t area;
|
||||
lv_obj_get_coords(obj, &area);
|
||||
return area.x1 + lv_area_get_width(&area) / 2;
|
||||
}
|
||||
|
||||
static int32_t get_center_y(lv_obj_t* obj) {
|
||||
lv_area_t area;
|
||||
lv_obj_get_coords(obj, &area);
|
||||
return area.y1 + lv_area_get_height(&area) / 2;
|
||||
}
|
||||
|
||||
// Finds the child in the top (or bottom) row that's horizontally closest to the reference child.
|
||||
// The reference child is returned when it's in that row already.
|
||||
static lv_obj_t* find_edge_row_child(lv_obj_t* container, bool top, lv_obj_t* reference) {
|
||||
const uint32_t child_count = lv_obj_get_child_count(container);
|
||||
bool found = false;
|
||||
int32_t edge_y = 0;
|
||||
for (uint32_t i = 0; i < child_count; i++) {
|
||||
auto* child = lv_obj_get_child(container, static_cast<int32_t>(i));
|
||||
if (is_focusable(child)) {
|
||||
const int32_t y = get_center_y(child);
|
||||
if (!found || (top ? y < edge_y : y > edge_y)) {
|
||||
edge_y = y;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return nullptr;
|
||||
}
|
||||
if (reference != nullptr && get_center_y(reference) == edge_y) {
|
||||
return reference;
|
||||
}
|
||||
const int32_t reference_x = reference != nullptr ? get_center_x(reference) : 0;
|
||||
lv_obj_t* closest = nullptr;
|
||||
int32_t closest_distance = 0;
|
||||
for (uint32_t i = 0; i < child_count; i++) {
|
||||
auto* child = lv_obj_get_child(container, static_cast<int32_t>(i));
|
||||
if (is_focusable(child) && get_center_y(child) == edge_y) {
|
||||
const int32_t distance = LV_ABS(get_center_x(child) - reference_x);
|
||||
if (closest == nullptr || distance < closest_distance) {
|
||||
closest = child;
|
||||
closest_distance = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
// Grid navigation only scrolls the container itself, which isn't scrollable when e.g. it's part of a scrolling page
|
||||
static void scroll_to_focused_child(lv_obj_t* container) {
|
||||
lv_obj_t* child = lvgl_grid_navigation_get_focused(container);
|
||||
if (child != nullptr) {
|
||||
lv_obj_scroll_to_view_recursive(child, LV_ANIM_ON);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_container_key(lv_event_t* event) {
|
||||
scroll_to_focused_child(lv_event_get_current_target_obj(event));
|
||||
}
|
||||
|
||||
// Grid navigation shows the focused child as selected whenever the container gets the focus. That's only
|
||||
// right when a key moved the focus there, not when it moved because e.g. the previously focused widget was deleted.
|
||||
static void on_container_focused(lv_event_t* event) {
|
||||
lv_indev_t* indev = lv_indev_active();
|
||||
const lv_indev_type_t type = indev != nullptr ? lv_indev_get_type(indev) : LV_INDEV_TYPE_NONE;
|
||||
if (type == LV_INDEV_TYPE_KEYPAD || type == LV_INDEV_TYPE_ENCODER) {
|
||||
lv_obj_t* container = lv_event_get_current_target_obj(event);
|
||||
if (type == LV_INDEV_TYPE_KEYPAD) {
|
||||
// Grid navigation restores the child that was focused last, but keys enter at the side they came from:
|
||||
// up and down enter at the bottom or top row, the other keys at the last or first child
|
||||
const uint32_t key = lv_indev_get_key(indev);
|
||||
lv_obj_t* focused = lvgl_grid_navigation_get_focused(container);
|
||||
lv_obj_t* child = nullptr;
|
||||
if (key == LV_KEY_DOWN || key == LV_KEY_UP) {
|
||||
child = find_edge_row_child(container, key == LV_KEY_DOWN, focused);
|
||||
} else if (key == LV_KEY_NEXT || key == LV_KEY_RIGHT) {
|
||||
child = find_focusable_child(container, -1, true);
|
||||
} else if (key == LV_KEY_PREV || key == LV_KEY_LEFT) {
|
||||
child = find_focusable_child(container, static_cast<int32_t>(lv_obj_get_child_count(container)), false);
|
||||
}
|
||||
if (child != nullptr && child != focused) {
|
||||
lv_gridnav_set_focused(container, child, LV_ANIM_ON);
|
||||
}
|
||||
scroll_to_focused_child(container);
|
||||
return;
|
||||
}
|
||||
lv_obj_t* child = lvgl_grid_navigation_get_focused(lv_event_get_current_target_obj(event));
|
||||
if (type == LV_INDEV_TYPE_ENCODER) {
|
||||
scroll_to_focused_child(container);
|
||||
return;
|
||||
}
|
||||
lv_obj_t* child = lvgl_grid_navigation_get_focused(container);
|
||||
if (child != nullptr) {
|
||||
lv_obj_remove_state(child, LV_STATE_FOCUS_KEY);
|
||||
}
|
||||
@ -37,7 +134,7 @@ void lvgl_grid_navigation_add(lv_obj_t* container) {
|
||||
lv_gridnav_add(container, LV_GRIDNAV_CTRL_NONE);
|
||||
// Added after grid navigation's own handler, so it runs after it
|
||||
lv_obj_add_event_cb(container, on_container_focused, LV_EVENT_FOCUSED, nullptr);
|
||||
lv_obj_add_flag(container, GRID_NAVIGATION_FLAG);
|
||||
lv_obj_add_event_cb(container, on_container_key, LV_EVENT_KEY, nullptr);
|
||||
|
||||
const uint32_t child_count = lv_obj_get_child_count(container);
|
||||
for (uint32_t i = 0; i < child_count; i++) {
|
||||
@ -62,7 +159,7 @@ void lvgl_grid_navigation_remove(lv_obj_t* container) {
|
||||
lv_gridnav_remove(container);
|
||||
lv_obj_remove_event_cb(container, on_child_created);
|
||||
lv_obj_remove_event_cb(container, on_container_focused);
|
||||
lv_obj_remove_flag(container, GRID_NAVIGATION_FLAG);
|
||||
lv_obj_remove_event_cb(container, on_container_key);
|
||||
|
||||
if (group == nullptr) {
|
||||
return;
|
||||
@ -80,23 +177,6 @@ void lvgl_grid_navigation_remove(lv_obj_t* container) {
|
||||
}
|
||||
}
|
||||
|
||||
// Matches which children grid navigation can focus
|
||||
static bool is_focusable(lv_obj_t* obj) {
|
||||
return !lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN) && lv_obj_has_flag(obj, static_cast<lv_obj_flag_t>(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_CLICK_FOCUSABLE));
|
||||
}
|
||||
|
||||
// Finds the next focusable child after (or before) the given index
|
||||
static lv_obj_t* find_focusable_child(lv_obj_t* container, int32_t index, bool next) {
|
||||
const auto child_count = static_cast<int32_t>(lv_obj_get_child_count(container));
|
||||
for (index += next ? 1 : -1; index >= 0 && index < child_count; index += next ? 1 : -1) {
|
||||
auto* child = lv_obj_get_child(container, index);
|
||||
if (is_focusable(child)) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void lvgl_grid_navigation_step(lv_group_t* group, bool next) {
|
||||
lv_obj_t* focused = lv_group_get_focused(group);
|
||||
if (lvgl_grid_navigation_is_container(focused)) {
|
||||
@ -105,6 +185,7 @@ void lvgl_grid_navigation_step(lv_group_t* group, bool next) {
|
||||
lv_obj_t* child = find_focusable_child(focused, index, next);
|
||||
if (child != nullptr) {
|
||||
lv_gridnav_set_focused(focused, child, LV_ANIM_ON);
|
||||
scroll_to_focused_child(focused);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -123,6 +204,7 @@ void lvgl_grid_navigation_step(lv_group_t* group, bool next) {
|
||||
lv_obj_t* child = find_focusable_child(entered, start, next);
|
||||
if (child != nullptr) {
|
||||
lv_gridnav_set_focused(entered, child, LV_ANIM_ON);
|
||||
scroll_to_focused_child(entered);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -156,5 +238,16 @@ lv_obj_t* lvgl_grid_navigation_get_focused(lv_obj_t* container) {
|
||||
}
|
||||
|
||||
bool lvgl_grid_navigation_is_container(const lv_obj_t* obj) {
|
||||
return obj != nullptr && lv_obj_has_flag(obj, GRID_NAVIGATION_FLAG);
|
||||
if (obj == nullptr) {
|
||||
return false;
|
||||
}
|
||||
// Grid navigation containers are the objects with this module's focus handler
|
||||
auto* container = const_cast<lv_obj_t*>(obj);
|
||||
const uint32_t event_count = lv_obj_get_event_count(container);
|
||||
for (uint32_t i = 0; i < event_count; i++) {
|
||||
if (lv_event_dsc_get_cb(lv_obj_get_event_dsc(container, i)) == on_container_focused) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -24,8 +24,10 @@ const char* const lvgl_icon_shared_names[] = {
|
||||
"download",
|
||||
"forum",
|
||||
"gamepad",
|
||||
"hard_disk",
|
||||
"help",
|
||||
"hub",
|
||||
"info",
|
||||
"image",
|
||||
"keyboard_arrow_up",
|
||||
"lightbulb",
|
||||
@ -33,6 +35,7 @@ const char* const lvgl_icon_shared_names[] = {
|
||||
"lists",
|
||||
"mail",
|
||||
"menu",
|
||||
"memory",
|
||||
"mop",
|
||||
"more_vert",
|
||||
"music_note",
|
||||
@ -41,6 +44,7 @@ const char* const lvgl_icon_shared_names[] = {
|
||||
"power_settings_new",
|
||||
"refresh",
|
||||
"search",
|
||||
"select_window_2",
|
||||
"settings",
|
||||
"toolbar",
|
||||
"navigation",
|
||||
@ -52,6 +56,7 @@ const char* const lvgl_icon_shared_names[] = {
|
||||
"logo_dev",
|
||||
"camera",
|
||||
"lightstrip",
|
||||
"phone_android",
|
||||
};
|
||||
|
||||
const size_t lvgl_icon_shared_name_count = sizeof(lvgl_icon_shared_names) / sizeof(lvgl_icon_shared_names[0]);
|
||||
|
||||
@ -30,12 +30,20 @@ static void update_edge_padding(void* data) {
|
||||
// The curve is widest at the object's edge that is nearest to the display's top or bottom
|
||||
const int32_t inset = std::max(lvgl_display_get_row_inset(display, coords.y1), lvgl_display_get_row_inset(display, coords.y2));
|
||||
const int32_t right_edge = lv_display_get_horizontal_resolution(display) - 1;
|
||||
lv_obj_set_style_pad_left(padding->obj, padding->base_left + std::max<int32_t>(0, inset - coords.x1), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_right(padding->obj, padding->base_right + std::max<int32_t>(0, inset - (right_edge - coords.x2)), LV_STATE_DEFAULT);
|
||||
const int32_t pad_left = padding->base_left + std::max<int32_t>(0, inset - coords.x1);
|
||||
const int32_t pad_right = padding->base_right + std::max<int32_t>(0, inset - (right_edge - coords.x2));
|
||||
// Setting a style marks the layout as dirty, which would resize the object and call this again
|
||||
if (lv_obj_get_style_pad_left(padding->obj, LV_PART_MAIN) != pad_left) {
|
||||
lv_obj_set_style_pad_left(padding->obj, pad_left, LV_STATE_DEFAULT);
|
||||
}
|
||||
if (lv_obj_get_style_pad_right(padding->obj, LV_PART_MAIN) != pad_right) {
|
||||
lv_obj_set_style_pad_right(padding->obj, pad_right, LV_STATE_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
// The position is final once the layout pass that changed the size has finished
|
||||
static void on_size_changed(lv_event_t* event) {
|
||||
lv_async_call_cancel(update_edge_padding, lv_event_get_user_data(event));
|
||||
lv_async_call(update_edge_padding, lv_event_get_user_data(event));
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/theme.h>
|
||||
|
||||
enum UiDensity lvgl_get_ui_density(void) {
|
||||
return TT_LVGL_UI_DENSITY;
|
||||
return lvgl_theme_is_compact() ? LVGL_UI_DENSITY_COMPACT : LVGL_UI_DENSITY_DEFAULT;
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <src/core/lv_global.h>
|
||||
#include <src/draw/lv_image_decoder_private.h>
|
||||
#include <src/image/lv_image_decoder_private.h>
|
||||
|
||||
#define DECODER_NAME "LODEPNG"
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/chip.h>
|
||||
#include <lvgl/widgets/icon_button.h>
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/page_indicator.h>
|
||||
#include <lvgl/widgets/spinner.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
@ -25,6 +26,8 @@
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
// Deprecated LVGL functions stay exported for existing apps
|
||||
LV_DEPRECATIONS_IGNORE_BEGIN
|
||||
const struct ModuleSymbol lvgl_module_symbols[] = {
|
||||
// lvgl_module
|
||||
DEFINE_MODULE_SYMBOL(lvgl_lock),
|
||||
@ -81,6 +84,7 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(lvgl_theme_get_default_settings),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_theme_set_settings),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_theme_is_mono),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_theme_is_compact),
|
||||
// lvgl_badge
|
||||
DEFINE_MODULE_SYMBOL(lvgl_badge_create),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_page_indicator_create),
|
||||
@ -93,6 +97,10 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
||||
// lvgl_icon_button
|
||||
DEFINE_MODULE_SYMBOL(lvgl_icon_button_create),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_icon_button_create_variant),
|
||||
// lvgl_list
|
||||
DEFINE_MODULE_SYMBOL(lvgl_list_create),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_list_add_text),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_list_add_button),
|
||||
// lvgl_spinner
|
||||
DEFINE_MODULE_SYMBOL(lvgl_spinner_create),
|
||||
// lvgl_toolbar
|
||||
@ -166,6 +174,26 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_is_valid),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_remove_event_cb),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_remove_flag),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_set_hidden),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_is_hidden),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_set_clickable),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_is_clickable),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_set_click_focusable),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_is_click_focusable),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_set_checkable),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_is_checkable),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_set_scrollable),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_is_scrollable),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_set_scroll_on_focus),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_is_scroll_on_focus),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_set_ignore_layout),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_is_ignore_layout),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_set_floating),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_is_floating),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_set_event_bubble),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_is_event_bubble),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_set_gesture_bubble),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_is_gesture_bubble),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_remove_state),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_set_pos),
|
||||
DEFINE_MODULE_SYMBOL(lv_obj_set_flex_align),
|
||||
@ -583,3 +611,4 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(lv_area_get_height),
|
||||
MODULE_SYMBOL_TERMINATOR,
|
||||
};
|
||||
LV_DEPRECATIONS_IGNORE_END
|
||||
|
||||
@ -29,6 +29,9 @@ void lvgl_theme_get_default_settings(LvglThemeSettings* settings) {
|
||||
settings->color_primary = material_config.color_primary;
|
||||
settings->color_secondary = material_config.color_secondary;
|
||||
settings->color_error = material_config.color_error;
|
||||
settings->is_compact = TT_LVGL_UI_DENSITY == LVGL_UI_DENSITY_COMPACT;
|
||||
settings->surface_override = material_config.surface_override;
|
||||
settings->color_surface = material_config.color_surface;
|
||||
}
|
||||
|
||||
void lvgl_theme_get_settings(LvglThemeSettings* settings) {
|
||||
@ -48,6 +51,12 @@ bool lvgl_theme_is_mono() {
|
||||
return active_theme == ActiveTheme::MaterialMono;
|
||||
}
|
||||
|
||||
bool lvgl_theme_is_compact() {
|
||||
LvglThemeSettings settings;
|
||||
lvgl_theme_get_settings(&settings);
|
||||
return settings.is_compact;
|
||||
}
|
||||
|
||||
static lv_theme_t* get_active_theme() {
|
||||
switch (active_theme) {
|
||||
case ActiveTheme::Material:
|
||||
@ -76,7 +85,7 @@ void lvgl_theme_deinit() {
|
||||
lv_theme_t* lvgl_theme_init_for_display(lv_display_t* display, bool isMonoDisplay, bool supportsAnimations) {
|
||||
LvglThemeSettings settings;
|
||||
lvgl_theme_get_settings(&settings);
|
||||
const bool is_compact = lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT;
|
||||
const bool is_compact = settings.is_compact;
|
||||
const bool animations_enabled = settings.animations_enabled && supportsAnimations;
|
||||
const ActiveTheme required_theme = (isMonoDisplay || settings.is_mono) ? ActiveTheme::MaterialMono : ActiveTheme::Material;
|
||||
|
||||
@ -104,6 +113,8 @@ lv_theme_t* lvgl_theme_init_for_display(lv_display_t* display, bool isMonoDispla
|
||||
config.color_primary = settings.color_primary;
|
||||
config.color_secondary = settings.color_secondary;
|
||||
config.color_error = settings.color_error;
|
||||
config.surface_override = settings.surface_override;
|
||||
config.color_surface = settings.color_surface;
|
||||
theme = lv_theme_material_init(display, &config);
|
||||
}
|
||||
active_theme = required_theme;
|
||||
|
||||
@ -18,6 +18,9 @@ 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_page_indicator_class;
|
||||
extern const lv_obj_class_t lvgl_page_indicator_dot_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_button_class;
|
||||
|
||||
#define NEUTRAL_4 lv_color_hex(0x0E0E0E)
|
||||
#define NEUTRAL_6 lv_color_hex(0x131313)
|
||||
@ -71,14 +74,17 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class;
|
||||
#define COLOR_ON_SECONDARY_CONTAINER (theme->config.is_dark ? lv_color_mix(theme->config.color_secondary, lv_color_white(), LV_OPA_20) : lv_color_mix(theme->config.color_secondary, lv_color_black(), LV_OPA_60))
|
||||
#define COLOR_ERROR (theme->config.is_dark ? lv_color_mix(theme->config.color_error, lv_color_white(), 140) : theme->config.color_error)
|
||||
#define COLOR_ON_ERROR (theme->config.is_dark ? lv_color_mix(theme->config.color_error, lv_color_black(), LV_OPA_50) : lv_color_white())
|
||||
#define COLOR_SURFACE (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_6 : NEUTRAL_98, TINT_NEUTRAL))
|
||||
#define DARK_BACKGROUND lv_color_hex(0x21222B)
|
||||
#define COLOR_SURFACE (theme->config.surface_override ? theme->config.color_surface : theme->config.is_dark ? lv_color_mix(lv_color_hex(0x000000), DARK_BACKGROUND, LV_OPA_20) : lv_color_mix(theme->config.color_primary, NEUTRAL_98, TINT_NEUTRAL))
|
||||
#define COLOR_ON_SURFACE (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_90 : NEUTRAL_10, TINT_NEUTRAL))
|
||||
#define COLOR_ON_SURFACE_VARIANT (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_80 : NEUTRAL_30, TINT_NEUTRAL_VARIANT))
|
||||
#define COLOR_SURFACE_CONTAINER_LOWEST (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_4 : NEUTRAL_100, TINT_NEUTRAL))
|
||||
#define COLOR_SURFACE_CONTAINER_LOW (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_10 : NEUTRAL_96, TINT_NEUTRAL))
|
||||
#define COLOR_SURFACE_CONTAINER (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_12 : NEUTRAL_94, TINT_NEUTRAL))
|
||||
#define COLOR_SURFACE_CONTAINER_HIGH (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_17 : NEUTRAL_92, TINT_NEUTRAL))
|
||||
#define COLOR_SURFACE_CONTAINER_HIGHEST (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_22 : NEUTRAL_90, TINT_NEUTRAL))
|
||||
#define SURFACE_LIFT (theme->config.is_dark ? lv_color_hex(0xFFFFFF) : lv_color_hex(0x000000))
|
||||
#define SURFACE_SINK (theme->config.is_dark ? lv_color_hex(0x000000) : lv_color_hex(0xFFFFFF))
|
||||
#define COLOR_SURFACE_CONTAINER_LOWEST (theme->config.surface_override ? lv_color_mix(SURFACE_SINK, theme->config.color_surface, 64) : theme->config.is_dark ? lv_color_mix(lv_color_hex(0x000000), DARK_BACKGROUND, 89) : lv_color_mix(theme->config.color_primary, NEUTRAL_100, TINT_NEUTRAL))
|
||||
#define COLOR_SURFACE_CONTAINER_LOW (theme->config.surface_override ? lv_color_mix(SURFACE_LIFT, theme->config.color_surface, theme->config.is_dark ? 10 : 5) : theme->config.is_dark ? DARK_BACKGROUND : lv_color_mix(theme->config.color_primary, NEUTRAL_96, TINT_NEUTRAL))
|
||||
#define COLOR_SURFACE_CONTAINER (theme->config.surface_override ? lv_color_mix(SURFACE_LIFT, theme->config.color_surface, theme->config.is_dark ? 15 : 10) : theme->config.is_dark ? lv_color_mix(lv_color_hex(0xFFFFFF), DARK_BACKGROUND, 8) : lv_color_mix(theme->config.color_primary, NEUTRAL_94, TINT_NEUTRAL))
|
||||
#define COLOR_SURFACE_CONTAINER_HIGH (theme->config.surface_override ? lv_color_mix(SURFACE_LIFT, theme->config.color_surface, theme->config.is_dark ? 28 : 15) : theme->config.is_dark ? lv_color_mix(lv_color_hex(0xFFFFFF), DARK_BACKGROUND, 20) : lv_color_mix(theme->config.color_primary, NEUTRAL_92, TINT_NEUTRAL))
|
||||
#define COLOR_SURFACE_CONTAINER_HIGHEST (theme->config.surface_override ? lv_color_mix(SURFACE_LIFT, theme->config.color_surface, theme->config.is_dark ? 41 : 20) : theme->config.is_dark ? lv_color_mix(lv_color_hex(0xFFFFFF), DARK_BACKGROUND, 33) : lv_color_mix(theme->config.color_primary, NEUTRAL_90, TINT_NEUTRAL))
|
||||
#define COLOR_OUTLINE (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_60 : NEUTRAL_50, TINT_NEUTRAL_VARIANT))
|
||||
#define COLOR_OUTLINE_VARIANT (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_30 : NEUTRAL_80, TINT_NEUTRAL_VARIANT))
|
||||
#define COLOR_INVERSE_SURFACE (lv_color_mix(theme->config.color_primary, theme->config.is_dark ? NEUTRAL_90 : NEUTRAL_20, TINT_NEUTRAL))
|
||||
@ -140,8 +146,8 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class;
|
||||
#define CHIP_PAD_HOR (theme->config.is_compact ? 3 : SPACE_4)
|
||||
#define CHIP_PAD_VER (theme->config.is_compact ? 2 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||
#define PAGE_INDICATOR_COLOR COLOR_ON_SURFACE_VARIANT
|
||||
#define PAGE_INDICATOR_DOT_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||
#define PAGE_INDICATOR_GAP (theme->config.is_compact ? 3 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||
#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 BADGE_COLOR COLOR_SECONDARY
|
||||
#define BADGE_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||
#define SCROLLBAR_COLOR COLOR_OUTLINE
|
||||
@ -174,8 +180,8 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class;
|
||||
#define ICON_BUTTON_TONAL_COLOR COLOR_SECONDARY_CONTAINER
|
||||
#define ICON_BUTTON_TONAL_ICON_COLOR COLOR_ON_SECONDARY_CONTAINER
|
||||
#define ICON_BUTTON_TONAL_PRESSED_COLOR (lv_color_mix(COLOR_ON_SECONDARY_CONTAINER, COLOR_SECONDARY_CONTAINER, OPACITY_STATE_PRESSED))
|
||||
#define APP_BAR_COLOR (lv_color_mix(theme->config.color_primary, NEUTRAL_22, TINT_NEUTRAL))
|
||||
#define APP_BAR_CONTENT_COLOR lv_color_white()
|
||||
#define APP_BAR_COLOR COLOR_SURFACE_CONTAINER_LOW
|
||||
#define APP_BAR_CONTENT_COLOR COLOR_ON_SURFACE
|
||||
#define APP_BAR_PRESSED_OPACITY (LV_OPA_20)
|
||||
#define APP_BAR_PAD_HOR (theme->config.is_compact ? 0 : SPACE_1)
|
||||
#define APP_BAR_GAP (theme->config.is_compact ? 2 : SPACE_1)
|
||||
@ -270,11 +276,14 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class;
|
||||
#define LIST_DIVIDER_COLOR COLOR_OUTLINE_VARIANT
|
||||
#define LIST_DIVIDER_WIDTH DENSITY_BORDER
|
||||
#define LIST_SUBHEADER_TEXT_COLOR COLOR_ON_SURFACE_VARIANT
|
||||
#define TABS_DIVIDER_COLOR COLOR_OUTLINE_VARIANT
|
||||
#define TABS_COLOR CARD_COLOR
|
||||
#define TABS_PAD (theme->config.is_compact ? 2 : SPACE_1)
|
||||
#define TABS_GAP (theme->config.is_compact ? 2 : SPACE_1)
|
||||
#define TAB_LABEL_COLOR COLOR_ON_SURFACE_VARIANT
|
||||
#define TAB_SELECTED_LABEL_COLOR COLOR_PRIMARY
|
||||
#define TAB_INDICATOR_COLOR COLOR_PRIMARY
|
||||
#define TAB_INDICATOR_WIDTH (theme->config.is_compact ? 2 : LV_DPX_CALC(theme->disp_dpi, 3))
|
||||
#define TAB_PAD (theme->config.is_compact ? 2 : SPACE_1)
|
||||
#define TAB_SHAPE SHAPE_CONTROL
|
||||
#define TAB_SELECTED_COLOR COLOR_SECONDARY_CONTAINER
|
||||
#define TAB_SELECTED_LABEL_COLOR COLOR_ON_SECONDARY_CONTAINER
|
||||
#define TAB_PAGE_PAD DENSITY_CONTAINER_PAD
|
||||
#define DIALOG_COLOR COLOR_SURFACE_CONTAINER_HIGH
|
||||
#define DIALOG_TEXT_COLOR COLOR_ON_SURFACE_VARIANT
|
||||
@ -485,18 +494,10 @@ typedef struct {
|
||||
#if LV_USE_BUTTONMATRIX || LV_USE_KEYBOARD
|
||||
lv_style_t key_selected;
|
||||
#endif
|
||||
#if LV_USE_LIST
|
||||
lv_style_t list;
|
||||
#endif
|
||||
#if LV_USE_LIST
|
||||
lv_style_t list_item;
|
||||
#endif
|
||||
#if LV_USE_LIST || LV_USE_TABLE
|
||||
lv_style_t list_item_focused;
|
||||
#endif
|
||||
#if LV_USE_LIST
|
||||
lv_style_t list_subheader;
|
||||
#endif
|
||||
#if LV_USE_TABVIEW
|
||||
lv_style_t tabs;
|
||||
#endif
|
||||
@ -658,6 +659,8 @@ static bool config_equals(const lv_theme_material_config_t * a, const lv_theme_m
|
||||
lv_color_eq(a->color_primary, b->color_primary) &&
|
||||
lv_color_eq(a->color_secondary, b->color_secondary) &&
|
||||
lv_color_eq(a->color_error, b->color_error) &&
|
||||
a->surface_override == b->surface_override &&
|
||||
lv_color_eq(a->color_surface, b->color_surface) &&
|
||||
a->font_small == b->font_small &&
|
||||
a->font_normal == b->font_normal &&
|
||||
a->font_large == b->font_large;
|
||||
@ -807,6 +810,7 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_radius(&theme->styles.chip, CHIP_SHAPE);
|
||||
lv_style_set_pad_hor(&theme->styles.chip, CHIP_PAD_HOR);
|
||||
lv_style_set_pad_ver(&theme->styles.chip, CHIP_PAD_VER);
|
||||
lv_style_set_margin_all(&theme->styles.chip, FOCUS_WIDTH + FOCUS_OFFSET);
|
||||
|
||||
style_init_reset(&theme->styles.chip_selected);
|
||||
lv_style_set_bg_color(&theme->styles.chip_selected, CHIP_SELECTED_COLOR);
|
||||
@ -823,6 +827,7 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_pad_hor(&theme->styles.button, BUTTON_PAD_HOR);
|
||||
lv_style_set_pad_ver(&theme->styles.button, BUTTON_PAD_VER);
|
||||
lv_style_set_pad_gap(&theme->styles.button, BUTTON_GAP);
|
||||
lv_style_set_margin_all(&theme->styles.button, FOCUS_WIDTH + FOCUS_OFFSET);
|
||||
#endif
|
||||
|
||||
#if LV_USE_BUTTON
|
||||
@ -923,6 +928,7 @@ static void style_init(my_theme_t * theme)
|
||||
style_init_reset(&theme->styles.switch_track);
|
||||
lv_style_set_width(&theme->styles.switch_track, SWITCH_WIDTH);
|
||||
lv_style_set_height(&theme->styles.switch_track, SWITCH_HEIGHT);
|
||||
lv_style_set_margin_all(&theme->styles.switch_track, FOCUS_WIDTH + FOCUS_OFFSET);
|
||||
lv_style_set_bg_color(&theme->styles.switch_track, SWITCH_TRACK_COLOR);
|
||||
lv_style_set_bg_opa(&theme->styles.switch_track, LV_OPA_COVER);
|
||||
lv_style_set_border_color(&theme->styles.switch_track, SWITCH_TRACK_BORDER_COLOR);
|
||||
@ -990,6 +996,7 @@ static void style_init(my_theme_t * theme)
|
||||
|
||||
style_init_reset(&theme->styles.sliderbox);
|
||||
lv_style_set_pad_column(&theme->styles.sliderbox, SLIDERBOX_GAP);
|
||||
lv_style_set_pad_all(&theme->styles.sliderbox, FOCUS_WIDTH + FOCUS_OFFSET);
|
||||
|
||||
#if LV_USE_BAR || LV_USE_SLIDER
|
||||
style_init_reset(&theme->styles.slider_track_size);
|
||||
@ -1181,7 +1188,6 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_text_color(&theme->styles.key_selected, KEY_SELECTED_TEXT_COLOR);
|
||||
#endif
|
||||
|
||||
#if LV_USE_LIST
|
||||
style_init_reset(&theme->styles.list);
|
||||
lv_style_set_bg_color(&theme->styles.list, LIST_COLOR);
|
||||
lv_style_set_bg_opa(&theme->styles.list, LV_OPA_COVER);
|
||||
@ -1189,9 +1195,7 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_pad_ver(&theme->styles.list, LIST_PAD);
|
||||
lv_style_set_pad_gap(&theme->styles.list, 0);
|
||||
lv_style_set_clip_corner(&theme->styles.list, true);
|
||||
#endif
|
||||
|
||||
#if LV_USE_LIST
|
||||
style_init_reset(&theme->styles.list_item);
|
||||
lv_style_set_bg_color(&theme->styles.list_item, LIST_ITEM_COLOR);
|
||||
lv_style_set_bg_opa(&theme->styles.list_item, LIST_ITEM_OPACITY);
|
||||
@ -1202,45 +1206,42 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_border_color(&theme->styles.list_item, LIST_DIVIDER_COLOR);
|
||||
lv_style_set_border_width(&theme->styles.list_item, LIST_DIVIDER_WIDTH);
|
||||
lv_style_set_border_side(&theme->styles.list_item, LV_BORDER_SIDE_BOTTOM);
|
||||
#endif
|
||||
|
||||
#if LV_USE_LIST || LV_USE_TABLE
|
||||
style_init_reset(&theme->styles.list_item_focused);
|
||||
lv_style_set_bg_color(&theme->styles.list_item_focused, LIST_ITEM_FOCUSED_COLOR);
|
||||
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);
|
||||
#endif
|
||||
|
||||
#if LV_USE_LIST
|
||||
style_init_reset(&theme->styles.list_subheader);
|
||||
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_pad_hor(&theme->styles.list_subheader, LIST_ITEM_PAD_HOR);
|
||||
lv_style_set_pad_top(&theme->styles.list_subheader, LIST_ITEM_PAD_VER);
|
||||
lv_style_set_pad_bottom(&theme->styles.list_subheader, LIST_PAD);
|
||||
#endif
|
||||
|
||||
#if LV_USE_TABVIEW
|
||||
style_init_reset(&theme->styles.tabs);
|
||||
lv_style_set_bg_opa(&theme->styles.tabs, LV_OPA_TRANSP);
|
||||
lv_style_set_border_color(&theme->styles.tabs, TABS_DIVIDER_COLOR);
|
||||
lv_style_set_border_width(&theme->styles.tabs, DENSITY_BORDER);
|
||||
lv_style_set_border_side(&theme->styles.tabs, LV_BORDER_SIDE_BOTTOM);
|
||||
lv_style_set_bg_color(&theme->styles.tabs, TABS_COLOR);
|
||||
lv_style_set_bg_opa(&theme->styles.tabs, LV_OPA_COVER);
|
||||
lv_style_set_border_width(&theme->styles.tabs, 0);
|
||||
lv_style_set_radius(&theme->styles.tabs, CARD_SHAPE);
|
||||
lv_style_set_pad_all(&theme->styles.tabs, TABS_PAD);
|
||||
lv_style_set_pad_gap(&theme->styles.tabs, TABS_GAP);
|
||||
#endif
|
||||
|
||||
#if LV_USE_BUTTON && LV_USE_TABVIEW
|
||||
style_init_reset(&theme->styles.tab);
|
||||
lv_style_set_bg_opa(&theme->styles.tab, LV_OPA_TRANSP);
|
||||
lv_style_set_text_color(&theme->styles.tab, TAB_LABEL_COLOR);
|
||||
lv_style_set_radius(&theme->styles.tab, 0);
|
||||
lv_style_set_radius(&theme->styles.tab, TAB_SHAPE);
|
||||
lv_style_set_pad_all(&theme->styles.tab, TAB_PAD);
|
||||
#endif
|
||||
|
||||
#if LV_USE_BUTTON && LV_USE_TABVIEW
|
||||
style_init_reset(&theme->styles.tab_selected);
|
||||
lv_style_set_bg_color(&theme->styles.tab_selected, TAB_SELECTED_COLOR);
|
||||
lv_style_set_bg_opa(&theme->styles.tab_selected, LV_OPA_COVER);
|
||||
lv_style_set_text_color(&theme->styles.tab_selected, TAB_SELECTED_LABEL_COLOR);
|
||||
lv_style_set_border_color(&theme->styles.tab_selected, TAB_INDICATOR_COLOR);
|
||||
lv_style_set_border_width(&theme->styles.tab_selected, TAB_INDICATOR_WIDTH);
|
||||
lv_style_set_border_side(&theme->styles.tab_selected, LV_BORDER_SIDE_BOTTOM);
|
||||
#endif
|
||||
|
||||
#if LV_USE_TABVIEW
|
||||
@ -1492,7 +1493,9 @@ void lv_theme_material_config_init(lv_theme_material_config_t * config)
|
||||
config->animations_enabled = true;
|
||||
config->color_primary = lv_color_hex(0x00BBFF);
|
||||
config->color_secondary = lv_color_hex(0xEE81C3);
|
||||
config->color_error = lv_color_hex(0xB3261E);
|
||||
config->color_error = lv_color_hex(0xEF6C00);
|
||||
config->surface_override = false;
|
||||
config->color_surface = lv_color_hex(0x000000);
|
||||
config->font_small = LV_FONT_DEFAULT;
|
||||
config->font_normal = LV_FONT_DEFAULT;
|
||||
config->font_large = LV_FONT_DEFAULT;
|
||||
@ -1841,6 +1844,8 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
lv_obj_add_style(obj, &theme->styles.page_indicator_label, 0);
|
||||
return;
|
||||
}
|
||||
/* label */
|
||||
lv_obj_add_style(obj, &theme->styles.focus_ring, LV_STATE_FOCUS_KEY);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@ -2078,6 +2083,28 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if(lv_obj_check_type(obj, &lvgl_list_class)) {
|
||||
/* flex_list */
|
||||
lv_obj_add_style(obj, &theme->styles.list, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.scrollbar, LV_PART_SCROLLBAR);
|
||||
lv_obj_add_style(obj, &theme->styles.scrollbar_scrolled, LV_PART_SCROLLBAR | LV_STATE_SCROLLED);
|
||||
return;
|
||||
}
|
||||
if(lv_obj_check_type(obj, &lvgl_list_text_class)) {
|
||||
/* flex_list_text */
|
||||
lv_obj_add_style(obj, &theme->styles.list_subheader, 0);
|
||||
return;
|
||||
}
|
||||
if(lv_obj_check_type(obj, &lvgl_list_button_class)) {
|
||||
/* flex_list_button */
|
||||
lv_obj_add_style(obj, &theme->styles.list_item, 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.transition_enter, LV_STATE_PRESSED);
|
||||
lv_obj_add_style(obj, &theme->styles.list_item_focused, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_add_style(obj, &theme->styles.disabled, LV_STATE_DISABLED);
|
||||
return;
|
||||
}
|
||||
#if LV_USE_TABLE
|
||||
if(lv_obj_check_type(obj, &lv_table_class)) {
|
||||
/* table */
|
||||
|
||||
@ -16,6 +16,8 @@ typedef struct {
|
||||
lv_color_t color_primary;
|
||||
lv_color_t color_secondary;
|
||||
lv_color_t color_error;
|
||||
bool surface_override;
|
||||
lv_color_t color_surface;
|
||||
const lv_font_t * font_small;
|
||||
const lv_font_t * font_normal;
|
||||
const lv_font_t * font_large;
|
||||
|
||||
@ -18,6 +18,9 @@ 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_page_indicator_class;
|
||||
extern const lv_obj_class_t lvgl_page_indicator_dot_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_button_class;
|
||||
|
||||
#define BLACK lv_color_black()
|
||||
#define WHITE lv_color_white()
|
||||
@ -90,8 +93,8 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class;
|
||||
#define CHIP_PAD_HOR (theme->config.is_compact ? 3 : SPACE_4)
|
||||
#define CHIP_PAD_VER (theme->config.is_compact ? 2 : SPACE_2)
|
||||
#define PAGE_INDICATOR_COLOR COLOR_ON_SURFACE
|
||||
#define PAGE_INDICATOR_DOT_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||
#define PAGE_INDICATOR_GAP (theme->config.is_compact ? 3 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||
#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 BADGE_COLOR COLOR_ON_SURFACE
|
||||
#define BADGE_SIZE (theme->config.is_compact ? 4 : LV_DPX_CALC(theme->disp_dpi, 6))
|
||||
#define SCROLLBAR_COLOR COLOR_ON_SURFACE
|
||||
@ -114,7 +117,6 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class;
|
||||
#define ICON_BUTTON_TONAL_ICON_COLOR COLOR_ON_SURFACE
|
||||
#define ICON_BUTTON_TONAL_BORDER_COLOR COLOR_OUTLINE
|
||||
#define APP_BAR_TITLE_COLOR COLOR_ON_SURFACE
|
||||
#define APP_BAR_DIVIDER_COLOR COLOR_OUTLINE
|
||||
#define APP_BAR_PAD_HOR (theme->config.is_compact ? 0 : SPACE_1)
|
||||
#define APP_BAR_GAP (theme->config.is_compact ? 2 : SPACE_1)
|
||||
#define APP_BAR_FOCUS_WIDTH FOCUS_INSET_WIDTH
|
||||
@ -181,9 +183,10 @@ extern const lv_obj_class_t lvgl_page_indicator_dot_class;
|
||||
#define LIST_ITEM_PAD_VER DENSITY_LIST_ITEM_PAD_VER
|
||||
#define LIST_ITEM_GAP (theme->config.is_compact ? 3 : SPACE_4)
|
||||
#define LIST_DIVIDER_COLOR COLOR_OUTLINE
|
||||
#define TABS_DIVIDER_COLOR COLOR_OUTLINE
|
||||
#define TAB_INDICATOR_COLOR COLOR_PRIMARY
|
||||
#define TAB_INDICATOR_WIDTH (theme->config.is_compact ? LINE_MEDIUM : LINE_THICK)
|
||||
#define TABS_PAD (theme->config.is_compact ? 2 : SPACE_1)
|
||||
#define TABS_GAP (theme->config.is_compact ? 2 : SPACE_1)
|
||||
#define TAB_PAD (theme->config.is_compact ? 2 : SPACE_1)
|
||||
#define TAB_SHAPE SHAPE_CONTROL
|
||||
#define TAB_PAGE_PAD DENSITY_CONTAINER_PAD
|
||||
#define DIALOG_COLOR COLOR_SURFACE
|
||||
#define DIALOG_BORDER_COLOR COLOR_OUTLINE
|
||||
@ -312,24 +315,15 @@ typedef struct {
|
||||
#if LV_USE_BUTTONMATRIX || LV_USE_KEYBOARD
|
||||
lv_style_t key;
|
||||
#endif
|
||||
#if LV_USE_LIST
|
||||
lv_style_t list;
|
||||
#endif
|
||||
#if LV_USE_LIST
|
||||
lv_style_t list_item;
|
||||
#endif
|
||||
#if LV_USE_LIST
|
||||
lv_style_t list_subheader;
|
||||
#endif
|
||||
#if LV_USE_TABVIEW
|
||||
lv_style_t tabs;
|
||||
#endif
|
||||
#if LV_USE_BUTTON && LV_USE_TABVIEW
|
||||
lv_style_t tab;
|
||||
#endif
|
||||
#if LV_USE_BUTTON && LV_USE_TABVIEW
|
||||
lv_style_t tab_selected;
|
||||
#endif
|
||||
#if LV_USE_TABVIEW
|
||||
lv_style_t tab_page;
|
||||
#endif
|
||||
@ -598,6 +592,7 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_radius(&theme->styles.chip, CHIP_SHAPE);
|
||||
lv_style_set_pad_hor(&theme->styles.chip, CHIP_PAD_HOR);
|
||||
lv_style_set_pad_ver(&theme->styles.chip, CHIP_PAD_VER);
|
||||
lv_style_set_margin_all(&theme->styles.chip, FOCUS_WIDTH + FOCUS_OFFSET);
|
||||
|
||||
#if LV_USE_BUTTON || LV_USE_MSGBOX
|
||||
style_init_reset(&theme->styles.button);
|
||||
@ -610,6 +605,7 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_pad_hor(&theme->styles.button, BUTTON_PAD_HOR);
|
||||
lv_style_set_pad_ver(&theme->styles.button, BUTTON_PAD_VER);
|
||||
lv_style_set_pad_gap(&theme->styles.button, BUTTON_GAP);
|
||||
lv_style_set_margin_all(&theme->styles.button, FOCUS_WIDTH + FOCUS_OFFSET);
|
||||
#endif
|
||||
|
||||
style_init_reset(&theme->styles.icon_button);
|
||||
@ -646,9 +642,7 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_bg_opa(&theme->styles.app_bar, LV_OPA_TRANSP);
|
||||
lv_style_set_text_color(&theme->styles.app_bar, APP_BAR_TITLE_COLOR);
|
||||
lv_style_set_radius(&theme->styles.app_bar, 0);
|
||||
lv_style_set_border_color(&theme->styles.app_bar, APP_BAR_DIVIDER_COLOR);
|
||||
lv_style_set_border_width(&theme->styles.app_bar, BORDER);
|
||||
lv_style_set_border_side(&theme->styles.app_bar, LV_BORDER_SIDE_BOTTOM);
|
||||
lv_style_set_border_width(&theme->styles.app_bar, 0);
|
||||
lv_style_set_pad_hor(&theme->styles.app_bar, APP_BAR_PAD_HOR);
|
||||
lv_style_set_pad_ver(&theme->styles.app_bar, 0);
|
||||
lv_style_set_pad_gap(&theme->styles.app_bar, APP_BAR_GAP);
|
||||
@ -666,6 +660,7 @@ static void style_init(my_theme_t * theme)
|
||||
style_init_reset(&theme->styles.switch_track);
|
||||
lv_style_set_width(&theme->styles.switch_track, SWITCH_WIDTH);
|
||||
lv_style_set_height(&theme->styles.switch_track, SWITCH_HEIGHT);
|
||||
lv_style_set_margin_all(&theme->styles.switch_track, FOCUS_WIDTH + FOCUS_OFFSET);
|
||||
lv_style_set_bg_color(&theme->styles.switch_track, SWITCH_TRACK_COLOR);
|
||||
lv_style_set_bg_opa(&theme->styles.switch_track, LV_OPA_COVER);
|
||||
lv_style_set_border_color(&theme->styles.switch_track, SWITCH_TRACK_BORDER_COLOR);
|
||||
@ -721,6 +716,7 @@ static void style_init(my_theme_t * theme)
|
||||
|
||||
style_init_reset(&theme->styles.sliderbox);
|
||||
lv_style_set_pad_column(&theme->styles.sliderbox, SLIDERBOX_GAP);
|
||||
lv_style_set_pad_all(&theme->styles.sliderbox, FOCUS_WIDTH + FOCUS_OFFSET);
|
||||
|
||||
#if LV_USE_BAR || LV_USE_SLIDER
|
||||
style_init_reset(&theme->styles.slider_track_size);
|
||||
@ -859,7 +855,6 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_radius(&theme->styles.key, KEY_SHAPE);
|
||||
#endif
|
||||
|
||||
#if LV_USE_LIST
|
||||
style_init_reset(&theme->styles.list);
|
||||
lv_style_set_bg_color(&theme->styles.list, COLOR_SURFACE);
|
||||
lv_style_set_bg_opa(&theme->styles.list, LV_OPA_COVER);
|
||||
@ -870,9 +865,7 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_pad_ver(&theme->styles.list, LIST_PAD);
|
||||
lv_style_set_pad_gap(&theme->styles.list, 0);
|
||||
lv_style_set_clip_corner(&theme->styles.list, true);
|
||||
#endif
|
||||
|
||||
#if LV_USE_LIST
|
||||
style_init_reset(&theme->styles.list_item);
|
||||
lv_style_set_bg_opa(&theme->styles.list_item, LV_OPA_TRANSP);
|
||||
lv_style_set_text_color(&theme->styles.list_item, COLOR_ON_SURFACE);
|
||||
@ -883,23 +876,20 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_pad_hor(&theme->styles.list_item, LIST_ITEM_PAD_HOR);
|
||||
lv_style_set_pad_ver(&theme->styles.list_item, LIST_ITEM_PAD_VER);
|
||||
lv_style_set_pad_column(&theme->styles.list_item, LIST_ITEM_GAP);
|
||||
#endif
|
||||
|
||||
#if LV_USE_LIST
|
||||
style_init_reset(&theme->styles.list_subheader);
|
||||
lv_style_set_text_font(&theme->styles.list_subheader, theme->config.font_small);
|
||||
lv_style_set_text_decor(&theme->styles.list_subheader, LV_TEXT_DECOR_UNDERLINE);
|
||||
lv_style_set_pad_hor(&theme->styles.list_subheader, LIST_ITEM_PAD_HOR);
|
||||
lv_style_set_pad_top(&theme->styles.list_subheader, LIST_ITEM_PAD_VER);
|
||||
lv_style_set_pad_bottom(&theme->styles.list_subheader, LIST_PAD);
|
||||
#endif
|
||||
|
||||
#if LV_USE_TABVIEW
|
||||
style_init_reset(&theme->styles.tabs);
|
||||
lv_style_set_bg_opa(&theme->styles.tabs, LV_OPA_TRANSP);
|
||||
lv_style_set_border_color(&theme->styles.tabs, TABS_DIVIDER_COLOR);
|
||||
lv_style_set_border_width(&theme->styles.tabs, BORDER);
|
||||
lv_style_set_border_side(&theme->styles.tabs, LV_BORDER_SIDE_BOTTOM);
|
||||
lv_style_set_border_width(&theme->styles.tabs, 0);
|
||||
lv_style_set_pad_all(&theme->styles.tabs, TABS_PAD);
|
||||
lv_style_set_pad_gap(&theme->styles.tabs, TABS_GAP);
|
||||
#endif
|
||||
|
||||
#if LV_USE_BUTTON && LV_USE_TABVIEW
|
||||
@ -907,14 +897,8 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_bg_opa(&theme->styles.tab, LV_OPA_TRANSP);
|
||||
lv_style_set_text_color(&theme->styles.tab, COLOR_ON_SURFACE);
|
||||
lv_style_set_border_width(&theme->styles.tab, 0);
|
||||
lv_style_set_radius(&theme->styles.tab, 0);
|
||||
#endif
|
||||
|
||||
#if LV_USE_BUTTON && LV_USE_TABVIEW
|
||||
style_init_reset(&theme->styles.tab_selected);
|
||||
lv_style_set_border_color(&theme->styles.tab_selected, TAB_INDICATOR_COLOR);
|
||||
lv_style_set_border_width(&theme->styles.tab_selected, TAB_INDICATOR_WIDTH);
|
||||
lv_style_set_border_side(&theme->styles.tab_selected, LV_BORDER_SIDE_BOTTOM);
|
||||
lv_style_set_radius(&theme->styles.tab, TAB_SHAPE);
|
||||
lv_style_set_pad_all(&theme->styles.tab, TAB_PAD);
|
||||
#endif
|
||||
|
||||
#if LV_USE_TABVIEW
|
||||
@ -1306,7 +1290,7 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
if(lv_obj_check_type(lss_ancestor(obj, 2), &lv_tabview_class) && lss_is_child(lss_ancestor(obj, 2), 0, parent)) {
|
||||
lv_obj_add_style(obj, &theme->styles.tab, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_PRESSED);
|
||||
lv_obj_add_style(obj, &theme->styles.tab_selected, LV_STATE_CHECKED);
|
||||
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_CHECKED);
|
||||
lv_obj_add_style(obj, &theme->styles.focus_ring, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_add_style(obj, &theme->styles.edit_ring, LV_STATE_EDITED);
|
||||
return;
|
||||
@ -1440,6 +1424,8 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
lv_obj_add_style(obj, &theme->styles.page_indicator_label, 0);
|
||||
return;
|
||||
}
|
||||
/* label */
|
||||
lv_obj_add_style(obj, &theme->styles.focus_ring, LV_STATE_FOCUS_KEY);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@ -1667,6 +1653,25 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if(lv_obj_check_type(obj, &lvgl_list_class)) {
|
||||
/* flex_list */
|
||||
lv_obj_add_style(obj, &theme->styles.list, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.scrollbar, LV_PART_SCROLLBAR);
|
||||
return;
|
||||
}
|
||||
if(lv_obj_check_type(obj, &lvgl_list_text_class)) {
|
||||
/* flex_list_text */
|
||||
lv_obj_add_style(obj, &theme->styles.list_subheader, 0);
|
||||
return;
|
||||
}
|
||||
if(lv_obj_check_type(obj, &lvgl_list_button_class)) {
|
||||
/* flex_list_button */
|
||||
lv_obj_add_style(obj, &theme->styles.list_item, 0);
|
||||
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.disabled, LV_STATE_DISABLED);
|
||||
return;
|
||||
}
|
||||
#if LV_USE_TABLE
|
||||
if(lv_obj_check_type(obj, &lv_table_class)) {
|
||||
/* table */
|
||||
|
||||
@ -5,9 +5,9 @@
|
||||
|
||||
static void badge_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
|
||||
LV_UNUSED(class_p);
|
||||
lv_obj_remove_flag(obj, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(obj, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
||||
lv_obj_set_clickable(obj, false);
|
||||
lv_obj_set_scrollable(obj, false);
|
||||
lv_obj_set_ignore_layout(obj, true);
|
||||
}
|
||||
|
||||
// Not static: the theme styles badges by this class
|
||||
|
||||
84
Modules/lvgl-module/source/widgets/list.cpp
Normal file
84
Modules/lvgl-module/source/widgets/list.cpp
Normal file
@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#define LV_USE_PRIVATE_API 1 // For the lv_obj_class_t declaration
|
||||
|
||||
#include <lvgl/widgets/list.h>
|
||||
|
||||
// Not static: the theme styles lists by these classes
|
||||
extern "C" const lv_obj_class_t lvgl_list_class = {
|
||||
.base_class = &lv_obj_class,
|
||||
.constructor_cb = nullptr,
|
||||
.destructor_cb = nullptr,
|
||||
.event_cb = nullptr,
|
||||
.user_data = nullptr,
|
||||
.name = "list",
|
||||
.width_def = (LV_DPI_DEF * 3) / 2,
|
||||
.height_def = LV_DPI_DEF * 2,
|
||||
.editable = LV_OBJ_CLASS_EDITABLE_INHERIT,
|
||||
.group_def = LV_OBJ_CLASS_GROUP_DEF_INHERIT,
|
||||
.instance_size = 0,
|
||||
.theme_inheritable = LV_OBJ_CLASS_THEME_INHERITABLE_FALSE
|
||||
};
|
||||
|
||||
extern "C" const lv_obj_class_t lvgl_list_text_class = {
|
||||
.base_class = &lv_label_class,
|
||||
.constructor_cb = nullptr,
|
||||
.destructor_cb = nullptr,
|
||||
.event_cb = nullptr,
|
||||
.user_data = nullptr,
|
||||
.name = "list_text",
|
||||
.width_def = LV_PCT(100),
|
||||
.height_def = LV_SIZE_CONTENT,
|
||||
.editable = LV_OBJ_CLASS_EDITABLE_INHERIT,
|
||||
.group_def = LV_OBJ_CLASS_GROUP_DEF_INHERIT,
|
||||
.instance_size = 0,
|
||||
.theme_inheritable = LV_OBJ_CLASS_THEME_INHERITABLE_FALSE
|
||||
};
|
||||
|
||||
extern "C" const lv_obj_class_t lvgl_list_button_class = {
|
||||
.base_class = &lv_button_class,
|
||||
.constructor_cb = nullptr,
|
||||
.destructor_cb = nullptr,
|
||||
.event_cb = nullptr,
|
||||
.user_data = nullptr,
|
||||
.name = "list_button",
|
||||
.width_def = LV_PCT(100),
|
||||
.height_def = LV_SIZE_CONTENT,
|
||||
.editable = LV_OBJ_CLASS_EDITABLE_INHERIT,
|
||||
.group_def = LV_OBJ_CLASS_GROUP_DEF_INHERIT,
|
||||
.instance_size = 0,
|
||||
.theme_inheritable = LV_OBJ_CLASS_THEME_INHERITABLE_FALSE
|
||||
};
|
||||
|
||||
lv_obj_t* lvgl_list_create(lv_obj_t* parent) {
|
||||
lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_list_class, parent);
|
||||
lv_obj_class_init_obj(obj);
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
|
||||
return obj;
|
||||
}
|
||||
|
||||
lv_obj_t* lvgl_list_add_text(lv_obj_t* list, const char* text) {
|
||||
lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_list_text_class, list);
|
||||
lv_obj_class_init_obj(obj);
|
||||
lv_label_set_text(obj, text);
|
||||
return obj;
|
||||
}
|
||||
|
||||
lv_obj_t* lvgl_list_add_button(lv_obj_t* list, const void* icon, const char* text) {
|
||||
lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_list_button_class, list);
|
||||
lv_obj_class_init_obj(obj);
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
|
||||
|
||||
if (icon != nullptr) {
|
||||
lv_obj_t* image = lv_image_create(obj);
|
||||
lv_image_set_src(image, icon);
|
||||
}
|
||||
|
||||
if (text != nullptr) {
|
||||
lv_obj_t* label = lv_label_create(obj);
|
||||
lv_label_set_text(label, text);
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
lv_obj_set_flex_grow(label, 1);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
@ -13,21 +13,23 @@ static void on_style_changed(lv_event_t* event) {
|
||||
|
||||
static void page_indicator_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
|
||||
LV_UNUSED(class_p);
|
||||
lv_obj_remove_flag(obj, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||
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_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
// Shows the page numbers instead of the dots when they don't fit in the indicator's maximum width
|
||||
lv_obj_t* label = lv_label_create(obj);
|
||||
lv_obj_add_flag(label, LV_OBJ_FLAG_HIDDEN);
|
||||
// One line, so the indicator's height doesn't change with its width
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_CLIP);
|
||||
lv_obj_set_hidden(label, true);
|
||||
// A change of the maximum width can switch between dots and numbers
|
||||
lv_obj_add_event_cb(obj, on_style_changed, LV_EVENT_STYLE_CHANGED, nullptr);
|
||||
}
|
||||
|
||||
static void page_indicator_dot_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
|
||||
LV_UNUSED(class_p);
|
||||
lv_obj_remove_flag(obj, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_clickable(obj, false);
|
||||
lv_obj_set_scrollable(obj, false);
|
||||
}
|
||||
|
||||
// Not static: the theme styles page indicators and their dots by these classes
|
||||
@ -85,11 +87,11 @@ static void refresh(lv_obj_t* obj) {
|
||||
const bool show_numbers = dots_width > lv_obj_get_style_max_width(obj, LV_PART_MAIN);
|
||||
|
||||
lv_obj_t* label = get_label(obj);
|
||||
lv_obj_set_flag(label, LV_OBJ_FLAG_HIDDEN, !show_numbers);
|
||||
lv_obj_set_hidden(label, !show_numbers);
|
||||
uint32_t page = 0;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
lv_obj_t* dot = get_dot(obj, i);
|
||||
lv_obj_set_flag(dot, LV_OBJ_FLAG_HIDDEN, show_numbers);
|
||||
lv_obj_set_hidden(dot, show_numbers);
|
||||
if (lv_obj_has_state(dot, LV_STATE_CHECKED)) {
|
||||
page = i;
|
||||
}
|
||||
|
||||
@ -35,10 +35,9 @@ extern "C" const lv_obj_class_t lvgl_sliderbox_class = {
|
||||
|
||||
static void sliderbox_constructor(const lv_obj_class_t* classPointer, lv_obj_t* obj) {
|
||||
LV_UNUSED(classPointer);
|
||||
lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||
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);
|
||||
lv_obj_set_style_pad_all(obj, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(obj, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(obj, 0, LV_STATE_DEFAULT);
|
||||
}
|
||||
@ -119,7 +118,7 @@ static void on_plus_button_clicked(lv_event_t* event) {
|
||||
|
||||
static lv_obj_t* create_step_button(lv_obj_t* parent, const char* symbol, lv_event_cb_t callback, void* userData, uint32_t size) {
|
||||
auto* button = lv_button_create(parent);
|
||||
lv_obj_remove_flag(button, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(button, false);
|
||||
lv_obj_set_size(button, size, size);
|
||||
lv_obj_set_style_pad_all(button, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_add_event_cb(button, callback, LV_EVENT_SHORT_CLICKED, userData);
|
||||
@ -152,7 +151,7 @@ lv_obj_t* lvgl_sliderbox_create(lv_obj_t* parent, int32_t min, int32_t max, int3
|
||||
sliderBox->minusButton = create_step_button(obj, LV_SYMBOL_MINUS, &on_minus_button_clicked, obj, button_size);
|
||||
|
||||
sliderBox->slider = lv_slider_create(obj);
|
||||
lv_obj_remove_flag(sliderBox->slider, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(sliderBox->slider, false);
|
||||
lv_obj_set_flex_grow(sliderBox->slider, 1);
|
||||
lv_slider_set_range(sliderBox->slider, min, max);
|
||||
lv_obj_add_event_cb(sliderBox->slider, &on_slider_value_changed, LV_EVENT_VALUE_CHANGED, obj);
|
||||
|
||||
@ -42,7 +42,7 @@ static void anim_rotation_callback(void* var, int32_t v) {
|
||||
}
|
||||
|
||||
static void spinner_constructor(const lv_obj_class_t* object_class, lv_obj_t* object) {
|
||||
lv_obj_remove_flag(object, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_clickable(object, false);
|
||||
|
||||
lv_anim_t a;
|
||||
lv_anim_init(&a);
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/insets.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/theme.h>
|
||||
#include <lvgl/widgets/icon_button.h>
|
||||
#include <lvgl/widgets/spinner.h>
|
||||
|
||||
@ -16,16 +17,16 @@
|
||||
|
||||
constexpr auto* TAG = "lvgl_toolbar";
|
||||
|
||||
static uint32_t getToolbarHeight(UiDensity uiDensity) {
|
||||
if (uiDensity == LVGL_UI_DENSITY_COMPACT) {
|
||||
static uint32_t getToolbarHeight(bool isCompact) {
|
||||
if (isCompact) {
|
||||
return lvgl_get_text_font_height(FONT_SIZE_DEFAULT) * 1.6f;
|
||||
} else {
|
||||
return lvgl_get_text_font_height(FONT_SIZE_LARGE) * 2.2f;
|
||||
}
|
||||
}
|
||||
|
||||
static const _lv_font_t* getToolbarFont(UiDensity uiDensity) {
|
||||
if (uiDensity == LVGL_UI_DENSITY_COMPACT) {
|
||||
static const _lv_font_t* getToolbarFont(bool isCompact) {
|
||||
if (isCompact) {
|
||||
return lvgl_get_text_font(FONT_SIZE_DEFAULT);
|
||||
} else {
|
||||
return lvgl_get_text_font(FONT_SIZE_LARGE);
|
||||
@ -33,9 +34,9 @@ static const _lv_font_t* getToolbarFont(UiDensity uiDensity) {
|
||||
}
|
||||
|
||||
/** The size of icon buttons, which leaves room for the theme's focus indicator */
|
||||
static uint32_t getActionButtonSize(UiDensity uiDensity) {
|
||||
auto toolbar_height = getToolbarHeight(uiDensity);
|
||||
auto padding = (uiDensity != LVGL_UI_DENSITY_COMPACT) ? (uint32_t)(toolbar_height * 0.2f) : 8;
|
||||
static uint32_t getActionButtonSize(bool isCompact) {
|
||||
auto toolbar_height = getToolbarHeight(isCompact);
|
||||
auto padding = !isCompact ? (uint32_t)(toolbar_height * 0.2f) : 8;
|
||||
return toolbar_height - padding;
|
||||
}
|
||||
|
||||
@ -80,22 +81,22 @@ static void default_nav_action(lv_event_t* event) {
|
||||
|
||||
static void toolbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
|
||||
LV_UNUSED(class_p);
|
||||
lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_add_flag(obj, LV_OBJ_FLAG_SCROLL_ON_FOCUS);
|
||||
lv_obj_set_scrollable(obj, false);
|
||||
lv_obj_set_scroll_on_focus(obj, true);
|
||||
}
|
||||
|
||||
static lv_obj_t* create_action_button(lv_obj_t* parent, UiDensity ui_density) {
|
||||
auto button_size = getActionButtonSize(ui_density);
|
||||
static lv_obj_t* create_action_button(lv_obj_t* parent, bool is_compact) {
|
||||
auto button_size = getActionButtonSize(is_compact);
|
||||
lv_obj_t* button = lvgl_icon_button_create(parent);
|
||||
lv_obj_set_size(button, button_size, button_size);
|
||||
return button;
|
||||
}
|
||||
|
||||
lv_obj_t* lvgl_toolbar_create(lv_obj_t* parent, const char* title) {
|
||||
auto ui_density = lvgl_get_ui_density();
|
||||
const bool is_compact = lvgl_theme_is_compact();
|
||||
lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_toolbar_class, parent);
|
||||
lv_obj_class_init_obj(obj);
|
||||
lv_obj_set_height(obj, getToolbarHeight(ui_density));
|
||||
lv_obj_set_height(obj, getToolbarHeight(is_compact));
|
||||
|
||||
auto* toolbar = reinterpret_cast<Toolbar*>(obj);
|
||||
toolbar->nav_action_callback = nullptr;
|
||||
@ -109,12 +110,12 @@ lv_obj_t* lvgl_toolbar_create(lv_obj_t* parent, const char* title) {
|
||||
// The arrow keys move between the close button and the actions, and up and down leave the toolbar
|
||||
lvgl_grid_navigation_add(obj);
|
||||
|
||||
toolbar->close_button = create_action_button(obj, ui_density);
|
||||
toolbar->close_button = create_action_button(obj, is_compact);
|
||||
toolbar->close_button_image = lv_image_create(toolbar->close_button);
|
||||
lv_obj_align(toolbar->close_button_image, LV_ALIGN_CENTER, 0, 0);
|
||||
|
||||
toolbar->title_label = lv_label_create(obj);
|
||||
lv_obj_set_style_text_font(toolbar->title_label, getToolbarFont(ui_density), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_text_font(toolbar->title_label, getToolbarFont(is_compact), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(toolbar->title_label, title);
|
||||
lv_label_set_long_mode(toolbar->title_label, LV_LABEL_LONG_MODE_SCROLL);
|
||||
lv_obj_set_flex_grow(toolbar->title_label, 1);
|
||||
@ -155,7 +156,7 @@ static lv_obj_t* toolbar_add_button_action(lv_obj_t* obj, const char* imageOrBut
|
||||
check(toolbar->action_count < TOOLBAR_ACTION_LIMIT, "max actions reached");
|
||||
toolbar->action_count++;
|
||||
|
||||
lv_obj_t* action_button = create_action_button(obj, lvgl_get_ui_density());
|
||||
lv_obj_t* action_button = create_action_button(obj, lvgl_theme_is_compact());
|
||||
lv_obj_add_event_cb(action_button, callback, LV_EVENT_SHORT_CLICKED, user_data);
|
||||
lv_obj_t* button_content;
|
||||
if (isImage) {
|
||||
|
||||
@ -14,7 +14,10 @@
|
||||
animationsEnabled: bool = true;
|
||||
colorPrimary: color = #00bbff;
|
||||
colorSecondary: color = #ee81c3;
|
||||
colorError: color = #B3261E;
|
||||
colorError: color = #EF6C00;
|
||||
// Replaces the surface (screen background) colour
|
||||
surfaceOverride: bool = false;
|
||||
colorSurface: color = #000000;
|
||||
fontSmall: font = raw("LV_FONT_DEFAULT");
|
||||
fontNormal: font = raw("LV_FONT_DEFAULT");
|
||||
fontLarge: font = raw("LV_FONT_DEFAULT");
|
||||
@ -31,6 +34,9 @@
|
||||
@widget badge: lvgl_badge_class;
|
||||
@widget page_indicator: lvgl_page_indicator_class;
|
||||
@widget page_indicator_dot: lvgl_page_indicator_dot_class;
|
||||
@widget flex_list: lvgl_list_class;
|
||||
@widget flex_list_text: lvgl_list_text_class;
|
||||
@widget flex_list_button: lvgl_list_button_class;
|
||||
|
||||
// ============================================================
|
||||
// 1. Base tokens
|
||||
@ -108,15 +114,20 @@
|
||||
@color-error: if($isDark, mix($colorError, white, 55%), $colorError);
|
||||
@color-on-error: if($isDark, mix($colorError, black, 50%), white);
|
||||
|
||||
// Surfaces: neutral tones tinted with the primary color
|
||||
@color-surface: mix($colorPrimary, if($isDark, @neutral-6, @neutral-98), @tint-neutral);
|
||||
// Surfaces: neutral tones tinted with the primary color.
|
||||
// In dark mode, they're derived from the screen background colour (the low container tone, which apps and the window use).
|
||||
@dark-background: #21222b;
|
||||
@color-surface: if($surfaceOverride, $colorSurface, if($isDark, mix(#000000, @dark-background, 20%), mix($colorPrimary, @neutral-98, @tint-neutral)));
|
||||
@color-on-surface: mix($colorPrimary, if($isDark, @neutral-90, @neutral-10), @tint-neutral);
|
||||
@color-on-surface-variant: mix($colorPrimary, if($isDark, @neutral-80, @neutral-30), @tint-neutral-variant);
|
||||
@color-surface-container-lowest: mix($colorPrimary, if($isDark, @neutral-4, @neutral-100), @tint-neutral);
|
||||
@color-surface-container-low: mix($colorPrimary, if($isDark, @neutral-10, @neutral-96), @tint-neutral);
|
||||
@color-surface-container: mix($colorPrimary, if($isDark, @neutral-12, @neutral-94), @tint-neutral);
|
||||
@color-surface-container-high: mix($colorPrimary, if($isDark, @neutral-17, @neutral-92), @tint-neutral);
|
||||
@color-surface-container-highest: mix($colorPrimary, if($isDark, @neutral-22, @neutral-90), @tint-neutral);
|
||||
// With a surface override, the containers are the surface lifted towards the text colour, in the same steps as the neutral tones
|
||||
@surface-lift: if($isDark, #FFFFFF, #000000);
|
||||
@surface-sink: if($isDark, #000000, #FFFFFF);
|
||||
@color-surface-container-lowest: if($surfaceOverride, mix(@surface-sink, $colorSurface, 25%), if($isDark, mix(#000000, @dark-background, 35%), mix($colorPrimary, @neutral-100, @tint-neutral)));
|
||||
@color-surface-container-low: if($surfaceOverride, mix(@surface-lift, $colorSurface, if($isDark, 4%, 2%)), if($isDark, @dark-background, mix($colorPrimary, @neutral-96, @tint-neutral)));
|
||||
@color-surface-container: if($surfaceOverride, mix(@surface-lift, $colorSurface, if($isDark, 6%, 4%)), if($isDark, mix(#FFFFFF, @dark-background, 3%), mix($colorPrimary, @neutral-94, @tint-neutral)));
|
||||
@color-surface-container-high: if($surfaceOverride, mix(@surface-lift, $colorSurface, if($isDark, 11%, 6%)), if($isDark, mix(#FFFFFF, @dark-background, 8%), mix($colorPrimary, @neutral-92, @tint-neutral)));
|
||||
@color-surface-container-highest: if($surfaceOverride, mix(@surface-lift, $colorSurface, if($isDark, 16%, 8%)), if($isDark, mix(#FFFFFF, @dark-background, 13%), mix($colorPrimary, @neutral-90, @tint-neutral)));
|
||||
@color-outline: mix($colorPrimary, if($isDark, @neutral-60, @neutral-50), @tint-neutral-variant);
|
||||
@color-outline-variant: mix($colorPrimary, if($isDark, @neutral-30, @neutral-80), @tint-neutral-variant);
|
||||
@color-inverse-surface: mix($colorPrimary, if($isDark, @neutral-90, @neutral-20), @tint-neutral);
|
||||
@ -203,8 +214,8 @@
|
||||
|
||||
// Page indicator (lvgl_page_indicator_create): a dot per page, the current page's dot is filled
|
||||
@page-indicator-color: @color-on-surface-variant;
|
||||
@page-indicator-dot-size: if($isCompact, 4, 6dp);
|
||||
@page-indicator-gap: if($isCompact, 3, 6dp);
|
||||
@page-indicator-dot-size: if($isCompact, 6, 8dp);
|
||||
@page-indicator-gap: if($isCompact, 4, 6dp);
|
||||
|
||||
// Badge (lvgl_badge_create): a small dot that marks an item, e.g. a favourite
|
||||
@badge-color: @color-secondary;
|
||||
@ -250,10 +261,9 @@
|
||||
@icon-button-tonal-icon-color: @color-on-secondary-container;
|
||||
@icon-button-tonal-pressed-color: mix(@color-on-secondary-container, @color-secondary-container, @opacity-state-pressed);
|
||||
|
||||
// Toolbar: Material 2 top app bar, filled with a dark neutral tone for contrast with the content.
|
||||
// The tone is the same in light and dark mode. The statusbar uses a darker neutral tone with the same tint.
|
||||
@app-bar-color: mix($colorPrimary, @neutral-22, @tint-neutral);
|
||||
@app-bar-content-color: white;
|
||||
// Toolbar: Material 3 top app bar, in the same colour as the app content and the statusbar (which shows the window's container colour)
|
||||
@app-bar-color: @color-surface-container-low;
|
||||
@app-bar-content-color: @color-on-surface;
|
||||
@app-bar-pressed-opacity: 20%;
|
||||
@app-bar-pad-hor: if($isCompact, 0, @space-1);
|
||||
@app-bar-gap: if($isCompact, 2, @space-1);
|
||||
@ -373,12 +383,15 @@
|
||||
@list-divider-width: @density-border;
|
||||
@list-subheader-text-color: @color-on-surface-variant;
|
||||
|
||||
// Tabs: Material 3 primary tabs
|
||||
@tabs-divider-color: @color-outline-variant;
|
||||
// Tabs: the tab bar is a Material 3 navigation bar with a pill-shaped active indicator
|
||||
@tabs-color: @card-color;
|
||||
@tabs-pad: if($isCompact, 2, @space-1);
|
||||
@tabs-gap: if($isCompact, 2, @space-1);
|
||||
@tab-label-color: @color-on-surface-variant;
|
||||
@tab-selected-label-color: @color-primary;
|
||||
@tab-indicator-color: @color-primary;
|
||||
@tab-indicator-width: if($isCompact, 2, 3dp);
|
||||
@tab-pad: if($isCompact, 2, @space-1);
|
||||
@tab-shape: @shape-control;
|
||||
@tab-selected-color: @color-secondary-container;
|
||||
@tab-selected-label-color: @color-on-secondary-container;
|
||||
@tab-page-pad: @density-container-pad;
|
||||
|
||||
// Dialog (message box)
|
||||
@ -613,6 +626,8 @@ chip {
|
||||
radius: @chip-shape;
|
||||
padHor: @chip-pad-hor;
|
||||
padVer: @chip-pad-ver;
|
||||
// Room for the focus ring, which the parent would clip
|
||||
marginAll: @focus-width + @focus-offset;
|
||||
}
|
||||
|
||||
chip_selected {
|
||||
@ -630,6 +645,8 @@ button {
|
||||
padHor: @button-pad-hor;
|
||||
padVer: @button-pad-ver;
|
||||
padGap: @button-gap;
|
||||
// Room for the focus ring, which the parent would clip
|
||||
marginAll: @focus-width + @focus-offset;
|
||||
}
|
||||
|
||||
button_pressed {
|
||||
@ -733,6 +750,8 @@ app_bar_field {
|
||||
switch_track {
|
||||
width: @switch-width;
|
||||
height: @switch-height;
|
||||
// Room for the focus ring, which the parent would clip
|
||||
marginAll: @focus-width + @focus-offset;
|
||||
bgColor: @switch-track-color;
|
||||
bgOpa: cover;
|
||||
borderColor: @switch-track-border-color;
|
||||
@ -792,6 +811,8 @@ checkbox_box_selected {
|
||||
|
||||
sliderbox {
|
||||
padColumn: @sliderbox-gap;
|
||||
// Room for the focus ring of the step buttons, which the sliderbox would clip
|
||||
padAll: @focus-width + @focus-offset;
|
||||
}
|
||||
|
||||
slider_track_size {
|
||||
@ -996,23 +1017,25 @@ list_subheader {
|
||||
}
|
||||
|
||||
tabs {
|
||||
bgOpa: transp;
|
||||
borderColor: @tabs-divider-color;
|
||||
borderWidth: @density-border;
|
||||
borderSide: bottom;
|
||||
bgColor: @tabs-color;
|
||||
bgOpa: cover;
|
||||
borderWidth: 0;
|
||||
radius: @card-shape;
|
||||
padAll: @tabs-pad;
|
||||
padGap: @tabs-gap;
|
||||
}
|
||||
|
||||
tab {
|
||||
bgOpa: transp;
|
||||
textColor: @tab-label-color;
|
||||
radius: 0;
|
||||
radius: @tab-shape;
|
||||
padAll: @tab-pad;
|
||||
}
|
||||
|
||||
tab_selected {
|
||||
bgColor: @tab-selected-color;
|
||||
bgOpa: cover;
|
||||
textColor: @tab-selected-label-color;
|
||||
borderColor: @tab-indicator-color;
|
||||
borderWidth: @tab-indicator-width;
|
||||
borderSide: bottom;
|
||||
}
|
||||
|
||||
tab_page {
|
||||
@ -1414,6 +1437,11 @@ apply(page_indicator > label) {
|
||||
page_indicator_label: default;
|
||||
}
|
||||
|
||||
// Labels can be selectable, e.g. rows that keys move through
|
||||
apply(label) {
|
||||
focus_ring: state.focusKey;
|
||||
}
|
||||
|
||||
apply(page_indicator_dot) {
|
||||
page_indicator_dot: default;
|
||||
page_indicator_dot_current: state.checked;
|
||||
@ -1582,6 +1610,25 @@ apply(list_button) {
|
||||
disabled: state.disabled;
|
||||
}
|
||||
|
||||
apply(flex_list) {
|
||||
list: default;
|
||||
scrollbar: part.scrollbar;
|
||||
scrollbar_scrolled: part.scrollbar, state.scrolled;
|
||||
}
|
||||
|
||||
apply(flex_list_text) {
|
||||
list_subheader: default;
|
||||
}
|
||||
|
||||
apply(flex_list_button) {
|
||||
list_item: default;
|
||||
transition_exit: default;
|
||||
state_pressed: state.pressed;
|
||||
transition_enter: state.pressed;
|
||||
list_item_focused: state.focusKey;
|
||||
disabled: state.disabled;
|
||||
}
|
||||
|
||||
apply(table) {
|
||||
table: default;
|
||||
focus_ring: state.focusKey;
|
||||
|
||||
@ -32,6 +32,9 @@
|
||||
@widget badge: lvgl_badge_class;
|
||||
@widget page_indicator: lvgl_page_indicator_class;
|
||||
@widget page_indicator_dot: lvgl_page_indicator_dot_class;
|
||||
@widget flex_list: lvgl_list_class;
|
||||
@widget flex_list_text: lvgl_list_text_class;
|
||||
@widget flex_list_button: lvgl_list_button_class;
|
||||
|
||||
// ============================================================
|
||||
// 1. Base tokens
|
||||
@ -146,8 +149,8 @@
|
||||
|
||||
// Page indicator (lvgl_page_indicator_create): a dot per page, the current page's dot is filled
|
||||
@page-indicator-color: @color-on-surface;
|
||||
@page-indicator-dot-size: if($isCompact, 4, 6dp);
|
||||
@page-indicator-gap: if($isCompact, 3, 6dp);
|
||||
@page-indicator-dot-size: if($isCompact, 6, 8dp);
|
||||
@page-indicator-gap: if($isCompact, 4, 6dp);
|
||||
|
||||
// Badge (lvgl_badge_create): a small dot that marks an item, e.g. a favourite
|
||||
@badge-color: @color-on-surface;
|
||||
@ -180,7 +183,6 @@
|
||||
|
||||
// Toolbar: top app bar with a divider, which shows the background like the statusbar does
|
||||
@app-bar-title-color: @color-on-surface;
|
||||
@app-bar-divider-color: @color-outline;
|
||||
@app-bar-pad-hor: if($isCompact, 0, @space-1);
|
||||
@app-bar-gap: if($isCompact, 2, @space-1);
|
||||
// The focus ring is drawn inside toolbar buttons, as there's little room around them
|
||||
@ -268,10 +270,11 @@
|
||||
@list-item-gap: if($isCompact, 3, @space-4);
|
||||
@list-divider-color: @color-outline;
|
||||
|
||||
// Tabs
|
||||
@tabs-divider-color: @color-outline;
|
||||
@tab-indicator-color: @color-primary;
|
||||
@tab-indicator-width: if($isCompact, @line-medium, @line-thick);
|
||||
// Tabs: the tab bar is a navigation bar, the selected tab is an inverted pill
|
||||
@tabs-pad: if($isCompact, 2, @space-1);
|
||||
@tabs-gap: if($isCompact, 2, @space-1);
|
||||
@tab-pad: if($isCompact, 2, @space-1);
|
||||
@tab-shape: @shape-control;
|
||||
@tab-page-pad: @density-container-pad;
|
||||
|
||||
// Dialog
|
||||
@ -441,6 +444,8 @@ chip {
|
||||
radius: @chip-shape;
|
||||
padHor: @chip-pad-hor;
|
||||
padVer: @chip-pad-ver;
|
||||
// Room for the focus ring, which the parent would clip
|
||||
marginAll: @focus-width + @focus-offset;
|
||||
}
|
||||
|
||||
button {
|
||||
@ -453,6 +458,8 @@ button {
|
||||
padHor: @button-pad-hor;
|
||||
padVer: @button-pad-ver;
|
||||
padGap: @button-gap;
|
||||
// Room for the focus ring, which the parent would clip
|
||||
marginAll: @focus-width + @focus-offset;
|
||||
}
|
||||
|
||||
icon_button {
|
||||
@ -494,9 +501,7 @@ app_bar {
|
||||
bgOpa: transp;
|
||||
textColor: @app-bar-title-color;
|
||||
radius: 0;
|
||||
borderColor: @app-bar-divider-color;
|
||||
borderWidth: @border;
|
||||
borderSide: bottom;
|
||||
borderWidth: 0;
|
||||
padHor: @app-bar-pad-hor;
|
||||
padVer: 0;
|
||||
padGap: @app-bar-gap;
|
||||
@ -516,6 +521,8 @@ app_bar_focus_ring {
|
||||
switch_track {
|
||||
width: @switch-width;
|
||||
height: @switch-height;
|
||||
// Room for the focus ring, which the parent would clip
|
||||
marginAll: @focus-width + @focus-offset;
|
||||
bgColor: @switch-track-color;
|
||||
bgOpa: cover;
|
||||
borderColor: @switch-track-border-color;
|
||||
@ -565,6 +572,8 @@ checkbox_box_selected {
|
||||
|
||||
sliderbox {
|
||||
padColumn: @sliderbox-gap;
|
||||
// Room for the focus ring of the step buttons, which the sliderbox would clip
|
||||
padAll: @focus-width + @focus-offset;
|
||||
}
|
||||
|
||||
slider_track_size {
|
||||
@ -723,22 +732,17 @@ list_subheader {
|
||||
|
||||
tabs {
|
||||
bgOpa: transp;
|
||||
borderColor: @tabs-divider-color;
|
||||
borderWidth: @border;
|
||||
borderSide: bottom;
|
||||
borderWidth: 0;
|
||||
padAll: @tabs-pad;
|
||||
padGap: @tabs-gap;
|
||||
}
|
||||
|
||||
tab {
|
||||
bgOpa: transp;
|
||||
textColor: @color-on-surface;
|
||||
borderWidth: 0;
|
||||
radius: 0;
|
||||
}
|
||||
|
||||
tab_selected {
|
||||
borderColor: @tab-indicator-color;
|
||||
borderWidth: @tab-indicator-width;
|
||||
borderSide: bottom;
|
||||
radius: @tab-shape;
|
||||
padAll: @tab-pad;
|
||||
}
|
||||
|
||||
tab_page {
|
||||
@ -978,7 +982,7 @@ apply(obj) {
|
||||
apply(tabview > *[0] > button) {
|
||||
tab: default;
|
||||
inverted: state.pressed;
|
||||
tab_selected: state.checked;
|
||||
inverted: state.checked;
|
||||
focus_ring: state.focusKey;
|
||||
edit_ring: state.edited;
|
||||
}
|
||||
@ -1077,6 +1081,11 @@ apply(page_indicator > label) {
|
||||
page_indicator_label: default;
|
||||
}
|
||||
|
||||
// Labels can be selectable, e.g. rows that keys move through
|
||||
apply(label) {
|
||||
focus_ring: state.focusKey;
|
||||
}
|
||||
|
||||
apply(page_indicator_dot) {
|
||||
page_indicator_dot: default;
|
||||
page_indicator_dot_current: state.checked;
|
||||
@ -1217,6 +1226,22 @@ apply(list_button) {
|
||||
disabled: state.disabled;
|
||||
}
|
||||
|
||||
apply(flex_list) {
|
||||
list: default;
|
||||
scrollbar: part.scrollbar;
|
||||
}
|
||||
|
||||
apply(flex_list_text) {
|
||||
list_subheader: default;
|
||||
}
|
||||
|
||||
apply(flex_list_button) {
|
||||
list_item: default;
|
||||
inverted: state.pressed;
|
||||
inverted: state.focusKey;
|
||||
disabled: state.disabled;
|
||||
}
|
||||
|
||||
apply(table) {
|
||||
table: default;
|
||||
focus_ring: state.focusKey;
|
||||
|
||||
@ -101,7 +101,7 @@ lv_obj_t* build_window_widget(lv_obj_t* content, WindowCreateWidgetsFn create_wi
|
||||
// Plain layout container, not meant to scroll on its own - every app already does this
|
||||
// for its own root object. Without it, a sub-pixel flex-layout overflow here can show the
|
||||
// theme's scrollbar styling as a thin line hugging this widget's edges.
|
||||
lv_obj_remove_flag(widget, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(widget, false);
|
||||
if (create_widgets != nullptr) {
|
||||
create_widgets(widget, user_data);
|
||||
}
|
||||
@ -206,7 +206,7 @@ error_t window_manager_start(void) {
|
||||
lv_obj_set_style_border_width(real_widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_radius(real_widget, 0, LV_STATE_DEFAULT);
|
||||
// See build_window_widget()'s identical flag removal for why.
|
||||
lv_obj_remove_flag(real_widget, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(real_widget, false);
|
||||
|
||||
content_widget = (screen_init != nullptr) ? screen_init(real_widget) : nullptr;
|
||||
if (content_widget == nullptr) {
|
||||
|
||||
@ -13,6 +13,13 @@ enum class ThemeMode {
|
||||
Dark
|
||||
};
|
||||
|
||||
enum class DensityMode {
|
||||
/** Regular or compact, as configured for the device */
|
||||
DeviceDefault,
|
||||
Regular,
|
||||
Compact
|
||||
};
|
||||
|
||||
struct AppearanceSettings {
|
||||
/** Default text font size in pixels, or 0 for the device's default size */
|
||||
uint16_t fontSize = 0;
|
||||
@ -23,15 +30,28 @@ struct AppearanceSettings {
|
||||
ThemeMode themeMode = ThemeMode::DeviceDefault;
|
||||
/** Use the monochrome theme, also on colour displays */
|
||||
bool monoTheme = false;
|
||||
DensityMode densityMode = DensityMode::DeviceDefault;
|
||||
bool animationsEnabled = true;
|
||||
/** Theme colours as 0xRRGGBB, or empty for the theme's default colour */
|
||||
std::optional<uint32_t> primaryColor;
|
||||
std::optional<uint32_t> secondaryColor;
|
||||
std::optional<uint32_t> errorColor;
|
||||
/** Surface (screen background) colour as 0xRRGGBB, or empty for the theme's own surface. See getSurfaceColor(). */
|
||||
std::optional<uint32_t> surfaceColor;
|
||||
/** How strongly surfaceColor shows, from 0 (low) to SURFACE_TINT_LEVEL_COUNT - 1 (high) */
|
||||
uint8_t surfaceTintLevel = 1;
|
||||
|
||||
bool operator==(const AppearanceSettings&) const = default;
|
||||
};
|
||||
|
||||
constexpr uint8_t SURFACE_TINT_LEVEL_COUNT = 3;
|
||||
|
||||
/**
|
||||
* Mixes the surface colour into black (dark themes) or white (light themes), so the theme's text stays readable on it.
|
||||
* @return the colour that the theme uses as its surface, as 0xRRGGBB
|
||||
*/
|
||||
uint32_t getSurfaceColor(uint32_t color, uint8_t tintLevel, bool isDark);
|
||||
|
||||
bool load(AppearanceSettings& settings);
|
||||
|
||||
AppearanceSettings loadOrGetDefault();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <src/display/lv_display.h>
|
||||
#include <lvgl.h>
|
||||
|
||||
namespace tt::settings::display {
|
||||
|
||||
|
||||
@ -48,9 +48,12 @@ public:
|
||||
Secondary
|
||||
};
|
||||
|
||||
/** Sets the colour of the tile icons. Call before creating the widgets. */
|
||||
/** Sets the colour of the tile icons. Applies from the next (re)population. */
|
||||
void setIconColor(IconColor color) { iconColor = color; }
|
||||
|
||||
/** Shows the first page from the next (re)population. */
|
||||
void showFirstPage() { page = 0; }
|
||||
|
||||
/** Swiping left or right over the grid goes to the next or previous page. Call before creating the widgets. */
|
||||
void setSwipeNavigation(bool enabled) { swipeNavigation = enabled; }
|
||||
|
||||
@ -86,11 +89,14 @@ private:
|
||||
bool swipeNavigation = false;
|
||||
bool keepSelectionOnRepopulate = true;
|
||||
lv_obj_t* grid = nullptr;
|
||||
/** The grid size that the current tiles were laid out for */
|
||||
int32_t populatedWidth = 0;
|
||||
int32_t populatedHeight = 0;
|
||||
lv_obj_t* prevButton = nullptr;
|
||||
lv_obj_t* nextButton = nullptr;
|
||||
lv_obj_t* pageIndicator = nullptr;
|
||||
lv_obj_t* pageButtons = nullptr;
|
||||
lv_obj_t* barButtons = nullptr;
|
||||
lv_obj_t* bottomBar = nullptr;
|
||||
lv_obj_t* barSpacer = nullptr;
|
||||
|
||||
void createGrid(lv_obj_t* parent);
|
||||
void goToPreviousPage();
|
||||
@ -98,6 +104,7 @@ private:
|
||||
void populate();
|
||||
|
||||
static void onDeferredRepopulate(void* userData);
|
||||
static void onDeferredResize(void* userData);
|
||||
static void onDeferredNextPage(void* userData);
|
||||
static void onDeferredPreviousPage(void* userData);
|
||||
static void onGridDeleted(lv_event_t* e);
|
||||
@ -105,6 +112,7 @@ private:
|
||||
static void onPrevPressed(lv_event_t* e);
|
||||
static void onNextPressed(lv_event_t* e);
|
||||
static void onGridGesture(lv_event_t* e);
|
||||
static void onBottomBarFocused(lv_event_t* e);
|
||||
static void onTileClicked(lv_event_t* e);
|
||||
static void onTileLongPressed(lv_event_t* e);
|
||||
static void onTileKey(lv_event_t* e);
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@ -30,6 +32,12 @@ class BootScreen {
|
||||
int logicalWidth() const { return (rotation % 2 == 0) ? panelWidth : panelHeight; }
|
||||
int logicalHeight() const { return (rotation % 2 == 0) ? panelHeight : panelWidth; }
|
||||
|
||||
/** @return true when the image has a pixel at (x, y), with its colour as RGB565 */
|
||||
using ImagePixel = std::function<bool(int x, int y, uint16_t& color)>;
|
||||
|
||||
/** Draws an image with text lines below it, centered on a black background */
|
||||
void draw(int imageWidth, int imageHeight, const ImagePixel& imagePixel, const std::vector<std::string>& lines);
|
||||
|
||||
public:
|
||||
|
||||
~BootScreen();
|
||||
@ -49,6 +57,14 @@ public:
|
||||
* @param[in] lines text lines, wrapped when they're too long for the display
|
||||
*/
|
||||
void show(const std::string& logoPath, const std::vector<std::string>& lines);
|
||||
|
||||
/**
|
||||
* Shows a QR code with text lines below it, centered on a black background. The set modules are white.
|
||||
* @param[in] moduleCount the width and height of the QR code in modules
|
||||
* @param[in] isModuleSet whether the module at (x, y) is set
|
||||
* @param[in] lines text lines, wrapped when they're too long for the display
|
||||
*/
|
||||
void showQrCode(int moduleCount, const std::function<bool(int x, int y)>& isModuleSet, const std::vector<std::string>& lines);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -26,7 +26,7 @@ class View final {
|
||||
void updateScanning();
|
||||
void updatePeerList();
|
||||
|
||||
void createPeerListItem(const bluetooth::PeerRecord& record, bool isPaired, size_t index);
|
||||
void createPeerListItem(lv_obj_t* list, const bluetooth::PeerRecord& record, bool isPaired, size_t index);
|
||||
|
||||
static void onConnect(lv_event_t* event);
|
||||
|
||||
|
||||
@ -2,9 +2,18 @@
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
namespace tt::app::boot {
|
||||
class BootScreen;
|
||||
}
|
||||
|
||||
namespace tt::app::crashdiagnostics {
|
||||
|
||||
void start();
|
||||
/**
|
||||
* Writes the crash log and shows the crash with a QR code for reporting it, after a panic reboot.
|
||||
* Returns when the user continues with any input, and blocks forever when there's no input device to continue with.
|
||||
* @param[in] screen the boot screen, which may have no display
|
||||
*/
|
||||
void showCrashScreen(boot::BootScreen& screen);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
struct Device;
|
||||
@ -9,7 +10,8 @@ namespace tt::app::i2cscanner {
|
||||
|
||||
std::string getAddressText(uint8_t address);
|
||||
|
||||
std::string getPortNamesForDropdown();
|
||||
/** The names of the ready I2C controllers, in the order of getActivePortAtIndex() */
|
||||
std::vector<std::string> getPortNames();
|
||||
|
||||
bool getActivePortAtIndex(int32_t index, struct Device** out);
|
||||
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace tt::app::launcher {
|
||||
|
||||
uint32_t start();
|
||||
|
||||
}
|
||||
27
Tactility/Private/Tactility/app/launcher/LauncherMode.h
Normal file
27
Tactility/Private/Tactility/app/launcher/LauncherMode.h
Normal file
@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/app/AppGrid.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app::launcher {
|
||||
|
||||
/** The content of the launcher grid and its tile interactions */
|
||||
struct LauncherMode {
|
||||
/** Represents this mode on the button that switches to it */
|
||||
const char* buttonIcon;
|
||||
/** The tile icon colour for themes that aren't monochrome */
|
||||
AppGrid::IconColor iconColor;
|
||||
/** Returns the items in display order */
|
||||
std::vector<AppGridItem> (*collect)();
|
||||
/** Optional */
|
||||
void (*onLongPressed)(AppGrid& grid, const ::AppManifest& manifest);
|
||||
/** Optional */
|
||||
void (*onKey)(AppGrid& grid, const ::AppManifest& manifest, uint32_t key);
|
||||
};
|
||||
|
||||
extern const LauncherMode APPS_MODE;
|
||||
extern const LauncherMode SETTINGS_MODE;
|
||||
|
||||
}
|
||||
@ -25,7 +25,7 @@ class View final {
|
||||
void updateScanning();
|
||||
void updateNetworkList();
|
||||
void updateConnectToHidden();
|
||||
void createSsidListItem(const WifiApRecord& record, bool isConnecting, size_t index);
|
||||
void createSsidListItem(lv_obj_t* list, const WifiApRecord& record, bool isConnecting, size_t index);
|
||||
|
||||
static void showDetails(lv_event_t* event);
|
||||
static void connect(lv_event_t* event);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <src/display/lv_display.h>
|
||||
#include <lvgl.h>
|
||||
|
||||
namespace tt::service::autorotate {
|
||||
|
||||
|
||||
@ -61,7 +61,6 @@ namespace app {
|
||||
namespace power { extern const ::AppManifest manifest; }
|
||||
namespace poweroff { extern const ::AppManifest manifest; }
|
||||
namespace selectiondialog { extern const ::AppManifest manifest; }
|
||||
namespace settings { extern const ::AppManifest manifest; }
|
||||
namespace setup { extern const ::AppManifest manifest; }
|
||||
namespace shell { extern const ::AppManifest manifest; extern const ::AppManifest sh_manifest; }
|
||||
namespace systeminfo { extern const ::AppManifest manifest; }
|
||||
@ -82,7 +81,6 @@ namespace app {
|
||||
#ifdef ESP_PLATFORM
|
||||
namespace apwebserver { extern const ::AppManifest manifest; }
|
||||
namespace camera { extern const ::AppManifest manifest; }
|
||||
namespace crashdiagnostics { extern const ::AppManifest manifest; }
|
||||
#if CONFIG_TT_TDECK_WORKAROUND == 1
|
||||
namespace keyboardsettings { extern const ::AppManifest manifest; } // T-Deck only for now
|
||||
#endif
|
||||
@ -131,7 +129,6 @@ static void registerInternalApps() {
|
||||
if (device_exists_of_type(&POWER_SUPPLY_TYPE)) {
|
||||
app_manager_add(&app::poweroff::manifest);
|
||||
}
|
||||
app_manager_add(&app::settings::manifest);
|
||||
app_manager_add(&app::selectiondialog::manifest);
|
||||
app_manager_add(&app::setup::manifest);
|
||||
app_manager_add(&app::shell::manifest);
|
||||
@ -154,7 +151,6 @@ static void registerInternalApps() {
|
||||
if (device_exists_of_type(&CAMERA_TYPE)) {
|
||||
app_manager_add(&app::camera::manifest);
|
||||
}
|
||||
app_manager_add(&app::crashdiagnostics::manifest);
|
||||
#if defined(CONFIG_TT_TDECK_WORKAROUND)
|
||||
app_manager_add(&app::keyboardsettings::manifest);
|
||||
#endif
|
||||
|
||||
@ -56,49 +56,50 @@ const AppGridItem* getTileItem(lv_event_t* e) {
|
||||
}
|
||||
|
||||
|
||||
lv_obj_t* createBarSection(lv_obj_t* parent, lv_flex_align_t align) {
|
||||
auto* section = lv_obj_create(parent);
|
||||
lv_obj_set_size(section, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(section, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(section, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(section, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(section, align, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_remove_flag(section, LV_OBJ_FLAG_SCROLLABLE);
|
||||
return section;
|
||||
}
|
||||
|
||||
// The width that a section's visible children need, including the gaps between them
|
||||
int32_t getContentWidth(lv_obj_t* section) {
|
||||
// The width of the visible buttons on one side of the bar's spacer, including the gaps between them
|
||||
int32_t getSideWidth(lv_obj_t* bar, bool rightSide) {
|
||||
int32_t width = 0;
|
||||
uint32_t visible = 0;
|
||||
const uint32_t count = lv_obj_get_child_count(section);
|
||||
bool afterSpacer = false;
|
||||
const uint32_t count = lv_obj_get_child_count(bar);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
lv_obj_t* child = lv_obj_get_child(section, static_cast<int32_t>(i));
|
||||
if (!lv_obj_has_flag(child, LV_OBJ_FLAG_HIDDEN)) {
|
||||
lv_obj_t* child = lv_obj_get_child(bar, static_cast<int32_t>(i));
|
||||
if (lv_obj_get_style_flex_grow(child, LV_PART_MAIN) > 0) {
|
||||
afterSpacer = true;
|
||||
} else if (afterSpacer == rightSide && !lv_obj_is_hidden(child) && !lv_obj_is_ignore_layout(child)) {
|
||||
width += lv_obj_get_width(child);
|
||||
visible++;
|
||||
}
|
||||
}
|
||||
return visible > 1 ? width + static_cast<int32_t>(visible - 1) * lv_obj_get_style_pad_column(section, LV_PART_MAIN) : width;
|
||||
return visible > 1 ? width + static_cast<int32_t>(visible - 1) * lv_obj_get_style_pad_column(bar, LV_PART_MAIN) : width;
|
||||
}
|
||||
|
||||
// The page indicator gets the space between the side sections, which grow equally to keep it centered
|
||||
// The page indicator is centered on the bar and gets the space that the buttons on the wider side leave on both sides
|
||||
void updatePageIndicatorWidth(void* data) {
|
||||
auto* bottom_bar = static_cast<lv_obj_t*>(data);
|
||||
auto* left_section = lv_obj_get_child(bottom_bar, 0);
|
||||
auto* indicator_section = lv_obj_get_child(bottom_bar, 1);
|
||||
auto* page_buttons = lv_obj_get_child(bottom_bar, 2);
|
||||
auto* indicator = lv_obj_get_child(indicator_section, 0);
|
||||
lv_obj_t* indicator = nullptr;
|
||||
const uint32_t count = lv_obj_get_child_count(bottom_bar);
|
||||
for (uint32_t i = 0; i < count && indicator == nullptr; i++) {
|
||||
lv_obj_t* child = lv_obj_get_child(bottom_bar, static_cast<int32_t>(i));
|
||||
if (lv_obj_is_ignore_layout(child)) {
|
||||
indicator = child;
|
||||
}
|
||||
}
|
||||
if (indicator == nullptr) {
|
||||
return;
|
||||
}
|
||||
const int32_t side_width = std::max(getContentWidth(left_section), getContentWidth(page_buttons));
|
||||
const int32_t side_width = std::max(getSideWidth(bottom_bar, false), getSideWidth(bottom_bar, true));
|
||||
const int32_t gaps = 2 * lv_obj_get_style_pad_column(bottom_bar, LV_PART_MAIN);
|
||||
const int32_t available = lv_obj_get_content_width(bottom_bar) - 2 * side_width - gaps;
|
||||
lv_obj_set_style_max_width(indicator, std::max<int32_t>(0, available), LV_STATE_DEFAULT);
|
||||
const int32_t max_width = std::max<int32_t>(0, available);
|
||||
// Setting a style marks the layout as dirty, which would resize the bar and call this again
|
||||
if (lv_obj_get_style_max_width(indicator, LV_PART_MAIN) != max_width) {
|
||||
lv_obj_set_style_max_width(indicator, max_width, LV_STATE_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
void onBottomBarSizeChanged(lv_event_t* event) {
|
||||
lv_async_call_cancel(updatePageIndicatorWidth, lv_event_get_target_obj(event));
|
||||
lv_async_call(updatePageIndicatorWidth, lv_event_get_target_obj(event));
|
||||
}
|
||||
|
||||
@ -120,16 +121,27 @@ void AppGrid::onDeferredRepopulate(void* userData) {
|
||||
static_cast<AppGrid*>(userData)->populate();
|
||||
}
|
||||
|
||||
// A layout pass can resize the grid temporarily (e.g. to its content height, before the parent's flex layout grows it again)
|
||||
void AppGrid::onDeferredResize(void* userData) {
|
||||
auto* self = static_cast<AppGrid*>(userData);
|
||||
if (lv_obj_get_width(self->grid) != self->populatedWidth || lv_obj_get_height(self->grid) != self->populatedHeight) {
|
||||
self->populate();
|
||||
}
|
||||
}
|
||||
|
||||
void AppGrid::onGridDeleted(lv_event_t* e) {
|
||||
// Cancels a still-pending repopulate scheduled right before the grid itself was deleted (e.g. the app closing)
|
||||
lv_async_call_cancel(onDeferredRepopulate, lv_event_get_user_data(e));
|
||||
lv_async_call_cancel(onDeferredResize, lv_event_get_user_data(e));
|
||||
lv_async_call_cancel(onDeferredNextPage, lv_event_get_user_data(e));
|
||||
lv_async_call_cancel(onDeferredPreviousPage, lv_event_get_user_data(e));
|
||||
}
|
||||
|
||||
// Deferred: page size depends on the grid size, which is final only after the layout pass that triggered this.
|
||||
void AppGrid::onGridSizeChanged(lv_event_t* e) {
|
||||
lv_async_call(onDeferredRepopulate, lv_event_get_user_data(e));
|
||||
// One pending check is enough, also when the size changes several times before it runs
|
||||
lv_async_call_cancel(onDeferredResize, lv_event_get_user_data(e));
|
||||
lv_async_call(onDeferredResize, lv_event_get_user_data(e));
|
||||
}
|
||||
|
||||
// Changing the page doesn't keep the selected app selected: that would return to the selected app's page
|
||||
@ -213,6 +225,8 @@ void AppGrid::populate() {
|
||||
// Centers a full page of rows, so a partially filled last page keeps its rows in the same positions.
|
||||
lv_obj_set_style_pad_top(grid, 0, LV_STATE_DEFAULT);
|
||||
const PageLayout layout = computePageLayout(grid);
|
||||
populatedWidth = lv_obj_get_width(grid);
|
||||
populatedHeight = lv_obj_get_height(grid);
|
||||
lv_obj_set_style_pad_top(grid, layout.topOffset, LV_STATE_DEFAULT);
|
||||
|
||||
const uint32_t item_count = items.size();
|
||||
@ -229,8 +243,9 @@ void AppGrid::populate() {
|
||||
page = std::min(page, page_count - 1);
|
||||
pageCount = page_count;
|
||||
|
||||
lv_obj_set_flag(pageIndicator, LV_OBJ_FLAG_HIDDEN, page_count <= 1);
|
||||
lv_obj_set_flag(pageButtons, LV_OBJ_FLAG_HIDDEN, page_count <= 1);
|
||||
lv_obj_set_hidden(pageIndicator, page_count <= 1);
|
||||
lv_obj_set_hidden(prevButton, page_count <= 1);
|
||||
lv_obj_set_hidden(nextButton, page_count <= 1);
|
||||
lvgl_page_indicator_set_page_count(pageIndicator, page_count);
|
||||
lvgl_page_indicator_set_page(pageIndicator, page);
|
||||
|
||||
@ -306,42 +321,64 @@ void AppGrid::createGrid(lv_obj_t* parent) {
|
||||
lv_obj_set_style_pad_row(grid, TILE_GAP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_column(grid, TILE_GAP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(grid, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_remove_flag(grid, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(grid, false);
|
||||
// The arrow keys move between the tiles in rows and columns
|
||||
lvgl_grid_navigation_add(grid);
|
||||
lv_obj_add_event_cb(grid, onGridDeleted, LV_EVENT_DELETE, this);
|
||||
if (swipeNavigation) {
|
||||
// LVGL sends a gesture to the first object, starting at the touched one, that doesn't pass it on to its parent
|
||||
lv_obj_remove_flag(grid, LV_OBJ_FLAG_GESTURE_BUBBLE);
|
||||
lv_obj_set_gesture_bubble(grid, false);
|
||||
lv_obj_add_event_cb(grid, onGridGesture, LV_EVENT_GESTURE, this);
|
||||
}
|
||||
}
|
||||
|
||||
// Keys enter the bar on the previous page button, or on the first button when there's only one page
|
||||
void AppGrid::onBottomBarFocused(lv_event_t* e) {
|
||||
const auto* self = static_cast<AppGrid*>(lv_event_get_user_data(e));
|
||||
lv_obj_t* current = lvgl_grid_navigation_get_focused(self->bottomBar);
|
||||
lv_obj_t* target = lv_obj_is_hidden(self->prevButton) ? lv_obj_get_child(self->bottomBar, 0) : self->prevButton;
|
||||
if (current == nullptr || target == current || target == self->barSpacer) {
|
||||
return;
|
||||
}
|
||||
const bool showSelection = lv_obj_has_state(current, LV_STATE_FOCUS_KEY);
|
||||
lv_gridnav_set_focused(self->bottomBar, target, LV_ANIM_OFF);
|
||||
if (!showSelection) {
|
||||
lv_obj_remove_state(target, LV_STATE_FOCUS_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
void AppGrid::createWidgetsWithBottomBar(lv_obj_t* parent) {
|
||||
// The side sections grow equally, so the page indicator stays centered
|
||||
auto* bottom_bar = createBarSection(parent, LV_FLEX_ALIGN_START);
|
||||
lv_obj_set_width(bottom_bar, LV_PCT(100));
|
||||
lvgl_obj_add_edge_padding(bottom_bar);
|
||||
barButtons = createBarSection(bottom_bar, LV_FLEX_ALIGN_START);
|
||||
lv_obj_set_flex_grow(barButtons, 1);
|
||||
auto* indicator_section = createBarSection(bottom_bar, LV_FLEX_ALIGN_CENTER);
|
||||
pageButtons = createBarSection(bottom_bar, LV_FLEX_ALIGN_END);
|
||||
lv_obj_set_flex_grow(pageButtons, 1);
|
||||
lv_obj_add_event_cb(bottom_bar, onBottomBarSizeChanged, LV_EVENT_SIZE_CHANGED, nullptr);
|
||||
lv_obj_add_event_cb(bottom_bar, onBottomBarDeleted, LV_EVENT_DELETE, nullptr);
|
||||
bottomBar = lv_obj_create(parent);
|
||||
lv_obj_set_size(bottomBar, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(bottomBar, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(bottomBar, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(bottomBar, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(bottomBar, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(bottomBar, false);
|
||||
lvgl_obj_add_edge_padding(bottomBar);
|
||||
lv_obj_add_event_cb(bottomBar, onBottomBarSizeChanged, LV_EVENT_SIZE_CHANGED, nullptr);
|
||||
lv_obj_add_event_cb(bottomBar, onBottomBarDeleted, LV_EVENT_DELETE, nullptr);
|
||||
|
||||
createGrid(parent);
|
||||
lv_obj_move_foreground(bottom_bar);
|
||||
lv_obj_move_foreground(bottomBar);
|
||||
|
||||
pageIndicator = lvgl_page_indicator_create(indicator_section);
|
||||
prevButton = lvgl_icon_button_create(pageButtons);
|
||||
// The bar buttons go before the spacer, which pushes the page buttons to the right
|
||||
barSpacer = lv_obj_create(bottomBar);
|
||||
lv_obj_set_size(barSpacer, 0, 0);
|
||||
lv_obj_set_flex_grow(barSpacer, 1);
|
||||
lv_obj_set_clickable(barSpacer, false);
|
||||
prevButton = lvgl_icon_button_create(bottomBar);
|
||||
lv_label_set_text(lv_label_create(prevButton), "<");
|
||||
lv_obj_add_event_cb(prevButton, onPrevPressed, LV_EVENT_SHORT_CLICKED, this);
|
||||
nextButton = lvgl_icon_button_create(pageButtons);
|
||||
nextButton = lvgl_icon_button_create(bottomBar);
|
||||
lv_label_set_text(lv_label_create(nextButton), ">");
|
||||
lv_obj_add_event_cb(nextButton, onNextPressed, LV_EVENT_SHORT_CLICKED, this);
|
||||
// The arrow keys move between the page buttons
|
||||
lvgl_grid_navigation_add(pageButtons);
|
||||
pageIndicator = lvgl_page_indicator_create(bottomBar);
|
||||
lv_obj_set_ignore_layout(pageIndicator, true);
|
||||
lv_obj_align(pageIndicator, LV_ALIGN_CENTER, 0, 0);
|
||||
// The arrow keys move between all buttons of the bar as one row
|
||||
lvgl_grid_navigation_add(bottomBar);
|
||||
lv_obj_add_event_cb(bottomBar, onBottomBarFocused, LV_EVENT_FOCUSED, this);
|
||||
|
||||
// Resolves the grid's flex_grow height, which the page size is derived from.
|
||||
lv_obj_update_layout(parent);
|
||||
@ -356,7 +393,8 @@ void AppGrid::createWidgetsWithBottomBar(lv_obj_t* parent) {
|
||||
}
|
||||
|
||||
lv_obj_t* AppGrid::addBarButton(const char* icon, lv_event_cb_t onClicked, void* userData) {
|
||||
auto* button = lvgl_icon_button_create(barButtons);
|
||||
auto* button = lvgl_icon_button_create(bottomBar);
|
||||
lv_obj_move_to_index(button, lv_obj_get_index(barSpacer));
|
||||
auto* label = lv_label_create(button);
|
||||
lv_obj_set_style_text_font(label, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(label, icon);
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/theme.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
@ -74,8 +75,11 @@ constexpr NamedColor PALETTE_COLORS[] = {
|
||||
{ "Grey", LV_PALETTE_GREY },
|
||||
};
|
||||
|
||||
constexpr size_t COLOR_COUNT = 3;
|
||||
constexpr const char* COLOR_TITLES[COLOR_COUNT] = { "Primary color", "Secondary color", "Error color" };
|
||||
constexpr size_t COLOR_COUNT = 4;
|
||||
constexpr size_t SURFACE_COLOR_INDEX = 2;
|
||||
constexpr const char* COLOR_TITLES[COLOR_COUNT] = { "Primary color", "Secondary color", "Surface color", "Error color" };
|
||||
// In tint level order (see settings::appearance::getSurfaceColor())
|
||||
constexpr const char* SURFACE_TINT_NAMES[settings::appearance::SURFACE_TINT_LEVEL_COUNT] = { "Low", "Medium", "High" };
|
||||
|
||||
struct ColorRowWidgets {
|
||||
lv_obj_t* row = nullptr;
|
||||
@ -118,7 +122,11 @@ struct Context {
|
||||
AppStream selectStream {};
|
||||
uint8_t selectBuffer[256] {};
|
||||
|
||||
/** The scroll position of the content, kept while e.g. a selection dialog covers the window. Accessed with the LVGL lock held. */
|
||||
int32_t contentScrollY = 0;
|
||||
|
||||
// Valid while the window is shown
|
||||
lv_obj_t* content = nullptr;
|
||||
lv_obj_t* applyButton = nullptr;
|
||||
lv_obj_t* spinner = nullptr;
|
||||
FontRowWidgets regularRow;
|
||||
@ -126,9 +134,17 @@ struct Context {
|
||||
lv_obj_t* errorLabel = nullptr;
|
||||
lv_obj_t* lightChip = nullptr;
|
||||
lv_obj_t* darkChip = nullptr;
|
||||
lv_obj_t* regularDensityChip = nullptr;
|
||||
lv_obj_t* compactDensityChip = nullptr;
|
||||
lv_obj_t* regularThemeChip = nullptr;
|
||||
lv_obj_t* monoThemeChip = nullptr;
|
||||
// Shown for the regular theme: the "Theme Colors" title and card
|
||||
lv_obj_t* colorsTitle = nullptr;
|
||||
lv_obj_t* colorsCard = nullptr;
|
||||
ColorRowWidgets colorRows[COLOR_COUNT];
|
||||
/** Shown when a surface colour is set */
|
||||
lv_obj_t* surfaceTintRow = nullptr;
|
||||
lv_obj_t* surfaceTintChips[settings::appearance::SURFACE_TINT_LEVEL_COUNT] = {};
|
||||
/** The display can only show the mono theme */
|
||||
bool isMonoDisplay = false;
|
||||
};
|
||||
@ -155,9 +171,9 @@ size_t getCharacterCount(const std::string& path) {
|
||||
|
||||
void setHidden(lv_obj_t* object, bool hidden) {
|
||||
if (hidden) {
|
||||
lv_obj_add_flag(object, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(object, true);
|
||||
} else {
|
||||
lv_obj_remove_flag(object, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(object, false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,6 +193,23 @@ bool isDeviceDefaultDark() {
|
||||
return defaults.is_dark;
|
||||
}
|
||||
|
||||
bool isDeviceDefaultCompact() {
|
||||
LvglThemeSettings defaults;
|
||||
lvgl_theme_get_default_settings(&defaults);
|
||||
return defaults.is_compact;
|
||||
}
|
||||
|
||||
bool isCompact(const settings::appearance::AppearanceSettings& settings) {
|
||||
switch (settings.densityMode) {
|
||||
case settings::appearance::DensityMode::Regular:
|
||||
return false;
|
||||
case settings::appearance::DensityMode::Compact:
|
||||
return true;
|
||||
default:
|
||||
return isDeviceDefaultCompact();
|
||||
}
|
||||
}
|
||||
|
||||
bool isDark(const settings::appearance::AppearanceSettings& settings) {
|
||||
switch (settings.themeMode) {
|
||||
case settings::appearance::ThemeMode::Light:
|
||||
@ -197,22 +230,39 @@ void setChecked(lv_obj_t* object, bool checked) {
|
||||
}
|
||||
|
||||
void updateColorRow(Context* ctx, size_t index);
|
||||
uint32_t getDefaultColor(size_t index);
|
||||
|
||||
void updateSurfaceTintRow(Context* ctx) {
|
||||
if (ctx->surfaceTintRow == nullptr) {
|
||||
return;
|
||||
}
|
||||
const auto& settings = ctx->pendingSettings;
|
||||
setHidden(ctx->surfaceTintRow, !settings.surfaceColor.has_value());
|
||||
for (uint8_t i = 0; i < settings::appearance::SURFACE_TINT_LEVEL_COUNT; i++) {
|
||||
setChecked(ctx->surfaceTintChips[i], settings.surfaceTintLevel == i);
|
||||
}
|
||||
}
|
||||
|
||||
void updateThemeWidgets(Context* ctx) {
|
||||
const bool dark = isDark(ctx->pendingSettings);
|
||||
setChecked(ctx->lightChip, !dark);
|
||||
setChecked(ctx->darkChip, dark);
|
||||
const bool compact = isCompact(ctx->pendingSettings);
|
||||
setChecked(ctx->regularDensityChip, !compact);
|
||||
setChecked(ctx->compactDensityChip, compact);
|
||||
if (ctx->regularThemeChip != nullptr) {
|
||||
setChecked(ctx->regularThemeChip, !ctx->pendingSettings.monoTheme);
|
||||
setChecked(ctx->monoThemeChip, ctx->pendingSettings.monoTheme);
|
||||
}
|
||||
// The mono theme doesn't use colours
|
||||
for (size_t i = 0; i < COLOR_COUNT; i++) {
|
||||
if (ctx->colorRows[i].row != nullptr) {
|
||||
setHidden(ctx->colorRows[i].row, ctx->pendingSettings.monoTheme);
|
||||
if (ctx->colorsCard != nullptr) {
|
||||
setHidden(ctx->colorsTitle, ctx->pendingSettings.monoTheme);
|
||||
setHidden(ctx->colorsCard, ctx->pendingSettings.monoTheme);
|
||||
for (size_t i = 0; i < COLOR_COUNT; i++) {
|
||||
updateColorRow(ctx, i);
|
||||
}
|
||||
}
|
||||
updateSurfaceTintRow(ctx);
|
||||
}
|
||||
|
||||
/** Updates the widgets to the pending settings. Requires the LVGL lock. */
|
||||
@ -247,6 +297,24 @@ void onDarkPressed(lv_event_t* event) {
|
||||
setDark(static_cast<Context*>(lv_event_get_user_data(event)), true);
|
||||
}
|
||||
|
||||
void setCompact(Context* ctx, bool compact) {
|
||||
// The device default is kept when it matches, so it doesn't count as a change
|
||||
if (compact == isDeviceDefaultCompact()) {
|
||||
ctx->pendingSettings.densityMode = settings::appearance::DensityMode::DeviceDefault;
|
||||
} else {
|
||||
ctx->pendingSettings.densityMode = compact ? settings::appearance::DensityMode::Compact : settings::appearance::DensityMode::Regular;
|
||||
}
|
||||
updateWidgets(ctx);
|
||||
}
|
||||
|
||||
void onRegularDensityPressed(lv_event_t* event) {
|
||||
setCompact(static_cast<Context*>(lv_event_get_user_data(event)), false);
|
||||
}
|
||||
|
||||
void onCompactDensityPressed(lv_event_t* event) {
|
||||
setCompact(static_cast<Context*>(lv_event_get_user_data(event)), true);
|
||||
}
|
||||
|
||||
void onRegularThemePressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
ctx->pendingSettings.monoTheme = false;
|
||||
@ -259,10 +327,16 @@ void onMonoThemePressed(lv_event_t* event) {
|
||||
updateWidgets(ctx);
|
||||
}
|
||||
|
||||
void onSurfaceTintPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
ctx->pendingSettings.surfaceTintLevel = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(lv_obj_get_user_data(lv_event_get_target_obj(event))));
|
||||
updateWidgets(ctx);
|
||||
}
|
||||
|
||||
void onAnimationsChanged(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* checkbox = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
ctx->pendingSettings.animationsEnabled = lv_obj_has_state(checkbox, LV_STATE_CHECKED);
|
||||
auto* animations_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
ctx->pendingSettings.animationsEnabled = lv_obj_has_state(animations_switch, LV_STATE_CHECKED);
|
||||
updateWidgets(ctx);
|
||||
}
|
||||
|
||||
@ -278,22 +352,35 @@ std::optional<uint32_t>& getColor(settings::appearance::AppearanceSettings& sett
|
||||
return settings.primaryColor;
|
||||
case 1:
|
||||
return settings.secondaryColor;
|
||||
case SURFACE_COLOR_INDEX:
|
||||
return settings.surfaceColor;
|
||||
default:
|
||||
return settings.errorColor;
|
||||
}
|
||||
}
|
||||
|
||||
/** The surface has no default colour of its own: the theme derives it */
|
||||
uint32_t getDefaultColor(size_t index) {
|
||||
LvglThemeSettings defaults;
|
||||
lvgl_theme_get_default_settings(&defaults);
|
||||
const lv_color_t colors[COLOR_COUNT] = { defaults.color_primary, defaults.color_secondary, defaults.color_error };
|
||||
return lv_color_to_u32(colors[index]) & 0xFFFFFF;
|
||||
const lv_color_t color = index == 0 ? defaults.color_primary : (index == 1 ? defaults.color_secondary : defaults.color_error);
|
||||
return lv_color_to_u32(color) & 0xFFFFFF;
|
||||
}
|
||||
|
||||
void updateColorRow(Context* ctx, size_t index) {
|
||||
const auto& widgets = ctx->colorRows[index];
|
||||
const auto& color = getColor(ctx->pendingSettings, index);
|
||||
lv_obj_set_style_bg_color(widgets.swatch, lv_color_hex(color.value_or(getDefaultColor(index))), LV_STATE_DEFAULT);
|
||||
uint32_t shown;
|
||||
if (color.has_value()) {
|
||||
// The picked colour: the surface's tinted version of it is often too dark (or light) to recognise
|
||||
shown = *color;
|
||||
} else if (index != SURFACE_COLOR_INDEX) {
|
||||
shown = getDefaultColor(index);
|
||||
} else {
|
||||
// The theme's own near-black or near-white surface
|
||||
shown = isDark(ctx->pendingSettings) ? 0x131313 : 0xFAFAFA;
|
||||
}
|
||||
lv_obj_set_style_bg_color(widgets.swatch, lv_color_hex(shown), LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
void onColorButtonPressed(lv_event_t* event) {
|
||||
@ -372,10 +459,7 @@ lv_obj_t* createRow(lv_obj_t* parent) {
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(row, lv_obj_get_style_pad_column(parent, LV_PART_MAIN), LV_STATE_DEFAULT);
|
||||
// Room for the focus outline of the buttons, which is clipped to the row in compact mode
|
||||
lv_obj_set_style_pad_ver(row, 3, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_hor(row, 3, LV_STATE_DEFAULT);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(row, false);
|
||||
return row;
|
||||
}
|
||||
|
||||
@ -390,8 +474,10 @@ lv_obj_t* createSection(lv_obj_t* parent, const char* title) {
|
||||
return card;
|
||||
}
|
||||
|
||||
/** A row with a title. Its other items move to the next line when they don't fit next to the title. */
|
||||
lv_obj_t* createLabeledRow(lv_obj_t* parent, const char* text) {
|
||||
auto* row = createRow(parent);
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW_WRAP);
|
||||
auto* label = lv_label_create(row);
|
||||
lv_label_set_text(label, text);
|
||||
lv_obj_set_flex_grow(label, 1);
|
||||
@ -414,8 +500,8 @@ ColorRowWidgets createColorRow(lv_obj_t* parent, size_t index, Context* ctx) {
|
||||
// The colour preview: its colour is the setting, its border uses the theme's text colour
|
||||
widgets.swatch = lv_obj_create(widgets.row);
|
||||
lv_obj_remove_style_all(widgets.swatch);
|
||||
lv_obj_remove_flag(widgets.swatch, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_remove_flag(widgets.swatch, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_scrollable(widgets.swatch, false);
|
||||
lv_obj_set_clickable(widgets.swatch, false);
|
||||
lv_obj_set_style_bg_opa(widgets.swatch, LV_OPA_COVER, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(widgets.swatch, 1, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_color(widgets.swatch, lv_obj_get_style_text_color(widgets.swatch, LV_PART_MAIN), LV_STATE_DEFAULT);
|
||||
@ -448,14 +534,34 @@ void createThemeCard(lv_obj_t* parent, Context* ctx) {
|
||||
ctx->monoThemeChip = createChip(style_row, "Mono", onMonoThemePressed, ctx);
|
||||
}
|
||||
|
||||
auto* animations_checkbox = lv_checkbox_create(card);
|
||||
lv_checkbox_set_text(animations_checkbox, "Animations");
|
||||
setChecked(animations_checkbox, ctx->pendingSettings.animationsEnabled);
|
||||
lv_obj_add_event_cb(animations_checkbox, onAnimationsChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
auto* density_row = createLabeledRow(card, "Density");
|
||||
ctx->regularDensityChip = createChip(density_row, "Regular", onRegularDensityPressed, ctx);
|
||||
ctx->compactDensityChip = createChip(density_row, "Compact", onCompactDensityPressed, ctx);
|
||||
|
||||
if (!ctx->isMonoDisplay) {
|
||||
for (size_t i = 0; i < COLOR_COUNT; i++) {
|
||||
ctx->colorRows[i] = createColorRow(card, i, ctx);
|
||||
auto* animations_row = createLabeledRow(card, "Animations");
|
||||
auto* animations_switch = lv_switch_create(animations_row);
|
||||
setChecked(animations_switch, ctx->pendingSettings.animationsEnabled);
|
||||
lv_obj_add_event_cb(animations_switch, onAnimationsChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
}
|
||||
|
||||
void createColorsCard(lv_obj_t* parent, Context* ctx) {
|
||||
ctx->colorsTitle = lv_label_create(parent);
|
||||
lv_label_set_text(ctx->colorsTitle, "Theme Colors");
|
||||
|
||||
auto* card = lvgl_card_create(parent);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
ctx->colorsCard = card;
|
||||
|
||||
for (size_t i = 0; i < COLOR_COUNT; i++) {
|
||||
ctx->colorRows[i] = createColorRow(card, i, ctx);
|
||||
if (i == SURFACE_COLOR_INDEX) {
|
||||
// How strongly the surface colour shows: it's mixed into black (dark) or white (light)
|
||||
ctx->surfaceTintRow = createLabeledRow(card, "Surface tint");
|
||||
for (uint8_t level = 0; level < settings::appearance::SURFACE_TINT_LEVEL_COUNT; level++) {
|
||||
ctx->surfaceTintChips[level] = createChip(ctx->surfaceTintRow, SURFACE_TINT_NAMES[level], onSurfaceTintPressed, ctx);
|
||||
lv_obj_set_user_data(ctx->surfaceTintChips[level], reinterpret_cast<void*>(static_cast<uintptr_t>(level)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -470,21 +576,30 @@ lv_obj_t* createButton(lv_obj_t* parent, const char* text, lv_event_cb_t callbac
|
||||
|
||||
FontRowWidgets createFontRow(lv_obj_t* parent, const char* title, lv_event_cb_t onSelect, lv_event_cb_t onDefault, Context* ctx) {
|
||||
FontRowWidgets widgets;
|
||||
// "Title [Default]"
|
||||
auto* title_row = createRow(parent);
|
||||
auto* label = lv_label_create(title_row);
|
||||
lv_label_set_text(label, title);
|
||||
lv_obj_set_flex_grow(label, 1);
|
||||
lv_obj_set_style_pad_ver(label, 0, LV_STATE_DEFAULT);
|
||||
widgets.defaultButton = createButton(title_row, "Default", onDefault, ctx);
|
||||
// "Title [Default] [Select]"
|
||||
// "file.ttf"
|
||||
auto* row = createRow(parent);
|
||||
|
||||
// " file.ttf [Select]"
|
||||
auto* file_row = createRow(parent);
|
||||
widgets.fileLabel = lv_label_create(file_row);
|
||||
lv_obj_set_flex_grow(widgets.fileLabel, 1);
|
||||
lv_label_set_long_mode(widgets.fileLabel, LV_LABEL_LONG_MODE_DOTS);
|
||||
lv_obj_set_style_pad_left(widgets.fileLabel, 12, LV_STATE_DEFAULT);
|
||||
createButton(file_row, "Select", onSelect, ctx);
|
||||
// The title with the file name below it, which take the width that the buttons leave. Texts that don't fit scroll.
|
||||
auto* texts = lv_obj_create(row);
|
||||
lv_obj_remove_style_all(texts);
|
||||
lv_obj_set_height(texts, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_grow(texts, 1);
|
||||
lv_obj_set_flex_flow(texts, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_scrollable(texts, false);
|
||||
|
||||
auto* label = lv_label_create(texts);
|
||||
lv_obj_set_width(label, LV_PCT(100));
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
lv_label_set_text(label, title);
|
||||
|
||||
widgets.fileLabel = lv_label_create(texts);
|
||||
lv_obj_set_width(widgets.fileLabel, LV_PCT(100));
|
||||
lv_label_set_long_mode(widgets.fileLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
lv_obj_set_style_text_font(widgets.fileLabel, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
||||
|
||||
widgets.defaultButton = createButton(row, "Default", onDefault, ctx);
|
||||
createButton(row, "Select", onSelect, ctx);
|
||||
|
||||
widgets.warningLabel = lv_label_create(parent);
|
||||
lv_obj_set_width(widgets.warningLabel, LV_PCT(100));
|
||||
@ -504,6 +619,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
ctx->applyButton = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_OK, onApplyPressed, ctx);
|
||||
|
||||
auto* content = lv_obj_create(parent);
|
||||
ctx->content = content;
|
||||
lv_obj_set_width(content, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(content, 1);
|
||||
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN);
|
||||
@ -512,6 +628,10 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
const auto color_format = lv_display_get_color_format(lv_obj_get_display(parent));
|
||||
ctx->isMonoDisplay = color_format == LV_COLOR_FORMAT_I1 || color_format == LV_COLOR_FORMAT_L8;
|
||||
createThemeCard(content, ctx);
|
||||
// Monochrome and greyscale displays always use the mono theme, which doesn't use colours
|
||||
if (!ctx->isMonoDisplay) {
|
||||
createColorsCard(content, ctx);
|
||||
}
|
||||
|
||||
auto* fonts_card = createSection(content, "Fonts");
|
||||
auto* font_size_row = createRow(fonts_card);
|
||||
@ -543,7 +663,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
createStepButton(font_size_row, LV_SYMBOL_PLUS, onFontSizeIncrementPressed, font_size_spinbox);
|
||||
|
||||
ctx->regularRow = createFontRow(fonts_card, "Regular font", onSelectRegularPressed, onDefaultRegularPressed, ctx);
|
||||
ctx->monoRow = createFontRow(fonts_card, "Mono font", onSelectMonoPressed, onDefaultMonoPressed, ctx);
|
||||
ctx->monoRow = createFontRow(fonts_card, "Monospace font", onSelectMonoPressed, onDefaultMonoPressed, ctx);
|
||||
|
||||
ctx->errorLabel = lv_label_create(fonts_card);
|
||||
lv_obj_set_width(ctx->errorLabel, LV_PCT(100));
|
||||
@ -552,10 +672,17 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_style_text_color(ctx->errorLabel, lv_palette_main(LV_PALETTE_RED), LV_STATE_DEFAULT);
|
||||
|
||||
updateWidgets(ctx);
|
||||
|
||||
// The window is rebuilt when it's shown again, e.g. after a selection dialog
|
||||
lv_obj_update_layout(content);
|
||||
lv_obj_scroll_to_y(content, ctx->contentScrollY, LV_ANIM_OFF);
|
||||
}
|
||||
|
||||
void destroyWidgets(void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
// The LVGL lock is already held, so the content can still be read
|
||||
ctx->contentScrollY = lv_obj_get_scroll_y(ctx->content);
|
||||
ctx->content = nullptr;
|
||||
ctx->applyButton = nullptr;
|
||||
ctx->spinner = nullptr;
|
||||
ctx->regularRow = {};
|
||||
@ -563,11 +690,19 @@ void destroyWidgets(void* userData) {
|
||||
ctx->errorLabel = nullptr;
|
||||
ctx->lightChip = nullptr;
|
||||
ctx->darkChip = nullptr;
|
||||
ctx->regularDensityChip = nullptr;
|
||||
ctx->compactDensityChip = nullptr;
|
||||
ctx->regularThemeChip = nullptr;
|
||||
ctx->monoThemeChip = nullptr;
|
||||
ctx->colorsTitle = nullptr;
|
||||
ctx->colorsCard = nullptr;
|
||||
for (auto& row : ctx->colorRows) {
|
||||
row = {};
|
||||
}
|
||||
ctx->surfaceTintRow = nullptr;
|
||||
for (auto& chip : ctx->surfaceTintChips) {
|
||||
chip = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t applyThreadMain(void* context) {
|
||||
|
||||
@ -20,6 +20,8 @@
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/spinner.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
@ -94,14 +96,21 @@ void onRefreshPressed(lv_event_t* e) {
|
||||
requestRefresh(ctx);
|
||||
}
|
||||
|
||||
lv_obj_t* createCard(lv_obj_t* parent) {
|
||||
auto* card = lvgl_card_create(parent);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
return card;
|
||||
}
|
||||
|
||||
void showRefreshFailedError(Context* ctx, const char* message) {
|
||||
lv_obj_clean(ctx->contentWrapper);
|
||||
|
||||
auto* label = lv_label_create(ctx->contentWrapper);
|
||||
auto* label = lv_label_create(createCard(ctx->contentWrapper));
|
||||
lv_obj_set_width(label, LV_PCT(100));
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_WRAP);
|
||||
lv_label_set_text(label, message);
|
||||
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
|
||||
|
||||
lv_obj_remove_flag(ctx->refreshButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->refreshButton, false);
|
||||
}
|
||||
|
||||
void showNoInternet(Context* ctx) {
|
||||
@ -129,15 +138,23 @@ void showApps(Context* ctx) {
|
||||
return left.appName < right.appName;
|
||||
});
|
||||
|
||||
auto* list = lv_list_create(ctx->contentWrapper);
|
||||
auto* card = createCard(ctx->contentWrapper);
|
||||
lv_obj_set_style_pad_all(card, 0, LV_STATE_DEFAULT);
|
||||
// The list items' pressed and focused backgrounds follow the card's rounded corners
|
||||
lv_obj_set_style_clip_corner(card, true, LV_STATE_DEFAULT);
|
||||
|
||||
// The card provides the background
|
||||
auto* list = lvgl_list_create(card);
|
||||
lv_obj_set_style_pad_all(list, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_size(list, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(list, 0, LV_STATE_DEFAULT);
|
||||
for (int i = 0; i < ctx->entries.size(); i++) {
|
||||
auto& entry = ctx->entries[i];
|
||||
LOG_I(TAG, "Adding %s", entry.appName.c_str());
|
||||
AppManifest manifest;
|
||||
const char* icon = app_manager_find_manifest(entry.appId.c_str(), &manifest) == ERROR_NONE ? LV_SYMBOL_OK : nullptr;
|
||||
auto* entry_button = lv_list_add_button(list, icon, entry.appName.c_str());
|
||||
auto* entry_button = lvgl_list_add_button(list, icon, entry.appName.c_str());
|
||||
auto int_as_voidptr = reinterpret_cast<void*>(i);
|
||||
lv_obj_set_user_data(entry_button, int_as_voidptr);
|
||||
lv_obj_add_event_cb(entry_button, onAppPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
@ -163,7 +180,7 @@ void refresh(Context* ctx) {
|
||||
lv_obj_clean(ctx->contentWrapper);
|
||||
auto* spinner = lvgl_spinner_create(ctx->contentWrapper);
|
||||
lv_obj_align(spinner, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_obj_add_flag(ctx->refreshButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->refreshButton, true);
|
||||
lvgl_unlock();
|
||||
|
||||
WifiStationState station_state = WIFI_STATION_STATE_DISCONNECTED;
|
||||
@ -245,13 +262,12 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
// The global toolbar nav callback only knows how to stop old-model apps.
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
ctx->refreshButton = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_REFRESH, onRefreshPressed, ctx);
|
||||
lv_obj_add_flag(ctx->refreshButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->refreshButton, true);
|
||||
|
||||
ctx->contentWrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(ctx->contentWrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_width(ctx->contentWrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(ctx->contentWrapper, 1);
|
||||
lv_obj_set_style_pad_all(ctx->contentWrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_ver(ctx->contentWrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
ctx->restoreScrollOnNextShow = true;
|
||||
if (ctx->needsInitialRefresh) {
|
||||
|
||||
@ -94,7 +94,7 @@ void uninstallApp(Context* ctx) {
|
||||
LOG_I(TAG, "Uninstall");
|
||||
|
||||
lvgl_lock();
|
||||
lv_obj_remove_flag(ctx->spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->spinner, false);
|
||||
lvgl_unlock();
|
||||
|
||||
app_uninstall(ctx->entry.appId.c_str());
|
||||
@ -172,7 +172,7 @@ void installApp(Context* ctx) {
|
||||
LOG_I(TAG, "Install");
|
||||
|
||||
lvgl_lock();
|
||||
lv_obj_remove_flag(ctx->spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->spinner, false);
|
||||
lvgl_unlock();
|
||||
|
||||
doInstall(ctx);
|
||||
@ -182,7 +182,7 @@ void updateApp(Context* ctx) {
|
||||
LOG_I(TAG, "Update");
|
||||
|
||||
lvgl_lock();
|
||||
lv_obj_remove_flag(ctx->spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->spinner, false);
|
||||
lvgl_unlock();
|
||||
|
||||
LOG_I(TAG, "Removing previous version");
|
||||
@ -195,8 +195,8 @@ void updateViews(Context* ctx) {
|
||||
lvgl_toolbar_clear_actions(ctx->toolbar);
|
||||
auto app_id = ctx->entry.appId.c_str();
|
||||
ctx->spinner = lvgl_toolbar_add_spinner_action(ctx->toolbar);
|
||||
lv_obj_add_flag(ctx->spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(ctx->updateLabel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->spinner, true);
|
||||
lv_obj_set_hidden(ctx->updateLabel, true);
|
||||
|
||||
char install_path[128];
|
||||
bool is_installed = app_get_install_path(app_id, install_path, sizeof(install_path)) == ERROR_NONE
|
||||
@ -208,7 +208,7 @@ void updateViews(Context* ctx) {
|
||||
if (app_package_manifest_parse(metadata_path.c_str(), &package, nullptr, 0) == ERROR_NONE
|
||||
&& package.version_code < ctx->entry.appVersionCode) {
|
||||
ctx->updateButton = lvgl_toolbar_add_image_button_action(ctx->toolbar, LV_SYMBOL_DOWNLOAD, onUpdatePressed, ctx);
|
||||
lv_obj_remove_flag(ctx->updateLabel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->updateLabel, false);
|
||||
}
|
||||
lvgl_toolbar_add_image_button_action(ctx->toolbar, LV_SYMBOL_TRASH, onUninstallPressed, ctx);
|
||||
} else {
|
||||
@ -226,6 +226,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
// The global toolbar nav callback only knows how to stop old-model apps.
|
||||
lvgl_toolbar_set_nav_action(ctx->toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
|
||||
#include <TactilityCpp/Allocator.h>
|
||||
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
#include <lvgl.h>
|
||||
#include <algorithm>
|
||||
@ -50,7 +52,7 @@ void createPackageWidget(const PackageManifest* package, lv_obj_t* list) {
|
||||
// A v2 package's single app shares its id, and its name is the nicer label. Otherwise show the package id.
|
||||
AppManifest appManifest;
|
||||
const char* label = (app_manager_find_manifest(package->id, &appManifest) == ERROR_NONE) ? appManifest.name : package->id;
|
||||
lv_obj_t* btn = lv_list_add_button(list, LVGL_ICON_SHARED_DEPLOYED_CODE, label);
|
||||
lv_obj_t* btn = lvgl_list_add_button(list, LVGL_ICON_SHARED_DEPLOYED_CODE, label);
|
||||
lv_obj_t* image = lv_obj_get_child(btn, 0);
|
||||
lv_obj_set_style_text_font(image, lvgl_get_shared_icon_default_font(), LV_PART_MAIN);
|
||||
lv_obj_add_event_cb(btn, &onPackagePressed, LV_EVENT_SHORT_CLICKED, const_cast<PackageManifest*>(package));
|
||||
@ -73,9 +75,14 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
// The global toolbar nav callback only knows how to stop old-model apps.
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
|
||||
lv_obj_t* list = lv_list_create(parent);
|
||||
lv_obj_set_width(list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(list, 1);
|
||||
auto* content = lv_obj_create(parent);
|
||||
lv_obj_set_width(content, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(content, 1);
|
||||
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_border_width(content, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* card = lvgl_card_create(content);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
|
||||
// createWidgets() can rerun for this same Context (window rebuild-on-remove).
|
||||
ctx->packages.clear();
|
||||
@ -84,25 +91,24 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
return strcmp(left.id, right.id) < 0;
|
||||
});
|
||||
|
||||
for (const auto& package : ctx->packages) {
|
||||
createPackageWidget(&package, list);
|
||||
if (ctx->packages.empty()) {
|
||||
auto* no_apps_label = lv_label_create(card);
|
||||
lv_label_set_text(no_apps_label, "No apps installed.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx->packages.empty()) {
|
||||
// lv_obj_align() is ignored for children of a flex-managed parent, so the empty-state
|
||||
// label needs its own flex-growing wrapper to center within; the (empty) list is hidden
|
||||
// rather than deleted so the wrapper can just take its place in the flex flow.
|
||||
lv_obj_add_flag(list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_flex_grow(list, 0);
|
||||
// The list items' pressed and focused backgrounds follow the card's rounded corners
|
||||
lv_obj_set_style_pad_all(card, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_clip_corner(card, true, LV_STATE_DEFAULT);
|
||||
|
||||
auto* empty_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(empty_wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(empty_wrapper, 1);
|
||||
lv_obj_set_flex_align(empty_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_border_width(empty_wrapper, 0, LV_STATE_DEFAULT);
|
||||
// The card provides the background
|
||||
lv_obj_t* list = lvgl_list_create(card);
|
||||
lv_obj_set_size(list, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(list, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* no_apps_label = lv_label_create(empty_wrapper);
|
||||
lv_label_set_text(no_apps_label, "No apps installed");
|
||||
for (const auto& package : ctx->packages) {
|
||||
createPackageWidget(&package, list);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -44,13 +44,14 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(parent, false);
|
||||
|
||||
auto* toolbar = lvgl_toolbar_create(parent, "AP Web Server");
|
||||
// The global toolbar nav callback only knows how to stop old-model apps.
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
|
||||
lv_obj_t* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_style_pad_all(wrapper, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_pad_row(wrapper, 4, LV_PART_MAIN);
|
||||
|
||||
@ -48,7 +48,7 @@ void onInputEnabledSwitch(lv_event_t* event) {
|
||||
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
|
||||
service::audio::setInputEnabled(enabled);
|
||||
lv_obj_set_flag(ctx->inputCard, LV_OBJ_FLAG_HIDDEN, !enabled);
|
||||
lv_obj_set_hidden(ctx->inputCard, !enabled);
|
||||
}
|
||||
|
||||
void onOutputEnabledSwitch(lv_event_t* event) {
|
||||
@ -56,7 +56,7 @@ void onOutputEnabledSwitch(lv_event_t* event) {
|
||||
auto* sw = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
|
||||
service::audio::setOutputEnabled(enabled);
|
||||
lv_obj_set_flag(ctx->outputCard, LV_OBJ_FLAG_HIDDEN, !enabled);
|
||||
lv_obj_set_hidden(ctx->outputCard, !enabled);
|
||||
}
|
||||
|
||||
void onInputMuteSwitch(lv_event_t* event) {
|
||||
@ -88,6 +88,9 @@ lv_obj_t* createRow(lv_obj_t* parent) {
|
||||
auto* row = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(row);
|
||||
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(row, false);
|
||||
return row;
|
||||
}
|
||||
|
||||
@ -102,17 +105,16 @@ Section createSection(lv_obj_t* parent, const char* title, bool enabled, lv_even
|
||||
|
||||
auto* title_label = lv_label_create(header);
|
||||
lv_label_set_text(title_label, title);
|
||||
lv_obj_align(title_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(title_label, 1);
|
||||
|
||||
auto* sw = lv_switch_create(header);
|
||||
lv_obj_align(sw, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_set_state(sw, LV_STATE_CHECKED, enabled);
|
||||
lv_obj_add_event_cb(sw, onEnabledChanged, LV_EVENT_VALUE_CHANGED, userData);
|
||||
|
||||
auto* card = lvgl_card_create(parent);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flag(card, LV_OBJ_FLAG_HIDDEN, !enabled);
|
||||
lv_obj_set_hidden(card, !enabled);
|
||||
|
||||
return { sw, card };
|
||||
}
|
||||
@ -122,10 +124,9 @@ lv_obj_t* createSwitchRow(lv_obj_t* parent, const char* label, lv_event_cb_t cb,
|
||||
|
||||
auto* row_label = lv_label_create(row);
|
||||
lv_label_set_text(row_label, label);
|
||||
lv_obj_align(row_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(row_label, 1);
|
||||
|
||||
auto* sw = lv_switch_create(row);
|
||||
lv_obj_align(sw, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(sw, cb, LV_EVENT_VALUE_CHANGED, userData);
|
||||
|
||||
return sw;
|
||||
@ -134,13 +135,14 @@ lv_obj_t* createSwitchRow(lv_obj_t* parent, const char* label, lv_event_cb_t cb,
|
||||
lv_obj_t* createSliderRow(lv_obj_t* parent, const char* label, int32_t initialValue, lv_event_cb_t cb, void* userData) {
|
||||
auto* row = createRow(parent);
|
||||
|
||||
// The label gets the width that the slider leaves, and scrolls when its text doesn't fit
|
||||
auto* row_label = lv_label_create(row);
|
||||
lv_label_set_text(row_label, label);
|
||||
lv_obj_align(row_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(row_label, 1);
|
||||
lv_label_set_long_mode(row_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
|
||||
auto* sliderBox = lvgl_sliderbox_create(row, 0, 100, 10, initialValue);
|
||||
lv_obj_set_width(sliderBox, LV_PCT(50));
|
||||
lv_obj_align(sliderBox, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_set_width(sliderBox, LV_PCT(75));
|
||||
lvgl_sliderbox_add_value_changed_cb(sliderBox, cb, userData);
|
||||
|
||||
return sliderBox;
|
||||
@ -150,7 +152,7 @@ void refresh(Context* ctx) {
|
||||
if (ctx->inputEnabledSwitch) {
|
||||
const bool enabled = service::audio::isInputEnabled();
|
||||
lv_obj_set_state(ctx->inputEnabledSwitch, LV_STATE_CHECKED, enabled);
|
||||
lv_obj_set_flag(ctx->inputCard, LV_OBJ_FLAG_HIDDEN, !enabled);
|
||||
lv_obj_set_hidden(ctx->inputCard, !enabled);
|
||||
}
|
||||
if (ctx->inputMuteSwitch) {
|
||||
if (service::audio::isInputMuted()) lv_obj_add_state(ctx->inputMuteSwitch, LV_STATE_CHECKED);
|
||||
@ -163,7 +165,7 @@ void refresh(Context* ctx) {
|
||||
if (ctx->outputEnabledSwitch) {
|
||||
const bool enabled = service::audio::isOutputEnabled();
|
||||
lv_obj_set_state(ctx->outputEnabledSwitch, LV_STATE_CHECKED, enabled);
|
||||
lv_obj_set_flag(ctx->outputCard, LV_OBJ_FLAG_HIDDEN, !enabled);
|
||||
lv_obj_set_hidden(ctx->outputCard, !enabled);
|
||||
}
|
||||
if (ctx->outputMuteSwitch) {
|
||||
if (service::audio::isOutputMuted()) lv_obj_add_state(ctx->outputMuteSwitch, LV_STATE_CHECKED);
|
||||
@ -185,6 +187,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
|
||||
auto* main_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(main_wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(main_wrapper, 1);
|
||||
|
||||
@ -187,13 +187,6 @@ void waitForInputAndReboot() {
|
||||
}
|
||||
|
||||
void startNextApp() {
|
||||
#ifdef ESP_PLATFORM
|
||||
if (esp_reset_reason() == ESP_RST_PANIC) {
|
||||
crashdiagnostics::start(); // fire-and-forget; no result expected back
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
auto launcher_app_id = getLauncherAppId();
|
||||
uint32_t launcher_instance_id = 0;
|
||||
AppStartContext context;
|
||||
@ -271,6 +264,12 @@ bool bootInit(TickType_t startTime) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
if (esp_reset_reason() == ESP_RST_PANIC) {
|
||||
crashdiagnostics::showCrashScreen(screen);
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_I(TAG, "Loading fonts");
|
||||
lvgl::loadFonts(lvgl::loadFontConfiguration());
|
||||
lvgl::configureTheme(settings::appearance::loadOrGetDefault());
|
||||
|
||||
@ -154,7 +154,41 @@ void BootScreen::show(const std::string& logoPath, const std::vector<std::string
|
||||
}
|
||||
}
|
||||
|
||||
// Layout in logical (rotated) coordinates: logo with text below it, centered as a whole
|
||||
draw(static_cast<int>(logo.width), static_cast<int>(logo.height), [&logo](int x, int y, uint16_t& color) {
|
||||
const unsigned char* pixel = logo.rgba + (static_cast<size_t>(y) * logo.width + x) * 4;
|
||||
const uint32_t alpha = pixel[3];
|
||||
if (alpha == 0) {
|
||||
return false;
|
||||
}
|
||||
// Blended onto the black background
|
||||
color = toRgb565(pixel[0] * alpha / 255, pixel[1] * alpha / 255, pixel[2] * alpha / 255);
|
||||
return true;
|
||||
}, lines);
|
||||
}
|
||||
|
||||
void BootScreen::showQrCode(int moduleCount, const std::function<bool(int x, int y)>& isModuleSet, const std::vector<std::string>& lines) {
|
||||
if (target == nullptr || moduleCount <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// About 60% of the display, which leaves room for the text
|
||||
const int module_size = std::max(1, getSmallestDimension() * 6 / 10 / moduleCount);
|
||||
const int size = module_size * moduleCount;
|
||||
draw(size, size, [&isModuleSet, module_size](int x, int y, uint16_t& color) {
|
||||
if (!isModuleSet(x / module_size, y / module_size)) {
|
||||
return false;
|
||||
}
|
||||
color = 0xFFFF;
|
||||
return true;
|
||||
}, lines);
|
||||
}
|
||||
|
||||
void BootScreen::draw(int imageWidth, int imageHeight, const ImagePixel& imagePixel, const std::vector<std::string>& lines) {
|
||||
if (target == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Layout in logical (rotated) coordinates: the image with text below it, centered as a whole
|
||||
if (!lines.empty() && textFont == nullptr) {
|
||||
textFont = lvgl::loadMonoFont();
|
||||
if (textFont == nullptr) {
|
||||
@ -179,10 +213,10 @@ void BootScreen::show(const std::string& logoPath, const std::vector<std::string
|
||||
}
|
||||
}
|
||||
const int text_height = static_cast<int>(wrapped_lines.size()) * line_height;
|
||||
const int gap = (logo.height > 0 && !wrapped_lines.empty()) ? line_height : 0;
|
||||
const int content_top = (logical_height - static_cast<int>(logo.height) - gap - text_height) / 2;
|
||||
const int logo_left = (logical_width - static_cast<int>(logo.width)) / 2;
|
||||
const int text_top = content_top + static_cast<int>(logo.height) + gap;
|
||||
const int gap = (imageHeight > 0 && !wrapped_lines.empty()) ? line_height : 0;
|
||||
const int content_top = (logical_height - imageHeight - gap - text_height) / 2;
|
||||
const int image_left = (logical_width - imageWidth) / 2;
|
||||
const int text_top = content_top + imageHeight + gap;
|
||||
|
||||
std::vector<uint8_t> glyph_bitmap;
|
||||
for (int band_top = 0; band_top < panelHeight; band_top += bandHeight) {
|
||||
@ -204,14 +238,11 @@ void BootScreen::show(const std::string& logoPath, const std::vector<std::string
|
||||
}
|
||||
};
|
||||
|
||||
for (unsigned y = 0; y < logo.height; y++) {
|
||||
for (unsigned x = 0; x < logo.width; x++) {
|
||||
const unsigned char* pixel = logo.rgba + (static_cast<size_t>(y) * logo.width + x) * 4;
|
||||
const uint32_t alpha = pixel[3];
|
||||
if (alpha != 0) {
|
||||
// Blended onto the black background
|
||||
const uint16_t color = toRgb565(pixel[0] * alpha / 255, pixel[1] * alpha / 255, pixel[2] * alpha / 255);
|
||||
plot(logo_left + static_cast<int>(x), content_top + static_cast<int>(y), color, PIXEL_BUFFER_CONVERSION_LUMA_THRESHOLD);
|
||||
for (int y = 0; y < imageHeight; y++) {
|
||||
for (int x = 0; x < imageWidth; x++) {
|
||||
uint16_t color;
|
||||
if (imagePixel(x, y, color)) {
|
||||
plot(image_left + x, content_top + y, color, PIXEL_BUFFER_CONVERSION_LUMA_THRESHOLD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,8 @@
|
||||
#include <Tactility/Tactility.h>
|
||||
|
||||
#include <app/event.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
namespace tt::app::btmanage {
|
||||
@ -98,7 +100,26 @@ static uint8_t mapRssiToPercentage(int8_t rssi) {
|
||||
return static_cast<uint8_t>((float)(90 - abs_rssi) / 60.f * 100.f);
|
||||
}
|
||||
|
||||
void View::createPeerListItem(const bluetooth::PeerRecord& record, bool isPaired, size_t index) {
|
||||
/** Creates a title with a card below it, and returns the list in the card */
|
||||
static lv_obj_t* createSection(lv_obj_t* parent, const char* title) {
|
||||
auto* label = lv_label_create(parent);
|
||||
lv_label_set_text(label, title);
|
||||
|
||||
auto* card = lvgl_card_create(parent);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(card, 0, LV_STATE_DEFAULT);
|
||||
// The list items' pressed and focused backgrounds follow the card's rounded corners
|
||||
lv_obj_set_style_clip_corner(card, true, LV_STATE_DEFAULT);
|
||||
|
||||
// The card provides the background
|
||||
auto* list = lvgl_list_create(card);
|
||||
lv_obj_set_size(list, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(list, 0, LV_STATE_DEFAULT);
|
||||
return list;
|
||||
}
|
||||
|
||||
void View::createPeerListItem(lv_obj_t* list, const bluetooth::PeerRecord& record, bool isPaired, size_t index) {
|
||||
const auto percentage = mapRssiToPercentage(record.rssi);
|
||||
const auto label = record.name.empty()
|
||||
? std::format("Unknown ({:02x}{:02x}{:02x}{:02x}{:02x}{:02x}) {}%",
|
||||
@ -107,7 +128,7 @@ void View::createPeerListItem(const bluetooth::PeerRecord& record, bool isPaired
|
||||
percentage)
|
||||
: std::format("{} {}%", record.name, percentage);
|
||||
|
||||
auto* button = lv_list_add_button(peers_list, nullptr, label.c_str());
|
||||
auto* button = lvgl_list_add_button(list, nullptr, label.c_str());
|
||||
|
||||
auto* item_data = new PeerListItemData { context, state, bindings, index, isPaired };
|
||||
lv_obj_set_user_data(button, item_data);
|
||||
@ -154,9 +175,9 @@ void View::updateEnableOnBootToggle() {
|
||||
|
||||
void View::updateScanning() {
|
||||
if (state->getRadioState() == bluetooth::RadioState::On && state->isScanning()) {
|
||||
lv_obj_remove_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(scanning_spinner, false);
|
||||
} else {
|
||||
lv_obj_add_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(scanning_spinner, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,22 +186,20 @@ void View::updatePeerList() {
|
||||
|
||||
// Enable on boot
|
||||
|
||||
// An unstyled row insets the wrapper like the list items' text: margins would make a full-width item overflow the list
|
||||
auto* enable_on_boot_row = lv_obj_create(peers_list);
|
||||
lv_obj_remove_style_all(enable_on_boot_row);
|
||||
lv_obj_set_size(enable_on_boot_row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
const int32_t enable_on_boot_inset = (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) ? 2 : LV_DPX(16);
|
||||
lv_obj_set_style_pad_hor(enable_on_boot_row, enable_on_boot_inset, LV_STATE_DEFAULT);
|
||||
|
||||
auto* enable_on_boot_wrapper = lv_obj_create(enable_on_boot_row);
|
||||
auto* enable_on_boot_wrapper = lv_obj_create(peers_list);
|
||||
lv_obj_set_size(enable_on_boot_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(enable_on_boot_wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(enable_on_boot_wrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(enable_on_boot_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(enable_on_boot_wrapper, false);
|
||||
|
||||
auto* enable_label = lv_label_create(enable_on_boot_wrapper);
|
||||
lv_label_set_text(enable_label, "Enable on boot");
|
||||
lv_obj_align(enable_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(enable_label, 1);
|
||||
|
||||
enable_on_boot_switch = lv_switch_create(enable_on_boot_wrapper);
|
||||
lv_obj_align(enable_on_boot_switch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(enable_on_boot_switch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
lv_obj_add_event_cb(enable_on_boot_wrapper, onEnableOnBootParentClicked, LV_EVENT_SHORT_CLICKED, enable_on_boot_switch);
|
||||
|
||||
@ -191,22 +210,21 @@ void View::updatePeerList() {
|
||||
// Paired peers section
|
||||
auto paired = state->getPairedPeers();
|
||||
if (!paired.empty()) {
|
||||
lv_list_add_text(peers_list, "Paired");
|
||||
auto* paired_section = createSection(peers_list, "Paired");
|
||||
for (size_t i = 0; i < paired.size(); ++i) {
|
||||
createPeerListItem(paired[i], true, i);
|
||||
createPeerListItem(paired_section, paired[i], true, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Scan results section
|
||||
auto scan_results = state->getScanResults();
|
||||
lv_list_add_text(peers_list, "Available");
|
||||
auto* available_section = createSection(peers_list, "Available");
|
||||
if (!scan_results.empty()) {
|
||||
for (size_t i = 0; i < scan_results.size(); ++i) {
|
||||
createPeerListItem(scan_results[i], false, i);
|
||||
createPeerListItem(available_section, scan_results[i], false, i);
|
||||
}
|
||||
} else if (!state->isScanning()) {
|
||||
auto* no_devices_label = lv_label_create(peers_list);
|
||||
lv_label_set_text(no_devices_label, "No devices found.");
|
||||
lvgl_list_add_text(available_section, "No devices found.");
|
||||
}
|
||||
// Never hide peers_list: it always contains the "Enable on boot" row.
|
||||
// While scanning with no results the spinner in the toolbar provides feedback.
|
||||
@ -241,9 +259,11 @@ void View::init(void* newContext, lv_obj_t* parent) {
|
||||
lv_obj_add_event_cb(enable_switch, onEnableSwitchChanged, LV_EVENT_VALUE_CHANGED, context);
|
||||
|
||||
// Peer list
|
||||
peers_list = lv_list_create(parent);
|
||||
peers_list = lv_obj_create(parent);
|
||||
lv_obj_set_flex_grow(peers_list, 1);
|
||||
lv_obj_set_width(peers_list, LV_PCT(100));
|
||||
lv_obj_set_flex_flow(peers_list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_border_width(peers_list, 0, LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
void View::update() {
|
||||
|
||||
@ -49,12 +49,12 @@ bool isCurrentlyConnected(const Context* ctx) {
|
||||
|
||||
void updateViews(const Context* ctx) {
|
||||
if (isCurrentlyConnected(ctx)) {
|
||||
lv_obj_remove_flag(ctx->disconnectButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(ctx->connectButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->disconnectButton, false);
|
||||
lv_obj_set_hidden(ctx->connectButton, true);
|
||||
lv_obj_remove_state(ctx->disconnectButton, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_obj_add_flag(ctx->disconnectButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(ctx->connectButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->disconnectButton, true);
|
||||
lv_obj_set_hidden(ctx->connectButton, false);
|
||||
lv_obj_remove_state(ctx->connectButton, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
@ -154,17 +154,19 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
// Auto-connect toggle row
|
||||
auto* auto_connect_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(auto_connect_wrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(auto_connect_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(auto_connect_wrapper, false);
|
||||
lvgl::obj_set_style_bg_invisible(auto_connect_wrapper);
|
||||
lv_obj_set_style_pad_all(auto_connect_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(auto_connect_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* auto_connect_label = lv_label_create(auto_connect_wrapper);
|
||||
lv_label_set_text(auto_connect_label, "Auto-connect");
|
||||
lv_obj_align(auto_connect_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(auto_connect_label, 1);
|
||||
|
||||
auto* auto_connect_switch = lv_switch_create(auto_connect_wrapper);
|
||||
lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
lv_obj_align(auto_connect_switch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
|
||||
if (deviceLoaded && device.autoConnect) {
|
||||
lv_obj_add_state(auto_connect_switch, LV_STATE_CHECKED);
|
||||
|
||||
@ -71,7 +71,7 @@ static void anim_opa_cb(void* var, int32_t v) {
|
||||
|
||||
static void toast_fade_out_ready_cb(lv_anim_t* a) {
|
||||
lv_obj_t* toast = (lv_obj_t*)a->var;
|
||||
lv_obj_add_flag(toast, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(toast, true);
|
||||
}
|
||||
|
||||
static void toastTimerCb(lv_timer_t *timer) {
|
||||
@ -101,7 +101,7 @@ static void showToast(Context* ctx, const char *text) {
|
||||
lv_anim_delete(ctx->toastLabel, anim_opa_cb);
|
||||
|
||||
lv_label_set_text(ctx->toastLabel, text);
|
||||
lv_obj_remove_flag(ctx->toastLabel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->toastLabel, false);
|
||||
lv_obj_move_foreground(ctx->toastLabel);
|
||||
|
||||
lv_anim_t a;
|
||||
@ -361,7 +361,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
ctx->stopping.store(false);
|
||||
|
||||
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(parent, false);
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
@ -378,7 +378,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_height(main_wrapper, LV_PCT(100));
|
||||
lv_obj_set_style_pad_all(main_wrapper, 6, 0);
|
||||
lv_obj_set_style_border_width(main_wrapper, 0, 0);
|
||||
lv_obj_remove_flag(main_wrapper, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(main_wrapper, false);
|
||||
|
||||
ctx->previewContainer = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(ctx->previewContainer, LV_PCT(100), LV_PCT(100));
|
||||
@ -386,7 +386,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_style_border_width(ctx->previewContainer, 0, 0);
|
||||
lv_obj_set_style_pad_all(ctx->previewContainer, 0, 0);
|
||||
lv_obj_set_style_radius(ctx->previewContainer, 0, 0);
|
||||
lv_obj_remove_flag(ctx->previewContainer, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(ctx->previewContainer, false);
|
||||
|
||||
ctx->previewCanvas = lv_canvas_create(ctx->previewContainer);
|
||||
lv_obj_set_style_bg_color(ctx->previewCanvas, lv_color_black(), 0);
|
||||
@ -406,7 +406,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_style_border_color(ctx->toastLabel, lv_color_hex(0x3B82F6), 0);
|
||||
lv_obj_set_style_shadow_width(ctx->toastLabel, 16, 0);
|
||||
lv_obj_set_style_shadow_color(ctx->toastLabel, lv_color_black(), 0);
|
||||
lv_obj_add_flag(ctx->toastLabel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->toastLabel, true);
|
||||
|
||||
lv_display_t* disp = lv_display_get_default();
|
||||
if (disp) {
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
|
||||
#include <app/event.h>
|
||||
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <cstdio>
|
||||
@ -43,10 +44,12 @@ void ChatView::createInputBar(lv_obj_t* parent) {
|
||||
inputWrapper = lv_obj_create(parent);
|
||||
auto* wrapper = inputWrapper;
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_column(wrapper, 4, 0);
|
||||
lv_obj_set_style_border_opa(wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(wrapper, false);
|
||||
|
||||
inputField = lv_textarea_create(wrapper);
|
||||
lv_obj_set_flex_grow(inputField, 1);
|
||||
@ -55,8 +58,6 @@ void ChatView::createInputBar(lv_obj_t* parent) {
|
||||
lv_textarea_set_max_length(inputField, MAX_MESSAGE_LEN);
|
||||
|
||||
auto* sendBtn = lv_button_create(wrapper);
|
||||
lv_obj_set_style_margin_all(sendBtn, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_margin_top(sendBtn, 2, LV_STATE_DEFAULT);
|
||||
lv_obj_add_event_cb(sendBtn, onSendClicked, LV_EVENT_CLICKED, this);
|
||||
|
||||
auto* btnLabel = lv_label_create(sendBtn);
|
||||
@ -66,12 +67,13 @@ void ChatView::createInputBar(lv_obj_t* parent) {
|
||||
|
||||
void ChatView::createSettingsPanel(lv_obj_t* parent) {
|
||||
settingsPanel = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(settingsPanel, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_width(settingsPanel, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(settingsPanel, 1);
|
||||
lv_obj_set_flex_flow(settingsPanel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_all(settingsPanel, 8, 0);
|
||||
lv_obj_set_style_pad_row(settingsPanel, 6, 0);
|
||||
lv_obj_add_flag(settingsPanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(settingsPanel, true);
|
||||
|
||||
// Nickname
|
||||
auto* nickLabel = lv_label_create(settingsPanel);
|
||||
@ -95,6 +97,7 @@ void ChatView::createSettingsPanel(lv_obj_t* parent) {
|
||||
// Buttons
|
||||
auto* btnRow = lv_obj_create(settingsPanel);
|
||||
lv_obj_set_flex_flow(btnRow, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(btnRow, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_size(btnRow, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(btnRow, 0, 0);
|
||||
lv_obj_set_style_pad_column(btnRow, 8, 0);
|
||||
@ -113,12 +116,13 @@ void ChatView::createSettingsPanel(lv_obj_t* parent) {
|
||||
|
||||
void ChatView::createChannelPanel(lv_obj_t* parent) {
|
||||
channelPanel = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(channelPanel, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_width(channelPanel, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(channelPanel, 1);
|
||||
lv_obj_set_flex_flow(channelPanel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_all(channelPanel, 8, 0);
|
||||
lv_obj_set_style_pad_row(channelPanel, 6, 0);
|
||||
lv_obj_add_flag(channelPanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(channelPanel, true);
|
||||
|
||||
auto* label = lv_label_create(channelPanel);
|
||||
lv_label_set_text(label, "Channel (e.g. #general):");
|
||||
@ -130,6 +134,7 @@ void ChatView::createChannelPanel(lv_obj_t* parent) {
|
||||
|
||||
auto* btnRow = lv_obj_create(channelPanel);
|
||||
lv_obj_set_flex_flow(btnRow, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(btnRow, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_size(btnRow, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(btnRow, 0, 0);
|
||||
lv_obj_set_style_pad_column(btnRow, 8, 0);
|
||||
@ -163,7 +168,7 @@ void ChatView::init(lv_obj_t* parent) {
|
||||
updateToolbarTitle();
|
||||
|
||||
// Message list
|
||||
msgList = lv_list_create(parent);
|
||||
msgList = lvgl_list_create(parent);
|
||||
lv_obj_set_flex_grow(msgList, 1);
|
||||
lv_obj_set_width(msgList, LV_PCT(100));
|
||||
if (lv_display_get_color_format(lv_obj_get_display(parent)) != LV_COLOR_FORMAT_L8) {
|
||||
@ -220,16 +225,16 @@ void ChatView::showSettings(const ChatSettingsData& current) {
|
||||
lv_textarea_set_text(keyInput, "");
|
||||
}
|
||||
|
||||
lv_obj_add_flag(msgList, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(inputWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(settingsPanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(msgList, true);
|
||||
lv_obj_set_hidden(inputWrapper, true);
|
||||
lv_obj_set_hidden(settingsPanel, false);
|
||||
}
|
||||
|
||||
void ChatView::hideSettings() {
|
||||
if (!settingsPanel || !msgList || !inputWrapper) return;
|
||||
lv_obj_add_flag(settingsPanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(msgList, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(inputWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(settingsPanel, true);
|
||||
lv_obj_set_hidden(msgList, false);
|
||||
lv_obj_set_hidden(inputWrapper, false);
|
||||
}
|
||||
|
||||
void ChatView::showChannelSelector() {
|
||||
@ -238,16 +243,16 @@ void ChatView::showChannelSelector() {
|
||||
std::string current = state->getCurrentChannel();
|
||||
lv_textarea_set_text(channelInput, current.c_str());
|
||||
|
||||
lv_obj_add_flag(msgList, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(inputWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(channelPanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(msgList, true);
|
||||
lv_obj_set_hidden(inputWrapper, true);
|
||||
lv_obj_set_hidden(channelPanel, false);
|
||||
}
|
||||
|
||||
void ChatView::hideChannelSelector() {
|
||||
if (!channelPanel || !msgList || !inputWrapper) return;
|
||||
lv_obj_add_flag(channelPanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(msgList, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(inputWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(channelPanel, true);
|
||||
lv_obj_set_hidden(msgList, false);
|
||||
lv_obj_set_hidden(inputWrapper, false);
|
||||
}
|
||||
|
||||
void ChatView::onSendClicked(lv_event_t* e) {
|
||||
|
||||
@ -1,26 +1,15 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "Tactility/PanicHandler.h"
|
||||
|
||||
#include <Tactility/app/crashdiagnostics/CrashDiagnostics.h>
|
||||
|
||||
#include <Tactility/PanicHandler.h>
|
||||
#include <Tactility/app/boot/BootScreen.h>
|
||||
#include <Tactility/app/crashdiagnostics/QrHelpers.h>
|
||||
#include <Tactility/app/crashdiagnostics/QrUrl.h>
|
||||
#include <Tactility/app/launcher/Launcher.h>
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/lvgl/Statusbar.h>
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/start.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <qrcode.h>
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/drivers/pointer.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/paths.h>
|
||||
|
||||
@ -32,32 +21,19 @@
|
||||
|
||||
#include <sdkconfig.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app::crashdiagnostics {
|
||||
|
||||
constexpr auto* TAG = "CrashDiagnostics";
|
||||
|
||||
extern const ::AppManifest manifest;
|
||||
|
||||
namespace {
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
// Set when widget creation hit an unrecoverable error (e.g. the QR code doesn't fit on
|
||||
// screen) - appMain() skips the event loop and closes immediately without ever starting
|
||||
// the launcher, matching the old model's stop()-without-launcher-start() error paths.
|
||||
bool hasFatalError = false;
|
||||
// Set by onContinuePressed() right before it emits APP_EVENT_CLOSE - read by appMain()
|
||||
// after its own thread finishes cleanup, to decide whether to start the launcher
|
||||
// afterwards (matches the old model's onContinuePressed(): stop() then launcher::start()).
|
||||
bool continuePressed = false;
|
||||
};
|
||||
|
||||
|
||||
const char* crashCauseToString(CrashCause cause) {
|
||||
switch (cause) {
|
||||
case CrashCause::Debug: return "Debug";
|
||||
@ -117,175 +93,73 @@ void writeCrashLogFile(const CrashData& crashData) {
|
||||
}
|
||||
}
|
||||
|
||||
void onContinuePressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
ctx->continuePressed = true;
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
auto* display = lv_obj_get_display(parent);
|
||||
int32_t parent_height = lv_display_get_vertical_resolution(display) - lvgl::statusbar_get_height();
|
||||
|
||||
lv_obj_add_event_cb(parent, onContinuePressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
auto* top_label = lv_label_create(parent);
|
||||
lv_label_set_text(top_label, "Oops! We've crashed ..."); // TODO: Funny messages
|
||||
lv_obj_align(top_label, LV_ALIGN_TOP_MID, 0, 2);
|
||||
|
||||
auto* bottom_label = lv_label_create(parent);
|
||||
if (device_has_active_by_type(&POINTER_TYPE)) {
|
||||
lv_label_set_text(bottom_label, "Tap screen to continue");
|
||||
void waitForInputOrForever(bool hasInput) {
|
||||
if (hasInput) {
|
||||
boot::waitForInput();
|
||||
} else {
|
||||
lv_label_set_text(bottom_label, "Reboot device to continue");
|
||||
// Without input to continue with, the device has to be restarted
|
||||
while (true) {
|
||||
delay_millis(1000);
|
||||
}
|
||||
}
|
||||
lv_obj_align(bottom_label, LV_ALIGN_BOTTOM_MID, 0, -2);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void showCrashScreen(boot::BootScreen& screen) {
|
||||
const auto& crash_data = getRtcCrashData();
|
||||
writeCrashLogFile(crash_data);
|
||||
|
||||
std::string url = getUrlFromCrashData(crash_data);
|
||||
std::string prompt = boot::getInputPrompt("continue");
|
||||
const bool has_input = !prompt.empty();
|
||||
if (!has_input) {
|
||||
prompt = "Restart device";
|
||||
}
|
||||
// Without a callstack, there's nothing worth reporting through the QR code
|
||||
if (crash_data.callstackLength == 0) {
|
||||
std::vector<std::string> lines = { "Oops! We've crashed ..."};
|
||||
if (crash_data.cause != CrashCause::Unknown) {
|
||||
lines.push_back(std::string("Only the cause is known: ") + crashCauseToString(crash_data.cause));
|
||||
}
|
||||
lines.push_back(prompt);
|
||||
screen.show("", lines);
|
||||
waitForInputOrForever(has_input);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<std::string> lines = { "Oops! We've crashed ...", prompt };
|
||||
|
||||
// The QR code links to a page that shows the crash details
|
||||
const std::string url = getUrlFromCrashData(crash_data);
|
||||
LOG_I(TAG, "%s", url.c_str());
|
||||
size_t url_length = url.length();
|
||||
|
||||
int qr_version;
|
||||
if (!getQrVersionForBinaryDataLength(url_length, qr_version)) {
|
||||
std::unique_ptr<uint8_t[]> qr_buffer;
|
||||
QRCode qr_code;
|
||||
bool has_qr_code = false;
|
||||
if (!getQrVersionForBinaryDataLength(url.length(), qr_version)) {
|
||||
LOG_E(TAG, "QR is too large");
|
||||
ctx->hasFatalError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "QR version %d (length: %d)", qr_version, (int)url_length);
|
||||
auto qrcodeData = std::make_shared<uint8_t[]>(qrcode_getBufferSize(qr_version));
|
||||
if (qrcodeData == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate QR buffer");
|
||||
ctx->hasFatalError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
QRCode qrcode;
|
||||
LOG_I(TAG, "QR init text");
|
||||
if (qrcode_initText(&qrcode, qrcodeData.get(), qr_version, ECC_LOW, url.c_str()) != 0) {
|
||||
LOG_E(TAG, "QR init text failed");
|
||||
ctx->hasFatalError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "QR size: %d", qrcode.size);
|
||||
|
||||
// Calculate QR dot size
|
||||
int32_t top_label_height = lv_obj_get_height(top_label) + 2;
|
||||
int32_t bottom_label_height = lv_obj_get_height(bottom_label) + 2;
|
||||
LOG_I(TAG, "Create canvas");
|
||||
int32_t available_height = parent_height - top_label_height - bottom_label_height;
|
||||
int32_t available_width = lv_display_get_horizontal_resolution(display);
|
||||
int32_t smallest_size = std::min(available_height, available_width);
|
||||
// Target ~60% of the available space so the code scales with screen size but keeps a margin
|
||||
// from the labels/screen edges.
|
||||
int32_t target_size = smallest_size * 6 / 10;
|
||||
int32_t pixel_size = std::max<int32_t>(1, target_size / qrcode.size);
|
||||
if (pixel_size * qrcode.size > smallest_size) {
|
||||
LOG_E(TAG, "QR code won't fit screen");
|
||||
ctx->hasFatalError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
auto* canvas = lv_canvas_create(parent);
|
||||
lv_obj_set_size(canvas, pixel_size * qrcode.size, pixel_size * qrcode.size);
|
||||
lv_obj_align(canvas, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_canvas_fill_bg(canvas, lv_color_black(), LV_OPA_COVER);
|
||||
lv_obj_set_content_height(canvas, qrcode.size * pixel_size);
|
||||
lv_obj_set_content_width(canvas, qrcode.size * pixel_size);
|
||||
|
||||
LOG_I(TAG, "Create draw buffer");
|
||||
auto* draw_buf = lv_draw_buf_create(pixel_size * qrcode.size, pixel_size * qrcode.size, LV_COLOR_FORMAT_RGB565, LV_STRIDE_AUTO);
|
||||
if (draw_buf == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate draw buffer");
|
||||
ctx->hasFatalError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
lv_canvas_set_draw_buf(canvas, draw_buf);
|
||||
|
||||
for (uint8_t y = 0; y < qrcode.size; y++) {
|
||||
for (uint8_t x = 0; x < qrcode.size; x++) {
|
||||
bool colored = qrcode_getModule(&qrcode, x, y);
|
||||
auto color = colored ? lv_color_white() : lv_color_black();
|
||||
int32_t pos_x = x * pixel_size;
|
||||
int32_t pos_y = y * pixel_size;
|
||||
for (int px = 0; px < pixel_size; px++) {
|
||||
for (int py = 0; py < pixel_size; py++) {
|
||||
lv_canvas_set_px(canvas, pos_x + px, pos_y + py, color, LV_OPA_COVER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
uint32_t appInstanceId = app_scheduler_current_app_id();
|
||||
Context ctx {};
|
||||
ctx.appInstanceId = appInstanceId;
|
||||
|
||||
writeCrashLogFile(getRtcCrashData());
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
AppEventSubscription sub {};
|
||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||
|
||||
if (!ctx.hasFatalError) {
|
||||
bool shouldClose = false;
|
||||
while (!shouldClose) {
|
||||
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||
|
||||
AppEvent event {};
|
||||
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||
switch (event.type) {
|
||||
case APP_EVENT_CLOSE:
|
||||
shouldClose = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (shouldClose) break;
|
||||
}
|
||||
} else {
|
||||
qr_buffer.reset(new (std::nothrow) uint8_t[qrcode_getBufferSize(qr_version)]);
|
||||
if (qr_buffer == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate QR buffer");
|
||||
} else if (qrcode_initText(&qr_code, qr_buffer.get(), qr_version, ECC_LOW, url.c_str()) != 0) {
|
||||
LOG_E(TAG, "QR init text failed");
|
||||
} else {
|
||||
has_qr_code = true;
|
||||
}
|
||||
}
|
||||
|
||||
window_manager_remove(window);
|
||||
check(app_event_unsubscribe(&sub) == ERROR_NONE);
|
||||
task_event_group_destruct(&event_group);
|
||||
|
||||
bool continuePressed = ctx.continuePressed;
|
||||
|
||||
if (continuePressed) {
|
||||
launcher::start();
|
||||
if (has_qr_code) {
|
||||
screen.showQrCode(qr_code.size, [&qr_code](int x, int y) {
|
||||
return qrcode_getModule(&qr_code, static_cast<uint8_t>(x), static_cast<uint8_t>(y));
|
||||
}, lines);
|
||||
} else {
|
||||
screen.show("", lines);
|
||||
}
|
||||
|
||||
return 0;
|
||||
waitForInputOrForever(has_input);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void start() {
|
||||
uint32_t instanceId = 0;
|
||||
AppStartContext context = app_start_context_for_manifest(&manifest);
|
||||
app_start_with_context(&context, &instanceId);
|
||||
}
|
||||
|
||||
extern const ::AppManifest manifest = {
|
||||
.id = "tactility.crashdiagnostics",
|
||||
.name = "Crash Diagnostics",
|
||||
.category = APP_CATEGORY_SYSTEM,
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
|
||||
.flags = APP_MANIFEST_FLAG_HIDDEN,
|
||||
.stack = {}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif
|
||||
|
||||
@ -19,6 +19,8 @@
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/theme.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <cstring>
|
||||
@ -132,37 +134,47 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
// Enable on boot
|
||||
|
||||
lv_obj_t* enable_wrapper = lv_obj_create(content_wrapper);
|
||||
auto* enable_card = lvgl_card_create(content_wrapper);
|
||||
lv_obj_set_size(enable_card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
|
||||
lv_obj_t* enable_wrapper = lv_obj_create(enable_card);
|
||||
lv_obj_set_size(enable_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(enable_wrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(enable_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(enable_wrapper, false);
|
||||
lvgl::obj_set_style_bg_invisible(enable_wrapper);
|
||||
lv_obj_set_style_border_width(enable_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_all(enable_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
lv_obj_t* enable_label = lv_label_create(enable_wrapper);
|
||||
lv_label_set_text(enable_label, "Enable on boot");
|
||||
lv_obj_align(enable_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(enable_label, 1);
|
||||
|
||||
ctx->enableOnBootSwitch = lv_switch_create(enable_wrapper);
|
||||
lv_obj_add_event_cb(ctx->enableOnBootSwitch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
lv_obj_align(ctx->enableOnBootSwitch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
if (service::development::shouldEnableOnBoot()) {
|
||||
lv_obj_add_state(ctx->enableOnBootSwitch, LV_STATE_CHECKED);
|
||||
} else {
|
||||
lv_obj_remove_state(ctx->enableOnBootSwitch, LV_STATE_CHECKED);
|
||||
}
|
||||
|
||||
// Status
|
||||
// Status and warning
|
||||
|
||||
ctx->statusLabel = lv_label_create(content_wrapper);
|
||||
auto* status_card = lvgl_card_create(content_wrapper);
|
||||
lv_obj_set_size(status_card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(status_card, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// Warning
|
||||
ctx->statusLabel = lv_label_create(status_card);
|
||||
|
||||
auto warning_label = lv_label_create(content_wrapper);
|
||||
auto warning_label = lv_label_create(status_card);
|
||||
lv_label_set_text(warning_label, "This feature is experimental and uses an unsecured http connection.");
|
||||
lv_obj_set_width(warning_label, LV_PCT(100));
|
||||
lv_label_set_long_mode(warning_label, LV_LABEL_LONG_WRAP);
|
||||
if (lv_display_get_color_format(lv_obj_get_display(parent)) != LV_COLOR_FORMAT_L8) {
|
||||
lv_obj_set_style_text_color(warning_label, lv_color_make(0xff, 0xff, 0x00), LV_STATE_DEFAULT);
|
||||
// The monochrome theme has no error colour
|
||||
if (!lvgl_theme_is_mono()) {
|
||||
LvglThemeSettings theme_settings;
|
||||
lvgl_theme_get_settings(&theme_settings);
|
||||
lv_obj_set_style_text_color(warning_label, theme_settings.color_error, LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
updateViewState(ctx);
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/app/selectiondialog/SelectionDialog.h>
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <Tactility/service/displayidle/DisplayIdleService.h>
|
||||
#endif
|
||||
@ -15,15 +16,25 @@
|
||||
#include <Tactility/settings/DisplaySettings.h>
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/chip.h>
|
||||
#include <lvgl/widgets/sliderbox.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
@ -36,16 +47,48 @@ constexpr auto* TAG = "Display";
|
||||
|
||||
namespace {
|
||||
|
||||
struct TimeoutOption {
|
||||
uint32_t ms;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
constexpr TimeoutOption TIMEOUT_OPTIONS[] = {
|
||||
{ 15000, "15 seconds" },
|
||||
{ 30000, "30 seconds" },
|
||||
{ 60000, "1 minute" },
|
||||
{ 120000, "2 minutes" },
|
||||
{ 300000, "5 minutes" },
|
||||
{ 0, "Never" },
|
||||
};
|
||||
|
||||
// The icon is an upright phone with its bar at the bottom, rotated clockwise (0.1 degree units) to the screen's shape.
|
||||
// In settings::display::Orientation order: Landscape, Portrait, LandscapeFlipped, PortraitFlipped
|
||||
constexpr int32_t ORIENTATION_ICON_ROTATIONS[] = { 2700, 0, 900, 1800 };
|
||||
|
||||
// In settings::display::ScreensaverType order
|
||||
constexpr const char* SCREENSAVER_NAMES[] = { "None", "Bouncing Balls", "Mystify", "Matrix Rain", "StackChan" };
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
settings::display::DisplaySettings displaySettings;
|
||||
bool displaySettingsUpdated = false;
|
||||
TaskEventGroup* eventGroup = nullptr;
|
||||
uint32_t selectTimeoutBit = 0;
|
||||
uint32_t selectScreensaverBit = 0;
|
||||
uint32_t timeoutDialogId = 0;
|
||||
uint32_t screensaverDialogId = 0;
|
||||
// Widgets, valid while the window is shown
|
||||
lv_obj_t* timeoutSwitch = nullptr;
|
||||
lv_obj_t* timeoutDropdown = nullptr;
|
||||
lv_obj_t* screensaverDropdown = nullptr;
|
||||
lv_obj_t* orientationDropdown = nullptr;
|
||||
// Shown when the auto screen off is enabled: the timeout and screensaver
|
||||
lv_obj_t* timeoutCard = nullptr;
|
||||
lv_obj_t* timeoutValueLabel = nullptr;
|
||||
lv_obj_t* screensaverValueLabel = nullptr;
|
||||
// Shown when auto-rotate is off or unavailable: the orientation label and chips
|
||||
lv_obj_t* orientationGroup = nullptr;
|
||||
lv_obj_t* orientationChips = nullptr;
|
||||
lv_obj_t* autoRotateSwitch = nullptr;
|
||||
lv_obj_t* mountRotationDropdown = nullptr;
|
||||
// Shown when auto-rotate is on: the sensor mounting label and chips
|
||||
lv_obj_t* mountRotationGroup = nullptr;
|
||||
};
|
||||
|
||||
|
||||
@ -69,29 +112,67 @@ void onBackPressed(lv_event_t* event) {
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
// The slider shows a percentage of the backlight's brightness range
|
||||
int32_t brightnessToPercent(Device* backlight, int32_t brightness) {
|
||||
const int32_t min = backlight_get_min_brightness(backlight);
|
||||
const int32_t range = backlight_get_max_brightness(backlight) - min;
|
||||
return range > 0 ? ((brightness - min) * 100 + range / 2) / range : 100;
|
||||
}
|
||||
|
||||
int32_t percentToBrightness(Device* backlight, int32_t percent) {
|
||||
const int32_t min = backlight_get_min_brightness(backlight);
|
||||
const int32_t range = backlight_get_max_brightness(backlight) - min;
|
||||
const int32_t brightness = min + (percent * range + 50) / 100;
|
||||
// A brightness of 0 turns the backlight off, so 0% is the dimmest setting that is still on
|
||||
return brightness < 1 ? 1 : brightness;
|
||||
}
|
||||
|
||||
void onBacklightSliderEvent(lv_event_t* event) {
|
||||
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
auto* slider_box = lv_event_get_current_target_obj(event);
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* backlight = getBacklightDevice();
|
||||
assert(backlight != nullptr);
|
||||
|
||||
int32_t slider_value = lv_slider_get_value(slider);
|
||||
ctx->displaySettings.backlightDuty = static_cast<uint8_t>(slider_value);
|
||||
const int32_t percent = lvgl_sliderbox_get_value(slider_box);
|
||||
ctx->displaySettings.backlightDuty = static_cast<uint8_t>(percentToBrightness(backlight, percent));
|
||||
ctx->displaySettingsUpdated = true;
|
||||
backlight_set_brightness(backlight, ctx->displaySettings.backlightDuty);
|
||||
device_put(backlight);
|
||||
}
|
||||
|
||||
void onOrientationSet(lv_event_t* event) {
|
||||
void forEachChild(lv_obj_t* parent, auto&& action) {
|
||||
const uint32_t count = lv_obj_get_child_count(parent);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
action(lv_obj_get_child(parent, static_cast<int32_t>(i)), i);
|
||||
}
|
||||
}
|
||||
|
||||
void setCheckedChip(lv_obj_t* chips, settings::display::Orientation orientation) {
|
||||
forEachChild(chips, [orientation](lv_obj_t* chip, uint32_t index) {
|
||||
lv_obj_set_state(chip, LV_STATE_CHECKED, index == static_cast<uint32_t>(orientation));
|
||||
});
|
||||
}
|
||||
|
||||
/** Shows the manual orientation or the sensor mounting, depending on whether auto-rotate is on */
|
||||
void updateOrientationVisibility(Context* ctx) {
|
||||
const bool auto_rotate = ctx->autoRotateSwitch != nullptr && ctx->displaySettings.autoRotateEnabled;
|
||||
lv_obj_set_hidden(ctx->orientationGroup, auto_rotate);
|
||||
if (ctx->mountRotationGroup != nullptr) {
|
||||
lv_obj_set_hidden(ctx->mountRotationGroup, !auto_rotate);
|
||||
}
|
||||
}
|
||||
|
||||
void onOrientationChipPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
uint32_t selected_index = lv_dropdown_get_selected(dropdown);
|
||||
LOG_I(TAG, "Selected %u", (unsigned)selected_index);
|
||||
auto* chip = lv_event_get_target_obj(event);
|
||||
const auto selected_index = lv_obj_get_index(chip);
|
||||
LOG_I(TAG, "Selected %d", (int)selected_index);
|
||||
auto selected_orientation = static_cast<settings::display::Orientation>(selected_index);
|
||||
if (selected_orientation != ctx->displaySettings.orientation) {
|
||||
ctx->displaySettings.orientation = selected_orientation;
|
||||
ctx->displaySettingsUpdated = true;
|
||||
lv_display_set_rotation(lv_display_get_default(), settings::display::toLvglDisplayRotation(selected_orientation));
|
||||
setCheckedChip(ctx->orientationChips, selected_orientation);
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,28 +182,63 @@ void onAutoRotateSwitch(lv_event_t* event) {
|
||||
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
|
||||
ctx->displaySettings.autoRotateEnabled = enabled;
|
||||
ctx->displaySettingsUpdated = true;
|
||||
if (ctx->mountRotationDropdown) {
|
||||
if (enabled) {
|
||||
lv_obj_clear_state(ctx->mountRotationDropdown, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_obj_add_state(ctx->mountRotationDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
if (ctx->orientationDropdown) {
|
||||
if (enabled) {
|
||||
lv_obj_add_state(ctx->orientationDropdown, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_obj_clear_state(ctx->orientationDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
updateOrientationVisibility(ctx);
|
||||
}
|
||||
|
||||
void onMountRotationSet(lv_event_t* event) {
|
||||
void onMountRotationChipPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
uint32_t selected_index = lv_dropdown_get_selected(dropdown);
|
||||
ctx->displaySettings.autoRotateMountRotation = static_cast<settings::display::Orientation>(selected_index);
|
||||
auto* chip = lv_event_get_target_obj(event);
|
||||
const auto orientation = static_cast<settings::display::Orientation>(lv_obj_get_index(chip));
|
||||
ctx->displaySettings.autoRotateMountRotation = orientation;
|
||||
ctx->displaySettingsUpdated = true;
|
||||
setCheckedChip(lv_obj_get_parent(chip), orientation);
|
||||
}
|
||||
|
||||
/** Creates a transparent column in a card with a title, which centers the title and the content below it */
|
||||
lv_obj_t* createCardGroup(lv_obj_t* card, const char* title) {
|
||||
auto* group = lv_obj_create(card);
|
||||
lv_obj_set_size(group, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(group, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(group, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_bg_opa(group, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(group, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_all(group, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(group, false);
|
||||
|
||||
auto* label = lv_label_create(group);
|
||||
lv_label_set_text(label, title);
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a row of chips with a phone icon per orientation, in settings::display::Orientation order.
|
||||
* The row is centered in its parent while the chips fit, and scrolls when they don't.
|
||||
*/
|
||||
lv_obj_t* createOrientationChips(lv_obj_t* parent, settings::display::Orientation selected, lv_event_cb_t onPressed, Context* ctx) {
|
||||
auto* chips = lv_obj_create(parent);
|
||||
lv_obj_set_size(chips, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_max_width(chips, LV_PCT(100), LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(chips, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_scroll_dir(chips, LV_DIR_HOR);
|
||||
lv_obj_set_style_bg_opa(chips, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(chips, 0, LV_STATE_DEFAULT);
|
||||
// The chips' margins leave room for their focus rings and space them apart
|
||||
lv_obj_set_style_pad_all(chips, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_column(chips, 0, LV_STATE_DEFAULT);
|
||||
|
||||
for (int32_t i = 0; i < 4; i++) {
|
||||
auto* chip = lvgl_chip_create(chips);
|
||||
lv_obj_add_event_cb(chip, onPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
auto* icon = lv_label_create(chip);
|
||||
lv_obj_set_style_text_font(icon, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(icon, LVGL_ICON_SHARED_PHONE_ANDROID);
|
||||
lv_obj_set_style_transform_pivot_x(icon, LV_PCT(50), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_transform_pivot_y(icon, LV_PCT(50), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_transform_rotation(icon, ORIENTATION_ICON_ROTATIONS[i], LV_STATE_DEFAULT);
|
||||
}
|
||||
lvgl_grid_navigation_add(chips);
|
||||
setCheckedChip(chips, selected);
|
||||
return chips;
|
||||
}
|
||||
|
||||
void onTimeoutSwitch(lv_event_t* event) {
|
||||
@ -131,52 +247,106 @@ void onTimeoutSwitch(lv_event_t* event) {
|
||||
bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED);
|
||||
ctx->displaySettings.backlightTimeoutEnabled = enabled;
|
||||
ctx->displaySettingsUpdated = true;
|
||||
if (ctx->timeoutDropdown) {
|
||||
if (enabled) {
|
||||
lv_obj_clear_state(ctx->timeoutDropdown, LV_STATE_DISABLED);
|
||||
if (ctx->screensaverDropdown) {
|
||||
lv_obj_clear_state(ctx->screensaverDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
} else {
|
||||
lv_obj_add_state(ctx->timeoutDropdown, LV_STATE_DISABLED);
|
||||
if (ctx->screensaverDropdown) {
|
||||
lv_obj_add_state(ctx->screensaverDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
if (ctx->timeoutCard) {
|
||||
lv_obj_set_hidden(ctx->timeoutCard, !enabled);
|
||||
}
|
||||
}
|
||||
|
||||
const char* getTimeoutName(uint32_t ms) {
|
||||
for (const auto& option : TIMEOUT_OPTIONS) {
|
||||
if (option.ms == ms) {
|
||||
return option.name;
|
||||
}
|
||||
}
|
||||
return "Custom";
|
||||
}
|
||||
|
||||
void onTimeoutChanged(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
uint32_t idx = lv_dropdown_get_selected(dropdown);
|
||||
// Map dropdown index to ms: 0=15s,1=30s,2=1m,3=2m,4=5m,5=Never
|
||||
static const uint32_t values_ms[] = {15000, 30000, 60000, 120000, 300000, 0};
|
||||
if (idx < (sizeof(values_ms)/sizeof(values_ms[0]))) {
|
||||
ctx->displaySettings.backlightTimeoutMs = values_ms[idx];
|
||||
ctx->displaySettingsUpdated = true;
|
||||
const char* getScreensaverName(settings::display::ScreensaverType type) {
|
||||
const auto index = static_cast<size_t>(type);
|
||||
return index < std::size(SCREENSAVER_NAMES) ? SCREENSAVER_NAMES[index] : "";
|
||||
}
|
||||
|
||||
/** Requires the LVGL lock */
|
||||
void updateTimeoutLabels(Context* ctx) {
|
||||
if (ctx->timeoutValueLabel != nullptr) {
|
||||
lv_label_set_text(ctx->timeoutValueLabel, getTimeoutName(ctx->displaySettings.backlightTimeoutMs));
|
||||
lv_label_set_text(ctx->screensaverValueLabel, getScreensaverName(ctx->displaySettings.screensaverType));
|
||||
}
|
||||
}
|
||||
|
||||
void onScreensaverChanged(lv_event_t* event) {
|
||||
void onChangeTimeoutPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
uint32_t idx = lv_dropdown_get_selected(dropdown);
|
||||
// Validate index bounds before casting to enum
|
||||
if (idx >= static_cast<uint32_t>(settings::display::ScreensaverType::Count)) {
|
||||
task_event_group_signal(ctx->eventGroup, ctx->selectTimeoutBit);
|
||||
}
|
||||
|
||||
void onChangeScreensaverPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
task_event_group_signal(ctx->eventGroup, ctx->selectScreensaverBit);
|
||||
}
|
||||
|
||||
void startTimeoutSelection(Context* ctx) {
|
||||
if (ctx->timeoutDialogId != 0) {
|
||||
return;
|
||||
}
|
||||
auto selected_type = static_cast<settings::display::ScreensaverType>(idx);
|
||||
if (selected_type != ctx->displaySettings.screensaverType) {
|
||||
ctx->displaySettings.screensaverType = selected_type;
|
||||
std::vector<std::string> items;
|
||||
for (const auto& option : TIMEOUT_OPTIONS) {
|
||||
items.emplace_back(option.name);
|
||||
}
|
||||
ctx->timeoutDialogId = selectiondialog::start(ctx->appInstanceId, "Timeout", items);
|
||||
}
|
||||
|
||||
void startScreensaverSelection(Context* ctx) {
|
||||
if (ctx->screensaverDialogId != 0) {
|
||||
return;
|
||||
}
|
||||
const std::vector<std::string> items(std::begin(SCREENSAVER_NAMES), std::end(SCREENSAVER_NAMES));
|
||||
ctx->screensaverDialogId = selectiondialog::start(ctx->appInstanceId, "Screensaver", items);
|
||||
}
|
||||
|
||||
// Other results than an index mean that the dialog was dismissed
|
||||
void onTimeoutSelectionResult(Context* ctx, int32_t selected) {
|
||||
ctx->timeoutDialogId = 0;
|
||||
if (selected >= 0 && selected < static_cast<int32_t>(std::size(TIMEOUT_OPTIONS))) {
|
||||
ctx->displaySettings.backlightTimeoutMs = TIMEOUT_OPTIONS[selected].ms;
|
||||
ctx->displaySettingsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
void onScreensaverSelectionResult(Context* ctx, int32_t selected) {
|
||||
ctx->screensaverDialogId = 0;
|
||||
if (selected >= 0 && selected < static_cast<int32_t>(settings::display::ScreensaverType::Count)) {
|
||||
ctx->displaySettings.screensaverType = static_cast<settings::display::ScreensaverType>(selected);
|
||||
ctx->displaySettingsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** A row in a card: "Title Value [Change]" */
|
||||
lv_obj_t* createValueRow(lv_obj_t* card, const char* title, lv_event_cb_t onChange, Context* ctx) {
|
||||
auto* row = lv_obj_create(card);
|
||||
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
|
||||
// The card provides the background
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(row, false);
|
||||
|
||||
auto* title_label = lv_label_create(row);
|
||||
lv_label_set_text(title_label, title);
|
||||
lv_obj_set_flex_grow(title_label, 1);
|
||||
|
||||
auto* value_label = lv_label_create(row);
|
||||
|
||||
auto* button = lv_button_create(row);
|
||||
lv_label_set_text(lv_label_create(button), "Change");
|
||||
lv_obj_add_event_cb(button, onChange, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
return value_label;
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
ctx->displaySettings = settings::display::loadOrGetDefault();
|
||||
bool has_imu = device_exists_of_type(&IMU_TYPE);
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
@ -189,6 +359,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
|
||||
auto* main_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(main_wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(main_wrapper, 1);
|
||||
@ -200,22 +371,16 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
if (backlight != nullptr) {
|
||||
bool is_on_off_brightness = backlight_get_min_brightness(backlight) == 0 && backlight_get_max_brightness(backlight) == 1;
|
||||
if (!is_on_off_brightness) {
|
||||
auto* brightness_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(brightness_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_hor(brightness_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(brightness_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* brightness_label = lv_label_create(brightness_wrapper);
|
||||
auto* brightness_label = lv_label_create(main_wrapper);
|
||||
lv_label_set_text(brightness_label, "Brightness");
|
||||
lv_obj_align(brightness_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
auto* brightness_slider = lv_slider_create(brightness_wrapper);
|
||||
lv_obj_set_width(brightness_slider, LV_PCT(50));
|
||||
lv_obj_align(brightness_slider, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_slider_set_range(brightness_slider, backlight_get_min_brightness(backlight), backlight_get_max_brightness(backlight));
|
||||
lv_obj_add_event_cb(brightness_slider, onBacklightSliderEvent, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
auto* brightness_card = lvgl_card_create(main_wrapper);
|
||||
lv_obj_set_size(brightness_card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
|
||||
lv_slider_set_value(brightness_slider, ctx->displaySettings.backlightDuty, LV_ANIM_OFF);
|
||||
const int32_t percent = brightnessToPercent(backlight, ctx->displaySettings.backlightDuty);
|
||||
auto* brightness_slider = lvgl_sliderbox_create(brightness_card, 0, 100, 10, percent);
|
||||
lv_obj_set_width(brightness_slider, LV_PCT(100));
|
||||
lvgl_sliderbox_add_value_changed_cb(brightness_slider, onBacklightSliderEvent, ctx);
|
||||
}
|
||||
// Only compared against nullptr below, never dereferenced again, so releasing it here is safe.
|
||||
device_put(backlight);
|
||||
@ -223,65 +388,41 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
// Orientation
|
||||
|
||||
auto* orientation_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(orientation_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(orientation_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(orientation_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* orientation_label = lv_label_create(orientation_wrapper);
|
||||
lv_label_set_text(orientation_label, "Orientation");
|
||||
lv_obj_align(orientation_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
ctx->orientationDropdown = lv_dropdown_create(orientation_wrapper);
|
||||
// Note: order correlates with settings::display::Orientation item order
|
||||
lv_dropdown_set_options(ctx->orientationDropdown, "Landscape\nPortrait Right\nLandscape Flipped\nPortrait Left");
|
||||
lv_obj_align(ctx->orientationDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->orientationDropdown, onOrientationSet, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
// Set the dropdown to match current orientation enum
|
||||
lv_dropdown_set_selected(ctx->orientationDropdown, static_cast<uint16_t>(ctx->displaySettings.orientation));
|
||||
if (has_imu && ctx->displaySettings.autoRotateEnabled) {
|
||||
lv_obj_add_state(ctx->orientationDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
|
||||
// Auto-rotate (IMU-driven)
|
||||
|
||||
// Auto-rotate (IMU-driven), which decides what the card below shows
|
||||
if (has_imu) {
|
||||
auto* auto_rotate_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(auto_rotate_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(auto_rotate_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(auto_rotate_wrapper, 0, LV_STATE_DEFAULT);
|
||||
auto* auto_rotate_row = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(auto_rotate_row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(auto_rotate_row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(auto_rotate_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_border_width(auto_rotate_row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_all(auto_rotate_row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(auto_rotate_row, false);
|
||||
|
||||
auto* auto_rotate_label = lv_label_create(auto_rotate_wrapper);
|
||||
auto* auto_rotate_label = lv_label_create(auto_rotate_row);
|
||||
lv_label_set_text(auto_rotate_label, "Auto-rotate");
|
||||
lv_obj_align(auto_rotate_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(auto_rotate_label, 1);
|
||||
|
||||
ctx->autoRotateSwitch = lv_switch_create(auto_rotate_wrapper);
|
||||
if (ctx->displaySettings.autoRotateEnabled) {
|
||||
lv_obj_add_state(ctx->autoRotateSwitch, LV_STATE_CHECKED);
|
||||
}
|
||||
lv_obj_align(ctx->autoRotateSwitch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
ctx->autoRotateSwitch = lv_switch_create(auto_rotate_row);
|
||||
lv_obj_set_state(ctx->autoRotateSwitch, LV_STATE_CHECKED, ctx->displaySettings.autoRotateEnabled);
|
||||
lv_obj_add_event_cb(ctx->autoRotateSwitch, onAutoRotateSwitch, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
auto* mount_rotation_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(mount_rotation_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(mount_rotation_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(mount_rotation_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* mount_rotation_label = lv_label_create(mount_rotation_wrapper);
|
||||
lv_label_set_text(mount_rotation_label, "Sensor mounting");
|
||||
lv_obj_align(mount_rotation_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
ctx->mountRotationDropdown = lv_dropdown_create(mount_rotation_wrapper);
|
||||
// Note: order correlates with settings::display::Orientation item order
|
||||
lv_dropdown_set_options(ctx->mountRotationDropdown, "Landscape\nPortrait Right\nLandscape Flipped\nPortrait Left");
|
||||
lv_obj_align(ctx->mountRotationDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->mountRotationDropdown, onMountRotationSet, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
lv_dropdown_set_selected(ctx->mountRotationDropdown, static_cast<uint16_t>(ctx->displaySettings.autoRotateMountRotation));
|
||||
if (!ctx->displaySettings.autoRotateEnabled) {
|
||||
lv_obj_add_state(ctx->mountRotationDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
// The card shows the manual orientation, or the sensor mounting when auto-rotate is on
|
||||
auto* orientation_card = lvgl_card_create(main_wrapper);
|
||||
lv_obj_set_size(orientation_card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(orientation_card, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
ctx->orientationGroup = createCardGroup(orientation_card, "Orientation");
|
||||
ctx->orientationChips = createOrientationChips(ctx->orientationGroup, ctx->displaySettings.orientation, onOrientationChipPressed, ctx);
|
||||
|
||||
// Auto-rotate needs to know how the sensor is mounted relative to the display
|
||||
if (has_imu) {
|
||||
ctx->mountRotationGroup = createCardGroup(orientation_card, "Sensor mounting");
|
||||
createOrientationChips(ctx->mountRotationGroup, ctx->displaySettings.autoRotateMountRotation, onMountRotationChipPressed, ctx);
|
||||
}
|
||||
|
||||
updateOrientationVisibility(ctx);
|
||||
|
||||
// Screen timeout
|
||||
// Note: DisplayIdleService doesn't act on these settings for kernel-driver displays yet
|
||||
// (it only looks up the deprecated tt::hal::display::DisplayDevice), so these currently
|
||||
@ -290,74 +431,46 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
if (backlight != nullptr) {
|
||||
auto* timeout_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(timeout_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(timeout_wrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(timeout_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_all(timeout_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(timeout_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(timeout_wrapper, false);
|
||||
|
||||
auto* timeout_label = lv_label_create(timeout_wrapper);
|
||||
lv_label_set_text(timeout_label, "Auto screen off");
|
||||
lv_obj_align(timeout_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(timeout_label, 1);
|
||||
|
||||
ctx->timeoutSwitch = lv_switch_create(timeout_wrapper);
|
||||
if (ctx->displaySettings.backlightTimeoutEnabled) {
|
||||
lv_obj_add_state(ctx->timeoutSwitch, LV_STATE_CHECKED);
|
||||
}
|
||||
lv_obj_align(ctx->timeoutSwitch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->timeoutSwitch, onTimeoutSwitch, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
auto* timeout_select_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(timeout_select_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(timeout_select_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(timeout_select_wrapper, 0, LV_STATE_DEFAULT);
|
||||
auto* timeout_card = lvgl_card_create(main_wrapper);
|
||||
lv_obj_set_size(timeout_card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(timeout_card, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_hidden(timeout_card, !ctx->displaySettings.backlightTimeoutEnabled);
|
||||
ctx->timeoutCard = timeout_card;
|
||||
|
||||
auto* timeout_value_label = lv_label_create(timeout_select_wrapper);
|
||||
lv_label_set_text(timeout_value_label, "Timeout");
|
||||
lv_obj_align(timeout_value_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
ctx->timeoutDropdown = lv_dropdown_create(timeout_select_wrapper);
|
||||
lv_dropdown_set_options(ctx->timeoutDropdown, "15 seconds\n30 seconds\n1 minute\n2 minutes\n5 minutes\nNever");
|
||||
lv_obj_align(ctx->timeoutDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->timeoutDropdown, onTimeoutChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
// Initialize dropdown selection from settings
|
||||
uint32_t ms = ctx->displaySettings.backlightTimeoutMs;
|
||||
uint32_t idx = 2; // default 1 minute
|
||||
if (ms == 15000) idx = 0;
|
||||
else if (ms == 30000)
|
||||
idx = 1;
|
||||
else if (ms == 60000)
|
||||
idx = 2;
|
||||
else if (ms == 120000)
|
||||
idx = 3;
|
||||
else if (ms == 300000)
|
||||
idx = 4;
|
||||
else if (ms == 0)
|
||||
idx = 5;
|
||||
lv_dropdown_set_selected(ctx->timeoutDropdown, idx);
|
||||
if (!ctx->displaySettings.backlightTimeoutEnabled) {
|
||||
lv_obj_add_state(ctx->timeoutDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
|
||||
// Screensaver type
|
||||
auto* screensaver_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(screensaver_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(screensaver_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(screensaver_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* screensaver_label = lv_label_create(screensaver_wrapper);
|
||||
lv_label_set_text(screensaver_label, "Screensaver");
|
||||
lv_obj_align(screensaver_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
ctx->screensaverDropdown = lv_dropdown_create(screensaver_wrapper);
|
||||
// Note: order correlates with settings::display::ScreensaverType enum order
|
||||
lv_dropdown_set_options(ctx->screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain\nStackChan");
|
||||
lv_obj_align(ctx->screensaverDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->screensaverDropdown, onScreensaverChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
lv_dropdown_set_selected(ctx->screensaverDropdown, static_cast<uint16_t>(ctx->displaySettings.screensaverType));
|
||||
if (!ctx->displaySettings.backlightTimeoutEnabled) {
|
||||
lv_obj_add_state(ctx->screensaverDropdown, LV_STATE_DISABLED);
|
||||
}
|
||||
ctx->timeoutValueLabel = createValueRow(timeout_card, "Timeout", onChangeTimeoutPressed, ctx);
|
||||
ctx->screensaverValueLabel = createValueRow(timeout_card, "Screensaver", onChangeScreensaverPressed, ctx);
|
||||
updateTimeoutLabels(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
void destroyWidgets(void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
ctx->timeoutSwitch = nullptr;
|
||||
ctx->timeoutCard = nullptr;
|
||||
ctx->timeoutValueLabel = nullptr;
|
||||
ctx->screensaverValueLabel = nullptr;
|
||||
ctx->orientationGroup = nullptr;
|
||||
ctx->orientationChips = nullptr;
|
||||
ctx->autoRotateSwitch = nullptr;
|
||||
ctx->mountRotationGroup = nullptr;
|
||||
}
|
||||
|
||||
// Mirrors the old onHide() behaviour: persist the settings (regardless of whether the app is
|
||||
// giving up its thread for a save/resume cycle, or closing for good) whenever they changed.
|
||||
void persistIfUpdated(Context& ctx) {
|
||||
@ -386,18 +499,31 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
uint32_t appInstanceId = app_scheduler_current_app_id();
|
||||
Context ctx {};
|
||||
ctx.appInstanceId = appInstanceId;
|
||||
// Loaded once: the window is rebuilt from it, e.g. after a dialog closed
|
||||
ctx.displaySettings = settings::display::loadOrGetDefault();
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
ctx.eventGroup = &event_group;
|
||||
check(task_event_group_claim_bit(&event_group, &ctx.selectTimeoutBit) == ERROR_NONE);
|
||||
check(task_event_group_claim_bit(&event_group, &ctx.selectScreensaverBit) == ERROR_NONE);
|
||||
|
||||
AppEventSubscription sub {};
|
||||
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);
|
||||
|
||||
bool shouldClose = false;
|
||||
while (!shouldClose) {
|
||||
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||
uint32_t flags = 0;
|
||||
task_event_group_wait_any(&event_group, &flags, portMAX_DELAY);
|
||||
|
||||
if (flags & ctx.selectTimeoutBit) {
|
||||
startTimeoutSelection(&ctx);
|
||||
}
|
||||
if (flags & ctx.selectScreensaverBit) {
|
||||
startScreensaverSelection(&ctx);
|
||||
}
|
||||
|
||||
AppEvent event {};
|
||||
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||
@ -406,6 +532,17 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
persistIfUpdated(ctx);
|
||||
shouldClose = true;
|
||||
break;
|
||||
case APP_EVENT_RESULT:
|
||||
if (event.result.launch_id == ctx.timeoutDialogId) {
|
||||
onTimeoutSelectionResult(&ctx, event.result.result);
|
||||
} else if (event.result.launch_id == ctx.screensaverDialogId) {
|
||||
onScreensaverSelectionResult(&ctx, event.result.result);
|
||||
}
|
||||
lvgl_lock();
|
||||
updateTimeoutLabels(&ctx);
|
||||
lvgl_unlock();
|
||||
app_manager_stop(event.result.launch_id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include <app/stream.h>
|
||||
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <Tactility/app/files/SupportedFiles.h>
|
||||
@ -356,7 +357,7 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t* button = lv_list_add_button(list, symbol, label_text.c_str());
|
||||
lv_obj_t* button = lvgl_list_add_button(list, symbol, label_text.c_str());
|
||||
lv_obj_add_event_cb(button, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
lv_obj_add_event_cb(button, &onDirEntryLongPressedCallback, LV_EVENT_LONG_PRESSED, this);
|
||||
lv_obj_add_event_cb(button, &onDirEntryKeyCallback, LV_EVENT_KEY, this);
|
||||
@ -431,20 +432,20 @@ void View::onNewFolderPressed() {
|
||||
}
|
||||
|
||||
void View::addCommonFileActions() {
|
||||
auto* copy_button = lv_list_add_button(action_list, LV_SYMBOL_COPY, "Copy");
|
||||
auto* copy_button = lvgl_list_add_button(action_list, LV_SYMBOL_COPY, "Copy");
|
||||
lv_obj_add_event_cb(copy_button, onCopyPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
auto* cut_button = lv_list_add_button(action_list, LV_SYMBOL_CUT, "Cut");
|
||||
auto* cut_button = lvgl_list_add_button(action_list, LV_SYMBOL_CUT, "Cut");
|
||||
lv_obj_add_event_cb(cut_button, onCutPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
auto* rename_button = lv_list_add_button(action_list, LV_SYMBOL_EDIT, "Rename");
|
||||
auto* rename_button = lvgl_list_add_button(action_list, LV_SYMBOL_EDIT, "Rename");
|
||||
lv_obj_add_event_cb(rename_button, onRenamePressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
auto* delete_button = lv_list_add_button(action_list, LV_SYMBOL_TRASH, "Delete");
|
||||
auto* delete_button = lvgl_list_add_button(action_list, LV_SYMBOL_TRASH, "Delete");
|
||||
lv_obj_add_event_cb(delete_button, onDeletePressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
}
|
||||
|
||||
void View::showActions() {
|
||||
lv_obj_clean(action_list);
|
||||
addCommonFileActions();
|
||||
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(action_list, false);
|
||||
}
|
||||
|
||||
void View::showActionsForDirectory() { showActions(); }
|
||||
@ -453,21 +454,21 @@ void View::showActionsForFile() {
|
||||
lv_obj_clean(action_list);
|
||||
|
||||
if (isExecutablePath(state->getSelectedChildPath())) {
|
||||
auto* run_button = lv_list_add_button(action_list, LV_SYMBOL_PLAY, "Run");
|
||||
auto* run_button = lvgl_list_add_button(action_list, LV_SYMBOL_PLAY, "Run");
|
||||
lv_obj_add_event_cb(run_button, onRunPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
}
|
||||
|
||||
addCommonFileActions();
|
||||
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(action_list, false);
|
||||
}
|
||||
|
||||
void View::showActionsForMountPoint() {
|
||||
lv_obj_clean(action_list);
|
||||
|
||||
auto* eject_button = lv_list_add_button(action_list, LV_SYMBOL_EJECT, "Eject");
|
||||
auto* eject_button = lvgl_list_add_button(action_list, LV_SYMBOL_EJECT, "Eject");
|
||||
lv_obj_add_event_cb(eject_button, onEjectPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
|
||||
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(action_list, false);
|
||||
}
|
||||
|
||||
void View::onRunPressed() {
|
||||
@ -517,7 +518,7 @@ void View::update(size_t start_index) {
|
||||
size_t count = 0;
|
||||
|
||||
if (!is_root && current_start_index > 0) {
|
||||
auto* back_btn = lv_list_add_btn(dir_entry_list, LV_SYMBOL_LEFT, "Back");
|
||||
auto* back_btn = lvgl_list_add_button(dir_entry_list, LV_SYMBOL_LEFT, "Back");
|
||||
lv_obj_add_event_cb(back_btn, [](lv_event_t* event) {
|
||||
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||
size_t new_index = (view->current_start_index >= view->MAX_BATCH) ?
|
||||
@ -541,7 +542,7 @@ void View::update(size_t start_index) {
|
||||
if (!is_root && last_loaded_index < total_entries) {
|
||||
if (total_entries > current_start_index &&
|
||||
(total_entries - current_start_index) > MAX_BATCH) {
|
||||
auto* next_btn = lv_list_add_btn(dir_entry_list, LV_SYMBOL_RIGHT, "Next");
|
||||
auto* next_btn = lvgl_list_add_button(dir_entry_list, LV_SYMBOL_RIGHT, "Next");
|
||||
lv_obj_add_event_cb(next_btn, [](lv_event_t* event) {
|
||||
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||
view->update(view->last_loaded_index); }, LV_EVENT_SHORT_CLICKED, this);
|
||||
@ -552,15 +553,15 @@ void View::update(size_t start_index) {
|
||||
});
|
||||
|
||||
if (is_root) {
|
||||
lv_obj_add_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(navigate_up_button, true);
|
||||
} else {
|
||||
lv_obj_remove_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(navigate_up_button, false);
|
||||
}
|
||||
|
||||
if (state->hasClipboard() && !is_root) {
|
||||
lv_obj_remove_flag(paste_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(paste_button, false);
|
||||
} else {
|
||||
lv_obj_add_flag(paste_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(paste_button, true);
|
||||
}
|
||||
|
||||
lvgl_unlock();
|
||||
@ -579,7 +580,7 @@ void View::init(uint32_t appInstanceId, lv_obj_t* parent) {
|
||||
new_file_button = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_FILE, &onNewFilePressedCallback, this);
|
||||
new_folder_button = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_DIRECTORY, &onNewFolderPressedCallback, this);
|
||||
paste_button = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_PASTE, &onPastePressedCallback, this);
|
||||
lv_obj_add_flag(paste_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(paste_button, true);
|
||||
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
@ -588,30 +589,30 @@ void View::init(uint32_t appInstanceId, lv_obj_t* parent) {
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
|
||||
|
||||
dir_entry_list = lv_list_create(wrapper);
|
||||
dir_entry_list = lvgl_list_create(wrapper);
|
||||
lv_obj_set_height(dir_entry_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(dir_entry_list, 1);
|
||||
|
||||
lv_obj_add_event_cb(dir_entry_list, dirEntryListScrollBeginCallback, LV_EVENT_SCROLL_BEGIN, this);
|
||||
|
||||
action_list = lv_list_create(wrapper);
|
||||
action_list = lvgl_list_create(wrapper);
|
||||
lv_obj_set_height(action_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(action_list, 1);
|
||||
lv_obj_add_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(action_list, true);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void View::onDirEntryListScrollBegin() {
|
||||
if (lvgl_try_lock(500 / portTICK_PERIOD_MS)) {
|
||||
lv_obj_add_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(action_list, true);
|
||||
lvgl_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void View::onNavigate() {
|
||||
if (lvgl_try_lock(500 / portTICK_PERIOD_MS)) {
|
||||
lv_obj_add_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(action_list, true);
|
||||
lvgl_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <cstring>
|
||||
@ -103,9 +104,9 @@ void View::onPathTextChanged(lv_event_t* event) {
|
||||
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||
const char* path = lv_textarea_get_text(view->path_textarea);
|
||||
if (isSelectableFilePath(path)) {
|
||||
lv_obj_remove_flag(view->select_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(view->select_button, false);
|
||||
} else {
|
||||
lv_obj_add_flag(view->select_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(view->select_button, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,7 +118,7 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
|
||||
} else {
|
||||
symbol = LV_SYMBOL_FILE;
|
||||
}
|
||||
lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry.d_name);
|
||||
lv_obj_t* button = lvgl_list_add_button(list, symbol, dir_entry.d_name);
|
||||
lv_obj_add_event_cb(button, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
}
|
||||
|
||||
@ -153,9 +154,9 @@ void View::update() {
|
||||
});
|
||||
|
||||
if (state->getCurrentPath() == "/") {
|
||||
lv_obj_add_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(navigate_up_button, true);
|
||||
} else {
|
||||
lv_obj_remove_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(navigate_up_button, false);
|
||||
}
|
||||
|
||||
lvgl_unlock();
|
||||
@ -177,7 +178,7 @@ void View::init(lv_obj_t* parent, Mode mode) {
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
|
||||
|
||||
dir_entry_list = lv_list_create(wrapper);
|
||||
dir_entry_list = lvgl_list_create(wrapper);
|
||||
lv_obj_set_height(dir_entry_list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(dir_entry_list, 1);
|
||||
|
||||
@ -196,7 +197,7 @@ void View::init(lv_obj_t* parent, Mode mode) {
|
||||
auto* select_button_label = lv_label_create(select_button);
|
||||
lv_label_set_text(select_button_label, "Select");
|
||||
lv_obj_add_event_cb(select_button, onSelectButtonPressed, LV_EVENT_SHORT_CLICKED, this);
|
||||
lv_obj_add_flag(select_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(select_button, true);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
@ -13,6 +13,9 @@
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/chip.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
namespace tt::app::grovesettings {
|
||||
@ -36,11 +39,35 @@ void collectDevices(Context* ctx) {
|
||||
});
|
||||
}
|
||||
|
||||
void onModeChanged(lv_event_t* e) {
|
||||
struct ModeOption {
|
||||
GroveMode mode;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
constexpr ModeOption MODE_OPTIONS[] = {
|
||||
{ GROVE_MODE_DISABLED, "Disabled" },
|
||||
{ GROVE_MODE_I2C, "I2C" },
|
||||
{ GROVE_MODE_UART, "UART" },
|
||||
};
|
||||
|
||||
GroveMode getChipMode(lv_obj_t* chip) {
|
||||
return static_cast<GroveMode>(reinterpret_cast<intptr_t>(lv_obj_get_user_data(chip)));
|
||||
}
|
||||
|
||||
void updateChips(lv_obj_t* chipsRow, GroveMode mode) {
|
||||
const uint32_t count = lv_obj_get_child_count(chipsRow);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
auto* chip = lv_obj_get_child(chipsRow, static_cast<int32_t>(i));
|
||||
lv_obj_set_state(chip, LV_STATE_CHECKED, getChipMode(chip) == mode);
|
||||
}
|
||||
}
|
||||
|
||||
void onModeChipPressed(lv_event_t* e) {
|
||||
auto* device = static_cast<::Device*>(lv_event_get_user_data(e));
|
||||
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(e));
|
||||
auto mode = static_cast<GroveMode>(lv_dropdown_get_selected(dropdown));
|
||||
grove_set_mode(device, mode);
|
||||
auto* chip = lv_event_get_target_obj(e);
|
||||
if (grove_set_mode(device, getChipMode(chip)) == ERROR_NONE) {
|
||||
updateChips(lv_obj_get_parent(chip), getChipMode(chip));
|
||||
}
|
||||
}
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
@ -60,29 +87,44 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
|
||||
auto* main_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(main_wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(main_wrapper, 1);
|
||||
|
||||
for (auto* device : ctx->devices) {
|
||||
auto* row = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* label = lv_label_create(row);
|
||||
auto* label = lv_label_create(main_wrapper);
|
||||
lv_label_set_text(label, device->name);
|
||||
lv_obj_align(label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
auto* dropdown = lv_dropdown_create(row);
|
||||
lv_dropdown_set_options(dropdown, "Disabled\nUART\nI2C");
|
||||
lv_obj_align(dropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
// The chips are centered in the card while they fit, and scroll when they don't
|
||||
auto* card = lvgl_card_create(main_wrapper);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(card, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_left(label, lv_obj_get_style_pad_left(card, LV_PART_MAIN), LV_STATE_DEFAULT);
|
||||
|
||||
auto* chips_row = lv_obj_create(card);
|
||||
lv_obj_set_size(chips_row, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_max_width(chips_row, LV_PCT(100), LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(chips_row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_scroll_dir(chips_row, LV_DIR_HOR);
|
||||
lv_obj_set_style_bg_opa(chips_row, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(chips_row, 0, LV_STATE_DEFAULT);
|
||||
// The chips' margins leave room for their focus rings and space them apart
|
||||
lv_obj_set_style_pad_all(chips_row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_column(chips_row, 0, LV_STATE_DEFAULT);
|
||||
|
||||
for (const auto& option : MODE_OPTIONS) {
|
||||
auto* chip = lvgl_chip_create(chips_row);
|
||||
lv_label_set_text(lv_label_create(chip), option.name);
|
||||
lv_obj_set_user_data(chip, reinterpret_cast<void*>(static_cast<intptr_t>(option.mode)));
|
||||
lv_obj_add_event_cb(chip, onModeChipPressed, LV_EVENT_SHORT_CLICKED, device);
|
||||
}
|
||||
lvgl_grid_navigation_add(chips_row);
|
||||
|
||||
GroveMode current = GROVE_MODE_DISABLED;
|
||||
grove_get_mode(device, ¤t);
|
||||
lv_dropdown_set_selected(dropdown, static_cast<uint32_t>(current));
|
||||
|
||||
lv_obj_add_event_cb(dropdown, onModeChanged, LV_EVENT_VALUE_CHANGED, device);
|
||||
updateChips(chips_row, current);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
#include "Tactility/app/i2cscanner/I2cHelpers.h"
|
||||
|
||||
#include <Tactility/StringUtils.h>
|
||||
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
|
||||
#include <iomanip>
|
||||
@ -18,7 +16,7 @@ std::string getAddressText(uint8_t address) {
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string getPortNamesForDropdown() {
|
||||
std::vector<std::string> getPortNames() {
|
||||
std::vector<std::string> config_names;
|
||||
device_for_each_of_type(&I2C_CONTROLLER_TYPE, &config_names, [](auto* device, auto* context) {
|
||||
auto* names = static_cast<std::vector<std::string>*>(context);
|
||||
@ -27,7 +25,7 @@ std::string getPortNamesForDropdown() {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return string::join(config_names, "\n");
|
||||
return config_names;
|
||||
}
|
||||
|
||||
bool getActivePortAtIndex(int32_t index, struct Device** out) {
|
||||
|
||||
@ -24,7 +24,10 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/chip.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
namespace tt::app::i2cscanner {
|
||||
@ -35,9 +38,6 @@ namespace {
|
||||
|
||||
constexpr auto* TAG = "I2cScanner";
|
||||
|
||||
constexpr auto* START_SCAN_TEXT = "Scan";
|
||||
constexpr auto* STOP_SCAN_TEXT = "Stop scan";
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
|
||||
@ -47,10 +47,14 @@ struct Context {
|
||||
// State
|
||||
ScanState scanState = ScanStateInitial;
|
||||
struct Device* portDevice = nullptr;
|
||||
int32_t selectedBus = 0;
|
||||
std::vector<uint8_t> scannedAddresses;
|
||||
// Widgets
|
||||
lv_obj_t* scanButtonLabelWidget = nullptr;
|
||||
lv_obj_t* portDropdownWidget = nullptr;
|
||||
/** Row and column sizes of the results grid, which must outlive the grid layout */
|
||||
std::vector<int32_t> gridRows;
|
||||
std::vector<int32_t> gridColumns;
|
||||
// Widgets: the refresh button and chips only exist when there are I2C interfaces
|
||||
lv_obj_t* refreshButton = nullptr;
|
||||
lv_obj_t* chipsRow = nullptr;
|
||||
lv_obj_t* scanListWidget = nullptr;
|
||||
};
|
||||
|
||||
@ -94,6 +98,37 @@ int32_t getLastBusIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
/** The names of the devicetree devices on the bus at the given address, separated by commas */
|
||||
std::string getDeviceNames(struct Device* port, uint8_t address) {
|
||||
struct Search {
|
||||
uint8_t address;
|
||||
std::string names;
|
||||
} search = { address, "" };
|
||||
device_for_each_child(port, &search, [](struct Device* child, void* context) {
|
||||
auto* search = static_cast<Search*>(context);
|
||||
if (child->address == search->address) {
|
||||
if (!search->names.empty()) {
|
||||
search->names += ", ";
|
||||
}
|
||||
search->names += child->name;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return search.names;
|
||||
}
|
||||
|
||||
// A disturbed probe can report a device that isn't there, while a real device acknowledges every probe
|
||||
constexpr int PROBE_COUNT = 3;
|
||||
|
||||
bool hasDeviceAt(struct Device* port, uint8_t address) {
|
||||
for (int i = 0; i < PROBE_COUNT; i++) {
|
||||
if (i2c_controller_has_device_at_address(port, address, 10 / portTICK_PERIOD_MS) != ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getPort(Context* ctx, struct Device** outPort) {
|
||||
if (ctx->mutex.lock(100 / portTICK_PERIOD_MS)) {
|
||||
*outPort = ctx->portDevice;
|
||||
@ -128,28 +163,57 @@ bool shouldStopScanTimer(Context* ctx) {
|
||||
|
||||
void updateViews(Context* ctx) {
|
||||
if (ctx->mutex.lock(100 / portTICK_PERIOD_MS)) {
|
||||
if (ctx->scanState == ScanStateScanning) {
|
||||
lv_label_set_text(ctx->scanButtonLabelWidget, STOP_SCAN_TEXT);
|
||||
lv_obj_remove_flag(ctx->portDropdownWidget, LV_OBJ_FLAG_CLICKABLE);
|
||||
} else {
|
||||
lv_label_set_text(ctx->scanButtonLabelWidget, START_SCAN_TEXT);
|
||||
lv_obj_add_flag(ctx->portDropdownWidget, LV_OBJ_FLAG_CLICKABLE);
|
||||
if (ctx->scanListWidget == nullptr) {
|
||||
ctx->mutex.unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
// The scan reads the port once, so the bus can't change and the scan can't restart while it runs
|
||||
const bool scanning = ctx->scanState == ScanStateScanning;
|
||||
lv_obj_set_state(ctx->refreshButton, LV_STATE_DISABLED, scanning);
|
||||
if (ctx->chipsRow != nullptr) {
|
||||
const uint32_t chip_count = lv_obj_get_child_count(ctx->chipsRow);
|
||||
for (uint32_t i = 0; i < chip_count; i++) {
|
||||
auto* chip = lv_obj_get_child(ctx->chipsRow, static_cast<int32_t>(i));
|
||||
lv_obj_set_state(chip, LV_STATE_CHECKED, static_cast<int32_t>(i) == ctx->selectedBus);
|
||||
lv_obj_set_state(chip, LV_STATE_DISABLED, scanning);
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_clean(ctx->scanListWidget);
|
||||
if (ctx->scanState == ScanStateStopped) {
|
||||
lv_obj_remove_flag(ctx->scanListWidget, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->scanListWidget, false);
|
||||
|
||||
if (!ctx->scannedAddresses.empty()) {
|
||||
for (auto address: ctx->scannedAddresses) {
|
||||
std::string address_text = getAddressText(address);
|
||||
lv_list_add_text(ctx->scanListWidget, address_text.c_str());
|
||||
}
|
||||
// A grid with the title in the first row, then an address and the devicetree device names per row
|
||||
const size_t count = ctx->scannedAddresses.size();
|
||||
ctx->gridColumns = { LV_GRID_CONTENT, LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST };
|
||||
ctx->gridRows.assign(count + 1, LV_GRID_CONTENT);
|
||||
ctx->gridRows.push_back(LV_GRID_TEMPLATE_LAST);
|
||||
lv_obj_set_grid_dsc_array(ctx->scanListWidget, ctx->gridColumns.data(), ctx->gridRows.data());
|
||||
|
||||
auto* title = lv_label_create(ctx->scanListWidget);
|
||||
lv_obj_set_grid_cell(title, LV_GRID_ALIGN_START, 0, 2, LV_GRID_ALIGN_CENTER, 0, 1);
|
||||
if (count == 0) {
|
||||
lv_label_set_text(title, "No devices found");
|
||||
} else {
|
||||
lv_list_add_text(ctx->scanListWidget, "No devices found");
|
||||
lv_label_set_text(title, std::format("{} {} found", count, count == 1 ? "device" : "devices").c_str());
|
||||
}
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
const uint8_t address = ctx->scannedAddresses[i];
|
||||
const auto row = static_cast<int32_t>(i + 1);
|
||||
auto* address_label = lv_label_create(ctx->scanListWidget);
|
||||
lv_label_set_text(address_label, getAddressText(address).c_str());
|
||||
// Keys and encoders move through the addresses, which scrolls the results into view
|
||||
lv_obj_set_clickable(address_label, true);
|
||||
lv_obj_set_grid_cell(address_label, LV_GRID_ALIGN_START, 0, 1, LV_GRID_ALIGN_START, row, 1);
|
||||
|
||||
auto* names_label = lv_label_create(ctx->scanListWidget);
|
||||
lv_label_set_long_mode(names_label, LV_LABEL_LONG_MODE_WRAP);
|
||||
lv_label_set_text(names_label, getDeviceNames(ctx->portDevice, address).c_str());
|
||||
lv_obj_set_grid_cell(names_label, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_START, row, 1);
|
||||
}
|
||||
} else {
|
||||
lv_obj_add_flag(ctx->scanListWidget, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->scanListWidget, true);
|
||||
}
|
||||
|
||||
ctx->mutex.unlock();
|
||||
@ -194,7 +258,7 @@ void onScanTimer(Context* ctx) {
|
||||
}
|
||||
|
||||
for (uint8_t address = 1; address < 128; ++address) {
|
||||
if (i2c_controller_has_device_at_address(safe_port, address, 10 / portTICK_PERIOD_MS) == ERROR_NONE) {
|
||||
if (hasDeviceAt(safe_port, address)) {
|
||||
LOG_I(TAG, "Found device at address 0x%02X", address);
|
||||
if (!shouldStopScanTimer(ctx)) {
|
||||
addAddressToList(ctx, address);
|
||||
@ -246,7 +310,7 @@ void startScanning(Context* ctx) {
|
||||
if (ctx->mutex.lock(100 / portTICK_PERIOD_MS)) {
|
||||
ctx->scannedAddresses.clear();
|
||||
|
||||
lv_obj_add_flag(ctx->scanListWidget, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->scanListWidget, true);
|
||||
lv_obj_clean(ctx->scanListWidget);
|
||||
|
||||
ctx->scanState = ScanStateScanning;
|
||||
@ -269,6 +333,7 @@ void selectBus(Context* ctx, int32_t selected) {
|
||||
if (ctx->mutex.lock(100 / portTICK_PERIOD_MS)) {
|
||||
ctx->scannedAddresses.clear();
|
||||
ctx->portDevice = found_device;
|
||||
ctx->selectedBus = selected;
|
||||
ctx->scanState = ScanStateInitial;
|
||||
ctx->mutex.unlock();
|
||||
}
|
||||
@ -288,20 +353,15 @@ void onBackPressed(lv_event_t* event) {
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onSelectBus(lv_event_t* event) {
|
||||
void onChipPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
uint32_t selected = lv_dropdown_get_selected(dropdown);
|
||||
selectBus(ctx, selected);
|
||||
auto* chip = lv_event_get_target_obj(event);
|
||||
selectBus(ctx, static_cast<int32_t>(reinterpret_cast<intptr_t>(lv_obj_get_user_data(chip))));
|
||||
}
|
||||
|
||||
void onPressScan(lv_event_t* event) {
|
||||
void onPressRefresh(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
if (ctx->scanState == ScanStateScanning) {
|
||||
stopScanning(ctx);
|
||||
} else {
|
||||
startScanning(ctx);
|
||||
}
|
||||
startScanning(ctx);
|
||||
updateViews(ctx);
|
||||
}
|
||||
|
||||
@ -318,50 +378,56 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
|
||||
auto* main_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(main_wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(main_wrapper, 1);
|
||||
|
||||
auto* wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
// Ensures showing selection state for keyboard/encoder devices
|
||||
lv_obj_set_style_pad_all(wrapper, 6, LV_ALIGN_DEFAULT);
|
||||
const std::vector<std::string> port_names = getPortNames();
|
||||
if (port_names.empty()) {
|
||||
lv_obj_set_flex_align(main_wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
auto* label = lv_label_create(main_wrapper);
|
||||
lv_obj_set_width(label, LV_PCT(100));
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_WRAP);
|
||||
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, LV_STATE_DEFAULT);
|
||||
lv_label_set_text(label, "I2C interface not available");
|
||||
return;
|
||||
}
|
||||
|
||||
auto* scan_button = lv_button_create(wrapper);
|
||||
lv_obj_set_width(scan_button, LV_PCT(48));
|
||||
lv_obj_align(scan_button, LV_ALIGN_TOP_LEFT, 0, 1); // Shift 1 pixel to align with selection box
|
||||
lv_obj_add_event_cb(scan_button, onPressScan, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
auto* scan_button_label = lv_label_create(scan_button);
|
||||
lv_obj_align(scan_button_label, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_label_set_text(scan_button_label, START_SCAN_TEXT);
|
||||
ctx->scanButtonLabelWidget = scan_button_label;
|
||||
ctx->refreshButton = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_REFRESH, onPressRefresh, ctx);
|
||||
|
||||
auto* port_dropdown = lv_dropdown_create(wrapper);
|
||||
std::string dropdown_items = getPortNamesForDropdown();
|
||||
lv_dropdown_set_options(port_dropdown, dropdown_items.c_str());
|
||||
lv_obj_set_width(port_dropdown, LV_PCT(48));
|
||||
lv_obj_align(port_dropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
|
||||
lv_obj_add_event_cb(port_dropdown, onSelectBus, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
auto selected_bus = getLastBusIndex();
|
||||
lv_dropdown_set_selected(port_dropdown, selected_bus);
|
||||
ctx->portDropdownWidget = port_dropdown;
|
||||
if (port_names.size() > 1) {
|
||||
// Centered while the chips fit, and scrollable when they don't
|
||||
lv_obj_set_flex_align(main_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START);
|
||||
auto* chips_row = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(chips_row, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_max_width(chips_row, LV_PCT(100), LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(chips_row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_scroll_dir(chips_row, LV_DIR_HOR);
|
||||
lv_obj_set_style_border_width(chips_row, 0, LV_STATE_DEFAULT);
|
||||
// The chips' margins leave room for their focus rings and space them apart
|
||||
lv_obj_set_style_pad_all(chips_row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_column(chips_row, 0, LV_STATE_DEFAULT);
|
||||
for (size_t i = 0; i < port_names.size(); i++) {
|
||||
auto* chip = lvgl_chip_create(chips_row);
|
||||
lv_label_set_text(lv_label_create(chip), port_names[i].c_str());
|
||||
lv_obj_set_user_data(chip, reinterpret_cast<void*>(static_cast<intptr_t>(i)));
|
||||
lv_obj_add_event_cb(chip, onChipPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
}
|
||||
lvgl_grid_navigation_add(chips_row);
|
||||
ctx->chipsRow = chips_row;
|
||||
}
|
||||
|
||||
auto* scan_list = lv_list_create(main_wrapper);
|
||||
lv_obj_set_style_margin_top(scan_list, 8, 0);
|
||||
lv_obj_set_width(scan_list, LV_PCT(100));
|
||||
lv_obj_set_height(scan_list, LV_SIZE_CONTENT);
|
||||
lv_obj_add_flag(scan_list, LV_OBJ_FLAG_HIDDEN);
|
||||
auto* scan_list = lvgl_card_create(main_wrapper);
|
||||
lv_obj_set_size(scan_list, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_layout(scan_list, LV_LAYOUT_GRID);
|
||||
lvgl_grid_navigation_add(scan_list);
|
||||
lv_obj_set_hidden(scan_list, true);
|
||||
ctx->scanListWidget = scan_list;
|
||||
|
||||
struct Device* dummy;
|
||||
if (getActivePortAtIndex(selected_bus, &dummy)) {
|
||||
selectBus(ctx, selected_bus);
|
||||
} else if (getActivePortAtIndex(0, &dummy)) {
|
||||
lv_dropdown_set_selected(port_dropdown, 0);
|
||||
selectBus(ctx, 0);
|
||||
}
|
||||
const int32_t last_bus = getLastBusIndex();
|
||||
selectBus(ctx, getActivePortAtIndex(last_bus, &dummy) ? last_bus : 0);
|
||||
}
|
||||
|
||||
// Mirrors the old model's onHide(): stop any in-flight scan before this app's task exits
|
||||
|
||||
@ -122,6 +122,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
|
||||
auto* main_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(main_wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(main_wrapper, 1);
|
||||
@ -129,15 +130,17 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
// Keyboard backlight toggle
|
||||
auto* bl_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(bl_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(bl_wrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(bl_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(bl_wrapper, false);
|
||||
lv_obj_set_style_pad_all(bl_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(bl_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* bl_label = lv_label_create(bl_wrapper);
|
||||
lv_label_set_text(bl_label, "Keyboard backlight");
|
||||
lv_obj_align(bl_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(bl_label, 1);
|
||||
ctx->switchBacklight = lv_switch_create(bl_wrapper);
|
||||
if (ctx->kbSettings.backlightEnabled) lv_obj_add_state(ctx->switchBacklight, LV_STATE_CHECKED);
|
||||
lv_obj_align(ctx->switchBacklight, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->switchBacklight, onBacklightSwitch, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
// Brightness slider
|
||||
@ -160,15 +163,17 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
// Backlight timeout enable
|
||||
auto* to_enable_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(to_enable_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(to_enable_wrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(to_enable_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(to_enable_wrapper, false);
|
||||
lv_obj_set_style_pad_all(to_enable_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(to_enable_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* to_enable_label = lv_label_create(to_enable_wrapper);
|
||||
lv_label_set_text(to_enable_label, "Auto backlight off");
|
||||
lv_obj_align(to_enable_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(to_enable_label, 1);
|
||||
ctx->switchTimeoutEnable = lv_switch_create(to_enable_wrapper);
|
||||
if (ctx->kbSettings.backlightTimeoutEnabled) lv_obj_add_state(ctx->switchTimeoutEnable, LV_STATE_CHECKED);
|
||||
lv_obj_align(ctx->switchTimeoutEnable, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->switchTimeoutEnable, onTimeoutEnableSwitch, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
auto* timeout_select_wrapper = lv_obj_create(main_wrapper);
|
||||
|
||||
131
Tactility/Source/app/launcher/AppsMode.cpp
Normal file
131
Tactility/Source/app/launcher/AppsMode.cpp
Normal file
@ -0,0 +1,131 @@
|
||||
#include <Tactility/app/launcher/Favourites.h>
|
||||
#include <Tactility/app/launcher/LauncherMode.h>
|
||||
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/package_manifest.h>
|
||||
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace tt::app::launcher {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto* TAG = "Launcher";
|
||||
|
||||
const Favourites favourites;
|
||||
|
||||
struct IconEntry {
|
||||
const char* id;
|
||||
const char* icon;
|
||||
};
|
||||
|
||||
constexpr IconEntry ICONS[] = {
|
||||
{"tactility.apphub", LVGL_ICON_SHARED_DOWNLOAD},
|
||||
{"tactility.camera", LVGL_ICON_SHARED_CAMERA},
|
||||
{"tactility.chat", LVGL_ICON_SHARED_FORUM},
|
||||
{"tactility.files", LVGL_ICON_SHARED_FOLDER},
|
||||
{"tactility.i2cscanner", LVGL_ICON_SHARED_CABLE},
|
||||
{"tactility.notes", LVGL_ICON_SHARED_EDIT_NOTE},
|
||||
{"tactility.screenshot", LVGL_ICON_SHARED_IMAGE},
|
||||
{"tactility.systeminfo", LVGL_ICON_SHARED_DEVICES},
|
||||
{"tactility.terminal", LVGL_ICON_SHARED_TERMINAL},
|
||||
{"tactility.webserversettings", LVGL_ICON_SHARED_CLOUD},
|
||||
};
|
||||
|
||||
// Hidden apps that are still shown in the launcher
|
||||
constexpr const char* SHOWN_HIDDEN_APP_IDS[] = {
|
||||
"tactility.files",
|
||||
};
|
||||
|
||||
const char* appIcon(const ::AppManifest* manifest) {
|
||||
for (const auto& entry : ICONS) {
|
||||
if (!strcmp(manifest->id, entry.id)) return entry.icon;
|
||||
}
|
||||
return LVGL_ICON_SHARED_DEPLOYED_CODE;
|
||||
}
|
||||
|
||||
bool isShown(const ::AppManifest& manifest) {
|
||||
if (manifest.category != APP_CATEGORY_USER && manifest.category != APP_CATEGORY_SYSTEM) {
|
||||
return false;
|
||||
}
|
||||
if ((manifest.flags & APP_MANIFEST_FLAG_HIDDEN) == 0) {
|
||||
return true;
|
||||
}
|
||||
return std::ranges::any_of(SHOWN_HIDDEN_APP_IDS, [&](const char* id) { return strcmp(manifest.id, id) == 0; });
|
||||
}
|
||||
|
||||
void collectManifest(const ::AppManifest* manifest, void* context) {
|
||||
auto* manifests = static_cast<std::vector<::AppManifest>*>(context);
|
||||
manifests->push_back(*manifest);
|
||||
}
|
||||
|
||||
// Apps from installed packages this device can't run (wrong device, too little RAM)
|
||||
void collectIncompatibleAppIds(const AppPackage* package, void* context) {
|
||||
if (app_package_manifest_is_compatible(&package->package)) {
|
||||
return;
|
||||
}
|
||||
auto* ids = static_cast<std::vector<std::string>*>(context);
|
||||
for (size_t i = 0; i < package->app_id_count; i++) {
|
||||
ids->emplace_back(package->app_ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<AppGridItem> collectItems() {
|
||||
const std::vector<std::string> favouriteIds = favourites.load();
|
||||
|
||||
std::vector<::AppManifest> collected;
|
||||
app_manager_for_each_manifest(collectManifest, &collected);
|
||||
std::vector<std::string> incompatibleIds;
|
||||
app_manager_for_each_package(collectIncompatibleAppIds, &incompatibleIds);
|
||||
std::erase_if(collected, [&](const ::AppManifest& manifest) {
|
||||
return !isShown(manifest) || std::ranges::find(incompatibleIds, std::string(manifest.id)) != incompatibleIds.end();
|
||||
});
|
||||
|
||||
std::vector<AppGridItem> items;
|
||||
items.reserve(collected.size());
|
||||
for (const auto& manifest : collected) {
|
||||
items.push_back({ manifest, appIcon(&manifest), Favourites::contains(favouriteIds, manifest.id) });
|
||||
}
|
||||
std::ranges::sort(items, [](const AppGridItem& a, const AppGridItem& b) {
|
||||
if (a.highlighted != b.highlighted) {
|
||||
return a.highlighted;
|
||||
}
|
||||
return strcmp(a.manifest.name, b.manifest.name) < 0;
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
void toggleFavourite(AppGrid& grid, const char* appId, bool keepSelection) {
|
||||
if (!favourites.toggle(appId)) {
|
||||
LOG_E(TAG, "Failed to save favourites");
|
||||
}
|
||||
grid.requestRepopulate(keepSelection);
|
||||
}
|
||||
|
||||
void onLongPressed(AppGrid& grid, const ::AppManifest& manifest) {
|
||||
toggleFavourite(grid, manifest.id, false);
|
||||
}
|
||||
|
||||
void onKey(AppGrid& grid, const ::AppManifest& manifest, uint32_t key) {
|
||||
if (key == 'f' || key == 'F') {
|
||||
toggleFavourite(grid, manifest.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const LauncherMode APPS_MODE = {
|
||||
.buttonIcon = LVGL_ICON_SHARED_APPS,
|
||||
.iconColor = AppGrid::IconColor::Primary,
|
||||
.collect = collectItems,
|
||||
.onLongPressed = onLongPressed,
|
||||
.onKey = onKey
|
||||
};
|
||||
|
||||
}
|
||||
@ -1,13 +1,10 @@
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/package_manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
#include <app/start.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <lvgl.h>
|
||||
@ -22,7 +19,7 @@
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <Tactility/app/AppGrid.h>
|
||||
#include <Tactility/app/launcher/Favourites.h>
|
||||
#include <Tactility/app/launcher/LauncherMode.h>
|
||||
#include <Tactility/app/setup/Setup.h>
|
||||
#include <Tactility/settings/BootSettings.h>
|
||||
#include <Tactility/Tactility.h>
|
||||
@ -33,69 +30,27 @@ constexpr auto* TAG = "Launcher";
|
||||
|
||||
namespace {
|
||||
|
||||
struct IconEntry {
|
||||
const char* id;
|
||||
const char* icon;
|
||||
};
|
||||
|
||||
constexpr IconEntry ICONS[] = {
|
||||
{"tactility.apphub", LVGL_ICON_SHARED_DOWNLOAD},
|
||||
{"tactility.camera", LVGL_ICON_SHARED_CAMERA},
|
||||
{"tactility.chat", LVGL_ICON_SHARED_FORUM},
|
||||
{"tactility.files", LVGL_ICON_SHARED_FOLDER},
|
||||
{"tactility.i2cscanner", LVGL_ICON_SHARED_CABLE},
|
||||
{"tactility.notes", LVGL_ICON_SHARED_EDIT_NOTE},
|
||||
{"tactility.screenshot", LVGL_ICON_SHARED_IMAGE},
|
||||
{"tactility.systeminfo", LVGL_ICON_SHARED_DEVICES},
|
||||
{"tactility.terminal", LVGL_ICON_SHARED_TERMINAL},
|
||||
{"tactility.webserversettings", LVGL_ICON_SHARED_CLOUD},
|
||||
};
|
||||
|
||||
// Hidden apps that are still shown in the launcher
|
||||
constexpr const char* SHOWN_HIDDEN_APP_IDS[] = {
|
||||
"tactility.files",
|
||||
};
|
||||
|
||||
const char* appIcon(const ::AppManifest* manifest) {
|
||||
for (const auto& entry : ICONS) {
|
||||
if (!strcmp(manifest->id, entry.id)) return entry.icon;
|
||||
}
|
||||
return LVGL_ICON_SHARED_DEPLOYED_CODE;
|
||||
}
|
||||
|
||||
bool isShown(const ::AppManifest& manifest) {
|
||||
if (manifest.category != APP_CATEGORY_USER && manifest.category != APP_CATEGORY_SYSTEM) {
|
||||
return false;
|
||||
}
|
||||
if ((manifest.flags & APP_MANIFEST_FLAG_HIDDEN) == 0) {
|
||||
return true;
|
||||
}
|
||||
return std::ranges::any_of(SHOWN_HIDDEN_APP_IDS, [&](const char* id) { return strcmp(manifest.id, id) == 0; });
|
||||
}
|
||||
|
||||
void collectManifest(const ::AppManifest* manifest, void* context) {
|
||||
auto* manifests = static_cast<std::vector<::AppManifest>*>(context);
|
||||
manifests->push_back(*manifest);
|
||||
}
|
||||
|
||||
// Apps from installed packages this device can't run (wrong device, too little RAM)
|
||||
void collectIncompatibleAppIds(const AppPackage* package, void* context) {
|
||||
if (app_package_manifest_is_compatible(&package->package)) {
|
||||
return;
|
||||
}
|
||||
auto* ids = static_cast<std::vector<std::string>*>(context);
|
||||
for (size_t i = 0; i < package->app_id_count; i++) {
|
||||
ids->emplace_back(package->app_ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<AppGridItem> collectItems(void* userData);
|
||||
void onAppClicked(const ::AppManifest& manifest, void* userData);
|
||||
void onAppLongPressed(const ::AppManifest& manifest, void* userData);
|
||||
void onAppKey(const ::AppManifest& manifest, uint32_t key, void* userData);
|
||||
|
||||
enum class Mode {
|
||||
Apps,
|
||||
Settings
|
||||
};
|
||||
|
||||
const LauncherMode& getLauncherMode(Mode mode) {
|
||||
return mode == Mode::Settings ? SETTINGS_MODE : APPS_MODE;
|
||||
}
|
||||
|
||||
Mode getOtherMode(Mode mode) {
|
||||
return mode == Mode::Settings ? Mode::Apps : Mode::Settings;
|
||||
}
|
||||
|
||||
struct Context {
|
||||
Favourites favourites;
|
||||
Mode mode = Mode::Apps;
|
||||
lv_obj_t* modeButtonIcon = nullptr;
|
||||
AppGrid grid { AppGrid::Callbacks {
|
||||
.collect = collectItems,
|
||||
.onClicked = onAppClicked,
|
||||
@ -106,29 +61,7 @@ struct Context {
|
||||
};
|
||||
|
||||
std::vector<AppGridItem> collectItems(void* userData) {
|
||||
const auto* ctx = static_cast<Context*>(userData);
|
||||
const std::vector<std::string> favouriteIds = ctx->favourites.load();
|
||||
|
||||
std::vector<::AppManifest> collected;
|
||||
app_manager_for_each_manifest(collectManifest, &collected);
|
||||
std::vector<std::string> incompatibleIds;
|
||||
app_manager_for_each_package(collectIncompatibleAppIds, &incompatibleIds);
|
||||
std::erase_if(collected, [&](const ::AppManifest& manifest) {
|
||||
return !isShown(manifest) || std::ranges::find(incompatibleIds, std::string(manifest.id)) != incompatibleIds.end();
|
||||
});
|
||||
|
||||
std::vector<AppGridItem> items;
|
||||
items.reserve(collected.size());
|
||||
for (const auto& manifest : collected) {
|
||||
items.push_back({ manifest, appIcon(&manifest), Favourites::contains(favouriteIds, manifest.id) });
|
||||
}
|
||||
std::ranges::sort(items, [](const AppGridItem& a, const AppGridItem& b) {
|
||||
if (a.highlighted != b.highlighted) {
|
||||
return a.highlighted;
|
||||
}
|
||||
return strcmp(a.manifest.name, b.manifest.name) < 0;
|
||||
});
|
||||
return items;
|
||||
return getLauncherMode(static_cast<Context*>(userData)->mode).collect();
|
||||
}
|
||||
|
||||
void startApp(const char* appId) {
|
||||
@ -143,23 +76,38 @@ void onAppClicked(const ::AppManifest& manifest, void*) {
|
||||
startApp(manifest.id);
|
||||
}
|
||||
|
||||
void toggleFavourite(Context* ctx, const char* appId, bool keepSelection) {
|
||||
if (!ctx->favourites.toggle(appId)) {
|
||||
LOG_E(TAG, "Failed to save favourites");
|
||||
}
|
||||
ctx->grid.requestRepopulate(keepSelection);
|
||||
}
|
||||
|
||||
void onAppLongPressed(const ::AppManifest& manifest, void* userData) {
|
||||
toggleFavourite(static_cast<Context*>(userData), manifest.id, false);
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
const LauncherMode& mode = getLauncherMode(ctx->mode);
|
||||
if (mode.onLongPressed != nullptr) {
|
||||
mode.onLongPressed(ctx->grid, manifest);
|
||||
}
|
||||
}
|
||||
|
||||
void onAppKey(const ::AppManifest& manifest, uint32_t key, void* userData) {
|
||||
if (key == 'f' || key == 'F') {
|
||||
toggleFavourite(static_cast<Context*>(userData), manifest.id, true);
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
const LauncherMode& mode = getLauncherMode(ctx->mode);
|
||||
if (mode.onKey != nullptr) {
|
||||
mode.onKey(ctx->grid, manifest, key);
|
||||
}
|
||||
}
|
||||
|
||||
void applyIconColor(Context* ctx) {
|
||||
// The monochrome theme's primary color is black, also on a black background
|
||||
if (!lvgl_theme_is_mono()) {
|
||||
ctx->grid.setIconColor(getLauncherMode(ctx->mode).iconColor);
|
||||
}
|
||||
}
|
||||
|
||||
void onModeButtonPressed(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
ctx->mode = getOtherMode(ctx->mode);
|
||||
applyIconColor(ctx);
|
||||
ctx->grid.showFirstPage();
|
||||
ctx->grid.requestRepopulate(false);
|
||||
lv_label_set_text(ctx->modeButtonIcon, getLauncherMode(getOtherMode(ctx->mode)).buttonIcon);
|
||||
}
|
||||
|
||||
void onShortcutPressed(lv_event_t* e) {
|
||||
startApp(static_cast<const char*>(lv_event_get_user_data(e)));
|
||||
}
|
||||
@ -188,13 +136,11 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
// The monochrome theme's primary color is black, also on a black background
|
||||
if (!lvgl_theme_is_mono()) {
|
||||
ctx->grid.setIconColor(AppGrid::IconColor::Primary);
|
||||
}
|
||||
applyIconColor(ctx);
|
||||
ctx->grid.setSwipeNavigation(true);
|
||||
ctx->grid.createWidgetsWithBottomBar(parent);
|
||||
ctx->grid.addBarButton(LVGL_ICON_SHARED_SETTINGS, onShortcutPressed, const_cast<char*>("tactility.settings"));
|
||||
lv_obj_t* modeButton = ctx->grid.addBarButton(getLauncherMode(getOtherMode(ctx->mode)).buttonIcon, onModeButtonPressed, ctx);
|
||||
ctx->modeButtonIcon = lv_obj_get_child(modeButton, 0);
|
||||
if (isAppRegistered("tactility.poweroff") && supportsPowerOff()) {
|
||||
ctx->grid.addBarButton(LVGL_ICON_SHARED_POWER_SETTINGS_NEW, onShortcutPressed, const_cast<char*>("tactility.poweroff"));
|
||||
}
|
||||
@ -275,13 +221,4 @@ extern const ::AppManifest manifest = {
|
||||
.stack = { .depth = 5120, .desired_memory_capability = 0 }
|
||||
};
|
||||
|
||||
// Kept for Tactility/Private/Tactility/app/launcher/Launcher.h's existing declaration (still
|
||||
// used by the old, unconverted CrashDiagnostics app to return to the launcher after a crash).
|
||||
uint32_t start() {
|
||||
uint32_t instance_id = 0;
|
||||
AppStartContext context = app_start_context_for_manifest(&manifest);
|
||||
app_start_with_context(&context, &instance_id);
|
||||
return instance_id;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
78
Tactility/Source/app/launcher/SettingsMode.cpp
Normal file
78
Tactility/Source/app/launcher/SettingsMode.cpp
Normal file
@ -0,0 +1,78 @@
|
||||
#include <Tactility/app/launcher/LauncherMode.h>
|
||||
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <lvgl/icons/shared.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace tt::app::launcher {
|
||||
|
||||
namespace {
|
||||
|
||||
struct IconEntry {
|
||||
const char* id;
|
||||
const char* icon;
|
||||
};
|
||||
|
||||
constexpr IconEntry ICONS[] = {
|
||||
{"tactility.appearance", LVGL_ICON_SHARED_PALETTE},
|
||||
{"tactility.apppackagelist", LVGL_ICON_SHARED_DEPLOYED_CODE},
|
||||
{"tactility.audiosettings", LVGL_ICON_SHARED_MUSIC_NOTE},
|
||||
{"tactility.btmanage", LVGL_ICON_SHARED_BLUETOOTH},
|
||||
{"tactility.development", LVGL_ICON_SHARED_LOGO_DEV},
|
||||
{"tactility.display", LVGL_ICON_SHARED_DISPLAY_SETTINGS},
|
||||
{"tactility.gpssettings", LVGL_ICON_SHARED_NAVIGATION},
|
||||
{"tactility.grovesettings", LVGL_ICON_SHARED_CABLE},
|
||||
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
|
||||
{"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP},
|
||||
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
|
||||
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},
|
||||
{"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH},
|
||||
{"tactility.touchcalibration", LVGL_ICON_SHARED_CIRCLE},
|
||||
{"tactility.trackballsettings", LVGL_ICON_SHARED_DEVICES},
|
||||
{"tactility.usbsettings", LVGL_ICON_SHARED_USB},
|
||||
{"tactility.wifimanage", LVGL_ICON_SHARED_WIFI},
|
||||
};
|
||||
|
||||
const char* appIcon(const ::AppManifest* manifest) {
|
||||
for (const auto& entry : ICONS) {
|
||||
if (!strcmp(manifest->id, entry.id)) return entry.icon;
|
||||
}
|
||||
return LVGL_ICON_SHARED_DEPLOYED_CODE;
|
||||
}
|
||||
|
||||
void collectManifest(const ::AppManifest* manifest, void* context) {
|
||||
auto* manifests = static_cast<std::vector<::AppManifest>*>(context);
|
||||
manifests->push_back(*manifest);
|
||||
}
|
||||
|
||||
std::vector<AppGridItem> collectItems() {
|
||||
std::vector<::AppManifest> collected;
|
||||
app_manager_for_each_manifest(collectManifest, &collected);
|
||||
std::ranges::sort(collected, [](const ::AppManifest& a, const ::AppManifest& b) {
|
||||
return strcmp(a.name, b.name) < 0;
|
||||
});
|
||||
|
||||
std::vector<AppGridItem> items;
|
||||
for (const auto& manifest : collected) {
|
||||
if (manifest.category == APP_CATEGORY_SETTINGS && (manifest.flags & APP_MANIFEST_FLAG_HIDDEN) == 0) {
|
||||
items.push_back({ manifest, appIcon(&manifest), false });
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const LauncherMode SETTINGS_MODE = {
|
||||
.buttonIcon = LVGL_ICON_SHARED_SETTINGS,
|
||||
.iconColor = AppGrid::IconColor::Secondary,
|
||||
.collect = collectItems,
|
||||
.onLongPressed = nullptr,
|
||||
.onKey = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@ -12,6 +12,7 @@
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/sliderbox.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
@ -82,9 +83,9 @@ void applyAndLog(Context& ctx) {
|
||||
void updateVisibility(Context& ctx) {
|
||||
const bool hasSecondary = ctx.settings.pattern != settings::ledstrip::Pattern::Solid;
|
||||
if (hasSecondary) {
|
||||
lv_obj_remove_flag(ctx.secondaryRow, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx.secondaryRow, false);
|
||||
} else {
|
||||
lv_obj_add_flag(ctx.secondaryRow, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx.secondaryRow, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,9 +94,9 @@ void onNavigationPressed(lv_event_t* event);
|
||||
void returnToMain(Context& ctx) {
|
||||
ctx.page = Context::Page::Main;
|
||||
ctx.choiceKind = Context::ChoiceKind::None;
|
||||
lv_obj_add_flag(ctx.choicePanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(ctx.customPanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(ctx.mainPanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx.choicePanel, true);
|
||||
lv_obj_set_hidden(ctx.customPanel, true);
|
||||
lv_obj_set_hidden(ctx.mainPanel, false);
|
||||
lv_obj_update_layout(ctx.mainPanel);
|
||||
lvgl_toolbar_set_title(ctx.toolbar, "LED Strip");
|
||||
lvgl_toolbar_set_nav_action(ctx.toolbar, LV_SYMBOL_CLOSE, onNavigationPressed, &ctx);
|
||||
@ -117,11 +118,11 @@ void openCustomEditor(Context& ctx, bool secondary) {
|
||||
ctx.page = Context::Page::CustomColor;
|
||||
ctx.choiceKind = Context::ChoiceKind::None;
|
||||
ctx.editingSecondary = secondary;
|
||||
lv_obj_add_flag(ctx.choicePanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(ctx.mainPanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(ctx.customPanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(secondary ? ctx.primaryEditor : ctx.secondaryEditor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(secondary ? ctx.secondaryEditor : ctx.primaryEditor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx.choicePanel, true);
|
||||
lv_obj_set_hidden(ctx.mainPanel, true);
|
||||
lv_obj_set_hidden(ctx.customPanel, false);
|
||||
lv_obj_set_hidden(secondary ? ctx.primaryEditor : ctx.secondaryEditor, true);
|
||||
lv_obj_set_hidden(secondary ? ctx.secondaryEditor : ctx.primaryEditor, false);
|
||||
lvgl_toolbar_set_title(ctx.toolbar, secondary ? "Custom second color" : "Custom color");
|
||||
lvgl_toolbar_set_nav_action(ctx.toolbar, LV_SYMBOL_LEFT, onNavigationPressed, &ctx);
|
||||
lv_obj_update_layout(ctx.customPanel);
|
||||
@ -203,15 +204,15 @@ void openChoice(Context& ctx, Context::ChoiceKind kind, lv_obj_t* returnFocus) {
|
||||
const size_t selected = currentChoice(ctx);
|
||||
lv_obj_t* selectedButton = nullptr;
|
||||
for (size_t index = 0; index < choiceCount(kind); index++) {
|
||||
auto* button = lv_list_add_button(ctx.choiceList, index == selected ? LV_SYMBOL_OK : nullptr, choiceName(kind, index));
|
||||
auto* button = lvgl_list_add_button(ctx.choiceList, index == selected ? LV_SYMBOL_OK : nullptr, choiceName(kind, index));
|
||||
lv_obj_add_event_cb(button, onChoiceSelected, LV_EVENT_SHORT_CLICKED, &ctx);
|
||||
if (index == selected) selectedButton = button;
|
||||
}
|
||||
|
||||
lvgl_toolbar_set_title(ctx.toolbar, choiceTitle(kind));
|
||||
lvgl_toolbar_set_nav_action(ctx.toolbar, LV_SYMBOL_LEFT, onNavigationPressed, &ctx);
|
||||
lv_obj_add_flag(ctx.mainPanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(ctx.choicePanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx.mainPanel, true);
|
||||
lv_obj_set_hidden(ctx.choicePanel, false);
|
||||
if (selectedButton != nullptr) {
|
||||
lv_group_focus_obj(selectedButton);
|
||||
}
|
||||
@ -284,9 +285,12 @@ lv_obj_t* createRow(lv_obj_t* parent, const char* title) {
|
||||
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(row, 2, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(row, false);
|
||||
auto* label = lv_label_create(row);
|
||||
lv_label_set_text(label, title);
|
||||
lv_obj_align(label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(label, 1);
|
||||
return row;
|
||||
}
|
||||
|
||||
@ -331,28 +335,28 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lvgl_toolbar_set_nav_action(ctx->toolbar, LV_SYMBOL_CLOSE, onNavigationPressed, ctx);
|
||||
|
||||
ctx->mainPanel = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(ctx->mainPanel, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_width(ctx->mainPanel, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(ctx->mainPanel, 1);
|
||||
lv_obj_set_flex_flow(ctx->mainPanel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(ctx->mainPanel, 2, LV_STATE_DEFAULT);
|
||||
lv_obj_add_flag(ctx->mainPanel, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(ctx->mainPanel, true);
|
||||
|
||||
ctx->customPanel = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(ctx->customPanel, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_width(ctx->customPanel, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(ctx->customPanel, 1);
|
||||
lv_obj_set_flex_flow(ctx->customPanel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_add_flag(ctx->customPanel, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->customPanel, true);
|
||||
|
||||
auto* outputRow = createRow(ctx->mainPanel, "Output");
|
||||
auto* outputSwitch = lv_switch_create(outputRow);
|
||||
if (ctx->settings.enabled) lv_obj_add_state(outputSwitch, LV_STATE_CHECKED);
|
||||
lv_obj_align(outputSwitch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(outputSwitch, onOutputChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
auto* modeRow = createRow(ctx->mainPanel, "Mode");
|
||||
auto* modeButton = lv_button_create(modeRow);
|
||||
lv_obj_set_width(modeButton, LV_PCT(62));
|
||||
lv_obj_align(modeButton, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
ctx->modeValue = lv_label_create(modeButton);
|
||||
lv_label_set_text(ctx->modeValue, patternName(ctx->settings.pattern));
|
||||
lv_obj_center(ctx->modeValue);
|
||||
@ -361,41 +365,39 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* primaryRow = createRow(ctx->mainPanel, "Color");
|
||||
auto* primaryButton = lv_button_create(primaryRow);
|
||||
lv_obj_set_width(primaryButton, LV_PCT(62));
|
||||
lv_obj_align(primaryButton, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
ctx->primaryValue = lv_label_create(primaryButton);
|
||||
lv_label_set_text(ctx->primaryValue, presetName(ctx->settings.primaryPreset));
|
||||
lv_obj_center(ctx->primaryValue);
|
||||
lv_obj_add_event_cb(primaryButton, onPrimaryColorPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
|
||||
createRgbEditor(ctx->customPanel, "Red / Green / Blue", ctx->settings.primaryCustom, ctx->primarySliderBoxes, ctx, true);
|
||||
lv_obj_add_flag(ctx->primaryEditor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->primaryEditor, true);
|
||||
|
||||
ctx->secondaryRow = createRow(ctx->mainPanel, "Second color");
|
||||
auto* secondaryButton = lv_button_create(ctx->secondaryRow);
|
||||
lv_obj_set_width(secondaryButton, LV_PCT(62));
|
||||
lv_obj_align(secondaryButton, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
ctx->secondaryValue = lv_label_create(secondaryButton);
|
||||
lv_label_set_text(ctx->secondaryValue, presetName(ctx->settings.secondaryPreset));
|
||||
lv_obj_center(ctx->secondaryValue);
|
||||
lv_obj_add_event_cb(secondaryButton, onSecondaryColorPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
|
||||
createRgbEditor(ctx->customPanel, "Red / Green / Blue", ctx->settings.secondaryCustom, ctx->secondarySliderBoxes, ctx, false);
|
||||
lv_obj_add_flag(ctx->secondaryEditor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->secondaryEditor, true);
|
||||
|
||||
auto* brightnessRow = createRow(ctx->mainPanel, "Brightness");
|
||||
ctx->brightnessValue = lv_label_create(brightnessRow);
|
||||
lv_label_set_text_fmt(ctx->brightnessValue, "%u%%", (static_cast<unsigned>(ctx->settings.brightness) * 100 + 127) / 255);
|
||||
lv_obj_align(ctx->brightnessValue, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
ctx->brightnessSlider = lvgl_sliderbox_create(ctx->mainPanel, 0, UINT8_MAX, 17, ctx->settings.brightness);
|
||||
lv_obj_set_width(ctx->brightnessSlider, LV_PCT(100));
|
||||
lvgl_sliderbox_add_value_changed_cb(ctx->brightnessSlider, onBrightnessChanged, ctx);
|
||||
|
||||
ctx->choicePanel = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(ctx->choicePanel, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_width(ctx->choicePanel, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(ctx->choicePanel, 1);
|
||||
lv_obj_set_flex_flow(ctx->choicePanel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_add_flag(ctx->choicePanel, LV_OBJ_FLAG_HIDDEN);
|
||||
ctx->choiceList = lv_list_create(ctx->choicePanel);
|
||||
lv_obj_set_hidden(ctx->choicePanel, true);
|
||||
ctx->choiceList = lvgl_list_create(ctx->choicePanel);
|
||||
lv_obj_set_width(ctx->choiceList, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(ctx->choiceList, 1);
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
#include <Tactility/Tactility.h>
|
||||
|
||||
#include <Tactility/RecursiveMutex.h>
|
||||
#include <Tactility/StringUtils.h>
|
||||
#include <Tactility/app/localesettings/TextResources.h>
|
||||
#include <Tactility/app/selectiondialog/SelectionDialog.h>
|
||||
#include <Tactility/settings/Language.h>
|
||||
#include <Tactility/settings/SystemSettings.h>
|
||||
|
||||
@ -14,11 +14,17 @@
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app::localesettings {
|
||||
|
||||
@ -38,14 +44,19 @@ struct Context {
|
||||
uint32_t appInstanceId;
|
||||
tt::i18n::TextResources textResources = tt::i18n::TextResources(TEXT_RESOURCE_PATH);
|
||||
RecursiveMutex mutex;
|
||||
lv_obj_t* languageDropdown = nullptr;
|
||||
TaskEventGroup* eventGroup = nullptr;
|
||||
uint32_t selectLanguageBit = 0;
|
||||
uint32_t languageSelectLaunchId = 0;
|
||||
// Valid while the window is shown
|
||||
lv_obj_t* languageLabel = nullptr;
|
||||
lv_obj_t* languageValueLabel = nullptr;
|
||||
bool settingsUpdated = false;
|
||||
|
||||
std::map<settings::Language, std::string> languageMap;
|
||||
};
|
||||
|
||||
|
||||
std::string getLanguageOptions(Context* ctx) {
|
||||
std::vector<std::string> getLanguageNames(Context* ctx) {
|
||||
std::vector<std::string> items;
|
||||
for (int i = 0; i < static_cast<int>(settings::Language::count); i++) {
|
||||
switch (static_cast<settings::Language>(i)) {
|
||||
@ -68,25 +79,44 @@ std::string getLanguageOptions(Context* ctx) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return string::join(items, "\n");
|
||||
return items;
|
||||
}
|
||||
|
||||
/** Requires the LVGL lock */
|
||||
void updateViews(Context* ctx) {
|
||||
ctx->textResources.load();
|
||||
|
||||
std::string language_options = getLanguageOptions(ctx);
|
||||
lv_dropdown_set_options(ctx->languageDropdown, language_options.c_str());
|
||||
lv_dropdown_set_selected(ctx->languageDropdown, static_cast<uint32_t>(settings::getLanguage()));
|
||||
if (ctx->languageLabel == nullptr) {
|
||||
return;
|
||||
}
|
||||
lv_label_set_text(ctx->languageLabel, ctx->textResources[i18n::Text::LANGUAGE].c_str());
|
||||
const auto names = getLanguageNames(ctx);
|
||||
const auto index = static_cast<size_t>(settings::getLanguage());
|
||||
lv_label_set_text(ctx->languageValueLabel, index < names.size() ? names[index].c_str() : "");
|
||||
}
|
||||
|
||||
void onLanguageSet(lv_event_t* event) {
|
||||
void onSelectLanguagePressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
auto index = lv_dropdown_get_selected(dropdown);
|
||||
auto language = static_cast<settings::Language>(index);
|
||||
settings::setLanguage(language);
|
||||
task_event_group_signal(ctx->eventGroup, ctx->selectLanguageBit);
|
||||
}
|
||||
|
||||
updateViews(ctx);
|
||||
void startLanguageSelection(Context* ctx) {
|
||||
if (ctx->languageSelectLaunchId != 0) {
|
||||
return;
|
||||
}
|
||||
ctx->languageSelectLaunchId = selectiondialog::start(ctx->appInstanceId, ctx->textResources[i18n::Text::LANGUAGE], getLanguageNames(ctx));
|
||||
}
|
||||
|
||||
void onLanguageSelectionResult(Context* ctx, const AppEvent& event) {
|
||||
ctx->languageSelectLaunchId = 0;
|
||||
// Other results mean that the dialog was dismissed
|
||||
const int32_t selected = event.result.result;
|
||||
if (selected >= 0 && selected < static_cast<int32_t>(settings::Language::count)) {
|
||||
settings::setLanguage(static_cast<settings::Language>(selected));
|
||||
ctx->textResources.load();
|
||||
lvgl_lock();
|
||||
updateViews(ctx);
|
||||
lvgl_unlock();
|
||||
}
|
||||
app_manager_stop(event.result.launch_id);
|
||||
}
|
||||
|
||||
// Preserved from the pre-conversion code as-is: declared but never wired to any widget there
|
||||
@ -114,29 +144,57 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
|
||||
auto* main_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(main_wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(main_wrapper, 1);
|
||||
|
||||
// Language
|
||||
// "Language English [Select]"
|
||||
auto* card = lvgl_card_create(main_wrapper);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
auto* language_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_width(language_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(language_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(language_wrapper, 8, 0);
|
||||
lv_obj_set_style_border_width(language_wrapper, 0, 0);
|
||||
// Transparent, so the card provides the background
|
||||
auto* language_row = lv_obj_create(card);
|
||||
lv_obj_set_size(language_row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(language_row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(language_row, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(language_row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(language_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_border_width(language_row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(language_row, false);
|
||||
|
||||
auto* languageLabel = lv_label_create(language_wrapper);
|
||||
lv_label_set_text(languageLabel, ctx->textResources[i18n::Text::LANGUAGE].c_str());
|
||||
lv_obj_align(languageLabel, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
// The title with the language below it, which take the width that the button leaves. Texts that don't fit scroll.
|
||||
auto* texts = lv_obj_create(language_row);
|
||||
lv_obj_set_height(texts, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_grow(texts, 1);
|
||||
lv_obj_set_flex_flow(texts, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_all(texts, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_row(texts, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(texts, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(texts, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(texts, false);
|
||||
|
||||
ctx->languageDropdown = lv_dropdown_create(language_wrapper);
|
||||
lv_obj_set_width(ctx->languageDropdown, 150);
|
||||
lv_obj_align(ctx->languageDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
std::string language_options = getLanguageOptions(ctx);
|
||||
lv_dropdown_set_options(ctx->languageDropdown, language_options.c_str());
|
||||
lv_dropdown_set_selected(ctx->languageDropdown, static_cast<uint32_t>(settings::getLanguage()));
|
||||
lv_obj_add_event_cb(ctx->languageDropdown, onLanguageSet, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
ctx->languageLabel = lv_label_create(texts);
|
||||
lv_obj_set_width(ctx->languageLabel, LV_PCT(100));
|
||||
lv_label_set_long_mode(ctx->languageLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
|
||||
ctx->languageValueLabel = lv_label_create(texts);
|
||||
lv_obj_set_width(ctx->languageValueLabel, LV_PCT(100));
|
||||
lv_label_set_long_mode(ctx->languageValueLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
lv_obj_set_style_text_font(ctx->languageValueLabel, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
||||
|
||||
auto* select_button = lv_button_create(language_row);
|
||||
lv_label_set_text(lv_label_create(select_button), "Change");
|
||||
lv_obj_add_event_cb(select_button, onSelectLanguagePressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
|
||||
updateViews(ctx);
|
||||
}
|
||||
|
||||
void destroyWidgets(void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
ctx->languageLabel = nullptr;
|
||||
ctx->languageValueLabel = nullptr;
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
@ -146,26 +204,31 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
ctx.eventGroup = &event_group;
|
||||
check(task_event_group_claim_bit(&event_group, &ctx.selectLanguageBit) == ERROR_NONE);
|
||||
|
||||
AppEventSubscription sub {};
|
||||
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);
|
||||
|
||||
bool shouldClose = false;
|
||||
while (!shouldClose) {
|
||||
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||
uint32_t flags = 0;
|
||||
task_event_group_wait_any(&event_group, &flags, portMAX_DELAY);
|
||||
|
||||
if (flags & ctx.selectLanguageBit) {
|
||||
startLanguageSelection(&ctx);
|
||||
}
|
||||
|
||||
AppEvent event {};
|
||||
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||
switch (event.type) {
|
||||
case APP_EVENT_CLOSE:
|
||||
shouldClose = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (event.type == APP_EVENT_CLOSE) {
|
||||
shouldClose = true;
|
||||
break;
|
||||
} else if (event.type == APP_EVENT_RESULT && event.result.launch_id == ctx.languageSelectLaunchId) {
|
||||
onLanguageSelectionResult(&ctx, event);
|
||||
}
|
||||
if (shouldClose) break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -124,7 +124,7 @@ void appNotesEventCb(lv_event_t* e) {
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(parent, false);
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
@ -148,7 +148,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_style_pad_all(wrapper, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_pad_row(wrapper, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(wrapper, 0, LV_PART_MAIN);
|
||||
lv_obj_remove_flag(wrapper, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(wrapper, false);
|
||||
|
||||
ctx->uiNoteText = lv_textarea_create(wrapper);
|
||||
lv_obj_set_width(ctx->uiNoteText, LV_PCT(100));
|
||||
@ -174,7 +174,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_width(footer, LV_PCT(100));
|
||||
lv_obj_set_height(footer, LV_PCT(14));
|
||||
lv_obj_set_style_pad_all(footer, 0, LV_PART_MAIN);
|
||||
lv_obj_remove_flag(footer, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(footer, false);
|
||||
|
||||
ctx->uiCurrentFileName = lv_label_create(footer);
|
||||
lv_label_set_long_mode(ctx->uiCurrentFileName, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
|
||||
@ -181,18 +181,20 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_t* switch_container = lv_obj_create(card);
|
||||
lv_obj_set_width(switch_container, LV_PCT(100));
|
||||
lv_obj_set_height(switch_container, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(switch_container, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(switch_container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(switch_container, false);
|
||||
lv_obj_set_style_pad_all(switch_container, 0, 0);
|
||||
lv_obj_set_style_pad_gap(switch_container, 0, 0);
|
||||
lvgl::obj_set_style_bg_invisible(switch_container);
|
||||
|
||||
lv_obj_t* label = lv_label_create(switch_container);
|
||||
lv_label_set_text(label, "Charging enabled");
|
||||
lv_obj_set_align(label, LV_ALIGN_LEFT_MID);
|
||||
lv_obj_set_flex_grow(label, 1);
|
||||
|
||||
lv_obj_t* enable_switch = lv_switch_create(switch_container);
|
||||
lv_obj_set_user_data(enable_switch, device);
|
||||
lv_obj_add_event_cb(enable_switch, onPowerEnabledChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
lv_obj_set_align(enable_switch, LV_ALIGN_RIGHT_MID);
|
||||
lv_obj_set_state(enable_switch, LV_STATE_CHECKED, power_supply_is_allowed_to_charge(device));
|
||||
entry.enableSwitch = enable_switch;
|
||||
}
|
||||
@ -201,18 +203,20 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_t* qc_container = lv_obj_create(card);
|
||||
lv_obj_set_width(qc_container, LV_PCT(100));
|
||||
lv_obj_set_height(qc_container, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(qc_container, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(qc_container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(qc_container, false);
|
||||
lv_obj_set_style_pad_all(qc_container, 0, 0);
|
||||
lv_obj_set_style_pad_gap(qc_container, 0, 0);
|
||||
lvgl::obj_set_style_bg_invisible(qc_container);
|
||||
|
||||
lv_obj_t* label = lv_label_create(qc_container);
|
||||
lv_label_set_text(label, "Quick charge");
|
||||
lv_obj_set_align(label, LV_ALIGN_LEFT_MID);
|
||||
lv_obj_set_flex_grow(label, 1);
|
||||
|
||||
lv_obj_t* qc_switch = lv_switch_create(qc_container);
|
||||
lv_obj_set_user_data(qc_switch, device);
|
||||
lv_obj_add_event_cb(qc_switch, onQuickChargeChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
lv_obj_set_align(qc_switch, LV_ALIGN_RIGHT_MID);
|
||||
lv_obj_set_state(qc_switch, LV_STATE_CHECKED, power_supply_is_quick_charge_enabled(device));
|
||||
entry.quickChargeSwitch = qc_switch;
|
||||
}
|
||||
|
||||
@ -21,6 +21,9 @@
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/chip.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
namespace tt::app::screenshot {
|
||||
@ -31,12 +34,29 @@ extern const ::AppManifest manifest;
|
||||
|
||||
namespace {
|
||||
|
||||
enum class CaptureMode {
|
||||
Timer,
|
||||
AppStart
|
||||
};
|
||||
|
||||
struct ModeOption {
|
||||
CaptureMode mode;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
constexpr ModeOption MODE_OPTIONS[] = {
|
||||
{ CaptureMode::Timer, "Timer" },
|
||||
{ CaptureMode::AppStart, "App start" },
|
||||
};
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
lv_obj_t* modeDropdown = nullptr;
|
||||
CaptureMode mode = CaptureMode::Timer;
|
||||
lv_obj_t* modeChips = nullptr;
|
||||
lv_obj_t* pathTextArea = nullptr;
|
||||
lv_obj_t* startStopButtonLabel = nullptr;
|
||||
lv_obj_t* timerWrapper = nullptr;
|
||||
// Shown in timer mode
|
||||
lv_obj_t* delayRow = nullptr;
|
||||
lv_obj_t* delayTextArea = nullptr;
|
||||
std::unique_ptr<Timer> updateTimer;
|
||||
};
|
||||
@ -56,12 +76,11 @@ void updateScreenshotMode(Context* ctx) {
|
||||
lv_label_set_text(label, "Start");
|
||||
}
|
||||
|
||||
uint32_t selected = lv_dropdown_get_selected(ctx->modeDropdown);
|
||||
if (selected == 0) { // Timer
|
||||
lv_obj_remove_flag(ctx->timerWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
} else {
|
||||
lv_obj_add_flag(ctx->timerWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
const uint32_t chip_count = lv_obj_get_child_count(ctx->modeChips);
|
||||
for (uint32_t i = 0; i < chip_count; i++) {
|
||||
lv_obj_set_state(lv_obj_get_child(ctx->modeChips, static_cast<int32_t>(i)), LV_STATE_CHECKED, MODE_OPTIONS[i].mode == ctx->mode);
|
||||
}
|
||||
lv_obj_set_hidden(ctx->delayRow, ctx->mode != CaptureMode::Timer);
|
||||
}
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
@ -85,7 +104,6 @@ void onStartPressed(lv_event_t* event) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t selected = lv_dropdown_get_selected(ctx->modeDropdown);
|
||||
const char* path = lv_textarea_get_text(ctx->pathTextArea);
|
||||
|
||||
error_t result = directory_make(path, true);
|
||||
@ -94,7 +112,7 @@ void onStartPressed(lv_event_t* event) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected == 0) {
|
||||
if (ctx->mode == CaptureMode::Timer) {
|
||||
LOG_I(TAG, "Start timed screenshots");
|
||||
const char* delay_text = lv_textarea_get_text(ctx->delayTextArea);
|
||||
int delay = atoi(delay_text);
|
||||
@ -111,59 +129,62 @@ void onStartPressed(lv_event_t* event) {
|
||||
updateScreenshotMode(ctx);
|
||||
}
|
||||
|
||||
void onModeSet(lv_event_t* event) {
|
||||
void onModeChipPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
ctx->mode = MODE_OPTIONS[lv_obj_get_index(lv_event_get_target_obj(event))].mode;
|
||||
updateScreenshotMode(ctx);
|
||||
}
|
||||
|
||||
void createModeSettingWidgets(Context* ctx, lv_obj_t* parent) {
|
||||
auto service = service::screenshot::optScreenshotService();
|
||||
if (service == nullptr) {
|
||||
LOG_E(TAG, "Service not found/running");
|
||||
return;
|
||||
}
|
||||
|
||||
auto* mode_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_size(mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(mode_wrapper, 0, 0);
|
||||
lv_obj_set_style_border_width(mode_wrapper, 0, 0);
|
||||
|
||||
auto* mode_label = lv_label_create(mode_wrapper);
|
||||
lv_label_set_text(mode_label, "Mode:");
|
||||
lv_obj_align(mode_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
ctx->modeDropdown = lv_dropdown_create(mode_wrapper);
|
||||
lv_dropdown_set_options(ctx->modeDropdown, "Timer\nApp start");
|
||||
lv_obj_align_to(ctx->modeDropdown, mode_label, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
|
||||
lv_obj_add_event_cb(ctx->modeDropdown, onModeSet, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
service::screenshot::Mode mode = service->getMode();
|
||||
if (mode == service::screenshot::Mode::Apps) {
|
||||
lv_dropdown_set_selected(ctx->modeDropdown, 1);
|
||||
}
|
||||
|
||||
auto* button = lv_button_create(mode_wrapper);
|
||||
lv_obj_align(button, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(button, onStartPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
ctx->startStopButtonLabel = lv_label_create(button);
|
||||
lv_obj_align(ctx->startStopButtonLabel, LV_ALIGN_CENTER, 0, 0);
|
||||
/** A transparent row in a card: "Title [content]" */
|
||||
lv_obj_t* createRow(lv_obj_t* card, const char* title) {
|
||||
auto* row = lv_obj_create(card);
|
||||
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(row, false);
|
||||
lv_label_set_text(lv_label_create(row), title);
|
||||
return row;
|
||||
}
|
||||
|
||||
void createFilePathWidgets(Context* ctx, lv_obj_t* parent) {
|
||||
auto* path_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_size(path_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(path_wrapper, 0, 0);
|
||||
lv_obj_set_style_border_width(path_wrapper, 0, 0);
|
||||
lv_obj_set_flex_flow(path_wrapper, LV_FLEX_FLOW_ROW);
|
||||
void createModeWidgets(Context* ctx, lv_obj_t* parent) {
|
||||
auto* title = lv_label_create(parent);
|
||||
lv_label_set_text(title, "Mode");
|
||||
lv_obj_set_width(title, LV_PCT(100));
|
||||
|
||||
auto* label_wrapper = lv_obj_create(path_wrapper);
|
||||
lv_obj_set_style_border_width(label_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(label_wrapper, 0, 0);
|
||||
lv_obj_set_size(label_wrapper, 44, 36);
|
||||
auto* path_label = lv_label_create(label_wrapper);
|
||||
lv_label_set_text(path_label, "Path:");
|
||||
lv_obj_align(path_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
// The chips are centered in the card while they fit, and scroll when they don't
|
||||
auto* card = lvgl_card_create(parent);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(card, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
ctx->pathTextArea = lv_textarea_create(path_wrapper);
|
||||
ctx->modeChips = lv_obj_create(card);
|
||||
lv_obj_set_size(ctx->modeChips, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_max_width(ctx->modeChips, LV_PCT(100), LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(ctx->modeChips, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_scroll_dir(ctx->modeChips, LV_DIR_HOR);
|
||||
lv_obj_set_style_bg_opa(ctx->modeChips, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ctx->modeChips, 0, LV_STATE_DEFAULT);
|
||||
// The chips' margins leave room for their focus rings and space them apart
|
||||
lv_obj_set_style_pad_all(ctx->modeChips, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_column(ctx->modeChips, 0, LV_STATE_DEFAULT);
|
||||
for (const auto& option : MODE_OPTIONS) {
|
||||
auto* chip = lvgl_chip_create(ctx->modeChips);
|
||||
lv_label_set_text(lv_label_create(chip), option.name);
|
||||
lv_obj_add_event_cb(chip, onModeChipPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
}
|
||||
lvgl_grid_navigation_add(ctx->modeChips);
|
||||
}
|
||||
|
||||
void createSettingsWidgets(Context* ctx, lv_obj_t* parent) {
|
||||
auto* card = lvgl_card_create(parent);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
auto* path_row = createRow(card, "Path");
|
||||
ctx->pathTextArea = lv_textarea_create(path_row);
|
||||
lv_textarea_set_one_line(ctx->pathTextArea, true);
|
||||
lv_obj_set_flex_grow(ctx->pathTextArea, 1);
|
||||
char data_path[FILE_MAX_PATH_STRING_LENGTH];
|
||||
@ -173,41 +194,20 @@ void createFilePathWidgets(Context* ctx, lv_obj_t* parent) {
|
||||
} else {
|
||||
lv_textarea_set_text(ctx->pathTextArea, "Error: no data path");
|
||||
}
|
||||
}
|
||||
|
||||
void createTimerSettingsWidgets(Context* ctx, lv_obj_t* parent) {
|
||||
ctx->timerWrapper = lv_obj_create(parent);
|
||||
lv_obj_set_size(ctx->timerWrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(ctx->timerWrapper, 0, 0);
|
||||
lv_obj_set_style_border_width(ctx->timerWrapper, 0, 0);
|
||||
|
||||
auto* delay_wrapper = lv_obj_create(ctx->timerWrapper);
|
||||
lv_obj_set_size(delay_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(delay_wrapper, 0, 0);
|
||||
lv_obj_set_style_border_width(delay_wrapper, 0, 0);
|
||||
lv_obj_set_flex_flow(delay_wrapper, LV_FLEX_FLOW_ROW);
|
||||
|
||||
auto* delay_label_wrapper = lv_obj_create(delay_wrapper);
|
||||
lv_obj_set_style_border_width(delay_label_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(delay_label_wrapper, 0, 0);
|
||||
lv_obj_set_size(delay_label_wrapper, 44, 36);
|
||||
auto* delay_label = lv_label_create(delay_label_wrapper);
|
||||
lv_label_set_text(delay_label, "Delay:");
|
||||
lv_obj_align(delay_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
ctx->delayTextArea = lv_textarea_create(delay_wrapper);
|
||||
ctx->delayRow = createRow(card, "Delay");
|
||||
ctx->delayTextArea = lv_textarea_create(ctx->delayRow);
|
||||
lv_textarea_set_one_line(ctx->delayTextArea, true);
|
||||
lv_textarea_set_accepted_chars(ctx->delayTextArea, "0123456789");
|
||||
lv_textarea_set_text(ctx->delayTextArea, "10");
|
||||
lv_obj_set_flex_grow(ctx->delayTextArea, 1);
|
||||
lv_label_set_text(lv_label_create(ctx->delayRow), "seconds");
|
||||
}
|
||||
|
||||
auto* delay_unit_label_wrapper = lv_obj_create(delay_wrapper);
|
||||
lv_obj_set_style_border_width(delay_unit_label_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(delay_unit_label_wrapper, 0, 0);
|
||||
lv_obj_set_size(delay_unit_label_wrapper, LV_SIZE_CONTENT, 36);
|
||||
auto* delay_unit_label = lv_label_create(delay_unit_label_wrapper);
|
||||
lv_obj_align(delay_unit_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_label_set_text(delay_unit_label, "seconds");
|
||||
void createStartStopButton(Context* ctx, lv_obj_t* parent) {
|
||||
auto* button = lv_button_create(parent);
|
||||
lv_obj_add_event_cb(button, onStartPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
ctx->startStopButtonLabel = lv_label_create(button);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
@ -230,10 +230,17 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
// The cards stretch, the start/stop button is centered
|
||||
lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START);
|
||||
|
||||
createModeSettingWidgets(ctx, wrapper);
|
||||
createFilePathWidgets(ctx, wrapper);
|
||||
createTimerSettingsWidgets(ctx, wrapper);
|
||||
auto service = service::screenshot::optScreenshotService();
|
||||
if (service != nullptr && service->getMode() == service::screenshot::Mode::Apps) {
|
||||
ctx->mode = CaptureMode::AppStart;
|
||||
}
|
||||
|
||||
createModeWidgets(ctx, wrapper);
|
||||
createSettingsWidgets(ctx, wrapper);
|
||||
createStartStopButton(ctx, wrapper);
|
||||
|
||||
updateScreenshotMode(ctx);
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
namespace tt::app::selectiondialog {
|
||||
@ -52,7 +53,7 @@ void onItemSelected(lv_event_t* e) {
|
||||
}
|
||||
|
||||
void createChoiceItem(Context* ctx, lv_obj_t* list, const std::string& title, int32_t index) {
|
||||
lv_obj_t* btn = lv_list_add_button(list, nullptr, title.c_str());
|
||||
lv_obj_t* btn = lvgl_list_add_button(list, nullptr, title.c_str());
|
||||
auto* itemCtx = new ItemContext { ctx, index };
|
||||
lv_obj_add_event_cb(btn, onItemSelected, LV_EVENT_SHORT_CLICKED, itemCtx);
|
||||
lv_obj_add_event_cb(btn, onItemDeleted, LV_EVENT_DELETE, itemCtx);
|
||||
@ -78,7 +79,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
const char* title = (argv[0][0] != '\0') ? argv[0] : DEFAULT_TITLE;
|
||||
lvgl_toolbar_create(parent, title);
|
||||
|
||||
auto* list = lv_list_create(parent);
|
||||
auto* list = lvgl_list_create(parent);
|
||||
lv_obj_set_width(list, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(list, 1);
|
||||
|
||||
|
||||
@ -1,163 +0,0 @@
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
#include <app/start.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <Tactility/app/AppGrid.h>
|
||||
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/theme.h>
|
||||
#include <tactility/check.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app::settings {
|
||||
|
||||
namespace {
|
||||
|
||||
struct IconEntry {
|
||||
const char* id;
|
||||
const char* icon;
|
||||
};
|
||||
|
||||
constexpr IconEntry ICONS[] = {
|
||||
{"tactility.appearance", LVGL_ICON_SHARED_PALETTE},
|
||||
{"tactility.apppackagelist", LVGL_ICON_SHARED_DEPLOYED_CODE},
|
||||
{"tactility.audiosettings", LVGL_ICON_SHARED_MUSIC_NOTE},
|
||||
{"tactility.btmanage", LVGL_ICON_SHARED_BLUETOOTH},
|
||||
{"tactility.development", LVGL_ICON_SHARED_LOGO_DEV},
|
||||
{"tactility.display", LVGL_ICON_SHARED_DISPLAY_SETTINGS},
|
||||
{"tactility.gpssettings", LVGL_ICON_SHARED_NAVIGATION},
|
||||
{"tactility.grovesettings", LVGL_ICON_SHARED_CABLE},
|
||||
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
|
||||
{"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP},
|
||||
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
|
||||
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},
|
||||
{"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH},
|
||||
{"tactility.touchcalibration", LVGL_ICON_SHARED_CIRCLE},
|
||||
{"tactility.trackballsettings", LVGL_ICON_SHARED_DEVICES},
|
||||
{"tactility.usbsettings", LVGL_ICON_SHARED_USB},
|
||||
{"tactility.wifimanage", LVGL_ICON_SHARED_WIFI},
|
||||
};
|
||||
|
||||
const char* appIcon(const ::AppManifest* manifest) {
|
||||
for (const auto& entry : ICONS) {
|
||||
if (!strcmp(manifest->id, entry.id)) return entry.icon;
|
||||
}
|
||||
return LVGL_ICON_SHARED_DEPLOYED_CODE;
|
||||
}
|
||||
|
||||
void onAppClicked(const ::AppManifest& manifest, void*) {
|
||||
// Fire-and-forget top-level navigation, same as the Launcher's own app-launch buttons.
|
||||
uint32_t instanceId = 0;
|
||||
// Re-resolved by id against the live ledger, not the (possibly stale) cached manifest above:
|
||||
// fails gracefully if the app was uninstalled since this button was built, instead of handing
|
||||
// app_manager_start_internal() a pointer it would store for the new instance's whole lifetime.
|
||||
AppStartContext context;
|
||||
if (app_start_context_from_id(manifest.id, &context) == ERROR_NONE) {
|
||||
app_start_with_context(&context, &instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
void collectManifest(const ::AppManifest* manifest, void* context) {
|
||||
auto* manifests = static_cast<std::vector<::AppManifest>*>(context);
|
||||
manifests->push_back(*manifest);
|
||||
}
|
||||
|
||||
std::vector<AppGridItem> collectItems(void*) {
|
||||
std::vector<::AppManifest> collected;
|
||||
app_manager_for_each_manifest(collectManifest, &collected);
|
||||
std::ranges::sort(collected, [](const ::AppManifest& a, const ::AppManifest& b) {
|
||||
return strcmp(a.name, b.name) < 0;
|
||||
});
|
||||
|
||||
std::vector<AppGridItem> items;
|
||||
for (const auto& manifest : collected) {
|
||||
if (manifest.category == APP_CATEGORY_SETTINGS && (manifest.flags & APP_MANIFEST_FLAG_HIDDEN) == 0) {
|
||||
items.push_back({ manifest, appIcon(&manifest), false });
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
AppGrid grid { AppGrid::Callbacks {
|
||||
.collect = collectItems,
|
||||
.onClicked = onAppClicked,
|
||||
.onLongPressed = nullptr,
|
||||
.onKey = nullptr,
|
||||
.userData = nullptr
|
||||
} };
|
||||
};
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
if (!lvgl_theme_is_mono()) {
|
||||
ctx->grid.setIconColor(AppGrid::IconColor::Secondary);
|
||||
}
|
||||
ctx->grid.setSwipeNavigation(true);
|
||||
ctx->grid.createWidgetsWithBottomBar(parent);
|
||||
ctx->grid.addBarButton(LVGL_ICON_SHARED_CLOSE, onBackPressed, ctx);
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
uint32_t appInstanceId = app_scheduler_current_app_id();
|
||||
Context ctx { appInstanceId };
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
AppEventSubscription sub {};
|
||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||
|
||||
while (true) {
|
||||
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||
|
||||
bool shouldClose = false;
|
||||
AppEvent event {};
|
||||
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||
if (event.type == APP_EVENT_CLOSE) {
|
||||
shouldClose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldClose) break;
|
||||
}
|
||||
|
||||
window_manager_remove(window);
|
||||
check(app_event_unsubscribe(&sub) == ERROR_NONE);
|
||||
task_event_group_destruct(&event_group);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern const ::AppManifest manifest = {
|
||||
.id = "tactility.settings",
|
||||
.name = "Settings",
|
||||
.category = APP_CATEGORY_SYSTEM,
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
|
||||
.flags = APP_MANIFEST_FLAG_HIDDEN,
|
||||
.stack = { .depth = 5120, .desired_memory_capability = 0 },
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@ -107,7 +107,7 @@ void renderCurrent(Context* ctx) {
|
||||
lv_label_set_text(ctx->titleLabel, "Welcome");
|
||||
auto device_names = string::split(std::string(CONFIG_TT_DEVICE_NAME_SIMPLE), ",");
|
||||
lv_label_set_text_fmt(ctx->descriptionLabel, "It's time to set up your %s!", device_names.front().c_str());
|
||||
lv_obj_add_flag(ctx->skipButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->skipButton, true);
|
||||
lv_label_set_text(lv_obj_get_child(ctx->continueButton, 0), "Continue");
|
||||
break;
|
||||
}
|
||||
@ -115,7 +115,7 @@ void renderCurrent(Context* ctx) {
|
||||
const auto& step = ctx->steps[ctx->stepIndex];
|
||||
lv_label_set_text(ctx->titleLabel, step.title.c_str());
|
||||
lv_label_set_text(ctx->descriptionLabel, step.description.c_str());
|
||||
lv_obj_remove_flag(ctx->skipButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->skipButton, false);
|
||||
lv_label_set_text(lv_obj_get_child(ctx->skipButton, 0), "Skip");
|
||||
lv_label_set_text(lv_obj_get_child(ctx->continueButton, 0), "Continue");
|
||||
break;
|
||||
@ -123,7 +123,7 @@ void renderCurrent(Context* ctx) {
|
||||
case Phase::Done:
|
||||
lv_label_set_text(ctx->titleLabel, "Setup Complete");
|
||||
lv_label_set_text(ctx->descriptionLabel, "You're all set.");
|
||||
lv_obj_add_flag(ctx->skipButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->skipButton, true);
|
||||
lv_label_set_text(lv_obj_get_child(ctx->continueButton, 0), "Finish");
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
#include <tactility/check.h>
|
||||
#include "lvgl/icons/shared.h"
|
||||
#include "lvgl/theme.h"
|
||||
|
||||
|
||||
#include "tactility/time.h"
|
||||
#include <tactility/check.h>
|
||||
|
||||
#include <Tactility/DeprecatedPaths.h>
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/TactilityConfig.h>
|
||||
#include <Tactility/Timer.h>
|
||||
|
||||
@ -14,11 +17,14 @@
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <strings.h>
|
||||
#include <format>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
@ -197,40 +203,81 @@ const char* getTaskState(const TaskStatus_t& task) {
|
||||
}
|
||||
}
|
||||
|
||||
void clearContainer(lv_obj_t* container) {
|
||||
lv_obj_clean(container);
|
||||
const char* getTaskName(const TaskStatus_t& task) {
|
||||
return (task.pcTaskName == nullptr || task.pcTaskName[0] == 0) ? "(unnamed)" : task.pcTaskName;
|
||||
}
|
||||
|
||||
void addRtosTask(lv_obj_t* parent, const TaskStatus_t& task) {
|
||||
auto* label = lv_label_create(parent);
|
||||
const char* name = (task.pcTaskName == nullptr || task.pcTaskName[0] == 0) ? "(unnamed)" : task.pcTaskName;
|
||||
lv_label_set_text_fmt(label, "%s (%s)", name, getTaskState(task));
|
||||
}
|
||||
|
||||
void updateRtosTasks(lv_obj_t* parent) {
|
||||
clearContainer(parent);
|
||||
/**
|
||||
* Updates the rows in place, so the list keeps its height and scroll position, and the selected task stays selected.
|
||||
* @param[in] parent the list
|
||||
* @param[in,out] taskNames the task name of each row
|
||||
*/
|
||||
void updateRtosTasks(lv_obj_t* parent, std::vector<std::string>& taskNames) {
|
||||
lv_obj_t* selected = lvgl_grid_navigation_get_focused(parent);
|
||||
const int32_t selected_index = selected != nullptr ? lv_obj_get_index(selected) : -1;
|
||||
const std::string selected_name = selected_index >= 0 && selected_index < static_cast<int32_t>(taskNames.size()) ? taskNames[selected_index] : "";
|
||||
const bool show_selection = selected != nullptr && lv_obj_has_state(selected, LV_STATE_FOCUS_KEY);
|
||||
|
||||
UBaseType_t count = uxTaskGetNumberOfTasks();
|
||||
auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count);
|
||||
if (!tasks) {
|
||||
lv_obj_clean(parent);
|
||||
taskNames.clear();
|
||||
auto* error_label = lv_label_create(parent);
|
||||
lv_label_set_text(error_label, "Failed to allocate memory for task list");
|
||||
return;
|
||||
}
|
||||
uint32_t totalRuntime = 0;
|
||||
UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime);
|
||||
std::sort(tasks, tasks + actual, [](const TaskStatus_t& a, const TaskStatus_t& b) {
|
||||
return strcasecmp(getTaskName(a), getTaskName(b)) < 0;
|
||||
});
|
||||
|
||||
for (int i = 0; i < actual; ++i) {
|
||||
addRtosTask(parent, tasks[i]);
|
||||
taskNames.clear();
|
||||
for (UBaseType_t i = 0; i < actual; ++i) {
|
||||
auto* row = i < lv_obj_get_child_count(parent) ? lv_obj_get_child(parent, static_cast<int32_t>(i)) : lv_label_create(parent);
|
||||
lv_label_set_text_fmt(row, "%s (%s)", getTaskName(tasks[i]), getTaskState(tasks[i]));
|
||||
// Keys and encoders move through the tasks, which scrolls the list
|
||||
lv_obj_set_clickable(row, true);
|
||||
taskNames.emplace_back(getTaskName(tasks[i]));
|
||||
}
|
||||
free(tasks);
|
||||
while (lv_obj_get_child_count(parent) > actual) {
|
||||
lv_obj_delete(lv_obj_get_child(parent, -1));
|
||||
}
|
||||
|
||||
free(tasks);
|
||||
if (selected_index < 0 || taskNames.empty()) {
|
||||
return;
|
||||
}
|
||||
// Follows the task, or keeps the position when the task is gone
|
||||
auto it = std::ranges::find(taskNames, selected_name);
|
||||
const auto index = it != taskNames.end() ? static_cast<int32_t>(it - taskNames.begin()) : std::min(selected_index, static_cast<int32_t>(taskNames.size()) - 1);
|
||||
// Also re-applied when the row didn't change: deleting rows resets grid navigation's selection
|
||||
auto* row = lv_obj_get_child(parent, index);
|
||||
lv_gridnav_set_focused(parent, row, LV_ANIM_OFF);
|
||||
const bool list_focused = lv_obj_get_group(parent) != nullptr && lv_group_get_focused(lv_obj_get_group(parent)) == parent;
|
||||
if (!show_selection) {
|
||||
lv_obj_remove_state(row, LV_STATE_FOCUS_KEY);
|
||||
}
|
||||
if (list_focused) {
|
||||
lv_obj_scroll_to_view_recursive(row, LV_ANIM_OFF);
|
||||
} else {
|
||||
lv_obj_remove_state(row, LV_STATE_FOCUSED);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
lv_obj_t* createTab(lv_obj_t* tabview, const char* name) {
|
||||
auto* tab = lv_tabview_add_tab(tabview, name);
|
||||
auto* tab_bar = lv_tabview_get_tab_bar(tabview);
|
||||
auto* tab_button = lv_obj_get_child(tab_bar, static_cast<int32_t>(lv_obj_get_child_count(tab_bar)) - 1);
|
||||
auto* shared_icon_font = lvgl_theme_is_compact() ? lvgl_get_shared_icon_default_font() : lvgl_get_shared_icon_large_font();
|
||||
lv_obj_set_style_text_font(tab_button, shared_icon_font, LV_STATE_DEFAULT);
|
||||
// Square buttons, so the theme's round selection indicator fits the icon
|
||||
const int32_t button_size = lv_font_get_line_height(shared_icon_font) + 2 * lv_obj_get_style_pad_top(tab_button, LV_PART_MAIN);
|
||||
lv_obj_set_flex_grow(tab_button, 0);
|
||||
lv_obj_set_size(tab_button, button_size, button_size);
|
||||
lv_obj_set_flex_flow(tab, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(tab, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(tab, 0, LV_STATE_DEFAULT);
|
||||
@ -250,6 +297,8 @@ struct Context {
|
||||
MemoryBarWidgets systemStorageBar;
|
||||
|
||||
lv_obj_t* tasksContainer = nullptr;
|
||||
/** The task name of each row in tasksContainer */
|
||||
std::vector<std::string> taskNames;
|
||||
lv_obj_t* psramContainer = nullptr;
|
||||
|
||||
bool hasExternalMem = false;
|
||||
@ -293,7 +342,7 @@ void updateStorage(Context* ctx) {
|
||||
void updateTasks(Context* ctx) {
|
||||
#if configUSE_TRACE_FACILITY
|
||||
if (ctx->tasksContainer) {
|
||||
updateRtosTasks(ctx->tasksContainer); // Tasks tab: show state
|
||||
updateRtosTasks(ctx->tasksContainer, ctx->taskNames); // Tasks tab: show state
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@ -303,6 +352,24 @@ void onBackPressed(lv_event_t* event) {
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
#if configUSE_TRACE_FACILITY
|
||||
void onTabChanged(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* tabview = lv_event_get_target_obj(event);
|
||||
auto* active_tab = lv_obj_get_child(lv_tabview_get_content(tabview), static_cast<int32_t>(lv_tabview_get_tab_active(tabview)));
|
||||
const bool tasks_shown = active_tab == lv_obj_get_parent(ctx->tasksContainer);
|
||||
auto* group = lv_group_get_default();
|
||||
if (group == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (tasks_shown && lv_obj_get_group(ctx->tasksContainer) == nullptr) {
|
||||
lv_group_add_obj(group, ctx->tasksContainer);
|
||||
} else if (!tasks_shown && lv_obj_get_group(ctx->tasksContainer) != nullptr) {
|
||||
lv_group_remove_obj(ctx->tasksContainer);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
@ -319,17 +386,25 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_style_pad_all(wrapper, 0, LV_STATE_DEFAULT);
|
||||
// Space between the screen edges and the rounded tab bar and content
|
||||
const int32_t edge_space = lvgl_theme_is_compact() ? 2 : LV_DPX(8);
|
||||
lv_obj_set_style_pad_hor(wrapper, edge_space, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_bottom(wrapper, edge_space, LV_STATE_DEFAULT);
|
||||
|
||||
auto* tabview = lv_tabview_create(wrapper);
|
||||
lv_tabview_set_tab_bar_position(tabview, LV_DIR_LEFT);
|
||||
auto tab_bar_width = 6 * lvgl_get_text_font_height(FONT_SIZE_DEFAULT);
|
||||
lv_tabview_set_tab_bar_size(tabview, tab_bar_width);
|
||||
|
||||
// Create tabs
|
||||
auto* memory_tab = createTab(tabview, "Memory");
|
||||
auto* storage_tab = createTab(tabview, "Storage");
|
||||
auto* tasks_tab = createTab(tabview, "Tasks");
|
||||
auto* about_tab = createTab(tabview, "About");
|
||||
auto* memory_tab = createTab(tabview, LVGL_ICON_SHARED_MEMORY);
|
||||
auto* storage_tab = createTab(tabview, LVGL_ICON_SHARED_HARD_DISK);
|
||||
auto* tasks_tab = createTab(tabview, LVGL_ICON_SHARED_SELECT_WINDOW_2);
|
||||
auto* about_tab = createTab(tabview, LVGL_ICON_SHARED_INFO);
|
||||
|
||||
// As wide as the buttons, which are spread over the bar's height
|
||||
auto* tab_bar = lv_tabview_get_tab_bar(tabview);
|
||||
lv_obj_set_width(tab_bar, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_hor(tab_bar, lvgl_theme_is_compact() ? 2 : LV_DPX(12), LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_align(tab_bar, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
// Memory tab content
|
||||
ctx->internalMemBar = createMemoryBar(memory_tab, "Internal");
|
||||
@ -370,6 +445,10 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_style_border_width(ctx->tasksContainer, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(ctx->tasksContainer, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_bg_opa(ctx->tasksContainer, 0, LV_STATE_DEFAULT);
|
||||
lvgl_grid_navigation_add(ctx->tasksContainer);
|
||||
// Only the shown tab's list can be focused, as focusing another tab's content would scroll to it
|
||||
lv_group_remove_obj(ctx->tasksContainer);
|
||||
lv_obj_add_event_cb(tabview, onTabChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
#endif
|
||||
|
||||
// Build info
|
||||
|
||||
@ -42,7 +42,7 @@ void onClosePressed(lv_event_t* event) {
|
||||
|
||||
void onCanvasClicked(lv_event_t* event) {
|
||||
auto* toolbar = static_cast<lv_obj_t*>(lv_event_get_user_data(event));
|
||||
lv_obj_set_flag(toolbar, LV_OBJ_FLAG_HIDDEN, !lv_obj_has_flag(toolbar, LV_OBJ_FLAG_HIDDEN));
|
||||
lv_obj_set_hidden(toolbar, !lv_obj_is_hidden(toolbar));
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* root, void* userData) {
|
||||
@ -60,10 +60,10 @@ void createWidgets(lv_obj_t* root, void* userData) {
|
||||
if (lvgl_indev_exists(LV_INDEV_TYPE_POINTER)) {
|
||||
auto* toolbar = lvgl_toolbar_create(root, "Terminal");
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onClosePressed, ctx);
|
||||
lv_obj_add_flag(toolbar, LV_OBJ_FLAG_FLOATING);
|
||||
lv_obj_set_floating(toolbar, true);
|
||||
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
lv_obj_add_flag(toolbar, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(canvas, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_hidden(toolbar, true);
|
||||
lv_obj_set_clickable(canvas, true);
|
||||
lv_obj_add_event_cb(canvas, onCanvasClicked, LV_EVENT_SHORT_CLICKED, toolbar);
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
#include <Tactility/app/selectiondialog/SelectionDialog.h>
|
||||
#include <Tactility/app/timedatesettings/TimeDateSettings.h>
|
||||
#include <Tactility/app/timezone/TimeZone.h>
|
||||
#include <Tactility/settings/SystemSettings.h>
|
||||
@ -12,11 +13,17 @@
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app::timedatesettings {
|
||||
|
||||
constexpr auto* TAG = "TimeDate";
|
||||
@ -25,13 +32,31 @@ extern const ::AppManifest manifest;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* DATE_FORMATS[] = { "MM/DD/YYYY", "DD/MM/YYYY", "YYYY-MM-DD", "YYYY/MM/DD" };
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
lv_obj_t* timeZoneLabel = nullptr;
|
||||
lv_obj_t* dateFormatDropdown = nullptr;
|
||||
TaskEventGroup* eventGroup = nullptr;
|
||||
uint32_t selectDateFormatBit = 0;
|
||||
uint32_t pendingTimeZoneDialogId = 0;
|
||||
uint32_t dateFormatDialogId = 0;
|
||||
// Valid while the window is shown
|
||||
lv_obj_t* timeZoneLabel = nullptr;
|
||||
lv_obj_t* dateFormatLabel = nullptr;
|
||||
};
|
||||
|
||||
std::string getDateFormat() {
|
||||
settings::SystemSettings sysSettings;
|
||||
if (settings::loadSystemSettings(sysSettings)) {
|
||||
for (const auto* format : DATE_FORMATS) {
|
||||
if (sysSettings.dateFormat == format) {
|
||||
return format;
|
||||
}
|
||||
}
|
||||
}
|
||||
return DATE_FORMATS[0];
|
||||
}
|
||||
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
@ -49,18 +74,63 @@ void onTimeZonePressed(lv_event_t* event) {
|
||||
ctx->pendingTimeZoneDialogId = timezone::start(ctx->appInstanceId, true);
|
||||
}
|
||||
|
||||
void onDateFormatChanged(lv_event_t* event) {
|
||||
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
auto index = lv_dropdown_get_selected(dropdown);
|
||||
void onSelectDateFormatPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
task_event_group_signal(ctx->eventGroup, ctx->selectDateFormatBit);
|
||||
}
|
||||
|
||||
const char* dateFormats[] = {"MM/DD/YYYY", "DD/MM/YYYY", "YYYY-MM-DD", "YYYY/MM/DD"};
|
||||
std::string selected_format = dateFormats[index];
|
||||
|
||||
settings::SystemSettings sysSettings;
|
||||
if (settings::loadSystemSettings(sysSettings)) {
|
||||
sysSettings.dateFormat = selected_format;
|
||||
settings::saveSystemSettings(sysSettings);
|
||||
void startDateFormatSelection(Context* ctx) {
|
||||
if (ctx->dateFormatDialogId != 0) {
|
||||
return;
|
||||
}
|
||||
const std::vector<std::string> items(std::begin(DATE_FORMATS), std::end(DATE_FORMATS));
|
||||
ctx->dateFormatDialogId = selectiondialog::start(ctx->appInstanceId, "Date format", items);
|
||||
}
|
||||
|
||||
void onDateFormatSelectionResult(Context* ctx, const AppEvent& event) {
|
||||
ctx->dateFormatDialogId = 0;
|
||||
// Other results mean that the dialog was dismissed
|
||||
const int32_t selected = event.result.result;
|
||||
if (selected >= 0 && selected < static_cast<int32_t>(std::size(DATE_FORMATS))) {
|
||||
settings::SystemSettings sysSettings;
|
||||
if (settings::loadSystemSettings(sysSettings)) {
|
||||
sysSettings.dateFormat = DATE_FORMATS[selected];
|
||||
settings::saveSystemSettings(sysSettings);
|
||||
}
|
||||
lvgl_lock();
|
||||
if (ctx->dateFormatLabel != nullptr) {
|
||||
lv_label_set_text(ctx->dateFormatLabel, getDateFormat().c_str());
|
||||
}
|
||||
lvgl_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a title with the setting's value below it, which take the width that the row's button leaves.
|
||||
* Texts that don't fit scroll.
|
||||
* @return the value label
|
||||
*/
|
||||
lv_obj_t* createTitleAndValue(lv_obj_t* row, const char* title) {
|
||||
auto* texts = lv_obj_create(row);
|
||||
lv_obj_set_height(texts, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_grow(texts, 1);
|
||||
lv_obj_set_flex_flow(texts, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_all(texts, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_row(texts, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(texts, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(texts, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(texts, false);
|
||||
|
||||
auto* title_label = lv_label_create(texts);
|
||||
lv_obj_set_width(title_label, LV_PCT(100));
|
||||
lv_label_set_long_mode(title_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
lv_label_set_text(title_label, title);
|
||||
|
||||
auto* value_label = lv_label_create(texts);
|
||||
lv_obj_set_width(value_label, LV_PCT(100));
|
||||
lv_obj_set_style_text_font(value_label, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
||||
lv_label_set_long_mode(value_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
return value_label;
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
@ -73,24 +143,32 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
|
||||
auto* main_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(main_wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(main_wrapper, 1);
|
||||
|
||||
// The rows are transparent, so the card provides the background
|
||||
auto* card = lvgl_card_create(main_wrapper);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// 24-hour format toggle
|
||||
|
||||
auto* time_format_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_width(time_format_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(time_format_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(time_format_wrapper, 8, 0);
|
||||
lv_obj_set_style_border_width(time_format_wrapper, 0, 0);
|
||||
auto* time_format_row = lv_obj_create(card);
|
||||
lv_obj_set_size(time_format_row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(time_format_row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(time_format_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_border_width(time_format_row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_all(time_format_row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(time_format_row, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(time_format_row, false);
|
||||
|
||||
auto* time_24h_label = lv_label_create(time_format_wrapper);
|
||||
auto* time_24h_label = lv_label_create(time_format_row);
|
||||
lv_label_set_text(time_24h_label, "24-hour format");
|
||||
lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
lv_obj_set_flex_grow(time_24h_label, 1);
|
||||
|
||||
auto* time_24h_switch = lv_switch_create(time_format_wrapper);
|
||||
lv_obj_align(time_24h_switch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
auto* time_24h_switch = lv_switch_create(time_format_row);
|
||||
lv_obj_add_event_cb(time_24h_switch, onTimeFormatChanged, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
if (settings::isTimeFormat24Hour()) {
|
||||
lv_obj_add_state(time_24h_switch, LV_STATE_CHECKED);
|
||||
@ -98,57 +176,51 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_remove_state(time_24h_switch, LV_STATE_CHECKED);
|
||||
}
|
||||
|
||||
// Date format dropdown
|
||||
// "Date format MM/DD/YYYY [Select]"
|
||||
|
||||
auto* date_format_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_width(date_format_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(date_format_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(date_format_wrapper, 8, 0);
|
||||
lv_obj_set_style_border_width(date_format_wrapper, 0, 0);
|
||||
auto* date_format_row = lv_obj_create(card);
|
||||
lv_obj_set_size(date_format_row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(date_format_row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(date_format_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_border_width(date_format_row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_all(date_format_row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(date_format_row, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(date_format_row, false);
|
||||
|
||||
auto* date_format_label = lv_label_create(date_format_wrapper);
|
||||
lv_label_set_text(date_format_label, "Date format");
|
||||
lv_obj_align(date_format_label, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
ctx->dateFormatLabel = createTitleAndValue(date_format_row, "Date format");
|
||||
lv_label_set_text(ctx->dateFormatLabel, getDateFormat().c_str());
|
||||
|
||||
ctx->dateFormatDropdown = lv_dropdown_create(date_format_wrapper);
|
||||
lv_obj_set_width(ctx->dateFormatDropdown, 150);
|
||||
lv_obj_align(ctx->dateFormatDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_dropdown_set_options(ctx->dateFormatDropdown, "MM/DD/YYYY\nDD/MM/YYYY\nYYYY-MM-DD\nYYYY/MM/DD");
|
||||
auto* date_format_button = lv_button_create(date_format_row);
|
||||
lv_label_set_text(lv_label_create(date_format_button), "Change");
|
||||
lv_obj_add_event_cb(date_format_button, onSelectDateFormatPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
|
||||
settings::SystemSettings sysSettings;
|
||||
if (settings::loadSystemSettings(sysSettings)) {
|
||||
int index = 0;
|
||||
if (sysSettings.dateFormat == "DD/MM/YYYY") index = 1;
|
||||
else if (sysSettings.dateFormat == "YYYY-MM-DD") index = 2;
|
||||
else if (sysSettings.dateFormat == "YYYY/MM/DD") index = 3;
|
||||
lv_dropdown_set_selected(ctx->dateFormatDropdown, index);
|
||||
}
|
||||
lv_obj_add_event_cb(ctx->dateFormatDropdown, onDateFormatChanged, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
// "Timezone Europe/Brussels [Change]"
|
||||
|
||||
// Timezone selector
|
||||
auto* timezone_row = lv_obj_create(card);
|
||||
lv_obj_set_size(timezone_row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(timezone_row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(timezone_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_border_width(timezone_row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_all(timezone_row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(timezone_row, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(timezone_row, false);
|
||||
|
||||
auto* timezone_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_width(timezone_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(timezone_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(timezone_wrapper, 8, 0);
|
||||
lv_obj_set_style_border_width(timezone_wrapper, 0, 0);
|
||||
|
||||
auto* timezone_label = lv_label_create(timezone_wrapper);
|
||||
lv_label_set_text(timezone_label, "Timezone");
|
||||
lv_obj_align(timezone_label, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
|
||||
auto* timezone_button = lv_button_create(timezone_wrapper);
|
||||
lv_obj_set_width(timezone_button, 150);
|
||||
lv_obj_align(timezone_button, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(timezone_button, onTimeZonePressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
|
||||
ctx->timeZoneLabel = lv_label_create(timezone_button);
|
||||
ctx->timeZoneLabel = createTitleAndValue(timezone_row, "Timezone");
|
||||
std::string timeZoneName = settings::getTimeZoneName();
|
||||
if (timeZoneName.empty()) {
|
||||
timeZoneName = "not set";
|
||||
}
|
||||
lv_obj_center(ctx->timeZoneLabel);
|
||||
lv_label_set_text(ctx->timeZoneLabel, timeZoneName.c_str());
|
||||
|
||||
auto* timezone_button = lv_button_create(timezone_row);
|
||||
lv_label_set_text(lv_label_create(timezone_button), "Change");
|
||||
lv_obj_add_event_cb(timezone_button, onTimeZonePressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
}
|
||||
|
||||
void destroyWidgets(void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
ctx->timeZoneLabel = nullptr;
|
||||
ctx->dateFormatLabel = nullptr;
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
@ -158,15 +230,22 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
ctx.eventGroup = &event_group;
|
||||
check(task_event_group_claim_bit(&event_group, &ctx.selectDateFormatBit) == ERROR_NONE);
|
||||
|
||||
AppEventSubscription sub {};
|
||||
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);
|
||||
|
||||
bool shouldClose = false;
|
||||
while (!shouldClose) {
|
||||
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||
uint32_t flags = 0;
|
||||
task_event_group_wait_any(&event_group, &flags, portMAX_DELAY);
|
||||
|
||||
if (flags & ctx.selectDateFormatBit) {
|
||||
startDateFormatSelection(&ctx);
|
||||
}
|
||||
|
||||
AppEvent event {};
|
||||
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||
@ -175,14 +254,18 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
shouldClose = true;
|
||||
break;
|
||||
case APP_EVENT_RESULT:
|
||||
if (event.result.launch_id == ctx.pendingTimeZoneDialogId) {
|
||||
if (event.result.launch_id == ctx.dateFormatDialogId) {
|
||||
onDateFormatSelectionResult(&ctx, event);
|
||||
} else if (event.result.launch_id == ctx.pendingTimeZoneDialogId) {
|
||||
ctx.pendingTimeZoneDialogId = 0;
|
||||
if (event.result.result == 0 /* Ok */) {
|
||||
const auto name = timezone::getLastName();
|
||||
LOG_I(TAG, "Result name=%s code=%s", name.c_str(), timezone::getLastCode().c_str());
|
||||
if (!name.empty()) {
|
||||
lvgl_lock();
|
||||
lv_label_set_text(ctx.timeZoneLabel, name.c_str());
|
||||
if (ctx.timeZoneLabel != nullptr) {
|
||||
lv_label_set_text(ctx.timeZoneLabel, name.c_str());
|
||||
}
|
||||
lvgl_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <memory>
|
||||
@ -84,7 +85,7 @@ void onTextareaValueChanged(lv_event_t* e) {
|
||||
}
|
||||
|
||||
void createListItem(Context* ctx, lv_obj_t* list, const std::string& title, size_t index) {
|
||||
auto* btn = lv_list_add_button(list, nullptr, title.c_str());
|
||||
auto* btn = lvgl_list_add_button(list, nullptr, title.c_str());
|
||||
struct ButtonContext {
|
||||
Context* ctx;
|
||||
size_t index;
|
||||
@ -219,7 +220,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
ctx->filterTextareaWidget = textarea;
|
||||
lv_obj_set_flex_grow(textarea, 1);
|
||||
|
||||
auto* list = lv_list_create(parent);
|
||||
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);
|
||||
|
||||
@ -100,7 +100,7 @@ void finishCalibration(Context* ctx) {
|
||||
if (xSpan <= 0 || ySpan <= 0) {
|
||||
lv_label_set_text(ctx->titleLabel, "Calibration Failed");
|
||||
lv_label_set_text(ctx->hintLabel, "Screen too small. Tap to close.");
|
||||
lv_obj_add_flag(ctx->target, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->target, true);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -119,14 +119,14 @@ void finishCalibration(Context* ctx) {
|
||||
if (!settings::touch::isValid(settings)) {
|
||||
lv_label_set_text(ctx->titleLabel, "Calibration Failed");
|
||||
lv_label_set_text(ctx->hintLabel, "Range invalid. Tap to close.");
|
||||
lv_obj_add_flag(ctx->target, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->target, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settings::touch::save(settings)) {
|
||||
lv_label_set_text(ctx->titleLabel, "Calibration Failed");
|
||||
lv_label_set_text(ctx->hintLabel, "Unable to save settings. Tap to close.");
|
||||
lv_obj_add_flag(ctx->target, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->target, true);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -147,7 +147,7 @@ void finishCalibration(Context* ctx) {
|
||||
LOG_I(TAG, "Saved calibration x=[%d, %d] y=[%d, %d]", xMin, xMax, yMin, yMax);
|
||||
lv_label_set_text(ctx->titleLabel, "Calibration Complete");
|
||||
lv_label_set_text(ctx->hintLabel, "Touch anywhere to continue.");
|
||||
lv_obj_add_flag(ctx->target, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->target, true);
|
||||
}
|
||||
|
||||
void onPress(lv_event_t* event) {
|
||||
@ -207,13 +207,13 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_set_style_radius(ctx->target, LV_RADIUS_CIRCLE, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_color(ctx->target, lv_palette_main(LV_PALETTE_RED), LV_STATE_DEFAULT);
|
||||
// Ensure root receives all presses for sampling.
|
||||
lv_obj_remove_flag(ctx->target, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_clickable(ctx->target, false);
|
||||
|
||||
auto* targetLabel = lv_label_create(ctx->target);
|
||||
lv_label_set_text(targetLabel, "+");
|
||||
lv_obj_center(targetLabel);
|
||||
|
||||
lv_obj_add_flag(ctx->root, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_clickable(ctx->root, true);
|
||||
lv_obj_add_event_cb(ctx->root, onPress, LV_EVENT_PRESSED, ctx);
|
||||
|
||||
updateUi(ctx);
|
||||
|
||||
@ -3,6 +3,10 @@
|
||||
#include <lvgl/devices/device_context.h>
|
||||
#include <lvgl/devices/trackball.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/chip.h>
|
||||
#include <lvgl/widgets/sliderbox.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
@ -25,14 +29,16 @@ extern const ::AppManifest manifest;
|
||||
|
||||
constexpr auto* TAG = "TrackballSettings";
|
||||
|
||||
// Convert mode to dropdown index (dropdown order: Keys=0, Pointer=1)
|
||||
static uint32_t modeToDropdownIndex(LvglTrackballMode mode) {
|
||||
switch (mode) {
|
||||
case LVGL_TRACKBALL_MODE_KEYS: return 0;
|
||||
case LVGL_TRACKBALL_MODE_POINTER: return 1;
|
||||
}
|
||||
return 0; // default to Keys
|
||||
}
|
||||
struct ModeOption {
|
||||
LvglTrackballMode mode;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
// The order of the mode chips
|
||||
constexpr ModeOption MODE_OPTIONS[] = {
|
||||
{ LVGL_TRACKBALL_MODE_KEYS, "Keys" },
|
||||
{ LVGL_TRACKBALL_MODE_POINTER, "Pointer" },
|
||||
};
|
||||
|
||||
static lv_indev_t* findFirstTrackballIndev() {
|
||||
lv_indev_t* indev = lv_indev_get_next(nullptr);
|
||||
@ -64,8 +70,14 @@ struct Context {
|
||||
// to whatever is already attached.
|
||||
lv_indev_t* trackballIndev = nullptr;
|
||||
lv_obj_t* switchTrackball = nullptr;
|
||||
lv_obj_t* trackballModeDropdown = nullptr;
|
||||
// Shown while the trackball is enabled
|
||||
lv_obj_t* settingsCard = nullptr;
|
||||
lv_obj_t* modeChips = nullptr;
|
||||
// Shown in keys mode
|
||||
lv_obj_t* keySensitivityGroup = nullptr;
|
||||
lv_obj_t* keySensitivitySlider = nullptr;
|
||||
// Shown in pointer mode
|
||||
lv_obj_t* pointerSensitivityGroup = nullptr;
|
||||
lv_obj_t* pointerSensitivitySlider = nullptr;
|
||||
};
|
||||
|
||||
@ -87,47 +99,37 @@ void applyLive(Context* ctx) {
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
void onTrackballSwitch(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
bool enabled = lv_obj_has_state(ctx->switchTrackball, LV_STATE_CHECKED);
|
||||
ctx->tbSettings.enabled = enabled;
|
||||
ctx->updated = true;
|
||||
applyLive(ctx);
|
||||
|
||||
// Enable/disable controls based on trackball state
|
||||
if (enabled) {
|
||||
if (ctx->trackballModeDropdown) lv_obj_clear_state(ctx->trackballModeDropdown, LV_STATE_DISABLED);
|
||||
if (ctx->keySensitivitySlider) lv_obj_clear_state(ctx->keySensitivitySlider, LV_STATE_DISABLED);
|
||||
if (ctx->pointerSensitivitySlider) lv_obj_clear_state(ctx->pointerSensitivitySlider, LV_STATE_DISABLED);
|
||||
} else {
|
||||
if (ctx->trackballModeDropdown) lv_obj_add_state(ctx->trackballModeDropdown, LV_STATE_DISABLED);
|
||||
if (ctx->keySensitivitySlider) lv_obj_add_state(ctx->keySensitivitySlider, LV_STATE_DISABLED);
|
||||
if (ctx->pointerSensitivitySlider) lv_obj_add_state(ctx->pointerSensitivitySlider, LV_STATE_DISABLED);
|
||||
/** Shows the settings of the enabled trackball and its mode */
|
||||
void updateWidgets(Context* ctx) {
|
||||
lv_obj_set_hidden(ctx->settingsCard, !ctx->tbSettings.enabled);
|
||||
const uint32_t chip_count = lv_obj_get_child_count(ctx->modeChips);
|
||||
for (uint32_t i = 0; i < chip_count; i++) {
|
||||
lv_obj_set_state(lv_obj_get_child(ctx->modeChips, static_cast<int32_t>(i)), LV_STATE_CHECKED, MODE_OPTIONS[i].mode == ctx->tbSettings.mode);
|
||||
}
|
||||
lv_obj_set_hidden(ctx->keySensitivityGroup, ctx->tbSettings.mode != LVGL_TRACKBALL_MODE_KEYS);
|
||||
lv_obj_set_hidden(ctx->pointerSensitivityGroup, ctx->tbSettings.mode != LVGL_TRACKBALL_MODE_POINTER);
|
||||
}
|
||||
|
||||
void onTrackballModeChanged(lv_event_t* e) {
|
||||
void onTrackballSwitch(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
uint32_t selected = lv_dropdown_get_selected(ctx->trackballModeDropdown);
|
||||
|
||||
// Validate selection matches expected enum values (dropdown order: Keys=0, Pointer=1)
|
||||
LvglTrackballMode mode;
|
||||
switch (selected) {
|
||||
case 0: mode = LVGL_TRACKBALL_MODE_KEYS; break;
|
||||
case 1: mode = LVGL_TRACKBALL_MODE_POINTER; break;
|
||||
default: return; // Invalid selection, ignore
|
||||
}
|
||||
|
||||
ctx->tbSettings.mode = mode;
|
||||
ctx->tbSettings.enabled = lv_obj_has_state(ctx->switchTrackball, LV_STATE_CHECKED);
|
||||
ctx->updated = true;
|
||||
applyLive(ctx);
|
||||
updateWidgets(ctx);
|
||||
}
|
||||
|
||||
void onModeChipPressed(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
ctx->tbSettings.mode = MODE_OPTIONS[lv_obj_get_index(lv_event_get_target_obj(e))].mode;
|
||||
ctx->updated = true;
|
||||
// Apply mode change immediately
|
||||
applyLive(ctx);
|
||||
updateWidgets(ctx);
|
||||
}
|
||||
|
||||
void onKeySensitivityChanged(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
int32_t value = lv_slider_get_value(ctx->keySensitivitySlider);
|
||||
int32_t value = lvgl_sliderbox_get_value(ctx->keySensitivitySlider);
|
||||
ctx->tbSettings.key_sensitivity = static_cast<uint8_t>(value);
|
||||
ctx->updated = true;
|
||||
|
||||
@ -137,7 +139,7 @@ void onKeySensitivityChanged(lv_event_t* e) {
|
||||
|
||||
void onPointerSensitivityChanged(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
int32_t value = lv_slider_get_value(ctx->pointerSensitivitySlider);
|
||||
int32_t value = lvgl_sliderbox_get_value(ctx->pointerSensitivitySlider);
|
||||
ctx->tbSettings.pointer_sensitivity = static_cast<uint8_t>(value);
|
||||
ctx->updated = true;
|
||||
|
||||
@ -145,6 +147,24 @@ void onPointerSensitivityChanged(lv_event_t* e) {
|
||||
applyLive(ctx);
|
||||
}
|
||||
|
||||
/** A transparent column with a title and a speed slider */
|
||||
lv_obj_t* createSpeedGroup(lv_obj_t* parent, const char* title, int32_t value, lv_event_cb_t onChanged, Context* ctx, lv_obj_t** outSlider) {
|
||||
auto* group = lv_obj_create(parent);
|
||||
lv_obj_set_size(group, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(group, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_all(group, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(group, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(group, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(group, false);
|
||||
|
||||
lv_label_set_text(lv_label_create(group), title);
|
||||
|
||||
*outSlider = lvgl_sliderbox_create(group, 1, 10, 1, value);
|
||||
lv_obj_set_width(*outSlider, LV_PCT(100));
|
||||
lvgl_sliderbox_add_value_changed_cb(*outSlider, onChanged, ctx);
|
||||
return group;
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
@ -161,12 +181,13 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
if (ctx->trackballIndev == nullptr) {
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
auto* label = lv_label_create(wrapper);
|
||||
lv_label_set_text(label, "No trackball device found");
|
||||
auto* card = lvgl_card_create(wrapper);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_label_set_text(lv_label_create(card), "No trackball device found.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -180,72 +201,39 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
if (ctx->tbSettings.enabled) lv_obj_add_state(ctx->switchTrackball, LV_STATE_CHECKED);
|
||||
|
||||
auto* main_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(main_wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(main_wrapper, 1);
|
||||
|
||||
// Trackball mode dropdown
|
||||
auto* tb_mode_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(tb_mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(tb_mode_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(tb_mode_wrapper, 0, LV_STATE_DEFAULT);
|
||||
ctx->settingsCard = lvgl_card_create(main_wrapper);
|
||||
lv_obj_set_size(ctx->settingsCard, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(ctx->settingsCard, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(ctx->settingsCard, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
auto* tb_mode_label = lv_label_create(tb_mode_wrapper);
|
||||
lv_label_set_text(tb_mode_label, "Mode");
|
||||
lv_obj_align(tb_mode_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
ctx->trackballModeDropdown = lv_dropdown_create(tb_mode_wrapper);
|
||||
lv_dropdown_set_options(ctx->trackballModeDropdown, "Keys\nPointer");
|
||||
lv_obj_align(ctx->trackballModeDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_dropdown_set_selected(ctx->trackballModeDropdown, modeToDropdownIndex(ctx->tbSettings.mode));
|
||||
lv_obj_add_event_cb(ctx->trackballModeDropdown, onTrackballModeChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
// Disable dropdown if trackball is disabled
|
||||
if (!ctx->tbSettings.enabled) {
|
||||
lv_obj_add_state(ctx->trackballModeDropdown, LV_STATE_DISABLED);
|
||||
// Mode: the chips are centered while they fit, and scroll when they don't
|
||||
ctx->modeChips = lv_obj_create(ctx->settingsCard);
|
||||
lv_obj_set_size(ctx->modeChips, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_max_width(ctx->modeChips, LV_PCT(100), LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(ctx->modeChips, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_scroll_dir(ctx->modeChips, LV_DIR_HOR);
|
||||
lv_obj_set_style_bg_opa(ctx->modeChips, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ctx->modeChips, 0, LV_STATE_DEFAULT);
|
||||
// The chips' margins leave room for their focus rings and space them apart
|
||||
lv_obj_set_style_pad_all(ctx->modeChips, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_column(ctx->modeChips, 0, LV_STATE_DEFAULT);
|
||||
for (const auto& option : MODE_OPTIONS) {
|
||||
auto* chip = lvgl_chip_create(ctx->modeChips);
|
||||
lv_label_set_text(lv_label_create(chip), option.name);
|
||||
lv_obj_add_event_cb(chip, onModeChipPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
}
|
||||
lvgl_grid_navigation_add(ctx->modeChips);
|
||||
|
||||
// Key sensitivity slider
|
||||
auto* key_sens_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(key_sens_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_hor(key_sens_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(key_sens_wrapper, 0, LV_STATE_DEFAULT);
|
||||
// The speed of the selected mode
|
||||
ctx->keySensitivityGroup = createSpeedGroup(ctx->settingsCard, "Key speed", ctx->tbSettings.key_sensitivity, onKeySensitivityChanged, ctx, &ctx->keySensitivitySlider);
|
||||
ctx->pointerSensitivityGroup = createSpeedGroup(ctx->settingsCard, "Pointer speed", ctx->tbSettings.pointer_sensitivity, onPointerSensitivityChanged, ctx, &ctx->pointerSensitivitySlider);
|
||||
|
||||
auto* key_sens_label = lv_label_create(key_sens_wrapper);
|
||||
lv_label_set_text(key_sens_label, "Key Speed");
|
||||
lv_obj_align(key_sens_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
ctx->keySensitivitySlider = lv_slider_create(key_sens_wrapper);
|
||||
lv_slider_set_range(ctx->keySensitivitySlider, 1, 10);
|
||||
lv_slider_set_value(ctx->keySensitivitySlider, ctx->tbSettings.key_sensitivity, LV_ANIM_OFF);
|
||||
lv_obj_set_width(ctx->keySensitivitySlider, LV_PCT(50));
|
||||
lv_obj_align(ctx->keySensitivitySlider, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->keySensitivitySlider, onKeySensitivityChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
if (!ctx->tbSettings.enabled) {
|
||||
lv_obj_add_state(ctx->keySensitivitySlider, LV_STATE_DISABLED);
|
||||
}
|
||||
|
||||
// Pointer sensitivity slider
|
||||
auto* ptr_sens_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(ptr_sens_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_hor(ptr_sens_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ptr_sens_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* ptr_sens_label = lv_label_create(ptr_sens_wrapper);
|
||||
lv_label_set_text(ptr_sens_label, "Pointer Speed");
|
||||
lv_obj_align(ptr_sens_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
ctx->pointerSensitivitySlider = lv_slider_create(ptr_sens_wrapper);
|
||||
lv_slider_set_range(ctx->pointerSensitivitySlider, 1, 10);
|
||||
lv_slider_set_value(ctx->pointerSensitivitySlider, ctx->tbSettings.pointer_sensitivity, LV_ANIM_OFF);
|
||||
lv_obj_set_width(ctx->pointerSensitivitySlider, LV_PCT(50));
|
||||
lv_obj_align(ctx->pointerSensitivitySlider, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->pointerSensitivitySlider, onPointerSensitivityChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
if (!ctx->tbSettings.enabled) {
|
||||
lv_obj_add_state(ctx->pointerSensitivitySlider, LV_STATE_DISABLED);
|
||||
}
|
||||
updateWidgets(ctx);
|
||||
}
|
||||
|
||||
// Mirrors the old onHide() behaviour: persist the settings (regardless of whether the app is
|
||||
|
||||
@ -16,6 +16,9 @@
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/chip.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
namespace tt::app::webserversettings {
|
||||
@ -33,7 +36,15 @@ struct Context {
|
||||
settings::webserver::WebServerSettings originalSettings;
|
||||
bool updated = false;
|
||||
bool wifiSettingsChanged = false;
|
||||
lv_obj_t* dropdownWifiMode = nullptr;
|
||||
lv_obj_t* wifiModeChips = nullptr;
|
||||
// Shown in access point mode
|
||||
lv_obj_t* apGroup = nullptr;
|
||||
// Shown when the access point isn't an open network
|
||||
lv_obj_t* apPasswordRow = nullptr;
|
||||
// Shown when authentication is required
|
||||
lv_obj_t* authCard = nullptr;
|
||||
// Shown when the web server is enabled
|
||||
lv_obj_t* urlCard = nullptr;
|
||||
lv_obj_t* textAreaApPassword = nullptr;
|
||||
lv_obj_t* switchApOpenNetwork = nullptr;
|
||||
lv_obj_t* switchWebServerEnabled = nullptr;
|
||||
@ -53,10 +64,34 @@ void onBackPressed(lv_event_t* event) {
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onWifiModeChanged(lv_event_t* e) {
|
||||
/** Shows the widgets of the enabled features. Requires the LVGL lock. */
|
||||
void updateVisibility(Context* ctx, bool accessPoint, bool openNetwork, bool authEnabled, bool serverEnabled) {
|
||||
const uint32_t chip_count = lv_obj_get_child_count(ctx->wifiModeChips);
|
||||
for (uint32_t i = 0; i < chip_count; i++) {
|
||||
lv_obj_set_state(lv_obj_get_child(ctx->wifiModeChips, static_cast<int32_t>(i)), LV_STATE_CHECKED, (i == 1) == accessPoint);
|
||||
}
|
||||
lv_obj_set_hidden(ctx->apGroup, !accessPoint);
|
||||
lv_obj_set_hidden(ctx->apPasswordRow, openNetwork);
|
||||
lv_obj_set_hidden(ctx->authCard, !authEnabled);
|
||||
lv_obj_set_hidden(ctx->urlCard, !serverEnabled);
|
||||
}
|
||||
|
||||
void updateVisibility(Context* ctx) {
|
||||
updateVisibility(ctx,
|
||||
lv_obj_has_state(lv_obj_get_child(ctx->wifiModeChips, 1), LV_STATE_CHECKED),
|
||||
lv_obj_has_state(ctx->switchApOpenNetwork, LV_STATE_CHECKED),
|
||||
lv_obj_has_state(ctx->switchWebServerAuthEnabled, LV_STATE_CHECKED),
|
||||
lv_obj_has_state(ctx->switchWebServerEnabled, LV_STATE_CHECKED));
|
||||
}
|
||||
|
||||
void onWifiModeChipPressed(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(e));
|
||||
auto index = lv_dropdown_get_selected(dropdown);
|
||||
// Chip 0 is station mode, chip 1 is access point mode
|
||||
const uint32_t index = lv_obj_get_index(lv_event_get_target_obj(e));
|
||||
updateVisibility(ctx, index == 1,
|
||||
lv_obj_has_state(ctx->switchApOpenNetwork, LV_STATE_CHECKED),
|
||||
lv_obj_has_state(ctx->switchWebServerAuthEnabled, LV_STATE_CHECKED),
|
||||
lv_obj_has_state(ctx->switchWebServerEnabled, LV_STATE_CHECKED));
|
||||
getMainDispatcher().dispatch([ctx, index] {
|
||||
ctx->wsSettings.wifiMode = static_cast<settings::webserver::WiFiMode>(index);
|
||||
ctx->updated = true;
|
||||
@ -70,6 +105,7 @@ void onWifiModeChanged(lv_event_t* e) {
|
||||
void onWebServerEnabledSwitch(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
bool enabled = lv_obj_has_state(ctx->switchWebServerEnabled, LV_STATE_CHECKED);
|
||||
updateVisibility(ctx);
|
||||
getMainDispatcher().dispatch([ctx, enabled] {
|
||||
ctx->wsSettings.webServerEnabled = enabled;
|
||||
ctx->updated = true;
|
||||
@ -91,22 +127,7 @@ void onWebServerEnabledSwitch(lv_event_t* e) {
|
||||
void onWebServerAuthEnabledSwitch(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
bool enabled = lv_obj_has_state(ctx->switchWebServerAuthEnabled, LV_STATE_CHECKED);
|
||||
|
||||
if (ctx->textAreaWebServerUsername && ctx->textAreaWebServerPassword) {
|
||||
if (enabled) {
|
||||
lv_obj_remove_state(ctx->textAreaWebServerUsername, LV_STATE_DISABLED);
|
||||
lv_obj_add_flag(ctx->textAreaWebServerUsername, LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
lv_obj_remove_state(ctx->textAreaWebServerPassword, LV_STATE_DISABLED);
|
||||
lv_obj_add_flag(ctx->textAreaWebServerPassword, LV_OBJ_FLAG_CLICKABLE);
|
||||
} else {
|
||||
lv_obj_add_state(ctx->textAreaWebServerUsername, LV_STATE_DISABLED);
|
||||
lv_obj_remove_flag(ctx->textAreaWebServerUsername, LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
lv_obj_add_state(ctx->textAreaWebServerPassword, LV_STATE_DISABLED);
|
||||
lv_obj_remove_flag(ctx->textAreaWebServerPassword, LV_OBJ_FLAG_CLICKABLE);
|
||||
}
|
||||
}
|
||||
updateVisibility(ctx);
|
||||
|
||||
getMainDispatcher().dispatch([ctx, enabled] {
|
||||
ctx->wsSettings.webServerAuthEnabled = enabled;
|
||||
@ -132,16 +153,7 @@ void onApPasswordChanged(lv_event_t* e) {
|
||||
void onApOpenNetworkSwitch(lv_event_t* e) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
|
||||
bool openNetwork = lv_obj_has_state(ctx->switchApOpenNetwork, LV_STATE_CHECKED);
|
||||
|
||||
if (ctx->textAreaApPassword) {
|
||||
if (openNetwork) {
|
||||
lv_obj_add_state(ctx->textAreaApPassword, LV_STATE_DISABLED);
|
||||
lv_obj_remove_flag(ctx->textAreaApPassword, LV_OBJ_FLAG_CLICKABLE);
|
||||
} else {
|
||||
lv_obj_remove_state(ctx->textAreaApPassword, LV_STATE_DISABLED);
|
||||
lv_obj_add_flag(ctx->textAreaApPassword, LV_OBJ_FLAG_CLICKABLE);
|
||||
}
|
||||
}
|
||||
updateVisibility(ctx);
|
||||
|
||||
getMainDispatcher().dispatch([ctx, openNetwork] {
|
||||
ctx->wsSettings.apOpenNetwork = openNetwork;
|
||||
@ -187,6 +199,51 @@ void updateUrlDisplay(Context* ctx) {
|
||||
lv_label_set_text(ctx->labelUrlValue, url.c_str());
|
||||
}
|
||||
|
||||
lv_obj_t* createCard(lv_obj_t* parent) {
|
||||
auto* card = lvgl_card_create(parent);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
return card;
|
||||
}
|
||||
|
||||
/** A transparent column of rows, to show or hide them together */
|
||||
lv_obj_t* createGroup(lv_obj_t* parent) {
|
||||
auto* group = lv_obj_create(parent);
|
||||
lv_obj_set_size(group, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(group, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_all(group, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(group, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(group, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(group, false);
|
||||
return group;
|
||||
}
|
||||
|
||||
/** A transparent row: "Title [content]" */
|
||||
lv_obj_t* createRow(lv_obj_t* parent, const char* title) {
|
||||
auto* row = lv_obj_create(parent);
|
||||
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(row, false);
|
||||
auto* label = lv_label_create(row);
|
||||
lv_label_set_text(label, title);
|
||||
lv_obj_set_flex_grow(label, 1);
|
||||
return row;
|
||||
}
|
||||
|
||||
lv_obj_t* createTextArea(lv_obj_t* row, const char* text, uint32_t maxLength, bool password) {
|
||||
auto* text_area = lv_textarea_create(row);
|
||||
lv_obj_set_width(text_area, LV_PCT(50));
|
||||
lv_textarea_set_one_line(text_area, true);
|
||||
lv_textarea_set_max_length(text_area, maxLength);
|
||||
lv_textarea_set_password_mode(text_area, password);
|
||||
lv_textarea_set_text(text_area, text);
|
||||
return text_area;
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
@ -205,144 +262,80 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_add_event_cb(ctx->switchWebServerEnabled, onWebServerEnabledSwitch, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
auto* main_wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(main_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_width(main_wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(main_wrapper, 1);
|
||||
|
||||
// WiFi Mode dropdown
|
||||
auto* wifi_mode_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(wifi_mode_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(wifi_mode_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(wifi_mode_wrapper, 0, LV_STATE_DEFAULT);
|
||||
auto* wifi_mode_label = lv_label_create(wifi_mode_wrapper);
|
||||
lv_label_set_text(wifi_mode_label, "WiFi Mode");
|
||||
lv_obj_align(wifi_mode_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
ctx->dropdownWifiMode = lv_dropdown_create(wifi_mode_wrapper);
|
||||
lv_obj_align(ctx->dropdownWifiMode, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_dropdown_set_options(ctx->dropdownWifiMode, "Station\nAccess Point");
|
||||
lv_dropdown_set_selected(ctx->dropdownWifiMode, static_cast<uint32_t>(ctx->wsSettings.wifiMode));
|
||||
lv_obj_add_event_cb(ctx->dropdownWifiMode, onWifiModeChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
// Address, while the web server is enabled
|
||||
ctx->urlCard = createCard(main_wrapper);
|
||||
ctx->labelUrl = lv_label_create(ctx->urlCard);
|
||||
lv_label_set_text(ctx->labelUrl, "Web server address");
|
||||
ctx->labelUrlValue = lv_label_create(ctx->urlCard);
|
||||
updateUrlDisplay(ctx);
|
||||
|
||||
// AP Open Network toggle
|
||||
auto* ap_open_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(ap_open_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(ap_open_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ap_open_wrapper, 0, LV_STATE_DEFAULT);
|
||||
auto* ap_open_label = lv_label_create(ap_open_wrapper);
|
||||
lv_label_set_text(ap_open_label, "AP Open Network");
|
||||
lv_obj_align(ap_open_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
ctx->switchApOpenNetwork = lv_switch_create(ap_open_wrapper);
|
||||
// WiFi mode, with the access point settings in access point mode
|
||||
lv_label_set_text(lv_label_create(main_wrapper), "WiFi mode");
|
||||
auto* wifi_card = createCard(main_wrapper);
|
||||
lv_obj_set_flex_align(wifi_card, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
// The chips are centered while they fit, and scroll when they don't
|
||||
ctx->wifiModeChips = lv_obj_create(wifi_card);
|
||||
lv_obj_set_size(ctx->wifiModeChips, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_max_width(ctx->wifiModeChips, LV_PCT(100), LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(ctx->wifiModeChips, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_scroll_dir(ctx->wifiModeChips, LV_DIR_HOR);
|
||||
lv_obj_set_style_bg_opa(ctx->wifiModeChips, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ctx->wifiModeChips, 0, LV_STATE_DEFAULT);
|
||||
// The chips' margins leave room for their focus rings and space them apart
|
||||
lv_obj_set_style_pad_all(ctx->wifiModeChips, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_column(ctx->wifiModeChips, 0, LV_STATE_DEFAULT);
|
||||
// In settings::webserver::WiFiMode order
|
||||
for (const char* name : { "Station", "Access point" }) {
|
||||
auto* chip = lvgl_chip_create(ctx->wifiModeChips);
|
||||
lv_label_set_text(lv_label_create(chip), name);
|
||||
lv_obj_add_event_cb(chip, onWifiModeChipPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
}
|
||||
lvgl_grid_navigation_add(ctx->wifiModeChips);
|
||||
|
||||
ctx->apGroup = createGroup(wifi_card);
|
||||
auto* ap_open_row = createRow(ctx->apGroup, "Open network");
|
||||
ctx->switchApOpenNetwork = lv_switch_create(ap_open_row);
|
||||
if (ctx->wsSettings.apOpenNetwork) lv_obj_add_state(ctx->switchApOpenNetwork, LV_STATE_CHECKED);
|
||||
lv_obj_align(ctx->switchApOpenNetwork, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->switchApOpenNetwork, onApOpenNetworkSwitch, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
// AP Password
|
||||
auto* ap_pass_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(ap_pass_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(ap_pass_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ap_pass_wrapper, 0, LV_STATE_DEFAULT);
|
||||
auto* ap_pass_label = lv_label_create(ap_pass_wrapper);
|
||||
lv_label_set_text(ap_pass_label, "AP Password");
|
||||
lv_obj_align(ap_pass_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
ctx->textAreaApPassword = lv_textarea_create(ap_pass_wrapper);
|
||||
lv_obj_set_width(ctx->textAreaApPassword, 120);
|
||||
lv_obj_align(ctx->textAreaApPassword, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_textarea_set_one_line(ctx->textAreaApPassword, true);
|
||||
lv_textarea_set_max_length(ctx->textAreaApPassword, 64);
|
||||
lv_textarea_set_password_mode(ctx->textAreaApPassword, true);
|
||||
lv_textarea_set_text(ctx->textAreaApPassword, ctx->wsSettings.apPassword.c_str());
|
||||
ctx->apPasswordRow = createRow(ctx->apGroup, "Password");
|
||||
ctx->textAreaApPassword = createTextArea(ctx->apPasswordRow, ctx->wsSettings.apPassword.c_str(), 64, true);
|
||||
lv_obj_add_event_cb(ctx->textAreaApPassword, onApPasswordChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
// Disable password field if open network is enabled
|
||||
if (ctx->wsSettings.apOpenNetwork) {
|
||||
lv_obj_add_state(ctx->textAreaApPassword, LV_STATE_DISABLED);
|
||||
lv_obj_remove_flag(ctx->textAreaApPassword, LV_OBJ_FLAG_CLICKABLE);
|
||||
}
|
||||
|
||||
// Web Server Authentication Enable toggle
|
||||
auto* ws_auth_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(ws_auth_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(ws_auth_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ws_auth_wrapper, 0, LV_STATE_DEFAULT);
|
||||
auto* ws_auth_label = lv_label_create(ws_auth_wrapper);
|
||||
lv_label_set_text(ws_auth_label, "Require Authentication");
|
||||
lv_obj_align(ws_auth_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
ctx->switchWebServerAuthEnabled = lv_switch_create(ws_auth_wrapper);
|
||||
// Authentication, with the credentials while it's required
|
||||
auto* auth_row = createRow(main_wrapper, "Require authentication");
|
||||
ctx->switchWebServerAuthEnabled = lv_switch_create(auth_row);
|
||||
if (ctx->wsSettings.webServerAuthEnabled) lv_obj_add_state(ctx->switchWebServerAuthEnabled, LV_STATE_CHECKED);
|
||||
lv_obj_align(ctx->switchWebServerAuthEnabled, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->switchWebServerAuthEnabled, onWebServerAuthEnabledSwitch, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
// WebServer Username
|
||||
auto* ws_user_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(ws_user_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(ws_user_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ws_user_wrapper, 0, LV_STATE_DEFAULT);
|
||||
auto* ws_user_label = lv_label_create(ws_user_wrapper);
|
||||
lv_label_set_text(ws_user_label, "Username");
|
||||
lv_obj_align(ws_user_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
ctx->textAreaWebServerUsername = lv_textarea_create(ws_user_wrapper);
|
||||
if (!ctx->wsSettings.webServerAuthEnabled) {
|
||||
lv_obj_add_state(ctx->textAreaWebServerUsername, LV_STATE_DISABLED);
|
||||
lv_obj_remove_flag(ctx->textAreaWebServerUsername, LV_OBJ_FLAG_CLICKABLE);
|
||||
}
|
||||
lv_obj_set_width(ctx->textAreaWebServerUsername, 120);
|
||||
lv_obj_align(ctx->textAreaWebServerUsername, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_textarea_set_one_line(ctx->textAreaWebServerUsername, true);
|
||||
lv_textarea_set_max_length(ctx->textAreaWebServerUsername, 32);
|
||||
lv_textarea_set_text(ctx->textAreaWebServerUsername, ctx->wsSettings.webServerUsername.c_str());
|
||||
ctx->authCard = createCard(main_wrapper);
|
||||
auto* user_row = createRow(ctx->authCard, "Username");
|
||||
ctx->textAreaWebServerUsername = createTextArea(user_row, ctx->wsSettings.webServerUsername.c_str(), 32, false);
|
||||
lv_obj_add_event_cb(ctx->textAreaWebServerUsername, onCredentialChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
// WebServer Password
|
||||
auto* ws_pass_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(ws_pass_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(ws_pass_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ws_pass_wrapper, 0, LV_STATE_DEFAULT);
|
||||
auto* ws_pass_label = lv_label_create(ws_pass_wrapper);
|
||||
lv_label_set_text(ws_pass_label, "Password");
|
||||
lv_obj_align(ws_pass_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
ctx->textAreaWebServerPassword = lv_textarea_create(ws_pass_wrapper);
|
||||
if (!ctx->wsSettings.webServerAuthEnabled) {
|
||||
lv_obj_add_state(ctx->textAreaWebServerPassword, LV_STATE_DISABLED);
|
||||
lv_obj_remove_flag(ctx->textAreaWebServerPassword, LV_OBJ_FLAG_CLICKABLE);
|
||||
}
|
||||
lv_obj_set_width(ctx->textAreaWebServerPassword, 120);
|
||||
lv_obj_align(ctx->textAreaWebServerPassword, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_textarea_set_one_line(ctx->textAreaWebServerPassword, true);
|
||||
lv_textarea_set_max_length(ctx->textAreaWebServerPassword, 64);
|
||||
lv_textarea_set_password_mode(ctx->textAreaWebServerPassword, true);
|
||||
lv_textarea_set_text(ctx->textAreaWebServerPassword, ctx->wsSettings.webServerPassword.c_str());
|
||||
auto* password_row = createRow(ctx->authCard, "Password");
|
||||
ctx->textAreaWebServerPassword = createTextArea(password_row, ctx->wsSettings.webServerPassword.c_str(), 64, true);
|
||||
lv_obj_add_event_cb(ctx->textAreaWebServerPassword, onCredentialChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
// URL Display
|
||||
auto* url_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_size(url_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(url_wrapper, 10, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(url_wrapper, 1, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(url_wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_flex_cross_place(url_wrapper, LV_FLEX_ALIGN_START, 0);
|
||||
|
||||
ctx->labelUrl = lv_label_create(url_wrapper);
|
||||
lv_label_set_text(ctx->labelUrl, "Web Server URL:");
|
||||
|
||||
ctx->labelUrlValue = lv_label_create(url_wrapper);
|
||||
if (lv_display_get_color_format(lv_obj_get_display(parent)) == LV_COLOR_FORMAT_L8) {
|
||||
lv_obj_set_style_text_color(ctx->labelUrlValue, lv_theme_get_color_secondary(ctx->labelUrlValue), LV_PART_MAIN);
|
||||
} else {
|
||||
lv_obj_set_style_text_color(ctx->labelUrlValue, lv_palette_main(LV_PALETTE_BLUE), 0);
|
||||
}
|
||||
|
||||
updateUrlDisplay(ctx);
|
||||
|
||||
// Info text
|
||||
auto* info_label = lv_label_create(main_wrapper);
|
||||
lv_label_set_long_mode(info_label, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(info_label, LV_PCT(95));
|
||||
if (lv_display_get_color_format(lv_obj_get_display(parent)) != LV_COLOR_FORMAT_L8) {
|
||||
lv_obj_set_style_text_color(info_label, lv_palette_main(LV_PALETTE_GREY), 0);
|
||||
}
|
||||
lv_obj_set_width(info_label, LV_PCT(100));
|
||||
lv_label_set_text(info_label,
|
||||
"WiFi Station credentials are managed separately.\n"
|
||||
"Use the WiFi menu to connect to networks.\n\n"
|
||||
"AP mode uses the password configured above.");
|
||||
|
||||
updateVisibility(ctx,
|
||||
ctx->wsSettings.wifiMode == settings::webserver::WiFiMode::AccessPoint,
|
||||
ctx->wsSettings.apOpenNetwork,
|
||||
ctx->wsSettings.webServerAuthEnabled,
|
||||
ctx->wsSettings.webServerEnabled);
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
|
||||
@ -141,12 +141,12 @@ void onPressDisconnect(lv_event_t* event) {
|
||||
|
||||
void updateConnectButton(Context* ctx) {
|
||||
if (isConnectedToSsid(ctx)) {
|
||||
lv_obj_remove_flag(ctx->disconnectButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(ctx->connectButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->disconnectButton, false);
|
||||
lv_obj_set_hidden(ctx->connectButton, true);
|
||||
lv_obj_remove_state(ctx->disconnectButton, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_obj_add_flag(ctx->disconnectButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(ctx->connectButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->disconnectButton, true);
|
||||
lv_obj_set_hidden(ctx->connectButton, false);
|
||||
lv_obj_remove_state(ctx->connectButton, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
@ -157,9 +157,9 @@ void updateBusySpinner(Context* ctx) {
|
||||
wifi_get_station_state(ctx->wifiDevice, &station_state);
|
||||
}
|
||||
if (station_state == WIFI_STATION_STATE_CONNECTION_PENDING) {
|
||||
lv_obj_remove_flag(ctx->busySpinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->busySpinner, false);
|
||||
} else {
|
||||
lv_obj_add_flag(ctx->busySpinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->busySpinner, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,12 +173,12 @@ void updateAutoConnectSection(Context* ctx) {
|
||||
} else {
|
||||
lv_obj_remove_state(ctx->autoConnectSwitch, LV_STATE_CHECKED);
|
||||
}
|
||||
lv_obj_remove_flag(ctx->forgetButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(ctx->autoConnectWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->forgetButton, false);
|
||||
lv_obj_set_hidden(ctx->autoConnectWrapper, false);
|
||||
} else {
|
||||
LOG_W(TAG, "No settings found");
|
||||
lv_obj_add_flag(ctx->forgetButton, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(ctx->autoConnectWrapper, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->forgetButton, true);
|
||||
lv_obj_set_hidden(ctx->autoConnectWrapper, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,24 +249,26 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* forget_button_label = lv_label_create(ctx->forgetButton);
|
||||
lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_label_set_text(forget_button_label, "Forget");
|
||||
lv_obj_add_flag(ctx->forgetButton, LV_OBJ_FLAG_HIDDEN); // shown by updateAutoConnectSection()
|
||||
lv_obj_set_hidden(ctx->forgetButton, true); // shown by updateAutoConnectSection()
|
||||
|
||||
// Auto-connect
|
||||
|
||||
ctx->autoConnectWrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_size(ctx->autoConnectWrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(ctx->autoConnectWrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(ctx->autoConnectWrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(ctx->autoConnectWrapper, false);
|
||||
lvgl::obj_set_style_bg_invisible(ctx->autoConnectWrapper);
|
||||
lv_obj_set_style_pad_all(ctx->autoConnectWrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ctx->autoConnectWrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_add_flag(ctx->autoConnectWrapper, LV_OBJ_FLAG_HIDDEN); // shown by updateAutoConnectSection()
|
||||
lv_obj_set_hidden(ctx->autoConnectWrapper, true); // shown by updateAutoConnectSection()
|
||||
|
||||
auto* auto_connect_label = lv_label_create(ctx->autoConnectWrapper);
|
||||
lv_label_set_text(auto_connect_label, "Auto-connect");
|
||||
lv_obj_align(auto_connect_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(auto_connect_label, 1);
|
||||
|
||||
ctx->autoConnectSwitch = lv_switch_create(ctx->autoConnectWrapper);
|
||||
lv_obj_add_event_cb(ctx->autoConnectSwitch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
lv_obj_align(ctx->autoConnectSwitch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
#include <wifi/wifi_settings.h>
|
||||
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/theme.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/spinner.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
@ -49,7 +51,7 @@ struct Context {
|
||||
lv_obj_t* password_textarea = nullptr;
|
||||
lv_obj_t* password_error = nullptr;
|
||||
lv_obj_t* connect_button = nullptr;
|
||||
lv_obj_t* remember_switch = nullptr;
|
||||
lv_obj_t* remember_checkbox = nullptr;
|
||||
lv_obj_t* connecting_spinner = nullptr;
|
||||
lv_obj_t* connection_error = nullptr;
|
||||
|
||||
@ -103,24 +105,24 @@ void onWifiEvent(Context* ctx, WifiEvent event) {
|
||||
}
|
||||
|
||||
void resetErrors(Context* ctx) {
|
||||
lv_obj_add_flag(ctx->password_error, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(ctx->ssid_error, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(ctx->connection_error, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->password_error, true);
|
||||
lv_obj_set_hidden(ctx->ssid_error, true);
|
||||
lv_obj_set_hidden(ctx->connection_error, true);
|
||||
}
|
||||
|
||||
void setLoading(Context* ctx, bool loading) {
|
||||
if (loading) {
|
||||
lv_obj_add_flag(ctx->connect_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_remove_flag(ctx->connecting_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->connect_button, true);
|
||||
lv_obj_set_hidden(ctx->connecting_spinner, false);
|
||||
lv_obj_add_state(ctx->password_textarea, LV_STATE_DISABLED);
|
||||
lv_obj_add_state(ctx->ssid_textarea, LV_STATE_DISABLED);
|
||||
lv_obj_add_state(ctx->remember_switch, LV_STATE_DISABLED);
|
||||
lv_obj_add_state(ctx->remember_checkbox, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_obj_remove_flag(ctx->connect_button, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(ctx->connecting_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->connect_button, false);
|
||||
lv_obj_set_hidden(ctx->connecting_spinner, true);
|
||||
lv_obj_remove_state(ctx->password_textarea, LV_STATE_DISABLED);
|
||||
lv_obj_remove_state(ctx->ssid_textarea, LV_STATE_DISABLED);
|
||||
lv_obj_remove_state(ctx->remember_switch, LV_STATE_DISABLED);
|
||||
lv_obj_remove_state(ctx->remember_checkbox, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,7 +135,7 @@ void updateView(Context* ctx) {
|
||||
setLoading(ctx, false);
|
||||
resetErrors(ctx);
|
||||
lv_label_set_text(ctx->connection_error, "Connection failed");
|
||||
lv_obj_remove_flag(ctx->connection_error, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->connection_error, false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,7 +150,7 @@ void onConnectPressed(lv_event_t* event) {
|
||||
if (ssid_len > WIFI_SETTINGS_SSID_LIMIT) {
|
||||
LOG_E(TAG, "SSID too long");
|
||||
lv_label_set_text(ctx->ssid_error, "SSID too long");
|
||||
lv_obj_remove_flag(ctx->ssid_error, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->ssid_error, false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -157,11 +159,11 @@ void onConnectPressed(lv_event_t* event) {
|
||||
if (password_len > WIFI_SETTINGS_PASSWORD_LIMIT) {
|
||||
LOG_E(TAG, "Password too long");
|
||||
lv_label_set_text(ctx->password_error, "Password too long");
|
||||
lv_obj_remove_flag(ctx->password_error, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->password_error, false);
|
||||
return;
|
||||
}
|
||||
|
||||
bool store = lv_obj_get_state(ctx->remember_switch) & LV_STATE_CHECKED;
|
||||
bool store = lv_obj_get_state(ctx->remember_checkbox) & LV_STATE_CHECKED;
|
||||
|
||||
setLoading(ctx, true);
|
||||
|
||||
@ -198,26 +200,23 @@ void createBottomButtons(Context* ctx, lv_obj_t* parent) {
|
||||
lv_obj_set_width(button_container, LV_PCT(100));
|
||||
lv_obj_set_height(button_container, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(button_container, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_gap(button_container, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_gap(button_container, 4, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(button_container, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(button_container, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
// "[x] Remember [Connect]", with everything centered vertically
|
||||
lv_obj_set_flex_flow(button_container, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(button_container, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
ctx->remember_switch = lv_switch_create(button_container);
|
||||
lv_obj_add_state(ctx->remember_switch, LV_STATE_CHECKED);
|
||||
lv_obj_align(ctx->remember_switch, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
auto* remember_label = lv_label_create(button_container);
|
||||
lv_label_set_text(remember_label, "Remember");
|
||||
lv_obj_align(remember_label, LV_ALIGN_CENTER, 0, 0);
|
||||
lv_obj_align_to(remember_label, ctx->remember_switch, LV_ALIGN_OUT_RIGHT_MID, 4, 0);
|
||||
ctx->remember_checkbox = lv_checkbox_create(button_container);
|
||||
lv_checkbox_set_text(ctx->remember_checkbox, "Remember");
|
||||
lv_obj_add_state(ctx->remember_checkbox, LV_STATE_CHECKED);
|
||||
|
||||
ctx->connecting_spinner = lvgl_spinner_create(button_container);
|
||||
lv_obj_align(ctx->connecting_spinner, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_flag(ctx->connecting_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->connecting_spinner, true);
|
||||
|
||||
ctx->connect_button = lv_btn_create(button_container);
|
||||
auto* connect_label = lv_label_create(ctx->connect_button);
|
||||
lv_label_set_text(connect_label, "Connect");
|
||||
lv_obj_align(ctx->connect_button, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->connect_button, onConnectPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
}
|
||||
|
||||
@ -230,11 +229,42 @@ void destroyWidgets(void* userData) {
|
||||
ctx->password_textarea = nullptr;
|
||||
ctx->password_error = nullptr;
|
||||
ctx->connect_button = nullptr;
|
||||
ctx->remember_switch = nullptr;
|
||||
ctx->remember_checkbox = nullptr;
|
||||
ctx->connecting_spinner = nullptr;
|
||||
ctx->connection_error = nullptr;
|
||||
}
|
||||
|
||||
/** A transparent row: "Title [content]" */
|
||||
lv_obj_t* createRow(lv_obj_t* parent, const char* title) {
|
||||
auto* row = lv_obj_create(parent);
|
||||
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_all(row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(row, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(row, false);
|
||||
auto* label = lv_label_create(row);
|
||||
lv_label_set_text(label, title);
|
||||
lv_obj_set_flex_grow(label, 1);
|
||||
return row;
|
||||
}
|
||||
|
||||
/** A hidden label in the theme's error colour */
|
||||
lv_obj_t* createErrorLabel(lv_obj_t* parent) {
|
||||
auto* label = lv_label_create(parent);
|
||||
lv_obj_set_width(label, LV_PCT(100));
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_MODE_WRAP);
|
||||
// The monochrome theme has no error colour
|
||||
if (!lvgl_theme_is_mono()) {
|
||||
LvglThemeSettings theme_settings;
|
||||
lvgl_theme_get_settings(&theme_settings);
|
||||
lv_obj_set_style_text_color(label, theme_settings.color_error, LV_STATE_DEFAULT);
|
||||
}
|
||||
lv_obj_set_hidden(label, true);
|
||||
return label;
|
||||
}
|
||||
|
||||
// TODO: Standardize dialogs
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
@ -247,76 +277,39 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
auto* card = lvgl_card_create(wrapper);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// SSID
|
||||
|
||||
auto* ssid_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_width(ssid_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(ssid_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(ssid_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_gap(ssid_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ssid_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* ssid_label_wrapper = lv_obj_create(ssid_wrapper);
|
||||
lv_obj_set_width(ssid_label_wrapper, LV_PCT(50));
|
||||
lv_obj_set_height(ssid_label_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_align(ssid_label_wrapper, LV_ALIGN_LEFT_MID, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ssid_label_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_left(ssid_label_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_right(ssid_label_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* ssid_label = lv_label_create(ssid_label_wrapper);
|
||||
lv_label_set_text(ssid_label, "Network:");
|
||||
|
||||
ctx->ssid_textarea = lv_textarea_create(ssid_wrapper);
|
||||
auto* ssid_row = createRow(card, "Network");
|
||||
ctx->ssid_textarea = lv_textarea_create(ssid_row);
|
||||
lv_textarea_set_one_line(ctx->ssid_textarea, true);
|
||||
lv_obj_align(ctx->ssid_textarea, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_set_width(ctx->ssid_textarea, LV_PCT(50));
|
||||
lv_obj_set_width(ctx->ssid_textarea, LV_PCT(60));
|
||||
|
||||
ctx->ssid_error = lv_label_create(wrapper);
|
||||
lv_obj_set_style_text_color(ctx->ssid_error, lv_color_make(255, 50, 50), LV_STATE_DEFAULT);
|
||||
lv_obj_add_flag(ctx->ssid_error, LV_OBJ_FLAG_HIDDEN);
|
||||
ctx->ssid_error = createErrorLabel(card);
|
||||
|
||||
// Password
|
||||
|
||||
auto* password_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_width(password_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(password_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(password_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_gap(password_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(password_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* password_label_wrapper = lv_obj_create(password_wrapper);
|
||||
lv_obj_set_width(password_label_wrapper, LV_PCT(50));
|
||||
lv_obj_set_height(password_label_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_align_to(password_label_wrapper, password_wrapper, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_style_border_width(password_label_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_left(password_label_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_right(password_label_wrapper, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* password_label = lv_label_create(password_label_wrapper);
|
||||
lv_label_set_text(password_label, "Password:");
|
||||
|
||||
ctx->password_textarea = lv_textarea_create(password_wrapper);
|
||||
auto* password_row = createRow(card, "Password");
|
||||
ctx->password_textarea = lv_textarea_create(password_row);
|
||||
lv_textarea_set_one_line(ctx->password_textarea, true);
|
||||
lv_textarea_set_password_mode(ctx->password_textarea, true);
|
||||
lv_obj_align(ctx->password_textarea, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_set_width(ctx->password_textarea, LV_PCT(50));
|
||||
lv_obj_set_width(ctx->password_textarea, LV_PCT(60));
|
||||
|
||||
ctx->password_error = lv_label_create(wrapper);
|
||||
lv_obj_set_style_text_color(ctx->password_error, lv_color_make(255, 50, 50), LV_STATE_DEFAULT);
|
||||
lv_obj_add_flag(ctx->password_error, LV_OBJ_FLAG_HIDDEN);
|
||||
ctx->password_error = createErrorLabel(card);
|
||||
|
||||
// Connection error
|
||||
ctx->connection_error = lv_label_create(wrapper);
|
||||
lv_obj_set_style_text_color(ctx->connection_error, lv_color_make(255, 50, 50), LV_STATE_DEFAULT);
|
||||
lv_obj_add_flag(ctx->connection_error, LV_OBJ_FLAG_HIDDEN);
|
||||
ctx->connection_error = createErrorLabel(card);
|
||||
|
||||
// Bottom buttons
|
||||
createBottomButtons(ctx, wrapper);
|
||||
createBottomButtons(ctx, card);
|
||||
|
||||
// Init from app parameters
|
||||
if (!ctx->initialSsid.empty()) {
|
||||
|
||||
@ -9,10 +9,12 @@
|
||||
#include <Tactility/Tactility.h>
|
||||
|
||||
#include <app/event.h>
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <tactility/log.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
|
||||
#include <wifi/wifi_settings.h>
|
||||
|
||||
@ -107,15 +109,34 @@ void View::showDetails(lv_event_t* event) {
|
||||
}
|
||||
}
|
||||
|
||||
void View::createSsidListItem(const WifiApRecord& record, bool isConnecting, size_t index) {
|
||||
/** Creates a title with a card below it, and returns the list in the card */
|
||||
static lv_obj_t* createSection(lv_obj_t* parent, const char* title) {
|
||||
auto* label = lv_label_create(parent);
|
||||
lv_label_set_text(label, title);
|
||||
|
||||
auto* card = lvgl_card_create(parent);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(card, 0, LV_STATE_DEFAULT);
|
||||
// The list items' pressed and focused backgrounds follow the card's rounded corners
|
||||
lv_obj_set_style_clip_corner(card, true, LV_STATE_DEFAULT);
|
||||
|
||||
// The card provides the background
|
||||
auto* list = lvgl_list_create(card);
|
||||
lv_obj_set_size(list, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(list, 0, LV_STATE_DEFAULT);
|
||||
return list;
|
||||
}
|
||||
|
||||
void View::createSsidListItem(lv_obj_t* list, const WifiApRecord& record, bool isConnecting, size_t index) {
|
||||
if (isConnecting) {
|
||||
auto* button = lv_list_add_button(networks_list, LV_SYMBOL_WIFI, record.ssid);
|
||||
auto* button = lvgl_list_add_button(list, LV_SYMBOL_WIFI, record.ssid);
|
||||
lv_obj_add_event_cb(button, showDetails, LV_EVENT_SHORT_CLICKED, this);
|
||||
} else {
|
||||
const std::string auth_info = (record.authentication_type == WIFI_AUTHENTICATION_TYPE_OPEN) ? "(open) " : " ";
|
||||
const auto percentage = mapRssiToPercentage(record.rssi);
|
||||
const auto label = std::format("{} {}{}%", std::string(record.ssid), auth_info, percentage);
|
||||
auto* button = lv_list_add_button(networks_list, nullptr, label.c_str());
|
||||
auto* button = lvgl_list_add_button(list, nullptr, label.c_str());
|
||||
lv_obj_set_user_data(button, reinterpret_cast<void*>(index));
|
||||
if (wifi_settings_contains(record.ssid)) {
|
||||
lv_obj_add_event_cb(button, showDetails, LV_EVENT_SHORT_CLICKED, this);
|
||||
@ -131,9 +152,9 @@ void View::updateConnectToHidden() {
|
||||
}
|
||||
|
||||
if (state->getRadioState() == WIFI_RADIO_STATE_ON) {
|
||||
lv_obj_remove_flag(connect_to_hidden, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(connect_to_hidden, false);
|
||||
} else {
|
||||
lv_obj_add_flag(connect_to_hidden, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(connect_to_hidden, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,22 +163,20 @@ void View::updateNetworkList() {
|
||||
|
||||
// Enable on boot
|
||||
|
||||
// An unstyled row insets the wrapper like the list items' text: margins would make a full-width item overflow the list
|
||||
auto* enable_on_boot_row = lv_obj_create(networks_list);
|
||||
lv_obj_remove_style_all(enable_on_boot_row);
|
||||
lv_obj_set_size(enable_on_boot_row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
const int32_t enable_on_boot_inset = (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) ? 2 : LV_DPX(16);
|
||||
lv_obj_set_style_pad_hor(enable_on_boot_row, enable_on_boot_inset, LV_STATE_DEFAULT);
|
||||
|
||||
auto* enable_on_boot_wrapper = lv_obj_create(enable_on_boot_row);
|
||||
auto* enable_on_boot_wrapper = lv_obj_create(networks_list);
|
||||
lv_obj_set_size(enable_on_boot_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(enable_on_boot_wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(enable_on_boot_wrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(enable_on_boot_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(enable_on_boot_wrapper, false);
|
||||
|
||||
auto* enable_label = lv_label_create(enable_on_boot_wrapper);
|
||||
lv_label_set_text(enable_label, "Enable on boot");
|
||||
lv_obj_align(enable_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_set_flex_grow(enable_label, 1);
|
||||
|
||||
enable_on_boot_switch = lv_switch_create(enable_on_boot_wrapper);
|
||||
lv_obj_align(enable_on_boot_switch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(enable_on_boot_switch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, bindings);
|
||||
lv_obj_add_event_cb(enable_on_boot_wrapper, onEnableOnBootParentClicked, LV_EVENT_SHORT_CLICKED, enable_on_boot_switch);
|
||||
|
||||
@ -180,15 +199,14 @@ void View::updateNetworkList() {
|
||||
for (int i = 0; i < ap_records.size(); ++i) {
|
||||
auto& record = ap_records[i];
|
||||
if (record.ssid == connection_target) {
|
||||
lv_list_add_text(networks_list, "Connected");
|
||||
createSsidListItem(record, false, i);
|
||||
createSsidListItem(createSection(networks_list, "Connected"), record, false, i);
|
||||
added_connected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lv_list_add_text(networks_list, "Other networks");
|
||||
auto* networks_section = createSection(networks_list, "Networks");
|
||||
std::set<std::string> used_ssids;
|
||||
if (!ap_records.empty()) {
|
||||
for (int i = 0; i < ap_records.size(); ++i) {
|
||||
@ -200,19 +218,18 @@ void View::updateNetworkList() {
|
||||
!connection_target.empty();
|
||||
bool skip = connection_target_match && added_connected;
|
||||
if (!skip) {
|
||||
createSsidListItem(record, is_connecting, i);
|
||||
createSsidListItem(networks_section, record, is_connecting, i);
|
||||
}
|
||||
used_ssids.insert(record.ssid);
|
||||
}
|
||||
}
|
||||
lv_obj_clear_flag(networks_list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(networks_list, false);
|
||||
} else if (!state->hasScannedAfterRadioOn() || state->isScanning()) {
|
||||
// hasScannedAfterRadioOn() prevents briefly showing "No networks found" when turning radio on.
|
||||
lv_obj_add_flag(networks_list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(networks_list, true);
|
||||
} else {
|
||||
lv_obj_clear_flag(networks_list, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_t* label = lv_label_create(networks_list);
|
||||
lv_label_set_text(label, "No networks found.");
|
||||
lv_obj_set_hidden(networks_list, false);
|
||||
lvgl_list_add_text(networks_section, "No networks found.");
|
||||
}
|
||||
|
||||
connect_to_hidden = lv_button_create(networks_list);
|
||||
@ -234,9 +251,9 @@ void View::updateNetworkList() {
|
||||
|
||||
void View::updateScanning() {
|
||||
if (state->getRadioState() == WIFI_RADIO_STATE_ON && state->getStationState() == WIFI_STATION_STATE_DISCONNECTED && state->isScanning()) {
|
||||
lv_obj_remove_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(scanning_spinner, false);
|
||||
} else {
|
||||
lv_obj_add_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(scanning_spinner, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -296,9 +313,11 @@ void View::init(uint32_t newAppInstanceId, lv_obj_t* parent) {
|
||||
|
||||
// Networks
|
||||
|
||||
networks_list = lv_list_create(parent);
|
||||
networks_list = lv_obj_create(parent);
|
||||
lv_obj_set_flex_grow(networks_list, 1);
|
||||
lv_obj_set_width(networks_list, LV_PCT(100));
|
||||
lv_obj_set_flex_flow(networks_list, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_border_width(networks_list, 0, LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
void View::update() {
|
||||
|
||||
@ -319,7 +319,7 @@ static void hidHostMouseReadCb(lv_indev_t* /*indev*/, lv_indev_data_t* data) {
|
||||
if (!hid_host_mouse_active.load()) {
|
||||
hid_host_mouse_active.store(true);
|
||||
if (hid_host_ctx && hid_host_ctx->mouseCursor) {
|
||||
lv_obj_remove_flag(hid_host_ctx->mouseCursor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(hid_host_ctx->mouseCursor, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -353,8 +353,8 @@ static void hidHostHandleMouseReport(const uint8_t* data, uint16_t len) {
|
||||
lv_indev_set_type(ms, LV_INDEV_TYPE_POINTER);
|
||||
lv_indev_set_read_cb(ms, hidHostMouseReadCb);
|
||||
auto* cur = lv_image_create(lv_layer_sys());
|
||||
lv_obj_remove_flag(cur, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_flag(cur, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_clickable(cur, false);
|
||||
lv_obj_set_hidden(cur, true);
|
||||
lv_image_set_src(cur, TT_ASSETS_UI_CURSOR);
|
||||
lv_indev_set_cursor(ms, cur);
|
||||
hid_host_ctx->mouseIndev = ms;
|
||||
|
||||
@ -80,7 +80,7 @@ static lv_obj_t* windowManagerScreenInit(lv_obj_t* root) {
|
||||
lv_obj_set_style_bg_opa(vertical_container, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(vertical_container, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_radius(vertical_container, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_remove_flag(vertical_container, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(vertical_container, false);
|
||||
|
||||
// The statusbar fits itself into the top of round and rounded displays
|
||||
auto* statusbar = lvgl::statusbar_create(vertical_container);
|
||||
@ -91,7 +91,7 @@ static lv_obj_t* windowManagerScreenInit(lv_obj_t* root) {
|
||||
lv_obj_set_width(app_container, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(app_container, 1);
|
||||
lv_obj_set_flex_flow(app_container, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_remove_flag(app_container, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(app_container, false);
|
||||
|
||||
// Keeps the apps inside the visible area of round displays.
|
||||
// On rounded displays, the widgets along the edges move their content inward from the corners instead.
|
||||
|
||||
@ -141,7 +141,7 @@ static void onTimeChanged(struct SystemEvent* /*event*/, void* /*context*/) {
|
||||
static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
|
||||
LV_UNUSED(class_p);
|
||||
LV_TRACE_OBJ_CREATE("begin");
|
||||
lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_scrollable(obj, false);
|
||||
LV_TRACE_OBJ_CREATE("finished");
|
||||
|
||||
// Deliberately does NOT subscribe to statusbar_data.pubsub here - that happens at the end of
|
||||
@ -163,11 +163,11 @@ static void statusbar_destructor(const lv_obj_class_t* class_p, lv_obj_t* obj) {
|
||||
static void update_icon(lv_obj_t* image, const StatusbarIcon* icon) {
|
||||
if (!icon->image.empty() && icon->visible && icon->claimed) {
|
||||
lv_image_set_src(image, icon->image.c_str());
|
||||
lv_obj_remove_flag(image, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(image, false);
|
||||
} else {
|
||||
lv_obj_add_flag(image, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(image, true);
|
||||
}
|
||||
lv_obj_set_flag(image, LV_OBJ_FLAG_CLICKABLE, !icon->appId.empty());
|
||||
lv_obj_set_clickable(image, !icon->appId.empty());
|
||||
}
|
||||
|
||||
static void onIconClicked(lv_event_t* event) {
|
||||
@ -197,8 +197,8 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
|
||||
lv_obj_set_style_pad_hor(obj, 2, LV_STATE_DEFAULT);
|
||||
lv_obj_center(obj);
|
||||
auto icon_size = lvgl_get_statusbar_icon_font_height();
|
||||
auto ui_density = lvgl_get_ui_density();
|
||||
auto icon_padding = (ui_density != LVGL_UI_DENSITY_COMPACT) ? static_cast<uint32_t>(icon_size * 0.2f) : 2;
|
||||
const bool is_compact = lvgl_theme_is_compact();
|
||||
auto icon_padding = !is_compact ? static_cast<uint32_t>(icon_size * 0.2f) : 2;
|
||||
|
||||
lv_display_t* display = lv_obj_get_display(obj);
|
||||
DisplayShape shape;
|
||||
@ -240,8 +240,8 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
|
||||
update_time(statusbar);
|
||||
|
||||
auto* left_spacer = lv_obj_create(obj);
|
||||
lv_obj_remove_flag(left_spacer, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_remove_flag(left_spacer, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_scrollable(left_spacer, false);
|
||||
lv_obj_set_clickable(left_spacer, false);
|
||||
lv_obj_set_size(left_spacer, 1, 1);
|
||||
lv_obj_set_flex_grow(left_spacer, 1);
|
||||
lv_obj_set_style_bg_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT);
|
||||
@ -252,7 +252,7 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
|
||||
|
||||
// The monochrome theme's focus ring doesn't contrast with the statusbar, so the icons get their own
|
||||
const bool own_focus_ring = lvgl_theme_is_mono();
|
||||
const int32_t focus_width = (ui_density != LVGL_UI_DENSITY_COMPACT) ? 2 : 1;
|
||||
const int32_t focus_width = !is_compact ? 2 : 1;
|
||||
const lv_color_t focus_color = lv_obj_get_style_text_color(obj, LV_PART_MAIN);
|
||||
|
||||
statusbar_data.mutex.lock(MAX_TICKS);
|
||||
|
||||
@ -11,6 +11,9 @@ void configureTheme(const settings::appearance::AppearanceSettings& settings) {
|
||||
theme_settings.is_dark = settings.themeMode == settings::appearance::ThemeMode::Dark;
|
||||
}
|
||||
theme_settings.is_mono = settings.monoTheme;
|
||||
if (settings.densityMode != settings::appearance::DensityMode::DeviceDefault) {
|
||||
theme_settings.is_compact = settings.densityMode == settings::appearance::DensityMode::Compact;
|
||||
}
|
||||
theme_settings.animations_enabled = settings.animationsEnabled;
|
||||
if (settings.primaryColor.has_value()) {
|
||||
theme_settings.color_primary = lv_color_hex(*settings.primaryColor);
|
||||
@ -21,6 +24,10 @@ void configureTheme(const settings::appearance::AppearanceSettings& settings) {
|
||||
if (settings.errorColor.has_value()) {
|
||||
theme_settings.color_error = lv_color_hex(*settings.errorColor);
|
||||
}
|
||||
if (settings.surfaceColor.has_value()) {
|
||||
theme_settings.surface_override = true;
|
||||
theme_settings.color_surface = lv_color_hex(settings::appearance::getSurfaceColor(*settings.surfaceColor, settings.surfaceTintLevel, theme_settings.is_dark));
|
||||
}
|
||||
lvgl_theme_set_settings(&theme_settings);
|
||||
}
|
||||
|
||||
|
||||
@ -193,7 +193,7 @@ static void addMouseIndev(UsbHidInputCtx* ctx) {
|
||||
lv_indev_set_user_data(ctx->mouse_indev, ctx);
|
||||
if (ctx->mouse_cursor != nullptr) {
|
||||
lv_indev_set_cursor(ctx->mouse_indev, ctx->mouse_cursor);
|
||||
lv_obj_remove_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->mouse_cursor, false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,7 +203,7 @@ static void removeMouseIndev(UsbHidInputCtx* ctx) {
|
||||
ctx->mouse_indev = nullptr;
|
||||
}
|
||||
if (ctx->mouse_cursor != nullptr) {
|
||||
lv_obj_add_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->mouse_cursor, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -385,9 +385,9 @@ void startUsbHidInput() {
|
||||
lv_obj_t* sys_layer = lv_layer_sys();
|
||||
if (sys_layer != nullptr) {
|
||||
ctx->mouse_cursor = lv_image_create(sys_layer);
|
||||
lv_obj_remove_flag(ctx->mouse_cursor, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_clickable(ctx->mouse_cursor, false);
|
||||
lv_image_set_src(ctx->mouse_cursor, TT_ASSETS_UI_CURSOR);
|
||||
lv_obj_add_flag(ctx->mouse_cursor, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(ctx->mouse_cursor, true);
|
||||
}
|
||||
lvgl_unlock();
|
||||
|
||||
|
||||
@ -110,7 +110,7 @@ void DisplayIdleService::activateScreensaver() {
|
||||
lv_obj_set_pos(screensaverOverlay, 0, 0);
|
||||
lv_obj_set_style_bg_color(screensaverOverlay, lv_color_black(), 0);
|
||||
lv_obj_set_style_bg_opa(screensaverOverlay, LV_OPA_COVER, 0);
|
||||
lv_obj_add_flag(screensaverOverlay, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_clickable(screensaverOverlay, true);
|
||||
lv_obj_add_event_cb(screensaverOverlay, stopScreensaverCb, LV_EVENT_CLICKED, this);
|
||||
|
||||
// Create and start the screensaver based on settings
|
||||
|
||||
@ -105,7 +105,7 @@ void MystifyScreensaver::update(lv_coord_t screenW, lv_coord_t screenH) {
|
||||
if (t >= validTrailCount) {
|
||||
for (int e = 0; e < NUM_VERTICES; e++) {
|
||||
if (trailLines[e]) {
|
||||
lv_obj_add_flag(trailLines[e], LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(trailLines[e], true);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
@ -118,7 +118,7 @@ void MystifyScreensaver::update(lv_coord_t screenW, lv_coord_t screenH) {
|
||||
for (int e = 0; e < NUM_VERTICES; e++) {
|
||||
if (!trailLines[e]) continue;
|
||||
|
||||
lv_obj_remove_flag(trailLines[e], LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_hidden(trailLines[e], false);
|
||||
|
||||
int v2 = (e + 1) % NUM_VERTICES;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user