mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
UX improvements (#686)
- Introduce "side" mode for system bar, versus classic statusbar on top with launcher bar below - Automatic system bar mode detection with override by Kconfig/device.properties and secondary override by user via Appereance settings app - Open a quick-settings menu from the status bar for available Wi-Fi, Bluetooth, GPS, and power controls. - Apps can display overlays with backdrop or Escape-key dismissal. - Added focus-container support and improved keyboard navigation, including Fn+Enter on Cardputer devices. - Power-off controls are available on supported device cards. - Updated status-bar layouts, simulator display dimensions, and toolbar navigation styling. - I²C scanning now supports selecting individual buses to rescan. - SD-card mount status is no longer shown in the status bar.
This commit is contained in:
parent
cb4ce5246d
commit
b9a96ca26d
@ -6,3 +6,21 @@ Two kernel modules cover LVGL:
|
||||
|
||||
- **`lvgl-module`** (`Modules/lvgl-module/`, `<lvgl/*.h>`) owns LVGL's lifecycle: init/deinit, the LVGL task loop, and `lvgl_lock()`/`lvgl_try_lock()`/`lvgl_unlock()` mutex-based locking that any task must hold before touching LVGL objects. It bridges Tactility's device model to LVGL indevs (`lvgl/devices/*.h`: `display`, `pointer`, `keyboard`, `trackball`), and provides shared fonts (`lvgl/fonts.h`: Montserrat text sizes, Material Symbols icon sets for statusbar/launcher/shared use) and a few shared widgets (`lvgl/widgets/*.h`: `toolbar`, `spinner`, `sliderbox`).
|
||||
- **`lvgl-window-manager-module`** (`Modules/lvgl-window-manager-module/`, `<lvgl_window_manager/*.h>`) manages a single stacked window per app instance on top of `lvgl-module`. `window_manager_start()`/`window_manager_stop()` create/tear down the root widget (plus optional chrome from a configured `WindowManagerScreenInitFn`); `window_manager_create()`/`window_manager_remove()` push/pop an app's window and (re)populate it via a `WindowCreateWidgetsFn`. Only the topmost window ever has live widgets - burying and resurfacing a window deletes and rebuilds its widget tree rather than hiding/showing it. That rebuild-on-remove path can run `create_widgets` on a *different* app's thread (whichever app's `window_manager_remove()` call caused this window to resurface), so `create_widgets` must only rebuild already-committed state, never decide what happens next - state transitions belong in the app's own `main()` event loop, driven by real `APP_EVENT_RESULT`s.
|
||||
|
||||
## Keyboard navigation
|
||||
|
||||
- Put groups of related focusable widgets (pills/chips, rows of buttons, toggle tiles) in a grid navigation container with `lvgl_grid_navigation_add()` (`lvgl/grid_navigation.h`). Only the container joins the input group: left/right moves within the group and up/down moves between groups.
|
||||
- Never make checkable widgets (switches, checkable buttons) direct input group members: LVGL toggles them on arrow keys. Grid navigation forwards only Enter to them.
|
||||
|
||||
## Theming (LSS)
|
||||
|
||||
The themes in `Modules/lvgl-module/source/themes/lv_theme_material*.c/.h` are generated: never edit them directly. They're compiled from the LVGL Style Sheets in `Modules/lvgl-module/themes/*.lss` by the LSS tool, a git submodule at `Modules/lvgl-module/lss/` (see its `README.md` for the syntax).
|
||||
|
||||
- Styles are defined once and attached to widget types with `apply(<widget>) { <style>: <state/part>; }` rules. Custom widget classes of `lvgl-module` (e.g. `lvgl_card_class`) are declared with `@widget <name>: <class>;`.
|
||||
- Change `material.lss` and `material_mono.lss` together, then regenerate (requires `pip install lark`):
|
||||
|
||||
```bash
|
||||
cd Modules/lvgl-module
|
||||
python lss/compile.py themes/material.lss --output source/themes
|
||||
python lss/compile.py themes/material_mono.lss --output source/themes
|
||||
```
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +1,4 @@
|
||||
.idea/
|
||||
.DS_Store
|
||||
|
||||
build*/
|
||||
!.github/actions/build*/
|
||||
@ -8,6 +7,8 @@ CMakeCache.txt
|
||||
*.cbp
|
||||
CMakeFiles
|
||||
|
||||
Data/data
|
||||
|
||||
release/
|
||||
|
||||
sdkconfig
|
||||
@ -29,3 +30,5 @@ sdkconfig.board.*.dev
|
||||
.clinerules/
|
||||
|
||||
__pycache__
|
||||
.DS_Store
|
||||
.cache
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -16,3 +16,6 @@
|
||||
[submodule "Libraries/cJSON/cJSON"]
|
||||
path = Libraries/cJSON/cJSON
|
||||
url = https://github.com/DaveGamble/cJSON.git
|
||||
[submodule "Modules/lvgl-module/lss"]
|
||||
path = Modules/lvgl-module/lss
|
||||
url = https://github.com/ByteWelder/LSS.git
|
||||
|
||||
@ -9,3 +9,4 @@ lvgl-window-manager-module
|
||||
service-module
|
||||
graphics-module
|
||||
wifi-module
|
||||
binfont-module
|
||||
|
||||
@ -86,3 +86,4 @@ terminal eb8e
|
||||
toolbar e9f7
|
||||
usb e1e0
|
||||
wifi e63e
|
||||
wifi_off e648
|
||||
|
||||
Binary file not shown.
@ -1,10 +1,12 @@
|
||||
/dts-v1/;
|
||||
|
||||
#include <tactility/bindings/root.h>
|
||||
#include <tactility/bindings/esp32_ble.h>
|
||||
#include <tactility/bindings/esp32_gpio.h>
|
||||
#include <tactility/bindings/esp32_grove.h>
|
||||
#include <tactility/bindings/esp32_i2c_master.h>
|
||||
#include <tactility/bindings/esp32_spi.h>
|
||||
#include <tactility/bindings/esp32_usbdevice.h>
|
||||
#include <tactility/bindings/esp32_sdspi.h>
|
||||
#include <tactility/bindings/esp32_wifi.h>
|
||||
|
||||
@ -22,7 +24,18 @@
|
||||
|
||||
wifi0 {
|
||||
compatible = "espressif,esp32-wifi";
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
usbdevice0 {
|
||||
compatible = "espressif,esp32-usbdevice";
|
||||
|
||||
usbdevicemsc0 {
|
||||
compatible = "espressif,esp32-usbdevice-msc";
|
||||
};
|
||||
};
|
||||
|
||||
ble0 {
|
||||
compatible = "espressif,esp32-ble";
|
||||
};
|
||||
|
||||
// Top Stemma/Qwiic port 1 shares this bus with the RTC (SDA: pin 1 <-> SCL: pin 2). External
|
||||
|
||||
@ -9,6 +9,7 @@ hardware.spiRamSpeed=80M
|
||||
hardware.spiRamXipDisabled=true
|
||||
hardware.esptoolFlashFreq=80M
|
||||
hardware.bluetooth=true
|
||||
hardware.tinyUsbMsc=true
|
||||
|
||||
storage.userDataLocation=SD
|
||||
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
/dts-v1/;
|
||||
|
||||
#include <tactility/bindings/root.h>
|
||||
#include <tactility/bindings/esp32_ble.h>
|
||||
#include <tactility/bindings/esp32_gpio.h>
|
||||
#include <tactility/bindings/esp32_grove.h>
|
||||
#include <tactility/bindings/esp32_i2c_master.h>
|
||||
#include <tactility/bindings/esp32_spi.h>
|
||||
#include <tactility/bindings/esp32_usbdevice.h>
|
||||
#include <tactility/bindings/esp32_sdspi.h>
|
||||
#include <tactility/bindings/esp32_wifi.h>
|
||||
|
||||
@ -25,7 +27,18 @@
|
||||
|
||||
wifi0 {
|
||||
compatible = "espressif,esp32-wifi";
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
usbdevice0 {
|
||||
compatible = "espressif,esp32-usbdevice";
|
||||
|
||||
usbdevicemsc0 {
|
||||
compatible = "espressif,esp32-usbdevice-msc";
|
||||
};
|
||||
};
|
||||
|
||||
ble0 {
|
||||
compatible = "espressif,esp32-ble";
|
||||
};
|
||||
|
||||
// Top Stemma/Qwiic port 1 shares this bus with the RTC (SDA: pin 1 <-> SCL: pin 2). External
|
||||
|
||||
@ -9,6 +9,7 @@ hardware.spiRamSpeed=80M
|
||||
hardware.spiRamXipDisabled=true
|
||||
hardware.esptoolFlashFreq=80M
|
||||
hardware.bluetooth=true
|
||||
hardware.tinyUsbMsc=true
|
||||
|
||||
storage.userDataLocation=SD
|
||||
|
||||
|
||||
@ -29,7 +29,6 @@
|
||||
|
||||
wifi0 {
|
||||
compatible = "espressif,esp32-wifi";
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
ble0 {
|
||||
|
||||
@ -18,5 +18,4 @@ display.dpi=227
|
||||
|
||||
lvgl.colorDepth=16
|
||||
lvgl.dpi=150
|
||||
lvgl.uiDensity=compact
|
||||
lvgl.fontSize=16
|
||||
|
||||
@ -16,3 +16,6 @@ display.dpi=139
|
||||
|
||||
lvgl.colorDepth=16
|
||||
lvgl.uiDensity=compact
|
||||
|
||||
ui.systemBarsPortrait=split
|
||||
ui.systemBarsLandscape=split
|
||||
|
||||
@ -17,4 +17,7 @@ display.dpi=139
|
||||
lvgl.colorDepth=16
|
||||
lvgl.uiDensity=compact
|
||||
|
||||
ui.systemBarsPortrait=split
|
||||
ui.systemBarsLandscape=split
|
||||
|
||||
sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y
|
||||
|
||||
@ -15,3 +15,6 @@ display.dpi=242
|
||||
|
||||
lvgl.colorDepth=16
|
||||
lvgl.uiDensity=compact
|
||||
|
||||
ui.systemBarsPortrait=split
|
||||
ui.systemBarsLandscape=split
|
||||
|
||||
@ -17,3 +17,6 @@ display.dpi=242
|
||||
|
||||
lvgl.colorDepth=16
|
||||
lvgl.uiDensity=compact
|
||||
|
||||
ui.systemBarsPortrait=split
|
||||
ui.systemBarsLandscape=split
|
||||
|
||||
@ -28,7 +28,6 @@
|
||||
|
||||
wifi0 {
|
||||
compatible = "espressif,esp32-wifi";
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
ble0 {
|
||||
|
||||
@ -29,7 +29,7 @@ static Driver* const simulator_drivers[] = {
|
||||
// These devices have no real bus to attach to (SDL has no notion of one), but every non-root
|
||||
// device is still expected to have a parent (see Device::parent) - they're parented to root once
|
||||
// it's available below.
|
||||
static const SdlDisplayConfig sdl_display_config = { 640, 480 };
|
||||
static const SdlDisplayConfig sdl_display_config = { 640, 320 };
|
||||
static Device sdl_display_device {};
|
||||
static Device sdl_pointer_device {};
|
||||
static Device sdl_keyboard_device {};
|
||||
|
||||
@ -20,3 +20,6 @@ display.dpi=143
|
||||
|
||||
lvgl.colorDepth=16
|
||||
lvgl.uiDensity=compact
|
||||
|
||||
ui.systemBarsPortrait=split
|
||||
ui.systemBarsLandscape=split
|
||||
|
||||
@ -19,3 +19,6 @@ display.dpi=261
|
||||
|
||||
lvgl.colorDepth=16
|
||||
lvgl.uiDensity=compact
|
||||
|
||||
ui.systemBarsPortrait=split
|
||||
ui.systemBarsLandscape=split
|
||||
|
||||
@ -20,3 +20,6 @@ display.dpi=265
|
||||
|
||||
lvgl.colorDepth=16
|
||||
lvgl.uiDensity=compact
|
||||
|
||||
ui.systemBarsPortrait=split
|
||||
ui.systemBarsLandscape=split
|
||||
|
||||
@ -18,3 +18,6 @@ display.dpi=247
|
||||
|
||||
lvgl.colorDepth=16
|
||||
lvgl.uiDensity=compact
|
||||
|
||||
ui.systemBarsPortrait=split
|
||||
ui.systemBarsLandscape=split
|
||||
@ -7,9 +7,8 @@
|
||||
|
||||
## Higher Priority
|
||||
|
||||
- esp_lvgl_port settings has a large stack size (~9kB) to fix stackoverflow when LVGL events (e.g. button click) do actions like file operations do actions like file operations. Can we reduce the callstack?
|
||||
- Improve posix support: use user home path and system /tmp folder. implement uart/i2c drivers properly.
|
||||
- Improve Setup: Add keyboard/keypad/encoder navigation explanation.
|
||||
e.g. encoder long press on t-lora pager to get out of textarea
|
||||
- NimBLE looses pairing key after reboot
|
||||
- Pick different font to improve monochrome screen readability (or change the weight).
|
||||
- Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual module.
|
||||
@ -31,17 +30,14 @@
|
||||
- netutils-module: ping, wget, ifconfig, nslookup, etc.
|
||||
- Core2: support power off via software
|
||||
- Improve Setup: Show "Step done" screen
|
||||
- Make it more clear to end-users that an SD card is required to run Tactility
|
||||
- Make it clearer to end-users that an SD card is required to run Tactility
|
||||
- Warn if file operations are done from prohibited tasks (e.g. lvgl task)
|
||||
- stopAppFromToolbar() in lvgl/Lvgl.cpp stops the top-most app. Change it so the toolbar knows for which app id it is created, so it can rely on that.
|
||||
- Consider not unpacking `.app` files and executing them directly. Might want to cache file offsets. Cache file must be pinned to app version.
|
||||
- lvgl-module's spinner relies on hard-coded spinner asset from Tactility main project.
|
||||
- esp_lvgl_port settings has a large stack size (~9kB) to fix stackoverflow when LVGL events (e.g. button click) do actions like file operations do actions like file operations. Can we reduce the callstack?
|
||||
- `struct Driver` has an `.owner`, but it's not always set. Either validate on Module construct that it matches, or otherwise set it during module start. The problem: NULL parent currently means that driver is not removable. This clashes with setting it dynamically. Consider some kind of flag to determine removability.
|
||||
- Consider moving certain drivers into separate modules: audio, bt, wifi, etc
|
||||
- Consider using https://github.com/Graphify-Labs/graphify
|
||||
- Consider implementing LVGL gridnav in apps https://lvgl.io/docs/open/9.3/details/auxiliary-modules/gridnav.html
|
||||
- TactilityTool: Make API compatibility table (and check for compatibility in the tool itself)
|
||||
- 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.
|
||||
@ -69,7 +65,6 @@
|
||||
- Show a warning screen if firmware encryption or secure boot are off when saving WiFi credentials.
|
||||
- Calculator app should show regular text input field on non-touch devices that have a keyboard (Cardputer, T-Lora Pager)
|
||||
- Allow for WSAD keys to navigate LVGL (this is extra nice for cardputer, but just handy in general)
|
||||
- Create a "How to" app for a device. It could explain things like keyboard navigation on first start.
|
||||
- Make WiFi setup app that starts an access point and hosts a webpage to set up the device.
|
||||
This will be useful for devices without a screen, a small screen or a non-touch screen.
|
||||
|
||||
|
||||
@ -256,6 +256,7 @@ static uint32_t scan_key(CardputerKeyboardInternal* internal) {
|
||||
if (has_regular) {
|
||||
switch (resolved_char) {
|
||||
case '`': return CODEPOINT_ESCAPE;
|
||||
case '\n': return CODEPOINT_ENTER;
|
||||
case ',': return CODEPOINT_ARROW_LEFT;
|
||||
case '/': return CODEPOINT_ARROW_RIGHT;
|
||||
case ';': return CODEPOINT_ARROW_UP;
|
||||
|
||||
@ -42,8 +42,8 @@ struct LcdInitCmd {
|
||||
uint8_t len;
|
||||
};
|
||||
|
||||
// No MADCTL (0x36): esp_lcd_panel_init() already sets it from rgb_ele_order, and esp_lcd's own swap_xy/mirror rewrite it from that value
|
||||
constexpr LcdInitCmd ST7789_INIT_CMDS[] = {
|
||||
{0x36, {0x08}, 1},
|
||||
{0x3A, {0x05}, 1},
|
||||
{0x20, {0}, 0},
|
||||
{0xB2, {0x0B, 0x0B, 0x00, 0x33, 0x33}, 5},
|
||||
|
||||
@ -177,6 +177,11 @@ shared_symbol_code_point_names = [
|
||||
"usb", # Power (settings) app
|
||||
"wifi", # WiFi (settings) app
|
||||
"bluetooth", # Bluetooth (settings) app
|
||||
# System menu toggles
|
||||
"wifi_off",
|
||||
"bluetooth_disabled",
|
||||
"location_on",
|
||||
"location_off",
|
||||
"terminal",
|
||||
"logo_dev",
|
||||
"camera",
|
||||
|
||||
@ -48,7 +48,7 @@ void lvgl_keyboard_remove(lv_indev_t* indev);
|
||||
lv_indev_t* lvgl_keyboard_find_by_device(struct Device* device);
|
||||
|
||||
/**
|
||||
* @brief Assigns the indev to the shared keyboard input group, so it can drive focus
|
||||
* @brief Assigns the indev to the active keyboard input group, so it can drive focus
|
||||
* navigation and input for focused widgets.
|
||||
* @warning Caller must hold the LVGL lock.
|
||||
*/
|
||||
@ -60,6 +60,20 @@ void lvgl_keyboard_enable(lv_indev_t* indev);
|
||||
*/
|
||||
void lvgl_keyboard_disable(lv_indev_t* indev);
|
||||
|
||||
/**
|
||||
* @brief Makes @a group the group that receives keypad input: it becomes the default group (new
|
||||
* widgets join it), and every keypad/encoder indev that is attached to a group is moved to it.
|
||||
* Indevs enabled later are attached to it too.
|
||||
* @warning Caller must hold the LVGL lock.
|
||||
* @param[in] group the group to activate, or NULL to activate the shared keyboard input group
|
||||
*/
|
||||
void lvgl_keyboard_set_active_group(lv_group_t* group);
|
||||
|
||||
/**
|
||||
* @return the shared keyboard input group, which is the default when no other group is active
|
||||
*/
|
||||
lv_group_t* lvgl_keyboard_get_app_group(void);
|
||||
|
||||
/**
|
||||
* @brief Adds the textarea to the shared keyboard navigation group (so any keypad indev -
|
||||
* hardware or on-screen - can type into it once it's focused), and, only when
|
||||
@ -136,7 +150,7 @@ bool lvgl_software_keyboard_is_enabled();
|
||||
struct LvglSoftwareKeyboard* lvgl_software_keyboard_get_last();
|
||||
|
||||
/**
|
||||
* @brief Attaches the shared keyboard navigation group to every currently registered keypad
|
||||
* @brief Attaches the active keyboard navigation group to every currently registered keypad
|
||||
* indev, so they can be used to navigate the on-screen keyboard and focused widgets.
|
||||
* @warning Caller must hold the LVGL lock.
|
||||
*/
|
||||
|
||||
@ -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 0xD5578751u
|
||||
#define LVGL_ICON_FONT_VERSION 0x3ED2CAAAu
|
||||
|
||||
// Icon names per icon font, resolved through the .codepoints file next to the TTF
|
||||
extern const char* const lvgl_icon_shared_names[];
|
||||
|
||||
@ -50,6 +50,10 @@
|
||||
#define LVGL_ICON_SHARED_USB "\xEE\x87\xA0"
|
||||
#define LVGL_ICON_SHARED_WIFI "\xEE\x98\xBE"
|
||||
#define LVGL_ICON_SHARED_BLUETOOTH "\xEE\x86\xA7"
|
||||
#define LVGL_ICON_SHARED_WIFI_OFF "\xEE\x99\x88"
|
||||
#define LVGL_ICON_SHARED_BLUETOOTH_DISABLED "\xEE\x86\xA9"
|
||||
#define LVGL_ICON_SHARED_LOCATION_ON "\xEF\x87\x9B"
|
||||
#define LVGL_ICON_SHARED_LOCATION_OFF "\xEE\x83\x87"
|
||||
#define LVGL_ICON_SHARED_TERMINAL "\xEE\xAE\x8E"
|
||||
#define LVGL_ICON_SHARED_LOGO_DEV "\xEE\xAB\x96"
|
||||
#define LVGL_ICON_SHARED_CAMERA "\xEE\x8E\xAF"
|
||||
|
||||
20
Modules/lvgl-module/include/lvgl/widgets/focus_container.h
Normal file
20
Modules/lvgl-module/include/lvgl/widgets/focus_container.h
Normal file
@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Creates a focus container: a transparent container without padding that the theme only styles with a focus ring.
|
||||
* Use it to make a group of widgets selectable as one, e.g. with a click handler on the container.
|
||||
* @param[in] parent the parent object for the new container
|
||||
* @return the created container
|
||||
*/
|
||||
lv_obj_t* lvgl_focus_container_create(lv_obj_t* parent);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
1
Modules/lvgl-module/lss
Submodule
1
Modules/lvgl-module/lss
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit e9c58a2bb506c0e1ef4ef4bc8c91e996be036163
|
||||
@ -17,6 +17,8 @@ static LvglSoftwareKeyboard last_software_keyboard = {
|
||||
};
|
||||
|
||||
static lv_group_t* keyboard_group;
|
||||
// The group that keypad indevs are attached to: keyboard_group, or the group of an overlay
|
||||
static lv_group_t* active_group;
|
||||
|
||||
extern "C" {
|
||||
|
||||
@ -28,6 +30,7 @@ void lvgl_keyboard_on_start_lvgl() {
|
||||
// but gets all widgets by default. This is a temporary work-around until a proper default group is
|
||||
// created to fix the trackball issue (see trackball.cpp and ideas.md, search for "group")
|
||||
lv_group_set_default(keyboard_group);
|
||||
active_group = keyboard_group;
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
@ -41,6 +44,24 @@ void lvgl_keyboard_on_stop_lvgl() {
|
||||
lvgl_unlock();
|
||||
|
||||
keyboard_group = nullptr;
|
||||
active_group = nullptr;
|
||||
}
|
||||
|
||||
void lvgl_keyboard_set_active_group(lv_group_t* group) {
|
||||
active_group = group != nullptr ? group : keyboard_group;
|
||||
lv_group_set_default(active_group);
|
||||
auto* indev = lv_indev_get_next(nullptr);
|
||||
while (indev != nullptr) {
|
||||
const lv_indev_type_t type = lv_indev_get_type(indev);
|
||||
if ((type == LV_INDEV_TYPE_KEYPAD || type == LV_INDEV_TYPE_ENCODER) && lv_indev_get_group(indev) != nullptr) {
|
||||
lv_indev_set_group(indev, active_group);
|
||||
}
|
||||
indev = lv_indev_get_next(indev);
|
||||
}
|
||||
}
|
||||
|
||||
lv_group_t* lvgl_keyboard_get_app_group() {
|
||||
return keyboard_group;
|
||||
}
|
||||
|
||||
// KeyboardKeyData::key is always a Unicode codepoint (see its doc comment / the CodePoint enum) -
|
||||
@ -176,8 +197,8 @@ lv_indev_t* lvgl_keyboard_find_by_device(Device* device) {
|
||||
}
|
||||
|
||||
void lvgl_keyboard_enable(lv_indev_t* indev) {
|
||||
check(keyboard_group != nullptr);
|
||||
lv_indev_set_group(indev, keyboard_group);
|
||||
check(active_group != nullptr);
|
||||
lv_indev_set_group(indev, active_group);
|
||||
}
|
||||
|
||||
void lvgl_keyboard_disable(lv_indev_t* indev) {
|
||||
@ -295,10 +316,10 @@ void lvgl_keyboard_add_textarea(LvglSoftwareKeyboard* keyboard, lv_obj_t* textar
|
||||
|
||||
void lvgl_software_keyboard_activate(LvglSoftwareKeyboard* keyboard) {
|
||||
auto* indev = lv_indev_get_next(nullptr);
|
||||
check(keyboard_group);
|
||||
check(active_group);
|
||||
while (indev) {
|
||||
if (lv_indev_get_type(indev) == LV_INDEV_TYPE_KEYPAD) {
|
||||
lv_indev_set_group(indev, keyboard_group);
|
||||
lv_indev_set_group(indev, active_group);
|
||||
}
|
||||
indev = lv_indev_get_next(indev);
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ static lv_obj_t* find_edge_row_child(lv_obj_t* container, bool top, lv_obj_t* re
|
||||
if (!found) {
|
||||
return nullptr;
|
||||
}
|
||||
if (reference != nullptr && get_center_y(reference) == edge_y) {
|
||||
if (reference != nullptr && is_focusable(reference) && get_center_y(reference) == edge_y) {
|
||||
return reference;
|
||||
}
|
||||
const int32_t reference_x = reference != nullptr ? get_center_x(reference) : 0;
|
||||
@ -95,6 +95,34 @@ static void on_container_key(lv_event_t* event) {
|
||||
scroll_to_focused_child(lv_event_get_current_target_obj(event));
|
||||
}
|
||||
|
||||
static bool has_focusable_child(lv_obj_t* container) {
|
||||
const uint32_t child_count = lv_obj_get_child_count(container);
|
||||
for (uint32_t i = 0; i < child_count; i++) {
|
||||
if (is_focusable(lv_obj_get_child(container, static_cast<int32_t>(i)))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Grid navigation ignores the arrow keys when the container has nothing to focus (e.g. only an "empty" label),
|
||||
// so they move the focus to the previous or next widget instead
|
||||
static void on_container_key_preprocess(lv_event_t* event) {
|
||||
lv_obj_t* container = lv_event_get_current_target_obj(event);
|
||||
lv_group_t* group = lv_obj_get_group(container);
|
||||
if (group == nullptr || has_focusable_child(container)) {
|
||||
return;
|
||||
}
|
||||
const uint32_t key = lv_event_get_key(event);
|
||||
if (key == LV_KEY_DOWN || key == LV_KEY_RIGHT) {
|
||||
lv_group_focus_next(group);
|
||||
lv_event_stop_processing(event);
|
||||
} else if (key == LV_KEY_UP || key == LV_KEY_LEFT) {
|
||||
lv_group_focus_prev(group);
|
||||
lv_event_stop_processing(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) {
|
||||
@ -114,6 +142,10 @@ static void on_container_focused(lv_event_t* event) {
|
||||
} 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);
|
||||
}
|
||||
// The restored child can be hidden by now, e.g. a page button when there's only one page
|
||||
if (child == nullptr && focused != nullptr && !is_focusable(focused)) {
|
||||
child = find_focusable_child(container, -1, true);
|
||||
}
|
||||
if (child != nullptr && child != focused) {
|
||||
lv_gridnav_set_focused(container, child, LV_ANIM_ON);
|
||||
}
|
||||
@ -135,6 +167,7 @@ void lvgl_grid_navigation_add(lv_obj_t* container) {
|
||||
// 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_event_cb(container, on_container_key, LV_EVENT_KEY, nullptr);
|
||||
lv_obj_add_event_cb(container, on_container_key_preprocess, static_cast<lv_event_code_t>(LV_EVENT_KEY | LV_EVENT_PREPROCESS), nullptr);
|
||||
|
||||
const uint32_t child_count = lv_obj_get_child_count(container);
|
||||
for (uint32_t i = 0; i < child_count; i++) {
|
||||
@ -160,6 +193,7 @@ void lvgl_grid_navigation_remove(lv_obj_t* container) {
|
||||
lv_obj_remove_event_cb(container, on_child_created);
|
||||
lv_obj_remove_event_cb(container, on_container_focused);
|
||||
lv_obj_remove_event_cb(container, on_container_key);
|
||||
lv_obj_remove_event_cb(container, on_container_key_preprocess);
|
||||
|
||||
if (group == nullptr) {
|
||||
return;
|
||||
|
||||
@ -52,6 +52,10 @@ const char* const lvgl_icon_shared_names[] = {
|
||||
"usb",
|
||||
"wifi",
|
||||
"bluetooth",
|
||||
"wifi_off",
|
||||
"bluetooth_disabled",
|
||||
"location_on",
|
||||
"location_off",
|
||||
"terminal",
|
||||
"logo_dev",
|
||||
"camera",
|
||||
|
||||
@ -8,14 +8,17 @@
|
||||
#include <lvgl/devices/indev.h>
|
||||
#include <lvgl/devices/keyboard.h>
|
||||
#include <lvgl/devices/pointer.h>
|
||||
#include <lvgl/devices/trackball.h>
|
||||
|
||||
#include <lvgl/widgets/sliderbox.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/insets.h>
|
||||
#include <lvgl/ppa.h>
|
||||
#include <lvgl/theme.h>
|
||||
#include <lvgl/widgets/badge.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/chip.h>
|
||||
#include <lvgl/widgets/focus_container.h>
|
||||
#include <lvgl/widgets/icon_button.h>
|
||||
#include <lvgl/widgets/list.h>
|
||||
#include <lvgl/widgets/page_indicator.h>
|
||||
@ -52,6 +55,8 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(lvgl_get_launcher_icon_font_height),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_get_statusbar_icon_font),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_get_statusbar_icon_font_height),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_set_text_font),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_set_icon_font),
|
||||
// lvgl_display
|
||||
DEFINE_MODULE_SYMBOL(lvgl_display_add),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_display_remove),
|
||||
@ -60,6 +65,23 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
||||
// lvgl_keyboard
|
||||
DEFINE_MODULE_SYMBOL(lvgl_keyboard_add),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_keyboard_remove),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_keyboard_find_by_device),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_keyboard_enable),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_keyboard_disable),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_keyboard_set_active_group),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_keyboard_get_app_group),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_keyboard_add_textarea),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_hardware_keyboard_is_available),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_hardware_keyboard_add_custom),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_hardware_keyboard_remove_custom),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_software_keyboard_construct),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_software_keyboard_destruct),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_software_keyboard_show),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_software_keyboard_hide),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_software_keyboard_is_enabled),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_software_keyboard_get_last),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_software_keyboard_activate),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_software_keyboard_deactivate),
|
||||
// lvgl_pointer
|
||||
DEFINE_MODULE_SYMBOL(lvgl_pointer_set_calibration),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_pointer_get_calibration),
|
||||
@ -67,6 +89,19 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(lvgl_pointer_add),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_pointer_get_slot_index),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_pointer_remove),
|
||||
// lvgl_trackball
|
||||
DEFINE_MODULE_SYMBOL(lvgl_trackball_add),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_trackball_remove),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_trackball_get_settings),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_trackball_set_settings),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_trackball_settings_get_default),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_trackball_set_cursor_image),
|
||||
// lvgl_ppa
|
||||
DEFINE_MODULE_SYMBOL(lvgl_ppa_is_supported),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_ppa_supports_color_format),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_ppa_get_or_create),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_ppa_delete),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_ppa_rotate),
|
||||
// lvgl_grid_navigation
|
||||
DEFINE_MODULE_SYMBOL(lvgl_grid_navigation_add),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_grid_navigation_remove),
|
||||
@ -87,6 +122,9 @@ const struct ModuleSymbol lvgl_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(lvgl_theme_is_compact),
|
||||
// lvgl_badge
|
||||
DEFINE_MODULE_SYMBOL(lvgl_badge_create),
|
||||
// lvgl_focus_container
|
||||
DEFINE_MODULE_SYMBOL(lvgl_focus_container_create),
|
||||
// lvgl_page_indicator
|
||||
DEFINE_MODULE_SYMBOL(lvgl_page_indicator_create),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_page_indicator_set_page_count),
|
||||
DEFINE_MODULE_SYMBOL(lvgl_page_indicator_set_page),
|
||||
|
||||
@ -14,6 +14,7 @@ extern const lv_obj_class_t lvgl_icon_button_filled_class;
|
||||
extern const lv_obj_class_t lvgl_icon_button_tonal_class;
|
||||
extern const lv_obj_class_t lvgl_sliderbox_class;
|
||||
extern const lv_obj_class_t lvgl_card_class;
|
||||
extern const lv_obj_class_t lvgl_focus_container_class;
|
||||
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;
|
||||
@ -49,7 +50,7 @@ extern const lv_obj_class_t lvgl_list_button_class;
|
||||
#define CORNER_LARGE (LV_DPX_CALC(theme->disp_dpi, 16))
|
||||
#define CORNER_EXTRA_LARGE (LV_DPX_CALC(theme->disp_dpi, 28))
|
||||
#define CORNER_FULL LV_RADIUS_CIRCLE
|
||||
#define CORNER_COMPACT 3
|
||||
#define CORNER_COMPACT 4
|
||||
#define SPACE_1 (LV_DPX_CALC(theme->disp_dpi, 4))
|
||||
#define SPACE_2 (LV_DPX_CALC(theme->disp_dpi, 8))
|
||||
#define SPACE_3 (LV_DPX_CALC(theme->disp_dpi, 12))
|
||||
@ -184,6 +185,7 @@ extern const lv_obj_class_t lvgl_list_button_class;
|
||||
#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_PAD_RIGHT (theme->config.is_compact ? APP_BAR_PAD_HOR + 2 : APP_BAR_PAD_HOR)
|
||||
#define APP_BAR_GAP (theme->config.is_compact ? 2 : SPACE_1)
|
||||
#define APP_BAR_FOCUS_WIDTH FOCUS_INSET_WIDTH
|
||||
#define FOCUS_RING_COLOR FOCUS_COLOR
|
||||
@ -383,6 +385,8 @@ typedef struct {
|
||||
lv_style_t icon_button_tonal;
|
||||
lv_style_t icon_button_tonal_pressed;
|
||||
lv_style_t app_bar;
|
||||
lv_style_t app_bar_nav_button;
|
||||
lv_style_t app_bar_button_focus_ring;
|
||||
lv_style_t app_bar_button;
|
||||
lv_style_t app_bar_button_pressed;
|
||||
lv_style_t app_bar_focus_ring;
|
||||
@ -907,9 +911,20 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_radius(&theme->styles.app_bar, 0);
|
||||
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_right(&theme->styles.app_bar, APP_BAR_PAD_RIGHT);
|
||||
lv_style_set_pad_ver(&theme->styles.app_bar, 0);
|
||||
lv_style_set_pad_gap(&theme->styles.app_bar, APP_BAR_GAP);
|
||||
|
||||
style_init_reset(&theme->styles.app_bar_nav_button);
|
||||
if(theme->config.is_compact) {
|
||||
lv_style_set_margin_left(&theme->styles.app_bar_nav_button, 4);
|
||||
}
|
||||
|
||||
style_init_reset(&theme->styles.app_bar_button_focus_ring);
|
||||
if(theme->config.is_compact) {
|
||||
lv_style_set_outline_pad(&theme->styles.app_bar_button_focus_ring, 0);
|
||||
}
|
||||
|
||||
style_init_reset(&theme->styles.app_bar_button);
|
||||
lv_style_set_pad_all(&theme->styles.app_bar_button, 0);
|
||||
lv_style_set_text_color(&theme->styles.app_bar_button, APP_BAR_CONTENT_COLOR);
|
||||
@ -1723,6 +1738,21 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
}
|
||||
#endif
|
||||
if(lv_obj_check_type(obj, &lvgl_icon_button_class)) {
|
||||
/* toolbar > icon_button[0] */
|
||||
if(lv_obj_check_type(parent, &lvgl_toolbar_class) && lss_is_child(parent, 0, obj)) {
|
||||
lv_obj_add_style(obj, &theme->styles.icon_button, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_button, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_nav_button, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.transition_exit, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_button_pressed, LV_STATE_PRESSED);
|
||||
lv_obj_add_style(obj, &theme->styles.transition_enter, LV_STATE_PRESSED);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_focus_ring, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_focus_ring, LV_STATE_EDITED);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_button_focus_ring, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_button_focus_ring, LV_STATE_EDITED);
|
||||
lv_obj_add_style(obj, &theme->styles.disabled, LV_STATE_DISABLED);
|
||||
return;
|
||||
}
|
||||
/* toolbar > icon_button */
|
||||
if(lv_obj_check_type(parent, &lvgl_toolbar_class)) {
|
||||
lv_obj_add_style(obj, &theme->styles.icon_button, 0);
|
||||
@ -1732,6 +1762,8 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
lv_obj_add_style(obj, &theme->styles.transition_enter, LV_STATE_PRESSED);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_focus_ring, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_focus_ring, LV_STATE_EDITED);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_button_focus_ring, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_button_focus_ring, LV_STATE_EDITED);
|
||||
lv_obj_add_style(obj, &theme->styles.disabled, LV_STATE_DISABLED);
|
||||
return;
|
||||
}
|
||||
@ -1925,6 +1957,11 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
lv_obj_add_style(obj, &theme->styles.scrollbar, LV_PART_SCROLLBAR);
|
||||
return;
|
||||
}
|
||||
if(lv_obj_check_type(obj, &lvgl_focus_container_class)) {
|
||||
/* focus_container */
|
||||
lv_obj_add_style(obj, &theme->styles.focus_ring, LV_STATE_FOCUS_KEY);
|
||||
return;
|
||||
}
|
||||
if(lv_obj_check_type(obj, &lvgl_sliderbox_class)) {
|
||||
/* sliderbox */
|
||||
lv_obj_add_style(obj, &theme->styles.sliderbox, 0);
|
||||
|
||||
@ -14,6 +14,7 @@ extern const lv_obj_class_t lvgl_icon_button_filled_class;
|
||||
extern const lv_obj_class_t lvgl_icon_button_tonal_class;
|
||||
extern const lv_obj_class_t lvgl_sliderbox_class;
|
||||
extern const lv_obj_class_t lvgl_card_class;
|
||||
extern const lv_obj_class_t lvgl_focus_container_class;
|
||||
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;
|
||||
@ -29,7 +30,7 @@ extern const lv_obj_class_t lvgl_list_button_class;
|
||||
#define CORNER_MEDIUM (LV_DPX_CALC(theme->disp_dpi, 8))
|
||||
#define CORNER_LARGE (LV_DPX_CALC(theme->disp_dpi, 12))
|
||||
#define CORNER_FULL LV_RADIUS_CIRCLE
|
||||
#define CORNER_COMPACT 2
|
||||
#define CORNER_COMPACT 4
|
||||
#define SPACE_1 (LV_DPX_CALC(theme->disp_dpi, 2))
|
||||
#define SPACE_2 (LV_DPX_CALC(theme->disp_dpi, 4))
|
||||
#define SPACE_3 (LV_DPX_CALC(theme->disp_dpi, 6))
|
||||
@ -118,6 +119,7 @@ extern const lv_obj_class_t lvgl_list_button_class;
|
||||
#define ICON_BUTTON_TONAL_BORDER_COLOR COLOR_OUTLINE
|
||||
#define APP_BAR_TITLE_COLOR COLOR_ON_SURFACE
|
||||
#define APP_BAR_PAD_HOR (theme->config.is_compact ? 0 : SPACE_1)
|
||||
#define APP_BAR_PAD_RIGHT (theme->config.is_compact ? APP_BAR_PAD_HOR + 2 : APP_BAR_PAD_HOR)
|
||||
#define APP_BAR_GAP (theme->config.is_compact ? 2 : SPACE_1)
|
||||
#define APP_BAR_FOCUS_WIDTH FOCUS_INSET_WIDTH
|
||||
#define FOCUS_RING_COLOR COLOR_ON_SURFACE
|
||||
@ -240,6 +242,8 @@ typedef struct {
|
||||
lv_style_t icon_button_filled_pressed;
|
||||
lv_style_t icon_button_tonal;
|
||||
lv_style_t app_bar;
|
||||
lv_style_t app_bar_nav_button;
|
||||
lv_style_t app_bar_button_focus_ring;
|
||||
lv_style_t app_bar_button;
|
||||
lv_style_t app_bar_focus_ring;
|
||||
#if LV_USE_SWITCH
|
||||
@ -644,9 +648,20 @@ static void style_init(my_theme_t * theme)
|
||||
lv_style_set_radius(&theme->styles.app_bar, 0);
|
||||
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_right(&theme->styles.app_bar, APP_BAR_PAD_RIGHT);
|
||||
lv_style_set_pad_ver(&theme->styles.app_bar, 0);
|
||||
lv_style_set_pad_gap(&theme->styles.app_bar, APP_BAR_GAP);
|
||||
|
||||
style_init_reset(&theme->styles.app_bar_nav_button);
|
||||
if(theme->config.is_compact) {
|
||||
lv_style_set_margin_left(&theme->styles.app_bar_nav_button, 4);
|
||||
}
|
||||
|
||||
style_init_reset(&theme->styles.app_bar_button_focus_ring);
|
||||
if(theme->config.is_compact) {
|
||||
lv_style_set_outline_pad(&theme->styles.app_bar_button_focus_ring, 0);
|
||||
}
|
||||
|
||||
style_init_reset(&theme->styles.app_bar_button);
|
||||
lv_style_set_pad_all(&theme->styles.app_bar_button, 0);
|
||||
|
||||
@ -1334,6 +1349,19 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
}
|
||||
#endif
|
||||
if(lv_obj_check_type(obj, &lvgl_icon_button_class)) {
|
||||
/* toolbar > icon_button[0] */
|
||||
if(lv_obj_check_type(parent, &lvgl_toolbar_class) && lss_is_child(parent, 0, obj)) {
|
||||
lv_obj_add_style(obj, &theme->styles.icon_button, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_button, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_nav_button, 0);
|
||||
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_PRESSED);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_focus_ring, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_focus_ring, LV_STATE_EDITED);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_button_focus_ring, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_button_focus_ring, LV_STATE_EDITED);
|
||||
lv_obj_add_style(obj, &theme->styles.disabled, LV_STATE_DISABLED);
|
||||
return;
|
||||
}
|
||||
/* toolbar > icon_button */
|
||||
if(lv_obj_check_type(parent, &lvgl_toolbar_class)) {
|
||||
lv_obj_add_style(obj, &theme->styles.icon_button, 0);
|
||||
@ -1341,6 +1369,8 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
lv_obj_add_style(obj, &theme->styles.inverted, LV_STATE_PRESSED);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_focus_ring, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_focus_ring, LV_STATE_EDITED);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_button_focus_ring, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_add_style(obj, &theme->styles.app_bar_button_focus_ring, LV_STATE_EDITED);
|
||||
lv_obj_add_style(obj, &theme->styles.disabled, LV_STATE_DISABLED);
|
||||
return;
|
||||
}
|
||||
@ -1486,6 +1516,11 @@ static void theme_apply(lv_theme_t * th, lv_obj_t * obj)
|
||||
lv_obj_add_style(obj, &theme->styles.scrollbar, LV_PART_SCROLLBAR);
|
||||
return;
|
||||
}
|
||||
if(lv_obj_check_type(obj, &lvgl_focus_container_class)) {
|
||||
/* focus_container */
|
||||
lv_obj_add_style(obj, &theme->styles.focus_ring, LV_STATE_FOCUS_KEY);
|
||||
return;
|
||||
}
|
||||
if(lv_obj_check_type(obj, &lvgl_sliderbox_class)) {
|
||||
/* sliderbox */
|
||||
lv_obj_add_style(obj, &theme->styles.sliderbox, 0);
|
||||
|
||||
27
Modules/lvgl-module/source/widgets/focus_container.cpp
Normal file
27
Modules/lvgl-module/source/widgets/focus_container.cpp
Normal file
@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#define LV_USE_PRIVATE_API 1 // For the lv_obj_class_t declaration
|
||||
|
||||
#include <lvgl/widgets/focus_container.h>
|
||||
|
||||
// Not static: the theme styles focus containers by this class
|
||||
extern "C" const lv_obj_class_t lvgl_focus_container_class = {
|
||||
.base_class = &lv_obj_class,
|
||||
.constructor_cb = nullptr,
|
||||
.destructor_cb = nullptr,
|
||||
.event_cb = nullptr,
|
||||
.user_data = nullptr,
|
||||
.name = "focus_container",
|
||||
.width_def = LV_SIZE_CONTENT,
|
||||
.height_def = LV_SIZE_CONTENT,
|
||||
.editable = LV_OBJ_CLASS_EDITABLE_INHERIT,
|
||||
.group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE,
|
||||
.instance_size = 0,
|
||||
.theme_inheritable = LV_OBJ_CLASS_THEME_INHERITABLE_FALSE
|
||||
};
|
||||
|
||||
lv_obj_t* lvgl_focus_container_create(lv_obj_t* parent) {
|
||||
lv_obj_t* obj = lv_obj_class_create_obj(&lvgl_focus_container_class, parent);
|
||||
lv_obj_class_init_obj(obj);
|
||||
lv_obj_set_scrollable(obj, false);
|
||||
return obj;
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
#include <lvgl/widgets/sliderbox.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
|
||||
typedef struct {
|
||||
lv_obj_t obj;
|
||||
@ -175,6 +176,9 @@ lv_obj_t* lvgl_sliderbox_create(lv_obj_t* parent, int32_t min, int32_t max, int3
|
||||
|
||||
sliderBox->plusButton = create_step_button(obj, LV_SYMBOL_PLUS, &on_plus_button_clicked, obj, button_size);
|
||||
|
||||
// The sliderbox is one stop for the keys: left and right move between the buttons and the slider, up and down leave it
|
||||
lvgl_grid_navigation_add(obj);
|
||||
|
||||
lvgl_sliderbox_set_value(obj, value, LV_ANIM_OFF);
|
||||
|
||||
return obj;
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
@widget icon_button_tonal: lvgl_icon_button_tonal_class;
|
||||
@widget sliderbox: lvgl_sliderbox_class;
|
||||
@widget card: lvgl_card_class;
|
||||
@widget focus_container: lvgl_focus_container_class;
|
||||
@widget chip: lvgl_chip_class;
|
||||
@widget badge: lvgl_badge_class;
|
||||
@widget page_indicator: lvgl_page_indicator_class;
|
||||
@ -74,7 +75,7 @@
|
||||
@corner-large: 16dp;
|
||||
@corner-extra-large: 28dp;
|
||||
@corner-full: circle;
|
||||
@corner-compact: 3;
|
||||
@corner-compact: 4;
|
||||
|
||||
// Spacing scale (4dp grid)
|
||||
@space-1: 4dp;
|
||||
@ -266,6 +267,8 @@
|
||||
@app-bar-content-color: @color-on-surface;
|
||||
@app-bar-pressed-opacity: 20%;
|
||||
@app-bar-pad-hor: if($isCompact, 0, @space-1);
|
||||
// Keeps the action buttons off the screen's edge in the compact layout
|
||||
@app-bar-pad-right: if($isCompact, @app-bar-pad-hor + 2, @app-bar-pad-hor);
|
||||
@app-bar-gap: if($isCompact, 2, @space-1);
|
||||
// The focus ring is drawn inside toolbar buttons, as there's little room around them
|
||||
@app-bar-focus-width: @focus-inset-width;
|
||||
@ -728,10 +731,21 @@ app_bar {
|
||||
radius: 0;
|
||||
borderWidth: 0;
|
||||
padHor: @app-bar-pad-hor;
|
||||
padRight: @app-bar-pad-right;
|
||||
padVer: 0;
|
||||
padGap: @app-bar-gap;
|
||||
}
|
||||
|
||||
// The toolbar's first button: the close (navigation) button
|
||||
app_bar_nav_button {
|
||||
@if ($isCompact) { marginLeft: 4; }
|
||||
}
|
||||
|
||||
// Compact toolbar buttons are as big as their icon, so their focus ring goes around the outside instead of the inside
|
||||
app_bar_button_focus_ring {
|
||||
@if ($isCompact) { outlinePad: 0; }
|
||||
}
|
||||
|
||||
app_bar_button {
|
||||
padAll: 0;
|
||||
textColor: @app-bar-content-color;
|
||||
@ -1329,6 +1343,20 @@ apply(button) {
|
||||
button_disabled: state.disabled;
|
||||
}
|
||||
|
||||
apply(toolbar > icon_button[0]) {
|
||||
icon_button: default;
|
||||
app_bar_button: default;
|
||||
app_bar_nav_button: default;
|
||||
transition_exit: default;
|
||||
app_bar_button_pressed: state.pressed;
|
||||
transition_enter: state.pressed;
|
||||
app_bar_focus_ring: state.focusKey;
|
||||
app_bar_focus_ring: state.edited;
|
||||
app_bar_button_focus_ring: state.focusKey;
|
||||
app_bar_button_focus_ring: state.edited;
|
||||
disabled: state.disabled;
|
||||
}
|
||||
|
||||
apply(toolbar > icon_button) {
|
||||
icon_button: default;
|
||||
app_bar_button: default;
|
||||
@ -1337,6 +1365,8 @@ apply(toolbar > icon_button) {
|
||||
transition_enter: state.pressed;
|
||||
app_bar_focus_ring: state.focusKey;
|
||||
app_bar_focus_ring: state.edited;
|
||||
app_bar_button_focus_ring: state.focusKey;
|
||||
app_bar_button_focus_ring: state.edited;
|
||||
disabled: state.disabled;
|
||||
}
|
||||
|
||||
@ -1480,6 +1510,10 @@ apply(card) {
|
||||
scrollbar: part.scrollbar;
|
||||
}
|
||||
|
||||
apply(focus_container) {
|
||||
focus_ring: state.focusKey;
|
||||
}
|
||||
|
||||
apply(sliderbox) {
|
||||
sliderbox: default;
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
@widget icon_button_tonal: lvgl_icon_button_tonal_class;
|
||||
@widget sliderbox: lvgl_sliderbox_class;
|
||||
@widget card: lvgl_card_class;
|
||||
@widget focus_container: lvgl_focus_container_class;
|
||||
@widget chip: lvgl_chip_class;
|
||||
@widget badge: lvgl_badge_class;
|
||||
@widget page_indicator: lvgl_page_indicator_class;
|
||||
@ -49,7 +50,7 @@
|
||||
@corner-medium: 8dp;
|
||||
@corner-large: 12dp;
|
||||
@corner-full: circle;
|
||||
@corner-compact: 2;
|
||||
@corner-compact: 4;
|
||||
|
||||
// Spacing scale
|
||||
@space-1: 2dp;
|
||||
@ -184,6 +185,8 @@
|
||||
// Toolbar: top app bar with a divider, which shows the background like the statusbar does
|
||||
@app-bar-title-color: @color-on-surface;
|
||||
@app-bar-pad-hor: if($isCompact, 0, @space-1);
|
||||
// Keeps the action buttons off the screen's edge in the compact layout
|
||||
@app-bar-pad-right: if($isCompact, @app-bar-pad-hor + 2, @app-bar-pad-hor);
|
||||
@app-bar-gap: if($isCompact, 2, @space-1);
|
||||
// The focus ring is drawn inside toolbar buttons, as there's little room around them
|
||||
@app-bar-focus-width: @focus-inset-width;
|
||||
@ -503,10 +506,21 @@ app_bar {
|
||||
radius: 0;
|
||||
borderWidth: 0;
|
||||
padHor: @app-bar-pad-hor;
|
||||
padRight: @app-bar-pad-right;
|
||||
padVer: 0;
|
||||
padGap: @app-bar-gap;
|
||||
}
|
||||
|
||||
// The toolbar's first button: the close (navigation) button
|
||||
app_bar_nav_button {
|
||||
@if ($isCompact) { marginLeft: 4; }
|
||||
}
|
||||
|
||||
// Compact toolbar buttons are as big as their icon, so their focus ring goes around the outside instead of the inside
|
||||
app_bar_button_focus_ring {
|
||||
@if ($isCompact) { outlinePad: 0; }
|
||||
}
|
||||
|
||||
app_bar_button {
|
||||
padAll: 0;
|
||||
}
|
||||
@ -987,12 +1001,26 @@ apply(tabview > *[0] > button) {
|
||||
edit_ring: state.edited;
|
||||
}
|
||||
|
||||
apply(toolbar > icon_button[0]) {
|
||||
icon_button: default;
|
||||
app_bar_button: default;
|
||||
app_bar_nav_button: default;
|
||||
inverted: state.pressed;
|
||||
app_bar_focus_ring: state.focusKey;
|
||||
app_bar_focus_ring: state.edited;
|
||||
app_bar_button_focus_ring: state.focusKey;
|
||||
app_bar_button_focus_ring: state.edited;
|
||||
disabled: state.disabled;
|
||||
}
|
||||
|
||||
apply(toolbar > icon_button) {
|
||||
icon_button: default;
|
||||
app_bar_button: default;
|
||||
inverted: state.pressed;
|
||||
app_bar_focus_ring: state.focusKey;
|
||||
app_bar_focus_ring: state.edited;
|
||||
app_bar_button_focus_ring: state.focusKey;
|
||||
app_bar_button_focus_ring: state.edited;
|
||||
disabled: state.disabled;
|
||||
}
|
||||
|
||||
@ -1110,6 +1138,10 @@ apply(card) {
|
||||
scrollbar: part.scrollbar;
|
||||
}
|
||||
|
||||
apply(focus_container) {
|
||||
focus_ring: state.focusKey;
|
||||
}
|
||||
|
||||
apply(sliderbox) {
|
||||
sliderbox: default;
|
||||
}
|
||||
|
||||
@ -145,6 +145,43 @@ enum WindowState window_manager_get_state(WindowId id);
|
||||
*/
|
||||
enum WindowState window_manager_await_state_change(WindowId id, TickType_t timeout);
|
||||
|
||||
/**
|
||||
* Windows live in layers. Only the active layer receives input: its widgets form the group that
|
||||
* keypad indevs are attached to, and a shown overlay covers the app layer for pointer input.
|
||||
*/
|
||||
enum WindowLayer {
|
||||
/** The chrome created by the screen-init callback and the app windows. */
|
||||
WINDOW_LAYER_APP,
|
||||
/** A single overlay on top of the app layer. */
|
||||
WINDOW_LAYER_OVERLAY,
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows an overlay on top of the app layer and makes it the active layer. A shown overlay is
|
||||
* replaced. The overlay is a full-size backdrop: clicking the backdrop itself (outside of the
|
||||
* widgets created by @a create_widgets) or pressing ESC on a focused overlay widget hides it.
|
||||
* App windows that are created or rebuilt while the overlay is shown stay in the app layer.
|
||||
* When a pointer device exists, the widget that is focused first doesn't show as selected.
|
||||
* @param[in] create_widgets called once to populate the backdrop, with the LVGL lock held
|
||||
* @param[in] destroy_widgets may be NULL, called once when the overlay is hidden, with the LVGL
|
||||
* lock held
|
||||
* @param[in] user_data passed to both callbacks
|
||||
* @warning Only uses the LVGL lock, so it may be called from LVGL event callbacks.
|
||||
* @retval ERROR_INVALID_STATE the window manager isn't started
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t window_manager_overlay_show(WindowCreateWidgetsFn create_widgets, WindowDestroyWidgetsFn destroy_widgets, void* user_data);
|
||||
|
||||
/**
|
||||
* Hides the overlay, if any, and makes the app layer the active layer.
|
||||
* @warning Only uses the LVGL lock, so it may be called from LVGL event callbacks, including
|
||||
* the events of the overlay's own widgets.
|
||||
*/
|
||||
void window_manager_overlay_hide(void);
|
||||
|
||||
/** @return the layer that currently receives input */
|
||||
enum WindowLayer window_manager_get_active_layer(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -12,6 +12,9 @@ static const ModuleSymbol SYMBOLS[] = {
|
||||
DEFINE_MODULE_SYMBOL(window_manager_remove),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_get_state),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_await_state_change),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_overlay_show),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_overlay_hide),
|
||||
DEFINE_MODULE_SYMBOL(window_manager_get_active_layer),
|
||||
MODULE_SYMBOL_TERMINATOR,
|
||||
};
|
||||
|
||||
|
||||
@ -3,6 +3,9 @@
|
||||
|
||||
#include <app/instance.h>
|
||||
|
||||
#include <lvgl/devices/indev.h>
|
||||
#include <lvgl/devices/keyboard.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
@ -88,6 +91,54 @@ WindowManagerState& state() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** Guarded by the LVGL lock instead of WindowManagerState's mutexes, so the overlay can be shown
|
||||
* and hidden from LVGL event callbacks. */
|
||||
struct OverlayState {
|
||||
lv_obj_t* widget = nullptr;
|
||||
lv_group_t* group = nullptr;
|
||||
WindowDestroyWidgetsFn destroy_widgets = nullptr;
|
||||
void* user_data = nullptr;
|
||||
};
|
||||
|
||||
OverlayState overlay;
|
||||
|
||||
void delete_overlay_widget(void* widget) {
|
||||
lv_obj_delete(static_cast<lv_obj_t*>(widget));
|
||||
}
|
||||
|
||||
// A pending deletion must not run for a widget that was deleted along with the root widget
|
||||
void on_overlay_widget_deleted(lv_event_t* event) {
|
||||
lv_async_call_cancel(delete_overlay_widget, lv_event_get_current_target_obj(event));
|
||||
}
|
||||
|
||||
// Call while holding the LVGL lock
|
||||
void hide_overlay_locked() {
|
||||
if (overlay.widget == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (overlay.destroy_widgets != nullptr) {
|
||||
overlay.destroy_widgets(overlay.user_data);
|
||||
}
|
||||
lvgl_keyboard_set_active_group(nullptr);
|
||||
lv_group_delete(overlay.group);
|
||||
// Deleted asynchronously, as this may run inside an event of one of the overlay's widgets
|
||||
lv_obj_add_flag(overlay.widget, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_async_call(delete_overlay_widget, overlay.widget);
|
||||
overlay = {};
|
||||
}
|
||||
|
||||
void on_overlay_backdrop_clicked(lv_event_t* event) {
|
||||
if (lv_event_get_target_obj(event) == lv_event_get_current_target_obj(event)) {
|
||||
hide_overlay_locked();
|
||||
}
|
||||
}
|
||||
|
||||
void on_overlay_key(lv_event_t* event) {
|
||||
if (lv_event_get_key(event) == LV_KEY_ESC) {
|
||||
hide_overlay_locked();
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t* build_window_widget(lv_obj_t* content, WindowCreateWidgetsFn create_widgets, void* user_data) {
|
||||
if (content == nullptr) {
|
||||
return nullptr;
|
||||
@ -103,7 +154,11 @@ lv_obj_t* build_window_widget(lv_obj_t* content, WindowCreateWidgetsFn create_wi
|
||||
// theme's scrollbar styling as a thin line hugging this widget's edges.
|
||||
lv_obj_set_scrollable(widget, false);
|
||||
if (create_widgets != nullptr) {
|
||||
// Windows belong to the app layer, also while the overlay is the active layer
|
||||
lv_group_t* default_group = lv_group_get_default();
|
||||
lv_group_set_default(lvgl_keyboard_get_app_group());
|
||||
create_widgets(widget, user_data);
|
||||
lv_group_set_default(default_group);
|
||||
}
|
||||
lvgl_unlock();
|
||||
return widget;
|
||||
@ -308,6 +363,10 @@ error_t window_manager_stop(void) {
|
||||
give_and_release(waiter);
|
||||
}
|
||||
|
||||
lvgl_lock();
|
||||
hide_overlay_locked();
|
||||
lvgl_unlock();
|
||||
|
||||
// Deleting the real widget cascades to everything under it - chrome and top_widget alike.
|
||||
delete_widget(widget, top_destroy_widgets, top_user_data);
|
||||
|
||||
@ -512,4 +571,75 @@ WindowState window_manager_await_state_change(WindowId id, TickType_t timeout) {
|
||||
return window_manager_get_state(id);
|
||||
}
|
||||
|
||||
error_t window_manager_overlay_show(WindowCreateWidgetsFn create_widgets, WindowDestroyWidgetsFn destroy_widgets, void* user_data) {
|
||||
auto& s = state();
|
||||
|
||||
lvgl_lock();
|
||||
|
||||
mutex_lock(&s.mutex);
|
||||
lv_obj_t* root = s.started ? s.real_root_widget : nullptr;
|
||||
mutex_unlock(&s.mutex);
|
||||
|
||||
if (root == nullptr) {
|
||||
lvgl_unlock();
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
hide_overlay_locked();
|
||||
|
||||
lv_group_t* group = lv_group_create();
|
||||
if (group == nullptr) {
|
||||
lvgl_unlock();
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
// Widgets created from here on join the overlay's group
|
||||
lvgl_keyboard_set_active_group(group);
|
||||
|
||||
// Created as the last child of the root widget, so it covers everything else
|
||||
lv_obj_t* widget = lv_obj_create(root);
|
||||
lv_obj_set_size(widget, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_pad_all(widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(widget, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_radius(widget, 0, LV_STATE_DEFAULT);
|
||||
// Monochrome displays can't dim the app layer, so the overlay draws over it as-is
|
||||
const bool monochrome = lv_display_get_color_format(lv_obj_get_display(root)) == LV_COLOR_FORMAT_I1;
|
||||
lv_obj_set_style_bg_color(widget, lv_color_black(), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(widget, monochrome ? LV_OPA_TRANSP : LV_OPA_50, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(widget, false);
|
||||
lv_obj_add_event_cb(widget, on_overlay_backdrop_clicked, LV_EVENT_CLICKED, nullptr);
|
||||
lv_obj_add_event_cb(widget, on_overlay_widget_deleted, LV_EVENT_DELETE, nullptr);
|
||||
|
||||
overlay = OverlayState { widget, group, destroy_widgets, user_data };
|
||||
|
||||
if (create_widgets != nullptr) {
|
||||
create_widgets(widget, user_data);
|
||||
}
|
||||
|
||||
// The first widget in the new group is focused automatically. With a pointer device, that shouldn't show as selected.
|
||||
if (lvgl_indev_exists(LV_INDEV_TYPE_POINTER)) {
|
||||
lvgl_focus_hide_key_selection(group);
|
||||
}
|
||||
|
||||
const uint32_t object_count = lv_group_get_obj_count(group);
|
||||
for (uint32_t i = 0; i < object_count; i++) {
|
||||
lv_obj_add_event_cb(lv_group_get_obj_by_index(group, i), on_overlay_key, LV_EVENT_KEY, nullptr);
|
||||
}
|
||||
|
||||
lvgl_unlock();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
void window_manager_overlay_hide(void) {
|
||||
lvgl_lock();
|
||||
hide_overlay_locked();
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
WindowLayer window_manager_get_active_layer(void) {
|
||||
lvgl_lock();
|
||||
WindowLayer layer = overlay.widget != nullptr ? WINDOW_LAYER_OVERLAY : WINDOW_LAYER_APP;
|
||||
lvgl_unlock();
|
||||
return layer;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@ -26,13 +26,18 @@ void statusbar_icon_set_image(int8_t id, const std::string& image);
|
||||
/** Update the visibility for an icon on the statusbar. Does not need to be called with LVGL lock. */
|
||||
void statusbar_icon_set_visibility(int8_t id, bool visible);
|
||||
|
||||
/**
|
||||
* Sets the app that starts when the icon is clicked. Does not need to be called with LVGL lock.
|
||||
* @param[in] id the icon id
|
||||
* @param[in] appId the app id, or an empty string to make the icon not clickable
|
||||
*/
|
||||
void statusbar_icon_set_app(int8_t id, const std::string& appId);
|
||||
|
||||
int statusbar_get_height();
|
||||
|
||||
/**
|
||||
* Switches between the split layout (a bar on top) and the side layout (a vertical strip on the left). Circular displays always use the split layout.
|
||||
* The caller places the statusbar in its parent. Needs to be called with LVGL lock.
|
||||
*/
|
||||
void statusbar_set_side(lv_obj_t* statusbar, bool side);
|
||||
|
||||
/**
|
||||
* @return the container for the action buttons of apps in the side layout, or nullptr on circular displays. It is hidden in the split layout.
|
||||
* Needs to be called with LVGL lock.
|
||||
*/
|
||||
lv_obj_t* statusbar_get_action_container(lv_obj_t* statusbar);
|
||||
|
||||
} // namespace
|
||||
|
||||
61
Tactility/Include/Tactility/settings/LauncherSettings.h
Normal file
61
Tactility/Include/Tactility/settings/LauncherSettings.h
Normal file
@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace tt::settings::launcher {
|
||||
|
||||
/** How the statusbar and the app's navigation bar are placed on screen */
|
||||
enum class SystemBarsLayout {
|
||||
/** The statusbar on top and the navigation bar at the bottom */
|
||||
Split,
|
||||
/** A single strip on the left side of the screen */
|
||||
Side
|
||||
};
|
||||
|
||||
enum class SystemBarsMode {
|
||||
/** Side for wide landscape screens, Split otherwise */
|
||||
Auto,
|
||||
/** The layouts that the user picked for portrait and landscape */
|
||||
Custom
|
||||
};
|
||||
|
||||
/** The system bars layouts that the device allows in an orientation */
|
||||
enum class SystemBarsCapability {
|
||||
/** The layout follows the settings */
|
||||
Any,
|
||||
Split,
|
||||
Side
|
||||
};
|
||||
|
||||
struct SystemBarsCapabilities {
|
||||
/** Used for portrait (including square) screens */
|
||||
SystemBarsCapability portrait;
|
||||
SystemBarsCapability landscape;
|
||||
};
|
||||
|
||||
struct LauncherSettings {
|
||||
SystemBarsMode systemBarsMode;
|
||||
/** Used for portrait (including square) screens in SystemBarsMode::Custom */
|
||||
SystemBarsLayout portraitLayout;
|
||||
/** Used for landscape screens in SystemBarsMode::Custom */
|
||||
SystemBarsLayout landscapeLayout;
|
||||
};
|
||||
|
||||
bool load(LauncherSettings& settings);
|
||||
|
||||
LauncherSettings loadOrGetDefault();
|
||||
|
||||
LauncherSettings getDefault();
|
||||
|
||||
bool save(const LauncherSettings& settings);
|
||||
|
||||
/** @return the system bars layouts that this device's configuration allows */
|
||||
SystemBarsCapabilities getSystemBarsCapabilities();
|
||||
|
||||
/**
|
||||
* @return the system bars layout for a screen of the given size in its current orientation.
|
||||
* A capability other than Any for that orientation decides the layout, instead of the settings.
|
||||
*/
|
||||
SystemBarsLayout resolveSystemBarsLayout(const LauncherSettings& settings, const SystemBarsCapabilities& capabilities, int32_t width, int32_t height);
|
||||
|
||||
} // namespace tt::settings::launcher
|
||||
@ -59,6 +59,32 @@ menu "Tactility App"
|
||||
help
|
||||
The minimum time to show the splash screen in milliseconds.
|
||||
When set to 0, startup will continue to desktop as soon as boot operations are finished.
|
||||
choice TT_SYSTEM_BARS_PORTRAIT
|
||||
prompt "System bars layouts in portrait"
|
||||
default TT_SYSTEM_BARS_PORTRAIT_ANY
|
||||
help
|
||||
The system bars layouts that portrait orientations (including square ones) can use.
|
||||
"Any" lets the user's Appearance settings decide.
|
||||
config TT_SYSTEM_BARS_PORTRAIT_ANY
|
||||
bool "Any"
|
||||
config TT_SYSTEM_BARS_PORTRAIT_SPLIT
|
||||
bool "Split"
|
||||
config TT_SYSTEM_BARS_PORTRAIT_SIDE
|
||||
bool "Side"
|
||||
endchoice
|
||||
choice TT_SYSTEM_BARS_LANDSCAPE
|
||||
prompt "System bars layouts in landscape"
|
||||
default TT_SYSTEM_BARS_LANDSCAPE_ANY
|
||||
help
|
||||
The system bars layouts that landscape orientations can use.
|
||||
"Any" lets the user's Appearance settings decide.
|
||||
config TT_SYSTEM_BARS_LANDSCAPE_ANY
|
||||
bool "Any"
|
||||
config TT_SYSTEM_BARS_LANDSCAPE_SPLIT
|
||||
bool "Split"
|
||||
config TT_SYSTEM_BARS_LANDSCAPE_SIDE
|
||||
bool "Side"
|
||||
endchoice
|
||||
config TT_TOUCH_CALIBRATION_SUPPORTED
|
||||
bool "Set true when a touch screen calibration app should be included"
|
||||
default n
|
||||
|
||||
@ -69,7 +69,7 @@ public:
|
||||
|
||||
/**
|
||||
* Creates the grid and populates it, with a bar below it: buttons on the left (see addBarButton()),
|
||||
* a page indicator in the center and page buttons on the right. The page indicator and buttons are
|
||||
* followed by the page buttons, and a page indicator on the right. The page indicator and buttons are
|
||||
* hidden when there's only one page, and the page buttons wrap around.
|
||||
* @param[in] parent the container for the grid and the bar
|
||||
*/
|
||||
@ -82,12 +82,22 @@ public:
|
||||
void setPageButtons(lv_obj_t* previous, lv_obj_t* next);
|
||||
|
||||
/**
|
||||
* Adds an icon button to the left side of the bottom bar. Call right after createWidgetsWithBottomBar().
|
||||
* Adds an icon button to the bottom bar, before the page buttons. Call right after createWidgetsWithBottomBar().
|
||||
* @param[in] icon a shared icon (see lvgl/icons/shared.h)
|
||||
* @return the button
|
||||
*/
|
||||
lv_obj_t* addBarButton(const char* icon, lv_event_cb_t onClicked, void* userData);
|
||||
|
||||
/**
|
||||
* Moves the bottom bar's buttons into a column in the given container, e.g. a side strip, and the page indicator below the grid.
|
||||
* Call after createWidgetsWithBottomBar(), and again whenever the container changes.
|
||||
* @param[in] sideContainer the container for the buttons, or nullptr to show the bar below the grid
|
||||
*/
|
||||
void setBarSideContainer(lv_obj_t* sideContainer);
|
||||
|
||||
/** Deletes the widgets that aren't part of the parent passed to createWidgetsWithBottomBar(). Call when the parent's widgets are deleted. */
|
||||
void destroyWidgets();
|
||||
|
||||
/**
|
||||
* Rebuilds the grid asynchronously, so it is safe to call from a tile's own event.
|
||||
* @param[in] keepSelection when true, the selected item stays selected and its page is shown
|
||||
@ -115,8 +125,16 @@ private:
|
||||
lv_obj_t* pageIndicator = nullptr;
|
||||
lv_obj_t* bottomBar = nullptr;
|
||||
lv_obj_t* barSpacer = nullptr;
|
||||
/** The parent passed to createWidgetsWithBottomBar() */
|
||||
lv_obj_t* barParent = nullptr;
|
||||
/** The container that holds the bar's buttons instead of the bar below the grid, or nullptr */
|
||||
lv_obj_t* barSideContainer = nullptr;
|
||||
/** The buttons added with addBarButton(), in the order they were added */
|
||||
std::vector<lv_obj_t*> barButtons;
|
||||
|
||||
void createGrid(lv_obj_t* parent);
|
||||
void arrangeBar();
|
||||
static void onSideBarKey(lv_event_t* e);
|
||||
void finishCreate(lv_obj_t* parent);
|
||||
void goToPreviousPage();
|
||||
void goToNextPage();
|
||||
|
||||
59
Tactility/Private/Tactility/lvgl/SystemBars.h
Normal file
59
Tactility/Private/Tactility/lvgl/SystemBars.h
Normal file
@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/settings/LauncherSettings.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
namespace tt::lvgl {
|
||||
|
||||
using SystemBarsListenerId = uint32_t;
|
||||
|
||||
/**
|
||||
* Takes control of the layout of the screen's statusbar and app container, and applies the layout of the launcher settings.
|
||||
* The layout follows display rotations by itself.
|
||||
* @warning Caller must hold the LVGL lock.
|
||||
* @param[in] container the flex container that holds the statusbar and the app container
|
||||
*/
|
||||
void systemBarsAttach(lv_obj_t* container, lv_obj_t* statusbar, lv_obj_t* appContainer);
|
||||
|
||||
/**
|
||||
* Releases the objects passed to systemBarsAttach(), before they're deleted.
|
||||
* @warning Caller must hold the LVGL lock.
|
||||
*/
|
||||
void systemBarsDetach();
|
||||
|
||||
/**
|
||||
* Applies the launcher settings and the current display size again, e.g. after the settings changed.
|
||||
* Notifies the listeners when the layout changed.
|
||||
* @warning Caller must hold the LVGL lock.
|
||||
*/
|
||||
void systemBarsRefresh();
|
||||
|
||||
/**
|
||||
* @warning Caller must hold the LVGL lock.
|
||||
* @return the layout that is shown
|
||||
*/
|
||||
settings::launcher::SystemBarsLayout systemBarsGetLayout();
|
||||
|
||||
/**
|
||||
* @warning Caller must hold the LVGL lock.
|
||||
* @return the container in the side strip for an app's action buttons, or nullptr when the side layout isn't shown
|
||||
*/
|
||||
lv_obj_t* systemBarsGetActionContainer();
|
||||
|
||||
/**
|
||||
* Adds a listener that is called when the layout changed. It's called with the LVGL lock held.
|
||||
* @warning Caller must hold the LVGL lock.
|
||||
*/
|
||||
SystemBarsListenerId systemBarsAddListener(std::function<void()> listener);
|
||||
|
||||
/**
|
||||
* Removes a listener. It doesn't take any lock besides the LVGL lock, so a window's destroy_widgets callback can call it.
|
||||
* @warning Caller must hold the LVGL lock.
|
||||
*/
|
||||
void systemBarsRemoveListener(SystemBarsListenerId id);
|
||||
|
||||
}
|
||||
11
Tactility/Private/Tactility/lvgl/SystemMenu.h
Normal file
11
Tactility/Private/Tactility/lvgl/SystemMenu.h
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
namespace tt::lvgl {
|
||||
|
||||
/**
|
||||
* Shows the system menu with quick toggles (Wi-Fi, GPS, Bluetooth) in the window manager's overlay layer.
|
||||
* @warning Caller must hold the LVGL lock.
|
||||
*/
|
||||
void systemMenuShow();
|
||||
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/insets.h>
|
||||
#include <lvgl/theme.h>
|
||||
#include <lvgl/widgets/badge.h>
|
||||
#include <lvgl/widgets/icon_button.h>
|
||||
#include <lvgl/widgets/page_indicator.h>
|
||||
@ -56,41 +57,32 @@ const TileGridItem* getTileItem(lv_event_t* e) {
|
||||
}
|
||||
|
||||
|
||||
// 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;
|
||||
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(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(bar, LV_PART_MAIN) : width;
|
||||
}
|
||||
|
||||
// The page indicator is centered on the bar and gets the space that the buttons on the wider side leave on both sides
|
||||
// The page indicator gets the space that the buttons leave.
|
||||
// It's the only child that is neither clickable nor the spacer (which grows).
|
||||
void updatePageIndicatorWidth(void* data) {
|
||||
auto* bottom_bar = static_cast<lv_obj_t*>(data);
|
||||
lv_obj_t* indicator = nullptr;
|
||||
int32_t buttons_width = 0;
|
||||
int32_t gap_count = -1;
|
||||
const uint32_t count = lv_obj_get_child_count(bottom_bar);
|
||||
for (uint32_t i = 0; i < count && indicator == nullptr; i++) {
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
lv_obj_t* child = lv_obj_get_child(bottom_bar, static_cast<int32_t>(i));
|
||||
if (lv_obj_is_ignore_layout(child)) {
|
||||
if (lv_obj_get_style_flex_grow(child, LV_PART_MAIN) > 0) {
|
||||
gap_count++;
|
||||
} else if (!lv_obj_is_clickable(child)) {
|
||||
indicator = child;
|
||||
gap_count++;
|
||||
} else if (!lv_obj_is_hidden(child)) {
|
||||
buttons_width += lv_obj_get_width(child);
|
||||
gap_count++;
|
||||
}
|
||||
}
|
||||
if (indicator == nullptr) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
const int32_t gaps = gap_count * lv_obj_get_style_pad_column(bottom_bar, LV_PART_MAIN);
|
||||
const int32_t margins = lv_obj_get_style_margin_left(indicator, LV_PART_MAIN) + lv_obj_get_style_margin_right(indicator, LV_PART_MAIN);
|
||||
const int32_t available = lv_obj_get_content_width(bottom_bar) - buttons_width - gaps - margins;
|
||||
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) {
|
||||
@ -103,6 +95,45 @@ void onBottomBarSizeChanged(lv_event_t* event) {
|
||||
lv_async_call(updatePageIndicatorWidth, lv_event_get_target_obj(event));
|
||||
}
|
||||
|
||||
// Puts the bar before or after the grid in their input group, without moving the focus
|
||||
void setGroupOrder(lv_obj_t* grid, lv_obj_t* bar, bool barFirst) {
|
||||
lv_group_t* group = lv_obj_get_group(bar);
|
||||
if (group == nullptr || lv_obj_get_group(grid) != group) {
|
||||
return;
|
||||
}
|
||||
int32_t grid_index = -1;
|
||||
int32_t bar_index = -1;
|
||||
const uint32_t count = lv_group_get_obj_count(group);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
lv_obj_t* obj = lv_group_get_obj_by_index(group, i);
|
||||
if (obj == grid) {
|
||||
grid_index = static_cast<int32_t>(i);
|
||||
} else if (obj == bar) {
|
||||
bar_index = static_cast<int32_t>(i);
|
||||
}
|
||||
}
|
||||
if ((bar_index < grid_index) == barFirst) {
|
||||
return;
|
||||
}
|
||||
// Swapping also swaps the focus
|
||||
lv_obj_t* focused = lv_group_get_focused(group);
|
||||
lv_group_swap_obj(grid, bar);
|
||||
if (focused != nullptr && lv_group_get_focused(group) != focused) {
|
||||
lv_group_focus_obj(focused);
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t* findFirstBarButton(lv_obj_t* bar) {
|
||||
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(bar, static_cast<int32_t>(i));
|
||||
if (!lv_obj_is_hidden(child) && lv_obj_is_clickable(child)) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void onBottomBarDeleted(lv_event_t* event) {
|
||||
lv_async_call_cancel(updatePageIndicatorWidth, lv_event_get_target_obj(event));
|
||||
}
|
||||
@ -346,15 +377,18 @@ void TileGrid::createGrid(lv_obj_t* parent) {
|
||||
}
|
||||
}
|
||||
|
||||
// Keys enter the bar on the previous page button, or on the first button when there's only one page
|
||||
// Keys enter the bar on the previous page button, or on the first visible button when there's only one page
|
||||
void TileGrid::onBottomBarFocused(lv_event_t* e) {
|
||||
const auto* self = static_cast<TileGrid*>(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) {
|
||||
lv_obj_t* target = lv_obj_is_hidden(self->prevButton) ? findFirstBarButton(self->bottomBar) : self->prevButton;
|
||||
if (current == nullptr || target == nullptr || target == current) {
|
||||
return;
|
||||
}
|
||||
const bool showSelection = lv_obj_has_state(current, LV_STATE_FOCUS_KEY);
|
||||
// Only keys show the selection, the bar's previously focused child can be stale
|
||||
lv_indev_t* indev = lv_indev_active();
|
||||
const lv_indev_type_t type = indev != nullptr ? lv_indev_get_type(indev) : LV_INDEV_TYPE_NONE;
|
||||
const bool showSelection = type == LV_INDEV_TYPE_KEYPAD || type == LV_INDEV_TYPE_ENCODER;
|
||||
lv_gridnav_set_focused(self->bottomBar, target, LV_ANIM_OFF);
|
||||
if (!showSelection) {
|
||||
lv_obj_remove_state(target, LV_STATE_FOCUS_KEY);
|
||||
@ -362,6 +396,9 @@ void TileGrid::onBottomBarFocused(lv_event_t* e) {
|
||||
}
|
||||
|
||||
void TileGrid::createWidgetsWithBottomBar(lv_obj_t* parent) {
|
||||
barParent = parent;
|
||||
barSideContainer = nullptr;
|
||||
barButtons.clear();
|
||||
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);
|
||||
@ -376,7 +413,10 @@ void TileGrid::createWidgetsWithBottomBar(lv_obj_t* parent) {
|
||||
createGrid(parent);
|
||||
lv_obj_move_foreground(bottomBar);
|
||||
|
||||
// The bar buttons go before the spacer, which pushes the page buttons to the right
|
||||
pageIndicator = lvgl_page_indicator_create(bottomBar);
|
||||
// Keeps the page indicator away from the screen edge
|
||||
lv_obj_set_style_margin_hor(pageIndicator, lv_obj_get_style_pad_column(bottomBar, LV_PART_MAIN), LV_STATE_DEFAULT);
|
||||
// The spacer pushes the buttons and the page indicator to opposite sides
|
||||
barSpacer = lv_obj_create(bottomBar);
|
||||
lv_obj_set_size(barSpacer, 0, 0);
|
||||
lv_obj_set_flex_grow(barSpacer, 1);
|
||||
@ -387,9 +427,7 @@ void TileGrid::createWidgetsWithBottomBar(lv_obj_t* parent) {
|
||||
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);
|
||||
pageIndicator = lvgl_page_indicator_create(bottomBar);
|
||||
lv_obj_set_ignore_layout(pageIndicator, true);
|
||||
lv_obj_align(pageIndicator, LV_ALIGN_CENTER, 0, 0);
|
||||
arrangeBar();
|
||||
// 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);
|
||||
@ -404,6 +442,9 @@ void TileGrid::createWidgets(lv_obj_t* parent) {
|
||||
pageIndicator = nullptr;
|
||||
bottomBar = nullptr;
|
||||
barSpacer = nullptr;
|
||||
barParent = nullptr;
|
||||
barSideContainer = nullptr;
|
||||
barButtons.clear();
|
||||
createGrid(parent);
|
||||
finishCreate(parent);
|
||||
}
|
||||
@ -443,12 +484,101 @@ lv_obj_t* TileGrid::findTile(const std::string& id) const {
|
||||
|
||||
lv_obj_t* TileGrid::addBarButton(const char* icon, lv_event_cb_t onClicked, void* userData) {
|
||||
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);
|
||||
lv_obj_add_event_cb(button, onClicked, LV_EVENT_SHORT_CLICKED, userData);
|
||||
barButtons.push_back(button);
|
||||
arrangeBar();
|
||||
return button;
|
||||
}
|
||||
|
||||
void TileGrid::arrangeBar() {
|
||||
// The order of the bar's children, from the start of the row or column
|
||||
std::vector<lv_obj_t*> order;
|
||||
if (barSideContainer != nullptr) {
|
||||
// The page indicator is below the grid and the spacer isn't used. The bar's own buttons are at the bottom.
|
||||
order.insert(order.end(), { barSpacer, nextButton, prevButton });
|
||||
order.insert(order.end(), barButtons.rbegin(), barButtons.rend());
|
||||
} else {
|
||||
order.insert(order.end(), barButtons.begin(), barButtons.end());
|
||||
order.insert(order.end(), { prevButton, nextButton, barSpacer, pageIndicator });
|
||||
}
|
||||
for (size_t i = 0; i < order.size(); i++) {
|
||||
lv_obj_move_to_index(order[i], static_cast<int32_t>(i));
|
||||
}
|
||||
}
|
||||
|
||||
void TileGrid::setBarSideContainer(lv_obj_t* sideContainer) {
|
||||
if (bottomBar == nullptr || sideContainer == barSideContainer) {
|
||||
return;
|
||||
}
|
||||
barSideContainer = sideContainer;
|
||||
if (sideContainer != nullptr) {
|
||||
lv_obj_set_parent(bottomBar, sideContainer);
|
||||
lv_obj_set_size(bottomBar, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(bottomBar, LV_FLEX_FLOW_COLUMN);
|
||||
// The strip has its own background
|
||||
lv_obj_set_style_bg_opa(bottomBar, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
// The buttons are close together, like the statusbar's icons above them
|
||||
lv_obj_set_style_pad_row(bottomBar, 2, LV_STATE_DEFAULT);
|
||||
lv_obj_set_hidden(barSpacer, true);
|
||||
// The page indicator is centered below the grid
|
||||
lv_obj_set_parent(pageIndicator, barParent);
|
||||
lv_obj_move_to_index(pageIndicator, lv_obj_get_index(grid) + 1);
|
||||
lv_obj_set_width(pageIndicator, LV_PCT(100));
|
||||
lv_obj_remove_local_style_prop(pageIndicator, LV_STYLE_MAX_WIDTH, LV_PART_MAIN);
|
||||
// Keeps the dots off the screen's bottom edge
|
||||
lv_obj_set_style_margin_bottom(pageIndicator, lvgl_theme_is_compact() ? 1 : 2, LV_STATE_DEFAULT);
|
||||
} else {
|
||||
lv_obj_set_parent(bottomBar, barParent);
|
||||
lv_obj_move_to_index(bottomBar, lv_obj_get_index(grid) + 1);
|
||||
lv_obj_set_size(bottomBar, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(bottomBar, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_remove_local_style_prop(bottomBar, LV_STYLE_BG_OPA, LV_PART_MAIN);
|
||||
lv_obj_remove_local_style_prop(bottomBar, LV_STYLE_PAD_ROW, LV_PART_MAIN);
|
||||
lv_obj_set_hidden(barSpacer, false);
|
||||
lv_obj_set_parent(pageIndicator, bottomBar);
|
||||
lv_obj_set_width(pageIndicator, LV_SIZE_CONTENT);
|
||||
lv_obj_remove_local_style_prop(pageIndicator, LV_STYLE_MARGIN_BOTTOM, LV_PART_MAIN);
|
||||
}
|
||||
// The keys move through the bar where it's shown: below the statusbar's icons in the side strip, or below the grid
|
||||
setGroupOrder(grid, bottomBar, sideContainer != nullptr);
|
||||
if (sideContainer != nullptr) {
|
||||
// Runs before grid navigation, which would move the focus along the group instead of sideways
|
||||
lv_obj_add_event_cb(bottomBar, onSideBarKey, static_cast<lv_event_code_t>(LV_EVENT_KEY | LV_EVENT_PREPROCESS), this);
|
||||
} else {
|
||||
lv_obj_remove_event_cb(bottomBar, onSideBarKey);
|
||||
}
|
||||
arrangeBar();
|
||||
}
|
||||
|
||||
// In the side strip on the left, the right key moves the focus to the grid, and the left key stays at the screen's edge
|
||||
void TileGrid::onSideBarKey(lv_event_t* e) {
|
||||
auto* self = static_cast<TileGrid*>(lv_event_get_user_data(e));
|
||||
const uint32_t key = lv_event_get_key(e);
|
||||
if (key == LV_KEY_RIGHT) {
|
||||
lv_group_focus_obj(self->grid);
|
||||
lv_event_stop_processing(e);
|
||||
} else if (key == LV_KEY_LEFT) {
|
||||
lv_event_stop_processing(e);
|
||||
}
|
||||
}
|
||||
|
||||
void TileGrid::destroyWidgets() {
|
||||
// The window only deletes its own widgets, and the bar can be in the side strip
|
||||
if (bottomBar != nullptr && barSideContainer != nullptr) {
|
||||
lv_obj_delete(bottomBar);
|
||||
}
|
||||
// The other bar widgets are deleted with the bar or the window, so no pointer may outlive them
|
||||
bottomBar = nullptr;
|
||||
barSideContainer = nullptr;
|
||||
barParent = nullptr;
|
||||
pageIndicator = nullptr;
|
||||
prevButton = nullptr;
|
||||
nextButton = nullptr;
|
||||
barSpacer = nullptr;
|
||||
barButtons.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,7 +4,9 @@
|
||||
#include <Tactility/lvgl/Fonts.h>
|
||||
#include <Tactility/lvgl/Lvgl.h>
|
||||
#include <Tactility/lvgl/Theme.h>
|
||||
#include <Tactility/lvgl/SystemBars.h>
|
||||
#include <Tactility/settings/AppearanceSettings.h>
|
||||
#include <Tactility/settings/LauncherSettings.h>
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
@ -21,6 +23,7 @@
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/theme.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
@ -147,6 +150,15 @@ struct Context {
|
||||
lv_obj_t* surfaceTintChips[settings::appearance::SURFACE_TINT_LEVEL_COUNT] = {};
|
||||
/** The display can only show the mono theme */
|
||||
bool isMonoDisplay = false;
|
||||
// System bars, which are applied right away instead of with the Apply button
|
||||
lv_obj_t* systemBarsAutoChip = nullptr;
|
||||
lv_obj_t* systemBarsCustomChip = nullptr;
|
||||
lv_obj_t* portraitRow = nullptr;
|
||||
lv_obj_t* portraitSplitChip = nullptr;
|
||||
lv_obj_t* portraitSideChip = nullptr;
|
||||
lv_obj_t* landscapeRow = nullptr;
|
||||
lv_obj_t* landscapeSplitChip = nullptr;
|
||||
lv_obj_t* landscapeSideChip = nullptr;
|
||||
};
|
||||
|
||||
lvgl::FontConfiguration toFontConfiguration(const settings::appearance::AppearanceSettings& settings) {
|
||||
@ -481,6 +493,8 @@ lv_obj_t* createLabeledRow(lv_obj_t* parent, const char* text) {
|
||||
auto* label = lv_label_create(row);
|
||||
lv_label_set_text(label, text);
|
||||
lv_obj_set_flex_grow(label, 1);
|
||||
// The keys move between the rows, and left and right between the row's items
|
||||
lvgl_grid_navigation_add(row);
|
||||
return row;
|
||||
}
|
||||
|
||||
@ -544,6 +558,83 @@ void createThemeCard(lv_obj_t* parent, Context* ctx) {
|
||||
lv_obj_add_event_cb(animations_switch, onAnimationsChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
}
|
||||
|
||||
void updateSystemBarsWidgets(Context* ctx, const settings::launcher::LauncherSettings& settings) {
|
||||
using settings::launcher::SystemBarsLayout;
|
||||
const bool custom = settings.systemBarsMode == settings::launcher::SystemBarsMode::Custom;
|
||||
setChecked(ctx->systemBarsAutoChip, !custom);
|
||||
setChecked(ctx->systemBarsCustomChip, custom);
|
||||
// The layouts per orientation are only used with the Custom mode. Orientations with a fixed layout have no row.
|
||||
if (ctx->portraitRow != nullptr) {
|
||||
setHidden(ctx->portraitRow, !custom);
|
||||
setChecked(ctx->portraitSplitChip, settings.portraitLayout == SystemBarsLayout::Split);
|
||||
setChecked(ctx->portraitSideChip, settings.portraitLayout == SystemBarsLayout::Side);
|
||||
}
|
||||
if (ctx->landscapeRow != nullptr) {
|
||||
setHidden(ctx->landscapeRow, !custom);
|
||||
setChecked(ctx->landscapeSplitChip, settings.landscapeLayout == SystemBarsLayout::Split);
|
||||
setChecked(ctx->landscapeSideChip, settings.landscapeLayout == SystemBarsLayout::Side);
|
||||
}
|
||||
}
|
||||
|
||||
// Saves the system bars setting of the pressed chip and shows the system bars with it
|
||||
void onSystemBarsChipPressed(lv_event_t* event) {
|
||||
using settings::launcher::SystemBarsLayout;
|
||||
using settings::launcher::SystemBarsMode;
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* chip = lv_event_get_target_obj(event);
|
||||
auto settings = settings::launcher::loadOrGetDefault();
|
||||
if (chip == ctx->systemBarsAutoChip || chip == ctx->systemBarsCustomChip) {
|
||||
settings.systemBarsMode = chip == ctx->systemBarsCustomChip ? SystemBarsMode::Custom : SystemBarsMode::Auto;
|
||||
} else if (chip == ctx->portraitSplitChip || chip == ctx->portraitSideChip) {
|
||||
settings.portraitLayout = chip == ctx->portraitSideChip ? SystemBarsLayout::Side : SystemBarsLayout::Split;
|
||||
} else {
|
||||
settings.landscapeLayout = chip == ctx->landscapeSideChip ? SystemBarsLayout::Side : SystemBarsLayout::Split;
|
||||
}
|
||||
if (!settings::launcher::save(settings)) {
|
||||
LOG_E(TAG, "Failed to save system bars settings");
|
||||
}
|
||||
updateSystemBarsWidgets(ctx, settings);
|
||||
lvgl::systemBarsRefresh();
|
||||
}
|
||||
|
||||
void createSystemBarsCard(lv_obj_t* parent, Context* ctx) {
|
||||
using settings::launcher::SystemBarsCapability;
|
||||
// The device's configuration can fix the layout per orientation, which leaves nothing to choose for it
|
||||
const auto capabilities = settings::launcher::getSystemBarsCapabilities();
|
||||
const bool portrait_choosable = capabilities.portrait == SystemBarsCapability::Any;
|
||||
const bool landscape_choosable = capabilities.landscape == SystemBarsCapability::Any;
|
||||
// Stale pointers from a previous build of the window must not match new widgets
|
||||
ctx->portraitRow = nullptr;
|
||||
ctx->portraitSplitChip = nullptr;
|
||||
ctx->portraitSideChip = nullptr;
|
||||
ctx->landscapeRow = nullptr;
|
||||
ctx->landscapeSplitChip = nullptr;
|
||||
ctx->landscapeSideChip = nullptr;
|
||||
if (!portrait_choosable && !landscape_choosable) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* card = createSection(parent, "System Bars");
|
||||
|
||||
auto* mode_row = createLabeledRow(card, "Layout");
|
||||
ctx->systemBarsAutoChip = createChip(mode_row, "Auto", onSystemBarsChipPressed, ctx);
|
||||
ctx->systemBarsCustomChip = createChip(mode_row, "Custom", onSystemBarsChipPressed, ctx);
|
||||
|
||||
if (portrait_choosable) {
|
||||
ctx->portraitRow = createLabeledRow(card, "Portrait");
|
||||
ctx->portraitSplitChip = createChip(ctx->portraitRow, "Split", onSystemBarsChipPressed, ctx);
|
||||
ctx->portraitSideChip = createChip(ctx->portraitRow, "Side", onSystemBarsChipPressed, ctx);
|
||||
}
|
||||
|
||||
if (landscape_choosable) {
|
||||
ctx->landscapeRow = createLabeledRow(card, "Landscape");
|
||||
ctx->landscapeSplitChip = createChip(ctx->landscapeRow, "Split", onSystemBarsChipPressed, ctx);
|
||||
ctx->landscapeSideChip = createChip(ctx->landscapeRow, "Side", onSystemBarsChipPressed, ctx);
|
||||
}
|
||||
|
||||
updateSystemBarsWidgets(ctx, settings::launcher::loadOrGetDefault());
|
||||
}
|
||||
|
||||
void createColorsCard(lv_obj_t* parent, Context* ctx) {
|
||||
ctx->colorsTitle = lv_label_create(parent);
|
||||
lv_label_set_text(ctx->colorsTitle, "Theme Colors");
|
||||
@ -632,6 +723,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
if (!ctx->isMonoDisplay) {
|
||||
createColorsCard(content, ctx);
|
||||
}
|
||||
createSystemBarsCard(content, ctx);
|
||||
|
||||
auto* fonts_card = createSection(content, "Fonts");
|
||||
auto* font_size_row = createRow(fonts_card);
|
||||
|
||||
@ -156,7 +156,7 @@ extern const ::AppManifest manifest = {
|
||||
.category = APP_CATEGORY_SETTINGS,
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(appMain) },
|
||||
.flags = 0,
|
||||
.stack = { .depth = 3072, .desired_memory_capability = 0 },
|
||||
.stack = { .depth = 4096, .desired_memory_capability = 0 },
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@ -52,8 +52,7 @@ struct Context {
|
||||
/** 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;
|
||||
// Widgets: the chips only exist when there are I2C interfaces. Pressing a chip scans its bus.
|
||||
lv_obj_t* chipsRow = nullptr;
|
||||
lv_obj_t* scanListWidget = nullptr;
|
||||
};
|
||||
@ -170,7 +169,6 @@ void updateViews(Context* ctx) {
|
||||
|
||||
// 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++) {
|
||||
@ -359,12 +357,6 @@ void onChipPressed(lv_event_t* event) {
|
||||
selectBus(ctx, static_cast<int32_t>(reinterpret_cast<intptr_t>(lv_obj_get_user_data(chip))));
|
||||
}
|
||||
|
||||
void onPressRefresh(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
startScanning(ctx);
|
||||
updateViews(ctx);
|
||||
}
|
||||
|
||||
// endregion Callbacks
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
@ -394,29 +386,25 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx->refreshButton = lvgl_toolbar_add_image_button_action(toolbar, LV_SYMBOL_REFRESH, onPressRefresh, ctx);
|
||||
|
||||
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;
|
||||
// Also shown for a single bus, as pressing a chip scans its bus again. 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 = lvgl_card_create(main_wrapper);
|
||||
lv_obj_set_size(scan_list, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
#include <app/start.h>
|
||||
@ -8,19 +7,17 @@
|
||||
#include <vector>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/theme.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/power_supply.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <Tactility/app/TileGrid.h>
|
||||
#include <Tactility/app/launcher/LauncherMode.h>
|
||||
#include <Tactility/app/setup/Setup.h>
|
||||
#include <Tactility/lvgl/SystemBars.h>
|
||||
#include <Tactility/settings/BootSettings.h>
|
||||
#include <Tactility/Tactility.h>
|
||||
|
||||
@ -51,6 +48,7 @@ Mode getOtherMode(Mode mode) {
|
||||
struct Context {
|
||||
Mode mode = Mode::Apps;
|
||||
lv_obj_t* modeButtonIcon = nullptr;
|
||||
lvgl::SystemBarsListenerId systemBarsListener = 0;
|
||||
TileGrid grid { TileGrid::Callbacks {
|
||||
.collect = collectItems,
|
||||
.onClicked = onAppClicked,
|
||||
@ -110,28 +108,6 @@ void onModeButtonPressed(lv_event_t* e) {
|
||||
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)));
|
||||
}
|
||||
|
||||
bool isAppRegistered(const char* appId) {
|
||||
::AppManifest manifest;
|
||||
return app_manager_find_manifest(appId, &manifest) == ERROR_NONE;
|
||||
}
|
||||
|
||||
bool supportsPowerOff() {
|
||||
bool supported = false;
|
||||
device_for_each_of_type(&POWER_SUPPLY_TYPE, &supported, [](Device* device, void* context) {
|
||||
if (device_is_ready(device) && power_supply_supports_power_off(device)) {
|
||||
*static_cast<bool*>(context) = true;
|
||||
return false; // stop iterating
|
||||
} else {
|
||||
return true; // continue iterating
|
||||
}
|
||||
});
|
||||
return supported;
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
@ -143,9 +119,19 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
ctx->grid.createWidgetsWithBottomBar(parent);
|
||||
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"));
|
||||
}
|
||||
|
||||
// In the side layout, the bar's buttons are in the system bars strip
|
||||
ctx->grid.setBarSideContainer(lvgl::systemBarsGetActionContainer());
|
||||
ctx->systemBarsListener = lvgl::systemBarsAddListener([ctx] {
|
||||
ctx->grid.setBarSideContainer(lvgl::systemBarsGetActionContainer());
|
||||
});
|
||||
}
|
||||
|
||||
void destroyWidgets(void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
lvgl::systemBarsRemoveListener(ctx->systemBarsListener);
|
||||
ctx->systemBarsListener = 0;
|
||||
ctx->grid.destroyWidgets();
|
||||
}
|
||||
|
||||
void runAutoStart() {
|
||||
@ -186,7 +172,7 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||
|
||||
Context ctx;
|
||||
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
|
||||
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||
|
||||
runAutoStart();
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
#include <app/start.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
@ -138,6 +139,19 @@ void onQuickChargeChanged(lv_event_t* event) {
|
||||
}
|
||||
}
|
||||
|
||||
bool isPowerOffAppRegistered() {
|
||||
::AppManifest manifest;
|
||||
return app_manager_find_manifest("tactility.poweroff", &manifest) == ERROR_NONE;
|
||||
}
|
||||
|
||||
void onPowerOffPressed(lv_event_t* /*event*/) {
|
||||
AppStartContext context;
|
||||
uint32_t instance_id = 0;
|
||||
if (app_start_context_from_id("tactility.poweroff", &context) == ERROR_NONE) {
|
||||
app_start_with_context(&context, &instance_id);
|
||||
}
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
@ -221,6 +235,26 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
entry.quickChargeSwitch = qc_switch;
|
||||
}
|
||||
|
||||
if (power_supply_supports_power_off(device) && isPowerOffAppRegistered()) {
|
||||
lv_obj_t* power_off_container = lv_obj_create(card);
|
||||
lv_obj_set_width(power_off_container, LV_PCT(100));
|
||||
lv_obj_set_height(power_off_container, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(power_off_container, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(power_off_container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(power_off_container, false);
|
||||
lv_obj_set_style_pad_all(power_off_container, 0, 0);
|
||||
lv_obj_set_style_pad_gap(power_off_container, 0, 0);
|
||||
lvgl::obj_set_style_bg_invisible(power_off_container);
|
||||
|
||||
lv_obj_t* label = lv_label_create(power_off_container);
|
||||
lv_label_set_text(label, "Device power");
|
||||
lv_obj_set_flex_grow(label, 1);
|
||||
|
||||
lv_obj_t* power_off_button = lv_button_create(power_off_container);
|
||||
lv_label_set_text(lv_label_create(power_off_button), "Power off");
|
||||
lv_obj_add_event_cb(power_off_button, onPowerOffPressed, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
}
|
||||
|
||||
PowerSupplyPropertyValue value;
|
||||
for (auto property : DISPLAYED_PROPERTIES) {
|
||||
if (power_supply_get_property(device, property, &value) == ERROR_NONE) {
|
||||
|
||||
@ -120,10 +120,10 @@ int main(int argc, char* argv[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef PLATFORM_ESP
|
||||
constexpr auto memory_sufficient = memory_external_total() > 0;
|
||||
#ifdef ESP_PLATFORM
|
||||
const bool memory_sufficient = memory_external_total() > 0;
|
||||
#else
|
||||
constexpr auto memory_sufficient = true;
|
||||
const bool memory_sufficient = true;
|
||||
#endif
|
||||
|
||||
// The canvas holds a full frame, which needs external RAM on larger displays.
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
#include <Tactility/TactilityConfig.h>
|
||||
#include <Tactility/lvgl/KeyboardDeviceListener.h>
|
||||
#include <Tactility/lvgl/Statusbar.h>
|
||||
#include <Tactility/lvgl/SystemBars.h>
|
||||
#include <Tactility/lvgl/TrackballInit.h>
|
||||
#include <Tactility/lvgl/UsbHidInput.h>
|
||||
#include <Tactility/service/ServiceManifest.h>
|
||||
@ -113,6 +114,9 @@ static lv_obj_t* windowManagerScreenInit(lv_obj_t* root) {
|
||||
// focused textarea shows it (see lvgl_keyboard_add_textarea()/textarea_show_keyboard()).
|
||||
lvgl_software_keyboard_construct(&softwareKeyboard, root);
|
||||
|
||||
// Places the statusbar on top, or in a strip on the side
|
||||
systemBarsAttach(vertical_container, statusbar, app_container);
|
||||
|
||||
return app_container;
|
||||
}
|
||||
|
||||
@ -211,6 +215,10 @@ static void onLvglStopped() {
|
||||
check(service::removeService(service::memorychecker::manifest.id));
|
||||
check(service::removeService(service::statusbar::manifest.id));
|
||||
|
||||
lvgl_lock();
|
||||
systemBarsDetach();
|
||||
lvgl_unlock();
|
||||
|
||||
if (softwareKeyboard.object != nullptr) {
|
||||
// lv_obj_delete() walks/mutates the object graph (event lists, group membership,
|
||||
// parent/child links). Without the LVGL lock this can race the LVGL port task's own
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include <Tactility/Timer.h>
|
||||
#include <Tactility/lvgl/Statusbar.h>
|
||||
#include <Tactility/lvgl/Style.h>
|
||||
#include <Tactility/lvgl/SystemMenu.h>
|
||||
#include <Tactility/settings/Time.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
@ -12,14 +13,13 @@
|
||||
#include <tactility/system_event.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <app/start.h>
|
||||
|
||||
#include <lvgl/devices/indev.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/insets.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/theme.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/widgets/focus_container.h>
|
||||
#include <lvgl/widgets/icon_button.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
@ -36,9 +36,10 @@ static void onUpdateTime();
|
||||
|
||||
struct StatusbarIcon {
|
||||
std::string image;
|
||||
std::string appId;
|
||||
bool visible = false;
|
||||
bool claimed = false;
|
||||
// When the icon was added: the side strip drops the newest icons that don't fit
|
||||
uint32_t added_order = 0;
|
||||
};
|
||||
|
||||
struct StatusbarData {
|
||||
@ -49,6 +50,7 @@ struct StatusbarData {
|
||||
uint8_t time_hours = 0;
|
||||
uint8_t time_minutes = 0;
|
||||
bool time_set = false;
|
||||
uint32_t next_added_order = 0;
|
||||
};
|
||||
|
||||
static StatusbarData statusbar_data;
|
||||
@ -59,6 +61,15 @@ typedef struct {
|
||||
lv_obj_t* icons[STATUSBAR_ICON_LIMIT];
|
||||
lv_obj_t* battery_icon;
|
||||
PubSub<void*>::SubscriptionHandle pubsub_subscription;
|
||||
// The parent of the icons, which opens the system menu
|
||||
lv_obj_t* icon_row;
|
||||
// Pushes the action container to the bottom in the side layout. Null on circular displays.
|
||||
lv_obj_t* spacer;
|
||||
// Holds the action buttons of apps in the side layout. Null on circular displays.
|
||||
lv_obj_t* action_container;
|
||||
// The horizontal padding of the split layout
|
||||
int32_t split_pad_hor;
|
||||
bool side;
|
||||
} Statusbar;
|
||||
|
||||
static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj);
|
||||
@ -67,6 +78,8 @@ static void statusbar_event(const lv_obj_class_t* class_p, lv_event_t* event);
|
||||
|
||||
static void update_time(Statusbar* statusbar);
|
||||
static void update_main(Statusbar* statusbar);
|
||||
static void update_icons(Statusbar* statusbar);
|
||||
static void onSideSizeChanged(lv_event_t* event);
|
||||
|
||||
static TickType_t getNextUpdateTime() {
|
||||
time_t now = ::time(nullptr);
|
||||
@ -161,21 +174,49 @@ static void update_icon(lv_obj_t* image, const StatusbarIcon* icon) {
|
||||
} else {
|
||||
lv_obj_set_hidden(image, true);
|
||||
}
|
||||
lv_obj_set_clickable(image, !icon->appId.empty());
|
||||
}
|
||||
|
||||
static void onIconClicked(lv_event_t* event) {
|
||||
const auto index = reinterpret_cast<intptr_t>(lv_event_get_user_data(event));
|
||||
statusbar_data.mutex.lock();
|
||||
const std::string app_id = statusbar_data.icons[index].appId;
|
||||
statusbar_data.mutex.unlock();
|
||||
if (app_id.empty()) {
|
||||
static void onIconRowClicked(lv_event_t* /*event*/) {
|
||||
systemMenuShow();
|
||||
}
|
||||
|
||||
static bool is_inside(lv_obj_t* obj, lv_obj_t* ancestor) {
|
||||
for (lv_obj_t* current = obj; current != nullptr; current = lv_obj_get_parent(current)) {
|
||||
if (current == ancestor) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// In the side strip on the left, the right key moves the focus to the first widget outside of the strip
|
||||
static void onIconRowKey(lv_event_t* event) {
|
||||
auto* statusbar = static_cast<Statusbar*>(lv_event_get_user_data(event));
|
||||
if (!statusbar->side) {
|
||||
return;
|
||||
}
|
||||
AppStartContext context;
|
||||
uint32_t instance_id = 0;
|
||||
if (app_start_context_from_id(app_id.c_str(), &context) == ERROR_NONE) {
|
||||
app_start_with_context(&context, &instance_id);
|
||||
if (lv_event_get_key(event) != LV_KEY_RIGHT) {
|
||||
return;
|
||||
}
|
||||
lv_group_t* group = lv_obj_get_group(statusbar->icon_row);
|
||||
if (group == nullptr) {
|
||||
return;
|
||||
}
|
||||
const uint32_t count = lv_group_get_obj_count(group);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
lv_group_focus_next(group);
|
||||
if (!is_inside(lv_group_get_focused(group), &statusbar->obj)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void onStatusbarGesture(lv_event_t* /*event*/) {
|
||||
lv_indev_t* indev = lv_indev_active();
|
||||
if (indev != nullptr && lv_indev_get_gesture_dir(indev) == LV_DIR_BOTTOM) {
|
||||
// The release must not click the pressed widget
|
||||
lv_indev_wait_release(indev);
|
||||
systemMenuShow();
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,7 +239,7 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
|
||||
DisplayShape shape;
|
||||
lvgl_display_get_shape(display, &shape);
|
||||
|
||||
// The parent of the icons, which grid navigation moves through
|
||||
// The parent of the icons, which opens the system menu
|
||||
lv_obj_t* icon_row;
|
||||
if (shape.shape == DISPLAY_SHAPE_CIRCLE) {
|
||||
// Two centered rows, as the top of a circle is narrow: the time, and the icons below it
|
||||
@ -211,9 +252,7 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
|
||||
statusbar->time = lv_label_create(obj);
|
||||
update_time(statusbar);
|
||||
|
||||
icon_row = lv_obj_create(obj);
|
||||
lv_obj_remove_style_all(icon_row);
|
||||
lv_obj_set_size(icon_row, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
icon_row = lvgl_focus_container_create(obj);
|
||||
lv_obj_set_style_pad_ver(icon_row, static_cast<int32_t>(icon_size / 10), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_column(icon_row, icon_padding, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(icon_row, LV_FLEX_FLOW_ROW);
|
||||
@ -240,14 +279,39 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
|
||||
lv_obj_set_flex_grow(left_spacer, 1);
|
||||
lv_obj_set_style_bg_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_opa(left_spacer, LV_OPA_0, LV_STATE_DEFAULT);
|
||||
statusbar->spacer = left_spacer;
|
||||
|
||||
icon_row = obj;
|
||||
icon_row = lvgl_focus_container_create(obj);
|
||||
lv_obj_set_style_pad_column(icon_row, icon_padding, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_row(icon_row, icon_padding, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_margin_right(icon_row, 2, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(icon_row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(icon_row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
auto* action_container = lv_obj_create(obj);
|
||||
lv_obj_remove_style_all(action_container);
|
||||
lv_obj_set_size(action_container, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(action_container, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(action_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_scrollable(action_container, false);
|
||||
lv_obj_set_hidden(action_container, true);
|
||||
statusbar->action_container = action_container;
|
||||
statusbar->split_pad_hor = lv_obj_get_style_pad_left(obj, LV_PART_MAIN);
|
||||
}
|
||||
statusbar->icon_row = icon_row;
|
||||
|
||||
lv_obj_add_flag(icon_row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(icon_row, onIconRowClicked, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
lv_obj_add_event_cb(icon_row, onIconRowKey, LV_EVENT_KEY, statusbar);
|
||||
|
||||
// Swiping down from anywhere on the statusbar opens the system menu
|
||||
lv_obj_set_gesture_bubble(obj, false);
|
||||
lv_obj_add_event_cb(obj, onStatusbarGesture, LV_EVENT_GESTURE, nullptr);
|
||||
if (statusbar->action_container != nullptr) {
|
||||
lv_obj_add_event_cb(obj, onSideSizeChanged, LV_EVENT_SIZE_CHANGED, statusbar);
|
||||
lv_obj_add_event_cb(statusbar->action_container, onSideSizeChanged, LV_EVENT_SIZE_CHANGED, statusbar);
|
||||
}
|
||||
|
||||
// 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 = !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);
|
||||
for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) {
|
||||
@ -255,12 +319,6 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
|
||||
lv_obj_set_size(image, icon_size, icon_size); // regular padding doesn't work
|
||||
lv_obj_set_style_text_font(image, lvgl_get_statusbar_icon_font(), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_all(image, 0, LV_STATE_DEFAULT);
|
||||
if (own_focus_ring) {
|
||||
lv_obj_set_style_outline_color(image, focus_color, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_set_style_outline_width(image, focus_width, LV_STATE_FOCUS_KEY);
|
||||
lv_obj_set_style_outline_pad(image, -focus_width, LV_STATE_FOCUS_KEY);
|
||||
}
|
||||
lv_obj_add_event_cb(image, onIconClicked, LV_EVENT_SHORT_CLICKED, reinterpret_cast<void*>(static_cast<intptr_t>(i)));
|
||||
statusbar->icons[i] = image;
|
||||
|
||||
update_icon(image, &(statusbar_data.icons[i]));
|
||||
@ -275,12 +333,8 @@ lv_obj_t* statusbar_create(lv_obj_t* parent) {
|
||||
});
|
||||
statusbar_data.mutex.unlock();
|
||||
|
||||
// Without a touchscreen, the icons are selected with the keys
|
||||
if (lvgl_indev_exists(LV_INDEV_TYPE_KEYPAD)) {
|
||||
lvgl_grid_navigation_add(icon_row);
|
||||
}
|
||||
// Only the icon row is focused when it isn't the statusbar itself
|
||||
if (icon_row != obj && lv_obj_get_group(obj) != nullptr) {
|
||||
// Only the icon row is focused, it joins the group by itself
|
||||
if (lv_obj_get_group(obj) != nullptr) {
|
||||
lv_group_remove_obj(obj);
|
||||
}
|
||||
|
||||
@ -297,17 +351,61 @@ static void update_time(Statusbar* statusbar) {
|
||||
}
|
||||
}
|
||||
|
||||
static void update_main(Statusbar* statusbar) {
|
||||
update_time(statusbar);
|
||||
// In the side strip, the time and the action buttons always show. The icons get the height that's left, and the newest ones are hidden when they don't fit.
|
||||
// Call while holding statusbar_data.mutex, after update_icon() set the icons' visibility.
|
||||
static void apply_side_icon_limit(Statusbar* statusbar) {
|
||||
if (!statusbar->side) {
|
||||
return;
|
||||
}
|
||||
lv_obj_t* obj = &statusbar->obj;
|
||||
const int32_t gap = lv_obj_get_style_pad_row(obj, LV_PART_MAIN);
|
||||
// The time, the icons, the spacer and the actions are separated by 3 gaps, and the spacer is at least 1 pixel high
|
||||
const int32_t taken = lv_obj_get_height(statusbar->time) + lv_obj_get_style_margin_bottom(statusbar->time, LV_PART_MAIN) +
|
||||
lv_obj_get_height(statusbar->action_container) + 3 * gap + 1;
|
||||
const int32_t available = lv_obj_get_content_height(obj) - taken;
|
||||
const int32_t icon_size = static_cast<int32_t>(lvgl_get_statusbar_icon_font_height());
|
||||
const int32_t icon_gap = lv_obj_get_style_pad_row(statusbar->icon_row, LV_PART_MAIN);
|
||||
const int32_t fitting = available >= icon_size ? (available + icon_gap) / (icon_size + icon_gap) : 0;
|
||||
|
||||
// The shown icons, oldest first
|
||||
int shown[STATUSBAR_ICON_LIMIT];
|
||||
int shown_count = 0;
|
||||
for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) {
|
||||
if (!lv_obj_is_hidden(statusbar->icons[i])) {
|
||||
shown[shown_count++] = i;
|
||||
}
|
||||
}
|
||||
std::sort(shown, shown + shown_count, [](int a, int b) {
|
||||
return statusbar_data.icons[a].added_order < statusbar_data.icons[b].added_order;
|
||||
});
|
||||
for (int i = static_cast<int>(fitting); i < shown_count; ++i) {
|
||||
lv_obj_set_hidden(statusbar->icons[shown[i]], true);
|
||||
}
|
||||
}
|
||||
|
||||
static void update_icons(Statusbar* statusbar) {
|
||||
if (statusbar_data.mutex.lock(200 / portTICK_PERIOD_MS)) {
|
||||
for (int i = 0; i < STATUSBAR_ICON_LIMIT; ++i) {
|
||||
update_icon(statusbar->icons[i], &(statusbar_data.icons[i]));
|
||||
}
|
||||
apply_side_icon_limit(statusbar);
|
||||
statusbar_data.mutex.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
// The room for the icons changes with the strip's height and the actions that apps add
|
||||
static void onSideSizeChanged(lv_event_t* event) {
|
||||
auto* statusbar = static_cast<Statusbar*>(lv_event_get_user_data(event));
|
||||
if (statusbar->side) {
|
||||
update_icons(statusbar);
|
||||
}
|
||||
}
|
||||
|
||||
static void update_main(Statusbar* statusbar) {
|
||||
update_time(statusbar);
|
||||
update_icons(statusbar);
|
||||
}
|
||||
|
||||
static void statusbar_event(const lv_obj_class_t* class_p, lv_event_t* event) {
|
||||
// Call the ancestor's event handler
|
||||
lv_result_t result = lv_obj_event_base(&statusbar_class, event);
|
||||
@ -331,6 +429,7 @@ int8_t statusbar_icon_add(const std::string& image, bool visible) {
|
||||
statusbar_data.icons[i].claimed = true;
|
||||
statusbar_data.icons[i].visible = visible;
|
||||
statusbar_data.icons[i].image = image;
|
||||
statusbar_data.icons[i].added_order = statusbar_data.next_added_order++;
|
||||
result = i;
|
||||
LOG_D(TAG, "id %d: added", (int)i);
|
||||
break;
|
||||
@ -353,7 +452,6 @@ void statusbar_icon_remove(int8_t id) {
|
||||
icon->claimed = false;
|
||||
icon->visible = false;
|
||||
icon->image = "";
|
||||
icon->appId = "";
|
||||
statusbar_data.mutex.unlock();
|
||||
statusbar_data.pubsub->publish(nullptr);
|
||||
}
|
||||
@ -384,20 +482,121 @@ void statusbar_icon_set_visibility(int8_t id, bool visible) {
|
||||
statusbar_data.pubsub->publish(nullptr);
|
||||
}
|
||||
|
||||
void statusbar_icon_set_app(int8_t id, const std::string& appId) {
|
||||
check(id >= 0 && id < STATUSBAR_ICON_LIMIT);
|
||||
statusbar_data.mutex.lock();
|
||||
StatusbarIcon* icon = &statusbar_data.icons[id];
|
||||
check(icon->claimed);
|
||||
icon->appId = appId;
|
||||
statusbar_data.mutex.unlock();
|
||||
statusbar_data.pubsub->publish(nullptr);
|
||||
// The colour that is visible behind the object: its own background or the first visible one of its parents
|
||||
static lv_color_t get_visible_bg_color(lv_obj_t* obj) {
|
||||
for (lv_obj_t* current = obj; current != nullptr; current = lv_obj_get_parent(current)) {
|
||||
if (lv_obj_get_style_bg_opa(current, LV_PART_MAIN) >= LV_OPA_50) {
|
||||
return lv_obj_get_style_bg_color(current, LV_PART_MAIN);
|
||||
}
|
||||
}
|
||||
return lv_obj_get_style_bg_color(obj, LV_PART_MAIN);
|
||||
}
|
||||
|
||||
// The width of an app's action button in the side strip, measured with a temporary button like the ones apps add
|
||||
static int32_t get_action_button_width(lv_obj_t* actionContainer) {
|
||||
auto* button = lvgl_icon_button_create(actionContainer);
|
||||
// It must not take the focus
|
||||
if (lv_obj_get_group(button) != nullptr) {
|
||||
lv_group_remove_obj(button);
|
||||
}
|
||||
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, LVGL_ICON_SHARED_SETTINGS);
|
||||
lv_obj_update_layout(button);
|
||||
const int32_t width = lv_obj_get_width(button);
|
||||
lv_obj_delete(button);
|
||||
return width;
|
||||
}
|
||||
|
||||
// The width of the widest time that the label can show, as the font's digits can differ in width
|
||||
static int32_t get_widest_time_width(const lv_font_t* font, int32_t letterSpace) {
|
||||
char widest_digit = '0';
|
||||
int32_t widest_digit_width = 0;
|
||||
for (char digit = '0'; digit <= '9'; digit++) {
|
||||
const char text[2] = { digit, '\0' };
|
||||
lv_point_t size;
|
||||
lv_text_get_size(&size, text, font, 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE);
|
||||
if (size.x > widest_digit_width) {
|
||||
widest_digit_width = size.x;
|
||||
widest_digit = digit;
|
||||
}
|
||||
}
|
||||
const char text[6] = { widest_digit, widest_digit, ':', widest_digit, widest_digit, '\0' };
|
||||
lv_point_t size;
|
||||
lv_text_get_size(&size, text, font, letterSpace, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE);
|
||||
return size.x;
|
||||
}
|
||||
|
||||
void statusbar_set_side(lv_obj_t* obj, bool side) {
|
||||
auto* statusbar = reinterpret_cast<Statusbar*>(obj);
|
||||
// Circular displays only have room for the statusbar at the top
|
||||
if (statusbar->action_container == nullptr || statusbar->side == side) {
|
||||
return;
|
||||
}
|
||||
statusbar->side = side;
|
||||
|
||||
lv_obj_remove_local_style_prop(obj, LV_STYLE_BG_COLOR, LV_PART_MAIN);
|
||||
lv_obj_remove_local_style_prop(obj, LV_STYLE_BG_OPA, LV_PART_MAIN);
|
||||
if (side) {
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_hor(obj, 2, LV_STATE_DEFAULT);
|
||||
// A fixed width, so the strip doesn't change when an app adds or removes action buttons
|
||||
lv_obj_set_hidden(statusbar->action_container, false);
|
||||
const int32_t time_width = get_widest_time_width(lvgl_get_text_font(FONT_SIZE_SMALL), lv_obj_get_style_text_letter_space(statusbar->time, LV_PART_MAIN));
|
||||
const int32_t content_width = std::max(get_action_button_width(statusbar->action_container), time_width);
|
||||
lv_obj_set_size(obj, content_width + 2 * 2, LV_PCT(100));
|
||||
// Keeps the action buttons away from the screen's bottom edge. The compact layout has no room to spare.
|
||||
const bool compact = lvgl_theme_is_compact();
|
||||
lv_obj_set_style_pad_top(obj, compact ? 0 : 4, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_bottom(obj, compact ? 0 : 8, LV_STATE_DEFAULT);
|
||||
// Slightly darker than the colour behind it, so the strip stands out from the app next to it
|
||||
const lv_color_t background = get_visible_bg_color(obj);
|
||||
lv_obj_set_style_bg_color(obj, lv_color_darken(background, LV_OPA_20), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_margin_left(statusbar->time, 0, LV_STATE_DEFAULT);
|
||||
// The small font fits the time in the narrow strip
|
||||
lv_obj_set_style_text_font(statusbar->time, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_margin_bottom(statusbar->time, 10, LV_STATE_DEFAULT);
|
||||
// As wide as the strip, so the column of items is centered on the strip's full width
|
||||
lv_obj_set_width(statusbar->time, LV_PCT(100));
|
||||
lv_obj_set_style_text_align(statusbar->time, LV_TEXT_ALIGN_CENTER, LV_STATE_DEFAULT);
|
||||
// The time stays on one line, also when it's wider than expected
|
||||
lv_label_set_long_mode(statusbar->time, LV_LABEL_LONG_MODE_CLIP);
|
||||
lv_obj_set_style_margin_right(statusbar->icon_row, 0, LV_STATE_DEFAULT);
|
||||
// The icon that's last in the top bar is closest to the screen's corner there, so it goes on top
|
||||
lv_obj_set_flex_flow(statusbar->icon_row, LV_FLEX_FLOW_COLUMN_REVERSE);
|
||||
// The time, the icons, the spacer and then the actions
|
||||
lv_obj_move_to_index(statusbar->icon_row, 1);
|
||||
lv_obj_set_hidden(statusbar->action_container, false);
|
||||
} else {
|
||||
lv_obj_set_size(obj, LV_PCT(100), statusbar_get_height());
|
||||
lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_hor(obj, statusbar->split_pad_hor, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_ver(obj, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_margin_left(statusbar->time, 4, LV_STATE_DEFAULT);
|
||||
lv_obj_remove_local_style_prop(statusbar->time, LV_STYLE_TEXT_FONT, LV_PART_MAIN);
|
||||
lv_obj_remove_local_style_prop(statusbar->time, LV_STYLE_MARGIN_BOTTOM, LV_PART_MAIN);
|
||||
lv_obj_set_width(statusbar->time, LV_SIZE_CONTENT);
|
||||
lv_obj_remove_local_style_prop(statusbar->time, LV_STYLE_TEXT_ALIGN, LV_PART_MAIN);
|
||||
lv_label_set_long_mode(statusbar->time, LV_LABEL_LONG_MODE_WRAP);
|
||||
lv_obj_set_style_margin_right(statusbar->icon_row, 2, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(statusbar->icon_row, LV_FLEX_FLOW_ROW);
|
||||
// The time, the spacer and then the icons
|
||||
lv_obj_move_to_index(statusbar->icon_row, 2);
|
||||
lv_obj_set_hidden(statusbar->action_container, true);
|
||||
}
|
||||
// The split layout shows all icons, the side strip only the ones that fit
|
||||
update_icons(statusbar);
|
||||
}
|
||||
|
||||
lv_obj_t* statusbar_get_action_container(lv_obj_t* obj) {
|
||||
return reinterpret_cast<Statusbar*>(obj)->action_container;
|
||||
}
|
||||
|
||||
int statusbar_get_height() {
|
||||
const auto icon_size = lvgl_get_statusbar_icon_font_height();
|
||||
const auto vertical_padding = static_cast<uint32_t>((static_cast<float>(icon_size) * 0.1f));
|
||||
return icon_size + (2 * vertical_padding);
|
||||
return icon_size + (2 * vertical_padding) + 4;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
194
Tactility/Source/lvgl/SystemBars.cpp
Normal file
194
Tactility/Source/lvgl/SystemBars.cpp
Normal file
@ -0,0 +1,194 @@
|
||||
#include <Tactility/lvgl/SystemBars.h>
|
||||
|
||||
#include <Tactility/lvgl/Statusbar.h>
|
||||
#include <Tactility/lvgl/SystemMenu.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <lvgl/theme.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::lvgl {
|
||||
|
||||
using settings::launcher::SystemBarsLayout;
|
||||
|
||||
namespace {
|
||||
|
||||
struct Listener {
|
||||
SystemBarsListenerId id;
|
||||
std::function<void()> callback;
|
||||
};
|
||||
|
||||
// All state is guarded by the LVGL lock
|
||||
struct SystemBarsState {
|
||||
lv_obj_t* container = nullptr;
|
||||
lv_obj_t* statusbar = nullptr;
|
||||
lv_obj_t* appContainer = nullptr;
|
||||
lv_display_t* display = nullptr;
|
||||
SystemBarsLayout layout = SystemBarsLayout::Split;
|
||||
bool applied = false;
|
||||
// Where the current press of a pointer started, for swipes from the top edge
|
||||
lv_point_t pressPoint = { 0, 0 };
|
||||
SystemBarsListenerId nextListenerId = 1;
|
||||
std::vector<Listener> listeners;
|
||||
};
|
||||
|
||||
SystemBarsState state;
|
||||
|
||||
void onResolutionChanged(lv_event_t* /*event*/) {
|
||||
systemBarsRefresh();
|
||||
}
|
||||
|
||||
// The target of a pointer device's own events is the device
|
||||
lv_indev_t* getEventIndev(lv_event_t* event) {
|
||||
return static_cast<lv_indev_t*>(lv_event_get_current_target(event));
|
||||
}
|
||||
|
||||
void onPointerPressed(lv_event_t* event) {
|
||||
lv_indev_get_point(getEventIndev(event), &state.pressPoint);
|
||||
}
|
||||
|
||||
// In the side layout, the top edge has no statusbar to swipe down from, so a swipe down that starts there opens the system menu
|
||||
void onPointerReleased(lv_event_t* event) {
|
||||
if (state.layout != SystemBarsLayout::Side || window_manager_get_active_layer() != WINDOW_LAYER_APP) {
|
||||
return;
|
||||
}
|
||||
lv_indev_t* indev = getEventIndev(event);
|
||||
lv_point_t point;
|
||||
lv_indev_get_point(indev, &point);
|
||||
const int32_t edge = statusbar_get_height();
|
||||
const int32_t dy = point.y - state.pressPoint.y;
|
||||
const int32_t dx = point.x - state.pressPoint.x;
|
||||
if (state.pressPoint.y <= edge && dy >= edge * 2 && dy > 2 * LV_ABS(dx)) {
|
||||
// The release must not click the widget below the pointer
|
||||
lv_indev_reset(indev, nullptr);
|
||||
systemMenuShow();
|
||||
}
|
||||
}
|
||||
|
||||
void forEachPointer(void (*action)(lv_indev_t* indev)) {
|
||||
for (lv_indev_t* indev = lv_indev_get_next(nullptr); indev != nullptr; indev = lv_indev_get_next(indev)) {
|
||||
if (lv_indev_get_type(indev) == LV_INDEV_TYPE_POINTER) {
|
||||
action(indev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void removePointerCallbacks(lv_indev_t* indev) {
|
||||
lv_indev_remove_event_cb_with_user_data(indev, onPointerPressed, nullptr);
|
||||
lv_indev_remove_event_cb_with_user_data(indev, onPointerReleased, nullptr);
|
||||
}
|
||||
|
||||
void addPointerCallbacks(lv_indev_t* indev) {
|
||||
removePointerCallbacks(indev);
|
||||
lv_indev_add_event_cb(indev, onPointerPressed, LV_EVENT_PRESSED, nullptr);
|
||||
lv_indev_add_event_cb(indev, onPointerReleased, LV_EVENT_RELEASED, nullptr);
|
||||
}
|
||||
|
||||
// The monochrome theme has no tints, so a line separates the strip from the app
|
||||
void applySeparator(bool side) {
|
||||
lv_obj_remove_local_style_prop(state.statusbar, LV_STYLE_BORDER_SIDE, LV_PART_MAIN);
|
||||
lv_obj_remove_local_style_prop(state.statusbar, LV_STYLE_BORDER_WIDTH, LV_PART_MAIN);
|
||||
lv_obj_remove_local_style_prop(state.statusbar, LV_STYLE_BORDER_COLOR, LV_PART_MAIN);
|
||||
lv_obj_remove_local_style_prop(state.statusbar, LV_STYLE_BORDER_OPA, LV_PART_MAIN);
|
||||
if (side && lvgl_theme_is_mono()) {
|
||||
lv_obj_set_style_border_side(state.statusbar, LV_BORDER_SIDE_RIGHT, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(state.statusbar, 1, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_color(state.statusbar, lv_obj_get_style_text_color(state.statusbar, LV_PART_MAIN), LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_opa(state.statusbar, LV_OPA_COVER, LV_STATE_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
// The statusbar is the container's first child: on top in the split layout, and on the left in the side layout
|
||||
void applyLayout(SystemBarsLayout layout) {
|
||||
const bool side = layout == SystemBarsLayout::Side;
|
||||
statusbar_set_side(state.statusbar, side);
|
||||
applySeparator(side);
|
||||
if (side) {
|
||||
lv_obj_set_flex_flow(state.container, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_size(state.appContainer, LV_SIZE_CONTENT, LV_PCT(100));
|
||||
} else {
|
||||
lv_obj_set_flex_flow(state.container, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(state.appContainer, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
}
|
||||
lv_obj_set_flex_grow(state.appContainer, 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void systemBarsAttach(lv_obj_t* container, lv_obj_t* statusbar, lv_obj_t* appContainer) {
|
||||
systemBarsDetach();
|
||||
state.container = container;
|
||||
state.statusbar = statusbar;
|
||||
state.appContainer = appContainer;
|
||||
state.display = lv_obj_get_display(container);
|
||||
state.applied = false;
|
||||
lv_display_add_event_cb(state.display, onResolutionChanged, LV_EVENT_RESOLUTION_CHANGED, nullptr);
|
||||
systemBarsRefresh();
|
||||
}
|
||||
|
||||
void systemBarsDetach() {
|
||||
if (state.display != nullptr) {
|
||||
lv_display_remove_event_cb_with_user_data(state.display, onResolutionChanged, nullptr);
|
||||
forEachPointer(removePointerCallbacks);
|
||||
}
|
||||
state.container = nullptr;
|
||||
state.statusbar = nullptr;
|
||||
state.appContainer = nullptr;
|
||||
state.display = nullptr;
|
||||
state.layout = SystemBarsLayout::Split;
|
||||
}
|
||||
|
||||
void systemBarsRefresh() {
|
||||
if (state.container == nullptr) {
|
||||
return;
|
||||
}
|
||||
// Also picks up pointer devices that were added since the last refresh
|
||||
forEachPointer(addPointerCallbacks);
|
||||
const auto settings = settings::launcher::loadOrGetDefault();
|
||||
const auto width = lv_display_get_horizontal_resolution(state.display);
|
||||
const auto height = lv_display_get_vertical_resolution(state.display);
|
||||
auto layout = settings::launcher::resolveSystemBarsLayout(settings, settings::launcher::getSystemBarsCapabilities(), width, height);
|
||||
// Circular displays only have room for the statusbar at the top
|
||||
if (statusbar_get_action_container(state.statusbar) == nullptr) {
|
||||
layout = SystemBarsLayout::Split;
|
||||
}
|
||||
if (state.applied && layout == state.layout) {
|
||||
return;
|
||||
}
|
||||
applyLayout(layout);
|
||||
state.layout = layout;
|
||||
state.applied = true;
|
||||
|
||||
// A copy, as a listener may remove itself
|
||||
const auto listeners = state.listeners;
|
||||
for (const auto& listener : listeners) {
|
||||
listener.callback();
|
||||
}
|
||||
}
|
||||
|
||||
SystemBarsLayout systemBarsGetLayout() {
|
||||
return state.layout;
|
||||
}
|
||||
|
||||
lv_obj_t* systemBarsGetActionContainer() {
|
||||
if (state.statusbar == nullptr || state.layout != SystemBarsLayout::Side) {
|
||||
return nullptr;
|
||||
}
|
||||
return statusbar_get_action_container(state.statusbar);
|
||||
}
|
||||
|
||||
SystemBarsListenerId systemBarsAddListener(std::function<void()> listener) {
|
||||
const auto id = state.nextListenerId++;
|
||||
state.listeners.push_back({ id, std::move(listener) });
|
||||
return id;
|
||||
}
|
||||
|
||||
void systemBarsRemoveListener(SystemBarsListenerId id) {
|
||||
std::erase_if(state.listeners, [id](const Listener& listener) { return listener.id == id; });
|
||||
}
|
||||
|
||||
}
|
||||
284
Tactility/Source/lvgl/SystemMenu.cpp
Normal file
284
Tactility/Source/lvgl/SystemMenu.cpp
Normal file
@ -0,0 +1,284 @@
|
||||
#include <Tactility/lvgl/SystemMenu.h>
|
||||
|
||||
#include <Tactility/TactilityPrivate.h>
|
||||
#include <Tactility/bluetooth/Bluetooth.h>
|
||||
|
||||
#include <app/manager.h>
|
||||
#include <app/start.h>
|
||||
|
||||
#include <gps/gps.h>
|
||||
|
||||
#include <lvgl/devices/indev.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/theme.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/icon_button.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <tactility/concurrent/dispatcher.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/bluetooth.h>
|
||||
#include <tactility/drivers/power_supply.h>
|
||||
#include <tactility/drivers/wifi.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
namespace tt::lvgl {
|
||||
|
||||
constexpr auto* TAG = "SystemMenu";
|
||||
constexpr auto* POWER_OFF_APP_ID = "tactility.poweroff";
|
||||
|
||||
static void* toContext(bool enabled) {
|
||||
return reinterpret_cast<void*>(static_cast<intptr_t>(enabled));
|
||||
}
|
||||
|
||||
static bool fromContext(void* context) {
|
||||
return reinterpret_cast<intptr_t>(context) != 0;
|
||||
}
|
||||
|
||||
static bool hasWifi() {
|
||||
return device_exists_of_type(&WIFI_TYPE);
|
||||
}
|
||||
|
||||
static bool hasGps() {
|
||||
return device_exists_of_type(&GPS_TYPE);
|
||||
}
|
||||
|
||||
static bool hasBluetooth() {
|
||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||
return device_exists_of_type(&BLUETOOTH_TYPE);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool isWifiEnabled() {
|
||||
Device* device = nullptr;
|
||||
if (device_get_first_by_type(&WIFI_TYPE, &device) != ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
WifiRadioState state = WIFI_RADIO_STATE_OFF;
|
||||
wifi_get_radio_state(device, &state);
|
||||
device_put(device);
|
||||
return state == WIFI_RADIO_STATE_ON || state == WIFI_RADIO_STATE_ON_PENDING;
|
||||
}
|
||||
|
||||
static void setWifiEnabled(void* context) {
|
||||
Device* device = nullptr;
|
||||
if (device_get_first_by_type(&WIFI_TYPE, &device) != ERROR_NONE) {
|
||||
return;
|
||||
}
|
||||
error_t result = fromContext(context) ? wifi_set_radio_on(device) : wifi_set_radio_off(device);
|
||||
if (result != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to set Wi-Fi radio state (%s)", error_to_string(result));
|
||||
}
|
||||
device_put(device);
|
||||
}
|
||||
|
||||
static bool isGpsEnabled() {
|
||||
bool enabled = false;
|
||||
device_for_each_of_type(&GPS_TYPE, &enabled, [](Device* device, void* context) {
|
||||
if (device_is_ready(device)) {
|
||||
*static_cast<bool*>(context) = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return enabled;
|
||||
}
|
||||
|
||||
static void setGpsEnabled(void* context) {
|
||||
device_for_each_of_type(&GPS_TYPE, context, [](Device* device, void* context) {
|
||||
const bool enabled = fromContext(context);
|
||||
if (enabled && !device_is_ready(device)) {
|
||||
device_start(device);
|
||||
} else if (!enabled && device_is_ready(device)) {
|
||||
device_stop(device);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||
|
||||
static bool isBluetoothEnabled() {
|
||||
Device* device = nullptr;
|
||||
if (device_get_first_by_type(&BLUETOOTH_TYPE, &device) != ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
bool enabled = bluetooth::isRadioOnOrPending(device);
|
||||
device_put(device);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
static void setBluetoothEnabled(void* context) {
|
||||
Device* device = nullptr;
|
||||
if (device_get_first_by_type(&BLUETOOTH_TYPE, &device) != ERROR_NONE) {
|
||||
return;
|
||||
}
|
||||
const bool enabled = fromContext(context);
|
||||
if (enabled != bluetooth::isRadioOnOrPending(device)) {
|
||||
if (enabled) {
|
||||
bluetooth::start(device);
|
||||
} else {
|
||||
bluetooth::stop(device);
|
||||
}
|
||||
}
|
||||
device_put(device);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// Changing the radio and device states can block, so it happens on the main task
|
||||
struct ToggleOption {
|
||||
const char* enabledIcon;
|
||||
const char* disabledIcon;
|
||||
bool (*isEnabled)();
|
||||
DispatcherCallback setEnabled;
|
||||
};
|
||||
|
||||
static const char* getToggleIcon(const ToggleOption* option, bool enabled) {
|
||||
return enabled ? option->enabledIcon : option->disabledIcon;
|
||||
}
|
||||
|
||||
static void onToggleChanged(lv_event_t* event) {
|
||||
auto* toggle = lv_event_get_target_obj(event);
|
||||
const auto* option = static_cast<const ToggleOption*>(lv_event_get_user_data(event));
|
||||
const bool enabled = lv_obj_has_state(toggle, LV_STATE_CHECKED);
|
||||
lv_label_set_text(lv_obj_get_child(toggle, 0), getToggleIcon(option, enabled));
|
||||
dispatcher_dispatch_timed(getMainDispatcherHandle(), toContext(enabled), option->setEnabled, portMAX_DELAY);
|
||||
}
|
||||
|
||||
static void onCloseClicked(lv_event_t* /*event*/) {
|
||||
window_manager_overlay_hide();
|
||||
}
|
||||
|
||||
static void onBackdropGesture(lv_event_t* /*event*/) {
|
||||
lv_indev_t* indev = lv_indev_active();
|
||||
if (indev != nullptr && lv_indev_get_gesture_dir(indev) == LV_DIR_TOP) {
|
||||
// The release must not click the pressed widget, e.g. a toggle
|
||||
lv_indev_wait_release(indev);
|
||||
window_manager_overlay_hide();
|
||||
}
|
||||
}
|
||||
|
||||
static bool supportsPowerOff() {
|
||||
::AppManifest manifest;
|
||||
if (app_manager_find_manifest(POWER_OFF_APP_ID, &manifest) != ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
bool supported = false;
|
||||
device_for_each_of_type(&POWER_SUPPLY_TYPE, &supported, [](Device* device, void* context) {
|
||||
if (device_is_ready(device) && power_supply_supports_power_off(device)) {
|
||||
*static_cast<bool*>(context) = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return supported;
|
||||
}
|
||||
|
||||
static void onPowerOffClicked(lv_event_t* /*event*/) {
|
||||
// The app's window is created in the app layer, which the overlay would cover
|
||||
window_manager_overlay_hide();
|
||||
AppStartContext context;
|
||||
uint32_t instance_id = 0;
|
||||
if (app_start_context_from_id(POWER_OFF_APP_ID, &context) == ERROR_NONE) {
|
||||
app_start_with_context(&context, &instance_id);
|
||||
}
|
||||
}
|
||||
|
||||
// A square icon button, sized for the menu's tiles
|
||||
static lv_obj_t* createTile(lv_obj_t* parent, const char* icon) {
|
||||
const auto icon_height = static_cast<int32_t>(lvgl_get_shared_icon_default_font_height());
|
||||
|
||||
auto* tile = lvgl_icon_button_create(parent);
|
||||
lv_obj_set_size(tile, icon_height * 2, icon_height * 2);
|
||||
|
||||
auto* icon_label = lv_label_create(tile);
|
||||
lv_obj_set_style_text_font(icon_label, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(icon_label, icon);
|
||||
lv_obj_center(icon_label);
|
||||
return tile;
|
||||
}
|
||||
|
||||
// A tile that the theme shows as on while it's checked
|
||||
static void createOption(lv_obj_t* parent, const ToggleOption* option) {
|
||||
const bool enabled = option->isEnabled();
|
||||
auto* toggle = createTile(parent, getToggleIcon(option, enabled));
|
||||
lv_obj_add_flag(toggle, LV_OBJ_FLAG_CHECKABLE);
|
||||
lv_obj_set_state(toggle, LV_STATE_CHECKED, enabled);
|
||||
lv_obj_add_event_cb(toggle, onToggleChanged, LV_EVENT_VALUE_CHANGED, const_cast<ToggleOption*>(option));
|
||||
}
|
||||
|
||||
static void createWidgets(lv_obj_t* parent, void* /*userData*/) {
|
||||
// Swiping up anywhere closes the menu
|
||||
lv_obj_set_gesture_bubble(parent, false);
|
||||
lv_obj_add_event_cb(parent, onBackdropGesture, LV_EVENT_GESTURE, nullptr);
|
||||
|
||||
// The card fits its content, up to most of the screen
|
||||
auto* card = lvgl_card_create(parent);
|
||||
lv_obj_set_size(card, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_max_height(card, LV_PCT(90), LV_STATE_DEFAULT);
|
||||
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_center(card);
|
||||
|
||||
auto* grid = lv_obj_create(card);
|
||||
lv_obj_remove_style_all(grid);
|
||||
lv_obj_set_size(grid, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
// A percentage can't limit the width, as the card's width depends on the grid. The tiles wrap at the limit.
|
||||
const int32_t max_card_width = lv_display_get_horizontal_resolution(lv_obj_get_display(parent)) * 9 / 10;
|
||||
const int32_t card_padding = lv_obj_get_style_pad_left(card, LV_PART_MAIN) + lv_obj_get_style_pad_right(card, LV_PART_MAIN);
|
||||
lv_obj_set_style_max_width(grid, max_card_width - card_padding, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP);
|
||||
lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
const int32_t gap = lvgl_theme_is_compact() ? 8 : 16;
|
||||
lv_obj_set_style_pad_column(grid, gap, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_row(grid, gap, LV_STATE_DEFAULT);
|
||||
|
||||
static constexpr ToggleOption wifi_option = { LVGL_ICON_SHARED_WIFI, LVGL_ICON_SHARED_WIFI_OFF, isWifiEnabled, setWifiEnabled };
|
||||
if (hasWifi()) {
|
||||
createOption(grid, &wifi_option);
|
||||
}
|
||||
static constexpr ToggleOption gps_option = { LVGL_ICON_SHARED_LOCATION_ON, LVGL_ICON_SHARED_LOCATION_OFF, isGpsEnabled, setGpsEnabled };
|
||||
if (hasGps()) {
|
||||
createOption(grid, &gps_option);
|
||||
}
|
||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||
static constexpr ToggleOption bluetooth_option = { LVGL_ICON_SHARED_BLUETOOTH, LVGL_ICON_SHARED_BLUETOOTH_DISABLED, isBluetoothEnabled, setBluetoothEnabled };
|
||||
if (hasBluetooth()) {
|
||||
createOption(grid, &bluetooth_option);
|
||||
}
|
||||
#endif
|
||||
if (supportsPowerOff()) {
|
||||
auto* power_tile = createTile(grid, LVGL_ICON_SHARED_POWER_SETTINGS_NEW);
|
||||
lv_obj_add_event_cb(power_tile, onPowerOffClicked, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
}
|
||||
// The arrow keys move between the tiles. Toggles that are focused directly would be switched by them.
|
||||
lvgl_grid_navigation_add(grid);
|
||||
|
||||
// Without a pointer device, tapping outside of the card or swiping up isn't possible
|
||||
if (!lvgl_indev_exists(LV_INDEV_TYPE_POINTER)) {
|
||||
auto* close_button = createTile(card, LVGL_ICON_SHARED_CLOSE);
|
||||
lv_obj_add_event_cb(close_button, onCloseClicked, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void systemMenuShow() {
|
||||
if (!hasWifi() && !hasGps() && !hasBluetooth() && !supportsPowerOff()) {
|
||||
return;
|
||||
}
|
||||
window_manager_overlay_show(createWidgets, nullptr, nullptr);
|
||||
}
|
||||
|
||||
}
|
||||
@ -55,7 +55,6 @@ bool MemoryCheckerService::onStart(ServiceContext& service) {
|
||||
lock.lock();
|
||||
|
||||
statusbarIconId = lvgl::statusbar_icon_add(LVGL_ICON_STATUSBAR_MEMORY, false);
|
||||
lvgl::statusbar_icon_set_app(statusbarIconId, "tactility.systeminfo");
|
||||
lvgl::statusbar_icon_set_visibility(statusbarIconId, false);
|
||||
|
||||
timer.setCallbackPriority(Thread::Priority::Lower);
|
||||
|
||||
@ -80,12 +80,13 @@ static const char* getWifiStatusIcon() {
|
||||
check(false, "not implemented");
|
||||
}
|
||||
|
||||
// Only shown while the radio is on
|
||||
static const char* getBluetoothStatusIcon(tt::bluetooth::RadioState state, bool scanning, bool connected) {
|
||||
switch (state) {
|
||||
using enum tt::bluetooth::RadioState;
|
||||
case Off:
|
||||
case OffPending:
|
||||
return LVGL_ICON_STATUSBAR_BLUETOOTH_DISABLED;
|
||||
return nullptr;
|
||||
case OnPending:
|
||||
case On:
|
||||
if (connected) return LVGL_ICON_STATUSBAR_BLUETOOTH_CONNECTED;
|
||||
@ -149,8 +150,6 @@ class StatusbarService final : public Service {
|
||||
const char* bt_last_icon = nullptr;
|
||||
int8_t wifi_icon_id;
|
||||
const char* wifi_last_icon = nullptr;
|
||||
int8_t sdcard_icon_id;
|
||||
const char* sdcard_last_icon = nullptr;
|
||||
int8_t power_icon_id;
|
||||
const char* power_last_icon = nullptr;
|
||||
int8_t usb_icon_id;
|
||||
@ -165,12 +164,8 @@ class StatusbarService final : public Service {
|
||||
}
|
||||
|
||||
void updateGpsIcon() {
|
||||
const char* icon = nullptr;
|
||||
if (device_has_active_by_type(&GPS_TYPE)) {
|
||||
icon = LVGL_ICON_STATUSBAR_LOCATION_ON;
|
||||
} else if (device_exists_of_type(&GPS_TYPE)) {
|
||||
icon = LVGL_ICON_STATUSBAR_LOCATION_OFF;
|
||||
}
|
||||
// Only shown while a GPS device is on
|
||||
const char* icon = device_has_active_by_type(&GPS_TYPE) ? LVGL_ICON_STATUSBAR_LOCATION_ON : nullptr;
|
||||
if (gps_last_icon != icon) {
|
||||
if (icon != nullptr) {
|
||||
lvgl::statusbar_icon_set_image(gps_icon_id, icon);
|
||||
@ -290,42 +285,12 @@ class StatusbarService final : public Service {
|
||||
}
|
||||
}
|
||||
|
||||
void updateSdCardIcon() {
|
||||
struct SdCardState { bool found; bool mounted; };
|
||||
SdCardState state = { false, false };
|
||||
|
||||
file_system_for_each(&state, [](FileSystem* fs, void* context) {
|
||||
auto* s = static_cast<SdCardState*>(context);
|
||||
char path[64];
|
||||
if (file_system_get_path(fs, path, sizeof(path)) != ERROR_NONE) return true;
|
||||
if (strncmp(path, "/sdcard", 7) != 0) return true;
|
||||
s->found = true;
|
||||
s->mounted = file_system_is_mounted(fs);
|
||||
return false;
|
||||
});
|
||||
|
||||
if (state.found) {
|
||||
auto* desired_icon = getSdCardStatusIcon(state.mounted);
|
||||
if (sdcard_last_icon != desired_icon) {
|
||||
lvgl::statusbar_icon_set_image(sdcard_icon_id, desired_icon);
|
||||
lvgl::statusbar_icon_set_visibility(sdcard_icon_id, true);
|
||||
sdcard_last_icon = desired_icon;
|
||||
}
|
||||
} else {
|
||||
if (sdcard_last_icon != nullptr) {
|
||||
lvgl::statusbar_icon_set_visibility(sdcard_icon_id, false);
|
||||
sdcard_last_icon = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void update() {
|
||||
if (lvgl_is_running()) {
|
||||
if (lvgl_try_lock(200)) {
|
||||
updateGpsIcon();
|
||||
updateBluetoothIcon();
|
||||
updateWifiIcon();
|
||||
updateSdCardIcon();
|
||||
updatePowerStatusIcon();
|
||||
updateUsbIcon();
|
||||
lvgl_unlock();
|
||||
@ -339,21 +304,13 @@ public:
|
||||
gps_icon_id = lvgl::statusbar_icon_add();
|
||||
bt_icon_id = lvgl::statusbar_icon_add();
|
||||
usb_icon_id = lvgl::statusbar_icon_add();
|
||||
sdcard_icon_id = lvgl::statusbar_icon_add();
|
||||
wifi_icon_id = lvgl::statusbar_icon_add();
|
||||
power_icon_id = lvgl::statusbar_icon_add();
|
||||
lvgl::statusbar_icon_set_app(gps_icon_id, "tactility.gpssettings");
|
||||
lvgl::statusbar_icon_set_app(bt_icon_id, "tactility.btmanage");
|
||||
lvgl::statusbar_icon_set_app(usb_icon_id, "tactility.usbsettings");
|
||||
lvgl::statusbar_icon_set_app(sdcard_icon_id, "tactility.files");
|
||||
lvgl::statusbar_icon_set_app(wifi_icon_id, "tactility.wifimanage");
|
||||
lvgl::statusbar_icon_set_app(power_icon_id, "tactility.power");
|
||||
}
|
||||
|
||||
~StatusbarService() override {
|
||||
lvgl::statusbar_icon_remove(power_icon_id);
|
||||
lvgl::statusbar_icon_remove(wifi_icon_id);
|
||||
lvgl::statusbar_icon_remove(sdcard_icon_id);
|
||||
lvgl::statusbar_icon_remove(usb_icon_id);
|
||||
lvgl::statusbar_icon_remove(bt_icon_id);
|
||||
lvgl::statusbar_icon_remove(gps_icon_id);
|
||||
|
||||
@ -206,7 +206,6 @@ bool WebServerService::onStart(ServiceContext& service) {
|
||||
|
||||
// Create statusbar icon (hidden initially, shown when server actually starts)
|
||||
statusbarIconId = lvgl::statusbar_icon_add();
|
||||
lvgl::statusbar_icon_set_app(statusbarIconId, "tactility.webserversettings");
|
||||
lvgl::statusbar_icon_set_visibility(statusbarIconId, false);
|
||||
|
||||
// Load and cache settings once at boot
|
||||
|
||||
138
Tactility/Source/settings/LauncherSettings.cpp
Normal file
138
Tactility/Source/settings/LauncherSettings.cpp
Normal file
@ -0,0 +1,138 @@
|
||||
#include <Tactility/settings/LauncherSettings.h>
|
||||
|
||||
#include <Tactility/file/File.h>
|
||||
#include <Tactility/file/PropertiesFile.h>
|
||||
|
||||
#include <tactility/paths.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace tt::settings::launcher {
|
||||
|
||||
constexpr auto* SETTINGS_KEY_SYSTEM_BARS = "systemBars";
|
||||
constexpr auto* SYSTEM_BARS_AUTO = "auto";
|
||||
constexpr auto* SYSTEM_BARS_CUSTOM = "custom";
|
||||
constexpr auto* SETTINGS_KEY_PORTRAIT_LAYOUT = "portraitLayout";
|
||||
constexpr auto* SETTINGS_KEY_LANDSCAPE_LAYOUT = "landscapeLayout";
|
||||
constexpr auto* LAYOUT_SPLIT = "split";
|
||||
constexpr auto* LAYOUT_SIDE = "side";
|
||||
|
||||
static const char* toString(SystemBarsLayout layout) {
|
||||
return layout == SystemBarsLayout::Side ? LAYOUT_SIDE : LAYOUT_SPLIT;
|
||||
}
|
||||
|
||||
static SystemBarsLayout toLayout(const std::map<std::string, std::string>& map, const char* key, SystemBarsLayout defaultValue) {
|
||||
auto entry = map.find(key);
|
||||
if (entry == map.end()) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (entry->second == LAYOUT_SIDE) {
|
||||
return SystemBarsLayout::Side;
|
||||
}
|
||||
if (entry->second == LAYOUT_SPLIT) {
|
||||
return SystemBarsLayout::Split;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
static bool getSettingsFilePath(std::string& outPath) {
|
||||
char root[128];
|
||||
if (paths_get_data_path(root, sizeof(root)) != ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
outPath = std::string(root) + "/settings/launcher.properties";
|
||||
return true;
|
||||
}
|
||||
|
||||
bool load(LauncherSettings& settings) {
|
||||
std::string settings_path;
|
||||
if (!getSettingsFilePath(settings_path) || !file::isFile(settings_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> map;
|
||||
if (!file::loadPropertiesFile(settings_path, map)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
settings = getDefault();
|
||||
auto system_bars = map.find(SETTINGS_KEY_SYSTEM_BARS);
|
||||
if (system_bars != map.end() && system_bars->second == SYSTEM_BARS_CUSTOM) {
|
||||
settings.systemBarsMode = SystemBarsMode::Custom;
|
||||
}
|
||||
settings.portraitLayout = toLayout(map, SETTINGS_KEY_PORTRAIT_LAYOUT, settings.portraitLayout);
|
||||
settings.landscapeLayout = toLayout(map, SETTINGS_KEY_LANDSCAPE_LAYOUT, settings.landscapeLayout);
|
||||
return true;
|
||||
}
|
||||
|
||||
LauncherSettings getDefault() {
|
||||
return LauncherSettings {
|
||||
.systemBarsMode = SystemBarsMode::Auto,
|
||||
.portraitLayout = SystemBarsLayout::Split,
|
||||
.landscapeLayout = SystemBarsLayout::Side
|
||||
};
|
||||
}
|
||||
|
||||
LauncherSettings loadOrGetDefault() {
|
||||
LauncherSettings settings;
|
||||
if (!load(settings)) {
|
||||
settings = getDefault();
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
bool save(const LauncherSettings& settings) {
|
||||
std::map<std::string, std::string> map;
|
||||
map[SETTINGS_KEY_SYSTEM_BARS] = settings.systemBarsMode == SystemBarsMode::Custom ? SYSTEM_BARS_CUSTOM : SYSTEM_BARS_AUTO;
|
||||
map[SETTINGS_KEY_PORTRAIT_LAYOUT] = toString(settings.portraitLayout);
|
||||
map[SETTINGS_KEY_LANDSCAPE_LAYOUT] = toString(settings.landscapeLayout);
|
||||
|
||||
std::string settings_path;
|
||||
if (!getSettingsFilePath(settings_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file::findOrCreateParentDirectory(settings_path, 0755)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return file::savePropertiesFile(settings_path, map);
|
||||
}
|
||||
|
||||
SystemBarsCapabilities getSystemBarsCapabilities() {
|
||||
SystemBarsCapabilities capabilities = { SystemBarsCapability::Any, SystemBarsCapability::Any };
|
||||
#if defined(CONFIG_TT_SYSTEM_BARS_PORTRAIT_SPLIT)
|
||||
capabilities.portrait = SystemBarsCapability::Split;
|
||||
#elif defined(CONFIG_TT_SYSTEM_BARS_PORTRAIT_SIDE)
|
||||
capabilities.portrait = SystemBarsCapability::Side;
|
||||
#endif
|
||||
#if defined(CONFIG_TT_SYSTEM_BARS_LANDSCAPE_SPLIT)
|
||||
capabilities.landscape = SystemBarsCapability::Split;
|
||||
#elif defined(CONFIG_TT_SYSTEM_BARS_LANDSCAPE_SIDE)
|
||||
capabilities.landscape = SystemBarsCapability::Side;
|
||||
#endif
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
SystemBarsLayout resolveSystemBarsLayout(const LauncherSettings& settings, const SystemBarsCapabilities& capabilities, int32_t width, int32_t height) {
|
||||
const bool landscape = width > height;
|
||||
const auto capability = landscape ? capabilities.landscape : capabilities.portrait;
|
||||
if (capability == SystemBarsCapability::Split) {
|
||||
return SystemBarsLayout::Split;
|
||||
}
|
||||
if (capability == SystemBarsCapability::Side) {
|
||||
return SystemBarsLayout::Side;
|
||||
}
|
||||
if (settings.systemBarsMode == SystemBarsMode::Custom) {
|
||||
return landscape ? settings.landscapeLayout : settings.portraitLayout;
|
||||
}
|
||||
// Only wide landscape screens: an aspect ratio of 1.5 or more
|
||||
return (landscape && (width * 2 >= height * 3)) ? SystemBarsLayout::Side : SystemBarsLayout::Split;
|
||||
}
|
||||
|
||||
} // namespace tt::settings::launcher
|
||||
49
Tactility/Tests/Source/SystemBarsLayoutTest.cpp
Normal file
49
Tactility/Tests/Source/SystemBarsLayoutTest.cpp
Normal file
@ -0,0 +1,49 @@
|
||||
#include "doctest.h"
|
||||
#include <Tactility/settings/LauncherSettings.h>
|
||||
|
||||
using namespace tt::settings::launcher;
|
||||
|
||||
static constexpr SystemBarsCapabilities ANY = { SystemBarsCapability::Any, SystemBarsCapability::Any };
|
||||
|
||||
TEST_CASE("auto system bars use the side layout only for wide landscape screens") {
|
||||
const LauncherSettings settings = getDefault();
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, ANY, 320, 240), SystemBarsLayout::Split);
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, ANY, 480, 222), SystemBarsLayout::Side);
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, ANY, 240, 320), SystemBarsLayout::Split);
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, ANY, 222, 480), SystemBarsLayout::Split);
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, ANY, 300, 200), SystemBarsLayout::Side);
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, ANY, 240, 240), SystemBarsLayout::Split);
|
||||
}
|
||||
|
||||
TEST_CASE("custom system bars use the layout of the screen's orientation") {
|
||||
LauncherSettings settings = getDefault();
|
||||
settings.systemBarsMode = SystemBarsMode::Custom;
|
||||
settings.portraitLayout = SystemBarsLayout::Side;
|
||||
settings.landscapeLayout = SystemBarsLayout::Split;
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, ANY, 240, 320), SystemBarsLayout::Side);
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, ANY, 480, 222), SystemBarsLayout::Split);
|
||||
// A square screen counts as portrait
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, ANY, 240, 240), SystemBarsLayout::Side);
|
||||
}
|
||||
|
||||
TEST_CASE("a fixed capability decides the layout instead of the settings") {
|
||||
const SystemBarsCapabilities fixed = { SystemBarsCapability::Side, SystemBarsCapability::Split };
|
||||
LauncherSettings settings = getDefault();
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, fixed, 240, 320), SystemBarsLayout::Side);
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, fixed, 480, 222), SystemBarsLayout::Split);
|
||||
settings.systemBarsMode = SystemBarsMode::Custom;
|
||||
settings.portraitLayout = SystemBarsLayout::Split;
|
||||
settings.landscapeLayout = SystemBarsLayout::Side;
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, fixed, 240, 320), SystemBarsLayout::Side);
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, fixed, 480, 222), SystemBarsLayout::Split);
|
||||
}
|
||||
|
||||
TEST_CASE("an orientation with the Any capability follows the settings while the other is fixed") {
|
||||
const SystemBarsCapabilities landscape_fixed = { SystemBarsCapability::Any, SystemBarsCapability::Split };
|
||||
LauncherSettings settings = getDefault();
|
||||
settings.systemBarsMode = SystemBarsMode::Custom;
|
||||
settings.portraitLayout = SystemBarsLayout::Side;
|
||||
settings.landscapeLayout = SystemBarsLayout::Side;
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, landscape_fixed, 240, 320), SystemBarsLayout::Side);
|
||||
CHECK_EQ(resolveSystemBarsLayout(settings, landscape_fixed, 480, 222), SystemBarsLayout::Split);
|
||||
}
|
||||
11
device.py
11
device.py
@ -101,6 +101,12 @@ def get_user_data_location(device_properties: dict):
|
||||
exit_with_error(f"storage.userDataLocation must be 'SD' or 'Internal', but was: '{user_data_location}'")
|
||||
return user_data_location
|
||||
|
||||
def get_system_bars_capability(device_properties: dict, key: str):
|
||||
capability = get_property_or_default(device_properties, key, "any").lower()
|
||||
if capability not in ("any", "split", "side"):
|
||||
exit_with_error(f"{key} must be 'any', 'split' or 'side', but was: '{capability}'")
|
||||
return capability
|
||||
|
||||
def write_partition_table(output_file, device_properties: dict, is_dev: bool):
|
||||
flash_size = get_property_or_exit(device_properties, "hardware.flashSize")
|
||||
if not flash_size.endswith("MB"):
|
||||
@ -150,6 +156,11 @@ def write_tactility_variables(output_file, device_properties: dict, device_id: s
|
||||
output_file.write("CONFIG_TT_USER_DATA_LOCATION_SD=y\n")
|
||||
else:
|
||||
output_file.write("CONFIG_TT_USER_DATA_LOCATION_INTERNAL=y\n")
|
||||
# System bars layouts per orientation
|
||||
portrait = get_system_bars_capability(device_properties, "ui.systemBarsPortrait")
|
||||
landscape = get_system_bars_capability(device_properties, "ui.systemBarsLandscape")
|
||||
output_file.write(f"CONFIG_TT_SYSTEM_BARS_PORTRAIT_{portrait.upper()}=y\n")
|
||||
output_file.write(f"CONFIG_TT_SYSTEM_BARS_LANDSCAPE_{landscape.upper()}=y\n")
|
||||
|
||||
def write_core_variables(output_file, device_properties: dict):
|
||||
idf_target = get_property_or_exit(device_properties, "hardware.target").lower()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user