Files
pyxis/lib/tdeck_ui/UI/LXMF/ComposeScreen.cpp
torlando-tech ac6ceca9f8 Initial commit: standalone Pyxis T-Deck firmware
Split T-Deck firmware from microReticulum examples/lxmf_tdeck/ into its
own repo. microReticulum is consumed as a git submodule dependency pinned
to feat/t-deck. All include paths updated from relative symlinks to bare
includes resolved via library build flags.

Both tdeck (NimBLE) and tdeck-bluedroid environments compile successfully.
Licensed under AGPLv3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 19:48:33 -05:00

321 lines
11 KiB
C++

// Copyright (c) 2024 microReticulum contributors
// SPDX-License-Identifier: MIT
#include "ComposeScreen.h"
#include "Theme.h"
#ifdef ARDUINO
#include "Log.h"
#include "../LVGL/LVGLLock.h"
#include "../LVGL/LVGLInit.h"
#include "../TextAreaHelper.h"
using namespace RNS;
namespace UI {
namespace LXMF {
ComposeScreen::ComposeScreen(lv_obj_t* parent)
: _screen(nullptr), _header(nullptr), _content_area(nullptr), _button_area(nullptr),
_text_area_dest(nullptr), _text_area_message(nullptr),
_btn_cancel(nullptr), _btn_send(nullptr), _btn_back(nullptr) {
LVGL_LOCK();
// Create screen object
if (parent) {
_screen = lv_obj_create(parent);
} else {
_screen = lv_obj_create(lv_scr_act());
}
lv_obj_set_size(_screen, LV_PCT(100), LV_PCT(100));
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_bg_color(_screen, lv_color_hex(0x121212), 0); // Dark background
lv_obj_set_style_bg_opa(_screen, LV_OPA_COVER, 0);
lv_obj_set_style_pad_all(_screen, 0, 0);
lv_obj_set_style_border_width(_screen, 0, 0);
lv_obj_set_style_radius(_screen, 0, 0);
// Create UI components
create_header();
create_content_area();
create_button_area();
// Hide by default
hide();
TRACE("ComposeScreen created");
}
ComposeScreen::~ComposeScreen() {
LVGL_LOCK();
if (_screen) {
lv_obj_del(_screen);
}
}
void ComposeScreen::create_header() {
_header = lv_obj_create(_screen);
lv_obj_set_size(_header, LV_PCT(100), 36);
lv_obj_align(_header, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_set_style_bg_color(_header, lv_color_hex(0x1a1a1a), 0); // Dark header
lv_obj_set_style_border_width(_header, 0, 0);
lv_obj_set_style_radius(_header, 0, 0);
lv_obj_set_style_pad_all(_header, 0, 0);
// Back button
_btn_back = lv_btn_create(_header);
lv_obj_set_size(_btn_back, 50, 28);
lv_obj_align(_btn_back, LV_ALIGN_LEFT_MID, 2, 0);
lv_obj_set_style_bg_color(_btn_back, lv_color_hex(0x333333), 0);
lv_obj_set_style_bg_color(_btn_back, lv_color_hex(0x444444), LV_STATE_PRESSED);
lv_obj_add_event_cb(_btn_back, on_back_clicked, LV_EVENT_CLICKED, this);
lv_obj_t* label_back = lv_label_create(_btn_back);
lv_label_set_text(label_back, LV_SYMBOL_LEFT);
lv_obj_center(label_back);
lv_obj_set_style_text_color(label_back, lv_color_hex(0xe0e0e0), 0);
// Title
lv_obj_t* title = lv_label_create(_header);
lv_label_set_text(title, "New Message");
lv_obj_align(title, LV_ALIGN_LEFT_MID, 60, 0);
lv_obj_set_style_text_color(title, lv_color_hex(0xffffff), 0);
lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0);
}
void ComposeScreen::create_content_area() {
_content_area = lv_obj_create(_screen);
lv_obj_set_size(_content_area, LV_PCT(100), 152);
lv_obj_align(_content_area, LV_ALIGN_TOP_MID, 0, 36);
lv_obj_set_style_pad_all(_content_area, 6, 0);
lv_obj_set_style_bg_color(_content_area, lv_color_hex(0x121212), 0); // Match screen bg
lv_obj_set_style_border_width(_content_area, 0, 0);
lv_obj_set_style_radius(_content_area, 0, 0);
lv_obj_clear_flag(_content_area, LV_OBJ_FLAG_SCROLLABLE);
// "To:" label
lv_obj_t* label_to = lv_label_create(_content_area);
lv_label_set_text(label_to, "To:");
lv_obj_align(label_to, LV_ALIGN_TOP_LEFT, 0, 0);
lv_obj_set_style_text_color(label_to, lv_color_hex(0xb0b0b0), 0);
// Destination hash input
_text_area_dest = lv_textarea_create(_content_area);
lv_obj_set_size(_text_area_dest, LV_PCT(100), 36);
lv_obj_align(_text_area_dest, LV_ALIGN_TOP_LEFT, 0, 18);
lv_textarea_set_placeholder_text(_text_area_dest, "Destination hash (32 hex)");
lv_textarea_set_one_line(_text_area_dest, true);
lv_textarea_set_max_length(_text_area_dest, 32);
lv_textarea_set_accepted_chars(_text_area_dest, "0123456789abcdefABCDEF");
// Dark text area styling
lv_obj_set_style_bg_color(_text_area_dest, lv_color_hex(0x2a2a2a), 0);
lv_obj_set_style_text_color(_text_area_dest, lv_color_hex(0xffffff), 0);
lv_obj_set_style_border_color(_text_area_dest, lv_color_hex(0x404040), 0);
// Enable paste on long-press
TextAreaHelper::enable_paste(_text_area_dest);
// "Message:" label
lv_obj_t* label_message = lv_label_create(_content_area);
lv_label_set_text(label_message, "Message:");
lv_obj_align(label_message, LV_ALIGN_TOP_LEFT, 0, 60);
lv_obj_set_style_text_color(label_message, lv_color_hex(0xb0b0b0), 0);
// Message input
_text_area_message = lv_textarea_create(_content_area);
lv_obj_set_size(_text_area_message, LV_PCT(100), 70);
lv_obj_align(_text_area_message, LV_ALIGN_TOP_LEFT, 0, 78);
lv_textarea_set_placeholder_text(_text_area_message, "Type your message...");
lv_textarea_set_one_line(_text_area_message, false);
lv_textarea_set_max_length(_text_area_message, 500);
// Dark text area styling
lv_obj_set_style_bg_color(_text_area_message, lv_color_hex(0x2a2a2a), 0);
lv_obj_set_style_text_color(_text_area_message, lv_color_hex(0xffffff), 0);
lv_obj_set_style_border_color(_text_area_message, lv_color_hex(0x404040), 0);
// Enable paste on long-press
TextAreaHelper::enable_paste(_text_area_message);
}
void ComposeScreen::create_button_area() {
_button_area = lv_obj_create(_screen);
lv_obj_set_size(_button_area, LV_PCT(100), 52);
lv_obj_align(_button_area, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_style_bg_color(_button_area, lv_color_hex(0x1a1a1a), 0); // Dark
lv_obj_set_style_border_width(_button_area, 0, 0);
lv_obj_set_style_radius(_button_area, 0, 0);
lv_obj_set_style_pad_all(_button_area, 0, 0);
lv_obj_set_flex_flow(_button_area, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(_button_area, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
// Cancel button
_btn_cancel = lv_btn_create(_button_area);
lv_obj_set_size(_btn_cancel, 110, 36);
lv_obj_set_style_bg_color(_btn_cancel, lv_color_hex(0x3a3a3a), 0);
lv_obj_set_style_bg_color(_btn_cancel, lv_color_hex(0x4a4a4a), LV_STATE_PRESSED);
lv_obj_add_event_cb(_btn_cancel, on_cancel_clicked, LV_EVENT_CLICKED, this);
lv_obj_t* label_cancel = lv_label_create(_btn_cancel);
lv_label_set_text(label_cancel, "Cancel");
lv_obj_center(label_cancel);
lv_obj_set_style_text_color(label_cancel, lv_color_hex(0xe0e0e0), 0);
// Spacer
lv_obj_t* spacer = lv_obj_create(_button_area);
lv_obj_set_size(spacer, 30, 1);
lv_obj_set_style_bg_opa(spacer, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(spacer, 0, 0);
// Send button
_btn_send = lv_btn_create(_button_area);
lv_obj_set_size(_btn_send, 110, 36);
lv_obj_add_event_cb(_btn_send, on_send_clicked, LV_EVENT_CLICKED, this);
lv_obj_set_style_bg_color(_btn_send, Theme::primary(), 0);
lv_obj_set_style_bg_color(_btn_send, Theme::primaryPressed(), LV_STATE_PRESSED);
lv_obj_t* label_send = lv_label_create(_btn_send);
lv_label_set_text(label_send, "Send");
lv_obj_center(label_send);
lv_obj_set_style_text_color(label_send, lv_color_hex(0xffffff), 0);
}
void ComposeScreen::clear() {
LVGL_LOCK();
lv_textarea_set_text(_text_area_dest, "");
lv_textarea_set_text(_text_area_message, "");
}
void ComposeScreen::set_destination(const Bytes& dest_hash) {
LVGL_LOCK();
String hash_str = dest_hash.toHex().c_str();
lv_textarea_set_text(_text_area_dest, hash_str.c_str());
}
void ComposeScreen::set_cancel_callback(CancelCallback callback) {
_cancel_callback = callback;
}
void ComposeScreen::set_send_callback(SendCallback callback) {
_send_callback = callback;
}
void ComposeScreen::show() {
LVGL_LOCK();
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_HIDDEN);
lv_obj_move_foreground(_screen); // Bring to front for touch events
// Add widgets to focus group for trackball navigation
// Note: text areas in edit mode consume arrow keys, so focus on button first
lv_group_t* group = LVGL::LVGLInit::get_default_group();
if (group) {
if (_btn_back) lv_group_add_obj(group, _btn_back);
if (_btn_cancel) lv_group_add_obj(group, _btn_cancel);
if (_btn_send) lv_group_add_obj(group, _btn_send);
// Focus on back button (user can roll to other buttons)
if (_btn_back) {
lv_group_focus_obj(_btn_back);
}
}
}
void ComposeScreen::hide() {
LVGL_LOCK();
// Remove from focus group when hiding
lv_group_t* group = LVGL::LVGLInit::get_default_group();
if (group) {
if (_btn_back) lv_group_remove_obj(_btn_back);
if (_btn_cancel) lv_group_remove_obj(_btn_cancel);
if (_btn_send) lv_group_remove_obj(_btn_send);
}
lv_obj_add_flag(_screen, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t* ComposeScreen::get_object() {
return _screen;
}
void ComposeScreen::on_back_clicked(lv_event_t* event) {
ComposeScreen* screen = (ComposeScreen*)lv_event_get_user_data(event);
if (screen->_cancel_callback) {
screen->_cancel_callback();
}
}
void ComposeScreen::on_cancel_clicked(lv_event_t* event) {
ComposeScreen* screen = (ComposeScreen*)lv_event_get_user_data(event);
if (screen->_cancel_callback) {
screen->_cancel_callback();
}
}
void ComposeScreen::on_send_clicked(lv_event_t* event) {
ComposeScreen* screen = (ComposeScreen*)lv_event_get_user_data(event);
// Get destination hash
const char* dest_text = lv_textarea_get_text(screen->_text_area_dest);
String dest_hash_str(dest_text);
dest_hash_str.trim();
dest_hash_str.toLowerCase();
// Validate destination hash
if (!screen->validate_destination_hash(dest_hash_str)) {
ERROR(("Invalid destination hash: " + dest_hash_str).c_str());
// Show error dialog
lv_obj_t* mbox = lv_msgbox_create(NULL, "Invalid Address",
"Destination must be a 32-character hex address.", NULL, true);
lv_obj_center(mbox);
return;
}
// Get message text
const char* message_text = lv_textarea_get_text(screen->_text_area_message);
String message(message_text);
message.trim();
if (message.length() == 0) {
ERROR("Message is empty");
// Show error dialog
lv_obj_t* mbox = lv_msgbox_create(NULL, "Empty Message",
"Please enter a message to send.", NULL, true);
lv_obj_center(mbox);
return;
}
// Convert hex string to bytes
Bytes dest_hash;
dest_hash.assignHex(dest_hash_str.c_str());
if (screen->_send_callback) {
screen->_send_callback(dest_hash, message);
}
// Clear form
screen->clear();
}
bool ComposeScreen::validate_destination_hash(const String& hash_str) {
// Must be exactly 32 hex characters (16 bytes)
if (hash_str.length() != 32) {
return false;
}
// Check all characters are valid hex
for (size_t i = 0; i < hash_str.length(); i++) {
char c = hash_str.charAt(i);
if (!isxdigit(c)) {
return false;
}
}
return true;
}
} // namespace LXMF
} // namespace UI
#endif // ARDUINO