Skip to content

Commit

Permalink
Rewrite Qukeys plugin from scratch
Browse files Browse the repository at this point in the history
This is a complete rewrite of Qukeys, in order to implement several improvements
and new features:

- A new EventQueue 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.
- 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.

Signed-off-by: Michael Richters <gedankenexperimenter@gmail.com>
  • Loading branch information
gedankenexperimenter committed May 3, 2019
1 parent be860f1 commit 928c6d9
Show file tree
Hide file tree
Showing 6 changed files with 693 additions and 407 deletions.
153 changes: 108 additions & 45 deletions doc/plugin/Qukeys.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions examples/Keystrokes/Qukeys/Qukeys.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
105 changes: 105 additions & 0 deletions src/kaleidoscope/EventQueue.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// -*- 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 "kaleidoscope/Kaleidoscope.h"
#include "kaleidoscope/KeyAddr.h"

namespace kaleidoscope {

// This class defines a generic event queue that stores both key press and
// release events, recording the key address, a timestamp, and the keyswitch
// state (press or release).
template <uint8_t _max_length,
typename _Bitfield = uint8_t,
typename _Timestamp = uint16_t>
class EventQueue {

static_assert(_max_length <= 255,
"EventQueue error: _max_length must be less than 256!");
static_assert(_max_length <= (sizeof(_Bitfield) * 8),
"EventQueue error: _Bitfield type too small for _max_length!");

private:
uint8_t length_{0};
KeyAddr addrs_[_max_length];
_Timestamp timestamps_[_max_length];
_Bitfield release_event_bits_;

public:
uint8_t length() const { return length_; }
bool isEmpty() const { return (length_ == 0); }
bool isFull() const { return (length_ == _max_length); }

KeyAddr addr(uint8_t index) const { return addrs_[index]; }

_Timestamp timestamp(uint8_t index) const { return timestamps_[index]; }

bool isRelease(uint8_t index) const {
return bitRead(release_event_bits_, index);
}
bool isPress(uint8_t index) const { return !isRelease(index); }

// Append a new event on the end of the queue.
void append(KeyAddr k, uint8_t keyswitch_state) {
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.
void shift() {
--length_;
for (uint8_t i{0}; i < length_; ++i) {
addrs_[i] = addrs_[i + 1];
timestamps_[i] = timestamps_[i + 1];
}
release_event_bits_ >>= 1;
}

// Remove an event from the middle of the queue. This is considerably more
// costly than removing the event at the head of the queue with `shift()` (see
// above).
void remove(uint8_t index) {
--length_;
for (uint8_t i{index}; i < length_; ++i) {
addrs_[i] = addrs_[i + 1];
timestamps_[i] = timestamps_[i + 1];
}
static constexpr _Bitfield all = -1;

_Bitfield tail_mask = all << index;
_Bitfield head_mask = ~tail_mask;

_Bitfield tail = (release_event_bits_ >> 1) & tail_mask;
_Bitfield head = release_event_bits_ & head_mask;

release_event_bits_ = tail | head;
}

// Empty the queue entirely.
void clear() {
length_ = 0;
release_event_bits_ = 0;
}
};

} // namespace kaleidoscope
Loading

0 comments on commit 928c6d9

Please sign in to comment.