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 5 years ago
parent f6d2e62649
commit 1e58fcf3b8

@ -2,9 +2,26 @@
## Concept ## Concept
This Kaleidoscope plugin allows you to overload keys on your keyboard so that they produce This Kaleidoscope plugin allows you to overload keys on your keyboard so that
one keycode (i.e. symbol) when tapped, and a different keycode -- most likely a modifier they produce one keycode (i.e. symbol) when tapped, and a different keycode --
(e.g. `shift` or `alt`) -- when held. 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 ## Setup
@ -18,23 +35,23 @@ one keycode (i.e. symbol) when tapped, and a different keycode -- most likely a
KALEIDOSCOPE_INIT_PLUGINS(Qukeys); KALEIDOSCOPE_INIT_PLUGINS(Qukeys);
``` ```
- Define some `Qukeys` of the format `Qukey(layer, key_addr, alt_keycode)` - 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 Layers and key addresses are all zero-indexed, in key addresses rows are top to bottom and
columns are left to right): columns are left to right:
- For the Keyboardio Model 01, key coordinates refer to [this header - 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). file](https://github.com/keyboardio/Kaleidoscope-Hardware-Model01/blob/f469015346535cb864a340bf8eb317d268943248/src/Kaleidoscope-Hardware-Model01.h#L267-L279).
``` ```
QUKEYS( QUKEYS(
// l, r, c, alt_keycode // left-side modifiers
kaleidoscope::plugin::Qukey(0, 2, 1, Key_LeftGui), // A/cmd kaleidoscope::plugin::Qukey(0, KeyAddr(2, 1), Key_LeftGui), // A
kaleidoscope::plugin::Qukey(0, 2, 2, Key_LeftAlt), // S/alt kaleidoscope::plugin::Qukey(0, KeyAddr(2, 2), Key_LeftAlt), // S
kaleidoscope::plugin::Qukey(0, 2, 3, Key_LeftControl), // D/ctrl kaleidoscope::plugin::Qukey(0, KeyAddr(2, 3), Key_LeftControl), // D
kaleidoscope::plugin::Qukey(0, 2, 4, Key_LeftShift), // F/shift kaleidoscope::plugin::Qukey(0, KeyAddr(2, 4), Key_LeftShift), // F
kaleidoscope::plugin::Qukey(0, 1, 14, Key_LeftShift), // P/shift // left-side layer shifts
kaleidoscope::plugin::Qukey(0, 3, 15, Key_LeftShift) // Minus/shift 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 `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 ## 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 > The `percentage` parameter should be between `1` and `100` (`75` means 75%),
> they modify (almost) simultaneously, since the Qukey may be detected as released > and represents the fraction of the _subsequent_ key press's duration that
> *slightly* before the other key, which would not trigger the desired alternate keycode. > 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 > Setting `percentage` to a low value (e.g. `30`) will result in a longer grace
> modifier to further keystrokes. > 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()` ### `.activate()`
### `.deactivate()` ### `.deactivate()`
### `.toggle()` ### `.toggle()`
> activate/deactivate `Qukeys` > Activate/deactivate `Qukeys` plugin.
### DualUse key definitions ### 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 > must be a plain old key, and can't have any modifiers or anything else
> applied. > 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 There is a special value (`Qukeys::layer_wildcard`) that can be used in place of
queue, and no new reports are sent to the host. Once a condition is met, the `Qukey` is the layer number in the definition of a `Qukey`. This will define a qukey with
flushed from the queue, and so are any subsequent keypresses (up to, but not including, the given alternate value on all layers, regardless of what the primary value is
the next `Qukey` that is still pressed). 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 ## Design & Implementation
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 When a qukey is pressed, it doesn't immediately add a corresponding keycode to
limit, you won't get any unintended alternate keycodes. 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 ## Further reading

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

@ -1,6 +1,6 @@
/* -*- mode: c++ -*- /* -*- mode: c++ -*-
* Kaleidoscope-Qukeys -- Assign two keycodes to a single key * 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 * 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 * 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/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "kaleidoscope/plugin/Qukeys.h"
#include <Kaleidoscope.h> #include <Kaleidoscope.h>
#include <Kaleidoscope-Qukeys.h>
#include <kaleidoscope/hid.h>
#include <MultiReport/Keyboard.h>
#include <Kaleidoscope-Ranges.h> #include <Kaleidoscope-Ranges.h>
#include "kaleidoscope/progmem_helpers.h"
#ifdef KALEIDOSCOPE_VIRTUAL_BUILD
#define debug_print(...) printf(__VA_ARGS__)
#else
#define debug_print(...)
#endif
namespace kaleidoscope { namespace kaleidoscope {
namespace plugin { namespace plugin {
inline // This is the event handler. It ignores certain events, but mostly just adds
bool isDualUse(Key k) { // them to the Qukeys event queue.
if (k.raw < ranges::DU_FIRST || k.raw > ranges::DU_LAST) EventHandlerResult Qukeys::onKeyswitchEvent(Key& key, KeyAddr k, uint8_t key_state) {
return false; // If k is not a physical key, ignore it; some other plugin injected it.
return true; if (! k.isValid() || (key_state & INJECTED) != 0) {
} return EventHandlerResult::OK;
}
inline // If the key was injected (from the queue being flushed), we need to ignore
bool isDualUse(byte key_addr_offset) { // it.
KeyAddr key_addr(key_addr_offset); if (flushing_queue_) {
Key k = Layer.lookup(key_addr); return EventHandlerResult::OK;
return isDualUse(k); }
}
Key getDualUsePrimaryKey(Key k) { // If Qukeys is turned off, continue to next plugin.
if (k.raw >= ranges::DUM_FIRST && k.raw <= ranges::DUM_LAST) { if (! active_) {
k.raw -= ranges::DUM_FIRST; if (isDualUseKey(key)) {
k.flags = 0; key = queue_head_.primary_key;
} else if (k.raw >= ranges::DUL_FIRST && k.raw <= ranges::DUL_LAST) { }
k.raw -= ranges::DUL_FIRST; return EventHandlerResult::OK;
k.flags = 0;
} }
return k;
}
Key getDualUseAlternateKey(Key k) { // Deal with keyswitch state changes.
if (k.raw >= ranges::DUM_FIRST && k.raw <= ranges::DUM_LAST) { if (keyToggledOn(key_state) || keyToggledOff(key_state)) {
k.raw -= ranges::DUM_FIRST; // If we can't trivially ignore the event, just add it to the queue.
k.raw = (k.raw >> 8) + Key_LeftControl.keyCode; event_queue_.append(k, key_state);
} else if (k.raw >= ranges::DUL_FIRST && k.raw <= ranges::DUL_LAST) { // In order to prevent overflowing the queue, process it now.
k.raw -= ranges::DUL_FIRST; if (event_queue_.isFull()) {
byte layer = k.flags; processQueue();
// Should be `ShiftToLayer(layer)`, but that gives "narrowing conversion" }
// warnings that I can't figure out how to resolve // Any event that gets added to the queue gets re-processed later, so we
k.keyCode = layer + LAYER_SHIFT_OFFSET; // need to abort processing now.
k.flags = KEY_FLAGS | SYNTHETIC | SWITCH_TO_KEYMAP; 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) { // Either this key doesn't have an event in the queue at all, or its first
this->layer = layer; // event in the queue is a release. We treat the key as a normal held key.
this->addr = key_addr.toInt(); return EventHandlerResult::OK;
this->alt_keycode = alt_keycode;
} }
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; // 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
// Empty constructor; nothing is stored at the instance level // cycle, because the keyboard HID report can't store all of the information
Qukeys::Qukeys(void) {} // necessary to correctly handle all of the rollover corner cases.
EventHandlerResult Qukeys::beforeReportingState() {
int8_t Qukeys::lookupQukey(uint8_t key_addr_offset) { // For keys that have been physically released, but whose release events are
if (key_addr_offset == QUKEY_UNKNOWN_ADDR) { // still waiting to be flushed from the queue, we need to restore them,
return QUKEY_NOT_FOUND; // 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;
} }
for (int8_t i = 0; i < qukeys_count; i++) { flushing_queue_ = true;
if (qukeys[i].addr == key_addr_offset) { handleKeyswitchEvent(Key_NoKey, k, IS_PRESSED | WAS_PRESSED);
KeyAddr key_addr(key_addr_offset); flushing_queue_ = false;
if ((qukeys[i].layer == QUKEY_ALL_LAYERS) ||
(qukeys[i].layer == Layer.lookupActiveLayer(key_addr))) {
return i;
} }
} }
// 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;
} }
return QUKEY_NOT_FOUND;
}
void Qukeys::enqueue(uint8_t key_addr) { // If we get here, that means that the first event in the queue is a qukey
if (key_queue_length_ == QUKEYS_QUEUE_MAX) { // press. All that's left to do is to check if it's been held long enough that
flushKey(QUKEY_STATE_PRIMARY, IS_PRESSED | WAS_PRESSED); // it has timed out.
flushQueue(); 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);
} }
// default to alternate state to stop keys being flushed from the queue before the grace return EventHandlerResult::OK;
// 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;
// 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;
} }
return QUKEY_NOT_FOUND;
}
// flush a single entry from the head of the queue // In other cases, we will want the KeyAddr of the first event in the queue.
bool Qukeys::flushKey(bool qukey_state, uint8_t keyswitch_state) { KeyAddr queue_head_addr = event_queue_.addr(0);
int8_t qukey_index = lookupQukey(key_queue_[0].addr);
bool is_qukey = (qukey_index != QUKEY_NOT_FOUND); // If that first event is a key release, it can be flushed right away.
KeyAddr key_addr(key_queue_[0].addr); if (event_queue_.isRelease(0)) {
Key keycode = Layer.lookupOnActiveLayer(key_addr); flushEvent(Key_NoKey);
bool is_dual_use = isDualUse(keycode); return true;
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;
} }
keycode = getDualUsePrimaryKey(keycode);
// 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;
} }
if (qukey_state == QUKEY_STATE_ALTERNATE) {
if (is_dual_use) { // Now we know that the first event is a key press, and that it's a qukey. In
keycode = getDualUseAlternateKey(keycode); // addition, `queue_head_` now contains the primary and secondary Key values
} else { // is_qukey // for that qukey.
keycode = qukeys[qukey_index].alt_keycode;
// 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;
} }
// 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 // Now we know the event `i` is a key release. Next, we check to see if it
// these events: // is a release of the qukey.
flushing_queue_ = true; if (event_queue_.addr(i) == queue_head_addr) {
// The qukey (at the head of the queue) was released. If it is a
// Since we're in the middle of the key scan, we don't necessarily // SpaceCadet key, or if no rollover compensation is being used, we can
// have a full HID report, and we don't want to accidentally turn // flush it now. Its state depends on whether or not it's a
// off keys that the scan hasn't reached yet, so we force the // SpaceCadet-type key.
// current report to be the same as the previous one, then proceed if (next_keypress_index == 0 || overlap_threshold_ == 0) {
HID_KeyboardReport_Data_t curr_hid_report; Key event_key = qukey_is_spacecadet ?
// First, save the current report & previous report's modifiers queue_head_.alternate_key : queue_head_.primary_key;
memcpy(&curr_hid_report, &Keyboard.keyReport, sizeof(curr_hid_report)); flushEvent(event_key);
byte prev_hid_report_modifiers = Keyboard.lastKeyReport.modifiers; return true;
// Next, copy the old report }
memcpy(&Keyboard.keyReport, &Keyboard.lastKeyReport, sizeof(Keyboard.keyReport)); // Now we know the qukey has been released, but we need to check to see if
// Instead of just calling pressKey here, we start processing the // it's release should continue to be delayed during rollover -- if the
// key again, as if it was just pressed, and mark it as injected, so // subsequent key is released soon enough after the qukey is released, it
// we can ignore it and don't start an infinite loop. It would be // will meet the maximum overlap requirement to make the qukey take on its
// nice if we could use key_state to also indicate which plugin // alternate state.
// injected the key. uint16_t overlap_start = event_queue_.timestamp(next_keypress_index);
handleKeyswitchEvent(keycode, key_addr, IS_PRESSED); uint16_t overlap_end = event_queue_.timestamp(i);
// Now we send the report (if there were any changes) if (releaseDelayed(overlap_start, overlap_end)) {
hid::sendKeyboardReport(); continue;
// 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;
}
}
// 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];
} }
key_queue_length_--; // 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; return true;
} }
// flushQueue() is called when a key that's in the key_queue is // Event `i` is a key release of some other key than the qukey. Now we check
// released. This means that all the keys ahead of it in the queue are // to see if it's also a key that was pressed subsequent to the press of the
// still being held, so first we flush them, then we flush the // qukey. We search from the next event after the qukey was pressed, and
// released key (with different parameters). // stop when we get to the release event we're currently looking at.
void Qukeys::flushQueue(int8_t index) { for (uint8_t j{1}; j < i; ++j) {
if (index == QUKEY_NOT_FOUND) // If we find an event with a matching KeyAddr, that means there are two
return; // events for the same key in the queue after the qukey was pressed. Since
for (int8_t i = 0; i < index; i++) { // the second (or maybe third) event `i` is a key release, even if `j` is
if (key_queue_length_ == 0) // not a key press, there must be one in the queue, so it shouldn't be
return; // necessary to confirm that `j` is a actually a key press.
flushKey(QUKEY_STATE_ALTERNATE, IS_PRESSED | WAS_PRESSED); if (event_queue_.addr(j) == event_queue_.addr(i)) {
} flushEvent(queue_head_.alternate_key);
flushKey(QUKEY_STATE_PRIMARY, WAS_PRESSED); return true;
} }
}
}
// Flush all the non-qukey keys from the front of the queue // Last, since our event queue is finite, we must make sure that there's
void Qukeys::flushQueue() { // always room to add another event to the queue by flushing one whenever the
// flush keys until we find a qukey: // queue fills up. We could get multiple events in the same cycle, so this is
while (key_queue_length_ > 0 && !isQukey(key_queue_[0].addr)) { // necessary to avoid reading and writing past the end of the array.
if (flushKey(QUKEY_STATE_PRIMARY, IS_PRESSED | WAS_PRESSED) == false) if (event_queue_.isFull()) {
break; flushEvent(queue_head_.primary_key);
return true;
} }
}
inline // If we got here, that means we're still waiting for an event (or a timeout)
bool Qukeys::isQukey(uint8_t addr) { // that will determine the state of the qukey. We do know that the event at
return (isDualUse(addr) || lookupQukey(addr) != QUKEY_NOT_FOUND); // 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;
} }
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 // Flush one event from the head of the queue, with the specified Key value.
if (!key_addr.isValid() || (key_state & INJECTED) != 0) void Qukeys::flushEvent(Key event_key) {
return EventHandlerResult::OK; // 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;
// If Qukeys is turned off, continue to next plugin // Remove the head event from the queue:
if (!active_) { event_queue_.shift();
mapped_key = getDualUsePrimaryKey(mapped_key); // This ensures that the flushed event will be ignored by the event handler hook:
return EventHandlerResult::OK; flushing_queue_ = true;
} handleKeyswitchEvent(event_key, queue_head_addr, keyswitch_state);
flushing_queue_ = false;
}
// get key addr & qukey (if any)
int8_t qukey_index = lookupQukey(key_addr.toInt());
// If the key was injected (from the queue being flushed) // Test if the key at address `k` is a qukey. As a side effect, if it is, cache
if (flushing_queue_) { // that qukey's primary and alternate `Key` values for use later. We do this
return EventHandlerResult::OK; // 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 the key was just pressed: // Next, we check to see if this is a DualUse-type qukey (defined in the keymap)
if (keyToggledOn(key_state)) { if (isDualUseKey(key)) {
// If the queue is empty and the key isn't a qukey, proceed: return true;
if (key_queue_length_ == 0 &&
! isDualUse(mapped_key) &&
qukey_index == QUKEY_NOT_FOUND) {
return EventHandlerResult::OK;
} }
// Otherwise, queue the key and stop processing: // Last, we check the qukeys array for a match
enqueue(key_addr.toInt()); uint8_t layer_index = Layer.lookupActiveLayer(k);
// flushQueue() has already handled this key release for (uint8_t i{0}; i < qukeys_count_; ++i) {
return EventHandlerResult::EVENT_CONSUMED; 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;
}
}
} }
// In all other cases, we need to know if the key is queued already // If no matches were found, clear queue_head_ and return false
int8_t queue_index = searchQueue(key_addr.toInt()); queue_head_.primary_key = key;
queue_head_.alternate_key = Key_Transparent;
return false;
}
// If the key was just released: // Specific test for DualUse keys (in-keymap qukeys); this is a separate
if (keyToggledOff(key_state)) { // function because it gets called on its own when Qukeys is turned off. Like
// If the key isn't in the key_queue, proceed // isQukey(), it sets `queue_head_.*` as a side effect.
if (queue_index == QUKEY_NOT_FOUND) { bool Qukeys::isDualUseKey(Key key) {
return EventHandlerResult::OK; // Test for DualUse modifiers:
} if (key >= ranges::DUM_FIRST && key <= ranges::DUM_LAST) {
// Finally, send the release event of the delayed qukey, if any. This is necessary in key.raw -= ranges::DUM_FIRST;
// 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. queue_head_.primary_key = key;
if (delayed_qukey_addr_ != QUKEY_UNKNOWN_ADDR) { queue_head_.primary_key.flags = 0;
flushQueue(queue_index);
flushQueue(); queue_head_.alternate_key.raw = key.flags + Key_LeftControl.keyCode;
flushing_queue_ = true; return 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;
} }
// 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 int8_t layer = key.flags;
// block events for held keys that are still in the queue. queue_head_.alternate_key = ShiftToLayer(layer);
if (queue_index == QUKEY_NOT_FOUND) { return true;
// The key is not in the queue; proceed:
return EventHandlerResult::OK;
} else {
// The key is still in the queue; abort:
return EventHandlerResult::EVENT_CONSUMED;
} }
// 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 // Helper function for `beforeReportingState()`. In order to restore a key that
// state to the alternate keycode and add it to the report // has been physically released, but whose release event has not yet been
while (key_queue_length_ > 0) { // flushed from the queue, we need to be able to search the queue for a given
if ((current_time - key_queue_[0].start_time) > time_limit_) { // release event's corresponding press event, to add only those holds that
flushKey(QUKEY_STATE_ALTERNATE, IS_PRESSED | WAS_PRESSED); // should still be present.
flushQueue(); bool Qukeys::isKeyAddrInQueueBeforeIndex(KeyAddr k, uint8_t index) const {
} else { for (uint8_t i{0}; i < index; ++i) {
break; if (event_queue_.addr(i) == k) {
return true;
} }
} }
return false;
return EventHandlerResult::OK;
} }
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 { } // namespace plugin {

@ -1,6 +1,6 @@
/* -*- mode: c++ -*- /* -*- mode: c++ -*-
* Kaleidoscope-Qukeys -- Assign two keycodes to a single key * 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 * 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 * it under the terms of the GNU General Public License as published by
@ -20,25 +20,9 @@
#include <Kaleidoscope.h> #include <Kaleidoscope.h>
#include <Kaleidoscope-Ranges.h> #include <Kaleidoscope-Ranges.h>
#include "kaleidoscope/plugin/Qukeys/KeyAddrEventQueue.h"
// Maximum length of the pending queue // DualUse Key definitions for Qukeys in the keymap
#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
#define MT(mod, key) Key( \ #define MT(mod, key) Key( \
kaleidoscope::ranges::DUM_FIRST + \ kaleidoscope::ranges::DUM_FIRST + \
(((Key_ ## mod).keyCode - Key_LeftControl.keyCode) << 8) + \ (((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 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 kaleidoscope {
namespace plugin { namespace plugin {
// Data structure for an individual qukey // Data structure for an individual qukey
struct Qukey { struct Qukey {
public: // The layer this qukey is mapped on.
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) {}
int8_t layer; int8_t layer;
uint8_t addr; // The keyswitch address of the qukey.
Key alt_keycode; 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 { 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; active_ = true;
} }
static void deactivate(void) { void deactivate() {
active_ = false; active_ = false;
} }
static void toggle(void) { void toggle() {
active_ = !active_; 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;
} }
static void setReleaseDelay(uint8_t release_delay) {
release_delay_ = release_delay;
} }
static Qukey * qukeys; // Function for defining the array of qukeys data (in PROGMEM). It's a
static uint8_t qukeys_count; // 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;
}
EventHandlerResult onSetup(); // Obsolete configuration functions.
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state); DEPRECATED(QUKEYS_TIMEOUT)
void setTimeout(uint16_t time_limit) {
setHoldTimeout(time_limit);
}
DEPRECATED(QUKEYS_RELEASEDELAY)
void setReleaseDelay(uint8_t release_delay) {
if (release_delay == 0) {
overlap_threshold_ = 0;
} else {
overlap_threshold_ = 60;
}
}
// A wildcard value for a qukey that exists on every layer.
static constexpr int8_t layer_wildcard{-1};
// Kaleidoscope hook functions.
EventHandlerResult onKeyswitchEvent(Key &mapped_key,
KeyAddr key_addr,
uint8_t key_state);
EventHandlerResult beforeReportingState(); EventHandlerResult beforeReportingState();
private: private:
static bool active_; // An array of Qukey objects in PROGMEM.
static uint16_t time_limit_; Qukey const * qukeys_{nullptr};
static uint8_t release_delay_; uint8_t qukeys_count_{0};
static QueueItem key_queue_[QUKEYS_QUEUE_MAX];
static uint8_t key_queue_length_; // The maximum number of events in the queue at a time.
static bool flushing_queue_; static constexpr uint8_t queue_capacity_{8};
static uint8_t delayed_qukey_addr_; // The event queue stores a series of press and release events.
static int16_t delayed_qukey_start_time_; KeyAddrEventQueue<queue_capacity_> event_queue_;
static int8_t lookupQukey(uint8_t key_addr); // This determines whether the plugin is on or off.
static void enqueue(uint8_t key_addr); bool active_{true};
static int8_t searchQueue(uint8_t key_addr);
static bool flushKey(bool qukey_state, uint8_t keyswitch_state); // This variable stores the percentage number between 0 and 99 that determines
static void flushQueue(int8_t index); // how forgiving the plugin is of rollover from a qukey to a modified key.
static void flushQueue(void); uint8_t overlap_threshold_{80};
static bool isQukey(uint8_t addr);
// 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 { } // namespace kaleidoscope {
extern kaleidoscope::plugin::Qukeys Qukeys; extern kaleidoscope::plugin::Qukeys Qukeys;
// macro for use in sketch file to simplify definition of qukeys // 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...) { \ #define QUKEYS(qukey_defs...) { \
static kaleidoscope::plugin::Qukey qk_table[] = { qukey_defs }; \ static kaleidoscope::plugin::Qukey const qk_table[] PROGMEM = { \
Qukeys.qukeys = qk_table; \ qukey_defs \
Qukeys.qukeys_count = sizeof(qk_table) / sizeof(kaleidoscope::plugin::Qukey); \ }; \
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