mirror of
https://github.com/meshtastic/firmware.git
synced 2026-02-15 10:01:29 -05:00
Add on-screen keyboard to InkHUD (#9445)
* 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>
This commit is contained in:
@@ -55,7 +55,7 @@ InkHUD::Tile *InkHUD::Applet::getTile()
|
||||
}
|
||||
|
||||
// Draw the applet
|
||||
void InkHUD::Applet::render()
|
||||
void InkHUD::Applet::render(bool full)
|
||||
{
|
||||
assert(assignedTile); // Ensure that we have a tile
|
||||
assert(assignedTile->getAssignedApplet() == this); // Ensure that we have a reciprocal link with the tile
|
||||
@@ -65,10 +65,11 @@ void InkHUD::Applet::render()
|
||||
wantRender = false; // Flag set by requestUpdate
|
||||
wantAutoshow = false; // Flag set by requestAutoShow. May or may not have been honored.
|
||||
wantUpdateType = Drivers::EInk::UpdateTypes::UNSPECIFIED; // Update type we wanted. May on may not have been granted.
|
||||
wantFullRender = true; // Default to a full render
|
||||
|
||||
updateDimensions();
|
||||
resetDrawingSpace();
|
||||
onRender(); // Derived applet's drawing takes place here
|
||||
onRender(full); // Draw the applet
|
||||
|
||||
// Handle "Tile Highlighting"
|
||||
// Some devices may use an auxiliary button to switch between tiles
|
||||
@@ -115,6 +116,11 @@ Drivers::EInk::UpdateTypes InkHUD::Applet::wantsUpdateType()
|
||||
return wantUpdateType;
|
||||
}
|
||||
|
||||
bool InkHUD::Applet::wantsFullRender()
|
||||
{
|
||||
return wantFullRender;
|
||||
}
|
||||
|
||||
// Get size of the applet's drawing space from its tile
|
||||
// Performed immediately before derived applet's drawing code runs
|
||||
void InkHUD::Applet::updateDimensions()
|
||||
@@ -142,10 +148,11 @@ void InkHUD::Applet::resetDrawingSpace()
|
||||
// Once the renderer has given other applets a chance to process whatever event we just detected,
|
||||
// it will run Applet::render(), which may draw our applet to screen, if it is shown (foreground)
|
||||
// We should requestUpdate even if our applet is currently background, because this might be changed by autoshow
|
||||
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type)
|
||||
void InkHUD::Applet::requestUpdate(Drivers::EInk::UpdateTypes type, bool full)
|
||||
{
|
||||
wantRender = true;
|
||||
wantUpdateType = type;
|
||||
wantFullRender = full;
|
||||
inkhud->requestUpdate();
|
||||
}
|
||||
|
||||
|
||||
@@ -64,10 +64,11 @@ class Applet : public GFX
|
||||
|
||||
// Rendering
|
||||
|
||||
void render(); // Draw the applet
|
||||
void render(bool full); // Draw the applet
|
||||
bool wantsToRender(); // Check whether applet wants to render
|
||||
bool wantsToAutoshow(); // Check whether applet wants to become foreground
|
||||
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
|
||||
bool wantsFullRender(); // Check whether applet wants to render over its previous render
|
||||
void updateDimensions(); // Get current size from tile
|
||||
void resetDrawingSpace(); // Makes sure every render starts with same parameters
|
||||
|
||||
@@ -82,7 +83,7 @@ class Applet : public GFX
|
||||
|
||||
// Event handlers
|
||||
|
||||
virtual void onRender() = 0; // All drawing happens here
|
||||
virtual void onRender(bool full) = 0; // For drawing the applet
|
||||
virtual void onActivate() {}
|
||||
virtual void onDeactivate() {}
|
||||
virtual void onForeground() {}
|
||||
@@ -96,6 +97,9 @@ class Applet : public GFX
|
||||
virtual void onNavDown() {}
|
||||
virtual void onNavLeft() {}
|
||||
virtual void onNavRight() {}
|
||||
virtual void onFreeText(char c) {}
|
||||
virtual void onFreeTextDone() {}
|
||||
virtual void onFreeTextCancel() {}
|
||||
|
||||
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
|
||||
|
||||
@@ -108,8 +112,9 @@ class Applet : public GFX
|
||||
protected:
|
||||
void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here
|
||||
|
||||
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update
|
||||
void requestAutoshow(); // Ask for applet to be moved to foreground
|
||||
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED,
|
||||
bool full = true); // Ask WindowManager to schedule a display update
|
||||
void requestAutoshow(); // Ask for applet to be moved to foreground
|
||||
|
||||
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
|
||||
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
|
||||
@@ -164,6 +169,7 @@ class Applet : public GFX
|
||||
bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground?
|
||||
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
|
||||
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display
|
||||
bool wantFullRender = true; // Render with a fresh canvas
|
||||
|
||||
using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly
|
||||
using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::MapApplet::onRender()
|
||||
void InkHUD::MapApplet::onRender(bool full)
|
||||
{
|
||||
// Abort if no markers to render
|
||||
if (!enoughMarkers()) {
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace NicheGraphics::InkHUD
|
||||
class MapApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
protected:
|
||||
virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes
|
||||
|
||||
@@ -103,7 +103,7 @@ uint8_t InkHUD::NodeListApplet::maxCards()
|
||||
}
|
||||
|
||||
// Draw, using info which derived applet placed into NodeListApplet::cards for us
|
||||
void InkHUD::NodeListApplet::onRender()
|
||||
void InkHUD::NodeListApplet::onRender(bool full)
|
||||
{
|
||||
|
||||
// ================================
|
||||
|
||||
@@ -46,7 +46,7 @@ class NodeListApplet : public Applet, public MeshModule
|
||||
public:
|
||||
NodeListApplet(const char *name);
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
@@ -6,7 +6,7 @@ using namespace NicheGraphics;
|
||||
|
||||
// All drawing happens here
|
||||
// Our basic example doesn't do anything useful. It just passively prints some text.
|
||||
void InkHUD::BasicExampleApplet::onRender()
|
||||
void InkHUD::BasicExampleApplet::onRender(bool full)
|
||||
{
|
||||
printAt(0, 0, "Hello, World!");
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class BasicExampleApplet : public Applet
|
||||
// You must have an onRender() method
|
||||
// All drawing happens here
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -35,7 +35,7 @@ ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_Mesh
|
||||
// We can trigger a render by calling requestUpdate()
|
||||
// Render might be called by some external source
|
||||
// We should always be ready to draw
|
||||
void InkHUD::NewMsgExampleApplet::onRender()
|
||||
void InkHUD::NewMsgExampleApplet::onRender(bool full)
|
||||
{
|
||||
printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class NewMsgExampleApplet : public Applet, public SinglePortModule
|
||||
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
|
||||
|
||||
// All drawing happens here
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
// Your applet might also want to use some of these
|
||||
// Useful for setting up or tidying up
|
||||
|
||||
@@ -10,7 +10,7 @@ InkHUD::AlignStickApplet::AlignStickApplet()
|
||||
bringToForeground();
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onRender()
|
||||
void InkHUD::AlignStickApplet::onRender(bool full)
|
||||
{
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Align Joystick:");
|
||||
@@ -152,19 +152,17 @@ void InkHUD::AlignStickApplet::onBackground()
|
||||
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onButtonLongPress()
|
||||
{
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onExitLong()
|
||||
{
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onNavUp()
|
||||
@@ -172,7 +170,6 @@ void InkHUD::AlignStickApplet::onNavUp()
|
||||
settings->joystick.aligned = true;
|
||||
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onNavDown()
|
||||
@@ -181,7 +178,6 @@ void InkHUD::AlignStickApplet::onNavDown()
|
||||
settings->joystick.aligned = true;
|
||||
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onNavLeft()
|
||||
@@ -190,7 +186,6 @@ void InkHUD::AlignStickApplet::onNavLeft()
|
||||
settings->joystick.aligned = true;
|
||||
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onNavRight()
|
||||
@@ -199,7 +194,6 @@ void InkHUD::AlignStickApplet::onNavRight()
|
||||
settings->joystick.aligned = true;
|
||||
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -23,7 +23,7 @@ class AlignStickApplet : public SystemApplet
|
||||
public:
|
||||
AlignStickApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonLongPress() override;
|
||||
|
||||
@@ -6,6 +6,8 @@ using namespace NicheGraphics;
|
||||
|
||||
InkHUD::BatteryIconApplet::BatteryIconApplet()
|
||||
{
|
||||
alwaysRender = true; // render everytime the screen is updated
|
||||
|
||||
// Show at boot, if user has previously enabled the feature
|
||||
if (settings->optionalFeatures.batteryIcon)
|
||||
bringToForeground();
|
||||
@@ -44,7 +46,7 @@ int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *sta
|
||||
return 0; // Tell Observable to continue informing other observers
|
||||
}
|
||||
|
||||
void InkHUD::BatteryIconApplet::onRender()
|
||||
void InkHUD::BatteryIconApplet::onRender(bool full)
|
||||
{
|
||||
// Fill entire tile
|
||||
// - size of icon controlled by size of tile
|
||||
|
||||
@@ -23,7 +23,7 @@ class BatteryIconApplet : public SystemApplet
|
||||
public:
|
||||
BatteryIconApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
|
||||
|
||||
private:
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
#include "./KeyboardApplet.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::KeyboardApplet::KeyboardApplet()
|
||||
{
|
||||
// Calculate row widths
|
||||
for (uint8_t row = 0; row < KBD_ROWS; row++) {
|
||||
rowWidths[row] = 0;
|
||||
for (uint8_t col = 0; col < KBD_COLS; col++)
|
||||
rowWidths[row] += keyWidths[row * KBD_COLS + col];
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onRender(bool full)
|
||||
{
|
||||
uint16_t em = fontSmall.lineHeight(); // 16 pt
|
||||
uint16_t keyH = Y(1.0) / KBD_ROWS;
|
||||
int16_t keyTopPadding = (keyH - fontSmall.lineHeight()) / 2;
|
||||
|
||||
if (full) { // Draw full keyboard
|
||||
for (uint8_t row = 0; row < KBD_ROWS; row++) {
|
||||
|
||||
// Calculate the remaining space to be used as padding
|
||||
int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
|
||||
|
||||
// Draw keys
|
||||
uint16_t xPos = 0;
|
||||
for (uint8_t col = 0; col < KBD_COLS; col++) {
|
||||
Color fgcolor = BLACK;
|
||||
uint8_t index = row * KBD_COLS + col;
|
||||
uint16_t keyX = ((xPos * em) >> 4) + ((col * keyXPadding) / (KBD_COLS - 1));
|
||||
uint16_t keyY = row * keyH;
|
||||
uint16_t keyW = (keyWidths[index] * em) >> 4;
|
||||
if (index == selectedKey) {
|
||||
fgcolor = WHITE;
|
||||
fillRect(keyX, keyY, keyW, keyH, BLACK);
|
||||
}
|
||||
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[index], fgcolor);
|
||||
xPos += keyWidths[index];
|
||||
}
|
||||
}
|
||||
} else { // Only draw the difference
|
||||
if (selectedKey != prevSelectedKey) {
|
||||
// Draw previously selected key
|
||||
uint8_t row = prevSelectedKey / KBD_COLS;
|
||||
int16_t keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
|
||||
uint16_t xPos = 0;
|
||||
for (uint8_t i = prevSelectedKey - (prevSelectedKey % KBD_COLS); i < prevSelectedKey; i++)
|
||||
xPos += keyWidths[i];
|
||||
uint16_t keyX = ((xPos * em) >> 4) + (((prevSelectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1));
|
||||
uint16_t keyY = row * keyH;
|
||||
uint16_t keyW = (keyWidths[prevSelectedKey] * em) >> 4;
|
||||
fillRect(keyX, keyY, keyW, keyH, WHITE);
|
||||
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[prevSelectedKey], BLACK);
|
||||
|
||||
// Draw newly selected key
|
||||
row = selectedKey / KBD_COLS;
|
||||
keyXPadding = X(1.0) - ((rowWidths[row] * em) >> 4);
|
||||
xPos = 0;
|
||||
for (uint8_t i = selectedKey - (selectedKey % KBD_COLS); i < selectedKey; i++)
|
||||
xPos += keyWidths[i];
|
||||
keyX = ((xPos * em) >> 4) + (((selectedKey % KBD_COLS) * keyXPadding) / (KBD_COLS - 1));
|
||||
keyY = row * keyH;
|
||||
keyW = (keyWidths[selectedKey] * em) >> 4;
|
||||
fillRect(keyX, keyY, keyW, keyH, BLACK);
|
||||
drawKeyLabel(keyX, keyY + keyTopPadding, keyW, keys[selectedKey], WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
prevSelectedKey = selectedKey;
|
||||
}
|
||||
|
||||
// Draw the key label corresponding to the char
|
||||
// for most keys it draws the character itself
|
||||
// for ['\b', '\n', ' ', '\x1b'] it draws special glyphs
|
||||
void InkHUD::KeyboardApplet::drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color)
|
||||
{
|
||||
if (key == '\b') {
|
||||
// Draw backspace glyph: 13 x 9 px
|
||||
/**
|
||||
* [][][][][][][][][]
|
||||
* [][] []
|
||||
* [][] [] [] []
|
||||
* [][] [] [] []
|
||||
* [][] [] []
|
||||
* [][] [] [] []
|
||||
* [][] [] [] []
|
||||
* [][] []
|
||||
* [][][][][][][][][]
|
||||
*/
|
||||
const uint8_t bsBitmap[] = {0x0f, 0xf8, 0x18, 0x08, 0x32, 0x28, 0x61, 0x48, 0xc0,
|
||||
0x88, 0x61, 0x48, 0x32, 0x28, 0x18, 0x08, 0x0f, 0xf8};
|
||||
uint16_t leftPadding = (width - 13) >> 1;
|
||||
drawBitmap(left + leftPadding, top + 1, bsBitmap, 13, 9, color);
|
||||
} else if (key == '\n') {
|
||||
// Draw done glyph: 12 x 9 px
|
||||
/**
|
||||
* [][]
|
||||
* [][]
|
||||
* [][]
|
||||
* [][]
|
||||
* [][]
|
||||
* [][] [][]
|
||||
* [][] [][]
|
||||
* [][][]
|
||||
* []
|
||||
*/
|
||||
const uint8_t doneBitmap[] = {0x00, 0x30, 0x00, 0x60, 0x00, 0xc0, 0x01, 0x80, 0x03,
|
||||
0x00, 0xc6, 0x00, 0x6c, 0x00, 0x38, 0x00, 0x10, 0x00};
|
||||
uint16_t leftPadding = (width - 12) >> 1;
|
||||
drawBitmap(left + leftPadding, top + 1, doneBitmap, 12, 9, color);
|
||||
} else if (key == ' ') {
|
||||
// Draw space glyph: 13 x 9 px
|
||||
/**
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* [] []
|
||||
* [] []
|
||||
* [][][][][][][][][][][][][]
|
||||
*
|
||||
*
|
||||
*/
|
||||
const uint8_t spaceBitmap[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
|
||||
0x08, 0x80, 0x08, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00};
|
||||
uint16_t leftPadding = (width - 13) >> 1;
|
||||
drawBitmap(left + leftPadding, top + 1, spaceBitmap, 13, 9, color);
|
||||
} else if (key == '\x1b') {
|
||||
setTextColor(color);
|
||||
std::string keyText = "ESC";
|
||||
uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1;
|
||||
printAt(left + leftPadding, top, keyText);
|
||||
} else {
|
||||
setTextColor(color);
|
||||
if (key >= 0x61)
|
||||
key -= 32; // capitalize
|
||||
std::string keyText = std::string(1, key);
|
||||
uint16_t leftPadding = (width - getTextWidth(keyText)) >> 1;
|
||||
printAt(left + leftPadding, top, keyText);
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onForeground()
|
||||
{
|
||||
handleInput = true; // Intercept the button input for our applet
|
||||
|
||||
// Select the first key
|
||||
selectedKey = 0;
|
||||
prevSelectedKey = 0;
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onBackground()
|
||||
{
|
||||
handleInput = false;
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onButtonShortPress()
|
||||
{
|
||||
char key = keys[selectedKey];
|
||||
if (key == '\n') {
|
||||
inkhud->freeTextDone();
|
||||
inkhud->closeKeyboard();
|
||||
} else if (key == '\x1b') {
|
||||
inkhud->freeTextCancel();
|
||||
inkhud->closeKeyboard();
|
||||
} else {
|
||||
inkhud->freeText(key);
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onButtonLongPress()
|
||||
{
|
||||
char key = keys[selectedKey];
|
||||
if (key == '\n') {
|
||||
inkhud->freeTextDone();
|
||||
inkhud->closeKeyboard();
|
||||
} else if (key == '\x1b') {
|
||||
inkhud->freeTextCancel();
|
||||
inkhud->closeKeyboard();
|
||||
} else {
|
||||
if (key >= 0x61)
|
||||
key -= 32; // capitalize
|
||||
inkhud->freeText(key);
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onExitShort()
|
||||
{
|
||||
inkhud->freeTextCancel();
|
||||
inkhud->closeKeyboard();
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onExitLong()
|
||||
{
|
||||
inkhud->freeTextCancel();
|
||||
inkhud->closeKeyboard();
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onNavUp()
|
||||
{
|
||||
if (selectedKey < KBD_COLS) // wrap
|
||||
selectedKey += KBD_COLS * (KBD_ROWS - 1);
|
||||
else // move 1 row back
|
||||
selectedKey -= KBD_COLS;
|
||||
|
||||
// Request rendering over the previously drawn render
|
||||
requestUpdate(EInk::UpdateTypes::FAST, false);
|
||||
// Force an update to bypass lockRequests
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onNavDown()
|
||||
{
|
||||
selectedKey += KBD_COLS;
|
||||
selectedKey %= (KBD_COLS * KBD_ROWS);
|
||||
|
||||
// Request rendering over the previously drawn render
|
||||
requestUpdate(EInk::UpdateTypes::FAST, false);
|
||||
// Force an update to bypass lockRequests
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onNavLeft()
|
||||
{
|
||||
if (selectedKey % KBD_COLS == 0) // wrap
|
||||
selectedKey += KBD_COLS - 1;
|
||||
else // move 1 column back
|
||||
selectedKey--;
|
||||
|
||||
// Request rendering over the previously drawn render
|
||||
requestUpdate(EInk::UpdateTypes::FAST, false);
|
||||
// Force an update to bypass lockRequests
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::KeyboardApplet::onNavRight()
|
||||
{
|
||||
if (selectedKey % KBD_COLS == KBD_COLS - 1) // wrap
|
||||
selectedKey -= KBD_COLS - 1;
|
||||
else // move 1 column forward
|
||||
selectedKey++;
|
||||
|
||||
// Request rendering over the previously drawn render
|
||||
requestUpdate(EInk::UpdateTypes::FAST, false);
|
||||
// Force an update to bypass lockRequests
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
uint16_t InkHUD::KeyboardApplet::getKeyboardHeight()
|
||||
{
|
||||
const uint16_t keyH = fontSmall.lineHeight() * 1.2;
|
||||
return keyH * KBD_ROWS;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,66 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
/*
|
||||
|
||||
System Applet to render an on-screeen keyboard
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
#include "graphics/niche/InkHUD/InkHUD.h"
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
#include <string>
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
||||
class KeyboardApplet : public SystemApplet
|
||||
{
|
||||
public:
|
||||
KeyboardApplet();
|
||||
|
||||
void onRender(bool full) override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
void onButtonLongPress() override;
|
||||
void onExitShort() override;
|
||||
void onExitLong() override;
|
||||
void onNavUp() override;
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
|
||||
static uint16_t getKeyboardHeight(); // used to set the keyboard tile height
|
||||
|
||||
private:
|
||||
void drawKeyLabel(uint16_t left, uint16_t top, uint16_t width, char key, Color color);
|
||||
|
||||
static const uint8_t KBD_COLS = 11;
|
||||
static const uint8_t KBD_ROWS = 4;
|
||||
|
||||
const char keys[KBD_COLS * KBD_ROWS] = {
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b', // row 0
|
||||
'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n', // row 1
|
||||
'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '!', ' ', // row 2
|
||||
'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '?', '\x1b' // row 3
|
||||
};
|
||||
|
||||
// This array represents the widths of each key in points
|
||||
// 16 pt = line height of the text
|
||||
const uint16_t keyWidths[KBD_COLS * KBD_ROWS] = {
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 0
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 1
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 24, // row 2
|
||||
16, 16, 16, 16, 16, 16, 16, 10, 10, 12, 40 // row 3
|
||||
};
|
||||
|
||||
uint16_t rowWidths[KBD_ROWS];
|
||||
uint8_t selectedKey = 0; // selected key index
|
||||
uint8_t prevSelectedKey = 0;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
@@ -30,7 +30,7 @@ InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
|
||||
// This is then drawn with a FULL refresh by Renderer::begin
|
||||
}
|
||||
|
||||
void InkHUD::LogoApplet::onRender()
|
||||
void InkHUD::LogoApplet::onRender(bool full)
|
||||
{
|
||||
// Size of the region which the logo should "scale to fit"
|
||||
uint16_t logoWLimit = X(0.8);
|
||||
@@ -120,7 +120,7 @@ void InkHUD::LogoApplet::onBackground()
|
||||
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||
}
|
||||
|
||||
// Begin displaying the screen which is shown at shutdown
|
||||
@@ -138,10 +138,10 @@ void InkHUD::LogoApplet::onShutdown()
|
||||
// Intention is to restore display health.
|
||||
|
||||
inverted = true;
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
|
||||
delay(1000); // Cooldown. Back to back updates aren't great for health.
|
||||
inverted = false;
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
|
||||
delay(1000); // Cooldown
|
||||
|
||||
// Prepare for the powered-off screen now
|
||||
@@ -176,7 +176,7 @@ void InkHUD::LogoApplet::onReboot()
|
||||
textTitle = "Rebooting...";
|
||||
fontTitle = fontSmall;
|
||||
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, true, false);
|
||||
// Perform the update right now, waiting here until complete
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class LogoApplet : public SystemApplet, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
LogoApplet();
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onShutdown() override;
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace NicheGraphics::InkHUD
|
||||
enum MenuAction {
|
||||
NO_ACTION,
|
||||
SEND_PING,
|
||||
FREE_TEXT,
|
||||
STORE_CANNEDMESSAGE_SELECTION,
|
||||
SEND_CANNEDMESSAGE,
|
||||
SHUTDOWN,
|
||||
|
||||
@@ -90,6 +90,8 @@ void InkHUD::MenuApplet::onForeground()
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
OSThread::enabled = true;
|
||||
|
||||
freeTextMode = false;
|
||||
|
||||
// Upgrade the refresh to FAST, for guaranteed responsiveness
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
}
|
||||
@@ -116,6 +118,8 @@ void InkHUD::MenuApplet::onBackground()
|
||||
SystemApplet::lockRequests = false;
|
||||
SystemApplet::handleInput = false;
|
||||
|
||||
handleFreeText = false;
|
||||
|
||||
// Restore the user applet whose tile we borrowed
|
||||
if (borrowedTileOwner)
|
||||
borrowedTileOwner->bringToForeground();
|
||||
@@ -340,12 +344,26 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
|
||||
break;
|
||||
|
||||
case FREE_TEXT:
|
||||
OSThread::enabled = false;
|
||||
handleFreeText = true;
|
||||
cm.freeTextItem.rawText.erase(); // clear the previous freetext message
|
||||
freeTextMode = true; // render input field instead of normal menu
|
||||
// Open the on-screen keyboard if the joystick is enabled
|
||||
if (settings->joystick.enabled)
|
||||
inkhud->openKeyboard();
|
||||
break;
|
||||
|
||||
case STORE_CANNEDMESSAGE_SELECTION:
|
||||
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
|
||||
if (!settings->joystick.enabled)
|
||||
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
|
||||
else
|
||||
cm.selectedMessageItem = &cm.messageItems.at(cursor - 2); // Minus two: offset for the "Send Ping" and free text entry
|
||||
break;
|
||||
|
||||
case SEND_CANNEDMESSAGE:
|
||||
cm.selectedRecipientItem = &cm.recipientItems.at(cursor);
|
||||
// send selected message
|
||||
sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str());
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here
|
||||
break;
|
||||
@@ -1373,8 +1391,14 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
currentPage = page;
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onRender()
|
||||
void InkHUD::MenuApplet::onRender(bool full)
|
||||
{
|
||||
// Free text mode draws a text input field and skips the normal rendering
|
||||
if (freeTextMode) {
|
||||
drawInputField(0, fontSmall.lineHeight(), X(1.0), Y(1.0) - fontSmall.lineHeight() - 1, cm.freeTextItem.rawText);
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.size() == 0)
|
||||
LOG_ERROR("Empty Menu");
|
||||
|
||||
@@ -1493,44 +1517,48 @@ void InkHUD::MenuApplet::onRender()
|
||||
|
||||
void InkHUD::MenuApplet::onButtonShortPress()
|
||||
{
|
||||
// Push the auto-close timer back
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (!freeTextMode) {
|
||||
// Push the auto-close timer back
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
if (!settings->joystick.enabled) {
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
} while (items.at(cursor).isHeader);
|
||||
}
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
} else {
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
else
|
||||
showPage(MenuPage::EXIT);
|
||||
if (!wantsToRender())
|
||||
if (!settings->joystick.enabled) {
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
} while (items.at(cursor).isHeader);
|
||||
}
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
} else {
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
else
|
||||
showPage(MenuPage::EXIT);
|
||||
if (!wantsToRender())
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onButtonLongPress()
|
||||
{
|
||||
// Push the auto-close timer back
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (!freeTextMode) {
|
||||
// Push the auto-close timer back
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
else
|
||||
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
else
|
||||
showPage(MenuPage::EXIT); // Special case: Peek at root-menu; longpress again to close
|
||||
|
||||
// If we didn't already request a specialized update, when handling a menu action,
|
||||
// then perform the usual fast update.
|
||||
// FAST keeps things responsive: important because we're dealing with user input
|
||||
if (!wantsToRender())
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
// If we didn't already request a specialized update, when handling a menu action,
|
||||
// then perform the usual fast update.
|
||||
// FAST keeps things responsive: important because we're dealing with user input
|
||||
if (!wantsToRender())
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onExitShort()
|
||||
@@ -1543,56 +1571,107 @@ void InkHUD::MenuApplet::onExitShort()
|
||||
|
||||
void InkHUD::MenuApplet::onNavUp()
|
||||
{
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (!freeTextMode) {
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
if (cursor == 0)
|
||||
cursor = items.size() - 1;
|
||||
else
|
||||
cursor--;
|
||||
} while (items.at(cursor).isHeader);
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
if (cursor == 0)
|
||||
cursor = items.size() - 1;
|
||||
else
|
||||
cursor--;
|
||||
} while (items.at(cursor).isHeader);
|
||||
}
|
||||
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onNavDown()
|
||||
{
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (!freeTextMode) {
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
} while (items.at(cursor).isHeader);
|
||||
if (!cursorShown) {
|
||||
cursorShown = true;
|
||||
cursor = 0;
|
||||
} else {
|
||||
do {
|
||||
cursor = (cursor + 1) % items.size();
|
||||
} while (items.at(cursor).isHeader);
|
||||
}
|
||||
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onNavLeft()
|
||||
{
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (!freeTextMode) {
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
|
||||
// Go to the previous menu page
|
||||
showPage(previousPage);
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
// Go to the previous menu page
|
||||
showPage(previousPage);
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onNavRight()
|
||||
{
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (!freeTextMode) {
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
if (!wantsToRender())
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
}
|
||||
|
||||
if (cursorShown)
|
||||
execute(items.at(cursor));
|
||||
if (!wantsToRender())
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
void InkHUD::MenuApplet::onFreeText(char c)
|
||||
{
|
||||
if (cm.freeTextItem.rawText.length() >= menuTextLimit && c != '\b')
|
||||
return;
|
||||
if (c == '\b') {
|
||||
if (!cm.freeTextItem.rawText.empty())
|
||||
cm.freeTextItem.rawText.pop_back();
|
||||
} else {
|
||||
cm.freeTextItem.rawText += c;
|
||||
}
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onFreeTextDone()
|
||||
{
|
||||
// Restart the auto-close timeout
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
OSThread::enabled = true;
|
||||
|
||||
handleFreeText = false;
|
||||
freeTextMode = false;
|
||||
|
||||
if (!cm.freeTextItem.rawText.empty()) {
|
||||
cm.selectedMessageItem = &cm.freeTextItem;
|
||||
showPage(MenuPage::CANNEDMESSAGE_RECIPIENT);
|
||||
}
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::onFreeTextCancel()
|
||||
{
|
||||
// Restart the auto-close timeout
|
||||
OSThread::setIntervalFromNow(MENU_TIMEOUT_SEC * 1000UL);
|
||||
OSThread::enabled = true;
|
||||
|
||||
handleFreeText = false;
|
||||
freeTextMode = false;
|
||||
|
||||
// Clear the free text message
|
||||
cm.freeTextItem.rawText.erase();
|
||||
|
||||
requestUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
// Dynamically create MenuItem entries for activating / deactivating Applets, for the "Applet Selection" submenu
|
||||
@@ -1647,6 +1726,10 @@ void InkHUD::MenuApplet::populateSendPage()
|
||||
// Position / NodeInfo packet
|
||||
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
|
||||
|
||||
// If joystick is available, include the Free Text option
|
||||
if (settings->joystick.enabled)
|
||||
items.push_back(MenuItem("Free Text", MenuAction::FREE_TEXT, MenuPage::SEND));
|
||||
|
||||
// One menu item for each canned message
|
||||
uint8_t count = cm.store->size();
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
@@ -1746,6 +1829,48 @@ void InkHUD::MenuApplet::populateRecipientPage()
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
}
|
||||
|
||||
void InkHUD::MenuApplet::drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height, std::string text)
|
||||
{
|
||||
setFont(fontSmall);
|
||||
uint16_t wrapMaxH = 0;
|
||||
|
||||
// Draw the text, input box, and cursor
|
||||
// Adjusting the box for screen height
|
||||
while (wrapMaxH < height - fontSmall.lineHeight()) {
|
||||
wrapMaxH += fontSmall.lineHeight();
|
||||
}
|
||||
|
||||
// If the text is so long that it goes outside of the input box, the text is actually rendered off screen.
|
||||
uint32_t textHeight = getWrappedTextHeight(0, width - 5, text);
|
||||
if (!text.empty()) {
|
||||
uint16_t textPadding = X(1.0) > Y(1.0) ? wrapMaxH - textHeight : wrapMaxH - textHeight + 1;
|
||||
if (textHeight > wrapMaxH)
|
||||
printWrapped(2, textPadding, width - 5, text);
|
||||
else
|
||||
printWrapped(2, top + 2, width - 5, text);
|
||||
}
|
||||
|
||||
uint16_t textCursorX = text.empty() ? 1 : getCursorX();
|
||||
uint16_t textCursorY = text.empty() ? fontSmall.lineHeight() + 2 : getCursorY() - fontSmall.lineHeight() + 3;
|
||||
|
||||
if (textCursorX + 1 > width - 5) {
|
||||
textCursorX = getCursorX() - width + 5;
|
||||
textCursorY += fontSmall.lineHeight();
|
||||
}
|
||||
|
||||
fillRect(textCursorX + 1, textCursorY, 1, fontSmall.lineHeight(), BLACK);
|
||||
|
||||
// A white rectangle clears the top part of the screen for any text that's printed beyond the input box
|
||||
fillRect(0, 0, X(1.0), top, WHITE);
|
||||
|
||||
// Draw character limit
|
||||
std::string ftlen = std::to_string(text.length()) + "/" + to_string(menuTextLimit);
|
||||
uint16_t textLen = getTextWidth(ftlen);
|
||||
printAt(X(1.0) - textLen - 2, 0, ftlen);
|
||||
|
||||
// Draw the border
|
||||
drawRect(0, top, width, wrapMaxH + 5, BLACK);
|
||||
}
|
||||
// Renders the panel shown at the top of the root menu.
|
||||
// Displays the clock, and several other pieces of instantaneous system info,
|
||||
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
|
||||
@@ -1887,4 +2012,4 @@ void InkHUD::MenuApplet::freeCannedMessageResources()
|
||||
cm.messageItems.clear();
|
||||
cm.recipientItems.clear();
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_INKHUD
|
||||
#endif // MESHTASTIC_INCLUDE_INKHUD
|
||||
|
||||
@@ -32,7 +32,10 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
void onRender() override;
|
||||
void onFreeText(char c) override;
|
||||
void onFreeTextDone() override;
|
||||
void onFreeTextCancel() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
void show(Tile *t); // Open the menu, onto a user tile
|
||||
void setStartPage(MenuPage page);
|
||||
@@ -51,6 +54,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
||||
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||
|
||||
void drawInputField(uint16_t left, uint16_t top, uint16_t width, uint16_t height,
|
||||
std::string text); // Draw input field for free text
|
||||
uint16_t getSystemInfoPanelHeight();
|
||||
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
||||
uint16_t *height = nullptr); // Info panel at top of root menu
|
||||
@@ -62,8 +67,9 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
MenuPage previousPage = MenuPage::EXIT;
|
||||
uint8_t cursor = 0; // Which menu item is currently highlighted
|
||||
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
|
||||
|
||||
bool freeTextMode = false;
|
||||
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
|
||||
uint16_t menuTextLimit = 200;
|
||||
|
||||
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
||||
std::vector<std::string> nodeConfigLabels; // Persistent labels for Node Config pages
|
||||
@@ -104,6 +110,8 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
// Cleared onBackground (when MenuApplet closes)
|
||||
std::vector<MessageItem> messageItems;
|
||||
std::vector<RecipientItem> recipientItems;
|
||||
|
||||
MessageItem freeTextItem;
|
||||
} cm;
|
||||
|
||||
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
||||
|
||||
@@ -65,7 +65,7 @@ int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onRender()
|
||||
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
|
||||
@@ -139,54 +139,47 @@ void InkHUD::NotificationApplet::onForeground()
|
||||
void InkHUD::NotificationApplet::onBackground()
|
||||
{
|
||||
handleInput = false;
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onButtonShortPress()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onButtonLongPress()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onExitShort()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onExitLong()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onNavUp()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onNavDown()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onNavLeft()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onNavRight()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
|
||||
|
||||
@@ -26,7 +26,7 @@ class NotificationApplet : public SystemApplet
|
||||
public:
|
||||
NotificationApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
|
||||
@@ -9,7 +9,7 @@ InkHUD::PairingApplet::PairingApplet()
|
||||
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
|
||||
}
|
||||
|
||||
void InkHUD::PairingApplet::onRender()
|
||||
void InkHUD::PairingApplet::onRender(bool full)
|
||||
{
|
||||
// Header
|
||||
setFont(fontMedium);
|
||||
@@ -45,7 +45,7 @@ void InkHUD::PairingApplet::onBackground()
|
||||
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||
}
|
||||
|
||||
int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status)
|
||||
|
||||
@@ -22,7 +22,7 @@ class PairingApplet : public SystemApplet
|
||||
public:
|
||||
PairingApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::PlaceholderApplet::onRender()
|
||||
void InkHUD::PlaceholderApplet::onRender(bool full)
|
||||
{
|
||||
// This placeholder applet fills its area with sparse diagonal lines
|
||||
hatchRegion(0, 0, width(), height(), 8, BLACK);
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace NicheGraphics::InkHUD
|
||||
class PlaceholderApplet : public SystemApplet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
// Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet.
|
||||
// The window manager decides when and where it should be rendered
|
||||
|
||||
@@ -45,7 +45,7 @@ InkHUD::TipsApplet::TipsApplet()
|
||||
bringToForeground();
|
||||
}
|
||||
|
||||
void InkHUD::TipsApplet::onRender()
|
||||
void InkHUD::TipsApplet::onRender(bool full)
|
||||
{
|
||||
switch (tipQueue.front()) {
|
||||
case Tip::WELCOME:
|
||||
@@ -261,7 +261,7 @@ void InkHUD::TipsApplet::onBackground()
|
||||
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
|
||||
}
|
||||
|
||||
// While our SystemApplet::handleInput flag is true
|
||||
@@ -292,9 +292,8 @@ void InkHUD::TipsApplet::onButtonShortPress()
|
||||
inkhud->persistence->saveSettings();
|
||||
}
|
||||
|
||||
// Close applet and clean the screen
|
||||
// Close applet
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
} else {
|
||||
requestUpdate();
|
||||
}
|
||||
@@ -306,4 +305,4 @@ void InkHUD::TipsApplet::onExitShort()
|
||||
onButtonShortPress();
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -33,7 +33,7 @@ class TipsApplet : public SystemApplet
|
||||
public:
|
||||
TipsApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
|
||||
@@ -34,7 +34,7 @@ int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InkHUD::AllMessageApplet::onRender()
|
||||
void InkHUD::AllMessageApplet::onRender(bool full)
|
||||
{
|
||||
// Find newest message, regardless of whether DM or broadcast
|
||||
MessageStore::Message *message;
|
||||
|
||||
@@ -30,7 +30,7 @@ class Applet;
|
||||
class AllMessageApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
|
||||
@@ -37,7 +37,7 @@ int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InkHUD::DMApplet::onRender()
|
||||
void InkHUD::DMApplet::onRender(bool full)
|
||||
{
|
||||
// Abort if no text message
|
||||
if (!latestMessage->dm.sender) {
|
||||
|
||||
@@ -30,7 +30,7 @@ class Applet;
|
||||
class DMApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::PositionsApplet::onRender()
|
||||
void InkHUD::PositionsApplet::onRender(bool full)
|
||||
{
|
||||
// Draw the usual map applet first
|
||||
MapApplet::onRender();
|
||||
MapApplet::onRender(full);
|
||||
|
||||
// Draw our latest "node of interest" as a special marker
|
||||
// -------------------------------------------------------
|
||||
|
||||
@@ -24,7 +24,7 @@ class PositionsApplet : public MapApplet, public SinglePortModule
|
||||
{
|
||||
public:
|
||||
PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {}
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
protected:
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
@@ -22,7 +22,7 @@ InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex)
|
||||
store = new MessageStore("ch" + to_string(channelIndex));
|
||||
}
|
||||
|
||||
void InkHUD::ThreadedMessageApplet::onRender()
|
||||
void InkHUD::ThreadedMessageApplet::onRender(bool full)
|
||||
{
|
||||
// =============
|
||||
// Draw a header
|
||||
|
||||
@@ -36,7 +36,7 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule
|
||||
explicit ThreadedMessageApplet(uint8_t channelIndex);
|
||||
ThreadedMessageApplet() = delete;
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
|
||||
@@ -238,6 +238,39 @@ void InkHUD::Events::onNavRight()
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onFreeText(char c)
|
||||
{
|
||||
// Trigger the first system applet that wants to handle the new character
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleFreeText) {
|
||||
sa->onFreeText(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onFreeTextDone()
|
||||
{
|
||||
// Trigger the first system applet that wants to handle it
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleFreeText) {
|
||||
sa->onFreeTextDone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onFreeTextCancel()
|
||||
{
|
||||
// Trigger the first system applet that wants to handle it
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleFreeText) {
|
||||
sa->onFreeTextCancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Callback for deepSleepObserver
|
||||
// Returns 0 to signal that we agree to sleep now
|
||||
int InkHUD::Events::beforeDeepSleep(void *unused)
|
||||
@@ -266,7 +299,7 @@ int InkHUD::Events::beforeDeepSleep(void *unused)
|
||||
// then prepared a final powered-off screen for us, which shows device shortname.
|
||||
// We're updating to show that one now.
|
||||
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false);
|
||||
delay(1000); // Cooldown, before potentially yanking display power
|
||||
|
||||
// InkHUD shutdown complete
|
||||
|
||||
@@ -37,6 +37,11 @@ class Events
|
||||
void onNavLeft(); // Navigate left
|
||||
void onNavRight(); // Navigate right
|
||||
|
||||
// Free text typing events
|
||||
void onFreeText(char c); // New freetext character input
|
||||
void onFreeTextDone();
|
||||
void onFreeTextCancel();
|
||||
|
||||
int beforeDeepSleep(void *unused); // Prepare for shutdown
|
||||
int beforeReboot(void *unused); // Prepare for reboot
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message
|
||||
|
||||
@@ -175,6 +175,25 @@ void InkHUD::InkHUD::navRight()
|
||||
}
|
||||
}
|
||||
|
||||
// Call this for keyboard input
|
||||
// The Keyboard Applet also calls this
|
||||
void InkHUD::InkHUD::freeText(char c)
|
||||
{
|
||||
events->onFreeText(c);
|
||||
}
|
||||
|
||||
// Call this to complete a freetext input
|
||||
void InkHUD::InkHUD::freeTextDone()
|
||||
{
|
||||
events->onFreeTextDone();
|
||||
}
|
||||
|
||||
// Call this to cancel a freetext input
|
||||
void InkHUD::InkHUD::freeTextCancel()
|
||||
{
|
||||
events->onFreeTextCancel();
|
||||
}
|
||||
|
||||
// Cycle the next user applet to the foreground
|
||||
// Only activated applets are cycled
|
||||
// If user has a multi-applet layout, the applets will cycle on the "focused tile"
|
||||
@@ -204,6 +223,18 @@ void InkHUD::InkHUD::openAlignStick()
|
||||
windowManager->openAlignStick();
|
||||
}
|
||||
|
||||
// Open the on-screen keyboard
|
||||
void InkHUD::InkHUD::openKeyboard()
|
||||
{
|
||||
windowManager->openKeyboard();
|
||||
}
|
||||
|
||||
// Close the on-screen keyboard
|
||||
void InkHUD::InkHUD::closeKeyboard()
|
||||
{
|
||||
windowManager->closeKeyboard();
|
||||
}
|
||||
|
||||
// In layouts where multiple applets are shown at once, change which tile is focused
|
||||
// The focused tile in the one which cycles applets on button short press, and displays menu on long press
|
||||
void InkHUD::InkHUD::nextTile()
|
||||
@@ -252,10 +283,11 @@ void InkHUD::InkHUD::requestUpdate()
|
||||
// Ignores all diplomacy:
|
||||
// - the display *will* update
|
||||
// - the specified update type *will* be used
|
||||
// If the all parameter is true, the whole screen buffer is cleared and re-rendered
|
||||
// If the async parameter is false, code flow is blocked while the update takes place
|
||||
void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async)
|
||||
void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool all, bool async)
|
||||
{
|
||||
renderer->forceUpdate(type, async);
|
||||
renderer->forceUpdate(type, all, async);
|
||||
}
|
||||
|
||||
// Wait for any in-progress display update to complete before continuing
|
||||
|
||||
@@ -63,6 +63,11 @@ class InkHUD
|
||||
void navLeft();
|
||||
void navRight();
|
||||
|
||||
// Freetext handlers
|
||||
void freeText(char c);
|
||||
void freeTextDone();
|
||||
void freeTextCancel();
|
||||
|
||||
// Trigger UI changes
|
||||
// - called by various InkHUD components
|
||||
// - suitable(?) for use by aux button, connected in variant nicheGraphics.h
|
||||
@@ -71,6 +76,8 @@ class InkHUD
|
||||
void prevApplet();
|
||||
void openMenu();
|
||||
void openAlignStick();
|
||||
void openKeyboard();
|
||||
void closeKeyboard();
|
||||
void nextTile();
|
||||
void prevTile();
|
||||
void rotate();
|
||||
@@ -84,7 +91,8 @@ class InkHUD
|
||||
// - called by various InkHUD components
|
||||
|
||||
void requestUpdate();
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true);
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false,
|
||||
bool async = true);
|
||||
void awaitUpdate();
|
||||
|
||||
// (Re)configuring WindowManager
|
||||
|
||||
@@ -56,15 +56,16 @@ void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMul
|
||||
|
||||
void InkHUD::Renderer::begin()
|
||||
{
|
||||
forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
|
||||
forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false);
|
||||
}
|
||||
|
||||
// Set a flag, which will be picked up by runOnce, ASAP.
|
||||
// Quite likely, multiple applets will all want to respond to one event (Observable, etc)
|
||||
// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce
|
||||
void InkHUD::Renderer::requestUpdate()
|
||||
void InkHUD::Renderer::requestUpdate(bool all)
|
||||
{
|
||||
requested = true;
|
||||
renderAll |= all;
|
||||
|
||||
// We will run the thread as soon as we loop(),
|
||||
// after all Applets have had a chance to observe whatever event set this off
|
||||
@@ -79,10 +80,11 @@ void InkHUD::Renderer::requestUpdate()
|
||||
// Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event
|
||||
// Display health, for example.
|
||||
// In these situations, we use forceUpdate
|
||||
void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async)
|
||||
void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool all, bool async)
|
||||
{
|
||||
requested = true;
|
||||
forced = true;
|
||||
renderAll |= all;
|
||||
displayHealth.forceUpdateType(type);
|
||||
|
||||
// Normally, we need to start the timer, in case the display is busy and we briefly defer the update
|
||||
@@ -219,7 +221,8 @@ void InkHUD::Renderer::render(bool async)
|
||||
Drivers::EInk::UpdateTypes updateType = decideUpdateType();
|
||||
|
||||
// Render the new image
|
||||
clearBuffer();
|
||||
if (renderAll)
|
||||
clearBuffer();
|
||||
renderUserApplets();
|
||||
renderPlaceholders();
|
||||
renderSystemApplets();
|
||||
@@ -247,6 +250,7 @@ void InkHUD::Renderer::render(bool async)
|
||||
// Tidy up, ready for a new request
|
||||
requested = false;
|
||||
forced = false;
|
||||
renderAll = false;
|
||||
}
|
||||
|
||||
// Manually fill the image buffer with WHITE
|
||||
@@ -259,6 +263,76 @@ void InkHUD::Renderer::clearBuffer()
|
||||
memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth);
|
||||
}
|
||||
|
||||
// Manually clear the pixels below a tile
|
||||
void InkHUD::Renderer::clearTile(Tile *t)
|
||||
{
|
||||
// Rotate the tile dimensions
|
||||
int16_t left = 0;
|
||||
int16_t top = 0;
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
switch (settings->rotation) {
|
||||
case 0:
|
||||
left = t->getLeft();
|
||||
top = t->getTop();
|
||||
width = t->getWidth();
|
||||
height = t->getHeight();
|
||||
break;
|
||||
case 1:
|
||||
left = driver->width - (t->getTop() + t->getHeight());
|
||||
top = t->getLeft();
|
||||
width = t->getHeight();
|
||||
height = t->getWidth();
|
||||
break;
|
||||
case 2:
|
||||
left = driver->width - (t->getLeft() + t->getWidth());
|
||||
top = driver->height - (t->getTop() + t->getHeight());
|
||||
width = t->getWidth();
|
||||
height = t->getHeight();
|
||||
break;
|
||||
case 3:
|
||||
left = t->getTop();
|
||||
top = driver->height - (t->getLeft() + t->getWidth());
|
||||
width = t->getHeight();
|
||||
height = t->getWidth();
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate the bounds to clear
|
||||
uint16_t xStart = (left < 0) ? 0 : left;
|
||||
uint16_t yStart = (top < 0) ? 0 : top;
|
||||
if (xStart >= driver->width || yStart >= driver->height || left + width < 0 || top + height < 0)
|
||||
return; // the box is completely off the screen
|
||||
uint16_t xEnd = left + width;
|
||||
uint16_t yEnd = top + height;
|
||||
if (xEnd > driver->width)
|
||||
xEnd = driver->width;
|
||||
if (yEnd > driver->height)
|
||||
yEnd = driver->height;
|
||||
|
||||
// Clear the pixels
|
||||
if (xStart == 0 && xEnd == driver->width) { // full width box is easier to clear
|
||||
memset(imageBuffer + (yStart * imageBufferWidth), 0xFF, (yEnd - yStart) * imageBufferWidth);
|
||||
} else {
|
||||
const uint16_t byteStart = (xStart / 8) + 1;
|
||||
const uint16_t byteEnd = xEnd / 8;
|
||||
const uint8_t leadingByte = 0xFF >> (xStart - ((byteStart - 1) * 8));
|
||||
const uint8_t trailingByte = (0xFF00 >> (xEnd - (byteEnd * 8))) & 0xFF;
|
||||
for (uint16_t i = yStart * imageBufferWidth; i < yEnd * imageBufferWidth; i += imageBufferWidth) {
|
||||
// Set the leading byte
|
||||
imageBuffer[i + byteStart - 1] |= leadingByte;
|
||||
|
||||
// Set the continuous bytes
|
||||
if (byteStart < byteEnd)
|
||||
memset(imageBuffer + i + byteStart, 0xFF, byteEnd - byteStart);
|
||||
|
||||
// Set the trailing byte
|
||||
if (byteEnd != imageBufferWidth)
|
||||
imageBuffer[i + byteEnd] |= trailingByte;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Renderer::checkLocks()
|
||||
{
|
||||
lockRendering = nullptr;
|
||||
@@ -323,12 +397,12 @@ Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType()
|
||||
if (!forced) {
|
||||
// User applets
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
if (ua && ua->isForeground())
|
||||
if (ua && ua->isForeground() && (ua->wantsToRender() || renderAll))
|
||||
displayHealth.requestUpdateType(ua->wantsUpdateType());
|
||||
}
|
||||
// System Applets
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa && sa->isForeground())
|
||||
if (sa && sa->isForeground() && (sa->wantsToRender() || sa->alwaysRender || renderAll))
|
||||
displayHealth.requestUpdateType(sa->wantsUpdateType());
|
||||
}
|
||||
}
|
||||
@@ -346,9 +420,16 @@ void InkHUD::Renderer::renderUserApplets()
|
||||
|
||||
// Render any user applets which are currently visible
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
if (ua && ua->isActive() && ua->isForeground()) {
|
||||
if (ua && ua->isActive() && ua->isForeground() && (ua->wantsToRender() || renderAll)) {
|
||||
|
||||
// Clear the tile unless the applet wants to draw over its previous render
|
||||
// or everything is getting re-rendered anyways
|
||||
if (ua->wantsFullRender() && !renderAll)
|
||||
clearTile(ua->getTile());
|
||||
|
||||
uint32_t start = millis();
|
||||
ua->render(); // Draw!
|
||||
bool full = ua->wantsFullRender() || renderAll;
|
||||
ua->render(full); // Draw!
|
||||
uint32_t stop = millis();
|
||||
LOG_DEBUG("%s took %dms to render", ua->name, stop - start);
|
||||
}
|
||||
@@ -370,6 +451,9 @@ void InkHUD::Renderer::renderSystemApplets()
|
||||
if (!sa->isForeground())
|
||||
continue;
|
||||
|
||||
if (!sa->wantsToRender() && !sa->alwaysRender && !renderAll)
|
||||
continue;
|
||||
|
||||
// Skip if locked by another applet
|
||||
if (lockRendering && lockRendering != sa)
|
||||
continue;
|
||||
@@ -381,8 +465,14 @@ void InkHUD::Renderer::renderSystemApplets()
|
||||
|
||||
assert(sa->getTile());
|
||||
|
||||
// Clear the tile unless the applet wants to draw over its previous render
|
||||
// or everything is getting re-rendered anyways
|
||||
if (sa->wantsFullRender() && !renderAll)
|
||||
clearTile(sa->getTile());
|
||||
|
||||
// uint32_t start = millis();
|
||||
sa->render(); // Draw!
|
||||
bool full = sa->wantsFullRender() || renderAll;
|
||||
sa->render(full); // Draw!
|
||||
// uint32_t stop = millis();
|
||||
// LOG_DEBUG("%s took %dms to render", sa->name, stop - start);
|
||||
}
|
||||
@@ -409,7 +499,10 @@ void InkHUD::Renderer::renderPlaceholders()
|
||||
// uint32_t start = millis();
|
||||
for (Tile *t : emptyTiles) {
|
||||
t->assignApplet(placeholder);
|
||||
placeholder->render();
|
||||
// Clear the tile unless everything is getting re-rendered
|
||||
if (!renderAll)
|
||||
clearTile(t);
|
||||
placeholder->render(true); // full render
|
||||
t->assignApplet(nullptr);
|
||||
}
|
||||
// uint32_t stop = millis();
|
||||
|
||||
@@ -37,8 +37,8 @@ class Renderer : protected concurrency::OSThread
|
||||
|
||||
// Call these to make the image change
|
||||
|
||||
void requestUpdate(); // Update display, if a foreground applet has info it wants to show
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED,
|
||||
void requestUpdate(bool all = false); // Update display, if a foreground applet has info it wants to show
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool all = false,
|
||||
bool async = true); // Update display, regardless of whether any applets requested this
|
||||
|
||||
// Wait for an update to complete
|
||||
@@ -65,6 +65,7 @@ class Renderer : protected concurrency::OSThread
|
||||
// Steps of the rendering process
|
||||
|
||||
void clearBuffer();
|
||||
void clearTile(Tile *t);
|
||||
void checkLocks();
|
||||
bool shouldUpdate();
|
||||
Drivers::EInk::UpdateTypes decideUpdateType();
|
||||
@@ -85,6 +86,7 @@ class Renderer : protected concurrency::OSThread
|
||||
|
||||
bool requested = false;
|
||||
bool forced = false;
|
||||
bool renderAll = false;
|
||||
|
||||
// For convenience
|
||||
InkHUD *inkhud = nullptr;
|
||||
|
||||
@@ -22,9 +22,11 @@ class SystemApplet : public Applet
|
||||
public:
|
||||
// System applets have the right to:
|
||||
|
||||
bool handleInput = false; // - respond to input from the user button
|
||||
bool lockRendering = false; // - prevent other applets from being rendered during an update
|
||||
bool lockRequests = false; // - prevent other applets from triggering display updates
|
||||
bool handleInput = false; // - respond to input from the user button
|
||||
bool handleFreeText = false; // - respond to free text input
|
||||
bool lockRendering = false; // - prevent other applets from being rendered during an update
|
||||
bool lockRequests = false; // - prevent other applets from triggering display updates
|
||||
bool alwaysRender = false; // - render every time the screen is updated
|
||||
|
||||
virtual void onReboot() { onShutdown(); } // - handle reboot specially
|
||||
virtual void onApplyingChanges() {}
|
||||
@@ -41,4 +43,4 @@ class SystemApplet : public Applet
|
||||
|
||||
}; // namespace NicheGraphics::InkHUD
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -18,7 +18,7 @@ static int32_t runtaskHighlight()
|
||||
LOG_DEBUG("Dismissing Highlight");
|
||||
InkHUD::Tile::highlightShown = false;
|
||||
InkHUD::Tile::highlightTarget = nullptr;
|
||||
InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting
|
||||
InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true); // Re-render, clearing the highlighting
|
||||
return taskHighlight->disable();
|
||||
}
|
||||
static void inittaskHighlight()
|
||||
@@ -190,6 +190,18 @@ void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c)
|
||||
}
|
||||
}
|
||||
|
||||
// Used in Renderer for clearing the tile
|
||||
int16_t InkHUD::Tile::getLeft()
|
||||
{
|
||||
return left;
|
||||
}
|
||||
|
||||
// Used in Renderer for clearing the tile
|
||||
int16_t InkHUD::Tile::getTop()
|
||||
{
|
||||
return top;
|
||||
}
|
||||
|
||||
// Called by Applet base class, when setting applet dimensions, immediately before render
|
||||
uint16_t InkHUD::Tile::getWidth()
|
||||
{
|
||||
@@ -220,7 +232,7 @@ void InkHUD::Tile::requestHighlight()
|
||||
{
|
||||
Tile::highlightTarget = this;
|
||||
Tile::highlightShown = false;
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST, true);
|
||||
}
|
||||
|
||||
// Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first
|
||||
|
||||
@@ -29,6 +29,8 @@ class Tile
|
||||
void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout
|
||||
void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually
|
||||
void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet
|
||||
int16_t getLeft();
|
||||
int16_t getTop();
|
||||
uint16_t getWidth();
|
||||
uint16_t getHeight();
|
||||
static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "./Applets/System/AlignStick/AlignStickApplet.h"
|
||||
#include "./Applets/System/BatteryIcon/BatteryIconApplet.h"
|
||||
#include "./Applets/System/Keyboard/KeyboardApplet.h"
|
||||
#include "./Applets/System/Logo/LogoApplet.h"
|
||||
#include "./Applets/System/Menu/MenuApplet.h"
|
||||
#include "./Applets/System/Notification/NotificationApplet.h"
|
||||
@@ -148,6 +149,28 @@ void InkHUD::WindowManager::openAlignStick()
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::WindowManager::openKeyboard()
|
||||
{
|
||||
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
|
||||
|
||||
if (keyboard) {
|
||||
keyboard->bringToForeground();
|
||||
keyboardOpen = true;
|
||||
changeLayout();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::WindowManager::closeKeyboard()
|
||||
{
|
||||
KeyboardApplet *keyboard = (KeyboardApplet *)inkhud->getSystemApplet("Keyboard");
|
||||
|
||||
if (keyboard) {
|
||||
keyboard->sendToBackground();
|
||||
keyboardOpen = false;
|
||||
changeLayout();
|
||||
}
|
||||
}
|
||||
|
||||
// On the currently focussed tile: cycle to the next available user applet
|
||||
// Applets available for this must be activated, and not already displayed on another tile
|
||||
void InkHUD::WindowManager::nextApplet()
|
||||
@@ -272,7 +295,6 @@ void InkHUD::WindowManager::toggleBatteryIcon()
|
||||
batteryIcon->sendToBackground();
|
||||
|
||||
// Force-render
|
||||
// - redraw all applets
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
@@ -311,9 +333,25 @@ void InkHUD::WindowManager::changeLayout()
|
||||
menu->show(ft);
|
||||
}
|
||||
|
||||
// Resize for the on-screen keyboard
|
||||
if (keyboardOpen) {
|
||||
// Send all user applets to the background
|
||||
// User applets currently don't handle free text input
|
||||
for (uint8_t i = 0; i < inkhud->userApplets.size(); i++)
|
||||
inkhud->userApplets.at(i)->sendToBackground();
|
||||
// Find the first system applet that can handle freetext and resize it
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleFreeText) {
|
||||
const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight();
|
||||
sa->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height() - keyboardHeight - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force-render
|
||||
// - redraw all applets
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST, true);
|
||||
}
|
||||
|
||||
// Perform necessary reconfiguration when user activates or deactivates applets at run-time
|
||||
@@ -347,7 +385,7 @@ void InkHUD::WindowManager::changeActivatedApplets()
|
||||
|
||||
// Force-render
|
||||
// - redraw all applets
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST);
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FAST, true);
|
||||
}
|
||||
|
||||
// Some applets may be permitted to bring themselves to foreground, to show new data
|
||||
@@ -433,8 +471,10 @@ void InkHUD::WindowManager::createSystemApplets()
|
||||
addSystemApplet("Logo", new LogoApplet, new Tile);
|
||||
addSystemApplet("Pairing", new PairingApplet, new Tile);
|
||||
addSystemApplet("Tips", new TipsApplet, new Tile);
|
||||
if (settings->joystick.enabled)
|
||||
if (settings->joystick.enabled) {
|
||||
addSystemApplet("AlignStick", new AlignStickApplet, new Tile);
|
||||
addSystemApplet("Keyboard", new KeyboardApplet, new Tile);
|
||||
}
|
||||
|
||||
addSystemApplet("Menu", new MenuApplet, nullptr);
|
||||
|
||||
@@ -457,9 +497,13 @@ void InkHUD::WindowManager::placeSystemTiles()
|
||||
inkhud->getSystemApplet("Logo")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||
inkhud->getSystemApplet("Pairing")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||
inkhud->getSystemApplet("Tips")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||
if (settings->joystick.enabled)
|
||||
if (settings->joystick.enabled) {
|
||||
inkhud->getSystemApplet("AlignStick")->getTile()->setRegion(0, 0, inkhud->width(), inkhud->height());
|
||||
|
||||
const uint16_t keyboardHeight = KeyboardApplet::getKeyboardHeight();
|
||||
inkhud->getSystemApplet("Keyboard")
|
||||
->getTile()
|
||||
->setRegion(0, inkhud->height() - keyboardHeight, inkhud->width(), keyboardHeight);
|
||||
}
|
||||
inkhud->getSystemApplet("Notification")->getTile()->setRegion(0, 0, inkhud->width(), 20);
|
||||
|
||||
const uint16_t batteryIconHeight = Applet::getHeaderHeight() - 2 - 2;
|
||||
|
||||
@@ -31,6 +31,8 @@ class WindowManager
|
||||
void prevTile();
|
||||
void openMenu();
|
||||
void openAlignStick();
|
||||
void openKeyboard();
|
||||
void closeKeyboard();
|
||||
void nextApplet();
|
||||
void prevApplet();
|
||||
void rotate();
|
||||
@@ -64,6 +66,7 @@ class WindowManager
|
||||
void findOrphanApplets(); // Find any applets left-behind when layout changes
|
||||
|
||||
std::vector<Tile *> userTiles; // Tiles which can host user applets
|
||||
bool keyboardOpen = false;
|
||||
|
||||
// For convenience
|
||||
InkHUD *inkhud = nullptr;
|
||||
|
||||
@@ -174,7 +174,7 @@ class BasicExampleApplet : public Applet
|
||||
// You must have an onRender() method
|
||||
// All drawing happens here
|
||||
|
||||
void onRender() override;
|
||||
void onRender(bool full) override;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -183,7 +183,7 @@ The `onRender` method is called when the display image is redrawn. This can happ
|
||||
```cpp
|
||||
// All drawing happens here
|
||||
// Our basic example doesn't do anything useful. It just passively prints some text.
|
||||
void InkHUD::BasicExampleApplet::onRender()
|
||||
void InkHUD::BasicExampleApplet::onRender(bool full)
|
||||
{
|
||||
printAt(0, 0, "Hello, world!");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user