diff --git a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.cpp b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.cpp index f369dbb7..6135c320 100644 --- a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.cpp +++ b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.cpp @@ -21,261 +21,203 @@ #include "kaleidoscope/keyswitch_state.h" #include "kaleidoscope/key_events.h" +//#include + namespace kaleidoscope { namespace plugin { -//Constructor with input and output, and assume default timeout -SpaceCadet::KeyBinding::KeyBinding(Key input_, Key output_) { - input = input_; - output = output_; -} +// Constructor with input and output, and assume default timeout +SpaceCadet::KeyBinding::KeyBinding(Key input, Key output) + : input(input), output(output) {} -//Constructor with all three set -SpaceCadet::KeyBinding::KeyBinding(Key input_, Key output_, uint16_t timeout_) { - input = input_; - output = output_; - timeout = timeout_; -} +// Constructor with all three set +SpaceCadet::KeyBinding::KeyBinding(Key input, Key output, uint16_t timeout) + : input(input), output(output), timeout(timeout) {} + +// ============================================================================= +// Space Cadet class variables + +// ----------------------------------------------------------------------------- +// Plugin configuration variables -//Space Cadet SpaceCadet::KeyBinding * SpaceCadet::map; -uint16_t SpaceCadet::time_out = 1000; +uint16_t SpaceCadet::time_out = 200; + +// ----------------------------------------------------------------------------- +// State variables + bool SpaceCadet::disabled = false; +// 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 +// there is no such pending keypress. Otherwise, it holds the value of the index +// of that key in the array. +int8_t SpaceCadet::pending_map_index_ = -1; + +KeyEventTracker SpaceCadet::event_tracker_; + +// ============================================================================= +// SpaceCadet functions + +// Constructor SpaceCadet::SpaceCadet() { static SpaceCadet::KeyBinding initialmap[] = { - //By default, respect the default timeout - {Key_LeftShift, Key_LeftParen, 0} - , {Key_RightShift, Key_RightParen, 0} - //These may be uncommented, added, or set in the main sketch - /*,{Key_LeftGui,Key_LeftCurlyBracket, 250} - ,{Key_RightAlt,Key_RightCurlyBracket, 250} - ,{Key_LeftControl,Key_LeftBracket, 250} - ,{Key_RightControl,Key_RightBracket, 250}*/ - , SPACECADET_MAP_END + // By default, respect the default timeout + {Key_LeftShift, Key_LeftParen, 0}, + {Key_RightShift, Key_RightParen, 0}, + // These may be uncommented, added, or set in the main sketch + /* + {Key_LeftGui, Key_LeftCurlyBracket, 250}, + {Key_RightAlt, Key_RightCurlyBracket, 250}, + {Key_LeftControl, Key_LeftBracket, 250}, + {Key_RightControl, Key_RightBracket, 250}, + */ + SPACECADET_MAP_END }; map = initialmap; } -//Function to enable SpaceCadet behavior +// ----------------------------------------------------------------------------- +// 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 +// Function to disable SpaceCadet behavior void SpaceCadet::disable() { disabled = true; } -//Function to determine whether SpaceCadet is active (useful for Macros and other plugins) -bool SpaceCadet::active() { - return !disabled; -} +// ============================================================================= +// Event handler hook functions +// ----------------------------------------------------------------------------- EventHandlerResult SpaceCadet::onNameQuery() { return ::Focus.sendName(F("SpaceCadet")); } -EventHandlerResult SpaceCadet::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) { - //Handle our synthetic keys for enabling and disabling functionality - if (mapped_key.getRaw() >= kaleidoscope::ranges::SC_FIRST && - mapped_key.getRaw() <= kaleidoscope::ranges::SC_LAST) { - //Only fire the activate / deactivate on the initial press (not held or release) - if (keyToggledOn(key_state)) { - if (mapped_key == Key_SpaceCadetEnable) { - enable(); - } else if (mapped_key == Key_SpaceCadetDisable) { - disable(); - } - } - - return EventHandlerResult::EVENT_CONSUMED; +// ----------------------------------------------------------------------------- +EventHandlerResult SpaceCadet::onKeyswitchEvent(KeyEvent &event) { + // If SpaceCadet has already processed and released this event, ignore + // it. There's no need to update the event tracker in this 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 (event_queue_.shouldAbort(event)) + return EventHandlerResult::ABORT; + return EventHandlerResult::OK; } - //if SpaceCadet is disabled, this was an injected key, it was NoKey, - //or if they key somehow came here without being either pressed or released, - //return the mapped key and just get out of here. - if ( - disabled - || (key_state & INJECTED) - || mapped_key == Key_NoKey - || (!keyIsPressed(key_state) && !keyWasPressed(key_state)) - ) { + // If event.addr is not a physical key, ignore it; some other plugin injected + // it. This check should be unnecessary. + if (!event.addr.isValid() || (event.state & INJECTED) != 0) { return EventHandlerResult::OK; } - // If a key has been just toggled on... - if (keyToggledOn(key_state)) { - - //check to see if we found a valid key. Assume not valid. - bool valid_key = false; - bool other_mapped_key_flagged = false; - - //Check the current map to see if any other key has been already flagged - //Exit condition is if we reach the special SPACECADET_MAP_END sentinel - for ( - uint8_t i = 0 ; - !( - map[i].input == Key_NoKey - && map[i].output == Key_NoKey - && map[i].timeout == 0 - ) ; - ++i - ) { - - if (map[i].flagged - && map[i].input != mapped_key) { - other_mapped_key_flagged = true; - break; - } - } - - //This will only set one key, and, if it isn't in our map, it clears everything for the non-pressed key - //Exit condition is if we reach the special SPACECADET_MAP_END sentinel - for ( - uint8_t i = 0 ; - !( - map[i].input == Key_NoKey - && map[i].output == Key_NoKey - && map[i].timeout == 0 - ) ; - ++i - ) { - - if (mapped_key == map[i].input) { - //Only activate this as part of the mapping if there isn't already a - //key waiting for timeout. This allows us to return OK later and for - //this loop to inject all the other flagged keys - if (!other_mapped_key_flagged) { - //The keypress was valid and a match. Mark it as flagged and reset the counter - map[i].flagged = true; - map[i].start_time = Runtime.millisAtCycleStart(); - - //yes, we found a valid key - valid_key = true; - } - } else { - //If the key entry we're looking at was flagged previously, add it to the - //report before we do anything else (this handles the situation where we - //hit another key after this -- if it's a modifier, we want the modifier - //key to be added to the report, for things like ctrl, alt, shift, etc) - if (map[i].flagged) { - handleKeyswitchEvent(map[i].input, UnknownKeyswitchLocation, IS_PRESSED | INJECTED); - } - - //The keypress wasn't a match, so we need to mark it as not flagged and - //reset the timer for it to disable everything. - map[i].flagged = false; - map[i].start_time = 0; - } + // Turn SpaceCadet on or off. + if (keyToggledOn(event.state)) { + if (event.key == Key_SpaceCadetEnable) { + enable(); + return EventHandlerResult::EVENT_CONSUMED; } - - //If we found a valid key in our map, we don't actually want to send anything. - //This gets around an issue in Windows if we map a SpaceCadet function on top - //of Alt -- sending Alt by itself activates the menubar. We don't want to send - //anything until we know that we're either sending the alternate key or we - //know for sure that we want to send the originally pressed key. - if (valid_key) { + if (event.key == Key_SpaceCadetDisable) { + disable(); return EventHandlerResult::EVENT_CONSUMED; } - - //this is all we need to do on keypress, let the next handler do its thing too. - //This case assumes we weren't a valid key that we were watching, so we don't - //need to do anything else. - return EventHandlerResult::OK; } - // if the state is empty, that means that either an activating key wasn't pressed, - // or we used another key in the interim. in both cases, nothing special to do. - bool valid_key = false; - bool pressed_key_was_valid = false; - uint8_t index = 0; - - //Look to see if any keys in our map are currently flagged. - //Exit condition is if we reach the special SPACECADET_MAP_END sentinel - for ( - uint8_t i = 0 ; - !( - map[i].input == Key_NoKey - && map[i].output == Key_NoKey - && map[i].timeout == 0 - ); - ++i - ) { - - //The key we're looking at was previously flagged (so perform action) - if (map[i].flagged) { - valid_key = true; - index = i; - } + // Do nothing if disabled, but keep the event tracker current. + if (disabled) + return EventHandlerResult::OK; - //the key we're looking at was valid (in the map) - if (map[i].input == mapped_key) { - pressed_key_was_valid = true; + if (!event_queue_.isEmpty()) { + // There's an unresolved SpaceCadet key press. + if (keyToggledOff(event.state)) { + if (event.addr == event_queue_.addr(0)) { + // SpaceCadet key released before timing out; send the event with the + // SpaceCadet key's alternate `Key` value before flushing the rest of + // the queue (see below). + flushEvent(true); + } else if (!event_queue_.isFull()) { + // Queue not full; add event and abort + event_queue_.append(event); + return EventHandlerResult::ABORT; + } } + // Either a new key was pressed, or the SpaceCadet key was released and has + // been flushed (see above), or the queue is full and is about to overflow. + // In all cases, we fulsh the whole queue now. + flushQueue(); } - //If no valid mapped keys were pressed, simply return the key that - //was originally passed in. - if (!valid_key) { - return EventHandlerResult::OK; - } - - //use the map index to find the local timeout for this key - uint16_t current_timeout = map[index].timeout; - //If that isn't set, use the global timeout setting. - if (current_timeout == 0) { - current_timeout = time_out; + // Event queue is now empty + if (keyToggledOn(event.state)) { + // Check for a SpaceCadet key + pending_map_index_ = getSpaceCadetKeyIndex(event.key); + if (pending_map_index_ >= 0) { + // A SpaceCadet key was pressed + event_queue_.append(event); + return EventHandlerResult::ABORT; + } } - //Check to determine if we have surpassed our timeout for holding this key - if (Runtime.hasTimeExpired(map[index].start_time, current_timeout)) { - // if we timed out, that means we need to keep pressing the mapped - // key, but we won't need to send the alternative key in the end - map[index].flagged = false; - map[index].start_time = 0; - - //Just return this key itself (we won't run alternative keys check) - return EventHandlerResult::OK; - } + return EventHandlerResult::OK; +} - // If the key that was pressed isn't one of our mapped keys, just - // return. This can happen when another key is released, and that should not - // interrupt us. - if (!pressed_key_was_valid) { +// ----------------------------------------------------------------------------- +EventHandlerResult SpaceCadet::afterEachCycle() { + // If there's no pending event, return. + if (event_queue_.isEmpty()) return EventHandlerResult::OK; - } - // if a key toggled off (and that must be one of the mapped keys at this point), - // send the alternative key instead (if we were interrupted, we bailed out earlier). - if (keyToggledOff(key_state)) { - Key alternate_key = map[index].output; + // Get timeout value for the pending key. + uint16_t pending_timeout = time_out; + if (map[pending_map_index_].timeout != 0) + pending_timeout = map[pending_map_index_].timeout; + uint16_t start_time = event_queue_.timestamp(0); - //Since we are sending the actual key (no need for shift, etc), - //only need to send that key and not the original key. + if (Runtime.hasTimeExpired(start_time, pending_timeout)) { + // The timer has expired; release the pending event unchanged. + flushQueue(); + } + return EventHandlerResult::OK; +} - //inject our new key - handleKeyswitchEvent(alternate_key, key_addr, IS_PRESSED | INJECTED); +// ============================================================================= +// Private helper function(s) - //Unflag the key so we don't try this again. - map[index].flagged = false; - map[index].start_time = 0; +int8_t SpaceCadet::getSpaceCadetKeyIndex(Key key) const { + for (uint8_t i = 0; !map[i].isEmpty(); ++i) { + if (map[i].input == key) { + return i; + } } + return -1; +} - //Special case here for if we had a valid key that's continuing to be held. - //If it's a valid key, and it's continuing to be held, return NoKey. - //This prevents us from accidentally triggering a keypress that we didn't - //mean to handle. - if (valid_key) { - return EventHandlerResult::EVENT_CONSUMED; +void SpaceCadet::flushQueue() { + while (!event_queue_.isEmpty()) { + flushEvent(false); } - - //Finally, as a final sanity check, simply return the passed-in key as-is. - return EventHandlerResult::OK; } +void SpaceCadet::flushEvent(bool is_tap) { + KeyEvent event = event_queue_.event(0); + if (is_tap && pending_map_index_ >= 0) { + event.key = map[pending_map_index_].output; + } + event_queue_.shift(); + Runtime.handleKeyswitchEvent(event); } -} + +} // namespace plugin +} // namespace kaleidoscope kaleidoscope::plugin::SpaceCadet SpaceCadet; diff --git a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.h b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.h index 682ecdb1..971bb540 100644 --- a/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.h +++ b/plugins/Kaleidoscope-SpaceCadet/src/kaleidoscope/plugin/SpaceCadet.h @@ -19,6 +19,8 @@ #pragma once #include "kaleidoscope/Runtime.h" +#include "kaleidoscope/KeyEventTracker.h" +#include "kaleidoscope/KeyAddrEventQueue.h" #include #ifndef SPACECADET_MAP_END @@ -33,44 +35,60 @@ namespace plugin { class SpaceCadet : public kaleidoscope::Plugin { public: - //Internal Class - //Declarations for the modifier key mapping + // Internal Class + // Declarations for the modifier key mapping class KeyBinding { public: - //Empty constructor; set the vars separately - KeyBinding(void) {} - //Constructor with input and output - KeyBinding(Key input_, Key output_); - //Constructor with all three set - KeyBinding(Key input_, Key output_, uint16_t timeout_); - //The key that is pressed + // Empty constructor; set the vars separately + KeyBinding() {} + // Constructor with input and output + KeyBinding(Key input, Key output); + // Constructor with all three set + KeyBinding(Key input, Key output, uint16_t timeout); + // The key that is pressed Key input; - //the key that is sent + // the key that is sent Key output; - //The timeout (default to global timeout) + // The timeout (default to global timeout) uint16_t timeout = 0; - //The flag (set to 0) - bool flagged = false; - //the start time for this key press - uint16_t start_time = 0; + // to check for the end of a list (SPACECADET_MAP_END) + bool isEmpty() const { + return (input == Key_NoKey && output == Key_NoKey && timeout == 0); + } }; SpaceCadet(void); - //Methods + // Methods static void enable(void); static void disable(void); static bool active(void); - //Publically accessible variables + // Publically accessible variables static uint16_t time_out; // The global timeout in milliseconds static SpaceCadet::KeyBinding * map; // The map of key bindings EventHandlerResult onNameQuery(); - EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state); + EventHandlerResult onKeyswitchEvent(KeyEvent &event); + EventHandlerResult afterEachCycle(); private: static bool disabled; + + static 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 event_queue_; + + static int8_t pending_map_index_; + + int8_t getSpaceCadetKeyIndex(Key key) const; + + void flushEvent(bool is_tap = false); + void flushQueue(); }; }