diff --git a/CONTEXT.md b/CONTEXT.md index 45bedf3..0335bde 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -66,6 +66,10 @@ The regulatory band plan the device transmits under (here EU868). It sets the al **Duty Cycle Budget**: The share of airtime the Region allows this device to transmit. When it's used up, outgoing traffic waits. +**Text Entry**: +When an App is editing text. During Text Entry, `;` `.` `,` `/` type their characters and Fn makes them arrows. Otherwise they are arrows on their own. +_Avoid_: edit mode, insert mode + **Compose Key**: The `opt` key used as a dead key. Pressing it and then a base letter types an accented character (e.g. `opt` `'` `e` → é). _Avoid_: modifier, alt diff --git a/lib/core/src/app.h b/lib/core/src/app.h index 1a47dcb..8ebef71 100644 --- a/lib/core/src/app.h +++ b/lib/core/src/app.h @@ -22,6 +22,9 @@ class App { return false; } + // True while the App is editing text: the arrow keys then type ; . , / and need Fn to move. + virtual bool textEntryActive() const { return false; } + // Called every main-loop pass while in the foreground (e.g. to refresh live values). virtual void update(uint32_t nowMs) { (void)nowMs; } diff --git a/lib/input/src/key_mapper.cpp b/lib/input/src/key_mapper.cpp index ecf5e0c..7c26726 100644 --- a/lib/input/src/key_mapper.cpp +++ b/lib/input/src/key_mapper.cpp @@ -87,14 +87,21 @@ void KeyMapper::onChar(char c, const RawKeys& keys, std::vector& out) return; } - if (keys.fn) { + if (keys.fn || !textEntry_) { switch (c) { case ';': out.push_back(KeyEvent::of(Key::Up)); return; case '.': out.push_back(KeyEvent::of(Key::Down)); return; case ',': out.push_back(KeyEvent::of(Key::Left)); return; case '/': out.push_back(KeyEvent::of(Key::Right)); return; - case '`': out.push_back(KeyEvent::of(Key::Home)); return; - default: return; // other Fn combos are unassigned + case '`': + if (keys.fn) { + out.push_back(KeyEvent::of(Key::Home)); + return; + } + break; + default: + if (keys.fn) return; // other Fn combos are unassigned + break; } } if (c == '`') { diff --git a/lib/input/src/key_mapper.h b/lib/input/src/key_mapper.h index 08c9fe7..4a4285c 100644 --- a/lib/input/src/key_mapper.h +++ b/lib/input/src/key_mapper.h @@ -22,12 +22,17 @@ struct RawKeys { }; // Turns keyboard state changes into logical KeyEvents: only newly pressed keys produce events; -// Fn + ; . , / are arrows; ` is Back and Fn + ` is Home; the Compose Key (opt) followed by an -// accent and a letter types the accented letter (opt ' e -> é). +// Fn + ; . , / are arrows, and so are ; . , / alone when no text is being entered; ` is Back and +// Fn + ` is Home; the Compose Key (opt) followed by an accent and a letter types the accented +// letter (opt ' e -> é). class KeyMapper { public: std::vector update(const RawKeys& keys); + // Whether the foreground App is editing text (default). Outside text entry, the arrow keys + // don't need Fn. + void setTextEntry(bool active) { textEntry_ = active; } + // 0 when idle, '*' when opt was pressed, or the accent character waiting for its letter. char pendingCompose() const { return compose_; } @@ -36,6 +41,7 @@ class KeyMapper { RawKeys previous_; char compose_ = 0; + bool textEntry_ = true; }; } // namespace roro diff --git a/src/apps/demo_app.cpp b/src/apps/demo_app.cpp index 585d86a..4a46f8c 100644 --- a/src/apps/demo_app.cpp +++ b/src/apps/demo_app.cpp @@ -11,7 +11,7 @@ const char* const kText = "Bienvenue sur roro9stack ! Ce texte teste le retour \xC3\xA0 la ligne automatique et les " "accents : \xC3\xA9t\xC3\xA9, \xC3\xA0 bient\xC3\xB4t, gar\xC3\xA7on, No\xC3\xABl, " "ma\xC3\xAEtre, o\xC3\xB9.\n\n" - "Use Fn+; and Fn+. to scroll, ` to go back. Fn+` always returns to the Launcher.\n\n" + "Use ; and . to scroll, ` to go back. Fn+` always returns to the Launcher.\n\n" "Unbreakablewordthatislongerthanawholelineofthescreen to test hard breaks."; } // namespace diff --git a/src/apps/demo_app.h b/src/apps/demo_app.h index 91b8899..8f2e39e 100644 --- a/src/apps/demo_app.h +++ b/src/apps/demo_app.h @@ -19,6 +19,7 @@ class DemoApp : public App { explicit DemoApp(EventBus& bus) : bus_(bus) {} void onEnter() override; bool onKey(const KeyEvent& e) override; + bool textEntryActive() const override { return page_ == Page::Editor && !dialog_; } void draw(Canvas& c) override; private: diff --git a/src/apps/settings_app.h b/src/apps/settings_app.h index 6a0c450..4332d18 100644 --- a/src/apps/settings_app.h +++ b/src/apps/settings_app.h @@ -34,6 +34,7 @@ class SettingsApp : public App { void onEnter() override; bool onKey(const KeyEvent& e) override; void update(uint32_t nowMs) override; + bool textEntryActive() const override { return page_ == Page::Text; } void draw(Canvas& c) override; private: diff --git a/src/apps/setup_app.h b/src/apps/setup_app.h index 4235998..2a9921a 100644 --- a/src/apps/setup_app.h +++ b/src/apps/setup_app.h @@ -13,6 +13,9 @@ class SetupApp : public App { public: SetupApp(Settings& settings, AppManager& apps); bool onKey(const KeyEvent& e) override; + bool textEntryActive() const override { + return wizard_.step() == SetupWizard::Step::LongName || wizard_.step() == SetupWizard::Step::ShortName; + } void draw(Canvas& c) override; private: diff --git a/src/main.cpp b/src/main.cpp index c8bc766..0c67698 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -143,6 +143,7 @@ void loop() { M5Cardputer.update(); if (M5Cardputer.Keyboard.isChange()) { bool swallow = M5Cardputer.Keyboard.isPressed() && power->onKey(now); // only woke the screen + keyMapper.setTextEntry(apps->foreground().textEntryActive()); auto events = keyMapper.update(readKeys()); // always, so held keys are tracked if (!swallow) for (auto& e : events) apps->handleKey(e); diff --git a/test/test_key_mapper/test_key_mapper.cpp b/test/test_key_mapper/test_key_mapper.cpp index 3b0fff4..99d178f 100644 --- a/test/test_key_mapper/test_key_mapper.cpp +++ b/test/test_key_mapper/test_key_mapper.cpp @@ -175,6 +175,51 @@ void test_compose_state_is_visible_for_the_ui() { TEST_ASSERT_EQUAL('^', m.pendingCompose()); } +void test_arrow_keys_work_without_fn_when_not_typing() { + KeyMapper m; + m.setTextEntry(false); + TEST_ASSERT_EQUAL(static_cast(Key::Up), static_cast(press(m, chars({';'}))[0].key)); + release(m); + TEST_ASSERT_EQUAL(static_cast(Key::Down), static_cast(press(m, chars({'.'}))[0].key)); + release(m); + TEST_ASSERT_EQUAL(static_cast(Key::Left), static_cast(press(m, chars({','}))[0].key)); + release(m); + TEST_ASSERT_EQUAL(static_cast(Key::Right), static_cast(press(m, chars({'/'}))[0].key)); +} + +void test_arrow_keys_type_characters_during_text_entry() { + KeyMapper m; + m.setTextEntry(true); + auto ev = press(m, chars({';'})); + TEST_ASSERT_EQUAL(static_cast(Key::Char), static_cast(ev[0].key)); + TEST_ASSERT_EQUAL(';', ev[0].ch); +} + +void test_fn_arrows_work_in_both_modes() { + KeyMapper m; + m.setTextEntry(false); + TEST_ASSERT_EQUAL(static_cast(Key::Up), static_cast(press(m, withFn({';'}))[0].key)); + release(m); + m.setTextEntry(true); + TEST_ASSERT_EQUAL(static_cast(Key::Up), static_cast(press(m, withFn({';'}))[0].key)); +} + +void test_other_characters_still_type_when_not_typing() { + KeyMapper m; + m.setTextEntry(false); + auto ev = press(m, chars({'w'})); + TEST_ASSERT_EQUAL('w', ev[0].ch); +} + +void test_shifted_arrow_keys_type_their_symbols_when_not_typing() { + // Shift+; is ':' — the driver reports the shifted character, which isn't an arrow. + KeyMapper m; + m.setTextEntry(false); + RawKeys r = chars({':'}); + r.shift = true; + TEST_ASSERT_EQUAL(':', press(m, r)[0].ch); +} + int main() { UNITY_BEGIN(); RUN_TEST(test_plain_character); @@ -191,5 +236,10 @@ int main() { RUN_TEST(test_opt_followed_by_a_non_accent_types_it_normally); RUN_TEST(test_pressing_opt_twice_cancels); RUN_TEST(test_compose_state_is_visible_for_the_ui); + RUN_TEST(test_arrow_keys_work_without_fn_when_not_typing); + RUN_TEST(test_arrow_keys_type_characters_during_text_entry); + RUN_TEST(test_fn_arrows_work_in_both_modes); + RUN_TEST(test_other_characters_still_type_when_not_typing); + RUN_TEST(test_shifted_arrow_keys_type_their_symbols_when_not_typing); return UNITY_END(); }