Public Access
Arrow keys work without Fn outside Text Entry
; . , / are arrows on their own unless the foreground App is editing text (App::textEntryActive); Fn + those keys are arrows everywhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -66,6 +66,10 @@ The regulatory band plan the device transmits under (here EU868). It sets the al
|
|||||||
**Duty Cycle Budget**:
|
**Duty Cycle Budget**:
|
||||||
The share of airtime the Region allows this device to transmit. When it's used up, outgoing traffic waits.
|
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**:
|
**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` → é).
|
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
|
_Avoid_: modifier, alt
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ class App {
|
|||||||
return false;
|
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).
|
// Called every main-loop pass while in the foreground (e.g. to refresh live values).
|
||||||
virtual void update(uint32_t nowMs) { (void)nowMs; }
|
virtual void update(uint32_t nowMs) { (void)nowMs; }
|
||||||
|
|
||||||
|
|||||||
@@ -87,14 +87,21 @@ void KeyMapper::onChar(char c, const RawKeys& keys, std::vector<KeyEvent>& out)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keys.fn) {
|
if (keys.fn || !textEntry_) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case ';': out.push_back(KeyEvent::of(Key::Up)); return;
|
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::Down)); return;
|
||||||
case ',': out.push_back(KeyEvent::of(Key::Left)); 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::Right)); return;
|
||||||
case '`': out.push_back(KeyEvent::of(Key::Home)); return;
|
case '`':
|
||||||
default: return; // other Fn combos are unassigned
|
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 == '`') {
|
if (c == '`') {
|
||||||
|
|||||||
@@ -22,12 +22,17 @@ struct RawKeys {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Turns keyboard state changes into logical KeyEvents: only newly pressed keys produce events;
|
// 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
|
// Fn + ; . , / are arrows, and so are ; . , / alone when no text is being entered; ` is Back and
|
||||||
// accent and a letter types the accented letter (opt ' e -> é).
|
// Fn + ` is Home; the Compose Key (opt) followed by an accent and a letter types the accented
|
||||||
|
// letter (opt ' e -> é).
|
||||||
class KeyMapper {
|
class KeyMapper {
|
||||||
public:
|
public:
|
||||||
std::vector<KeyEvent> update(const RawKeys& keys);
|
std::vector<KeyEvent> 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.
|
// 0 when idle, '*' when opt was pressed, or the accent character waiting for its letter.
|
||||||
char pendingCompose() const { return compose_; }
|
char pendingCompose() const { return compose_; }
|
||||||
|
|
||||||
@@ -36,6 +41,7 @@ class KeyMapper {
|
|||||||
|
|
||||||
RawKeys previous_;
|
RawKeys previous_;
|
||||||
char compose_ = 0;
|
char compose_ = 0;
|
||||||
|
bool textEntry_ = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace roro
|
} // namespace roro
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const char* const kText =
|
|||||||
"Bienvenue sur roro9stack ! Ce texte teste le retour \xC3\xA0 la ligne automatique et les "
|
"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, "
|
"accents : \xC3\xA9t\xC3\xA9, \xC3\xA0 bient\xC3\xB4t, gar\xC3\xA7on, No\xC3\xABl, "
|
||||||
"ma\xC3\xAEtre, o\xC3\xB9.\n\n"
|
"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.";
|
"Unbreakablewordthatislongerthanawholelineofthescreen to test hard breaks.";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class DemoApp : public App {
|
|||||||
explicit DemoApp(EventBus& bus) : bus_(bus) {}
|
explicit DemoApp(EventBus& bus) : bus_(bus) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
bool onKey(const KeyEvent& e) override;
|
bool onKey(const KeyEvent& e) override;
|
||||||
|
bool textEntryActive() const override { return page_ == Page::Editor && !dialog_; }
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class SettingsApp : public App {
|
|||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
bool onKey(const KeyEvent& e) override;
|
bool onKey(const KeyEvent& e) override;
|
||||||
void update(uint32_t nowMs) override;
|
void update(uint32_t nowMs) override;
|
||||||
|
bool textEntryActive() const override { return page_ == Page::Text; }
|
||||||
void draw(Canvas& c) override;
|
void draw(Canvas& c) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ class SetupApp : public App {
|
|||||||
public:
|
public:
|
||||||
SetupApp(Settings& settings, AppManager& apps);
|
SetupApp(Settings& settings, AppManager& apps);
|
||||||
bool onKey(const KeyEvent& e) override;
|
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;
|
void draw(Canvas& c) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ void loop() {
|
|||||||
M5Cardputer.update();
|
M5Cardputer.update();
|
||||||
if (M5Cardputer.Keyboard.isChange()) {
|
if (M5Cardputer.Keyboard.isChange()) {
|
||||||
bool swallow = M5Cardputer.Keyboard.isPressed() && power->onKey(now); // only woke the screen
|
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
|
auto events = keyMapper.update(readKeys()); // always, so held keys are tracked
|
||||||
if (!swallow)
|
if (!swallow)
|
||||||
for (auto& e : events) apps->handleKey(e);
|
for (auto& e : events) apps->handleKey(e);
|
||||||
|
|||||||
@@ -175,6 +175,51 @@ void test_compose_state_is_visible_for_the_ui() {
|
|||||||
TEST_ASSERT_EQUAL('^', m.pendingCompose());
|
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<int>(Key::Up), static_cast<int>(press(m, chars({';'}))[0].key));
|
||||||
|
release(m);
|
||||||
|
TEST_ASSERT_EQUAL(static_cast<int>(Key::Down), static_cast<int>(press(m, chars({'.'}))[0].key));
|
||||||
|
release(m);
|
||||||
|
TEST_ASSERT_EQUAL(static_cast<int>(Key::Left), static_cast<int>(press(m, chars({','}))[0].key));
|
||||||
|
release(m);
|
||||||
|
TEST_ASSERT_EQUAL(static_cast<int>(Key::Right), static_cast<int>(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<int>(Key::Char), static_cast<int>(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<int>(Key::Up), static_cast<int>(press(m, withFn({';'}))[0].key));
|
||||||
|
release(m);
|
||||||
|
m.setTextEntry(true);
|
||||||
|
TEST_ASSERT_EQUAL(static_cast<int>(Key::Up), static_cast<int>(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() {
|
int main() {
|
||||||
UNITY_BEGIN();
|
UNITY_BEGIN();
|
||||||
RUN_TEST(test_plain_character);
|
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_opt_followed_by_a_non_accent_types_it_normally);
|
||||||
RUN_TEST(test_pressing_opt_twice_cancels);
|
RUN_TEST(test_pressing_opt_twice_cancels);
|
||||||
RUN_TEST(test_compose_state_is_visible_for_the_ui);
|
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();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user