diff --git a/doc/plugin/TapDance.md b/doc/plugin/TapDance.md new file mode 100644 index 00000000..99ad7bed --- /dev/null +++ b/doc/plugin/TapDance.md @@ -0,0 +1,153 @@ +# Kaleidoscope-TapDance + +Tap-dance keys are general purpose, multi-use keys, which trigger a different +action based on the number of times they were tapped in sequence. As an example +to make this clearer, one can have a key that inputs `A` when tapped once, +inputs `B` when tapped twice, and lights up the keyboard in Christmas colors +when tapped a third time. + +This behaviour is most useful in cases where we have a number of things we +perform rarely, where tapping a single key repeatedly is not counter-productive. +Such cases include - for example - multimedia forward / backward keys: forward +on single tap, backward on double. Of course, one could use modifiers to achieve +a similar effect, but that's two keys to use, this is only one. We can also hide +some destructive functionality behind a number of taps: reset the keyboard after +4 taps, and light up LEDs in increasingly frightful colors until then. + +## How does it work? + +To not interfere with normal typing, tap-dance keys have two ways to decide when +to call an action: they either get interrupted, or they time out. Every time a +tap-dance key is pressed, the timer resets, so one does not have to finish the +whole tapping sequence within a short time limit. The tap-dance counter +continues incrementing until one of these cases happen. + +When a tap-dance key is pressed and released, and nothing is pressed on the +keyboard until the timeout is reached, then the key will time out, and trigger +an action. Which action, depends on the number of times it has been tapped up +until this point. + +When a tap-dance key is pressed and released, and another key is hit before the +timer expires, then the tap-dance key will trigger an action first, perform it, +and only then will the firmware continue handling the interrupting key press. +This is to preserve the order of keys pressed. + +In both of these cases, the [`tapDanceAction`][tdaction] will be called, with +`tapDanceIndex` set to the index of the tap-dance action (as set in the keymap), +the `tapCount`, and `tapDanceAction` set to either +`kaleidoscope::plugin::TapDance::Interrupt`, or +`kaleidoscope::plugin::TapDance::Timeout`. If we continue holding the key, then +as long as it is held, the same function will be called with `tapDanceAction` +set to `kaleidoscope::plugin::TapDance::Hold`. When the key is released, after +either an `Interrupt` or `Timeout` action was triggered, the function will be +called with `tapDanceAction` set to `kaleidoscope::plugin::TapDance::Release`. + +These actions allow us to create sophisticated tap-dance setups, where one can +tap a key twice and hold it, and have it repeat, for example. + +There is one additional value the `tapDanceAction` parameter can take: +`kaleidoscope::plugin::TapDance::Tap`. It is called with this argument for each +and every tap, even if no action is to be triggered yet. This is so that we can +have a way to do some side-effects, like light up LEDs to show progress, and so +on. + +## Using the plugin + +To use the plugin, we need to include the header, and declare the behaviour +used. Then, we need to place tap-dance keys on the keymap. And finally, we need +to implement the [`tapDanceAction`][tdaction] function that gets called each +time an action is to be performed. + +```c++ +#include +#include + +// Somewhere in the keymap: +TD(0) + +// later in the Sketch: +void tapDanceAction(uint8_t tap_dance_index, byte row, byte col, uint8_t tap_count, + kaleidoscope::plugin::TapDance::ActionType tap_dance_action) { + switch (tap_dance_index) { + case 0: + return tapDanceActionKeys(tap_count, tap_dance_action, + Consumer_ScanNextTrack, Consumer_ScanPreviousTrack); + } +} + +KALEIDOSCOPE_INIT_PLUGINS(TapDance); + +void setup() { + Kaleidoscope.setup (); +} +``` + +## Keymap markup + +### `TD(id)` + +> A key that acts as a tap-dance key. The actions performed depend on the +> implementation for the `id` index within the [`tapDanceActions`][tdactions] +> function. +> +> The `id` parameter here is what will be used as `tap_dance_index` in the +> handler function. + + [tdaction]: #tapdanceactiontapdanceindex-tapcount-tapdanceaction + +## Plugin methods + +The plugin provides a `TapDance` object, but to implement the actions, we need +to define a function ([`tapDanceAction`][tdaction]) outside of the object. A +handler, of sorts. Nevertheless, the plugin provides one macro that is +particularly useful: `tapDanceActionKeys`. Apart from that, it provides one +property only: + +### `.time_out` + +> The number of loop iterations to wait before a tap-dance sequence times out. +> Once the sequence timed out, the action for it will trigger, even without an +> interruptor. Defaults to 5, and the timer resets with every tap of the same + +### `tapDanceActionKeys(tap_count, tap_dance_action, keys...)` + +> Sets up an action where for each subsequent tap, a different key will be +> chosen from the list of keys supplied in the `keys...` argument. +> +> If we have `Key_A` and `Key_B` in the list, then, if tapped once, this +> function will input `A`, but when tapped twice, will input `B`. +> +> When all our actions are just different keys, this is a very handy macro to +> use. +> +> The `tap_count` and `tap_dance_actions` parameters should be the same as the +> similarly named parameters of the `tapDanceAction` function. + +### `tapDanceAction(tap_dance_index, row, col, tap_count, tap_dance_action)` + +> The heart of the tap-dance plugin is the handler method. This is called every +> time any kind of tap-dance action is to be performed. See the +> *[How does it work?](#how-does-it-work)* section for details about when and +> how this function is called. +> +> The `tap_dance_index` and `tap_count` parameters help us choose which action +> to perform. The `row` and `col` parameters tell us where the tap-dance key is +> on the keyboard. + +## Dependencies + +* [Kaleidoscope-Ranges](Ranges.md) + +## Further reading + +Starting from the [example][plugin:example] is the recommended way of getting +started with the plugin. + + [plugin:example]: ../../examples/TapDance/TapDance.ino + +## Upgrading + +Previous versions of `TapDance` used `kaleidoscope::TapDance::ActionType` as the +type of TapDance actions. In newer versions, this is +`kaleidoscope::plugin::TapDance::ActionType`. The old name still works, but will +be removed by 2019-01-14. diff --git a/examples/TapDance/TapDance.ino b/examples/TapDance/TapDance.ino new file mode 100644 index 00000000..4093b8cb --- /dev/null +++ b/examples/TapDance/TapDance.ino @@ -0,0 +1,64 @@ +/* -*- mode: c++ -*- + * Kaleidoscope-TapDance -- Tap-dance keys + * Copyright (C) 2016, 2017, 2018 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 . + */ + +#include +#include + +// *INDENT-OFF* +const Key keymaps[][ROWS][COLS] PROGMEM = { + [0] = KEYMAP_STACKED + ( + Key_NoKey, Key_1, Key_2, Key_3, Key_4, Key_5, Key_NoKey, + Key_Backtick, Key_Q, Key_W, Key_E, Key_R, Key_T, Key_Tab, + Key_PageUp, Key_A, Key_S, Key_D, Key_F, Key_G, + Key_PageDown, Key_Z, Key_X, Key_C, Key_V, Key_B, Key_Escape, + + Key_LeftControl, Key_Backspace, Key_LeftGui, Key_LeftShift, + TD(0), + + Key_skip, Key_6, Key_7, Key_8, Key_9, Key_0, Key_skip, + Key_Enter, Key_Y, Key_U, Key_I, Key_O, Key_P, Key_Equals, + Key_H, Key_J, Key_K, Key_L, Key_Semicolon, Key_Quote, + Key_skip, Key_N, Key_M, Key_Comma, Key_Period, Key_Slash, Key_Minus, + + Key_RightShift, Key_RightAlt, Key_Spacebar, Key_RightControl, + TD(1)), +}; +// *INDENT-ON* + +static void tapDanceEsc(uint8_t tap_dance_index, uint8_t tap_count, kaleidoscope::TapDance::ActionType tap_dance_action) { + tapDanceActionKeys(tap_count, tap_dance_action, Key_Escape, Key_Tab); +} + +void tapDanceAction(uint8_t tap_dance_index, byte row, byte col, uint8_t tap_count, kaleidoscope::TapDance::ActionType tap_dance_action) { + switch (tap_dance_index) { + case 0: + return tapDanceActionKeys(tap_count, tap_dance_action, Key_Tab, Key_Escape); + case 1: + return tapDanceEsc(tap_dance_index, tap_count, tap_dance_action); + } +} + +KALEIDOSCOPE_INIT_PLUGINS(TapDance); + +void setup() { + Kaleidoscope.setup(); +} + +void loop() { + Kaleidoscope.loop(); +} diff --git a/src/Kaleidoscope-TapDance.h b/src/Kaleidoscope-TapDance.h new file mode 100644 index 00000000..786acd9a --- /dev/null +++ b/src/Kaleidoscope-TapDance.h @@ -0,0 +1,20 @@ +/* -*- mode: c++ -*- + * Kaleidoscope-TapDance -- Tap-dance keys + * Copyright (C) 2016, 2017 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 . + */ + +#pragma once + +#include diff --git a/src/kaleidoscope/plugin/TapDance.cpp b/src/kaleidoscope/plugin/TapDance.cpp new file mode 100644 index 00000000..7dee8aa5 --- /dev/null +++ b/src/kaleidoscope/plugin/TapDance.cpp @@ -0,0 +1,232 @@ +/* -*- mode: c++ -*- + * Kaleidoscope-TapDance -- Tap-dance keys + * Copyright (C) 2016, 2017, 2018 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 . + */ + +#include +#include + +namespace kaleidoscope { +namespace plugin { + +// --- state --- +uint32_t TapDance::end_time_; +uint16_t TapDance::time_out = 200; +uint8_t TapDance::tap_count_[16]; +uint16_t TapDance::pressed_state_; +uint16_t TapDance::triggered_state_; +uint16_t TapDance::release_next_state_; +Key TapDance::last_tap_dance_key_; +byte TapDance::last_tap_dance_row_; +byte TapDance::last_tap_dance_col_; + +// --- helpers --- + +#define isTapDance(k) (k.raw >= ranges::TD_FIRST && k.raw <= ranges::TD_LAST) +#define isInSeq(k) (last_tap_dance_key_.raw == k.raw) +#define stillHeld(idx) (tap_count_[idx]) +#define isActive() (last_tap_dance_key_.raw != Key_NoKey.raw) + +// --- actions --- + +void TapDance::interrupt(byte row, byte col) { + uint8_t idx = last_tap_dance_key_.raw - ranges::TD_FIRST; + + tapDanceAction(idx, last_tap_dance_row_, last_tap_dance_col_, tap_count_[idx], Interrupt); + bitWrite(triggered_state_, idx, 1); + + end_time_ = 0; + + KeyboardHardware.maskKey(row, col); + kaleidoscope::hid::sendKeyboardReport(); + kaleidoscope::hid::releaseAllKeys(); + + if (bitRead(pressed_state_, idx)) + return; + + release(idx); +} + +void TapDance::timeout(void) { + uint8_t idx = last_tap_dance_key_.raw - ranges::TD_FIRST; + + tapDanceAction(idx, last_tap_dance_row_, last_tap_dance_col_, tap_count_[idx], Timeout); + bitWrite(triggered_state_, idx, 1); + + if (bitRead(pressed_state_, idx)) + return; + + last_tap_dance_key_.raw = Key_NoKey.raw; + + release(idx); +} + +void TapDance::release(uint8_t tap_dance_index) { + end_time_ = 0; + last_tap_dance_key_.raw = Key_NoKey.raw; + + bitClear(pressed_state_, tap_dance_index); + bitClear(triggered_state_, tap_dance_index); + bitWrite(release_next_state_, tap_dance_index, 1); +} + +void TapDance::tap(void) { + uint8_t idx = last_tap_dance_key_.raw - ranges::TD_FIRST; + + tap_count_[idx]++; + end_time_ = millis() + time_out; + + tapDanceAction(idx, last_tap_dance_row_, last_tap_dance_col_, tap_count_[idx], Tap); +} + +// --- api --- + +void TapDance::actionKeys(uint8_t tap_count, ActionType tap_dance_action, uint8_t max_keys, const Key tap_keys[]) { + if (tap_count > max_keys) + tap_count = max_keys; + + Key key; + key.raw = pgm_read_word(&(tap_keys[tap_count - 1].raw)); + + switch (tap_dance_action) { + case Tap: + break; + case Interrupt: + case Timeout: + handleKeyswitchEvent(key, last_tap_dance_row_, last_tap_dance_col_, IS_PRESSED | INJECTED); + break; + case Hold: + handleKeyswitchEvent(key, last_tap_dance_row_, last_tap_dance_col_, IS_PRESSED | WAS_PRESSED | INJECTED); + break; + case Release: + hid::sendKeyboardReport(); + handleKeyswitchEvent(key, last_tap_dance_row_, last_tap_dance_col_, WAS_PRESSED | INJECTED); + break; + } +} + +// --- hooks --- + +EventHandlerResult TapDance::onKeyswitchEvent(Key &mapped_key, byte row, byte col, uint8_t keyState) { + if (keyState & INJECTED) + return EventHandlerResult::OK; + + if (!keyIsPressed(keyState) && !keyWasPressed(keyState)) { + if (isTapDance(mapped_key)) { + return EventHandlerResult::EVENT_CONSUMED; + } + + return EventHandlerResult::OK; + } + + if (!isTapDance(mapped_key)) { + if (!isActive()) + return EventHandlerResult::OK; + + if (keyToggledOn(keyState)) + interrupt(row, col); + + if (KeyboardHardware.isKeyMasked(row, col)) { + KeyboardHardware.unMaskKey(row, col); + return EventHandlerResult::EVENT_CONSUMED; + } + return EventHandlerResult::OK; + } + + uint8_t tap_dance_index = mapped_key.raw - ranges::TD_FIRST; + + if (keyToggledOff(keyState)) + bitClear(pressed_state_, tap_dance_index); + + if (!isInSeq(mapped_key)) { + if (!isActive()) { + if (bitRead(triggered_state_, tap_dance_index)) { + if (keyToggledOff(keyState)) { + release(tap_dance_index); + } + + return EventHandlerResult::EVENT_CONSUMED; + } + + last_tap_dance_key_.raw = mapped_key.raw; + last_tap_dance_row_ = row; + last_tap_dance_col_ = col; + + tap(); + + return EventHandlerResult::EVENT_CONSUMED; + } else { + if (keyToggledOff(keyState) && stillHeld(tap_dance_index)) { + release(tap_dance_index); + return EventHandlerResult::EVENT_CONSUMED; + } + + if (!keyToggledOn(keyState)) { + return EventHandlerResult::EVENT_CONSUMED; + } + + interrupt(row, col); + } + } + + // in sequence + + if (keyToggledOff(keyState)) { + return EventHandlerResult::EVENT_CONSUMED; + } + + last_tap_dance_key_.raw = mapped_key.raw; + last_tap_dance_row_ = row; + last_tap_dance_col_ = col; + bitSet(pressed_state_, tap_dance_index); + + if (keyToggledOn(keyState)) { + tap(); + return EventHandlerResult::EVENT_CONSUMED; + } + + if (bitRead(triggered_state_, tap_dance_index)) + tapDanceAction(tap_dance_index, row, col, tap_count_[tap_dance_index], Hold); + + return EventHandlerResult::EVENT_CONSUMED; +} + +EventHandlerResult TapDance::afterEachCycle() { + for (uint8_t i = 0; i < 16; i++) { + if (!bitRead(release_next_state_, i)) + continue; + + tapDanceAction(i, last_tap_dance_row_, last_tap_dance_col_, tap_count_[i], Release); + tap_count_[i] = 0; + bitClear(release_next_state_, i); + } + + if (!isActive()) + return EventHandlerResult::OK; + + if (end_time_ && millis() > end_time_) + timeout(); + + return EventHandlerResult::OK; +} + +} +} + +__attribute__((weak)) void tapDanceAction(uint8_t tap_dance_index, byte row, byte col, uint8_t tap_count, + kaleidoscope::plugin::TapDance::ActionType tap_dance_action) { +} + +kaleidoscope::plugin::TapDance TapDance; diff --git a/src/kaleidoscope/plugin/TapDance.h b/src/kaleidoscope/plugin/TapDance.h new file mode 100644 index 00000000..2bd69e3c --- /dev/null +++ b/src/kaleidoscope/plugin/TapDance.h @@ -0,0 +1,77 @@ +/* -*- mode: c++ -*- + * Kaleidoscope-TapDance -- Tap-dance keys + * Copyright (C) 2016, 2017, 2018 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 . + */ + +#pragma once + +#include +#include + +#define TD(n) (Key) {.raw = kaleidoscope::ranges::TD_FIRST + n } + +#define tapDanceActionKeys(tap_count, tap_dance_action, ...) ({ \ + static const Key __k[] PROGMEM = { __VA_ARGS__ }; \ + TapDance.actionKeys(tap_count, tap_dance_action, \ + sizeof (__k) / sizeof (Key), &__k[0]); \ + }) + +namespace kaleidoscope { +namespace plugin { +class TapDance : public kaleidoscope::Plugin { + public: + typedef enum { + Tap, + Hold, + Interrupt, + Timeout, + Release, + } ActionType; + + TapDance(void) {} + + static uint16_t time_out; + + void actionKeys(uint8_t tap_count, ActionType tap_dance_action, uint8_t max_keys, const Key tap_keys[]); + + EventHandlerResult onKeyswitchEvent(Key &mapped_key, byte row, byte col, uint8_t keyState); + EventHandlerResult afterEachCycle(); + + private: + static uint32_t end_time_; + static uint8_t tap_count_[16]; + static uint16_t pressed_state_; + static uint16_t triggered_state_; + static uint16_t release_next_state_; + static Key last_tap_dance_key_; + static byte last_tap_dance_row_; + static byte last_tap_dance_col_; + + static void tap(void); + static void interrupt(byte row, byte col); + static void timeout(void); + static void release(uint8_t tap_dance_index); +}; +} + +// Backwards compatibility +typedef plugin::TapDance TapDance; + +} + +void tapDanceAction(uint8_t tap_dance_index, byte row, byte col, uint8_t tap_count, + kaleidoscope::plugin::TapDance::ActionType tap_dance_action); + +extern kaleidoscope::plugin::TapDance TapDance;