mirror of
https://github.com/meshtastic/firmware.git
synced 2026-02-03 07:31:58 +00:00
* Added keyboard option to menu. Shows a keyboard layout but does not type. * Keyboard types into text box and wraps. * send FreeText messages from the send submenu - renamed `KEYBOARD` action to `FREE_TEXT` and moved its menu location to the send submenu - opening the FreeText applet from the menu keeps the menu open and disabled the timeout - the FreeText applet writes to inkhud->freetext - the sending a canned message checks inkhud->freetext and if it isn't empty, sends and clears the inkhud->freetext * Text scrolls along with input * handle free text message completion as an event implements `handleFreeText` and `OnFreeText()` for system applets to interface with the FreeText Applet The FreeText Applet generates an `OnFreeText` event when completing a message which is handled by the first system applet with the `handleFreeText` flag set to true. The Menu Applet now handles this event. * call `onFreeText` whenever the FreeText Applet exits allows the menu to consistently restart its auto-close timeout * Add text cursor * Change UI to remove the header and make text box longer Keyboard displays captial letters for legibility Keyboard types captial letters with long press * center FreeText keys and draw symbolic buttons Move input field and keyboard drawing to their own functions: - `drawInputField()` - `drawKeyboard()` Store the keys in a 1-dimensional array Implement a matching array, `keyWidths`, to set key widths relative to the font size * Add character limit and counter * Fix softlock when hitting character limit * Move text box as its own menu page * rework FreeTextApplet into KeyboardApplet - The Keyboard Applet renders an on-screen keyboard at the lower portion of the screen. - Calling `inkhud->openKeyboard()` sends all the user applets to the background and resizes the first system applet with `handleFreeText` set to True to fit above the on-screen keyboard - `inkhud->closeKeyboard()` reverses this layout change * Fix input box rendering and add character limit to menu free text * remove FREE_TEXT menu page and use the FREE_TEXT menu action solely * force update when changing the free text message * reorganize KeyboardApplet - add comments after each row of `key[]` and `keyWidths[]` to preserve formatting - The selected key is now set using the key index directly - rowWidths are pre-calculated in the KeyboardApplet constructor - removed `drawKeyboard()` and implemented `drawKeyLabel()` * implement `Renderer::clearTile()` to clear the region below a tile * add parameter to forceUpdate() for re-rendering the full screen setting the `all` parameter to true in `inkhud->forceUpdate()` now causes the full screen buffer to clear an re-render. This is helpful for when sending applets to the background and the UI needs a clean canvas. System Applets can now set the `alwaysRender` flag true which causes it to re-render on every screen update. This is set to true in the Battery Icon Applet. * clean up tile clearing loops * implement dirty rendering to let applets draw over their previous render - `Applet::requestUpdate()` now has an optional flag to keep the old canvas - If honored, the renderer calls `render(true)` which runs `onDirtyRender()` instead of `onRender()` for said applet - The renderer will not call a dirty render if the full screen is getting re-rendered * simplify arithmetic in clearTile for better understanding * combine Applet::onRender() and Applet::onDirtyRender() into Applet::onRender(bool full) - add new `full` parameter to onRender() in every applet. This parameter can be ignored by most applets. - `Applet::requestUpdate()` has an optional flag that requests a full render by default * implement tile and partial rendering in KeyboardApplet * add comment for drawKeyLabel() * improve clarity of byte operations in clearTile() * remove typo and commented code * fix inaccurate comments * add null check to openKeyboard() and closeKeyboard() --------- Co-authored-by: zeropt <ferr0fluidmann@gmail.com> Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
272 lines
7.6 KiB
C++
272 lines
7.6 KiB
C++
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
|
|
|
#include "./NotificationApplet.h"
|
|
|
|
#include "./Notification.h"
|
|
#include "graphics/niche/InkHUD/Persistence.h"
|
|
|
|
#include "meshUtils.h"
|
|
#include "modules/TextMessageModule.h"
|
|
|
|
#include "RTC.h"
|
|
|
|
using namespace NicheGraphics;
|
|
|
|
InkHUD::NotificationApplet::NotificationApplet()
|
|
{
|
|
textMessageObserver.observe(textMessageModule);
|
|
}
|
|
|
|
// Collect meta-info about the text message, and ask for approval for the notification
|
|
// No need to save the message itself; we can use the cached InkHUD::latestMessage data during render()
|
|
int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
|
{
|
|
// System applets are always active
|
|
assert(isActive());
|
|
|
|
// Abort if feature disabled
|
|
// This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled
|
|
if (!settings->optionalFeatures.notifications)
|
|
return 0;
|
|
|
|
// Abort if this is an outgoing message
|
|
if (getFrom(p) == nodeDB->getNodeNum())
|
|
return 0;
|
|
|
|
Notification n;
|
|
n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
|
|
|
// Gather info: in-channel message
|
|
if (isBroadcast(p->to)) {
|
|
n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
|
n.channel = p->channel;
|
|
}
|
|
|
|
// Gather info: DM
|
|
else {
|
|
n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT;
|
|
n.sender = p->from;
|
|
}
|
|
|
|
// Close an old notification, if shown
|
|
dismiss();
|
|
|
|
// Check if we should display the notification
|
|
// A foreground applet might already be displaying this info
|
|
hasNotification = true;
|
|
currentNotification = n;
|
|
if (isApproved()) {
|
|
bringToForeground();
|
|
inkhud->forceUpdate();
|
|
} else
|
|
hasNotification = false; // Clear the pending notification: it was rejected
|
|
|
|
// Return zero: no issues here, carry on notifying other observers!
|
|
return 0;
|
|
}
|
|
|
|
void InkHUD::NotificationApplet::onRender(bool full)
|
|
{
|
|
// Clear the region beneath the tile
|
|
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
|
// We do need to do this with the battery though, as it is an "overlay"
|
|
fillRect(0, 0, width(), height(), WHITE);
|
|
|
|
// Padding (horizontal)
|
|
const uint16_t padW = 4;
|
|
|
|
// Main border
|
|
drawRect(0, 0, width(), height(), BLACK);
|
|
// drawRect(1, 1, width() - 2, height() - 2, BLACK);
|
|
|
|
// Timestamp (potentially)
|
|
// ====================
|
|
std::string ts = getTimeString(currentNotification.timestamp);
|
|
uint16_t tsW = 0;
|
|
int16_t divX = 0;
|
|
|
|
// Timestamp available
|
|
if (ts.length() > 0) {
|
|
tsW = getTextWidth(ts);
|
|
divX = padW + tsW + padW;
|
|
|
|
hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background
|
|
drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text
|
|
|
|
setCrop(1, 1, divX - 1, height() - 2);
|
|
|
|
// Drop shadow
|
|
setTextColor(WHITE);
|
|
printThick(padW + (tsW / 2), height() / 2, ts, 4, 4);
|
|
|
|
// Bold text
|
|
setTextColor(BLACK);
|
|
printThick(padW + (tsW / 2), height() / 2, ts, 2, 1);
|
|
}
|
|
|
|
// Main text
|
|
// =====================
|
|
|
|
// Background fill
|
|
// - medium dark (1/3)
|
|
hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK);
|
|
|
|
uint16_t availableWidth = width() - divX - padW;
|
|
std::string text = getNotificationText(availableWidth);
|
|
|
|
int16_t textM = divX + padW + (getTextWidth(text) / 2);
|
|
|
|
// Restrict area for printing
|
|
// - don't overlap border, or divider
|
|
setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2);
|
|
|
|
// Drop shadow
|
|
// - thick white text
|
|
setTextColor(WHITE);
|
|
printThick(textM, height() / 2, text, 4, 4);
|
|
|
|
// Main text
|
|
// - faux bold: double width
|
|
setTextColor(BLACK);
|
|
printThick(textM, height() / 2, text, 2, 1);
|
|
}
|
|
|
|
void InkHUD::NotificationApplet::onForeground()
|
|
{
|
|
handleInput = true; // Intercept the button input for our applet, so we can dismiss the notification
|
|
}
|
|
|
|
void InkHUD::NotificationApplet::onBackground()
|
|
{
|
|
handleInput = false;
|
|
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
|
}
|
|
|
|
void InkHUD::NotificationApplet::onButtonShortPress()
|
|
{
|
|
dismiss();
|
|
}
|
|
|
|
void InkHUD::NotificationApplet::onButtonLongPress()
|
|
{
|
|
dismiss();
|
|
}
|
|
|
|
void InkHUD::NotificationApplet::onExitShort()
|
|
{
|
|
dismiss();
|
|
}
|
|
|
|
void InkHUD::NotificationApplet::onExitLong()
|
|
{
|
|
dismiss();
|
|
}
|
|
|
|
void InkHUD::NotificationApplet::onNavUp()
|
|
{
|
|
dismiss();
|
|
}
|
|
|
|
void InkHUD::NotificationApplet::onNavDown()
|
|
{
|
|
dismiss();
|
|
}
|
|
|
|
void InkHUD::NotificationApplet::onNavLeft()
|
|
{
|
|
dismiss();
|
|
}
|
|
|
|
void InkHUD::NotificationApplet::onNavRight()
|
|
{
|
|
dismiss();
|
|
}
|
|
|
|
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
|
|
// Called internally when we first get a "notifiable event", and then again before render,
|
|
// in case autoshow swapped which applet was displayed
|
|
bool InkHUD::NotificationApplet::isApproved()
|
|
{
|
|
// Instead of an assert
|
|
if (!hasNotification) {
|
|
LOG_WARN("No notif to approve");
|
|
return false;
|
|
}
|
|
|
|
// Ask all visible user applets for approval
|
|
for (Applet *ua : inkhud->userApplets) {
|
|
if (ua->isForeground() && !ua->approveNotification(currentNotification))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Mark that the notification should no-longer be rendered
|
|
// In addition to calling thing method, code needs to request a re-render of all applets
|
|
void InkHUD::NotificationApplet::dismiss()
|
|
{
|
|
sendToBackground();
|
|
hasNotification = false;
|
|
// Not requesting update directly from this method,
|
|
// as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever drawn
|
|
}
|
|
|
|
// Get a string for the main body text of a notification
|
|
// Formatted to suit screen width
|
|
// Takes info from InkHUD::currentNotification
|
|
std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable)
|
|
{
|
|
assert(hasNotification);
|
|
|
|
std::string text;
|
|
|
|
// Text message
|
|
// ==============
|
|
|
|
if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT,
|
|
Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) {
|
|
|
|
// Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently
|
|
bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
|
|
|
// Pick source of message
|
|
MessageStore::Message *message =
|
|
isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
|
|
|
|
// Find info about the sender
|
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
|
|
|
|
// Leading tag (channel vs. DM)
|
|
text += isBroadcast ? "From:" : "DM: ";
|
|
|
|
// Sender id
|
|
if (node && node->has_user)
|
|
text += parseShortName(node);
|
|
else
|
|
text += hexifyNodeNum(message->sender);
|
|
|
|
// Check if text fits
|
|
// - use a longer string, if we have the space
|
|
if (getTextWidth(text) < widthAvailable * 0.5) {
|
|
text.clear();
|
|
|
|
// Leading tag (channel vs. DM)
|
|
text += isBroadcast ? "Msg from " : "DM from ";
|
|
|
|
// Sender id
|
|
if (node && node->has_user)
|
|
text += parseShortName(node);
|
|
else
|
|
text += hexifyNodeNum(message->sender);
|
|
|
|
text += ": ";
|
|
text += message->text;
|
|
}
|
|
}
|
|
|
|
// Parse any non-ascii characters and return
|
|
return parse(text);
|
|
}
|
|
|
|
#endif |