Rewrite Qukeys plugin from scratch

This is a complete rewrite of Qukeys, in order to implement several improvements
and new features:

- A new KeyAddrEventQueue class has been introduced, in order to store both key
  press and release events in the queue.
- The direct dependence on KeyboardioHID is removed by only flushing one event
  from the queue per cycle.
- The array of Qukey objects is now stored in PROGMEM instead of SRAM, and is
  configured via an array reference template function in order to automatically
  ensure the count will be correct.
- There is a new algorithm for determining which state a qukey will collapse
  into in the case of rollover from qukey to another key, which should reduce
  the rate of errors for "sloppy" typists.
- A Qukey with a primary key value that is a modifier (including layer shift
  keys) is treated like a SpaceCadet key, with different semantics. The
  alternate (non-modifier) key value is only used if the SpaceCadet key is
  pressed and released on its own, without rolling over to any other key.
- The code is generally simpler and easier to understand, with better inline
  comments explaining how it all works.

Fixes #626.

Signed-off-by: Michael Richters <gedankenexperimenter@gmail.com>
pull/640/head
Michael Richters 6 years ago
parent f6d2e62649
commit 1e58fcf3b8

@ -2,9 +2,26 @@
## Concept
This Kaleidoscope plugin allows you to overload keys on your keyboard so that they produce
one keycode (i.e. symbol) when tapped, and a different keycode -- most likely a modifier
(e.g. `shift` or `alt`) -- when held.
This Kaleidoscope plugin allows you to overload keys on your keyboard so that
they produce one keycode (i.e. symbol) when tapped, and a different keycode --
most likely a modifier (e.g. `shift` or `alt`) -- when held. The name is a play
on the term _qubit_; a qukey is a "quantum key". When it is first pressed it is
in a superposition of states until some event determines which state it ends up
in. While a qukey is in this indeterminate state, its key press event and any
subsequent key presses are delayed until something determines the qukey's
ultimate state.
Most likely, what determines the qukey's state (_primary_ or _alternate_) is the
release of a key; if the qukey is released before a subsequent key, it will take
on its primary value (most likely a printable character), but if the subsequent
key is released first, it will take on its alternate value (usually a modifier).
Qukeys is designed to make it practical to use these overloaded keys on the home
row, where similar designs have historically been problematic. For some typists
(particularly those who are accustomed to rolling over from modifiers to
modified keys, rather than deliberately holding the modifier until the
subsequent key has been released), this may still not work perfectly with
Qukeys, but some people have reported good results with home-row qukeys.
## Setup
@ -18,23 +35,23 @@ one keycode (i.e. symbol) when tapped, and a different keycode -- most likely a
KALEIDOSCOPE_INIT_PLUGINS(Qukeys);
```
- Define some `Qukeys` of the format `Qukey(layer, key_addr, alt_keycode)`
(layers, and key addresses are all zero-indexed, in key addresses rows are top to bottom and
columns are left to right):
- Define some `Qukeys` of the format `Qukey(layer, key_addr, alternate_key)`.
Layers and key addresses are all zero-indexed, in key addresses rows are top to bottom and
columns are left to right:
- For the Keyboardio Model 01, key coordinates refer to [this header
file](https://github.com/keyboardio/Kaleidoscope-Hardware-Model01/blob/f469015346535cb864a340bf8eb317d268943248/src/Kaleidoscope-Hardware-Model01.h#L267-L279).
```
QUKEYS(
// l, r, c, alt_keycode
kaleidoscope::plugin::Qukey(0, 2, 1, Key_LeftGui), // A/cmd
kaleidoscope::plugin::Qukey(0, 2, 2, Key_LeftAlt), // S/alt
kaleidoscope::plugin::Qukey(0, 2, 3, Key_LeftControl), // D/ctrl
kaleidoscope::plugin::Qukey(0, 2, 4, Key_LeftShift), // F/shift
kaleidoscope::plugin::Qukey(0, 1, 14, Key_LeftShift), // P/shift
kaleidoscope::plugin::Qukey(0, 3, 15, Key_LeftShift) // Minus/shift
)
// left-side modifiers
kaleidoscope::plugin::Qukey(0, KeyAddr(2, 1), Key_LeftGui), // A
kaleidoscope::plugin::Qukey(0, KeyAddr(2, 2), Key_LeftAlt), // S
kaleidoscope::plugin::Qukey(0, KeyAddr(2, 3), Key_LeftControl), // D
kaleidoscope::plugin::Qukey(0, KeyAddr(2, 4), Key_LeftShift), // F
// left-side layer shifts
kaleidoscope::plugin::Qukey(0, KeyAddr(3, 3), ShiftToLayer(NUMPAD)), // C
kaleidoscope::plugin::Qukey(0, KeyAddr(3, 4), ShiftToLayer(FUNCTION)), // V
```
`Qukeys` will work best if it's the first plugin in the `INIT()` list, because when typing
@ -47,30 +64,44 @@ likely to generate errors and out-of-order events.
## Configuration
### `.setTimeout(time_limit)`
### `.setHoldTimeout(timeout)`
> Sets the time length in milliseconds which determines if a key has been tapped or held.
> Sets the time (in milliseconds) after which a qukey held on its own will take
> on its alternate state. Note: this is not the primary determining factor for a
> qukey's state. It is not necessary to wait this long before pressing a key
> that should be modified by the qukey's alternate value. The primary function
> of this timeout is so that a qukey can be used as a modifier for an separate
> pointing device (i.e. `shift` + `click`).
>
> Defaults to 250.
> Defaults to `250`.
### `.setReleaseDelay(release_delay)`
### `.setOverlapThreshold(percentage)`
> Sets the time length in milliseconds to artificially delay the release of the Qukey.
> This sets a variable that allows the user to roll over from a qukey to a
> subsequent key (i.e. the qukey is released first), and still get the qukey's
> alternate (modifier) state.
>
> This is to accommodate users who are in the habit of releasing modifiers and the keys
> they modify (almost) simultaneously, since the Qukey may be detected as released
> *slightly* before the other key, which would not trigger the desired alternate keycode.
> The `percentage` parameter should be between `1` and `100` (`75` means 75%),
> and represents the fraction of the _subsequent_ key press's duration that
> overlaps with the qukey's press. If the subsequent key is released soon enough
> after the qukey is released, the percentage overlap will be high, and the
> qukey will take on its alternate (modifier) value. If, on the other hand, the
> subsequent key is held longer after the qukey is released, the qukey will take
> on its primary (non-modifier) value.
>
> It is best to keep this a very small value such as 20 to avoid over-extending the
> modifier to further keystrokes.
> Setting `percentage` to a low value (e.g. `30`) will result in a longer grace
> period. If you're getting primary values when you intended modifiers, try
> decreasing this setting. If, on the other hand, you start getting modifiers
> when you intend primary values, try increasing this setting. If you're getting
> both, the only solution is to change your typing habits, unfortunately.
>
> Defaults to 0.
> Defaults to `80`.
### `.activate()`
### `.deactivate()`
### `.toggle()`
> activate/deactivate `Qukeys`
> Activate/deactivate `Qukeys` plugin.
### DualUse key definitions
@ -118,32 +149,64 @@ The plugin provides a number of macros one can use in keymap definitions:
> must be a plain old key, and can't have any modifiers or anything else
> applied.
## Design & Implementation
DualUse keys are more limited than `Qukey` definitions, which can contain any
valid `Key` value for both the primary and alternate keys, but they take up less
space in program memory, and are just as functional for typical definitions.
When a `Qukey` is pressed, it doesn't immediately add a corresponding keycode to the HID
report; it adds that key to a queue, and waits until one of three things happens:
1. a time limit is reached
### SpaceCadet Emulation
It is possible to define a `Qukey` on a key with a _primary_ value that is a
modifier. In this case, the qukey is treated specially, and the _primary_ value
is used when the key is held, rather than the alternate value. The _alternate_
value is only used if the qukey is tapped on its own, without rolling over to
any other key. This is a reasonable facsimile of the behaviour of the SpaceCadet
plugin, and is much more suitable for keys that are mainly used as modifiers,
with an additional "tap" feature.
In addition to working this way on keyboard modifiers (`shift`, `control`, _et
al_), this works for keys that are primarily layer shift keys
(e.g. `ShiftToLayer(N)`).
As an added bonus, if Qukeys is deactivated, such a key reverts to being a
modifier, because that's what's in the keymap.
2. the `Qukey` is released
3. a subsequently-pressed key is released
### The Wildcard Layer
Until one of those conditions is met, all subsequent keypresses are simply added to the
queue, and no new reports are sent to the host. Once a condition is met, the `Qukey` is
flushed from the queue, and so are any subsequent keypresses (up to, but not including,
the next `Qukey` that is still pressed).
There is a special value (`Qukeys::layer_wildcard`) that can be used in place of
the layer number in the definition of a `Qukey`. This will define a qukey with
the given alternate value on all layers, regardless of what the primary value is
for that key on the top currently active layer.
Basically, if you hold the `Qukey`, then press and release some other key, you'll get the
alternate keycode (probably a modifier) for the `Qukey`, even if you don't wait for a
timeout. If you're typing quickly, and there's some overlap between two keypresses, you
won't get the alternate keycode, and the keys will be reported in the order that they were
pressed -- as long as the keys are released in the same order they were pressed.
The time limit is mainly there so that a `Qukey` can be used as a modifier (in its
alternate state) with a second input device (e.g. a mouse). It can be quite short (200ms
is probably short enough) -- as long as your "taps" while typing are shorter than the time
limit, you won't get any unintended alternate keycodes.
## Design & Implementation
When a qukey is pressed, it doesn't immediately add a corresponding keycode to
the HID report; it adds that key to a queue, and waits until one of three things
happens:
1. the qukey is released
1. a subsequently-pressed key is released
1. a time limit is reached
Until one of those conditions is met, all subsequent keypresses are simply added
to the queue, and no new reports are sent to the host. Once a condition is met,
the qukey is flushed from the queue, and so are any subsequent keypresses (up
to, but not including, the next qukey that is still pressed).
Basically, if you hold the qukey, then press and release some other key, you'll
get the alternate keycode (probably a modifier) for the qukey, even if you don't
wait for a timeout. If you're typing quickly, and there's some overlap between
two keypresses, you won't get the alternate keycode, and the keys will be
reported in the order that they were pressed -- as long as the keys are released
in the same order they were pressed.
The time limit is mainly there so that a qukey can be used as a modifier (in its
alternate state) with a second input device (e.g. a mouse). It can be quite
short (200ms is probably short enough) -- as long as your "taps" while typing
are shorter than the time limit, you won't get any unintended alternate
keycodes.
## Further reading

@ -70,8 +70,8 @@ void setup() {
kaleidoscope::plugin::Qukey(0, KeyAddr(2, 4), Key_LeftShift), // F/shift
kaleidoscope::plugin::Qukey(0, KeyAddr(3, 6), ShiftToLayer(1)) // Q/layer-shift (on `fn`)
)
Qukeys.setTimeout(200);
Qukeys.setReleaseDelay(20);
Qukeys.setHoldTimeout(1000);
Qukeys.setOverlapThreshold(50);
Kaleidoscope.setup();
}

@ -1,6 +1,6 @@
/* -*- mode: c++ -*-
* Kaleidoscope-Qukeys -- Assign two keycodes to a single key
* Copyright (C) 2017 Michael Richters
* Copyright (C) 2017-2019 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
@ -16,359 +16,385 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "kaleidoscope/plugin/Qukeys.h"
#include <Kaleidoscope.h>
#include <Kaleidoscope-Qukeys.h>
#include <kaleidoscope/hid.h>
#include <MultiReport/Keyboard.h>
#include <Kaleidoscope-Ranges.h>
#ifdef KALEIDOSCOPE_VIRTUAL_BUILD
#define debug_print(...) printf(__VA_ARGS__)
#else
#define debug_print(...)
#endif
#include "kaleidoscope/progmem_helpers.h"
namespace kaleidoscope {
namespace plugin {
inline
bool isDualUse(Key k) {
if (k.raw < ranges::DU_FIRST || k.raw > ranges::DU_LAST)
return false;
return true;
}
// This is the event handler. It ignores certain events, but mostly just adds
// them to the Qukeys event queue.
EventHandlerResult Qukeys::onKeyswitchEvent(Key& key, KeyAddr k, uint8_t key_state) {
// If k is not a physical key, ignore it; some other plugin injected it.
if (! k.isValid() || (key_state & INJECTED) != 0) {
return EventHandlerResult::OK;
}
inline
bool isDualUse(byte key_addr_offset) {
KeyAddr key_addr(key_addr_offset);
Key k = Layer.lookup(key_addr);
return isDualUse(k);
}
// If the key was injected (from the queue being flushed), we need to ignore
// it.
if (flushing_queue_) {
return EventHandlerResult::OK;
}
Key getDualUsePrimaryKey(Key k) {
if (k.raw >= ranges::DUM_FIRST && k.raw <= ranges::DUM_LAST) {
k.raw -= ranges::DUM_FIRST;
k.flags = 0;
} else if (k.raw >= ranges::DUL_FIRST && k.raw <= ranges::DUL_LAST) {
k.raw -= ranges::DUL_FIRST;
k.flags = 0;
// If Qukeys is turned off, continue to next plugin.
if (! active_) {
if (isDualUseKey(key)) {
key = queue_head_.primary_key;
}
return EventHandlerResult::OK;
}
return k;
}
Key getDualUseAlternateKey(Key k) {
if (k.raw >= ranges::DUM_FIRST && k.raw <= ranges::DUM_LAST) {
k.raw -= ranges::DUM_FIRST;
k.raw = (k.raw >> 8) + Key_LeftControl.keyCode;
} else if (k.raw >= ranges::DUL_FIRST && k.raw <= ranges::DUL_LAST) {
k.raw -= ranges::DUL_FIRST;
byte layer = k.flags;
// Should be `ShiftToLayer(layer)`, but that gives "narrowing conversion"
// warnings that I can't figure out how to resolve
k.keyCode = layer + LAYER_SHIFT_OFFSET;
k.flags = KEY_FLAGS | SYNTHETIC | SWITCH_TO_KEYMAP;
// Deal with keyswitch state changes.
if (keyToggledOn(key_state) || keyToggledOff(key_state)) {
// If we can't trivially ignore the event, just add it to the queue.
event_queue_.append(k, key_state);
// In order to prevent overflowing the queue, process it now.
if (event_queue_.isFull()) {
processQueue();
}
// Any event that gets added to the queue gets re-processed later, so we
// need to abort processing now.
return EventHandlerResult::EVENT_CONSUMED;
}
return k;
}
// The key is being held. We need to determine if we should block it because
// its key press event is still in the queue, waiting to be
// flushed. Therefore, we search the event queue for the same key. If the
// first event we find there is a key press, that means we need to suppress
// this hold, because it's still waiting on an earlier event.
for (uint8_t i{0}; i < event_queue_.length(); ++i) {
if (event_queue_.addr(i) == k) {
// If the first matching event is a release, we do not suppress it,
// because its press event has already been flushed.
if (event_queue_.isRelease(i)) {
break;
}
// Otherwise, the first matching event was a key press, so we need to
// suppress it for now.
return EventHandlerResult::EVENT_CONSUMED;
}
}
Qukey::Qukey(int8_t layer, KeyAddr key_addr, Key alt_keycode) {
this->layer = layer;
this->addr = key_addr.toInt();
this->alt_keycode = alt_keycode;
// Either this key doesn't have an event in the queue at all, or its first
// event in the queue is a release. We treat the key as a normal held key.
return EventHandlerResult::OK;
}
Qukey * Qukeys::qukeys;
uint8_t Qukeys::qukeys_count = 0;
bool Qukeys::active_ = true;
uint16_t Qukeys::time_limit_ = 250;
uint8_t Qukeys::release_delay_ = 0;
QueueItem Qukeys::key_queue_[] = {};
uint8_t Qukeys::key_queue_length_ = 0;
bool Qukeys::flushing_queue_ = false;
uint8_t Qukeys::delayed_qukey_addr_ = QUKEY_UNKNOWN_ADDR;
int16_t Qukeys::delayed_qukey_start_time_ = 0;
constexpr uint16_t QUKEYS_RELEASE_DELAY_OFFSET = 4096;
// Empty constructor; nothing is stored at the instance level
Qukeys::Qukeys(void) {}
int8_t Qukeys::lookupQukey(uint8_t key_addr_offset) {
if (key_addr_offset == QUKEY_UNKNOWN_ADDR) {
return QUKEY_NOT_FOUND;
}
for (int8_t i = 0; i < qukeys_count; i++) {
if (qukeys[i].addr == key_addr_offset) {
KeyAddr key_addr(key_addr_offset);
if ((qukeys[i].layer == QUKEY_ALL_LAYERS) ||
(qukeys[i].layer == Layer.lookupActiveLayer(key_addr))) {
return i;
// This hook runs once each cycle, and checks to see if the first event in the
// queue is ready to be flushed. It only allows one event to be flushed per
// cycle, because the keyboard HID report can't store all of the information
// necessary to correctly handle all of the rollover corner cases.
EventHandlerResult Qukeys::beforeReportingState() {
// For keys that have been physically released, but whose release events are
// still waiting to be flushed from the queue, we need to restore them,
// because `handleKeyswitchEvent()` didn't get called for those KeyAddrs.
for (uint8_t i{0}; i < event_queue_.length(); ++i) {
if (event_queue_.isRelease(i)) {
KeyAddr k = event_queue_.addr(i);
// Now for the tricky bit. Before "restoring" this key hold, we need to
// make sure that its key press event has already been flushed from the
// queue, so we need to search for a matching key press event preceding
// this release event. If we find one, we need to ignore it.
if (isKeyAddrInQueueBeforeIndex(k, i)) {
continue;
}
flushing_queue_ = true;
handleKeyswitchEvent(Key_NoKey, k, IS_PRESSED | WAS_PRESSED);
flushing_queue_ = false;
}
}
return QUKEY_NOT_FOUND;
}
void Qukeys::enqueue(uint8_t key_addr) {
if (key_queue_length_ == QUKEYS_QUEUE_MAX) {
flushKey(QUKEY_STATE_PRIMARY, IS_PRESSED | WAS_PRESSED);
flushQueue();
// If any events get flushed from the queue, stop there; we can only safely
// send the one report per cycle.
if (processQueue()) {
return EventHandlerResult::OK;
}
// default to alternate state to stop keys being flushed from the queue before the grace
// period timeout
key_queue_[key_queue_length_].addr = key_addr;
key_queue_[key_queue_length_].start_time = millis();
key_queue_length_++;
}
int8_t Qukeys::searchQueue(uint8_t key_addr) {
for (int8_t i = 0; i < key_queue_length_; i++) {
if (key_queue_[i].addr == key_addr)
return i;
// If we get here, that means that the first event in the queue is a qukey
// press. All that's left to do is to check if it's been held long enough that
// it has timed out.
if (Kaleidoscope.hasTimeExpired(event_queue_.timestamp(0), hold_timeout_)) {
// If it's a SpaceCadet-type key, it takes on its primary value, otherwise
// it takes on its secondary value.
Key event_key = isModifierKey(queue_head_.primary_key) ?
queue_head_.primary_key : queue_head_.alternate_key;
flushEvent(event_key);
}
return QUKEY_NOT_FOUND;
return EventHandlerResult::OK;
}
// flush a single entry from the head of the queue
bool Qukeys::flushKey(bool qukey_state, uint8_t keyswitch_state) {
int8_t qukey_index = lookupQukey(key_queue_[0].addr);
bool is_qukey = (qukey_index != QUKEY_NOT_FOUND);
KeyAddr key_addr(key_queue_[0].addr);
Key keycode = Layer.lookupOnActiveLayer(key_addr);
bool is_dual_use = isDualUse(keycode);
if (is_qukey || is_dual_use) {
if (qukey_state == QUKEY_STATE_PRIMARY) {
// If there's a release delay in effect, and there's at least one key after it in
// the queue, delay this key's release event:
if (release_delay_ > 0 && key_queue_length_ > 1
&& delayed_qukey_addr_ == QUKEY_UNKNOWN_ADDR) {
delayed_qukey_start_time_ = millis();
// Store the delayed key's address to send the toggle-off event later, if
// appropriate:
delayed_qukey_addr_ = key_queue_[0].addr;
return false;
// -----------------------------------------------------------------------------
// 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
// an event has already been flushed. It's not perfect because we might be
// getting more than one event in a given cycle, and because the queue might
// overflow, but those are both rare cases, and should not cause any serious
// problems even when they do come up.
bool Qukeys::processQueue() {
// If the queue is empty, signal that the beforeReportingState() process
// should abort before checking for a hold timeout (since there's nothing to
// do).
if (event_queue_.isEmpty()) {
return true;
}
// In other cases, we will want the KeyAddr of the first event in the queue.
KeyAddr queue_head_addr = event_queue_.addr(0);
// 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 now know that the first event is a key press. If it's not a qukey, or if
// it's only there because the plugin was just turned off, we can flush it
// immediately.
if (! isQukey(queue_head_addr) || ! active_) {
flushEvent(queue_head_.primary_key);
return true;
}
// Now we know that the first event is a key press, and that it's a qukey. In
// addition, `queue_head_` now contains the primary and secondary Key values
// for that qukey.
// This variable will be used to record the index in the event queue of the
// first subsequent key press (after the qukey), if any.
uint8_t next_keypress_index{0};
// Next we record if the qukey (at the head of the queue) is a SpaceCadet-type
// key, so we don't need to do it repeatedly later.
bool qukey_is_spacecadet = isModifierKey(queue_head_.primary_key);
// Now we search the queue for events that will let us decide if the qukey
// should be flushed (and if so, in which of its two states). We start with
// the second event in the queue (index 1).
for (uint8_t i{1}; i < event_queue_.length(); ++i) {
if (event_queue_.isPress(i)) {
// If some other key was pressed after a SpaceCadet key, that means the
// SpaceCadet qukey press should be flushed immediately, in its primary
// (modifier) state. SpaceCadet keys only fall into their alternate state
// if they are pressed and released with no rollover.
if (qukey_is_spacecadet) {
flushEvent(queue_head_.primary_key);
return true;
}
keycode = getDualUsePrimaryKey(keycode);
}
if (qukey_state == QUKEY_STATE_ALTERNATE) {
if (is_dual_use) {
keycode = getDualUseAlternateKey(keycode);
} else { // is_qukey
keycode = qukeys[qukey_index].alt_keycode;
// Otherwise, we've found a subsequent key press, so we record it for the
// overlap comparison later, unless we've already done so.
if (next_keypress_index == 0) {
next_keypress_index = i;
}
continue;
}
}
// Before calling handleKeyswitchEvent() below, make sure Qukeys knows not to handle
// these events:
flushing_queue_ = true;
// Now we know the event `i` is a key release. Next, we check to see if it
// is a release of the qukey.
if (event_queue_.addr(i) == queue_head_addr) {
// The qukey (at the head of the queue) was released. If it is a
// SpaceCadet key, or if no rollover compensation is being used, we can
// flush it now. Its state depends on whether or not it's a
// SpaceCadet-type key.
if (next_keypress_index == 0 || overlap_threshold_ == 0) {
Key event_key = qukey_is_spacecadet ?
queue_head_.alternate_key : queue_head_.primary_key;
flushEvent(event_key);
return true;
}
// Now we know the qukey has been released, but we need to check to see if
// it's release should continue to be delayed during rollover -- if the
// subsequent key is released soon enough after the qukey is released, it
// will meet the maximum overlap requirement to make the qukey take on its
// alternate state.
uint16_t overlap_start = event_queue_.timestamp(next_keypress_index);
uint16_t overlap_end = event_queue_.timestamp(i);
if (releaseDelayed(overlap_start, overlap_end)) {
continue;
}
// The subsequent key was held long enough that the qukey can now be
// flushed in its primary state. We're treating the rollover as normal
// typing rollover, not deliberate chording.
flushEvent(queue_head_.primary_key);
return true;
}
// Since we're in the middle of the key scan, we don't necessarily
// have a full HID report, and we don't want to accidentally turn
// off keys that the scan hasn't reached yet, so we force the
// current report to be the same as the previous one, then proceed
HID_KeyboardReport_Data_t curr_hid_report;
// First, save the current report & previous report's modifiers
memcpy(&curr_hid_report, &Keyboard.keyReport, sizeof(curr_hid_report));
byte prev_hid_report_modifiers = Keyboard.lastKeyReport.modifiers;
// Next, copy the old report
memcpy(&Keyboard.keyReport, &Keyboard.lastKeyReport, sizeof(Keyboard.keyReport));
// Instead of just calling pressKey here, we start processing the
// key again, as if it was just pressed, and mark it as injected, so
// we can ignore it and don't start an infinite loop. It would be
// nice if we could use key_state to also indicate which plugin
// injected the key.
handleKeyswitchEvent(keycode, key_addr, IS_PRESSED);
// Now we send the report (if there were any changes)
hid::sendKeyboardReport();
// Next, we restore the current state of the report
memcpy(&Keyboard.keyReport, &curr_hid_report, sizeof(curr_hid_report));
// Last, if the key is still down, add its code back in
if (keyswitch_state & IS_PRESSED) {
handleKeyswitchEvent(keycode, key_addr, IS_PRESSED | WAS_PRESSED);
} else {
// If this is the key that was released, send that release event now
handleKeyswitchEvent(Key_NoKey, key_addr, WAS_PRESSED);
// ...and if there's another key in the queue that's about to also be
// flushed, we need to do something to clear this one's modifier flags (if
// any) from the previous report
if (key_queue_length_ > 1) {
// Restore the previous report; whatever was added by this key flush
// should not appear in the next one, because this key has now been
// released. This is necessary to handle the case where a qukey's primary
// key value has a modifier flag. Because we copy the last report
// directly, we're bypassing the mod-flag rollover protection offered by
// the HIDAdapter. Unfortunately, this does not help if we're rolling
// over multiple keys, and one of the unreleased ones has a mod flag.
// That's probably rare enough that it won't be noticed, however. THIS IS
// AN UGLY HACK, AND IT SHOULD BE FIXED WITH SOMETHING BETTER EVENTUALLY.
// Doing it right will most likely involve either major changes in
// KeyboardioHID or Kaleidoscope itself.
Keyboard.lastKeyReport.modifiers = prev_hid_report_modifiers;
// Event `i` is a key release of some other key than the qukey. Now we check
// to see if it's also a key that was pressed subsequent to the press of the
// qukey. We search from the next event after the qukey was pressed, and
// stop when we get to the release event we're currently looking at.
for (uint8_t j{1}; j < i; ++j) {
// If we find an event with a matching KeyAddr, that means there are two
// events for the same key in the queue after the qukey was pressed. Since
// the second (or maybe third) event `i` is a key release, even if `j` is
// not a key press, there must be one in the queue, so it shouldn't be
// necessary to confirm that `j` is a actually a key press.
if (event_queue_.addr(j) == event_queue_.addr(i)) {
flushEvent(queue_head_.alternate_key);
return true;
}
}
}
// Now that we're done sending the report(s), Qukeys can process events again:
flushing_queue_ = false;
// Shift the queue, so key_queue[0] is always the first key that gets processed
for (byte i = 0; i < key_queue_length_; i++) {
key_queue_[i] = key_queue_[i + 1];
// Last, since our event queue is finite, we must make sure that there's
// always room to add another event to the queue by flushing one whenever the
// queue fills up. We could get multiple events in the same cycle, so this is
// necessary to avoid reading and writing past the end of the array.
if (event_queue_.isFull()) {
flushEvent(queue_head_.primary_key);
return true;
}
key_queue_length_--;
return true;
}
// flushQueue() is called when a key that's in the key_queue is
// released. This means that all the keys ahead of it in the queue are
// still being held, so first we flush them, then we flush the
// released key (with different parameters).
void Qukeys::flushQueue(int8_t index) {
if (index == QUKEY_NOT_FOUND)
return;
for (int8_t i = 0; i < index; i++) {
if (key_queue_length_ == 0)
return;
flushKey(QUKEY_STATE_ALTERNATE, IS_PRESSED | WAS_PRESSED);
}
flushKey(QUKEY_STATE_PRIMARY, WAS_PRESSED);
// If we got here, that means we're still waiting for an event (or a timeout)
// that will determine the state of the qukey. We do know that the event at
// the head of the queue is a qukey press, and that the `queue_head_.*_key`
// values are valid. We return false to let the `beforeReportingState()` hook
// check for hold timeout.
return false;
}
// Flush all the non-qukey keys from the front of the queue
void Qukeys::flushQueue() {
// flush keys until we find a qukey:
while (key_queue_length_ > 0 && !isQukey(key_queue_[0].addr)) {
if (flushKey(QUKEY_STATE_PRIMARY, IS_PRESSED | WAS_PRESSED) == false)
break;
}
}
inline
bool Qukeys::isQukey(uint8_t addr) {
return (isDualUse(addr) || lookupQukey(addr) != QUKEY_NOT_FOUND);
// Flush one event from the head of the queue, with the specified Key value.
void Qukeys::flushEvent(Key event_key) {
// First we record the address and state of the event:
KeyAddr queue_head_addr = event_queue_.addr(0);
uint8_t keyswitch_state = event_queue_.isRelease(0) ? WAS_PRESSED : IS_PRESSED;
// Remove the head event from the queue:
event_queue_.shift();
// This ensures that the flushed event will be ignored by the event handler hook:
flushing_queue_ = true;
handleKeyswitchEvent(event_key, queue_head_addr, keyswitch_state);
flushing_queue_ = false;
}
EventHandlerResult Qukeys::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
// If key_addr is not a physical key, ignore it; some other plugin injected it
if (!key_addr.isValid() || (key_state & INJECTED) != 0)
return EventHandlerResult::OK;
// Test if the key at address `k` is a qukey. As a side effect, if it is, cache
// that qukey's primary and alternate `Key` values for use later. We do this
// because it's much more efficient than doing that as a separate step.
bool Qukeys::isQukey(KeyAddr k) {
// First, look up the value from the keymap. We need to do a full lookup, not
// just looking up the cached value (i.e. `Layer.lookup(k)`), because the
// cached value will be out of date if a layer change happened since the
// keyswitch toggled on.
Key key = Layer.lookupOnActiveLayer(k);
// If Qukeys is turned off, continue to next plugin
if (!active_) {
mapped_key = getDualUsePrimaryKey(mapped_key);
return EventHandlerResult::OK;
// Next, we check to see if this is a DualUse-type qukey (defined in the keymap)
if (isDualUseKey(key)) {
return true;
}
// get key addr & qukey (if any)
int8_t qukey_index = lookupQukey(key_addr.toInt());
// If the key was injected (from the queue being flushed)
if (flushing_queue_) {
return EventHandlerResult::OK;
// Last, we check the qukeys array for a match
uint8_t layer_index = Layer.lookupActiveLayer(k);
for (uint8_t i{0}; i < qukeys_count_; ++i) {
Qukey qukey = cloneFromProgmem(qukeys_[i]);
if (qukey.addr == k) {
if ((qukey.layer == layer_index) ||
(qukey.layer == layer_wildcard)) {
queue_head_.primary_key = key;
queue_head_.alternate_key = qukey.alternate_key;
return true;
}
}
}
// If the key was just pressed:
if (keyToggledOn(key_state)) {
// If the queue is empty and the key isn't a qukey, proceed:
if (key_queue_length_ == 0 &&
! isDualUse(mapped_key) &&
qukey_index == QUKEY_NOT_FOUND) {
return EventHandlerResult::OK;
}
// If no matches were found, clear queue_head_ and return false
queue_head_.primary_key = key;
queue_head_.alternate_key = Key_Transparent;
return false;
}
// Otherwise, queue the key and stop processing:
enqueue(key_addr.toInt());
// flushQueue() has already handled this key release
return EventHandlerResult::EVENT_CONSUMED;
}
// Specific test for DualUse keys (in-keymap qukeys); this is a separate
// function because it gets called on its own when Qukeys is turned off. Like
// isQukey(), it sets `queue_head_.*` as a side effect.
bool Qukeys::isDualUseKey(Key key) {
// Test for DualUse modifiers:
if (key >= ranges::DUM_FIRST && key <= ranges::DUM_LAST) {
key.raw -= ranges::DUM_FIRST;
// In all other cases, we need to know if the key is queued already
int8_t queue_index = searchQueue(key_addr.toInt());
queue_head_.primary_key = key;
queue_head_.primary_key.flags = 0;
// If the key was just released:
if (keyToggledOff(key_state)) {
// If the key isn't in the key_queue, proceed
if (queue_index == QUKEY_NOT_FOUND) {
return EventHandlerResult::OK;
}
// Finally, send the release event of the delayed qukey, if any. This is necessary in
// order to send a toggle off of a `ShiftToLayer()` key; otherwise, that layer gets
// stuck on if there's a release delay and a rollover.
if (delayed_qukey_addr_ != QUKEY_UNKNOWN_ADDR) {
flushQueue(queue_index);
flushQueue();
flushing_queue_ = true;
handleKeyswitchEvent(Key_NoKey, KeyAddr(delayed_qukey_addr_), WAS_PRESSED);
flushing_queue_ = false;
delayed_qukey_addr_ = QUKEY_UNKNOWN_ADDR;
} else {
flushQueue(queue_index);
flushQueue();
}
//if (delayed_qukey_addr_ != QUKEY_UNKNOWN_ADDR)
// return EventHandlerResult::EVENT_CONSUMED;
mapped_key = getDualUsePrimaryKey(mapped_key);
return EventHandlerResult::OK;
queue_head_.alternate_key.raw = key.flags + Key_LeftControl.keyCode;
return true;
}
// Test for DualUse layer shifts:
if (key >= ranges::DUL_FIRST && key <= ranges::DUL_LAST) {
key.raw -= ranges::DUL_FIRST;
// Otherwise, the key is still pressed
queue_head_.primary_key = key;
queue_head_.primary_key.flags = 0;
// Only keys in the queue can still evaluate as qukeys, so all we need to do here is
// block events for held keys that are still in the queue.
if (queue_index == QUKEY_NOT_FOUND) {
// The key is not in the queue; proceed:
return EventHandlerResult::OK;
} else {
// The key is still in the queue; abort:
return EventHandlerResult::EVENT_CONSUMED;
int8_t layer = key.flags;
queue_head_.alternate_key = ShiftToLayer(layer);
return true;
}
// It's not a DualUse Key:
return false;
}
EventHandlerResult Qukeys::beforeReportingState() {
uint16_t current_time = millis();
// Return true if the release of the qukey still needs to be delayed due to
// rollover. This is called when a qukey is released before a subsequent key,
// and that key is still being held. It checks to see if the subsequent key has
// been held long enough that the qukey should be flushed in its primary state
// (in which case we return `false`).
bool Qukeys::releaseDelayed(uint16_t overlap_start,
uint16_t overlap_end) const {
// We want to calculate the timeout by dividing the overlap duration by the
// percentage required to make the qukey take on its alternate state. Since
// we're doing integer arithmetic, we need to first multiply by 100, then
// divide by the percentage value (as an integer). We use 32-bit integers
// here to make sure it doesn't overflow when we multiply by 100.
uint32_t overlap_duration = overlap_end - overlap_start;
uint32_t release_timeout = (overlap_duration * 100) / overlap_threshold_;
return !Kaleidoscope.hasTimeExpired(overlap_start, uint16_t(release_timeout));
}
if (delayed_qukey_addr_ != QUKEY_UNKNOWN_ADDR) {
int16_t diff_time = current_time - delayed_qukey_start_time_;
if (diff_time > release_delay_) {
flushKey(QUKEY_STATE_PRIMARY, WAS_PRESSED);
flushQueue();
// If the release delay has timed out, we need to prevent the wrong toggle-off
// event from being sent:
delayed_qukey_addr_ = QUKEY_UNKNOWN_ADDR;
}
return EventHandlerResult::OK;
}
// If the qukey has been held longer than the time limit, set its
// state to the alternate keycode and add it to the report
while (key_queue_length_ > 0) {
if ((current_time - key_queue_[0].start_time) > time_limit_) {
flushKey(QUKEY_STATE_ALTERNATE, IS_PRESSED | WAS_PRESSED);
flushQueue();
} else {
break;
// Helper function for `beforeReportingState()`. In order to restore a key that
// has been physically released, but whose release event has not yet been
// flushed from the queue, we need to be able to search the queue for a given
// release event's corresponding press event, to add only those holds that
// should still be present.
bool Qukeys::isKeyAddrInQueueBeforeIndex(KeyAddr k, uint8_t index) const {
for (uint8_t i{0}; i < index; ++i) {
if (event_queue_.addr(i) == k) {
return true;
}
}
return EventHandlerResult::OK;
return false;
}
EventHandlerResult Qukeys::onSetup() {
// initializing the key_queue seems unnecessary, actually
for (int8_t i = 0; i < QUKEYS_QUEUE_MAX; i++) {
key_queue_[i].addr = QUKEY_UNKNOWN_ADDR;
key_queue_[i].start_time = 0;
}
key_queue_length_ = 0;
return EventHandlerResult::OK;
// -----------------------------------------------------------------------------
// This function could get lifted into Kaleidoscope proper, since it might be
// more generally useful. It's here to provide the test for a SpaceCadet-type
// qukey, which is any Qukey that has a modifier (including layer shifts) as its
// primary value.
bool isModifierKey(Key key) {
// If it's a plain keyboard key, return true if its base keycode is a
// modifier, otherwise return false:
if ((key.flags & (SYNTHETIC | RESERVED)) == 0) {
return (key.keyCode >= HID_KEYBOARD_FIRST_MODIFIER &&
key.keyCode <= HID_KEYBOARD_LAST_MODIFIER);
}
// If it's a layer shift key, return true:
if (key.flags == (SYNTHETIC | SWITCH_TO_KEYMAP) &&
key.keyCode >= LAYER_SHIFT_OFFSET) {
return true;
}
// In all other cases, return false:
return false;
}
} // namespace plugin {

@ -1,6 +1,6 @@
/* -*- mode: c++ -*-
* Kaleidoscope-Qukeys -- Assign two keycodes to a single key
* Copyright (C) 2017 Michael Richters
* Copyright (C) 2017-2019 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
@ -20,25 +20,9 @@
#include <Kaleidoscope.h>
#include <Kaleidoscope-Ranges.h>
#include "kaleidoscope/plugin/Qukeys/KeyAddrEventQueue.h"
// Maximum length of the pending queue
#define QUKEYS_QUEUE_MAX 8
// Boolean values for storing qukey state
#define QUKEY_STATE_PRIMARY false
#define QUKEY_STATE_ALTERNATE true
// Initialization addr value for empty key_queue. This seems to be
// unnecessary, because we rely on keeping track of the lenght of the
// queue, anyway.
#define QUKEY_UNKNOWN_ADDR 0xFF
// Value to return when no match is found in Qukeys.dict. A successful
// match returns an index in the array, so this must be negative. Also
// used for failed search of the key_queue.
#define QUKEY_NOT_FOUND -1
// Wildcard value; this matches any layer
#define QUKEY_ALL_LAYERS -1
// DualUse Key definitions for Qukeys in the keymap
#define MT(mod, key) Key( \
kaleidoscope::ranges::DUM_FIRST + \
(((Key_ ## mod).keyCode - Key_LeftControl.keyCode) << 8) + \
@ -51,90 +35,184 @@
#define LT(layer, key) Key(kaleidoscope::ranges::DUL_FIRST + (layer << 8) + (Key_ ## key).keyCode)
#define _DEPRECATED_MESSAGE_QUKEY_ROW_COL_CONSTRUCTOR \
"The `Qukey(layer, row, col, alternate_key)` constructor using separate\n" \
"`row` & `col` parameters has been deprecated. Please replace this\n" \
"constructor with the new `KeyAddr` version:\n" \
" `Qukey(layer, KeyAddr(row, col), alternate_key)`"
#define _DEPRECATED_MESSAGE_QUKEYS_TIMEOUT \
"The Qukeys.setTimeout() function has been renamed to setHoldTimeout()\n" \
"in order to distinguish it from the other timeouts more clearly."
#define _DEPRECATED_MESSAGE_QUKEYS_RELEASEDELAY \
"The Qukeys.setReleaseDelay() is now obsolete. The rollover grace period\n" \
"for qukey release has been replaced with an improved version based on\n" \
"the percentage of overlap between the qukey and the subsequent\n" \
"key. Please use the setOverlapThreshold() function instead."
namespace kaleidoscope {
namespace plugin {
// Data structure for an individual qukey
struct Qukey {
public:
Qukey(void) {}
Qukey(int8_t layer, KeyAddr key_addr, Key alt_keycode);
DEPRECATED(ROW_COL_FUNC) Qukey(int8_t layer, byte row, byte col, Key alt_keycode)
: Qukey(layer, KeyAddr(row, col), alt_keycode) {}
// The layer this qukey is mapped on.
int8_t layer;
uint8_t addr;
Key alt_keycode;
// The keyswitch address of the qukey.
KeyAddr addr;
// The alternake Key value this qukey should use (when held).
Key alternate_key;
// This is the constructor that should be used when creating a Qukey object in
// the PROGMEM array that will be used by Qukeys (i.e. in the `QUKEYS()`
// macro).
constexpr
Qukey(int8_t layer, KeyAddr k, Key alternate_key)
: layer(layer), addr(k), alternate_key(alternate_key) {}
// This constructor is here so that we can create an empty Qukey object in RAM
// into which we can copy the values from a PROGMEM Qukey object.
Qukey() = default;
// Old-style Qukey constructor for backwards compatibility.
DEPRECATED(QUKEY_ROW_COL_CONSTRUCTOR)
constexpr
Qukey(int8_t layer, uint8_t row, uint8_t col, Key alternate_key)
: layer(layer), addr(KeyAddr(row, col)), alternate_key(alternate_key) {}
};
// Data structure for an entry in the key_queue
struct QueueItem {
uint8_t addr; // keyswitch coordinates
uint16_t start_time; // time a queued key was pressed
};
// The plugin itself
class Qukeys : public kaleidoscope::Plugin {
// I could use a bitfield to get the state values, but then we'd
// have to check the key_queue (there are three states). Or use a
// second bitfield for the indeterminite state. Using a bitfield
// would enable storing the qukey list in PROGMEM, but I don't know
// if the added complexity is worth it.
public:
Qukeys(void);
static void activate(void) {
public:
// Methods for turning the plugin on and off.
void activate() {
active_ = true;
}
static void deactivate(void) {
void deactivate() {
active_ = false;
}
static void toggle(void) {
void toggle() {
active_ = !active_;
}
static void setTimeout(uint16_t time_limit) {
time_limit_ = time_limit;
// Set the timeout (in milliseconds) for a held qukey. If a qukey is held at
// least this long, it will take on its alternate value (unless its primary
// value is a modifier -- i.e. a SpaceCadet type key).
void setHoldTimeout(uint16_t hold_timeout) {
hold_timeout_ = hold_timeout;
}
// 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
// key `K`, and the overlap of the two keys (`O`) is less than this
// percentage, the qukey will take on its primary value, but if it's greater
// (i.e. `K` spends a very small fraction of its time pressed after the `Q` is
// released), the qukey will take on its alternate value.
void setOverlapThreshold(uint8_t percentage) {
// Only percentages less than 100% are meaningful. 0% means to turn off the
// rollover grace period and rely solely on release order to determine the
// qukey's state.
if (percentage < 100) {
overlap_threshold_ = percentage;
} else {
overlap_threshold_ = 0;
}
}
// Function for defining the array of qukeys data (in PROGMEM). It's a
// template function that takes as its sole argument an array reference of
// size `_qukeys_count`, so there's no need to use `sizeof` to calculate the
// correct size, and pass it as a separate parameter.
template <uint8_t _qukeys_count>
void configureQukeys(Qukey const(&qukeys)[_qukeys_count]) {
qukeys_ = qukeys;
qukeys_count_ = _qukeys_count;
}
// Obsolete configuration functions.
DEPRECATED(QUKEYS_TIMEOUT)
void setTimeout(uint16_t time_limit) {
setHoldTimeout(time_limit);
}
static void setReleaseDelay(uint8_t release_delay) {
release_delay_ = release_delay;
DEPRECATED(QUKEYS_RELEASEDELAY)
void setReleaseDelay(uint8_t release_delay) {
if (release_delay == 0) {
overlap_threshold_ = 0;
} else {
overlap_threshold_ = 60;
}
}
static Qukey * qukeys;
static uint8_t qukeys_count;
// A wildcard value for a qukey that exists on every layer.
static constexpr int8_t layer_wildcard{-1};
EventHandlerResult onSetup();
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
// Kaleidoscope hook functions.
EventHandlerResult onKeyswitchEvent(Key &mapped_key,
KeyAddr key_addr,
uint8_t key_state);
EventHandlerResult beforeReportingState();
private:
static bool active_;
static uint16_t time_limit_;
static uint8_t release_delay_;
static QueueItem key_queue_[QUKEYS_QUEUE_MAX];
static uint8_t key_queue_length_;
static bool flushing_queue_;
static uint8_t delayed_qukey_addr_;
static int16_t delayed_qukey_start_time_;
static int8_t lookupQukey(uint8_t key_addr);
static void enqueue(uint8_t key_addr);
static int8_t searchQueue(uint8_t key_addr);
static bool flushKey(bool qukey_state, uint8_t keyswitch_state);
static void flushQueue(int8_t index);
static void flushQueue(void);
static bool isQukey(uint8_t addr);
// An array of Qukey objects in PROGMEM.
Qukey const * qukeys_{nullptr};
uint8_t qukeys_count_{0};
// The maximum number of events in the queue at a time.
static constexpr uint8_t queue_capacity_{8};
// The event queue stores a series of press and release events.
KeyAddrEventQueue<queue_capacity_> event_queue_;
// This determines whether the plugin is on or off.
bool active_{true};
// This variable stores the percentage number between 0 and 99 that determines
// how forgiving the plugin is of rollover from a qukey to a modified key.
uint8_t overlap_threshold_{80};
// The number of milliseconds until a qukey held on its own will take on its
// alternate state (or primary state, in the case of a SpaceCadet-type qukey).
uint16_t hold_timeout_{250};
// This is a guard against re-processing events when qukeys flushes them from
// its event queue. We can't just use an "injected" key state flag, because
// that would cause other plugins to also ignore the event.
bool flushing_queue_{false};
// A cache of the current qukey's primary and alternate key values, so we
// don't have to keep looking them up from PROGMEM.
struct {
Key primary_key{Key_Transparent};
Key alternate_key{Key_Transparent};
} queue_head_;
// Internal helper methods.
bool processQueue();
void flushEvent(Key event_key);
bool isQukey(KeyAddr k);
bool isDualUseKey(Key key);
bool releaseDelayed(uint16_t overlap_start, uint16_t overlap_end) const;
bool isKeyAddrInQueueBeforeIndex(KeyAddr k, uint8_t index) const;
};
} // namespace plugin {
// This function returns true for any key that we expect to be used chorded with
// a "modified" key. This includes actual keyboard modifiers, but also layer
// shift keys. Used for determining if a qukey is a SpaceCadet-type key.
bool isModifierKey(Key key);
} // namespace plugin {
} // namespace kaleidoscope {
extern kaleidoscope::plugin::Qukeys Qukeys;
// macro for use in sketch file to simplify definition of qukeys
#define QUKEYS(qukey_defs...) { \
static kaleidoscope::plugin::Qukey qk_table[] = { qukey_defs }; \
Qukeys.qukeys = qk_table; \
Qukeys.qukeys_count = sizeof(qk_table) / sizeof(kaleidoscope::plugin::Qukey); \
// Macro for use in sketch file to simplify definition of the qukeys array and
// guarantee that the count is set correctly. This is considerably less
// important than it used to be, with the `configureQukeys()` function taking
// care of guaranteeing the correct count setting.
#define QUKEYS(qukey_defs...) { \
static kaleidoscope::plugin::Qukey const qk_table[] PROGMEM = { \
qukey_defs \
}; \
Qukeys.configureQukeys(qk_table); \
}

@ -0,0 +1,115 @@
// -*- mode: c++ -*-
/* Kaleidoscope - Firmware for computer input devices
* Copyright (C) 2013-2019 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Arduino.h>
//#include <assert.h>
#include "kaleidoscope/Kaleidoscope.h"
#include "kaleidoscope/KeyAddr.h"
namespace kaleidoscope {
namespace plugin {
// This class defines a keyswitch event queue that stores both press and release
// events, recording the key address, a timestamp, and the keyswitch state
// (press or release). It is optimized for random access to the queue entries,
// so that each property of each entry can be retrieved without fetching any
// other data, in order to best serve the specific needs of the Qukeys
// plugin. Its performance is better for a queue that needs to be searched much
// more frequently than entries are added or removed.
template <uint8_t _capacity,
typename _Bitfield = uint8_t,
typename _Timestamp = uint16_t>
class KeyAddrEventQueue {
static_assert(_capacity <= (sizeof(_Bitfield) * 8),
"EventQueue error: _Bitfield type too small for _capacity!");
private:
uint8_t length_{0};
KeyAddr addrs_[_capacity];
_Timestamp timestamps_[_capacity];
_Bitfield release_event_bits_;
public:
uint8_t length() const {
return length_;
}
bool isEmpty() const {
return (length_ == 0);
}
bool isFull() const {
return (length_ == _capacity);
}
// Queue entry access methods. Note: the caller is responsible for bounds
// checking, because it's expected that a for loop will be used when searching
// the queue, which will terminate when `index >= queue.length()`.
KeyAddr addr(uint8_t index) const {
// assert(index < length_);
return addrs_[index];
}
_Timestamp timestamp(uint8_t index) const {
// assert(index < length_);
return timestamps_[index];
}
bool isRelease(uint8_t index) const {
// assert(index < length_);
return bitRead(release_event_bits_, index);
}
bool isPress(uint8_t index) const {
// assert(index < length_);
return !isRelease(index);
}
// Append a new event on the end of the queue. Note: the caller is responsible
// for bounds checking; we don't guard against it here.
void append(KeyAddr k, uint8_t keyswitch_state) {
// assert(length_ < _capacity);
addrs_[length_] = k;
timestamps_[length_] = Kaleidoscope.millisAtCycleStart();
bitWrite(release_event_bits_, length_, keyToggledOff(keyswitch_state));
++length_;
}
// Remove the first event from the head of the queue, shifting the
// others. This function actually shifts the queue by copying element values,
// rather than using a ring buffer because we expect it will be called much
// less often than the queue is searched via a for loop.
void shift() {
// assert(length > 0);
--length_;
for (uint8_t i{0}; i < length_; ++i) {
addrs_[i] = addrs_[i + 1];
timestamps_[i] = timestamps_[i + 1];
}
release_event_bits_ >>= 1;
}
// Empty the queue entirely.
void clear() {
length_ = 0;
release_event_bits_ = 0;
}
};
} // namespace plugin
} // namespace kaleidoscope

@ -0,0 +1,35 @@
// -*- mode: c++ -*-
/* Kaleidoscope - Firmware for computer input devices
* Copyright (C) 2013-2019 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
// Load any intrinsic data type or trivial class stored in PROGMEM into an
// object of that type in memory.
template <typename _Type>
void loadFromProgmem(_Type const& pgm_object, _Type& object) {
memcpy_P(&object, &pgm_object, sizeof(object));
}
// Copy an object from PROGMEM to RAM. This works as long as the type has a
// suitable constructor that does not require arguments (i.e. "trivial classes")
// or if `_Type` is an intrinsic data type.
template <typename _Type>
_Type cloneFromProgmem(_Type const& pgm_object) {
_Type object;
memcpy_P(&object, &pgm_object, sizeof(object));
return object;
}
Loading…
Cancel
Save