From 53231ae4b16271de4af7027a4a47d434a757f438 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 7 Feb 2026 15:41:31 -0600 Subject: [PATCH] Added toggable config and default for larger screens to enable / hide bubbles on chat messages (#9560) * Added toggable config and default for largeer screens to enable / hide bubbles on chat messages * Refactor message bubble rendering logic for improved layout and consistency * Move osk_found initialization for trackball/encoder devices before module setup to fix missing keyboard for L1 * Utilize current checks for consistency * Reverted last changes --------- Co-authored-by: Jason P Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> --- src/graphics/draw/MenuHandler.cpp | 300 ++++++++++++++------------ src/graphics/draw/MenuHandler.h | 110 +++++----- src/graphics/draw/MessageRenderer.cpp | 192 ++++++++--------- src/main.cpp | 13 +- src/mesh/NodeDB.cpp | 4 + src/modules/KeyVerificationModule.cpp | 4 +- 6 files changed, 330 insertions(+), 293 deletions(-) diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 9fe018d81..195da09f9 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -58,7 +58,7 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp } // namespace -menuHandler::screenMenus menuHandler::menuQueue = menu_none; +menuHandler::screenMenus menuHandler::menuQueue = MenuNone; uint32_t menuHandler::pickedNodeNum = 0; bool test_enabled = false; uint8_t test_count = 0; @@ -66,7 +66,7 @@ uint8_t test_count = 0; void menuHandler::loraMenu() { static const char *optionsArray[] = {"Back", "Device Role", "Radio Preset", "Frequency Slot", "LoRa Region"}; - enum optionsNumbers { Back = 0, device_role_picker = 1, radio_preset_picker = 2, frequency_slot = 3, lora_picker = 4 }; + enum optionsNumbers { Back = 0, DeviceRolePicker = 1, RadioPresetPicker = 2, FrequencySlot = 3, LoraPicker = 4 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "LoRa Actions"; bannerOptions.optionsArrayPtr = optionsArray; @@ -74,14 +74,14 @@ void menuHandler::loraMenu() bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { // No action - } else if (selected == device_role_picker) { - menuHandler::menuQueue = menuHandler::device_role_picker; - } else if (selected == radio_preset_picker) { - menuHandler::menuQueue = menuHandler::radio_preset_picker; - } else if (selected == frequency_slot) { - menuHandler::menuQueue = menuHandler::frequency_slot; - } else if (selected == lora_picker) { - menuHandler::menuQueue = menuHandler::lora_picker; + } else if (selected == DeviceRolePicker) { + menuHandler::menuQueue = menuHandler::DeviceRolePicker; + } else if (selected == RadioPresetPicker) { + menuHandler::menuQueue = menuHandler::RadioPresetPicker; + } else if (selected == FrequencySlot) { + menuHandler::menuQueue = menuHandler::FrequencySlot; + } else if (selected == LoraPicker) { + menuHandler::menuQueue = menuHandler::LoraPicker; } }; screen->showOverlayBanner(bannerOptions); @@ -102,7 +102,7 @@ void menuHandler::OnboardMessage() bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { - menuHandler::menuQueue = menuHandler::no_timeout_lora_picker; + menuHandler::menuQueue = menuHandler::NoTimeoutLoraPicker; screen->runNow(); }; screen->showOverlayBanner(bannerOptions); @@ -216,7 +216,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration) screen->showOverlayBanner(bannerOptions); } -void menuHandler::DeviceRolePicker() +void menuHandler::deviceRolePicker() { static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"}; enum optionsNumbers { @@ -232,7 +232,7 @@ void menuHandler::DeviceRolePicker() bannerOptions.optionsCount = 5; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; + menuHandler::menuQueue = menuHandler::LoraMenu; screen->runNow(); return; } else if (selected == devicerole_client) { @@ -300,7 +300,7 @@ void menuHandler::FrequencySlotPicker() bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; + menuHandler::menuQueue = menuHandler::LoraMenu; screen->runNow(); return; } @@ -313,7 +313,7 @@ void menuHandler::FrequencySlotPicker() screen->showOverlayBanner(bannerOptions); } -void menuHandler::RadioPresetPicker() +void menuHandler::radioPresetPicker() { static const RadioPresetOption presetOptions[] = { {"Back", OptionsAction::Back}, @@ -333,7 +333,7 @@ void menuHandler::RadioPresetPicker() auto bannerOptions = createStaticBannerOptions("Radio Preset", presetOptions, presetLabels, [](const RadioPresetOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::lora_Menu; + menuHandler::menuQueue = menuHandler::LoraMenu; screen->runNow(); return; } @@ -352,7 +352,7 @@ void menuHandler::RadioPresetPicker() screen->showOverlayBanner(bannerOptions); } -void menuHandler::TwelveHourPicker() +void menuHandler::twelveHourPicker() { static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; enum optionsNumbers { Back = 0, twelve = 1, twentyfour = 2 }; @@ -362,7 +362,7 @@ void menuHandler::TwelveHourPicker() bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { - menuHandler::menuQueue = menuHandler::clock_menu; + menuHandler::menuQueue = menuHandler::ClockMenu; screen->runNow(); } else if (selected == twelve) { config.display.use_12h_clock = true; @@ -390,7 +390,7 @@ void menuHandler::showConfirmationBanner(const char *message, std::functionshowOverlayBanner(confirmBanner); } -void menuHandler::ClockFacePicker() +void menuHandler::clockFacePicker() { static const ClockFaceOption clockFaceOptions[] = { {"Back", OptionsAction::Back}, @@ -404,7 +404,7 @@ void menuHandler::ClockFacePicker() auto bannerOptions = createStaticBannerOptions("Which Face?", clockFaceOptions, clockFaceLabels, [](const ClockFaceOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::clock_menu; + menuHandler::menuQueue = menuHandler::ClockMenu; screen->runNow(); return; } @@ -456,7 +456,7 @@ void menuHandler::TZPicker() auto bannerOptions = createStaticBannerOptions( "Pick Timezone", timezoneOptions, timezoneLabels, [](const TimezoneOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuHandler::menuQueue = menuHandler::clock_menu; + menuHandler::menuQueue = menuHandler::ClockMenu; screen->runNow(); return; } @@ -503,13 +503,13 @@ void menuHandler::clockMenu() bannerOptions.optionsCount = 4; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Clock) { - menuHandler::menuQueue = menuHandler::clock_face_picker; + menuHandler::menuQueue = menuHandler::ClockFacePicker; screen->runNow(); } else if (selected == Time) { - menuHandler::menuQueue = menuHandler::twelve_hour_picker; + menuHandler::menuQueue = menuHandler::TwelveHourPicker; screen->runNow(); } else if (selected == Timezone) { - menuHandler::menuQueue = menuHandler::TZ_picker; + menuHandler::menuQueue = menuHandler::TzPicker; screen->runNow(); } }; @@ -572,12 +572,12 @@ void menuHandler::messageResponseMenu() LOG_DEBUG("[ReplyCtx] mode=%d ch=%d peer=0x%08x", (int)mode, ch, (unsigned int)peer); if (selected == ViewMode) { - menuHandler::menuQueue = menuHandler::message_viewmode_menu; + menuHandler::menuQueue = menuHandler::MessageViewModeMenu; screen->runNow(); // Reply submenu } else if (selected == ReplyMenu) { - menuHandler::menuQueue = menuHandler::reply_menu; + menuHandler::menuQueue = menuHandler::ReplyMenu; screen->runNow(); } else if (selected == MuteChannel) { @@ -589,7 +589,7 @@ void menuHandler::messageResponseMenu() } } else if (selected == DeleteMenu) { - menuHandler::menuQueue = menuHandler::delete_messages_menu; + menuHandler::menuQueue = menuHandler::DeleteMessagesMenu; screen->runNow(); #ifdef HAS_I2S @@ -649,7 +649,7 @@ void menuHandler::replyMenu() uint32_t peer = graphics::MessageRenderer::getThreadPeer(); if (selected == Back) { - menuHandler::menuQueue = menuHandler::message_response_menu; + menuHandler::menuQueue = menuHandler::MessageResponseMenu; screen->runNow(); return; } @@ -737,7 +737,7 @@ void menuHandler::deleteMessagesMenu() uint32_t peer = graphics::MessageRenderer::getThreadPeer(); if (selected == Back) { - menuHandler::menuQueue = menuHandler::message_response_menu; + menuHandler::menuQueue = menuHandler::MessageResponseMenu; screen->runNow(); return; } @@ -901,7 +901,7 @@ void menuHandler::messageViewModeMenu() bannerOptions.bannerCallback = [=](int selected) -> void { LOG_DEBUG("messageViewModeMenu: selected=%d", selected); if (selected == -1) { - menuHandler::menuQueue = menuHandler::message_response_menu; + menuHandler::menuQueue = menuHandler::MessageResponseMenu; screen->runNow(); } else if (selected == -2) { graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL); @@ -1083,23 +1083,23 @@ void menuHandler::systemBaseMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Notifications) { - menuHandler::menuQueue = menuHandler::buzzermodemenupicker; + menuHandler::menuQueue = menuHandler::BuzzerModeMenuPicker; screen->runNow(); } else if (selected == ScreenOptions) { - menuHandler::menuQueue = menuHandler::screen_options_menu; + menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; screen->runNow(); } else if (selected == PowerMenu) { - menuHandler::menuQueue = menuHandler::power_menu; + menuHandler::menuQueue = menuHandler::PowerMenu; screen->runNow(); } else if (selected == Test) { - menuHandler::menuQueue = menuHandler::test_menu; + menuHandler::menuQueue = menuHandler::TestMenu; screen->runNow(); } else if (selected == Bluetooth) { - menuQueue = bluetooth_toggle_menu; + menuQueue = BluetoothToggleMenu; screen->runNow(); #if HAS_WIFI && !defined(ARCH_PORTDUINO) } else if (selected == WiFiToggle) { - menuQueue = wifi_toggle_menu; + menuQueue = WifiToggleMenu; screen->runNow(); #endif } else if (selected == Back && !test_enabled) { @@ -1177,7 +1177,7 @@ void menuHandler::favoriteBaseMenu() evt.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; screen->handleUIFrameEvent(&evt); } else if (selected == Remove) { - menuHandler::menuQueue = menuHandler::remove_favorite; + menuHandler::menuQueue = menuHandler::RemoveFavorite; screen->runNow(); } else if (selected == TraceRoute) { if (traceRouteModule) { @@ -1238,15 +1238,15 @@ void menuHandler::positionBaseMenu() auto action = static_cast(option.value); switch (action) { case PositionAction::GpsToggle: - menuQueue = gps_toggle_menu; + menuQueue = GpsToggleMenu; screen->runNow(); break; case PositionAction::GpsFormat: - menuQueue = gps_format_menu; + menuQueue = GpsFormatMenu; screen->runNow(); break; case PositionAction::CompassMenu: - menuQueue = compass_point_north_menu; + menuQueue = CompassPointNorthMenu; screen->runNow(); break; case PositionAction::CompassCalibrate: @@ -1255,15 +1255,15 @@ void menuHandler::positionBaseMenu() } break; case PositionAction::GPSSmartPosition: - menuQueue = gps_smart_position_menu; + menuQueue = GpsSmartPositionMenu; screen->runNow(); break; case PositionAction::GPSUpdateInterval: - menuQueue = gps_update_interval_menu; + menuQueue = GpsUpdateIntervalMenu; screen->runNow(); break; case PositionAction::GPSPositionBroadcast: - menuQueue = gps_position_broadcast_menu; + menuQueue = GpsPositionBroadcastMenu; screen->runNow(); break; } @@ -1303,13 +1303,13 @@ void menuHandler::nodeListMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == NodePicker) { - menuQueue = NodePicker_menu; + menuQueue = NodePickerMenu; screen->runNow(); } else if (selected == Reset) { - menuQueue = reset_node_db_menu; + menuQueue = ResetNodeDbMenu; screen->runNow(); } else if (selected == NodeNameLength) { - menuHandler::menuQueue = menuHandler::node_name_length_menu; + menuHandler::menuQueue = menuHandler::NodeNameLengthMenu; screen->runNow(); } }; @@ -1330,12 +1330,12 @@ void menuHandler::NodePicker() menuHandler::pickedNodeNum = nodenum; // Keep UI favorite context in sync (used elsewhere for some node-based actions) graphics::UIRenderer::currentFavoriteNodeNum = nodenum; - menuQueue = Manage_Node_menu; + menuQueue = ManageNodeMenu; screen->runNow(); }); } -void menuHandler::ManageNodeMenu() +void menuHandler::manageNodeMenu() { // If we don't have a node selected yet, go fast exit auto node = nodeDB->getMeshNode(menuHandler::pickedNodeNum); @@ -1391,7 +1391,7 @@ void menuHandler::ManageNodeMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { - menuQueue = node_base_menu; + menuQueue = NodeBaseMenu; screen->runNow(); return; } @@ -1483,7 +1483,7 @@ void menuHandler::nodeNameLengthMenu() auto bannerOptions = createStaticBannerOptions("Node Name Length", nodeNameOptions, nodeNameLabels, [](const NodeNameOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = node_base_menu; + menuQueue = NodeBaseMenu; screen->runNow(); return; } @@ -1529,7 +1529,7 @@ void menuHandler::resetNodeDBMenu() nodeDB->resetNodes(1); rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); } else if (selected == 0) { - menuQueue = node_base_menu; + menuQueue = NodeBaseMenu; screen->runNow(); } }; @@ -1551,7 +1551,7 @@ void menuHandler::compassNorthMenu() auto bannerOptions = createStaticBannerOptions("North Directions?", compassOptions, compassLabels, [](const CompassOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); return; } @@ -1596,7 +1596,7 @@ void menuHandler::GPSToggleMenu() auto bannerOptions = createStaticBannerOptions("Toggle GPS", gpsToggleOptions, toggleLabels, [](const GPSToggleOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); return; } @@ -1661,7 +1661,7 @@ void menuHandler::GPSFormatMenu() auto onSelection = [](const GPSFormatOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); return; } @@ -1716,7 +1716,7 @@ void menuHandler::GPSSmartPositionMenu() bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); } else if (selected == 1) { config.position.position_broadcast_smart_enabled = true; @@ -1745,7 +1745,7 @@ void menuHandler::GPSUpdateIntervalMenu() bannerOptions.optionsCount = 16; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); } else if (selected == 1) { config.position.gps_update_interval = 8; @@ -1833,7 +1833,7 @@ void menuHandler::GPSPositionBroadcastMenu() bannerOptions.optionsCount = 17; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == 0) { - menuQueue = position_base_menu; + menuQueue = PositionBaseMenu; screen->runNow(); } else if (selected == 1) { config.position.position_broadcast_secs = 60; @@ -1916,7 +1916,7 @@ void menuHandler::GPSPositionBroadcastMenu() #endif -void menuHandler::BluetoothToggleMenu() +void menuHandler::bluetoothToggleMenu() { static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; BannerOverlayOptions bannerOptions; @@ -2044,7 +2044,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) auto bannerOptions = createStaticBannerOptions( "Select Screen Color", colorOptions, colorLabels, [display](const ScreenColorOption &option, int) -> void { if (option.action == OptionsAction::Back) { - menuQueue = system_base_menu; + menuQueue = SystemBaseMenu; screen->runNow(); return; } @@ -2139,7 +2139,7 @@ void menuHandler::rebootMenu() messageStore.saveToFlash(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } else { - menuQueue = power_menu; + menuQueue = PowerMenu; screen->runNow(); } }; @@ -2161,7 +2161,7 @@ void menuHandler::shutdownMenu() InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; inputBroker->injectInputEvent(&event); } else { - menuQueue = power_menu; + menuQueue = PowerMenu; screen->runNow(); } }; @@ -2222,14 +2222,14 @@ void menuHandler::testMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == NumberPicker) { - menuQueue = number_test; + menuQueue = NumberTest; screen->runNow(); } else if (selected == ShowChirpy) { screen->toggleFrameVisibility("chirpy"); screen->setFrames(Screen::FOCUS_SYSTEM); } else { - menuQueue = system_base_menu; + menuQueue = SystemBaseMenu; screen->runNow(); } }; @@ -2253,7 +2253,7 @@ void menuHandler::wifiBaseMenu() bannerOptions.optionsCount = 2; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Wifi_toggle) { - menuQueue = wifi_toggle_menu; + menuQueue = WifiToggleMenu; screen->runNow(); } }; @@ -2302,9 +2302,9 @@ void menuHandler::screenOptionsMenu() hasSupportBrightness = false; #endif - enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits }; - static const char *optionsArray[5] = {"Back"}; - static int optionsEnumArray[5] = {Back}; + enum optionsNumbers { Back, Brightness, ScreenColor, FrameToggles, DisplayUnits, MessageBubbles }; + static const char *optionsArray[6] = {"Back"}; + static int optionsEnumArray[6] = {Back}; int options = 1; // Only show brightness for B&W displays @@ -2326,6 +2326,9 @@ void menuHandler::screenOptionsMenu() optionsArray[options] = "Display Units"; optionsEnumArray[options++] = DisplayUnits; + optionsArray[options] = "Message Bubbles"; + optionsEnumArray[options++] = MessageBubbles; + BannerOverlayOptions bannerOptions; bannerOptions.message = "Display Options"; bannerOptions.optionsArrayPtr = optionsArray; @@ -2333,10 +2336,10 @@ void menuHandler::screenOptionsMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Brightness) { - menuHandler::menuQueue = menuHandler::brightness_picker; + menuHandler::menuQueue = menuHandler::BrightnessPicker; screen->runNow(); } else if (selected == ScreenColor) { - menuHandler::menuQueue = menuHandler::tftcolormenupicker; + menuHandler::menuQueue = menuHandler::TftColorMenuPicker; screen->runNow(); } else if (selected == FrameToggles) { menuHandler::menuQueue = menuHandler::FrameToggles; @@ -2344,8 +2347,11 @@ void menuHandler::screenOptionsMenu() } else if (selected == DisplayUnits) { menuHandler::menuQueue = menuHandler::DisplayUnits; screen->runNow(); + } else if (selected == MessageBubbles) { + menuHandler::menuQueue = menuHandler::MessageBubblesMenu; + screen->runNow(); } else { - menuQueue = system_base_menu; + menuQueue = SystemBaseMenu; screen->runNow(); } }; @@ -2381,16 +2387,16 @@ void menuHandler::powerMenu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Reboot) { - menuHandler::menuQueue = menuHandler::reboot_menu; + menuHandler::menuQueue = menuHandler::RebootMenu; screen->runNow(); } else if (selected == Shutdown) { - menuHandler::menuQueue = menuHandler::shutdown_menu; + menuHandler::menuQueue = menuHandler::ShutdownMenu; screen->runNow(); } else if (selected == MUI) { - menuHandler::menuQueue = menuHandler::mui_picker; + menuHandler::menuQueue = menuHandler::MuiPicker; screen->runNow(); } else { - menuQueue = system_base_menu; + menuQueue = SystemBaseMenu; screen->runNow(); } }; @@ -2428,7 +2434,7 @@ void menuHandler::keyVerificationFinalPrompt() } } -void menuHandler::FrameToggles_menu() +void menuHandler::frameTogglesMenu() { enum optionsNumbers { Finish, @@ -2572,7 +2578,7 @@ void menuHandler::FrameToggles_menu() screen->showOverlayBanner(bannerOptions); } -void menuHandler::DisplayUnits_menu() +void menuHandler::displayUnitsMenu() { enum optionsNumbers { Back, MetricUnits, ImperialUnits }; @@ -2593,7 +2599,34 @@ void menuHandler::DisplayUnits_menu() config.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL; service->reloadConfig(SEGMENT_CONFIG); } else { - menuHandler::menuQueue = menuHandler::screen_options_menu; + menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + +void menuHandler::messageBubblesMenu() +{ + enum optionsNumbers { Back, ShowBubbles, HideBubbles }; + + static const char *optionsArray[] = {"Back", "Show Bubbles", "Hide Bubbles"}; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Message Bubbles"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 3; + bannerOptions.InitialSelected = config.display.enable_message_bubbles ? 1 : 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == ShowBubbles) { + config.display.enable_message_bubbles = true; + service->reloadConfig(SEGMENT_CONFIG); + LOG_INFO("Message bubbles enabled"); + } else if (selected == HideBubbles) { + config.display.enable_message_bubbles = false; + service->reloadConfig(SEGMENT_CONFIG); + LOG_INFO("Message bubbles disabled"); + } else { + menuHandler::menuQueue = menuHandler::ScreenOptionsMenu; screen->runNow(); } }; @@ -2602,153 +2635,156 @@ void menuHandler::DisplayUnits_menu() void menuHandler::handleMenuSwitch(OLEDDisplay *display) { - if (menuQueue != menu_none) + if (menuQueue != MenuNone) test_count = 0; switch (menuQueue) { - case menu_none: + case MenuNone: break; - case lora_Menu: + case LoraMenu: loraMenu(); break; - case lora_picker: + case LoraPicker: LoraRegionPicker(); break; - case device_role_picker: - DeviceRolePicker(); + case DeviceRolePicker: + deviceRolePicker(); break; - case radio_preset_picker: - RadioPresetPicker(); + case RadioPresetPicker: + radioPresetPicker(); break; - case frequency_slot: + case FrequencySlot: FrequencySlotPicker(); break; - case no_timeout_lora_picker: + case NoTimeoutLoraPicker: LoraRegionPicker(0); break; - case TZ_picker: + case TzPicker: TZPicker(); break; - case twelve_hour_picker: - TwelveHourPicker(); + case TwelveHourPicker: + twelveHourPicker(); break; - case clock_face_picker: - ClockFacePicker(); + case ClockFacePicker: + clockFacePicker(); break; - case clock_menu: + case ClockMenu: clockMenu(); break; - case system_base_menu: + case SystemBaseMenu: systemBaseMenu(); break; - case position_base_menu: + case PositionBaseMenu: positionBaseMenu(); break; - case node_base_menu: + case NodeBaseMenu: nodeListMenu(); break; #if !MESHTASTIC_EXCLUDE_GPS - case gps_toggle_menu: + case GpsToggleMenu: GPSToggleMenu(); break; - case gps_format_menu: + case GpsFormatMenu: GPSFormatMenu(); break; - case gps_smart_position_menu: + case GpsSmartPositionMenu: GPSSmartPositionMenu(); break; - case gps_update_interval_menu: + case GpsUpdateIntervalMenu: GPSUpdateIntervalMenu(); break; - case gps_position_broadcast_menu: + case GpsPositionBroadcastMenu: GPSPositionBroadcastMenu(); break; #endif - case compass_point_north_menu: + case CompassPointNorthMenu: compassNorthMenu(); break; - case reset_node_db_menu: + case ResetNodeDbMenu: resetNodeDBMenu(); break; - case buzzermodemenupicker: + case BuzzerModeMenuPicker: BuzzerModeMenu(); break; - case mui_picker: + case MuiPicker: switchToMUIMenu(); break; - case tftcolormenupicker: + case TftColorMenuPicker: TFTColorPickerMenu(display); break; - case brightness_picker: + case BrightnessPicker: BrightnessPickerMenu(); break; - case node_name_length_menu: + case NodeNameLengthMenu: nodeNameLengthMenu(); break; - case reboot_menu: + case RebootMenu: rebootMenu(); break; - case shutdown_menu: + case ShutdownMenu: shutdownMenu(); break; - case NodePicker_menu: + case NodePickerMenu: NodePicker(); break; - case Manage_Node_menu: - ManageNodeMenu(); + case ManageNodeMenu: + manageNodeMenu(); break; - case remove_favorite: + case RemoveFavorite: removeFavoriteMenu(); break; - case trace_route_menu: + case TraceRouteMenu: traceRouteMenu(); break; - case test_menu: + case TestMenu: testMenu(); break; - case number_test: + case NumberTest: numberTest(); break; - case wifi_toggle_menu: + case WifiToggleMenu: wifiToggleMenu(); break; - case key_verification_init: + case KeyVerificationInit: keyVerificationInitMenu(); break; - case key_verification_final_prompt: + case KeyVerificationFinalPrompt: keyVerificationFinalPrompt(); break; - case bluetooth_toggle_menu: - BluetoothToggleMenu(); + case BluetoothToggleMenu: + bluetoothToggleMenu(); break; - case screen_options_menu: + case ScreenOptionsMenu: screenOptionsMenu(); break; - case power_menu: + case PowerMenu: powerMenu(); break; case FrameToggles: - FrameToggles_menu(); + frameTogglesMenu(); break; case DisplayUnits: - DisplayUnits_menu(); + displayUnitsMenu(); break; - case throttle_message: + case ThrottleMessage: screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); break; - case message_response_menu: + case MessageResponseMenu: messageResponseMenu(); break; - case reply_menu: + case ReplyMenu: replyMenu(); break; - case delete_messages_menu: + case DeleteMessagesMenu: deleteMessagesMenu(); break; - case message_viewmode_menu: + case MessageViewModeMenu: messageViewModeMenu(); break; + case MessageBubblesMenu: + messageBubblesMenu(); + break; } - menuQueue = menu_none; + menuQueue = MenuNone; } void menuHandler::saveUIConfig() diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 1b964678b..4a0360412 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -8,53 +8,54 @@ class menuHandler { public: enum screenMenus { - menu_none, - lora_Menu, - lora_picker, - device_role_picker, - radio_preset_picker, - frequency_slot, - no_timeout_lora_picker, - TZ_picker, - twelve_hour_picker, - clock_face_picker, - clock_menu, - position_base_menu, - node_base_menu, - gps_toggle_menu, - gps_format_menu, - gps_smart_position_menu, - gps_update_interval_menu, - gps_position_broadcast_menu, - compass_point_north_menu, - reset_node_db_menu, - buzzermodemenupicker, - mui_picker, - tftcolormenupicker, - brightness_picker, - reboot_menu, - shutdown_menu, - NodePicker_menu, - Manage_Node_menu, - remove_favorite, - test_menu, - number_test, - wifi_toggle_menu, - bluetooth_toggle_menu, - screen_options_menu, - power_menu, - system_base_menu, - key_verification_init, - key_verification_final_prompt, - trace_route_menu, - throttle_message, - message_response_menu, - message_viewmode_menu, - reply_menu, - delete_messages_menu, - node_name_length_menu, + MenuNone, + LoraMenu, + LoraPicker, + DeviceRolePicker, + RadioPresetPicker, + FrequencySlot, + NoTimeoutLoraPicker, + TzPicker, + TwelveHourPicker, + ClockFacePicker, + ClockMenu, + PositionBaseMenu, + NodeBaseMenu, + GpsToggleMenu, + GpsFormatMenu, + GpsSmartPositionMenu, + GpsUpdateIntervalMenu, + GpsPositionBroadcastMenu, + CompassPointNorthMenu, + ResetNodeDbMenu, + BuzzerModeMenuPicker, + MuiPicker, + TftColorMenuPicker, + BrightnessPicker, + RebootMenu, + ShutdownMenu, + NodePickerMenu, + ManageNodeMenu, + RemoveFavorite, + TestMenu, + NumberTest, + WifiToggleMenu, + BluetoothToggleMenu, + ScreenOptionsMenu, + PowerMenu, + SystemBaseMenu, + KeyVerificationInit, + KeyVerificationFinalPrompt, + TraceRouteMenu, + ThrottleMessage, + MessageResponseMenu, + MessageViewModeMenu, + ReplyMenu, + DeleteMessagesMenu, + NodeNameLengthMenu, FrameToggles, - DisplayUnits + DisplayUnits, + MessageBubblesMenu }; static screenMenus menuQueue; static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu @@ -62,15 +63,15 @@ class menuHandler static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); static void loraMenu(); - static void DeviceRolePicker(); - static void RadioPresetPicker(); + static void deviceRolePicker(); + static void radioPresetPicker(); static void FrequencySlotPicker(); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm); static void clockMenu(); static void TZPicker(); - static void TwelveHourPicker(); - static void ClockFacePicker(); + static void twelveHourPicker(); + static void clockFacePicker(); static void messageResponseMenu(); static void messageViewModeMenu(); static void replyMenu(); @@ -95,7 +96,7 @@ class menuHandler static void rebootMenu(); static void shutdownMenu(); static void NodePicker(); - static void ManageNodeMenu(); + static void manageNodeMenu(); static void addFavoriteMenu(); static void removeFavoriteMenu(); static void traceRouteMenu(); @@ -106,15 +107,16 @@ class menuHandler static void screenOptionsMenu(); static void powerMenu(); static void nodeNameLengthMenu(); - static void FrameToggles_menu(); - static void DisplayUnits_menu(); + static void frameTogglesMenu(); + static void displayUnitsMenu(); + static void messageBubblesMenu(); static void textMessageMenu(); private: static void saveUIConfig(); static void keyVerificationInitMenu(); static void keyVerificationFinalPrompt(); - static void BluetoothToggleMenu(); + static void bluetoothToggleMenu(); }; /* Generic Menu Options designations */ diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 193164439..79d8b1ccd 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -527,8 +527,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 constexpr int BUBBLE_MIN_W = 24; constexpr int BUBBLE_TEXT_INDENT = 2; + // Check if bubbles are enabled + const bool showBubbles = config.display.enable_message_bubbles; + const int textIndent = showBubbles ? (BUBBLE_PAD_X + BUBBLE_TEXT_INDENT) : LEFT_MARGIN; + // Derived widths - const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (BUBBLE_PAD_X * 2); + const int leftTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - (showBubbles ? (BUBBLE_PAD_X * 2) : 0); const int rightTextWidth = SCREEN_WIDTH - LEFT_MARGIN - RIGHT_MARGIN - SCROLLBAR_WIDTH; // Title string depending on mode @@ -796,114 +800,105 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } } - // Draw bubbles - for (size_t bi = 0; bi < blocks.size(); ++bi) { - const auto &b = blocks[bi]; - if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end) - continue; + // Draw bubbles (only if enabled) + if (showBubbles) { + for (size_t bi = 0; bi < blocks.size(); ++bi) { + const auto &b = blocks[bi]; + if (b.start >= cachedLines.size() || b.end >= cachedLines.size() || b.start > b.end) + continue; - int visualTop = lineTop[b.start]; + int visualTop = lineTop[b.start]; - int topY; - if (isHeader[b.start]) { - // Header start - constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2 - topY = visualTop - BUBBLE_PAD_TOP_HEADER; - } else { - // Body start - bool thisLineHasEmote = false; - for (int e = 0; e < numEmotes; ++e) { - if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) { - thisLineHasEmote = true; - break; + int topY; + if (isHeader[b.start]) { + // Header start + constexpr int BUBBLE_PAD_TOP_HEADER = 1; // try 1 or 2 + topY = visualTop - BUBBLE_PAD_TOP_HEADER; + } else { + // Body start + bool thisLineHasEmote = false; + for (int e = 0; e < numEmotes; ++e) { + if (cachedLines[b.start].find(emotes[e].label) != std::string::npos) { + thisLineHasEmote = true; + break; + } } + if (thisLineHasEmote) { + constexpr int EMOTE_PADDING_ABOVE = 4; + visualTop -= EMOTE_PADDING_ABOVE; + } + topY = visualTop - BUBBLE_PAD_Y; } - if (thisLineHasEmote) { - constexpr int EMOTE_PADDING_ABOVE = 4; - visualTop -= EMOTE_PADDING_ABOVE; + int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]); + int bottomY = visualBottom + BUBBLE_PAD_Y; + + if (bi + 1 < blocks.size()) { + int nextHeaderIndex = (int)blocks[bi + 1].start; + int nextTop = lineTop[nextHeaderIndex]; + int maxBottom = nextTop - 1 - bubbleGapY; + if (bottomY > maxBottom) + bottomY = maxBottom; } - topY = visualTop - BUBBLE_PAD_Y; - } - int visualBottom = getDrawnLinePixelBottom(lineTop[b.end], cachedLines[b.end], isHeader[b.end]); - int bottomY = visualBottom + BUBBLE_PAD_Y; - if (bi + 1 < blocks.size()) { - int nextHeaderIndex = (int)blocks[bi + 1].start; - int nextTop = lineTop[nextHeaderIndex]; - int maxBottom = nextTop - 1 - bubbleGapY; - if (bottomY > maxBottom) - bottomY = maxBottom; - } + if (bottomY <= topY + 2) + continue; - if (bottomY <= topY + 2) - continue; + if (bottomY < contentTop || topY > contentBottom - 1) + continue; - if (bottomY < contentTop || topY > contentBottom - 1) - continue; + int maxLineW = 0; - int maxLineW = 0; - - for (size_t i = b.start; i <= b.end; ++i) { - int w = 0; - if (isHeader[i]) { - w = display->getStringWidth(cachedLines[i].c_str()); - if (b.mine) - w += 12; // room for ACK/NACK/relay mark - } else { - w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); + for (size_t i = b.start; i <= b.end; ++i) { + int w = 0; + if (isHeader[i]) { + w = display->getStringWidth(cachedLines[i].c_str()); + if (b.mine) + w += 12; // room for ACK/NACK/relay mark + } else { + w = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); + } + if (w > maxLineW) + maxLineW = w; } - if (w > maxLineW) - maxLineW = w; - } - - int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (BUBBLE_PAD_X * 2)); - int bubbleH = (bottomY - topY) + 1; - int bubbleX = 0; - if (b.mine) { - bubbleX = rightEdge - bubbleW; - } else { - bubbleX = x; - } - if (bubbleX < x) - bubbleX = x; - if (bubbleX + bubbleW > rightEdge) - bubbleW = std::max(1, rightEdge - bubbleX); - - if (bubbleW > 1 && bubbleH > 1) { - int x1 = bubbleX + bubbleW - 1; - int y1 = topY + bubbleH - 1; + int bubbleW = std::max(BUBBLE_MIN_W, maxLineW + (textIndent * 2)); + int bubbleH = (bottomY - topY) + 1; + int bubbleX = 0; if (b.mine) { - // Send Message (Right side) - display->drawRect(x1 + 2 - bubbleW, y1 - bubbleH, bubbleW, bubbleH); - // Top Right Corner - display->drawRect(x1, topY, 2, 1); - display->drawRect(x1, topY, 1, 2); - // Bottom Right Corner - display->drawRect(x1 - 1, bottomY - 2, 2, 1); - display->drawRect(x1, bottomY - 3, 1, 2); - // Knock the corners off to make a bubble - display->setColor(BLACK); - display->drawRect(x1 - bubbleW, topY - 1, 1, 1); - display->drawRect(x1 - bubbleW, bottomY - 1, 1, 1); - display->setColor(WHITE); + bubbleX = rightEdge - bubbleW; } else { - // Received Message (Left Side) - display->drawRect(bubbleX, topY, bubbleW + 1, bubbleH); - // Top Left Corner - display->drawRect(bubbleX + 1, topY + 1, 2, 1); - display->drawRect(bubbleX + 1, topY + 1, 1, 2); - // Bottom Left Corner - display->drawRect(bubbleX + 1, bottomY - 1, 2, 1); - display->drawRect(bubbleX + 1, bottomY - 2, 1, 2); - // Knock the corners off to make a bubble - display->setColor(BLACK); - display->drawRect(bubbleX + bubbleW, topY, 1, 1); - display->drawRect(bubbleX + bubbleW, bottomY, 1, 1); - display->setColor(WHITE); + bubbleX = x; + } + if (bubbleX < x) + bubbleX = x; + if (bubbleX + bubbleW > rightEdge) + bubbleW = std::max(1, rightEdge - bubbleX); + + // Draw rounded rectangle bubble + if (bubbleW > BUBBLE_RADIUS * 2 && bubbleH > BUBBLE_RADIUS * 2) { + const int r = BUBBLE_RADIUS; + const int bx = bubbleX; + const int by = topY; + const int bw = bubbleW; + const int bh = bubbleH; + + // Draw the 4 corner arcs using drawCircleQuads + display->drawCircleQuads(bx + r, by + r, r, 0x2); // Top-left + display->drawCircleQuads(bx + bw - r - 1, by + r, r, 0x1); // Top-right + display->drawCircleQuads(bx + r, by + bh - r - 1, r, 0x4); // Bottom-left + display->drawCircleQuads(bx + bw - r - 1, by + bh - r - 1, r, 0x8); // Bottom-right + + // Draw the 4 edges between corners + display->drawHorizontalLine(bx + r, by, bw - 2 * r); // Top edge + display->drawHorizontalLine(bx + r, by + bh - 1, bw - 2 * r); // Bottom edge + display->drawVerticalLine(bx, by + r, bh - 2 * r); // Left edge + display->drawVerticalLine(bx + bw - 1, by + r, bh - 2 * r); // Right edge + } else if (bubbleW > 1 && bubbleH > 1) { + // Fallback to simple rectangle for very small bubbles + display->drawRect(bubbleX, topY, bubbleW, bubbleH); } } - } + } // end if (showBubbles) // Render visible lines int lineY = yOffset; @@ -916,11 +911,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 int headerX; if (isMine[i]) { // push header left to avoid overlap with scrollbar - headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - BUBBLE_TEXT_INDENT; + headerX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - w - (showBubbles ? textIndent : 0); if (headerX < LEFT_MARGIN) headerX = LEFT_MARGIN; } else { - headerX = x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT; + headerX = x + textIndent; } display->drawString(headerX, lineY, cachedLines[i].c_str()); @@ -960,14 +955,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 if (isMine[i]) { // Calculate actual rendered width including emotes int renderedWidth = getRenderedLineWidth(display, cachedLines[i], emotes, numEmotes); - int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - BUBBLE_TEXT_INDENT; + int rightX = (SCREEN_WIDTH - SCROLLBAR_WIDTH - RIGHT_MARGIN) - renderedWidth - (showBubbles ? textIndent : 0); if (rightX < LEFT_MARGIN) rightX = LEFT_MARGIN; drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes); } else { - drawStringWithEmotes(display, x + BUBBLE_PAD_X + BUBBLE_TEXT_INDENT, lineY, cachedLines[i], emotes, - numEmotes); + drawStringWithEmotes(display, x + textIndent, lineY, cachedLines[i], emotes, numEmotes); } } } diff --git a/src/main.cpp b/src/main.cpp index d9773dfb9..b9ef2f9fe 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -929,6 +929,13 @@ void setup() service = new MeshService(); service->init(); + // Set osk_found for trackball/encoder devices BEFORE setupModules so CannedMessageModule can detect it +#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) +#ifndef HAS_PHYSICAL_KEYBOARD + osk_found = true; +#endif +#endif + // Now that the mesh service is created, create any modules setupModules(); @@ -1019,12 +1026,6 @@ void setup() #endif #endif -#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) -#ifndef HAS_PHYSICAL_KEYBOARD - osk_found = true; -#endif -#endif - #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER // Start web server thread. webServerThread = new WebServerThread(); diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 1d2fbf8fd..f76877e65 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -574,6 +574,10 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; #endif +#if defined(TFT_WIDTH) && defined(TFT_HEIGHT) && (TFT_WIDTH >= 200 || TFT_HEIGHT >= 200) + config.display.enable_message_bubbles = true; +#endif + #ifdef USERPREFS_CONFIG_DEVICE_ROLE // Restrict ROUTER*, LOST AND FOUND roles for security reasons if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER, diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp index 3b8225763..6d0255d53 100644 --- a/src/modules/KeyVerificationModule.cpp +++ b/src/modules/KeyVerificationModule.cpp @@ -123,7 +123,7 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode) // generate nonce updateState(); if (currentState != KEY_VERIFICATION_IDLE) { - IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::throttle_message;) + IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;) return false; } currentNonce = random(); @@ -259,7 +259,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) p->priority = meshtastic_MeshPacket_Priority_HIGH; service->sendToMesh(p, RX_SRC_LOCAL, true); currentState = KEY_VERIFICATION_SENDER_AWAITING_USER; - IF_SCREEN(screen->requestMenu(graphics::menuHandler::key_verification_final_prompt);) + IF_SCREEN(screen->requestMenu(graphics::menuHandler::KeyVerificationFinalPrompt);) meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message);