Rewrite OneShot plugin

This is a complete rewrite of OneShot, based on the keymap cache
redesign. This allows OneShot to abort the release of a key, causing
its cache entry to stay valid if it's in an active state after the key
is released, allowing us to fix #896 (double-tapping a layer shift key
doesn't make it sticky).

Instead of tracking `Key` values, OneShot now uses two bitfields of
the keyboard in order to track the OneShot state of every valid
`KeyAddr` independently. This could enable the creation of a OneShot
"meta" key, which could be used as a way to make any key on the
keyboard exhibit OneShot behaviour.

The new OneShot plugin immediately replaces the OneShot `Key` value
with its corresponding "normal" key, and activates its OneShot status
by setting one bit in one of the bitfields.

Also included:

* A rewrite of LED-ActiveModColor that makes it compatible
  with the new OneShot, and add support for Qukeys

* Updates to Escape-OneShot for compatibility and efficiency

* Minor updates to Qukeys

* The new KeyAddrBitfield class

KeyAddrBitfield:

This class can be used to represent a binary state of the physical key
addresses on the keyboard. For example, ActiveModColor can use to to
mark all the keys which should be highlighted at any given time. It
includes a very efficient iterator, which returns only `KeyAddr`
values corresponding to bits that are set in the bitfield. It checks a
whole byte at a time before examining individual bits, so if most bits
are unset most of the time, it's very fast, and suitable for use in
hooks that get called every cycle.

ActiveModColor:

This makes LED-ActiveModColor compatible with Qukeys, and removes its
16-modifier limit, while simultaneously reducing it's footprint in RAM
and eliminating a potential buffer overrun bug where it could have
written past the end of its state array.

Fixes #882
Fixes #894
Fixes #896

Signed-off-by: Michael Richters <gedankenexperimenter@gmail.com>
pull/1024/head
Michael Richters 4 years ago
parent f1e727dba7
commit dc21cc2895
No known key found for this signature in database
GPG Key ID: 1288FD13E4EEF0C0

