From cd82afd50a7d884cb0e58a5fcad1fb155cdfdfde Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Thu, 15 Sep 2022 18:12:47 +0200 Subject: [PATCH] SpaceCadet: Implement a SpaceCadetConfig plugin The new plugin enables configuring some aspects of SpaceCadet through Focus: the current mode, and the global timeout. This is makes it possible to ship firmware with SpaceCadet included, disabled by default, but still allow one to enable it without having to map and tap the enable key. The settings are also persisted into storage. Signed-off-by: Gergely Nagy --- plugins/Kaleidoscope-SpaceCadet/README.md | 59 ++++++++-- .../src/kaleidoscope/plugin/SpaceCadet.cpp | 10 +- .../src/kaleidoscope/plugin/SpaceCadet.h | 50 +++++++-- .../kaleidoscope/plugin/SpaceCadetConfig.cpp | 103 ++++++++++++++++++ 4 files changed, 200 insertions(+), 22 deletions(-) create mode 100644 plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadetConfig.cpp diff --git a/plugins/Kaleidoscope-SpaceCadet/README.md b/plugins/Kaleidoscope-SpaceCadet/README.md index 9ebaeb74..d2ccdfe7 100644 --- a/plugins/Kaleidoscope-SpaceCadet/README.md +++ b/plugins/Kaleidoscope-SpaceCadet/README.md @@ -81,7 +81,11 @@ void setup() { ## Plugin methods -The plugin provides the `SpaceCadet` object, with the following methods: +The plugin provides two objects, `SpaceCadet` and `SpaceCadetConfig`. The latter +requires the first, and allows configuring some aspects of `SpaceCadet` through +[Focus][focus]. + +The `SpaceCadet` object provides the following methods: ### `.setMap(map)` @@ -110,7 +114,14 @@ The plugin provides the `SpaceCadet` object, with the following methods: > timeout setting can be overridden by an individual key in the keymap, but if > it is omitted or set to `0` in the key map, the global timeout will be used. > -> Defaults to 1000. +> Defaults to 200. + +### `.getTimeout()` + +> Returns the number of milliseconds SpaceCadet will wait before considering a +> key held in isolation as its secondary role. This returns the *global* +> setting, as set by `.setTimeout()`. If any key in the mapping set by +> `.setMap()` has a different timeout, that is not considered here. ### `.enable()` @@ -120,6 +131,13 @@ The plugin provides the `SpaceCadet` object, with the following methods: > > The default behavior is `enabled`. +### `.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. + ### `.disable()` > This method disables the SpaceCadet behavior. This is useful for interfacing @@ -132,12 +150,11 @@ The plugin provides the `SpaceCadet` object, with the following methods: > 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. +### `.activeWithoutDelay()` + +> This method returns `true` if SpaceCadet is enabled, and is in "no-delay" +> mode, as set by `.enableWithoutDelay()`. ### `Key_SpaceCadetEnable` @@ -151,10 +168,38 @@ The plugin provides the `SpaceCadet` object, with the following methods: > behavior. This is only triggered on initial press, and does not > trigger again if held down or when the key is released. +## Focus commands + +When using the `SpaceCadetConfig` plugin, the following Focus commands become +available: + +### `spacecadet.mode` + +> Without arguments, returns the mode SpaceCadet is currently in, as a number. +> When `SpaceCadet` is enabled in normal mode, this returns 0. When it is turned +> off, it returns 1. When it is active in no-delay mode, it returns 2. +> +> When an argument is supplied, it must be one of the above, and will set the +> SpaceCadet mode appropriately. Giving a numeric argument other than the +> allowed ones will disable SpaceCadet. + +### `spacecadet.timeout` + +> Without arguments, prints the global timeout used by SpaceCadet. +> +> When an argument is given, it sets the global timeout. + ## Dependencies * [Kaleidoscope-Ranges](Kaleidoscope-Ranges.md) +### Optional dependencies, if using the `SpaceCadetConfig` object + +* [Kaleidoscope-EEPROM-Settings](Kaleidoscope-EEPROM-Settings.md) +* [Kaleidoscope-FocusSerial][focus] + + [focus]: Kaleidoscope-FocusSerial.md + ## Further reading Starting from the [example][plugin:example] is the recommended way of getting diff --git a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.cpp b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.cpp index 4105e0ec..0bb0a7f9 100644 --- a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.cpp +++ b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.cpp @@ -102,7 +102,7 @@ EventHandlerResult SpaceCadet::onKeyswitchEvent(KeyEvent &event) { } // Do nothing if disabled, but keep the event tracker current. - if (mode_ == Mode::OFF) + if (settings_.mode == Mode::OFF) return EventHandlerResult::OK; if (!event_queue_.isEmpty()) { @@ -133,7 +133,7 @@ EventHandlerResult SpaceCadet::onKeyswitchEvent(KeyEvent &event) { // 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) + if (settings_.mode == Mode::NO_DELAY) Runtime.handleKeyEvent(event); // Queue the press event and abort; this press event will be resolved // later. @@ -151,8 +151,8 @@ EventHandlerResult SpaceCadet::afterEachCycle() { if (event_queue_.isEmpty()) return EventHandlerResult::OK; - // Get timeout value for the pending key. - uint16_t pending_timeout = timeout_; + // Get timeout value for the pending key. + uint16_t pending_timeout = settings_.timeout; if (map_[pending_map_index_].timeout != 0) pending_timeout = map_[pending_map_index_].timeout; uint16_t start_time = event_queue_.timestamp(0); @@ -187,7 +187,7 @@ void SpaceCadet::flushEvent(bool is_tap) { 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) { + if (settings_.mode == Mode::NO_DELAY) { Runtime.handleKeyEvent(KeyEvent(event.addr, WAS_PRESSED)); } event.key = map_[pending_map_index_].output; diff --git a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.h b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.h index 45958dea..60e49439 100644 --- a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.h +++ b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.h @@ -1,7 +1,7 @@ /* -*- mode: c++ -*- * Kaleidoscope-SpaceCadet -- Space Cadet Shift Extended * Copyright (C) 2016, 2017, 2018 Keyboard.io, Inc, Ben Gemperline - * Copyright (C) 2019-2021 Keyboard.io, Inc + * Copyright (C) 2019-2022 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 @@ -39,7 +39,11 @@ constexpr Key Key_SpaceCadetDisable = Key(kaleidoscope::ranges::SC_LAST); namespace kaleidoscope { namespace plugin { +class SpaceCadetConfig; + class SpaceCadet : public kaleidoscope::Plugin { + friend class SpaceCadetConfig; + public: // Internal Class // Declarations for the modifier key mapping @@ -67,20 +71,27 @@ class SpaceCadet : public kaleidoscope::Plugin { // Methods void enable() { - mode_ = Mode::ON; + settings_.mode = Mode::ON; } void disable() { - mode_ = Mode::OFF; + settings_.mode = Mode::OFF; } void enableWithoutDelay() { - mode_ = Mode::NO_DELAY; + settings_.mode = Mode::NO_DELAY; } bool active() { - return (mode_ == Mode::ON || mode_ == Mode::NO_DELAY); + return (settings_.mode == Mode::ON || settings_.mode == Mode::NO_DELAY); + } + bool activeWithoutDelay() { + return settings_.mode == Mode::NO_DELAY; } void setTimeout(uint16_t timeout) { - timeout_ = timeout; + settings_.timeout = timeout; + } + + uint16_t getTimeout() { + return settings_.timeout; } void setMap(KeyBinding *bindings) { @@ -91,17 +102,20 @@ class SpaceCadet : public kaleidoscope::Plugin { EventHandlerResult onKeyswitchEvent(KeyEvent &event); EventHandlerResult afterEachCycle(); - private: + protected: enum Mode : uint8_t { ON, OFF, NO_DELAY, }; - uint8_t mode_; + struct { + Mode mode; - // Global timeout in milliseconds - uint16_t timeout_ = 200; + // Global timeout in milliseconds + uint16_t timeout = 200; + } settings_; + private: // The map of keybindings KeyBinding *map_ = nullptr; @@ -125,7 +139,23 @@ class SpaceCadet : public kaleidoscope::Plugin { void flushQueue(); }; +class SpaceCadetConfig : public kaleidoscope::Plugin { + public: + EventHandlerResult onSetup(); + EventHandlerResult onFocusEvent(const char *command); + + void disableSpaceCadetIfUnconfigured(); + + private: + struct Settings { + SpaceCadet::Mode mode; + uint16_t timeout; + }; + uint16_t settings_base_; +}; + } // namespace plugin } // namespace kaleidoscope extern kaleidoscope::plugin::SpaceCadet SpaceCadet; +extern kaleidoscope::plugin::SpaceCadetConfig SpaceCadetConfig; diff --git a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadetConfig.cpp b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadetConfig.cpp new file mode 100644 index 00000000..ca05b454 --- /dev/null +++ b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadetConfig.cpp @@ -0,0 +1,103 @@ +/* -*- mode: c++ -*- + * Kaleidoscope-SpaceCadet -- Space Cadet Shift Extended + * Copyright (C) 2016, 2017, 2018 Keyboard.io, Inc, Ben Gemperline + * Copyright (C) 2019-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 "SpaceCadet.h" +#include "kaleidoscope/plugin/SpaceCadet.h" + +#include // for F, __FlashStringHelper +#include // for Focus, FocusSerial +#include // for EEPROMSettings +#include // for uint16_t, int8_t, uint8_t + +#include "kaleidoscope/Runtime.h" // for Runtime, Runtime_ +#include "kaleidoscope/event_handler_result.h" // for EventHandlerResult, EventHandlerResult::OK + +namespace kaleidoscope { +namespace plugin { + +EventHandlerResult SpaceCadetConfig::onSetup() { + settings_base_ = ::EEPROMSettings.requestSlice(sizeof(SpaceCadet::settings_)); + + // If our slice is uninitialized, then return early. + if (Runtime.storage().isSliceUninitialized(settings_base_, sizeof(SpaceCadet::settings_))) + return EventHandlerResult::OK; + + Runtime.storage().get(settings_base_, ::SpaceCadet.settings_); + + return EventHandlerResult::OK; +} + +void SpaceCadetConfig::disableSpaceCadetIfUnconfigured() { + if (Runtime.storage().isSliceUninitialized(settings_base_, sizeof(SpaceCadet::settings_))) + ::SpaceCadet.disable(); +} + +EventHandlerResult SpaceCadetConfig::onFocusEvent(const char *command) { + const char *cmd_mode = PSTR("spacecadet.mode"); + const char *cmd_timeout = PSTR("spacecadet.timeout"); + + // Note: These two calls are intentionally separate, because we want the side + // effects. If we were to use `||`, only one of them would run. + bool help_handled = ::Focus.handleHelp(command, cmd_mode); + help_handled |= ::Focus.handleHelp(command, cmd_timeout); + + if (help_handled) + return EventHandlerResult::OK; + + if (strcmp_P(command, cmd_mode) == 0) { + if (::Focus.isEOL()) { + ::Focus.send(::SpaceCadet.settings_.mode); + } else { + uint8_t mode; + ::Focus.read(mode); + switch (mode) { + case SpaceCadet::Mode::ON: + ::SpaceCadet.settings_.mode = SpaceCadet::Mode::ON; + break; + case SpaceCadet::Mode::NO_DELAY: + ::SpaceCadet.settings_.mode = SpaceCadet::Mode::NO_DELAY; + break; + case SpaceCadet::Mode::OFF: + default: + ::SpaceCadet.settings_.mode = SpaceCadet::Mode::OFF; + break; + } + + Runtime.storage().put(settings_base_, ::SpaceCadet.settings_); + Runtime.storage().commit(); + } + } else if (strcmp_P(command, cmd_timeout) == 0) { + if (::Focus.isEOL()) { + ::Focus.send(::SpaceCadet.settings_.timeout); + } else { + ::Focus.read(::SpaceCadet.settings_.timeout); + + Runtime.storage().put(settings_base_, ::SpaceCadet.settings_); + Runtime.storage().commit(); + } + } else { + return EventHandlerResult::OK; + } + + return EventHandlerResult::EVENT_CONSUMED; +} + +} // namespace plugin +} // namespace kaleidoscope + +kaleidoscope::plugin::SpaceCadetConfig SpaceCadetConfig;