diff --git a/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.cpp b/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.cpp index 732c461f..d18b734d 100644 --- a/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.cpp +++ b/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.cpp @@ -1,6 +1,6 @@ /* -*- mode: c++ -*- * Kaleidoscope-Qukeys -- Assign two keycodes to a single key - * Copyright (C) 2017-2019 Michael Richters + * Copyright (C) 2017-2020 Michael Richters * * 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 @@ -153,7 +153,6 @@ EventHandlerResult Qukeys::beforeReportingState() { // ----------------------------------------------------------------------------- - // This function contains most of the logic behind Qukeys. It gets called after // an event gets added to the queue, and again once per cycle. It returns `true` // if nothing more should be done, either because the queue is empty, or because @@ -174,8 +173,22 @@ bool Qukeys::processQueue() { // If that first event is a key release, it can be flushed right away. if (event_queue_.isRelease(0)) { - flushEvent(Key_NoKey); - return true; + // We can't unconditionally flush the release event, because it might be + // second half of a tap-repeat event. If the queue is full, we won't bother to + // check, but otherwise, ift `tap_repeat_.addr` is set (and matches), we call + // `shouldWaitForTapRepeat()` to determine whether or not to flush the key + // release event. + if (event_queue_.isFull() || + queue_head_addr != tap_repeat_.addr || + !shouldWaitForTapRepeat()) { + flushEvent(Key_NoKey); + return true; + } + // We now know that we're waiting to determine if we're getting a tap-repeat + // sequence, so we can't flush the release event at the head of the queue. + // Warning: Returning false here is only okay because we already checked to + // make sure the queue isn't full. + return false; } // We now know that the first event is a key press. If it's not a qukey, or if @@ -237,6 +250,13 @@ bool Qukeys::processQueue() { if (next_keypress_index == 0 || overlap_threshold_ == 0) { Key event_key = qukey_is_spacecadet ? queue_head_.alternate_key : queue_head_.primary_key; + // A qukey just got released in primary state; this might turn out to be + // the beginning of a tap-repeat sequence, so we set the tap-repeat + // address and start time to the time of the initial press event before + // flushing it from the queue. This will come into play when processing + // the corresponding release event later. + tap_repeat_.addr = queue_head_addr; + tap_repeat_.start_time = event_queue_.timestamp(0); flushEvent(event_key); return true; } @@ -420,6 +440,100 @@ bool Qukeys::isKeyAddrInQueueBeforeIndex(KeyAddr k, uint8_t index) const { } +// This question gets called early in `processQueue()` if a key release event is +// at the head of the queue, and the `tap_repeat_.addr` is the same as that +// event's KeyAddr. It returns true if `processQueue()` should wait for either +// subsequent events or a timeout instead of proceeding to flush the key release +// event immediately, and false if it is still waiting. It assumes that +// `event_queue_[0]` is a release event, and that `event_queue_[0].addr == +// tap_repeat_.addr`. (The latter should only be set to a valid KeyAddr if a qukey +// press event has been flushed with its primary Key value, and could still +// represent the start of a double-tap or tap-repeat sequeunce.) +bool Qukeys::shouldWaitForTapRepeat() { + // First, we set up a variable to store the queue index of a subsequent press + // of the same qukey addr, if any. + uint8_t second_press_index = 0; + + // Next, we search the event queue (starting at index 1 because the first + // event in the queue is known), trying to find a matching sequeunce for + // either a double-tap, or a tap-repeat. + for (uint8_t i{1}; i < event_queue_.length(); ++i) { + if (event_queue_.isPress(i)) { + // Found a keypress event following the release of the initial primary + // qukey. + if (event_queue_.addr(i) == tap_repeat_.addr) { + // The same qukey toggled on twice in a row, and because of the timeout + // check below, we know it was quick enough that it could represent a + // tap-repeat sequence. Now we update the start time (which had been set + // to the timestamp of the first press event) to the timestamp of the + // first release event (currently at the head of the queue), because we + // want to compare the release times of the two taps to determine if + // it's actually a double-tap sequence instead (otherwise it could be + // too difficult to tap it fast enough). + tap_repeat_.start_time = event_queue_.timestamp(0); + // We also record the index of this second press event. If it turns out + // that we've got a tap-repeat sequence, we want to silently suppress the + // first release and second press by removing them from the queue + // without flushing them. We don't know yet whether we'll be doing so. + second_press_index = i; + } else { + // Some other key was pressed. For it to be a tap-repeat sequence, we + // require that the same key be pressed twice in a row, with no + // intervening presses of other keys. Therefore, we can return false to + // signal that the release event at the head of the queue can be + // flushed. + return false; + } + + } else if (event_queue_.addr(i) == tap_repeat_.addr) { + // We've found a key release event in the queue, and it's the same key as + // the qukey at the head of the queue, so this is the second release that + // has occurred before timing out (see below for the timeout + // check). Therefore, this is a double-tap sequence (the second release + // happened very close to the first release), not a tap-repeat, so we + // clear the tap-repeat address, and return false to trigger the flush of + // the first release event. + tap_repeat_.addr = KeyAddr{KeyAddr::invalid_state}; + return false; + } + } + + // We've now searched the queue. Either we found only irrelevant key release + // events (for other keys that were pressed before the sequence began), or we + // found only a second key press event of the same qukey (but not a double + // tap). Next, we check the timeout. If we didn't find a second press event, + // the start time will still be that of the initial key press event (already + // flushed from the queue), but if the second press has been detected, the + // start time will be that of the key release event currently at the head of + // the queue. + if (Runtime.hasTimeExpired(tap_repeat_.start_time, tap_repeat_.timeout)) { + // Time has expired. The sequence represents either a single tap or a + // tap-repeat of the qukey's primary value. Either way, we can clear the + // stored address. + tap_repeat_.addr = KeyAddr{KeyAddr::invalid_state}; + + if (second_press_index > 0) { + // A second press was found (but it's release didn't come quick enough to + // be a double tap), so this is a tap-repeat event. To turn it into a + // single key press and hold, we need to remove the second press event and + // the first release event from the queue without flushing the + // events. Order matters here! + event_queue_.remove(second_press_index); + event_queue_.remove(0); + } else { + // The key was not pressed again, so the single tap has timed out. We + // return false to let the release event be flushed. + return false; + } + } + + // We haven't found a double-tap sequence, and the timeout hasn't expired, so + // we return true to signal that we should just wait until we get another + // event, or until time runs out. + return true; +} + + // ----------------------------------------------------------------------------- // This function is here to provide the test for a SpaceCadet-type qukey, which diff --git a/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.h b/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.h index 80a89f4a..b2e4ca7e 100644 --- a/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.h +++ b/plugins/Kaleidoscope-Qukeys/src/kaleidoscope/plugin/Qukeys.h @@ -1,6 +1,6 @@ /* -*- mode: c++ -*- * Kaleidoscope-Qukeys -- Assign two keycodes to a single key - * Copyright (C) 2017-2019 Michael Richters + * Copyright (C) 2017-2020 Michael Richters * * 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 @@ -81,6 +81,17 @@ class Qukeys : public kaleidoscope::Plugin { hold_timeout_ = hold_timeout; } + // Set the timeout (in milliseconds) for the tap-repeat feature. If a qukey is + // tapped twice in a row in less time than this amount, it will allow the user + // to hold the key with its primary value (unless it's a SpaceCadet type key, + // in which case it will repeat the alternate value instead). This requires a + // quick tap immediately followed by a press and hold, and will result in a + // single press-and-hold on the host system. If a double tap is done instead, + // it will result in two independent taps. + void setMaxIntervalForTapRepeat(uint8_t ttl) { + tap_repeat_.timeout = ttl; + } + // Set the percentage of the duration of a subsequent key's press that must // overlap with the qukey preceding it above which the qukey will take on its // alternate key value. In other words, if the user presses qukey `Q`, then @@ -190,6 +201,14 @@ class Qukeys : public kaleidoscope::Plugin { bool isDualUseKey(Key key); bool releaseDelayed(uint16_t overlap_start, uint16_t overlap_end) const; bool isKeyAddrInQueueBeforeIndex(KeyAddr k, uint8_t index) const; + + // Tap-repeat feature support. + struct { + KeyAddr addr{KeyAddr::invalid_state}; + uint16_t start_time; + uint8_t timeout{200}; + } tap_repeat_; + bool shouldWaitForTapRepeat(); }; // This function returns true for any key that we expect to be used chorded with