@ -1,6 +1,6 @@
/* -*- mode: c++ -*-
* Kaleidoscope-Escape-OneShot -- Turn ESC into a key that cancels OneShots, if active.
* Copyright (C) 2016, 2017, 2018 Keyboard.io, Inc
* Copyright (C) 2016-2020 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
@ -19,26 +19,33 @@
#include <Kaleidoscope-OneShot.h>
#include <Kaleidoscope-Escape-OneShot.h>
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/layers.h"
namespace kaleidoscope {
namespace plugin {
EventHandlerResult EscapeOneShot::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState) {
if (mapped_key != Key_Escape || (keyState & INJECTED))
return EventHandlerResult::OK;
if (!keyToggledOn(keyState))
return EventHandlerResult::OK;
if ((!::OneShot.isActive() || ::OneShot.isPressed()) && !::OneShot.isSticky()) {
return EventHandlerResult::OK;
}
EventHandlerResult EscapeOneShot::onKeyswitchEvent(
Key &key, KeyAddr key_addr, uint8_t key_state) {
// We only act on an escape key that has just been pressed, and not
// generated by some other plugin. Also, only if at least one
// OneShot key is active and/or sticky. Last, only if there are no
// OneShot keys currently being held.
if (key == Key_Escape &&
keyToggledOn(key_state) &&
!(key_state & INJECTED) &&
::OneShot.isActive()) {
// Cancel all OneShot keys
::OneShot.cancel(true);
mapped_key = Key_NoKey;
// Change the escape key to a blank key, and signal that event processing is
// complete.
key = Key_NoKey;
return EventHandlerResult::EVENT_CONSUMED;
}
// Otherwise, do nothing
return EventHandlerResult::OK;
}
}
}

@ -1,6 +1,6 @@
/* -*- mode: c++ -*-
* Kaleidoscope-Escape-OneShot -- Turn ESC into a key that cancels OneShots, if active.
* Copyright (C) 2016, 2017, 2018 Keyboard.io, Inc
* Copyright (C) 2016-2020 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
@ -25,8 +25,7 @@ class EscapeOneShot : public kaleidoscope::Plugin {
public:
EscapeOneShot(void) {}
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState);
EventHandlerResult onKeyswitchEvent(Key &key, KeyAddr key_addr, uint8_t key_state);
};
}
}

@ -1,6 +1,6 @@
/* -*- mode: c++ -*-
* Kaleidoscope-LED-ActiveModColor -- Light up the LEDs under the active modifiers
* Copyright (C) 2016, 2017, 2018 Keyboard.io, Inc
* Copyright (C) 2016-2020 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
@ -18,34 +18,44 @@
#include <Kaleidoscope-LED-ActiveModColor.h>
#include <Kaleidoscope-OneShot.h>
#include "kaleidoscope/layers.h"
#include "kaleidoscope/keyswitch_state.h"
namespace kaleidoscope {
namespace plugin {
KeyAddr ActiveModColorEffect::mod_keys_[MAX_MODS_PER_LAYER];
uint8_t ActiveModColorEffect::mod_key_count_;
KeyAddrBitfield ActiveModColorEffect::mod_key_bits_;
bool ActiveModColorEffect::highlight_normal_modifiers_ = true;
cRGB ActiveModColorEffect::highlight_color = (cRGB) {
160, 160, 160
};
cRGB ActiveModColorEffect::highlight_color = CRGB(160, 160, 160);
cRGB ActiveModColorEffect::oneshot_color = CRGB(160, 160, 0);
cRGB ActiveModColorEffect::sticky_color = CRGB(160, 0, 0);
EventHandlerResult ActiveModColorEffect::onLayerChange() {
if (!Runtime.has_leds)
return EventHandlerResult::OK;
mod_key_count_ = 0;
EventHandlerResult ActiveModColorEffect::onKeyswitchEvent(
Key &key,
KeyAddr key_addr,
uint8_t key_state) {
for (auto key_addr : KeyAddr::all()) {
Key k = Layer.lookupOnActiveLayer(key_addr);
// If `key_addr` is not a physical key address, ignore it:
if (! key_addr.isValid()) {
return EventHandlerResult::OK;
}
if (::OneShot.isOneShotKey(k) ||
(highlight_normal_modifiers_ && (
(k >= Key_LeftControl && k <= Key_RightGui) ||
(k.getFlags() == (SYNTHETIC | SWITCH_TO_KEYMAP))))) {
mod_keys_[mod_key_count_++] = key_addr;
if (keyToggledOn(key_state)) {
// If a key toggles on, we check its value. If it's a OneShot key,
// it will get highlighted. Conditionally (if
// `highlight_normal_modifiers_` is set), we also highlight
// modifier and layer-shift keys.
if ((key >= Key_LeftControl && key <= Key_RightGui) ||
(key.getFlags() == (SYNTHETIC | SWITCH_TO_KEYMAP))) {
mod_key_bits_.set(key_addr);
}
} else if (keyToggledOff(key_state)) {
// Things get a bit ugly here because this plugin might come
// before OneShot in the order, so we can't just count on OneShot
// stopping the suppressed release event before we see it here.
if (mod_key_bits_.read(key_addr) && !::OneShot.isActive(key_addr)) {
mod_key_bits_.clear(key_addr);
::LEDControl.refreshAt(key_addr);
}
}
@ -53,36 +63,20 @@ EventHandlerResult ActiveModColorEffect::onLayerChange() {
}
EventHandlerResult ActiveModColorEffect::beforeReportingState() {
if (mod_key_count_ == 0) {
onLayerChange();
}
for (uint8_t i = 0; i < mod_key_count_; i++) {
const KeyAddr &key_addr = mod_keys_[i];
// This loop iterates through only the `key_addr`s that have their
// bits in the `mod_key_bits_` bitfield set.
for (KeyAddr key_addr : mod_key_bits_) {
Key k = Layer.lookupOnActiveLayer(key_addr);
if (::OneShot.isOneShotKey(k)) {
if (::OneShot.isSticky(k))
if (::OneShot.isTemporary(key_addr)) {
// Temporary OneShot keys get one color:
::LEDControl.setCrgbAt(key_addr, oneshot_color);
} else if (::OneShot.isSticky(key_addr)) {
// Sticky OneShot keys get another color:
::LEDControl.setCrgbAt(key_addr, sticky_color);
else if (::OneShot.isActive(k))
::LEDControl.setCrgbAt(key_addr, highlight_color);
else
::LEDControl.refreshAt(key_addr);
} else if (k >= Key_LeftControl && k <= Key_RightGui) {
if (kaleidoscope::Runtime.hid().keyboard().isModifierKeyActive(k))
} else if (highlight_normal_modifiers_) {
// Normal modifiers get a third color:
::LEDControl.setCrgbAt(key_addr, highlight_color);
else
::LEDControl.refreshAt(key_addr);
} else if (k.getFlags() == (SYNTHETIC | SWITCH_TO_KEYMAP)) {
uint8_t layer = k.getKeyCode();
if (layer >= LAYER_SHIFT_OFFSET)
layer -= LAYER_SHIFT_OFFSET;
if (Layer.isActive(layer))
::LEDControl.setCrgbAt(key_addr, highlight_color);
else
::LEDControl.refreshAt(key_addr);
}
}

@ -1,6 +1,6 @@
/* -*- mode: c++ -*-
* Kaleidoscope-LED-ActiveModColor -- Light up the LEDs under the active modifiers
* Copyright (C) 2016, 2017, 2018 Keyboard.io, Inc
* Copyright (C) 2016-2020 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
@ -18,6 +18,7 @@
#pragma once
#include "kaleidoscope/Runtime.h"
#include "kaleidoscope/KeyAddrBitfield.h"
#include <Kaleidoscope-LEDControl.h>
#define MAX_MODS_PER_LAYER 16
@ -29,22 +30,21 @@ class ActiveModColorEffect : public kaleidoscope::Plugin {
ActiveModColorEffect(void) {}
static cRGB highlight_color;
static cRGB oneshot_color;
static cRGB sticky_color;
static void highlightNormalModifiers(bool value) {
highlight_normal_modifiers_ = value;
}
EventHandlerResult onKeyswitchEvent(Key &key,
KeyAddr key_addr,
uint8_t key_state);
EventHandlerResult beforeReportingState();
EventHandlerResult onLayerChange();
EventHandlerResult onSetup() {
return onLayerChange();
}
private:
static bool highlight_normal_modifiers_;
static KeyAddr mod_keys_[MAX_MODS_PER_LAYER];
static uint8_t mod_key_count_;
static KeyAddrBitfield mod_key_bits_;
};
}
}

@ -19,274 +19,375 @@
#include <Kaleidoscope-FocusSerial.h>
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/key_events.h"
#include "kaleidoscope/layers.h"
namespace kaleidoscope {
namespace plugin {
// ---- state ---------
// ----------------------------------------------------------------------------
// Configuration variables
uint16_t OneShot::start_time_ = 0;
uint16_t OneShot::time_out = 2500;
uint16_t OneShot::hold_time_out = 250;
int16_t OneShot::double_tap_time_out = -1;
OneShot::key_state_t OneShot::state_[OneShot::ONESHOT_KEY_COUNT];
Key OneShot::prev_key_;
bool OneShot::should_cancel_ = false;
bool OneShot::should_cancel_stickies_ = false;
bool OneShot::isPressed() {
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT; i++) {
if (state_[i].pressed)
return true;
}
return false;
}
bool OneShot::isSticky() {
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT; i++) {
if (state_[i].sticky)
return true;
}
return false;
}
// ----------------------------------------------------------------------------
// State variables
bool OneShot::isStickable(Key key) {
return state_[key.getRaw() - ranges::OS_FIRST].stickable;
}
uint16_t OneShot::stickable_keys_ = -1;
// ---- OneShot stuff ----
void OneShot::injectNormalKey(uint8_t idx, uint8_t key_state) {
Key key;
KeyAddrBitfield OneShot::temp_addrs_;
KeyAddrBitfield OneShot::glue_addrs_;
if (idx < 8) {
key = Key(Key_LeftControl.getKeyCode() + idx,
Key_LeftControl.getFlags());
} else {
key = Key(LAYER_SHIFT_OFFSET + idx - 8,
KEY_FLAGS | SYNTHETIC | SWITCH_TO_KEYMAP);
}
uint16_t OneShot::start_time_ = 0;
KeyAddr OneShot::prev_key_addr_ = OneShot::invalid_key_addr;
uint8_t OneShot::release_countdown_ = 0;
handleKeyswitchEvent(key, UnknownKeyswitchLocation, key_state | INJECTED);
}
void OneShot::activateOneShot(uint8_t idx) {
injectNormalKey(idx, IS_PRESSED);
}
// ============================================================================
// Public interface
void OneShot::cancelOneShot(uint8_t idx) {
state_[idx].active = false;
injectNormalKey(idx, WAS_PRESSED);
}
// ----------------------------------------------------------------------------
// Configuration functions
EventHandlerResult OneShot::onNameQuery() {
return ::Focus.sendName(F("OneShot"));
void OneShot::enableStickability(Key key) {
uint8_t n = getKeyIndex(key);
stickable_keys_ |= (1 << n);
}
EventHandlerResult OneShot::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState) {
uint8_t idx = mapped_key.getRaw() - ranges::OS_FIRST;
if (keyState & INJECTED)
return EventHandlerResult::OK;
if (!isActive()) {
if (!isOneShotKey_(mapped_key)) {
return EventHandlerResult::OK;
void OneShot::disableStickability(Key key) {
uint8_t n = getKeyIndex(key);
stickable_keys_ &= ~(1 << n);
}
if (keyToggledOff(keyState)) {
state_[idx].pressed = false;
} else if (keyToggledOn(keyState)) {
start_time_ = Runtime.millisAtCycleStart();
state_[idx].position = key_addr.toInt();
state_[idx].pressed = true;
state_[idx].active = true;
prev_key_ = mapped_key;
activateOneShot(idx);
void OneShot::enableStickabilityForModifiers() {
stickable_keys_ |= stickable_modifiers_mask;
}
return EventHandlerResult::EVENT_CONSUMED;
void OneShot::disableStickabilityForModifiers() {
stickable_keys_ &= ~stickable_modifiers_mask;
}
if (isOneShotKey_(mapped_key)) {
if (state_[idx].sticky) {
if (keyToggledOn(keyState)) { // maybe on _off instead?
prev_key_ = mapped_key;
state_[idx].sticky = false;
cancelOneShot(idx);
should_cancel_ = false;
}
} else {
if (keyToggledOff(keyState)) {
state_[idx].pressed = false;
if (Runtime.hasTimeExpired(start_time_, hold_time_out)) {
cancelOneShot(idx);
should_cancel_ = false;
}
void OneShot::enableStickabilityForLayers() {
stickable_keys_ |= stickable_layers_mask;
}
if (keyToggledOn(keyState)) {
state_[idx].pressed = true;
if (prev_key_ == mapped_key && isStickable(mapped_key)) {
uint16_t dtto = (double_tap_time_out == -1) ? time_out : double_tap_time_out;
if (!Runtime.hasTimeExpired(start_time_, dtto)) {
state_[idx].sticky = true;
prev_key_ = mapped_key;
void OneShot::disableStickabilityForLayers() {
stickable_keys_ &= ~stickable_layers_mask;
}
} else {
start_time_ = Runtime.millisAtCycleStart();
state_[idx].position = key_addr.toInt();
state_[idx].active = true;
prev_key_ = mapped_key;
// ----------------------------------------------------------------------------
// Global tests for any OneShot key
activateOneShot(idx);
bool OneShot::isActive() {
for (KeyAddr key_addr __attribute__((unused)) : temp_addrs_) {
return true;
}
for (KeyAddr key_addr __attribute__((unused)) : glue_addrs_) {
return true;
}
return false;
}
return EventHandlerResult::EVENT_CONSUMED;
bool OneShot::isSticky() {
for (KeyAddr key_addr : glue_addrs_) {
if (! temp_addrs_.read(key_addr)) {
return true;
}
}
return false;
}
// ordinary key here, with some event
// ----------------------------------------------------------------------------
// Key-specific OneShot key tests
// These functions are particularly useful for ActiveModColor, which
// could potentially use three different color values for the three
// states (sticky | active && !sticky | pressed && !active).
if (keyIsPressed(keyState)) {
prev_key_ = mapped_key;
if (!(mapped_key >= Key_LeftControl && mapped_key <= Key_RightGui) &&
!(mapped_key.getFlags() == (KEY_FLAGS | SYNTHETIC | SWITCH_TO_KEYMAP))) {
should_cancel_ = true;
bool OneShot::isModifier(Key key) {
// Returns `true` if `key` is a modifier key, including modifiers
// with extra mod flags applied (e.g. `Key_Meh`).
if ((key.getFlags() & (SYNTHETIC | RESERVED)) != 0) {
return false;
}
return (key.getKeyCode() >= Key_LeftControl.getKeyCode() &&
key.getKeyCode() <= Key_RightGui.getKeyCode());
}
return EventHandlerResult::OK;
bool OneShot::isLayerShift(Key key) {
// Returns `true` if `key` is a layer-shift key.
return (key.getFlags() == (SYNTHETIC | SWITCH_TO_KEYMAP) &&
key.getKeyCode() >= LAYER_SHIFT_OFFSET &&
key.getKeyCode() < LAYER_MOVE_OFFSET);
}
EventHandlerResult OneShot::beforeReportingState() {
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT / 2; i++) {
if (state_[i].active) {
activateOneShot(i);
bool OneShot::isStickable(Key key) {
int8_t n;
if (isModifier(key)) {
n = key.getKeyCode() - Key_LeftControl.getKeyCode();
return bitRead(stickable_keys_, n);
} else if (isLayerShift(key)) {
n = oneshot_mod_count + key.getKeyCode() - LAYER_SHIFT_OFFSET;
if (n < oneshot_key_count) {
return bitRead(stickable_keys_, n);
}
}
return false;
}
return EventHandlerResult::OK;
bool OneShot::isTemporary(KeyAddr key_addr) {
return temp_addrs_.read(key_addr);
}
EventHandlerResult OneShot::afterEachCycle() {
bool oneshot_active = false;
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT; i++) {
if (state_[i].active) {
oneshot_active = true;
break;
bool OneShot::isSticky(KeyAddr key_addr) {
return (glue_addrs_.read(key_addr) && !temp_addrs_.read(key_addr));
}
bool OneShot::isActive(KeyAddr key_addr) {
return (isTemporary(key_addr) || glue_addrs_.read(key_addr));
}
if (oneshot_active && hasTimedOut())
cancel();
bool is_cancelled = false;
// ----------------------------------------------------------------------------
// Other functions
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT; i++) {
if (should_cancel_) {
if (state_[i].sticky) {
if (should_cancel_stickies_) {
is_cancelled = true;
state_[i].sticky = false;
cancelOneShot(i);
state_[i].pressed = false;
// Cancel all active OneShot keys (if `cancel_stickies` is true) or
// just non-sticky active OneShot keys. This function is called by
// Escape-OneShot to release active OneShot keys.
void OneShot::cancel(bool cancel_stickies) {
if (cancel_stickies) {
for (KeyAddr key_addr : glue_addrs_) {
releaseKey(key_addr);
}
}
} else if (state_[i].active && !state_[i].pressed) {
is_cancelled = true;
cancelOneShot(i);
for (KeyAddr key_addr : temp_addrs_) {
if (glue_addrs_.read(key_addr)) {
releaseKey(key_addr);
} else {
temp_addrs_.clear(key_addr);
}
}
}
if (is_cancelled) {
should_cancel_ = false;
should_cancel_stickies_ = false;
// ----------------------------------------------------------------------------
// Plugin hook functions
EventHandlerResult OneShot::onNameQuery() {
return ::Focus.sendName(F("OneShot"));
}
EventHandlerResult OneShot::onKeyswitchEvent(
Key &key, KeyAddr key_addr, uint8_t key_state) {
// Ignore injected key events. This prevents re-processing events
// that the hook functions generate (by calling `injectNormalKey()`
// via one of the `*OneShot()` functions). There are more robust
// ways to do this, but since OneShot is intended to react to only
// physical keypresses, this is adequate.
if (key_state & INJECTED)
return EventHandlerResult::OK;
}
void OneShot::inject(Key mapped_key, uint8_t key_state) {
onKeyswitchEvent(mapped_key, UnknownKeyswitchLocation, key_state);
bool temp = temp_addrs_.read(key_addr);
bool glue = glue_addrs_.read(key_addr);
if (keyToggledOn(key_state)) {
if (!temp && !glue) {
// This key_addr is not in a OneShot state.
if (isOneShotKey(key)) {
// Replace the OneShot key with its corresponding normal key.
pressKey(key_addr, key);
return EventHandlerResult::ABORT;
} else if (!isModifier(key) && !isLayerShift(key)) {
// Only trigger release of temporary OneShot keys if the
// pressed key is neither a modifier nor a layer shift.
release_countdown_ = (1 << 1);
}
// return EventHandlerResult::OK;
} else if (temp && glue) {
// This key_addr is in the temporary OneShot state.
if (key_addr == prev_key_addr_) {
// The same OneShot key has been pressed twice in a row. It
// will either become sticky (if it has been double-tapped),
// or it will be become a normal key. Either way, its `temp`
// state will be cleared.
temp_addrs_.clear(key_addr);
// Derive the true double-tap timeout value if we're using the default.
uint16_t dtto = (double_tap_time_out < 0) ? time_out : double_tap_time_out;
// If the key is not stickable, or the double-tap timeout has
// expired, clear the `glue` state, as well; this OneShot key
// has been cancelled, and will become a normal key.
if (!isStickable(key) || hasTimedOut(dtto)) {
glue_addrs_.clear(key_addr);
} else {
return EventHandlerResult::ABORT;
}
} else {
// This is a temporary OneShot key, but has not been pressed
// twice in a row, so we need to clear its state.
temp_addrs_.clear(key_addr);
glue_addrs_.clear(key_addr);
}
} else if (!temp && glue) {
// This is a sticky OneShot key that has been pressed. Clear
// state now, so it will become a normal key.
glue_addrs_.clear(key_addr);
} else { // (temp && !glue)
// A key has been pressed that is in the "pending" OneShot
// state. Since this key should have entered the "temporary"
// OneShot state as soon as it was released (from its first
// press), it should only be possible to release a key that's in
// this state.
}
// Always record the address of a keypress. It might be useful for
// other plugins, so this could perhaps be tracked in the
// Kaleidoscope core.
prev_key_addr_ = key_addr;
} else if (keyToggledOff(key_state)) {
if (temp || glue) {
// Any key in the "pending" OneShot state needs its `glue` state
// bit set to make it "temporary". If it's in the "sticky"
// OneShot state, this is redundant, but we're trading time
// efficiency to get smaller binary size.
glue_addrs_.set(key_addr);
// This is an active OneShot key that has just been released. We
// need to stop that event from sending a report, and instead
// send a "hold" event. This is handled in the
// `beforeReportingState()` hook below.
//Layer.updateLiveCompositeKeymap(key_addr, key);
return EventHandlerResult::ABORT;
}
// --- glue code ---
bool OneShot::isActive(void) {
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT; i++) {
if ((state_[i].active && !hasTimedOut()) ||
state_[i].pressed ||
state_[i].sticky)
return true;
} else {
// This key is being held.
if (temp && !glue) {
// This key is in the "pending" OneShot state. We need to check
// its hold timeout, and turn it back into a normal key if it
// has timed out.
if (hasTimedOut(hold_time_out)) {
temp_addrs_.clear(key_addr);
}
return false;
}
bool OneShot::isActive(Key key) {
uint8_t idx = key.getRaw() - ranges::OS_FIRST;
if (isOneShotKey(key)) {
// whoops! someone cancelled a oneshot key while it was being
// held; reactivate it, but set it as a normal modifier
// instead. Or better yet, mask it, in case of a layer change.
}
}
return (state_[idx].active && !hasTimedOut()) ||
state_[idx].pressed ||
state_[idx].sticky;
return EventHandlerResult::OK;
}
bool OneShot::isSticky(Key key) {
uint8_t idx = key.getRaw() - ranges::OS_FIRST;
return state_[idx].sticky;
// For any active OneShot modifier keys, keep those modifiers active
// in the keyboard HID report.
EventHandlerResult OneShot::beforeReportingState() {
for (KeyAddr key_addr : glue_addrs_) {
holdKey(key_addr);
}
return EventHandlerResult::OK;
}
bool OneShot::isModifierActive(Key key) {
if (key < Key_LeftControl || key > Key_RightGui)
return false;
uint8_t idx = key.getKeyCode() - Key_LeftControl.getKeyCode();
return state_[idx].active;
}
EventHandlerResult OneShot::afterEachCycle() {
// If a normal, non-modifier key has been pressed, or if active,
// non-sticky OneShot keys have timed out, this is where they get
// released. Release is triggered when `release_countdown_` gets to
// 1, not 0, because most of the time it will be 0 (see below). It
// gets set to 2 on the press of a normal key when there are any
// active OneShot keys; that way, the OneShot keys will stay active
// long enough to apply to the newly-pressed key.
if ((release_countdown_ == 1) || hasTimedOut(time_out)) {
for (KeyAddr key_addr : temp_addrs_) {
if (glue_addrs_.read(key_addr)) {
releaseKey(key_addr);
}
temp_addrs_.clear(key_addr);
}
}
// Also, advance the counter for OneShot keys that have been
// cancelled by the press of a non-OneShot, non-modifier key. An
// unconditional bit shift should be more efficient than checking
// for zero to avoid underflow.
release_countdown_ >>= 1;
void OneShot::cancel(bool with_stickies) {
should_cancel_ = true;
should_cancel_stickies_ = with_stickies;
return EventHandlerResult::OK;
}
void OneShot::enableStickability(Key key) {
if (key >= ranges::OS_FIRST && key <= ranges::OS_LAST)
state_[key.getRaw() - ranges::OS_FIRST].stickable = true;
}
// ============================================================================
// Private functions, not exposed to other plugins
void OneShot::disableStickability(Key key) {
if (key >= ranges::OS_FIRST && key <= ranges::OS_LAST)
state_[key.getRaw() - ranges::OS_FIRST].stickable = false;
}
// ----------------------------------------------------------------------------
// Helper functions for acting on OneShot key events
void OneShot::enableStickabilityForModifiers() {
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT / 2; i++) {
state_[i].stickable = true;
}
uint8_t OneShot::getOneShotKeyIndex(Key oneshot_key) {
// The calling function is responsible for verifying that
// `oneshot_key` is an actual OneShot key (i.e. call
// `isOneShotKey(oneshot_key)` first).
uint8_t index = oneshot_key.getRaw() - ranges::OS_FIRST;
return index;
}
void OneShot::enableStickabilityForLayers() {
for (uint8_t i = ONESHOT_KEY_COUNT / 2; i < ONESHOT_KEY_COUNT; i++) {
state_[i].stickable = true;
uint8_t OneShot::getKeyIndex(Key key) {
// Default to returning a value that's out of range. This should be
// harmless because we only use the returned index to reference a
// bit in a bitfield, not as a memory address.
uint8_t n{oneshot_key_count};
if (isOneShotKey(key)) {
n = getOneShotKeyIndex(key);
} else if (isModifier(key)) {
n = key.getKeyCode() - Key_LeftControl.getKeyCode();
} else if (isLayerShift(key)) {
n = oneshot_mod_count + key.getKeyCode() - LAYER_SHIFT_OFFSET;
}
return n;
}
void OneShot::disableStickabilityForModifiers() {
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT / 2; i++) {
state_[i].stickable = false;
Key OneShot::decodeOneShotKey(Key oneshot_key) {
// The calling function is responsible for verifying that
// `oneshot_key` is an actual OneShot key (i.e. call
// `isOneShotKey(oneshot_key)` first).
uint8_t n = getOneShotKeyIndex(oneshot_key);
if (n < oneshot_mod_count) {
return Key(Key_LeftControl.getKeyCode() + n,
Key_LeftControl.getFlags());
} else {
return Key(LAYER_SHIFT_OFFSET + n - oneshot_mod_count,
KEY_FLAGS | SYNTHETIC | SWITCH_TO_KEYMAP);
}
}
void OneShot::disableStickabilityForLayers() {
for (uint8_t i = ONESHOT_KEY_COUNT / 2; i < ONESHOT_KEY_COUNT; i++) {
state_[i].stickable = false;
}
// ------------------------------------------------------------------------------
// Helper functions for sending key events for keys in OneShot states
void OneShot::pressKey(KeyAddr key_addr, Key oneshot_key) {
Key key = decodeOneShotKey(oneshot_key);
prev_key_addr_ = key_addr;
start_time_ = Runtime.millisAtCycleStart();
temp_addrs_.set(key_addr);
handleKeyswitchEvent(key, key_addr, IS_PRESSED | INJECTED);
}
void OneShot::holdKey(KeyAddr key_addr) {
handleKeyswitchEvent(Key_NoKey, key_addr, WAS_PRESSED | IS_PRESSED | INJECTED);
}
void OneShot::releaseKey(KeyAddr key_addr) {
glue_addrs_.clear(key_addr);
temp_addrs_.clear(key_addr);
handleKeyswitchEvent(Key_NoKey, key_addr, WAS_PRESSED | INJECTED);
}
} // namespace plugin
} // namespace kaleidoscope
kaleidoscope::plugin::OneShot OneShot;

@ -19,6 +19,11 @@
#include "kaleidoscope/Runtime.h"
#include <Kaleidoscope-Ranges.h>
#include "kaleidoscope/key_events.h"
#include "kaleidoscope/KeyAddrBitfield.h"
// ----------------------------------------------------------------------------
// Keymap macros
#define OSM(kc) Key(kaleidoscope::ranges::OSM_FIRST + (Key_ ## kc).getKeyCode() - Key_LeftControl.getKeyCode())
#define OSL(n) Key(kaleidoscope::ranges::OSL_FIRST + n)
@ -28,25 +33,11 @@ namespace plugin {
class OneShot : public kaleidoscope::Plugin {
public:
OneShot(void) {
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT; i++) {
state_[i].stickable = true;
}
}
// Constructor
OneShot() {}
static bool isOneShotKey(Key key) {
return (key.getRaw() >= kaleidoscope::ranges::OS_FIRST && key.getRaw() <= kaleidoscope::ranges::OS_LAST);
}
static bool isActive(void);
static bool isActive(Key key);
static bool isPressed();
static bool isSticky();
static bool isSticky(Key key);
static void cancel(bool with_stickies = false);
static uint16_t time_out;
static int16_t double_tap_time_out;
static uint16_t hold_time_out;
// --------------------------------------------------------------------------
// Configuration functions
static inline void enableStickablity() {}
static void enableStickability(Key key);
@ -68,46 +59,86 @@ class OneShot : public kaleidoscope::Plugin {
static void disableStickabilityForModifiers();
static void disableStickabilityForLayers();
static bool isStickable(Key key);
// --------------------------------------------------------------------------
// Global test functions
static bool isModifierActive(Key key);
static bool isActive();
static bool isSticky();
// --------------------------------------------------------------------------
// Single-key test functions
static bool isOneShotKey(Key key) {
return (key.getRaw() >= kaleidoscope::ranges::OS_FIRST &&
key.getRaw() <= kaleidoscope::ranges::OS_LAST);
}
static bool isStickable(Key key); // inline?
static bool isTemporary(KeyAddr key_addr); // inline?
static bool isSticky(KeyAddr key_addr); // inline?
static bool isActive(KeyAddr key_addr); // inline?
// --------------------------------------------------------------------------
// Utility function for other plugins to cancel OneShot keys
static void cancel(bool with_stickies = false);
// --------------------------------------------------------------------------
// Vestigial functions?
void inject(Key key, uint8_t key_state) {}
static bool isModifierActive(Key key) {
return false;
}
// --------------------------------------------------------------------------
// Configuration variables (should probably be private)
static uint16_t time_out;
static uint16_t hold_time_out;
static int16_t double_tap_time_out;
// --------------------------------------------------------------------------
// Plugin hook functions
EventHandlerResult onNameQuery();
EventHandlerResult onKeyswitchEvent(Key &key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult beforeReportingState();
EventHandlerResult afterEachCycle();
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState);
void inject(Key mapped_key, uint8_t key_state);
private:
static constexpr uint8_t ONESHOT_KEY_COUNT = 16;
typedef struct {
bool active: 1;
bool pressed: 1;
bool stickable: 1;
bool sticky: 1;
uint8_t __reserved: 4;
uint8_t position;
} key_state_t;
static key_state_t state_[ONESHOT_KEY_COUNT];
static uint16_t start_time_;
static Key prev_key_;
static bool should_cancel_;
static bool should_cancel_stickies_;
// --------------------------------------------------------------------------
// Constants
static constexpr uint8_t oneshot_key_count = 16;
static constexpr uint8_t oneshot_mod_count = 8;
static constexpr uint8_t oneshot_layer_count = oneshot_key_count - oneshot_mod_count;
static constexpr uint16_t stickable_modifiers_mask = uint16_t(uint16_t(-1) >> oneshot_layer_count);
static constexpr uint16_t stickable_layers_mask = uint16_t(uint16_t(-1) << oneshot_mod_count);
static constexpr KeyAddr invalid_key_addr = KeyAddr(KeyAddr::invalid_state);
static void injectNormalKey(uint8_t idx, uint8_t key_state);
static void activateOneShot(uint8_t idx);
static void cancelOneShot(uint8_t idx);
// --------------------------------------------------------------------------
// State variables
static uint16_t stickable_keys_;
static bool isOneShotKey_(Key key) {
return key.getRaw() >= ranges::OS_FIRST && key.getRaw() <= ranges::OS_LAST;
}
static bool hasTimedOut() {
return Runtime.hasTimeExpired(start_time_, time_out);
static KeyAddrBitfield temp_addrs_;
static KeyAddrBitfield glue_addrs_;
static uint16_t start_time_;
static KeyAddr prev_key_addr_;
static uint8_t release_countdown_;
// --------------------------------------------------------------------------
// Internal utility functions
static bool hasTimedOut(uint16_t ttl) {
return Runtime.hasTimeExpired(start_time_, ttl);
}
static uint8_t getOneShotKeyIndex(Key oneshot_key);
static uint8_t getKeyIndex(Key key);
static Key decodeOneShotKey(Key oneshot_key);
static void pressKey(KeyAddr key_addr, Key oneshot_key);
static void holdKey(KeyAddr key_addr);
static void releaseKey(KeyAddr key_addr);
};
}
}
} // namespace plugin
} // namespace kaleidoscope
extern kaleidoscope::plugin::OneShot OneShot;

@ -327,11 +327,10 @@ void Qukeys::flushEvent(Key event_key) {
// 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);
// First, look up the value from the keymap. This value should be
// correct in the cache, even if there's been a layer change since
// the key was pressed.
Key key = Layer.lookup(k);
// Next, we check to see if this is a DualUse-type qukey (defined in the keymap)
if (isDualUseKey(key)) {

@ -0,0 +1,184 @@
/* Kaleidoscope - Firmware for computer input devices
* Copyright (C) 2020 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/KeyAddr.h"
namespace kaleidoscope {
// Return the number of `UnitType` units required to store `n` bits. Both `UnitType` &
// `WidthType` should be integer types. `WidthType` is whatever type the parameter `n` is
// stored as, and can be deduced by the compiler, so it's not necessary to declare it
// when calling this function (e.g. `bitfieldSize<uint16_t>(n)`). The default `UnitType`
// is `byte` (i.e. `uint8_t`, which is almost always what we want, so most of the time we
// can also drop that template parameter (e.g. `bitfieldSize(n)`).
template <typename _UnitType = byte, typename _WidthType>
constexpr _WidthType bitfieldSize(_WidthType n) {
return ((n - 1) / (8 * sizeof(_UnitType))) + 1;
}
// ================================================================================
// Generic Bitfield class, useful for defining KeyAddrBitfield, and others.
class KeyAddrBitfield {
public:
static constexpr uint8_t size = KeyAddr::upper_limit;
static constexpr uint8_t block_size = 8 * sizeof(uint8_t);
static constexpr uint8_t total_blocks = bitfieldSize<uint8_t>(size);
static constexpr uint8_t blockIndex(KeyAddr k) {
return k.toInt() / block_size;
}
static constexpr uint8_t bitIndex(KeyAddr k) {
return k.toInt() % block_size;
}
static constexpr KeyAddr index(uint8_t block_index, uint8_t bit_index) {
uint8_t offset = (block_index * block_size) + bit_index;
return KeyAddr(offset);
}
bool read(KeyAddr k) const {
// assert(k.toInt() < size);
return bitRead(data_[blockIndex(k)], bitIndex(k));
}
void set(KeyAddr k) {
// assert(k.toInt() < size);
bitSet(data_[blockIndex(k)], bitIndex(k));
}
void clear(KeyAddr k) {
// assert(k.toInt() < size);
bitClear(data_[blockIndex(k)], bitIndex(k));
}
void write(KeyAddr k, bool value) {
// assert(k.toInt() < size);
bitWrite(data_[blockIndex(k)], bitIndex(k), value);
}
// This function returns the number of set bits in the bitfield up to and
// including the bit at index `k`. Two important things to note: it doesn't
// verify that the bit for index `k` is set (the caller must do so first,
// using `read()`), and what is returned is 1-indexed, so the caller will need
// to subtract 1 before using it as an array index (e.g. when doing a `Key`
// lookup for a sparse keymap layer).
uint8_t ordinal(KeyAddr k) const {
// assert(k.toInt() < size);
uint8_t block_index = blockIndex(k);
uint8_t count{0};
for (uint8_t b{0}; b < block_index; ++b) {
count += __builtin_popcount(data_[b]);
}
uint8_t last_data_unit = data_[block_index];
last_data_unit &= ~(0xFF << bitIndex(k));
count += __builtin_popcount(last_data_unit);
return count;
}
uint8_t &block(uint8_t block_index) {
// assert(block_index < total_blocks);
return data_[block_index];
}
private:
uint8_t data_[total_blocks] = {};
// ----------------------------------------------------------------------------
// Iterator!
public:
class Iterator;
friend class KeyAddrBitfield::Iterator;
Iterator begin() {
return Iterator{*this, 0};
}
Iterator end() {
return Iterator{*this, total_blocks};
}
class Iterator {
public:
Iterator(KeyAddrBitfield &bitfield, uint8_t x)
: bitfield_(bitfield), block_index_(x) {}
bool operator!=(const Iterator &other) {
// First, the test for the end condition (return false when all the blocks have been
// tested):
while (block_index_ < other.block_index_) {
// Get the data for the block at `block_index_` from the bitfield, then shift it
// by the number of bits we've already checked (`bit_index_`):
block_ = bitfield_.data_[block_index_];
block_ >>= bit_index_;
// Now we iterate through that block until we either find a bit that is set, or we
// find that there are no more bits set. If (as expected most of the time) no bits
// are set, we do nothing:
while (block_ != 0) {
// If the low (remaining) bit is set, generate an `KeyAddr` object from the
// bitfield coordinates and store it for the dereference operator to return:
if (block_ & 1) {
index_ = KeyAddrBitfield::index(block_index_, bit_index_);
return true;
}
// The low bit wasn't set, so we shift the data block by one and track that
// shift with the bit coordinate (`bit_index_`):
block_ >>= 1;
bit_index_ += 1;
}
// When we're done checking a block, move on to the next one:
block_index_ += 1;
bit_index_ = 0;
}
return false;
}
KeyAddr operator*() {
// assert(index_ < size);
return index_;
}
void operator++() {
++bit_index_;
}
private:
KeyAddrBitfield &bitfield_;
uint8_t block_index_; // index of the block
uint8_t bit_index_{0}; // bit index in the block
uint8_t block_;
KeyAddr index_;
}; // class Iterator {
} __attribute__((packed)); // class KeyAddrBitfield {
} // namespace kaleidoscope {
// ================================================================================
// How to use the iterator above:
#if 0
// To use the KeyAddrBitfield::Iterator, write a loop like the following:
KeyAddrBitfield bitfield;
for (KeyAddr k : bitfield) {
// Here, you'll get a `KeyAddr` object for each bit that is set in `bitfield`.
}
#endif
Loading…
Cancel
Save