diff --git a/docs/NEWS.md b/docs/NEWS.md index b00e3872..56841b76 100644 --- a/docs/NEWS.md +++ b/docs/NEWS.md @@ -12,6 +12,13 @@ See [UPGRADING.md](UPGRADING.md) for more detailed instructions about upgrading ## New features +### SpaceCadet "no-delay" mode + +SpaceCadet can now be enabled in "no-delay" mode, wherein the primary (modifier) +value of the key will be sent to the host immediately when the key is pressed. +If the SpaceCadet key is released before the timeout, the modifier is released, +and then the alternate (symbol) value is sent. To activate "no-delay" mode, call `SpaceCadet.enableWithoutDelay()`. + ### New Qukeys features #### Tap-repeat diff --git a/plugins/Kaleidoscope-SpaceCadet/README.md b/plugins/Kaleidoscope-SpaceCadet/README.md index 4b6a0574..bae8ea0c 100644 --- a/plugins/Kaleidoscope-SpaceCadet/README.md +++ b/plugins/Kaleidoscope-SpaceCadet/README.md @@ -133,6 +133,13 @@ properties: > is disabled. This is useful for interfacing with other plugins or macros, > especially where SpaceCadet functionality isn't always desired. +### `.enableWithoutDelay()` + +> This method enables the SpaceCadet plugin in "no-delay" mode. In this mode, +> SpaceCadet immediately sends the primary (modifier) value of the SpaceCadet +> key when it is pressed. If it is then released before timing out, it sends the +> alternate "tap" value, replacing the modifier. + ### `Key_SpaceCadetEnable` > This provides a key for placing on a keymap for enabling the SpaceCadet diff --git a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.cpp b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.cpp index 6135c320..b3fd2edb 100644 --- a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.cpp +++ b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.cpp @@ -46,7 +46,7 @@ uint16_t SpaceCadet::time_out = 200; // ----------------------------------------------------------------------------- // State variables -bool SpaceCadet::disabled = false; +uint8_t SpaceCadet::mode_ = SpaceCadet::Mode::ON; // These variables are used to keep track of any pending unresolved SpaceCadet // key that has been pressed. If `pending_map_index_` is negative, it means @@ -78,23 +78,6 @@ SpaceCadet::SpaceCadet() { map = initialmap; } -// ----------------------------------------------------------------------------- -// Function to determine whether SpaceCadet is active (useful for Macros and -// other plugins). -bool SpaceCadet::active() { - return !disabled; -} - -// Function to enable SpaceCadet behavior -void SpaceCadet::enable() { - disabled = false; -} - -// Function to disable SpaceCadet behavior -void SpaceCadet::disable() { - disabled = true; -} - // ============================================================================= // Event handler hook functions @@ -134,7 +117,7 @@ EventHandlerResult SpaceCadet::onKeyswitchEvent(KeyEvent &event) { } // Do nothing if disabled, but keep the event tracker current. - if (disabled) + if (mode_ == Mode::OFF) return EventHandlerResult::OK; if (!event_queue_.isEmpty()) { @@ -162,7 +145,13 @@ EventHandlerResult SpaceCadet::onKeyswitchEvent(KeyEvent &event) { // Check for a SpaceCadet key pending_map_index_ = getSpaceCadetKeyIndex(event.key); if (pending_map_index_ >= 0) { - // A SpaceCadet key was pressed + // A SpaceCadet key has just toggled on. First, if we're in no-delay mode, + // we need to send the event unchanged (with the primary `Key` value), + // bypassing other `onKeyswitchEvent()` handlers. + if (mode_ == Mode::NO_DELAY) + Runtime.handleKeyEvent(event); + // Queue the press event and abort; this press event will be resolved + // later. event_queue_.append(event); return EventHandlerResult::ABORT; } @@ -211,6 +200,11 @@ void SpaceCadet::flushQueue() { void SpaceCadet::flushEvent(bool is_tap) { KeyEvent event = event_queue_.event(0); if (is_tap && pending_map_index_ >= 0) { + // If we're in no-delay mode, we should first send the release of the + // modifier key as a courtesy before sending the tap event. + if (mode_ == Mode::NO_DELAY) { + Runtime.handleKeyEvent(KeyEvent(event.addr, WAS_PRESSED)); + } event.key = map[pending_map_index_].output; } event_queue_.shift(); diff --git a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.h b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.h index 971bb540..3365d755 100644 --- a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.h +++ b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.h @@ -60,9 +60,18 @@ class SpaceCadet : public kaleidoscope::Plugin { SpaceCadet(void); // Methods - static void enable(void); - static void disable(void); - static bool active(void); + static void enable() { + mode_ = Mode::ON; + } + static void disable() { + mode_ = Mode::OFF; + } + static void enableWithoutDelay() { + mode_ = Mode::NO_DELAY; + } + static bool active() { + return (mode_ == Mode::ON || mode_ == Mode::NO_DELAY); + } // Publically accessible variables static uint16_t time_out; // The global timeout in milliseconds @@ -73,7 +82,12 @@ class SpaceCadet : public kaleidoscope::Plugin { EventHandlerResult afterEachCycle(); private: - static bool disabled; + enum Mode : uint8_t { + ON, + OFF, + NO_DELAY, + }; + static uint8_t mode_; static KeyEventTracker event_tracker_; diff --git a/tests/plugins/SpaceCadet/no-delay/no-delay.ino b/tests/plugins/SpaceCadet/no-delay/no-delay.ino new file mode 100644 index 00000000..7881a202 --- /dev/null +++ b/tests/plugins/SpaceCadet/no-delay/no-delay.ino @@ -0,0 +1,68 @@ +/* -*- 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, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, + ___, + + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, ___, ___, ___, + ___, ___, ___, ___, + ___ + ), +) +// *INDENT-ON* + +KALEIDOSCOPE_INIT_PLUGINS(SpaceCadet); + +void setup() { + Kaleidoscope.setup(); + + //Set the SpaceCadet map + //Setting is {KeyThatWasPressed, AlternativeKeyToSend, TimeoutInMS} + //Note: must end with the SPACECADET_MAP_END delimiter + static kaleidoscope::plugin::SpaceCadet::KeyBinding spacecadetmap[] = { + {Key_LeftShift, Key_X, 10}, + {Key_RightShift, Key_Y, 0}, + {Key_LeftGui, Key_LeftCurlyBracket, 10}, + {Key_RightAlt, Key_RightCurlyBracket, 10}, + {Key_LeftAlt, Key_RightCurlyBracket, 10}, + {Key_LeftControl, Key_LeftBracket, 10}, + {Key_RightControl, Key_RightBracket, 10}, + SPACECADET_MAP_END + }; + //Set the map. + SpaceCadet.map = spacecadetmap; + SpaceCadet.time_out = 20; + + SpaceCadet.enableWithoutDelay(); +} + +void loop() { + Kaleidoscope.loop(); +} diff --git a/tests/plugins/SpaceCadet/no-delay/sketch.json b/tests/plugins/SpaceCadet/no-delay/sketch.json new file mode 100644 index 00000000..8cc86922 --- /dev/null +++ b/tests/plugins/SpaceCadet/no-delay/sketch.json @@ -0,0 +1,6 @@ +{ + "cpu": { + "fqbn": "keyboardio:virtual:model01", + "port": "" + } +} diff --git a/tests/plugins/SpaceCadet/no-delay/test.ktest b/tests/plugins/SpaceCadet/no-delay/test.ktest new file mode 100644 index 00000000..ba35c59a --- /dev/null +++ b/tests/plugins/SpaceCadet/no-delay/test.ktest @@ -0,0 +1,141 @@ +VERSION 1 + +KEYSWITCH LSHIFT 0 0 +KEYSWITCH RSHIFT 0 1 +KEYSWITCH A 1 0 +KEYSWITCH B 1 1 + +# ============================================================================== +NAME SpaceCadet tap + +RUN 4 ms +PRESS LSHIFT +RUN 1 cycle +EXPECT keyboard-report Key_LeftShift # Report should contain `shift` (0xE1) + +RUN 4 ms +RELEASE LSHIFT +RUN 1 cycle +EXPECT keyboard-report empty # Report should be empty +EXPECT keyboard-report Key_X # Report should contain `X` (0x1B) +EXPECT keyboard-report empty # Report should be empty + +RUN 5 ms + +# ============================================================================== +NAME SpaceCadet hold + +RUN 4 ms +PRESS LSHIFT +RUN 1 cycle +EXPECT keyboard-report Key_LeftShift # Report should contain `shift` (0xE1) + +RUN 10 ms # timeout = 10 ms (for this key) +RELEASE LSHIFT +RUN 1 cycle +EXPECT keyboard-report empty # Report should be empty + +RUN 5 ms + +# ============================================================================== +NAME SpaceCadet hold with global timeout + +RUN 4 ms +PRESS RSHIFT +RUN 1 cycle +EXPECT keyboard-report Key_RightShift # Report should contain `shift` (0xE5) + +RUN 20 ms # timeout = 20 ms +RELEASE RSHIFT +RUN 1 cycle +EXPECT keyboard-report empty # Report should be empty + +RUN 5 ms + +# ============================================================================== +NAME SpaceCadet interrupt + +RUN 4 ms +PRESS LSHIFT +RUN 1 cycle +EXPECT keyboard-report Key_LeftShift # Report should contain `shift` (0xE1) + +RUN 4 ms +PRESS A +RUN 1 cycle +EXPECT keyboard-report Key_LeftShift Key_A # Report should add `A` (0x04, 0xE1) + +RUN 4 ms +RELEASE LSHIFT +RUN 1 cycle +EXPECT keyboard-report Key_A # Report should contain only `A` (0x04) + +RUN 4 ms +RELEASE A +RUN 1 cycle +EXPECT keyboard-report empty # Report should be empty + +RUN 5 ms + +# ============================================================================== +NAME SpaceCadet interrupt SpaceCadet with tap + +RUN 4 ms +PRESS LSHIFT +RUN 1 cycle +EXPECT keyboard-report Key_LeftShift # Report should contain `shift` (0xE1) + +RUN 4 ms +PRESS RSHIFT +RUN 1 cycle +EXPECT keyboard-report Key_LeftShift Key_RightShift # Report should add `shift` (0xE5) + +RUN 4 ms +RELEASE RSHIFT +RUN 1 cycle +EXPECT keyboard-report Key_LeftShift # Report should contain `shift` (0xE1) +EXPECT keyboard-report Key_LeftShift Key_Y # Report should add `Y` (0x1C) +EXPECT keyboard-report Key_LeftShift # Report should contain `shift` (0xE1) + +RUN 4 ms +RELEASE LSHIFT +RUN 1 cycle +EXPECT keyboard-report empty # Report should be empty + +RUN 5 ms + +# ============================================================================== +NAME SpaceCadet interrupt SpaceCadet with hold + +# First, press left shift. It takes effect immediately, because SpaceCadet is in +# "no-delay" mode. +RUN 4 ms +PRESS LSHIFT +RUN 1 cycle +EXPECT keyboard-report Key_LeftShift # report: { e1 } + +# Before left shift times out (timeout=10ms), press right shift, which also +# takes effect without delay. +RUN 4 ms +PRESS RSHIFT +RUN 1 cycle +EXPECT keyboard-report Key_LeftShift Key_RightShift # report: { e1 e5 } + +# Next, release left shift after it times out (so it's not a "tap"), but before +# the right shift times out. This does not generate a report, because the right +# shift might still become a "tap" if it's released soon enough. +RUN 10 ms +RELEASE LSHIFT + +# Next, the right shift times out, resolving to its modifier state. This allows +# the left shift to be released, because the right shift can't be a "tap". +RUN 10 ms +EXPECT keyboard-report Key_RightShift # Report should contain `shift` (0xE5) + +# Last, release the right shift as normal. +RUN 4 ms +RELEASE RSHIFT +RUN 1 cycle +EXPECT keyboard-report empty # Report should be empty + +RUN 5 ms