diff --git a/examples/Keystrokes/LongPress/LongPress.ino b/examples/Keystrokes/LongPress/LongPress.ino new file mode 100644 index 0000000000..e644ce47fd --- /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(kaleidoscope::plugin::longpress::ALL_LAYERS, 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..85f352e228 --- /dev/null +++ b/plugins/Kaleidoscope-LongPress/README.md @@ -0,0 +1,148 @@ +# 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 include +a definition like the following: + +```c++ +LONGPRESS( + kaleidoscope::plugin::LongPressKey(kaleidoscope::plugin::longpress::ALL_LAYERS, + KeyAddr(1, 1), Key_Q), // Long-press the key on QWERTY position of “q” to enter a “q” on all layers + kaleidoscope::plugin::LongPressKey(QWERTY, + KeyAddr(2, 4), LCTRL(Key_C)), // Long-press the key below the index finger to enter “Ctrl-C“ on the QWERTY layer + kaleidoscope::plugin::LongPressKey(kaleidoscope::plugin::longpress::ALL_LAYERS, + Key_T, RALT(Key_T)), // Long-press “t” to enter a “þ” on all layers +) +``` + +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 long presses can be +restricted to a single layer (the second one is restricted to the QWERTY +layer). To apply the mapping to all layers, use the constant +`kaleidoscope::plugin::longpress::ALL_LAYERS`[^1] as can be seen the first and the +third mapping in the example above. + +### 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 + + +[^1] The constant `kaleidoscope::plugin::longpress::ALL_LAYERS` is a bit long +and unwieldy. Before integrating this plugin there were some discussions about +whether that is acceptable. Therefore people using that plugin to apply +mappings to all layers are kindly requested to provide some feedback about +their usage and whether they are annoyed by that long constant name or not. 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..0c6b04bc46 --- /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 != kaleidoscope::plugin::longpress::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..63da7b9b0b --- /dev/null +++ b/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPress.h @@ -0,0 +1,275 @@ +/* -*- 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/plugin/LongPressAutoShift.h" // for longpress::AutoShiftCategories +#include "kaleidoscope/plugin/LongPressConstants.h" // for ALL_LAYERS + +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). + 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). + 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..db47f721c5 --- /dev/null +++ b/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPressAutoShift.h @@ -0,0 +1,131 @@ +/* -*- 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/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPressConstants.h b/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPressConstants.h new file mode 100644 index 0000000000..421b2b28c4 --- /dev/null +++ b/plugins/Kaleidoscope-LongPress/src/kaleidoscope/plugin/LongPressConstants.h @@ -0,0 +1,31 @@ +/* -*- 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 + +namespace kaleidoscope { +namespace plugin { +namespace longpress { + +// 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 longpress +} // namespace plugin +} // namespace kaleidoscope diff --git a/tests/plugins/LongPress/autoshift/autoshift.ino b/tests/plugins/LongPress/autoshift/autoshift.ino new file mode 100644 index 0000000000..0a2e6816e3 --- /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(kaleidoscope::plugin::longpress::ALL_LAYERS, KeyAddr(1, 2), Key_Z), + // overrides the auto-shift functionality + kaleidoscope::plugin::LongPressKey(kaleidoscope::plugin::longpress::ALL_LAYERS, 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..8351378857 --- /dev/null +++ b/tests/plugins/LongPress/basic/basic.ino @@ -0,0 +1,85 @@ +/* -*- 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(kaleidoscope::plugin::longpress::ALL_LAYERS, + KeyAddr(1, 0), Key_Z), + // Keys generating a B should produce a Y on long press on all layers + kaleidoscope::plugin::LongPressKey(kaleidoscope::plugin::longpress::ALL_LAYERS, + 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 +