From e00f189d568514f4948ae2a50aac6b3117eef563 Mon Sep 17 00:00:00 2001 From: Marco Herrn Date: Thu, 16 May 2024 23:59:28 +0200 Subject: [PATCH] Introduce new LongPress plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit provides a new plugin “LongPress” that allows producing different Keys when keys are held for a short time instead of only tapped. It is based on the existing “AutoShift” plugin and contains its functionality, but extends it for a broader area of application. Signed-off-by: Marco Herrn --- examples/Keystrokes/LongPress/LongPress.ino | 92 ++++++ examples/Keystrokes/LongPress/sketch.json | 6 + examples/Keystrokes/LongPress/sketch.yaml | 1 + plugins/Kaleidoscope-LongPress/README.md | 137 ++++++++ .../Kaleidoscope-LongPress/library.properties | 7 + .../src/Kaleidoscope-LongPress.h | 20 ++ .../src/kaleidoscope/plugin/LongPress.cpp | 256 +++++++++++++++ .../src/kaleidoscope/plugin/LongPress.h | 293 ++++++++++++++++++ .../kaleidoscope/plugin/LongPressAutoShift.h | 132 ++++++++ .../kaleidoscope/plugin/LongPressConfig.cpp | 116 +++++++ .../plugins/LongPress/autoshift/autoshift.ino | 60 ++++ tests/plugins/LongPress/autoshift/sketch.json | 6 + tests/plugins/LongPress/autoshift/sketch.yaml | 1 + tests/plugins/LongPress/autoshift/test.ktest | 134 ++++++++ tests/plugins/LongPress/basic/basic.ino | 81 +++++ tests/plugins/LongPress/basic/sketch.json | 6 + tests/plugins/LongPress/basic/sketch.yaml | 1 + tests/plugins/LongPress/basic/test.ktest | 200 ++++++++++++ 18 files changed, 1549 insertions(+) create mode 100644 examples/Keystrokes/LongPress/LongPress.ino create mode 100644 examples/Keystrokes/LongPress/sketch.json create mode 100644 examples/Keystrokes/LongPress/sketch.yaml create mode 100644 plugins/Kaleidoscope-LongPress/README.md create mode 100644 plugins/Kaleidoscope-LongPress/library.properties create mode 100644 plugins/Kaleidoscope-LongPress/src/Kaleidoscope-LongPress.h create mode 100644 plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.cpp create mode 100644 plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.h create mode 100644 plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPressAutoShift.h create mode 100644 plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPressConfig.cpp create mode 100644 tests/plugins/LongPress/autoshift/autoshift.ino create mode 100644 tests/plugins/LongPress/autoshift/sketch.json create mode 100644 tests/plugins/LongPress/autoshift/sketch.yaml create mode 100644 tests/plugins/LongPress/autoshift/test.ktest create mode 100644 tests/plugins/LongPress/basic/basic.ino create mode 100644 tests/plugins/LongPress/basic/sketch.json create mode 100644 tests/plugins/LongPress/basic/sketch.yaml create mode 100644 tests/plugins/LongPress/basic/test.ktest diff --git a/examples/Keystrokes/LongPress/LongPress.ino b/examples/Keystrokes/LongPress/LongPress.ino new file mode 100644 index 0000000000..2a42dd568d --- /dev/null +++ b/examples/Keystrokes/LongPress/LongPress.ino @@ -0,0 +1,92 @@ +// -*- mode: c++ -*- + +#include + +#include +#include +#include +#include +#include + +enum { + TOGGLE_LONGPRESS, +}; // macros + +enum { + QWERTY, +}; // layers + +// clang-format off +KEYMAPS( + [QWERTY] = KEYMAP_STACKED + ( + Key_NoKey, Key_1, Key_2, Key_3, Key_4, Key_5, Key_NoKey, + Key_Backtick, Key_Q, Key_W, Key_E, Key_R, Key_T, Key_Tab, + Key_PageUp, Key_A, Key_S, Key_D, Key_F, Key_G, + Key_PageDown, Key_Z, Key_X, Key_C, Key_V, Key_B, Key_Escape, + + Key_LeftControl, Key_Backspace, Key_LeftGui, Key_LeftShift, + XXX, + + M(TOGGLE_LONGPRESS), Key_6, Key_7, Key_8, Key_9, Key_0, Key_skip, + Key_Enter, Key_Y, Key_U, Key_I, Key_O, Key_P, Key_Equals, + Key_H, Key_J, Key_K, Key_L, Key_Semicolon, Key_Quote, + Key_skip, Key_N, Key_M, Key_Comma, Key_Period, Key_Slash, Key_Minus, + + Key_RightShift, Key_RightAlt, Key_Spacebar, Key_RightControl, + XXX + ), +) +// clang-format on + +// Defining a macro (on the "any" key: see above) to turn LongPress on and off +const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) { + switch (macro_id) { + case TOGGLE_LONGPRESS: + if (keyToggledOn(event.state)) + LongPress.toggle(); + break; + } + return MACRO_NONE; +} + +// This sketch uses the LongPressConfig plugin, which enables run-time +// configuration of LongPress configuration settings. All of the plugins marked +// "for LongPressConfig" are optional; LongPress itself will work without them. +KALEIDOSCOPE_INIT_PLUGINS( + EEPROMSettings, // for LongPressConfig + EEPROMKeymap, // for LongPressConfig + Focus, // for LongPressConfig + FocusEEPROMCommand, // for LongPressConfig + FocusSettingsCommand, // for LongPressConfig + LongPress, + LongPressConfig, // for LongPressConfig + Macros // for toggle LongPress Macro +); + +void setup() { + // Enable AutoShift for letter keys and number keys only: + LongPress.setAutoshiftEnabled(LongPress.letterKeys() | LongPress.numberKeys()); + // Add symbol keys to the enabled autoshift categories: + LongPress.enableAutoshift(LongPress.symbolKeys()); + + LONGPRESS( + // Long pressing the second key in the first row on the QWERTY layer + // produces a 0 instead of a 1 (and instead of Shift-1) + kaleidoscope::plugin::LongPressKey(QWERTY, KeyAddr(0, 1), Key_0), + + // instead of shifting, produce a backslash on long pressing slash on + // all layers + kaleidoscope::plugin::LongPressKey(Key_Slash, Key_Backslash), ) + + // Set the LongPress trigger time to 150ms: + LongPress.setTimeout(150); + // Start with LongPress turned off: + LongPress.disable(); + + Kaleidoscope.setup(); +} + +void loop() { + Kaleidoscope.loop(); +} diff --git a/examples/Keystrokes/LongPress/sketch.json b/examples/Keystrokes/LongPress/sketch.json new file mode 100644 index 0000000000..884ed009eb --- /dev/null +++ b/examples/Keystrokes/LongPress/sketch.json @@ -0,0 +1,6 @@ +{ + "cpu": { + "fqbn": "keyboardio:avr:model01", + "port": "" + } +} diff --git a/examples/Keystrokes/LongPress/sketch.yaml b/examples/Keystrokes/LongPress/sketch.yaml new file mode 100644 index 0000000000..9902e2986c --- /dev/null +++ b/examples/Keystrokes/LongPress/sketch.yaml @@ -0,0 +1 @@ +default_fqbn: keyboardio:avr:model01 diff --git a/plugins/Kaleidoscope-LongPress/README.md b/plugins/Kaleidoscope-LongPress/README.md new file mode 100644 index 0000000000..f8c8e94687 --- /dev/null +++ b/plugins/Kaleidoscope-LongPress/README.md @@ -0,0 +1,137 @@ +# LongPress + +LongPress allows you to type different characters when long-pressing a key +rather than tapping it. + +It is derived from the AutoShift plugin and integrates its functionality. When +using LongPress it supersedes the AutoShift plugin. They conflict with each +other and should not be used together. + + +## Setup + +To use the plugin put the following into your .ino file: + +```c++ +#include + +KALEIDOSCOPE_INIT_PLUGINS(LongPress); +``` + +## Configuration + +To do anything useful some configuration is necessary. + +### Long-press mappings + +To define the keys that should behave differently on long-press use include a definition like the following: + +```c++ +LONGPRESS( + kaleidoscope::plugin::LongPressKey( KeyAddr(1, 1), Key_Q), // Long-press the key on QWERTY position of “q” to enter a “q” + kaleidoscope::plugin::LongPressKey(QWERTY, KeyAddr(2, 4), LCTRL(Key_C)), // Long-press the key below the index finger to enter “Ctrl-C“ + kaleidoscope::plugin::LongPressKey( Key_T, RALT(Key_T)), // Long-press “t” to enter a “þ” +) +``` + +As can be seen in the example long-presses can be configured on either `KeyAddr` +or `Key`, even in combination. Which variant is preferred is based on the use +case. + +For example for mirroring the numbers in the number row (produce a “0” on long +pressing “1”, produce a “9” on long pressing “2”, etc.) the best approach is to +configure these on the `KeyAddr`. However to always generate an “ä” when “a” is +long-pressed, regardless of where the “a” is mapped on (and whether it is mapped +to different physical keys, probably on different layers), configuring it on the +`Key` may be preferable. + +Be aware, however, that the order of the entries in LONGPRESS matters! Ealier +definitions take precedence over later ones. Usually it is best to define +long-presses on `KeyAddr` first and long-presses on `Key` afterwards as that is +the least surprising behaviour in case of conflicting mappings. + +Another thing that can be seen in the example above is that such long presses can be restricted to a single layer (the second one is restricted to the QWERTY layer). This parameter is optional and if missing the entry applies to all layers. Alternatively to leaving out that parameter, the special value `ALL_LAYERS` may be used. So the following two entries are totally equivalent: + +```c++ +LONGPRESS( + kaleidoscope::plugin::LongPressKey( KeyAddr(1, 1), Key_Q), + kaleidoscope::plugin::LongPressKey(ALL_LAYERS, KeyAddr(1, 1), Key_Q), +) +``` + + +### Enabling and disabling LongPress + +The following methods are provided for enabling / disabling the plugin altogether: + +- `LongPress.enable()` to enable the plugin (after loading the plugin is enabled by default). +- `LongPress.disable()` to disable the plugin. +- `LongPress.toggle()` to switch the plugin between enabled and disabled state. +- `LongPress.enabled()` to check whether the plugin is currently enabled. + +### Setting the long-press delay + +To set the amount of time (in milliseconds) the LongPress plugin will wait until it executes the long-press behaviour use `LongPress.setTimeout(timeout)`. + +The default is 175: + +### Auto-Shifting + +One of the most common use cases for Long-Presses is auto-shifting of the generated character. +This use case has special support to avoid having to configure every single key. + +By default no auto-shifting behaviour is applied. To set this behaviour to some certain sets of keys use one of the following methods: + +- `LongPress.setAutoshiftEnabled(categories)` to activate auto-shifting for exactly the given categories. + To set multiple categories combine them using `|` (bitwise or), e.g.: `LongPress.setAutoshiftEnabled(LongPress.letterKeys() | LongPress.numberKeys())`. +- `LongPress.enableAutoshift(category)` to add a single category to be auto-shifted. +- `LongPress.disableAutoshift(category)` to remove a single category from the auto-shifted ones. +- `LongPress.isAutoshiftEnabled(category)` to check whether auto-shifting is enabled for the given category. +- `LongPress.enabledAutoShiftCategories()` to get an array of the categories for which auto-shifting is enabled. + +These are the predefined categories for auto-shifting: + +- `LongPress.noKeys()`: Can be used with `LongPress.setAutoshiftEnabled()` to remove all categories from being auto-shifted. +- `LongPress.letterKeys`: All letter keys. +- `LongPress.numberKeys`: All number keys (in the number row, not the numeric keypad). +- `LongPress.symbolKeys`: Other printable symbols. +- `LongPress.arrowKeys`: Navigational arrow keys. +- `LongPress.functionKeys`: All function keys (F1 – F24). +- `LongPress.printableKeys`: Letters, numbers and symbols. +- `LongPress.allKeys`: All non-modifier USB keyboard keys. + +If the above categories are not sufficient for your auto-shifting needs, it is +possible to get even finer-grained control of which keys are affected by +auto-shifting, by overriding the `isAutoShiftable()` method in your sketch. For +example, to make LongPress only auto-shift keys `A` and `Z`, include the following +code in your sketch: + +```c++ +bool LongPress::isAutoShiftable(Key key) { + if (key == Key_A || key == key_Z) + return true; + return false; +} +``` + +As you can see, this method takes a `Key` as its input and returns either +`true` (for keys eligible to be auto-shifted) or `false` (for keys to be left +alone). + +In contrast to the explict configuration of long-presses via `LongPressKey`, such auto-shift behaviour always applies to all layers. + +## Conflicts with other plugins + +Care should be taken when using the plugin together with the Qukeys, SpaceCadet +and Chords plugins. Most of the time they conflict with each other and when +using one of these plugins together with LongPress it should be avoided to +configure them on the same keys. + +In any case the LongPress plugin should be defined as the last one of these. + +## Further reading + +Starting from the [example][plugin:example] is the recommended way of getting +started with the plugin. + +[plugin:example]: /examples/Keystrokes/LongPress/LongPress.ino diff --git a/plugins/Kaleidoscope-LongPress/library.properties b/plugins/Kaleidoscope-LongPress/library.properties new file mode 100644 index 0000000000..6e5c29c194 --- /dev/null +++ b/plugins/Kaleidoscope-LongPress/library.properties @@ -0,0 +1,7 @@ +name=Kaleidoscope-LongPress +version=0.0.0 +sentence=Provide different key strokes on long press +maintainer=Kaleidoscope's Developers +url=https://github.com/keyboardio/Kaleidoscope +author=Marco Herrn +paragraph= diff --git a/plugins/Kaleidoscope-LongPress/src/Kaleidoscope-LongPress.h b/plugins/Kaleidoscope-LongPress/src/Kaleidoscope-LongPress.h new file mode 100644 index 0000000000..8846a236b7 --- /dev/null +++ b/plugins/Kaleidoscope-LongPress/src/Kaleidoscope-LongPress.h @@ -0,0 +1,20 @@ +/* -*- mode: c++ -*- + * Kaleidoscope-LongPress -- Provide different key strokes on long press + * Copyright (C) 2024 Keyboard.io, Inc + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include "kaleidoscope/plugin/LongPress.h" // IWYU pragma: export diff --git a/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.cpp b/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.cpp new file mode 100644 index 0000000000..55ebbb4b0d --- /dev/null +++ b/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.cpp @@ -0,0 +1,256 @@ +/* -*- mode: c++ -*- + * Kaleidoscope-LongPress -- Provide different key strokes on long press + * Copyright (C) 2024 Keyboard.io, Inc + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include "kaleidoscope/plugin/LongPress.h" + +#include "kaleidoscope/KeyAddr.h" // for KeyAddr, MatrixAddr +#include "kaleidoscope/KeyEvent.h" // for KeyEvent +#include "kaleidoscope/KeyEventTracker.h" // for KeyEventTracker +#include "kaleidoscope/Runtime.h" // for Runtime, Runtime_ +#include "kaleidoscope/key_defs.h" // for Key, Key_0, Key_1, Key_A, Key_F1, Key_F12, Key... +#include "kaleidoscope/keyswitch_state.h" // for keyToggledOn, keyIsInjected +#include "kaleidoscope/progmem_helpers.h" // for cloneFromProgmem + +// IWYU pragma: no_include "HIDAliases.h" + +namespace kaleidoscope { +namespace plugin { + + +// ============================================================================= +// LongPress functions + +void LongPress::disable() { + settings_.enabled = false; + if (pending_event_.addr.isValid()) { + Runtime.handleKeyswitchEvent(pending_event_); + } +} + +// ----------------------------------------------------------------------------- +// Test for whether or not to apply auto-shift to a given `Key`. This function +// can be overridden from the user sketch. +__attribute__((weak)) bool LongPress::isAutoShiftable(Key key) { + return autoShiftEnabledForKey(key); +} + +// The default method that determines whether a particular key is an auto-shift +// candidate. Used by `isAutoShiftable()`, separate to allow re-use when the +// caller is overridden. +bool LongPress::autoShiftEnabledForKey(Key key) { + // We only support auto-shifting keyboard keys. We could also explicitly + // ignore modifier keys, but there's no need to do so, as none of the code + // below matches modifiers. + if (!key.isKeyboardKey()) + return false; + + // We compare only the keycode, and disregard any modifier flags applied to + // the key. This simplifies the comparison, and also allows AutoShift to + // apply to keys like `RALT(Key_E)`. + uint8_t keycode = key.getKeyCode(); + + if (isAutoshiftEnabled(longpress::AutoShiftCategories::allKeys())) { + if (keycode < HID_KEYBOARD_FIRST_MODIFIER) + return true; + } + if (isAutoshiftEnabled(longpress::AutoShiftCategories::letterKeys())) { + if (keycode >= Key_A.getKeyCode() && keycode <= Key_Z.getKeyCode()) + return true; + } + if (isAutoshiftEnabled(longpress::AutoShiftCategories::numberKeys())) { + if (keycode >= Key_1.getKeyCode() && keycode <= Key_0.getKeyCode()) + return true; + } + if (isAutoshiftEnabled(longpress::AutoShiftCategories::symbolKeys())) { + if ((keycode >= Key_Minus.getKeyCode() && keycode <= Key_Slash.getKeyCode()) || + (keycode == Key_NonUsBackslashAndPipe.getKeyCode())) + return true; + } + if (isAutoshiftEnabled(longpress::AutoShiftCategories::arrowKeys())) { + if (keycode >= Key_RightArrow.getKeyCode() && + keycode <= Key_LeftArrow.getKeyCode()) + return true; + } + if (isAutoshiftEnabled(longpress::AutoShiftCategories::functionKeys())) { + if ((keycode >= Key_F1.getKeyCode() && keycode <= Key_F12.getKeyCode()) || + (keycode >= Key_F13.getKeyCode() && keycode <= Key_F24.getKeyCode())) + return true; + } + + return false; +} + +// ============================================================================= +// Event handler hook functions + +// ----------------------------------------------------------------------------- +EventHandlerResult LongPress::onKeyswitchEvent(KeyEvent &event) { + // If LongPress has already processed and released this event, ignore it. + // There's no need to update the event tracker in this one case. + if (event_tracker_.shouldIgnore(event)) { + // We should never get an event that's in our queue here, but just in case + // some other plugin sends one, abort. + if (queue_.shouldAbort(event)) + return EventHandlerResult::ABORT; + return EventHandlerResult::OK; + } + + // If event.addr is not a physical key, ignore it; some other plugin injected + // it. This check should be unnecessary. + if (!event.addr.isValid() || keyIsInjected(event.state)) { + return EventHandlerResult::OK; + } + + // Do nothing if disabled. + if (!settings_.enabled) + return EventHandlerResult::OK; + + if (!queue_.isEmpty()) { + // There's an unresolved LongPress key press. + if (keyToggledOn(event.state) || + event.addr == queue_.addr(0) || + queue_.isFull()) { + // If a new key toggled on, the unresolved key toggled off (it was a + // "tap"), or if the queue is full, we clear the queue, and the key event + // does not get modified. + flushEvent(false); + flushQueue(); + } else { + // Otherwise, add the release event to the queue. We do this so that + // rollover from a modifier to an auto-shifted key will result in the + // modifier being applied to the key. + queue_.append(event); + return EventHandlerResult::ABORT; + } + } + + if (keyToggledOn(event.state) && + (isExplicitlyMapped(event.addr, event.key) || isAutoShiftable(event.key))) { + // The key is explicitly configured for long presses or is eligible to + // be auto-shifted, so we add it to the queue and defer processing of + // the event. + queue_.append(event); + return EventHandlerResult::ABORT; + } + + return EventHandlerResult::OK; +} + + +bool LongPress::isExplicitlyMapped(KeyAddr addr, Key key) { + // check the active layer as mappings may be active only for certain layers + uint8_t active_layer = Layer.lookupActiveLayer(addr); + + // Check whether the given physical KeyAddr or logical Key has an + // explicit mapping to a logical one for the current layer + for (uint8_t i{0}; i < explicitmappings_count_; ++i) { + LongPressKey mappedKey = cloneFromProgmem(explicitmappings_[i]); + + // don’t consider the mapping if it does not apply to the current or all layers + if (mappedKey.layer != ALL_LAYERS && mappedKey.layer != active_layer) { + continue; + } + + // cache the mapped key to not have to search it again + + if (mappedKey.addr == addr) { + // check for a mapping on a KeyAddr + mapped_key_.addr = mappedKey.addr; + mapped_key_.key = Key_Transparent; + mapped_key_.longpress_result = mappedKey.longpress_result; + return true; + } else if (mappedKey.key == key) { + // then check for a mapping a o Key + mapped_key_.addr = KeyAddr::none(); + mapped_key_.key = mappedKey.key; + mapped_key_.longpress_result = mappedKey.longpress_result; + return true; + } + } + + // If no matches were found, clear mapped_key_ and return false + mapped_key_.addr = KeyAddr::none(); + mapped_key_.key = Key_Transparent; + mapped_key_.longpress_result = Key_Transparent; + return false; +} + + +// ----------------------------------------------------------------------------- +EventHandlerResult LongPress::afterEachCycle() { + // If there's a pending LongPress event, and it has timed out, we need to + // release the event with the `shift` flag applied. + if (!queue_.isEmpty() && + Runtime.hasTimeExpired(queue_.timestamp(0), settings_.timeout)) { + // Toggle the state of the `SHIFT_HELD` bit in the modifier flags for the + // key for the pending event. + flushEvent(true); + flushQueue(); + } + return EventHandlerResult::OK; +} + +void LongPress::flushQueue() { + while (!queue_.isEmpty()) { + if (queue_.isRelease(0) || checkForRelease()) { + flushEvent(false); + } else { + return; + } + } +} + +bool LongPress::checkForRelease() const { + KeyAddr queue_head_addr = queue_.addr(0); + for (uint8_t i = 1; i < queue_.length(); ++i) { + if (queue_.addr(i) == queue_head_addr) { + // This key's release is also in the queue + return true; + } + } + return false; +} + +void LongPress::flushEvent(bool is_long_press) { + if (queue_.isEmpty()) { + return; + } + + KeyEvent event = queue_.event(0); + if (is_long_press) { + if (mapped_key_.addr != KeyAddr::none()) { + // If we have an explicit mapping for that physical key, apply that. + event.key = mapped_key_.longpress_result; + } else if (mapped_key_.key != Key_Transparent) { + // If we have an explicit mapping for that logical key, apply that. + event.key = mapped_key_.longpress_result; + } else { + // If there was no explicit mapping, just add the shift modifier + event.key = Runtime.lookupKey(event.addr); + uint8_t flags = event.key.getFlags(); + flags ^= SHIFT_HELD; + event.key.setFlags(flags); + } + } + queue_.shift(); + Runtime.handleKeyswitchEvent(event); +} + +} // namespace plugin +} // namespace kaleidoscope + +kaleidoscope::plugin::LongPress LongPress; diff --git a/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.h b/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.h new file mode 100644 index 0000000000..b7d4062aa3 --- /dev/null +++ b/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.h @@ -0,0 +1,293 @@ +/* -*- mode: c++ -*- + * Kaleidoscope-LongPress -- Provide different key strokes on long press + * Copyright (C) 2024 Keyboard.io, Inc + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include // for uint8_t, uint16_t + +#include "kaleidoscope/KeyAddrEventQueue.h" // for KeyAddrEventQueue +#include "kaleidoscope/KeyEvent.h" // for KeyEvent +#include "kaleidoscope/KeyEventTracker.h" // for KeyEventTracker +#include "kaleidoscope/event_handler_result.h" // for EventHandlerResult +#include "kaleidoscope/key_defs.h" // for Key +#include "kaleidoscope/plugin.h" // for Plugin +#include "kaleidoscope/layers.h" // for ALL_LAYERS + +#include "kaleidoscope/plugin/LongPressAutoShift.h" // for longpress::AutoShiftCategories + +// A constant to be used as a placeholder for the layer number in case the +// long-press behaviour should act on all layers. +static constexpr uint8_t ALL_LAYERS = -1; + +namespace kaleidoscope { +namespace plugin { + +// A mapping for a long-press configuration. Either a KeyAddr or a Key (but +// not both) must be given as the source of the event and a Key as the +// result of a long-press. A specific layer may be specified to restrict +// the long-press behaviour to that layer. +struct LongPressKey { + // The layer to react on to this long press + uint8_t layer; + // The physical key that should result in a different value on long press. + KeyAddr addr; + // The logical key that should result in a different value on long press. + Key key; + // The alternate logical Key value that should be produced on long press. + Key longpress_result; + + // This is the constructor that should be used when creating a + // LongPressKey object for a physical KeyAddr in the PROGMEM array + // that will be used by explicit mappings (i.e. in the `LONGPRESS()` + // macro) on all layers (a shortcut for specifying ALL_LAYERS). + constexpr LongPressKey(KeyAddr addr, Key longpress_result) + : layer(ALL_LAYERS), addr(addr), key(Key_Transparent), longpress_result(longpress_result) {} + + // This is the constructor that should be used when creating a + // LongPressKey object for a physical KeyAddr in the PROGMEM array + // that will be used by explicit mappings (i.e. in the `LONGPRESS()` + // macro) on a specific layer. + constexpr LongPressKey(uint8_t layer, KeyAddr addr, Key longpress_result) + : layer(layer), addr(addr), key(Key_Transparent), longpress_result(longpress_result) {} + + // This is the constructor that should be used when creating a + // LongPressKey object for a logical Key in the PROGMEM array + // that will be used by explicit mappings (i.e. in the `LONGPRESS()` + // macro) on all layers (a shortcut for specifying ALL_LAYERS). + constexpr LongPressKey(Key key, Key longpress_result) + : layer(ALL_LAYERS), addr(KeyAddr::none()), key(key), longpress_result(longpress_result) {} + + // This is the constructor that should be used when creating a + // LongPressKey object for a logical Key in the PROGMEM array + // that will be used by explicit mappings (i.e. in the `LONGPRESS()` + // macro) on a specific layer. + constexpr LongPressKey(uint8_t layer, Key key, Key longpress_result) + : layer(layer), addr(KeyAddr::none()), key(key), longpress_result(longpress_result) {} + + // This constructor is here so that we can create an empty LongPressKey object in RAM + // into which we can copy the values from a PROGMEM LongPressKey object. + LongPressKey() = default; +}; + +// ============================================================================= +/// Kaleidoscope plugin for long-press keys +/// +/// This plugin allows the user to "long-press" keys to produce different +/// key strokes than on normal key presses. +/// Shortcut methods are provided to generate shifted letters on long-press +/// without having to configure each letter explicitly. +class LongPress : public Plugin { + + public: + // --------------------------------------------------------------------------- + // This lets the LongPressConfig plugin access the internal config variables + // directly. Mainly useful for calls to `Runtime.storage.get()`. + friend class LongPressConfig; + + // --------------------------------------------------------------------------- + // Configuration functions + + /// Returns `true` if LongPress is active, `false` otherwise + bool enabled() { + return settings_.enabled; + } + /// Activates the LongPress plugin (held keys will produce different Keys) + void enable() { + settings_.enabled = true; + } + /// Deactivates the LongPress plugin (held keys will not produce different Keys) + void disable(); + /// Turns LongPress on if it's off, and vice versa + void toggle() { + if (settings_.enabled) { + disable(); + } else { + enable(); + } + } + + /// Returns the hold time required to trigger long press (in ms) + uint16_t timeout() { + return settings_.timeout; + } + /// Sets the hold time required to trigger long press (in ms) + void setTimeout(uint16_t new_timeout) { + settings_.timeout = new_timeout; + } + + /// Returns the set of categories currently eligible for auto-shift + longpress::AutoShiftCategories enabledAutoShiftCategories() { + return settings_.enabled_categories; + } + + /// Adds `category` to the set eligible for auto-shift + /// + /// Possible values for `category` are: + /// - `longpress::AutoShiftCategories::noKeys()` + /// - `longpress::AutoShiftCategories::letterKeys()` + /// - `longpress::AutoShiftCategories::numberKeys()` + /// - `longpress::AutoShiftCategories::symbolKeys()` + /// - `longpress::AutoShiftCategories::arrowKeys()` + /// - `longpress::AutoShiftCategories::functionKeys()` + /// - `longpress::AutoShiftCategories::printableKeys()` + /// - `longpress::AutoShiftCategories::allKeys()` + void enableAutoshift(longpress::AutoShiftCategories category) { + settings_.enabled_categories.add(category); + } + /// Removes a `Key` category from the set eligible for auto-shift + void disableAutoshift(longpress::AutoShiftCategories category) { + settings_.enabled_categories.remove(category); + } + /// Replaces the list of `Key` categories eligible for auto-shift + void setAutoshiftEnabled(longpress::AutoShiftCategories categories) { + settings_.enabled_categories = categories; + } + /// Returns `true` if the given category is eligible for auto-shift + bool isAutoshiftEnabled(longpress::AutoShiftCategories category) { + return settings_.enabled_categories.contains(category); + } + + /// The category representing no keys + static constexpr longpress::AutoShiftCategories noKeys() { + return longpress::AutoShiftCategories::noKeys(); + } + /// The category representing letter keys + static constexpr longpress::AutoShiftCategories letterKeys() { + return longpress::AutoShiftCategories::letterKeys(); + } + /// The category representing number keys (on the number row) + static constexpr longpress::AutoShiftCategories numberKeys() { + return longpress::AutoShiftCategories::numberKeys(); + } + /// The category representing other printable symbol keys + static constexpr longpress::AutoShiftCategories symbolKeys() { + return longpress::AutoShiftCategories::symbolKeys(); + } + /// The category representing arrow keys + static constexpr longpress::AutoShiftCategories arrowKeys() { + return longpress::AutoShiftCategories::arrowKeys(); + } + /// The category representing function keys + static constexpr longpress::AutoShiftCategories functionKeys() { + return longpress::AutoShiftCategories::functionKeys(); + } + /// Letters, numbers, and other symbols + static constexpr longpress::AutoShiftCategories printableKeys() { + return longpress::AutoShiftCategories::printableKeys(); + } + /// All non-modifier keyboard keys + static constexpr longpress::AutoShiftCategories allKeys() { + return longpress::AutoShiftCategories::allKeys(); + } + + // --------------------------------------------------------------------------- + /// Determines which keys will trigger auto-shift if held long enough + /// + /// This function can be overridden by the user sketch to configure which keys + /// can trigger auto-shift. + bool isAutoShiftable(Key key); + + // --------------------------------------------------------------------------- + // Event handlers + EventHandlerResult onKeyswitchEvent(KeyEvent &event); + EventHandlerResult afterEachCycle(); + + template + void configureLongPresses(LongPressKey const (&explicitmappings)[_explicitmappings_count]) { + explicitmappings_ = explicitmappings; + explicitmappings_count_ = _explicitmappings_count; + } + + private: + // --------------------------------------------------------------------------- + /// A container for LongPress configuration settings + struct Settings { + /// The overall state of the plugin (on/off) + bool enabled = true; + /// The length of time (ms) a key must be held to trigger a long press + uint16_t timeout = 175; + /// The set of `Key` categories eligible to be auto-shifted + longpress::AutoShiftCategories enabled_categories = longpress::AutoShiftCategories::noKeys(); + }; + Settings settings_; + + // --------------------------------------------------------------------------- + // Key event queue state variables + + // A device for processing only new events + KeyEventTracker event_tracker_; + + // The maximum number of events in the queue at a time. + static constexpr uint8_t queue_capacity_{4}; + + // The event queue stores a series of press and release events. + KeyAddrEventQueue queue_; + + // If there's a delayed keypress from LongPress, this stored event will + // contain a valid `KeyAddr`. The default constructor produces an event addr + // of `KeyAddr::none()`, so the plugin will start in an inactive state. + KeyEvent pending_event_; + + void flushQueue(); + void flushEvent(bool is_long_press = false); + bool checkForRelease() const; + + /// The default function for `isAutoShiftable()` + bool autoShiftEnabledForKey(Key key); + + /// Checks whether an explicit long-press mapping exists for either the + /// given `addr` or `key`. + bool isExplicitlyMapped(KeyAddr addr, Key key); + + // An array of LongPressKey objects in PROGMEM. + LongPressKey const *explicitmappings_{nullptr}; + uint8_t explicitmappings_count_{0}; + + // A cache of the current explicit config key values, so we + // don't have to keep looking them up from PROGMEM. + struct { + KeyAddr addr{KeyAddr::none()}; + Key key{Key_Transparent}; + Key longpress_result{Key_Transparent}; + } mapped_key_; +}; + +// ============================================================================= +/// Configuration plugin for persistent storage of settings +class LongPressConfig : public Plugin { + public: + EventHandlerResult onSetup(); + EventHandlerResult onFocusEvent(const char *input); + void disableLongPressIfUnconfigured(); + + private: + // The base address in persistent storage for configuration data + uint16_t settings_base_; +}; + +} // namespace plugin +} // namespace kaleidoscope + +extern kaleidoscope::plugin::LongPress LongPress; +extern kaleidoscope::plugin::LongPressConfig LongPressConfig; + +#define LONGPRESS(longpress_defs...) \ + { \ + static kaleidoscope::plugin::LongPressKey const longpress_defs_[] PROGMEM = { \ + longpress_defs}; \ + LongPress.configureLongPresses(longpress_defs_); \ + } diff --git a/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPressAutoShift.h b/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPressAutoShift.h new file mode 100644 index 0000000000..f316c8e697 --- /dev/null +++ b/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPressAutoShift.h @@ -0,0 +1,132 @@ + +/* -*- mode: c++ -*- + * Kaleidoscope-LongPress -- Provide different key strokes on long press + * Copyright (C) 2024 Keyboard.io, Inc + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include // for uint8_t, uint16_t + +#include "kaleidoscope/KeyAddrEventQueue.h" // for KeyAddrEventQueue +#include "kaleidoscope/KeyEvent.h" // for KeyEvent +#include "kaleidoscope/KeyEventTracker.h" // for KeyEventTracker +#include "kaleidoscope/event_handler_result.h" // for EventHandlerResult +#include "kaleidoscope/key_defs.h" // for Key +#include "kaleidoscope/plugin.h" // for Plugin + +namespace kaleidoscope { +namespace plugin { +namespace longpress { + +// --------------------------------------------------------------------------- +// Inner class for `Key` categories that can be configured to be auto-shifted +// by long-pressing. Most of this class is purely internal, but user code +// that enables or disables these auto-shift categories might use the +// following as constants: +// +// - `longpress::AutoShiftCategories::noKeys()` +// - `longpress::AutoShiftCategories::letterKeys()` +// - `longpress::AutoShiftCategories::numberKeys()` +// - `longpress::AutoShiftCategories::symbolKeys()` +// - `longpress::AutoShiftCategories::arrowKeys()` +// - `longpress::AutoShiftCategories::functionKeys()` +// - `longpress::AutoShiftCategories::printableKeys()` +// - `longpress::AutoShiftCategories::allKeys()` +// +// The first two ("letter keys" and "number keys") are self-explanatory. The +// third ("symbol keys") includes keys that produce symbols other than letters +// and numbers, but not whitespace characters, modifiers, et cetera. We could +// perhaps add another category for function keys. +// +// As these methods are delegated to in the LongPress plugin itself there +// should be no need to call these directly from client code. +class AutoShiftCategories { + private: + // raw bitfield data + uint8_t raw_bits_{0}; + + // constants for bits in the `raw_bits_` bitfield + static constexpr uint8_t NONE = 0 << 0; + static constexpr uint8_t LETTERS = 1 << 0; + static constexpr uint8_t NUMBERS = 1 << 1; + static constexpr uint8_t SYMBOLS = 1 << 2; + static constexpr uint8_t ARROWS = 1 << 3; + static constexpr uint8_t FUNCTIONS = 1 << 4; + static constexpr uint8_t ALL = 1 << 7; + + public: + // Basic un-checked constructor + explicit constexpr AutoShiftCategories(uint8_t raw_bits) + : raw_bits_(raw_bits) {} + + static constexpr AutoShiftCategories noKeys() { + return AutoShiftCategories(NONE); + } + static constexpr AutoShiftCategories letterKeys() { + return AutoShiftCategories(LETTERS); + } + static constexpr AutoShiftCategories numberKeys() { + return AutoShiftCategories(NUMBERS); + } + static constexpr AutoShiftCategories symbolKeys() { + return AutoShiftCategories(SYMBOLS); + } + static constexpr AutoShiftCategories arrowKeys() { + return AutoShiftCategories(ARROWS); + } + static constexpr AutoShiftCategories functionKeys() { + return AutoShiftCategories(FUNCTIONS); + } + static constexpr AutoShiftCategories printableKeys() { + return AutoShiftCategories(LETTERS | NUMBERS | SYMBOLS); + } + static constexpr AutoShiftCategories allKeys() { + return AutoShiftCategories(ALL); + } + + constexpr void set(uint8_t raw_bits) { + raw_bits_ = raw_bits; + } + constexpr void add(AutoShiftCategories categories) { + this->raw_bits_ |= categories.raw_bits_; + } + constexpr void remove(AutoShiftCategories categories) { + this->raw_bits_ &= ~(categories.raw_bits_); + } + constexpr bool contains(AutoShiftCategories categories) const { + return (this->raw_bits_ & categories.raw_bits_) != 0; + // More correct test: + // return (~(this->raw_bits_) & categories.raw_bits_) == 0; + } + + // Shorthand for combining categories: + // e.g. `AutoShiftCategories::letterKeys() | AutoShiftCategories::numberKeys()` + constexpr AutoShiftCategories operator|(AutoShiftCategories other) const { + return AutoShiftCategories(this->raw_bits_ | other.raw_bits_); + } + + // A conversion to integer is provided for the sake of interactions with the + // Focus plugin. + explicit constexpr operator uint8_t() { + return raw_bits_; + } +}; +} // namespace longpress + +extern longpress::AutoShiftCategories AutoShiftCategories; + +} // namespace plugin +} // namespace kaleidoscope diff --git a/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPressConfig.cpp b/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPressConfig.cpp new file mode 100644 index 0000000000..c0db0d9eff --- /dev/null +++ b/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPressConfig.cpp @@ -0,0 +1,116 @@ +/* -*- mode: c++ -*- + * Kaleidoscope-LongPress -- Provide different key strokes on long press + * Copyright (C) 2024 Keyboard.io, Inc + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include "kaleidoscope/plugin/LongPress.h" // IWYU pragma: associated + +#include // for PSTR +#include // for EEPROMSettings +#include // for Focus, FocusSerial +#include // for uint8_t, uint16_t + +#include "kaleidoscope/Runtime.h" // for Runtime, Runtime_ +#include "kaleidoscope/device/device.h" // for VirtualProps::Storage, Base<>::Storage +#include "kaleidoscope/event_handler_result.h" // for EventHandlerResult, EventHandlerResult::OK + +namespace kaleidoscope { +namespace plugin { + +// ============================================================================= +// LongPress configurator + +EventHandlerResult LongPressConfig::onSetup() { + ::EEPROMSettings.requestSliceAndLoadData(&settings_base_, &::LongPress.settings_); + return EventHandlerResult::OK; +} + +void LongPressConfig::disableLongPressIfUnconfigured() { + if (Runtime.storage().isSliceUninitialized(settings_base_, sizeof(LongPress::settings_))) + ::LongPress.disable(); +} + +EventHandlerResult LongPressConfig::onFocusEvent(const char *input) { + enum { + ENABLED, + TIMEOUT, + AUTOSHIFT, + } subCommand; + + const char *cmd_enabled = PSTR("longpress.enabled"); + const char *cmd_timeout = PSTR("longpress.timeout"); + const char *cmd_autoshift = PSTR("longpress.autoshift"); + + if (::Focus.inputMatchesHelp(input)) + return ::Focus.printHelp(cmd_enabled, cmd_timeout, cmd_autoshift); + + if (::Focus.inputMatchesCommand(input, cmd_enabled)) + subCommand = ENABLED; + else if (::Focus.inputMatchesCommand(input, cmd_timeout)) + subCommand = TIMEOUT; + else if (::Focus.inputMatchesCommand(input, cmd_autoshift)) + subCommand = AUTOSHIFT; + else + return EventHandlerResult::OK; + + switch (subCommand) { + case ENABLED: + if (::Focus.isEOL()) { + ::Focus.send(::LongPress.enabled()); + } else { + uint8_t v; + ::Focus.read(v); + // if (::Focus.readUint8() != 0) { + if (v != 0) { + ::LongPress.enable(); + } else { + ::LongPress.disable(); + } + } + break; + + case TIMEOUT: + if (::Focus.isEOL()) { + ::Focus.send(::LongPress.timeout()); + } else { + uint16_t t; + ::Focus.read(t); + // auto t = ::Focus.readUint16(); + ::LongPress.setTimeout(t); + } + break; + + case AUTOSHIFT: + if (::Focus.isEOL()) { + ::Focus.send(uint8_t(::LongPress.enabledAutoShiftCategories())); + } else { + uint8_t v; + ::Focus.read(v); + auto autoshift = longpress::AutoShiftCategories(v); + // auto autoshift = longpress::AutoShiftCategories(::Focus.readUint8()); + ::LongPress.setAutoshiftEnabled(autoshift); + } + break; + } + + Runtime.storage().put(settings_base_, ::LongPress.settings_); + Runtime.storage().commit(); + return EventHandlerResult::EVENT_CONSUMED; +} + +} // namespace plugin +} // namespace kaleidoscope + +kaleidoscope::plugin::LongPressConfig LongPressConfig; diff --git a/tests/plugins/LongPress/autoshift/autoshift.ino b/tests/plugins/LongPress/autoshift/autoshift.ino new file mode 100644 index 0000000000..9a422474a5 --- /dev/null +++ b/tests/plugins/LongPress/autoshift/autoshift.ino @@ -0,0 +1,60 @@ +/* -*- mode: c++ -*- + * Copyright (C) 2021 Keyboard.io, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include +#include + +// *INDENT-OFF* +KEYMAPS( + [0] = KEYMAP_STACKED + ( + Key_LeftShift, Key_RightShift, ___, ___, ___, ___, ___, + Key_A, Key_B, Key_B, ___, ___, ___, ___, + Key_1, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, + ___, + + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, + ___ + ), +) +// *INDENT-ON* + +KALEIDOSCOPE_INIT_PLUGINS(LongPress); + +void setup() { + Kaleidoscope.setup(); + LongPress.setTimeout(20); + LongPress.setAutoshiftEnabled(LongPress.letterKeys()); + + LONGPRESS( + // ATTENTION! The order matters here! The first matching entry wins. + + // overrides the long-press for a logical Key (in the next line) + kaleidoscope::plugin::LongPressKey(KeyAddr(1, 2), Key_Z), + // overrides the auto-shift functionality + kaleidoscope::plugin::LongPressKey(Key_B, Key_Y), + ) +} + +void loop() { + Kaleidoscope.loop(); +} diff --git a/tests/plugins/LongPress/autoshift/sketch.json b/tests/plugins/LongPress/autoshift/sketch.json new file mode 100644 index 0000000000..8cc869221a --- /dev/null +++ b/tests/plugins/LongPress/autoshift/sketch.json @@ -0,0 +1,6 @@ +{ + "cpu": { + "fqbn": "keyboardio:virtual:model01", + "port": "" + } +} diff --git a/tests/plugins/LongPress/autoshift/sketch.yaml b/tests/plugins/LongPress/autoshift/sketch.yaml new file mode 100644 index 0000000000..4d94810065 --- /dev/null +++ b/tests/plugins/LongPress/autoshift/sketch.yaml @@ -0,0 +1 @@ +default_fqbn: keyboardio:virtual:model01 diff --git a/tests/plugins/LongPress/autoshift/test.ktest b/tests/plugins/LongPress/autoshift/test.ktest new file mode 100644 index 0000000000..8cf62bf279 --- /dev/null +++ b/tests/plugins/LongPress/autoshift/test.ktest @@ -0,0 +1,134 @@ +VERSION 1 + +KEYSWITCH LSHIFT 0 0 +KEYSWITCH RSHIFT 0 1 +KEYSWITCH A 1 0 +KEYSWITCH B 1 1 +KEYSWITCH B2 1 2 +KEYSWITCH 1 2 0 + +# ============================================================================== +NAME LongPress AutoShift tap +# This tests that short tapping any of the keys should always produce the +# normal key + +RUN 4 ms +PRESS A +RUN 1 cycle + +RUN 4 ms +RELEASE A +RUN 1 cycle +EXPECT keyboard-report Key_A # report: { 4 } +EXPECT keyboard-report empty + +RUN 4 ms +PRESS B +RUN 1 cycle + +RUN 4 ms +RELEASE B +RUN 1 cycle +EXPECT keyboard-report Key_B # report: { 5 } +EXPECT keyboard-report empty + +RUN 4 ms +PRESS B2 +RUN 1 cycle + +RUN 4 ms +RELEASE B2 +RUN 1 cycle +EXPECT keyboard-report Key_B # report: { 5 } +EXPECT keyboard-report empty + +RUN 4 ms +PRESS 1 +RUN 1 cycle +EXPECT keyboard-report Key_1 # report: { 1e } + +RUN 4 ms +RELEASE 1 +RUN 1 cycle +EXPECT keyboard-report empty + + +# ============================================================================== +NAME AutoShift long press +# This tests that a long-press on an autoshiftable key produces a shift + +# the key + +RUN 4 ms +PRESS A +RUN 1 cycle + +# Timeout is 20ms +RUN 20 ms +EXPECT keyboard-report Key_LeftShift # report: { e1 } +EXPECT keyboard-report Key_LeftShift Key_A # report: { 4 e1 } + +RUN 4 ms +RELEASE A +RUN 1 cycle +EXPECT keyboard-report Key_LeftShift # report: { e1 } +EXPECT keyboard-report empty + + +# ============================================================================== +NAME LongPress explicit on Key overrides AutoShift +# Test that an explicit configuration of a LongPress always overrides the +# auto-shift behaviour on the same key. + +RUN 4 ms +PRESS B +RUN 1 cycle + +# Timeout is 20ms +RUN 20 ms +EXPECT keyboard-report Key_Y # report: { 28 } + +RUN 4 ms +RELEASE B +RUN 1 cycle +EXPECT keyboard-report empty + + +# ============================================================================== +NAME LongPress explicit on Addr overrides LongPress explicit on Key +# This tests that an explicit configuration on a KeyAddr takes precedence +# over an explicit configuration on a Key that was defined later (the first +# matching LongPressKey entry wins). + +RUN 4 ms +PRESS B2 +RUN 1 cycle + +# Timeout is 20ms +RUN 20 ms +EXPECT keyboard-report Key_Z # report: { 29 } + +RUN 4 ms +RELEASE B2 +RUN 1 cycle +EXPECT keyboard-report empty + + +# ============================================================================== +NAME LongPress AutoShift no modification +# Test that long-pressing a number keys produces that number as no +# auto-shift was defined for number keys and no explicit long-press +# behaviour was configured for that key either + +RUN 4 ms +PRESS 1 +RUN 1 cycle +EXPECT keyboard-report Key_1 # report: { 1e } + +# Timeout is 20ms +RUN 20 ms + +RUN 4 ms +RELEASE 1 +RUN 1 cycle +EXPECT keyboard-report empty + diff --git a/tests/plugins/LongPress/basic/basic.ino b/tests/plugins/LongPress/basic/basic.ino new file mode 100644 index 0000000000..74f2e37806 --- /dev/null +++ b/tests/plugins/LongPress/basic/basic.ino @@ -0,0 +1,81 @@ +/* -*- mode: c++ -*- + * Copyright (C) 2021 Keyboard.io, Inc. + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#include +#include +#include + +// *INDENT-OFF* +KEYMAPS( + [0] = KEYMAP_STACKED + ( + Key_LeftShift, Key_RightShift, ___, ___, ___, ___, ShiftToLayer(1), + Key_A, Key_B, Key_C, Key_D, ___, ___, ___, + Key_E, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, + ___, + + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, + ___ + ), + + [1] = KEYMAP_STACKED + ( + ___, ___, ___, ___, ___, ___, ___, + Key_A, Key_B, Key_C, Key_D, ___, ___, ___, + ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, + ___, + + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, + ___ + ), +) +// *INDENT-ON* + +KALEIDOSCOPE_INIT_PLUGINS(LongPress); + +void setup() { + Kaleidoscope.setup(); + LongPress.setTimeout(20); + + // no auto-shift enabled + + LONGPRESS( + // Key at 1,0 should produce a Z on long press on all layers + kaleidoscope::plugin::LongPressKey( KeyAddr(1, 0), Key_Z), + // Keys generating a B should produce a Y on long press on all layers + kaleidoscope::plugin::LongPressKey( Key_B, Key_Y), + // Key at 1,2 should produce a X on long press on the first layer + kaleidoscope::plugin::LongPressKey(0, KeyAddr(1, 2), Key_X), + // Keys generating a D should produce a W on long press on the second layer + kaleidoscope::plugin::LongPressKey(1, Key_D, Key_W), + ) +} + +void loop() { + Kaleidoscope.loop(); +} diff --git a/tests/plugins/LongPress/basic/sketch.json b/tests/plugins/LongPress/basic/sketch.json new file mode 100644 index 0000000000..8cc869221a --- /dev/null +++ b/tests/plugins/LongPress/basic/sketch.json @@ -0,0 +1,6 @@ +{ + "cpu": { + "fqbn": "keyboardio:virtual:model01", + "port": "" + } +} diff --git a/tests/plugins/LongPress/basic/sketch.yaml b/tests/plugins/LongPress/basic/sketch.yaml new file mode 100644 index 0000000000..4d94810065 --- /dev/null +++ b/tests/plugins/LongPress/basic/sketch.yaml @@ -0,0 +1 @@ +default_fqbn: keyboardio:virtual:model01 diff --git a/tests/plugins/LongPress/basic/test.ktest b/tests/plugins/LongPress/basic/test.ktest new file mode 100644 index 0000000000..d4fe30ec68 --- /dev/null +++ b/tests/plugins/LongPress/basic/test.ktest @@ -0,0 +1,200 @@ +VERSION 1 + +KEYSWITCH LSHIFT 0 0 +KEYSWITCH RSHIFT 0 1 +KEYSWITCH A 1 0 +KEYSWITCH B 1 1 +KEYSWITCH C 1 2 +KEYSWITCH D 1 3 +KEYSWITCH E 2 0 +KEYSWITCH LAYER1 0 6 + +# ============================================================================== +NAME LongPress tap +# Check that a short tap produces the normal key value + +RUN 4 ms +PRESS A +RUN 1 cycle + +RUN 4 ms +RELEASE A +RUN 1 cycle +EXPECT keyboard-report Key_A # report: { 4 } +EXPECT keyboard-report empty + +RUN 4 ms +PRESS B +RUN 1 cycle + +RUN 4 ms +RELEASE B +RUN 1 cycle +EXPECT keyboard-report Key_B # report: { 5 } +EXPECT keyboard-report empty + + +# ============================================================================== +NAME LongPress explicit on Addr on all layers +# Check that a long pres configured on a KeyAddr on all layers produces the +# correct key value on all layers + +RUN 4 ms +PRESS A +RUN 1 cycle + +# Timeout is 20ms +RUN 20 ms +EXPECT keyboard-report Key_Z # report: { 29 } + +RUN 4 ms +RELEASE A +RUN 1 cycle +EXPECT keyboard-report empty + +# Now switch to the other layer and do the same + +RUN 4 ms +PRESS LAYER1 +PRESS A +RUN 1 cycle + +# Timeout is 20ms +RUN 20 ms +EXPECT keyboard-report Key_Z # report: { 29 } + +RUN 4 ms +RELEASE A +RELEASE LAYER1 +RUN 1 cycle +EXPECT keyboard-report empty + + +# ============================================================================== +NAME LongPress explicit on Key +# Check that a long pres configured on a Key on all layers produces the +# correct key value on all layers + +RUN 4 ms +PRESS B +RUN 1 cycle + +# Timeout is 20ms +RUN 20 ms +EXPECT keyboard-report Key_Y # report: { 28 } + +RUN 4 ms +RELEASE B +RUN 1 cycle +EXPECT keyboard-report empty + +# Now switch to the other layer and do the same + +RUN 4 ms +PRESS LAYER1 +PRESS B +RUN 1 cycle + +# Timeout is 20ms +RUN 20 ms +EXPECT keyboard-report Key_Y # report: { 28 } + +RUN 4 ms +RELEASE B +RELEASE LAYER1 +RUN 1 cycle +EXPECT keyboard-report empty + + +# ============================================================================== +NAME LongPress explicit on Addr on single layers +# Check that a long press configured on a KeyAddr on a single layers produces the +# correct key value only on that layer + +RUN 4 ms +PRESS C +RUN 1 cycle + +# Timeout is 20ms +RUN 20 ms +EXPECT keyboard-report Key_X # report: { 27 } + +RUN 4 ms +RELEASE C +RUN 1 cycle +EXPECT keyboard-report empty + +# Now switch to the other layer and do the same + +RUN 4 ms +PRESS LAYER1 +PRESS C +RUN 1 cycle +EXPECT keyboard-report Key_C # report: { 5 } + +# Timeout is 20ms +RUN 20 ms + +RUN 4 ms +RELEASE C +RELEASE LAYER1 +RUN 1 cycle +EXPECT keyboard-report empty + + +# ============================================================================== +NAME LongPress explicit on Key +# Check that a long press configured on a Key on all layers produces the +# correct key value on all layers + +# This key reacts to long-press on layer 1 + +RUN 4 ms +PRESS LAYER1 +PRESS D +RUN 1 cycle + +# Timeout is 20ms +RUN 20 ms +EXPECT keyboard-report Key_W # report: { 26 } + +RUN 4 ms +RELEASE D +RELEASE LAYER1 +RUN 1 cycle +EXPECT keyboard-report empty + +# Now switch to the other layer and do the same + +RUN 4 ms +PRESS D +RUN 1 cycle +EXPECT keyboard-report Key_D # report: { 6 } + +# Timeout is 20ms +RUN 20 ms + +RUN 4 ms +RELEASE D +RUN 1 cycle +EXPECT keyboard-report empty + + +# ============================================================================== +NAME LongPress no modification +# long-pressing a key for which no long-press behaviour was configured +# should only produce the normal key value + +RUN 4 ms +PRESS E +RUN 1 cycle +EXPECT keyboard-report Key_E # report: { 7 } + +# Timeout is 20ms +RUN 20 ms + +RUN 4 ms +RELEASE E +RUN 1 cycle +EXPECT keyboard-report empty +