Merge pull request #1024 from gedankenexperimenter/key-event-handler

New `KeyEvent` handlers and main event loop rewrite
pull/1038/head
Gergely Nagy 4 years ago committed by GitHub
commit 3a8eb5d839
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -12,6 +12,78 @@ See [UPGRADING.md](UPGRADING.md) for more detailed instructions about upgrading
## New features
### New Qukeys features
#### Tap-repeat
It is now possible to get the "tap" value of a qukey to repeat (as if that key
for that character was simply being held down on a normal keyboard) by tapping
the qukey, then quickly pressing and holding it down. The result on the OS will
be as if the key was pressed and held just once, so that users of macOS apps
that use the Cocoa input system can get the menu for characters with diacritics
without an extra character in the output.
The maximum interval between the two keypresses that will trigger a tap repeat
can be configured via the `Qukeys.setMaxIntervalForTapRepeat(ms)` function,
where the argument specifies the number of milliseconds Qukeys will wait after a
qukey is tapped for it to be pressed a second time. If it is, but the qukey is
released within that same interval from the first tap's release, it will be
treated as a double-tap, and both taps will be sent to the OS.
### New OneShot features
#### Auto-OneShot modifiers & layers
OneShot can now treat modifiers and layer-shift keys as automatic OneShot
keys. This includes modifiers with other modifier flags applied, so it is now
very simple to turn `Key_Meh` or `Key_Hyper` into a OneShot key. The feature is
controlled by the following new functions:
- `OneShot.toggleAutoModifiers()`: Turn auto-OneShot modifiers on or off.
- `OneShot.toggleAutoLayers()`: Turn auto-OneShot layer shifts on or off.
- `OneShot.toggleAutoOneShot()`: Both of the above.
There are also `enable` and `disable` versions of these functions.
Note, it is still possible to define a modifier key in the keymap that will not
automatically become a OneShot key when pressed, by applying modifier flags to
`Key_NoKey` (e.g. `LSHIFT(Key_NoKey)`).
#### Two new special OneShot keys
OneShot can now also turn _any_ key into a sticky key, using either of two
special `Key` values that can be inserted in the keymap.
##### `OneShot_MetaStickyKey`
This is a special OneShot key (it behaves like other OneShot keys), but its
effect is to make any key pressed while it is active sticky. Press
`OneShot_MetaStickyKey`, then press `X`, and `X` will become sticky. Sticky
keys can be deactivated just like other OneShot keys, by pressing them
again. This works for any key value, so use it with caution.
##### `OneShot_ActiveStickyKey`
Like `OneShot_ActiveStickyKey`, this key makes other keys sticky, but rather than
affecting a subsequent key, it affects any keys already held when it is
pressed. Press `X`, press `OneShot_ActiveStickyKey`, and release `X`, and `X`
will be sticky until it is pressed again to deactivate it. Again, it works on
any key value, so use with caution.
#### LED-ActiveModColor highlighting
With the updates to OneShot, LED-ActiveModColor now recognizes and highlights
OneShot keys in three different states (along with normal modifiers):
- one-shot (a key that's active after release, but will time out)
- sticky (a key that will stay active indefinitely after release)
- normal (a key that will stay active only while physically held; also applies
to normal modifier keys)
The colors of theses three highlights are controlled by the properties
`ActiveModColorEffect.oneshot_color`, `ActiveModColorEffect.sticky_color`, and
`ActiveModColorEffect.highlight_color`, respectively.
### Better protection against unintended modifiers from Qukeys
Qukeys has two new configuration options for preventing unintended modifiers in

@ -7,6 +7,8 @@ If any of this does not make sense to you, or you have trouble updating your .in
* [Upgrade notes](#upgrade-notes)
+ [New features](#new-features)
- [Event-driven main loop](#event-driven-main-loop)
- [Keyboard state array](#keyboard-state-array)
- [New build system](#new-build-system)
- [New device API](#new-device-api)
- [New plugin API](#new-plugin-api)
@ -18,6 +20,7 @@ If any of this does not make sense to you, or you have trouble updating your .in
- [The `RxCy` macros and peeking into the keyswitch state](#the-rxcy-macros-and-peeking-into-the-keyswitch-state)
- [HostOS](#hostos)
- [MagicCombo](#magiccombo)
- [OneShot](#oneshot)
- [Qukeys](#qukeys)
- [TypingBreaks](#typingbreaks)
- [Redial](#redial)
@ -34,6 +37,103 @@ any API we've included in a release. Typically, this means that any code that us
## New features
### Event-driven main loop
Kaleidoscope's main loop has been rewritten. It now responds to key toggle-on and toggle-off events, dealing with one event at a time (and possibly more than one in a given cycle). Instead of sending a keyboard HID report at the end of every scan cycle (and letting the HID module suppress duplicates), it now only sends HID reports in response to input events.
Furthermore, there are now two functions for initiating the processing of key events:
- `Runtime.handleKeyswitchEvent()` is the starting point for events that represent physical keyswitches toggling on or off.
- `Runtime.handleKeyEvent()` is the starting point for "artificial" key events. It is also called at the end of `handleKeyswitchEvent()`.
In general, if a plugin needs to generate a key event, it should call `handleKeyEvent()`, not `handleKeyswitchEvent()`.
Each of the above functions calls its own set of plugin event handlers. When those event handlers are all done, event processing continues as `handleKeyEvent()` prepares a new keyboard HID report, then sends it:
- `Runtime.prepareKeyboardReport()` first clears the HID report, then populates it based on the contents of the `live_keys[]` array. Note that the HID report is not cleared until _after_ the new plugin event handlers have been called.
- `Runtime.sendKeyboardReport()` handles generating extra HID reports required for keys with keyboard modifier flags to avoid certain bugs, then calls a new plugin event handler before finally sending the new HID report.
These functions should rarely, if ever, need to be called by plugins.
#### The `KeyEvent` data type
There is a new `KeyEvent` type that encapsulates all the data relevant to a new key event, and it is used as the parameter for the new event-handling functions.
- `event.addr` contains the `KeyAddr` associated with the event.
- `event.state` contains the state bitfield (`uint8_t`), which can be tested with `keyToggledOn()`/`keyToggledOff()`.
- `event.key` contains a `Key` value, usually looked up from the keymap.
- `event.id` contains a pseudo-unique ID number of type `KeyEventId` (an 8-bit integer), used by certain plugins (see `onKeyswitchEvent()` below).
#### New plugin event handlers
##### `onKeyswitchEvent(KeyEvent &event)`
##### `onKeyEvent(KeyEvent &event)`
##### `onAddToReport(Key key)`
##### `beforeReportingState(const KeyEvent &event)`
#### For end-users
Existing sketches should be mostly backwards-compatible, but some updates will be needed for sketches that use custom code. In particular, users of the Macros plugin are likely to need to make adjustments to the code in the user-defined `macroAction()` function, including that function's signature, the new version of which takes a `KeyEvent` parameter instead of just an event state value. In most cases, this will make the resulting code more straightforward without any loss of functionality.
In addition to Macros, these changes might also affect user-defined code executed by the TapDance, Leader, and Syster plugins. Please see the documentation and examples for the affected plugins for details.
### Keyboard State array
The keymap cache (`Layer_::live_composite_keymap_[]`) has been replaced by a keyboard state array (`kaleidoscope::live_keys[]`). The top-level functions that handle keyswitch events have been updated to treat this new array as a representation of the current state of the keyboard, with corresponding `Key` values for any keys that are active (physically held or activated by a plugin).
#### For end-users
There should be no user-visible changes for anyone who simply uses core plugins. A few functions have been deprecated (`Layer.eventHandler()` & `Layer.updateLiveCompositeKeymap()`), but there are straightforward replacements for both.
#### For developers
The major changes are to the `handleKeyswitchEvent()` function, which has been reorganized in order to update the new keyboard state array with correct values at the appropriate times. In addition to that, two new facilities are available:
##### `EventHandlerResult::ABORT`
This is a new return value available to plugin event handlers, which is similar to `EVENT_CONSUMED` in that it causes the calling hook function to return early (stopping any subsequent handlers from seeing the event), but is treated differently by `handleKeyswitchEvent()`. If a handler returns `EVENT_CONSUMED`, the keyboard state array will still be updated by `handleKeyswitchEvent()`, but if it returns `ABORT`, it will not. In both cases, no further event processing will be done by the built-in event handler.
##### `live_keys[key_addr]`
This is the new facility for checking the value of an entry in the keyboard state array. It is indexed directly by `KeyAddr` values, without the need to convert them to integers first. For example, it could be used in a range-based `for` loop to check for values of interest:
```c++
for (KeyAddr key_addr : KeyAddr::all()) {
Key key = live_keys[key_addr];
if (key == Key_LeftShift || key == Key_RightShift) {
// do something special...
}
}
```
Additionally, if the `KeyAddr` values are not needed, one can use the iterator from the new `KeyMap` class like so:
```c++
for (Key key : live_keys.all()) {
if (key == Key_X) {
// do something special...
}
}
```
The `live_keys` object's subscript operator can also be used to set values in the keyboard state array:
```c++
live_keys[key_addr] = Key_X;
```
It also comes with several convenience functions which can be used to make the intention of the code clear:
```c++
// Set a value in the keyboard state array to a specified Key value:
live_keys.activate(key_addr, Key_X);
// Set a value to Key_Inactive, deactivating the key:
live_keys.clear(key_addr);
// Set all values in the array to Key_Inactive:
live_keys.clear();)
// Set a value to Key_Masked, masking the key until its next release event:
live_keys.mask(key_addr);
```
In most cases, it won't be necessary for plugins or user sketches to call any of these functions directly, as the built-in event handler functions will manage the keyboard state array automatically.
### New build system
In this release, we replace kaleidoscope-builder with a new Makefile based build system that uses `arduino-cli` instead of of the full Arduino IDE. This means that you can now check out development copies of Kaliedoscope into any directory, using the `KALEIDOSCOPE_DIR` environment variable to point to your installation.
@ -517,6 +617,44 @@ If your actions made use of the `left_hand` or `right_hand` arguments of
more involved to get to, out of scope for this simple migration guide. Please
open an issue, or ask for help on the forums, and we'll help you.
### OneShot
Older versions of the plugin were based on `Key` values; OneShot is now based on
`KeyAddr` coordinates instead, in order to improve reliability and
functionality.
The following deprecated functions and variables will be removed after
**2021-04-31**.
#### Deprecated functions
- `OneShot.inject(key, key_state)`: This `Key`-based function still works, but
because OneShot keys are now required to have a valid `KeyAddr`, it will now
look for an idle key, and use that, masking whatever value was mapped to that
key. Most of the reasons for using this function are better addressed by using
the newer features of the plugin, such as automatic one-shot modifiers. Use is
very strongly discouraged.
- `OneShot.isActive(key)`: This `Key`-based function no longer makes sense now
that OneShot is `KeyAddr`-based. There is a `OneShot.isActive(key_addr)`
function that should be used instead. The deprecated function still works, but
its use is discouraged.
- `OneShot.isSticky(key)`: This `Key`-based function no longer makes sense now
that OneShot is `KeyAddr`-based. There is a `OneShot.isSticky(key_addr)`
function that should be used instead. The deprecated function still works, but
its use is discouraged.
- `OneShot.isPressed()`: This function no longer has any reason for existing. In
older versions, the Escape-OneShot companion plugin used it to solve a problem
that no longer exists. It now always returns `false`.
- `OneShot.isModifierActive(key)`: This function still works, but is not
perfectly reliable, because it now returns positive results for keys other
than OneShot modifiers. It should not be used.
#### Deprecated variables
- `OneShot.time_out`: Use `OneShot.setTimeout()` instead.
- `OneShot.hold_time_out`: Use `OneShot.setHoldTimeout()` instead.
- `OneShot.double_tap_time_out`: Use `OneShot.setDoubleTapTimeout()` instead.
### Qukeys
Older versions of the plugin used `row` and `col` indexing for defining `Qukey`
@ -584,6 +722,14 @@ The following headers and names have changed:
- [Syster](plugins/Kaleidoscope-Syster.md) had the `kaleidoscope::Syster::action_t` type replaced by `kaleidoscope::plugin::Syster::action_t`.
- [TapDance](plugins/Kaleidoscope-TapDance.md) had the `kaleidoscope::TapDance::ActionType` type replaced by `kaleidoscope::plugin::TapDance::ActionType`.
### Live Composite Keymap Cache
The live composite keymap, which contained a lazily-updated version of the current keymap, has been replaced. The `Layer.updateLiveCompositeKeymap()` functions have been deprecated, and depending on the purpose of the caller, it might be appropriate to use `live_keys.activate()` instead.
When `handleKeyswitchEvent()` is looking up a `Key` value for an event, it first checks the value in the active keys cache before calling `Layer.lookup()` to get the value from the keymap. In the vast majority of cases, it won't be necessary to call `live_keys.activate()` manually, however, because simply changing the value of the `Key` parameter of an `onKeyswitchEvent()` handler will have the same effect.
Second, the `Layer.eventHandler()` function has been deprecated. There wasn't much need for this to be available to plugins, and it's possible to call `Layer.handleKeymapKeyswitchEvent()` directly instead.
# Removed APIs
### Removed on 2020-10-10

@ -0,0 +1,249 @@
# Kaleidoscope's Plugin Event Handlers
Kaleidoscope provides a set of hook functions that plugins can define in order
to do their work. If one or more of the functions listed here are defined as
methods in a plugin class, that plugin can act on the input events that drive
Kaleidoscope.
In response to input events (plus a few other places), Kaleidoscope calls the
event handlers for each plugin that defines them, in sequence.
## Return values
Every Kaleidoscope event handler function returns a value of type
`EventHandlerResult`, an enum with several variants. In some handlers,
Kaleidoscope ignores the return value, but for others, the result is used as a
signal to control Kaleidoscope's behavior. In particular, some event handler
hooks are "abortable". For those hooks, the return value of the plugin handlers
are used to control what Kaleidoscope does after each plugin's event handler
returns.
- `EventHandlerResult::OK` is used to signal that Kaleidoscope should continue
on to the next handler in the sequence.
- `EventHandlerResult::ABORT` is used to signal that Kaleidoscope should not
continue to call the other plugin handlers in the sequence, and stop
processing the event entirely. This is used by some plugins to cancel events
and/or delay them so that they occur at a later time, possibly with different
values.
- `EventHandlerResult::EVENT_CONSUMED` is used to signal that the plugin has
successfully handled the event, and that there is nothing further to be done,
so there is no point in continuing to call further plugin event handlers for
the event.
## Non-event "event" handlers
There are three special "event" handlers that are not called in response to
input events, but are instead called at fixed points during Kaleidoscope's run
time.
### `onSetup()`
This handler is called when Kaleidoscope first starts. If a plugin needs to do
some work after its constructor is called, but before Kaleidoscope enters its
main loop and starts scanning for keyswitch events, it can do it in this
function.
### `beforeEachCycle()`
This handler gets called at the beginning of every keyswitch scan cycle, before
the scan. It can be used by plugins to do things that need to be done
repeatedly, regardless of any input from the user. Typically, this involves
things like checking for timeouts.
### `afterEachCycle()`
This is just like `beforeEachCycle()`, but gets called after the keyswitches
have been scanned (and any input events handled).
## Keyswitch input event handlers
This group of event handlers is triggered when keys on the keyboard are pressed
and released. With one exception, they use a `KeyEvent` object as their one
parameter. The `KeyEvent` class encapsulates the essential data about a key
press (or release):
- `event.addr` contains the `KeyAddr` of the key that toggled on or off.
- `event.state` contains information about the current and former state of the
key in the form of a `uint8_t` bitfield.
- `event.key` contains the `Key` value of the event. For key presses, this is
generally determined by means of a keymap lookup. For releases, the value is
taken from the `live_keys` structure. Because the `event` is passed by
reference, changing this value in a plugin handler will affect which value
ends up in the `live_keys` array, and thus, the output of the keyboard.
- `event.id` contains a `KeyEventId` value: an integer, usually monotonically
increasing. This is useful as a tool to allow plugins to avoid re-processing
the same event, thus avoiding infinite loops without resorting to an
`INJECTED` key state flag which would cause other plugins to ignore events
that they might otherwise be interested in.
### `onKeyswitchEvent(KeyEvent &event)`
This handler is called in response to changes detected in the state of
keyswitches, via the `Runtime.handleKeyswitchEvent()` function. After the
keyswitches are scanned in each cycle, Kaleidoscope goes through them all and
compares the state of each one to its previous state. For any of them that have
either toggled on or off, plugins that define this function get called (until
one of them returns either `ABORT` or `EVENT_CONSUMED`).
This handler should be defined by any plugin that is concerned only with
physical keyswitch events, where the user has pressed or released a physical
key. For example, plugins that determine key values based on the timing of these
physical events should define this handler (for example, Qukeys and
TapDance). Plugins that don't explicitly need to use this handler should define
`onKeyEvent()` instead.
Plugins that use this handler should abide by certain rules in order to interact
with each other to avoid infinite loops. A plugin might return `ABORT` to delay
an event (until some other event or a timeout occurs), then later re-start
processing of the same event by calling `Runtime.handleKeyswitchEvent()`. When
it does this, it must take care to use the same `KeyEventId` value as that
event's `id` parameter, and it should also take care to preserve the order of
any such events. This way, plugins implementing `onKeyswitchEvent()` are able
to keep track of event id numbers that they have already processed fully, and
ignore those events when plugins later in the sequence re-start them.
In more specific detail, plugins that implement `onKeyswitchEvent()` must
guarantee that the `event.id` values they emit when returning `OK` are
monotonically increasing, and should only include `id` values that the plugin
has already received as input. Additionally, such plugins must ignore any event
with an `id` value that it has recently received and finished processing. The
class `KeyEventTracker` can help simplify following these rules.
### `onKeyEvent(KeyEvent &event)`
After a physical keyswitch event is processed by all of the plugins with
`onKeyswitchEvent()` handlers (and they all return `OK`), Kaleidoscope passes
that event on to the `Runtime.handleKeyEvent()` function, which calls plugins'
`onKeyEvent()` handlers. This is also the starting point for events which do not
correspond to physical key events, and can have an invalid `event.addr` value.
Plugins that need to respond to keyboard input, but which do not need to be
closely tied to physical key events (and only those events) should use
`onKeyEvent()` to do their work.
After all `onKeyEvent()` handlers have returned `OK` for an event, the
`live_keys` state array gets updated. For a key press event, the final
`event.key` value gets inserted into `live_keys[event.addr]`. From that point
on, the keyboard will behave as though a key with that value is being held until
that entry in `live_keys` is cleared (most likely as a result of a key release
event's `onKeyEvent()` handlers returning `OK`). Thus, if an `onKeyEvent()`
handler returns `ABORT` for a key release event, the keyboard will behave as
though that key is still held after it has been released. This is what enables
plugins like OneShot to function, but it also means that plugin authors need to
take care about returning `ABORT` (but not `EVENT_CONSUMED`) from an
`onKeyEvent()` handler, because it could result in "stuck" keys.
`onKeyEvent()` handlers should not store events and release them later (by
calling `Runtime.handleKeyEvent()`), and must never call
`Runtime.handleKeyswitchEvent()`.
### `onAddToReport(Key key)`
After the `onKeyEvent()` handlers have all returned `OK`, Kaleidoscope moves on
to sending Keyboard HID reports. It clears the current report, and iterates
through the `live_keys` array, looking for non-empty values, and adding them to
the report. For System Control, Consumer Control, and Keyboard HID type `Key`
values, Kaleidoscope handles adding the keycodes to the correct report, but it
also calls this handler, in case a plugin needs to alter that report.
A return value of `OK` allows Kaleidoscope to proceed with adding the
corresponding keycode(s) to the HID report, and `ABORT` causes it to leave and
keycodes from `key` out of the report.
Note that this only applies to the Keyboard and Consumer Control HID reports,
not the System Control report, which has different semantics, and only supports
a single keycode at a time.
### `beforeReportingState(const KeyEvent &event)`
This gets called right before a set of HID reports is sent. At this point,
plugins have access to a (tentative) complete HID report, as well as the full
state of all live keys on the keyboard. This is especially useful for plugins
that might need to do things like remove keycodes (such as keyboard modifiers)
from the forthcoming report just before it gets sent.
This event handler still has access to the event information for the event that
triggered the report, but because it is passed as a `const` reference, it is no
longer possible to change any of its values.
[Note: The older version of `beforeReportingState()` got called once per cycle,
regardless of the pattern of keyswitches toggling on and off, and many plugins
used it as a place to do things like check for timeouts. This new version does
not get called every cycle, so when porting old code to the newer handlers, it's
important to move any code that must be called every cycle to either
`beforeEachCycle()` or `afterEachCycle()`.]
[Also note: Unlike the deprecated `beforeReportingState()`, this one is
abortable. That is, if it returns a result other than `OK` it will stop the
subsequent handlers from getting called, and if it returns `ABORT`, it will also
stop the report from being sent.]
## Other events
### `onLayerChange()`
Called whenever one or more keymap layers are activated or deactivated (just
after the change takes place).
### `onLEDModeChange()`
Called by `LEDControl` whenever the active LED mode changes.
### `beforeSyncingLeds()`
Called immediately before Kaleidoscope sends updated color values to the
LEDs. This event handler is particularly useful to plugins that need to override
the active LED mode (e.g. LED-ActiveModColor).
### `onFocusEvent()`
### `onNameQuery()`
### `exploreSketch()`
## Deprecated
Two existing "event" handlers have been deprecated. In the old version of
Kaleidoscope's main loop, the keyboard's state information was stored in the
keyscanner (which physical switches were on in the current and former scans),
and in the HID reports. The Keyboard HID report would be cleared at the start of
every cycle, and re-populated, on key at a time, calling every
`onKeyswitchEvent()` handler for every active key. Then, once the tentative HID
report was complete, the `beforeReportingState()` handlers would be called, and
the complete report would be sent to the host. In most cycles, that report would
be identical to the previous report, and would be suppressed.
The new system stores the keyboard's current state in the `live_keys` array
instead, and only calls event handlers in response to keyswitch state changes
(and artificially generated events), ultimately sending HID reports in response
to events, rather than at the end of every cycle.
### `onKeyswitchEvent(Key &key, KeyAddr key_addr, uint8_t key_state)`
This handler was called in every cycle, for every non-idle key. Its concept of
an "event" included held keys that did not have a state change. These deprecated
handlers are still called, in response to events and also when preparing the HID
reports, but there is no longer a reasonable mechanism to call them in every
cycle, for every active key, so some functionality could be lost.
It is strongly recommended to switch to using one of the two `KeyEvent`
functions instead, depending on the needs of the plugin (either `onKeyEvent()`
if it is fit for the purpose, or `onKeyswitchEvent()` if necessary). The
`onAddToReport()` function might also be useful, particularly if the plugin in
question uses special `Key` values not recognized by Kaleidoscope itself, but
which should result in keycodes being added to HID reports.
### `beforeReportingState()`
The old version of this handler has been deprecated, but it will still be called
both before HID reports are sent and also once per cycle. It is likely that
these handlers will continue to function, but the code therein should be moved
either to the new `KeyEvent` version of `beforeReportingState()` and/or
`afterEachCycle()` (or `beforeEachCycle()`), depending on whether it needs to be
run only in response to input events or if it must execute every cycle,
respectively.

@ -17,7 +17,7 @@ A single physical input, such as a keyswitch or other input like a knob or a sli
### Key number
An integer representing a Keyswitchs position in the “Physical Layout”
An integer representing a Keyswitchs position in the “Physical Layout”. Represented in the code by the `KeyAddr` type.
### Physical Layout
@ -33,7 +33,7 @@ A representation of a specific behavior. Most often a representation of a specif
### Keymap
A list of key bindings for all keyswitchess on the Physical Layout
A list of key bindings for all keyswitchess on the Physical Layout. Represented in the code by the `KeyMap` type.
### Keymaps
@ -47,11 +47,17 @@ An entry in that ordered list of keymaps. Each layer has a unique id number that
An ordered list of all the currently-active layers, in the order they should be evaluated when figuring out what a key does.
### Override Layer
### Live keys
A special layer thats always active and evaluated before checking keys in the “Active layer stack”
A representation of the current state of the keyboard's keys, where non-transparent entries indicate keys that are active (logically—usually, but not necessarily, physically held). Represented in the code by the `LiveKeys` type (and the `live_keys` object).
#### Active/inactive keys
In the `live_keys[]` array, an _active_ key usually corresponds to a keyswitch that is physically pressed. In the common case of HID Keyboard keys, an active key will result in one or more keycodes being inserted in any new HID report. In some cases, an key can be active when its physical keyswitch is not pressed (e.g. OneShot keys that have been tapped), and in other cases a key might be _inactive_ even though its keyswitch is pressed (e.g. a Qukeys key whose value has not yet been resolved). Inactive keys are represented in the `live_keys[]` array by the special value `Key_Inactive`.
#### Masked keys
In the `live_keys[]` array, a _masked_ key is one whose next key press (either physical or logical) will be ignored. A masked key is automatically unmasked the next time it toggles off. Masked keys are represented by the special value `Key_Masked`.
## Keyswitch state

@ -75,10 +75,11 @@ KEYMAPS(
KALEIDOSCOPE_INIT_PLUGINS(Macros);
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) {
switch (macroIndex) {
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
switch (macro_id) {
case RESET:
Kaleidoscope.rebootBootloader();
if (keyToggledOn(event.state))
Kaleidoscope.rebootBootloader();
break;
default:
break;

@ -113,25 +113,24 @@ KALEIDOSCOPE_INIT_PLUGINS(
MouseKeys
);
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) {
switch (macroIndex) {
case MACRO_QWERTY:
// This macro is currently unused, but is kept around for compatibility
// reasons. We used to use it in place of `MoveToLayer(QWERTY)`, but no
// longer do. We keep it so that if someone still has the old layout with
// the macro in EEPROM, it will keep working after a firmware update.
Layer.move(QWERTY);
break;
case MACRO_VERSION_INFO:
if (keyToggledOn(keyState)) {
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
if (keyToggledOn(event.state)) {
switch (macro_id) {
case MACRO_QWERTY:
// This macro is currently unused, but is kept around for compatibility
// reasons. We used to use it in place of `MoveToLayer(QWERTY)`, but no
// longer do. We keep it so that if someone still has the old layout with
// the macro in EEPROM, it will keep working after a firmware update.
Layer.move(QWERTY);
break;
case MACRO_VERSION_INFO:
Macros.type(PSTR("Keyboardio Atreus - Kaleidoscope "));
Macros.type(PSTR(BUILD_INFORMATION));
break;
default:
break;
}
break;
default:
break;
}
return MACRO_NONE;
}

@ -85,8 +85,8 @@ static kaleidoscope::plugin::LEDSolidColor solidBlue(0, 15, 100);
static kaleidoscope::plugin::LEDSolidColor solidIndigo(0, 0, 100);
static kaleidoscope::plugin::LEDSolidColor solidViolet(70, 0, 60);
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) {
if (macroIndex == 1 && keyToggledOn(keyState)) {
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
if (macro_id == 1 && keyToggledOn(event.state)) {
Kaleidoscope.serialPort().print("Keyboard.IO keyboard driver v0.00");
return MACRO(I(25),
D(LeftShift), T(M), U(LeftShift), T(O), T(D), T(E), T(L),

@ -82,10 +82,11 @@ KEYMAPS(
KALEIDOSCOPE_INIT_PLUGINS(Macros);
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) {
switch (macroIndex) {
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
switch (macro_id) {
case QWERTY:
Layer.move(QWERTY);
if (keyToggledOn(event.state))
Layer.move(QWERTY);
break;
default:
break;

@ -0,0 +1,79 @@
/* -*- mode: c++ -*-
* AppSwitcher -- A Kaleidoscope Example
* Copyright (C) 2021 Keyboardio, 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/>.
*/
#define KALEIDOSCOPE_HOSTOS_GUESSER 1
#include <Kaleidoscope-HostOS.h>
#include "AppSwitcher.h"
namespace kaleidoscope {
namespace plugin {
EventHandlerResult AppSwitcher::onKeyEvent(KeyEvent &event) {
// Ignore all key releases
if (keyToggledOff(event.state))
return EventHandlerResult::OK;
if (event.key == AppSwitcher_Next || event.key == AppSwitcher_Prev) {
bool add_shift_flag = false;
if (event.key == AppSwitcher_Prev) {
add_shift_flag = true;
}
// For good measure:
event.state |= INJECTED;
// If AppSwitcher was not already active, hold its modifier first.
if (!active_addr_.isValid()) {
if (::HostOS.os() == hostos::OSX) {
event.key = Key_LeftGui;
} else {
event.key = Key_LeftAlt;
}
Runtime.handleKeyEvent(event);
}
// Clear the event's key address so we don't clobber the modifier.
event.addr.clear();
event.key = Key_Tab;
if (add_shift_flag)
event.key.setFlags(SHIFT_HELD);
// Press tab
Runtime.handleKeyEvent(event);
// Change state to release; this will get processed when we return OK below.
event.state = WAS_PRESSED | INJECTED;
} else if (active_addr_.isValid()) {
// If any non-AppSwitcher key is pressed while AppSwitcher is active, that
// will close AppSwitcher instead of processing that keypress. We mask the
// address of the key that closed AppSwitcher so that its release doesn't
// have any effect. Then we turn the event for that key's press into an
// event for the release of the AppSwitcher's modifier key.
live_keys.mask(event.addr);
event.addr = active_addr_;
event.state = WAS_PRESSED | INJECTED;
event.key = live_keys[event.addr];
// Turn off AppSwitcher:
active_addr_.clear();
}
return EventHandlerResult::OK;
}
} // namespace plugin
} // namespace kaleidoscope
kaleidoscope::plugin::AppSwitcher AppSwitcher;

@ -0,0 +1,43 @@
/* -*- mode: c++ -*-
* AppSwitcher -- A Kaleidoscope Example
* Copyright (C) 2021 Keyboardio, 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 <Kaleidoscope.h>
#include "Kaleidoscope-Ranges.h"
constexpr Key AppSwitcher_Next{kaleidoscope::ranges::SAFE_START};
constexpr uint16_t _prev_val = AppSwitcher_Next.getRaw() + 1;
constexpr Key AppSwitcher_Prev{_prev_val};
namespace kaleidoscope {
namespace plugin {
class AppSwitcher : public kaleidoscope::Plugin {
public:
EventHandlerResult onKeyEvent(KeyEvent &event);
private:
KeyAddr active_addr_ = KeyAddr::none();
};
} // namespace plugin
} // namespace kaleidoscope
extern kaleidoscope::plugin::AppSwitcher AppSwitcher;

@ -17,10 +17,10 @@
#include "Kaleidoscope.h"
#include "Kaleidoscope-EEPROM-Settings.h"
#include "Kaleidoscope-Macros.h"
#include "Kaleidoscope-HostOS.h"
#include "Kaleidoscope-Ranges.h"
#include "Macros.h"
#include "AppSwitcher.h"
/* *INDENT-OFF* */
KEYMAPS(
@ -32,7 +32,7 @@ KEYMAPS(
Key_PageDown, Key_Z, Key_X, Key_C, Key_V, Key_B, Key_Escape,
Key_LeftControl, Key_Backspace, Key_LeftGui, Key_LeftShift,
M(M_APPSWITCH),
AppSwitcher_Next,
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,
@ -40,24 +40,13 @@ KEYMAPS(
Key_skip, Key_N, Key_M, Key_Comma, Key_Period, Key_Slash, Key_Minus,
Key_RightShift, Key_RightAlt, Key_Spacebar, Key_RightControl,
M(M_APPCANCEL)
AppSwitcher_Prev
),
)
/* *INDENT-ON* */
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) {
switch (macroIndex) {
case M_APPSWITCH:
return macroAppSwitch(keyState);
case M_APPCANCEL:
return macroAppCancel(keyState);
}
return MACRO_NONE;
}
KALEIDOSCOPE_INIT_PLUGINS(EEPROMSettings,
HostOS,
Macros);
KALEIDOSCOPE_INIT_PLUGINS(HostOS,
AppSwitcher);
void setup() {
Kaleidoscope.setup();
@ -67,6 +56,5 @@ void setup() {
}
void loop() {
macroAppSwitchLoop();
Kaleidoscope.loop();
}

@ -1,65 +0,0 @@
/* -*- mode: c++ -*-
* AppSwitcher -- A Kaleidoscope Example
* Copyright (C) 2016-2018 Keyboardio, 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/>.
*/
#define KALEIDOSCOPE_HOSTOS_GUESSER 1
#include <Kaleidoscope-HostOS.h>
#include "Macros.h"
namespace H = kaleidoscope::hostos;
static bool appSwitchActive = false;
const macro_t *macroAppSwitch(uint8_t keyState) {
appSwitchActive = true;
// Key was just pressed, or is being held
if (keyIsPressed(keyState)) {
if (HostOS.os() == H::OSX)
return MACRO(Dr(Key_LeftGui), D(Tab));
else
return MACRO(Dr(Key_LeftAlt), D(Tab));
}
// Key was just released
if (keyToggledOff(keyState)) {
if (HostOS.os() == H::OSX)
return MACRO(U(Tab), Dr(Key_LeftGui));
else
return MACRO(U(Tab), Dr(Key_LeftAlt));
}
// otherwise we do nothing
return MACRO_NONE;
}
const macro_t *macroAppCancel(uint8_t keyState) {
if (keyToggledOn(keyState))
appSwitchActive = false;
return MACRO_NONE;
}
void macroAppSwitchLoop() {
Key mod = Key_LeftAlt;
if (HostOS.os() == H::OSX)
mod = Key_LeftGui;
// if appSwitchActive is true, we continue holding Alt.
if (appSwitchActive) {
handleKeyswitchEvent(mod, UnknownKeyswitchLocation, IS_PRESSED);
}
}

@ -42,8 +42,8 @@ KEYMAPS(
)
// *INDENT-ON*
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) {
if (macroIndex == 0 && keyToggledOff(keyState)) {
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
if (macro_id == 0 && keyToggledOff(event.state)) {
EEPROMKeymapProgrammer.activate();
}

@ -20,115 +20,85 @@
#include <Kaleidoscope-LED-Stalker.h>
#include <Kaleidoscope-Macros.h>
// This sketch is set up to demonstrate the GhostInTheFirmware plugin. The left
// palm key will activate the plugin, virtually pressing each key on the bottom
// row in sequence, and lighting up the keys using the Stalker LED effect. It
// will type out the letters from A to N, but the right palm key can be used to
// toggle the custom EventDropper plugin to suppress USB output.
// *INDENT-OFF*
KEYMAPS(
[0] = KEYMAP_STACKED
(___, ___, ___, ___, ___, ___, M(0),
(___, ___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___, ___,
Key_A, Key_B, Key_C, Key_D, Key_E, Key_F, Key_G,
___, ___, ___, ___,
___,
M(0),
___, ___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___, ___,
Key_H, Key_I, Key_J, Key_K, Key_L, Key_M, Key_N,
___, ___, ___, ___,
___),
M(1)),
)
// *INDENT-ON*
class EventDropper_ : public kaleidoscope::Plugin {
public:
EventDropper_() {}
namespace kaleidoscope {
namespace plugin {
kaleidoscope::EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
return kaleidoscope::EventHandlerResult::EVENT_CONSUMED;
class EventDropper : public Plugin {
public:
EventHandlerResult onKeyEvent(KeyEvent &event) {
if (active_)
return EventHandlerResult::EVENT_CONSUMED;
return EventHandlerResult::OK;
}
void toggle() {
active_ = !active_;
}
private:
bool active_ = false;
};
static EventDropper_ EventDropper;
} // namespace plugin
} // namespace kaleidoscope
kaleidoscope::plugin::EventDropper EventDropper;
const macro_t *macroAction(uint8_t macro_index, uint8_t key_state) {
if (macro_index == 0 && keyToggledOn(key_state))
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
if (macro_id == 0 && keyToggledOn(event.state))
GhostInTheFirmware.activate();
if (macro_id == 1 && keyToggledOn(event.state))
EventDropper.toggle();
return MACRO_NONE;
}
static const kaleidoscope::plugin::GhostInTheFirmware::GhostKey ghost_keys[] PROGMEM = {
{0, 6, 200, 50},
{0, 5, 200, 50},
{0, 4, 200, 50},
{0, 3, 200, 50},
{0, 2, 200, 50},
{0, 1, 200, 50},
{0, 0, 200, 50},
{1, 0, 200, 50},
{1, 1, 200, 50},
{1, 2, 200, 50},
{1, 3, 200, 50},
{1, 4, 200, 50},
{1, 5, 200, 50},
{1, 6, 200, 50},
{2, 6, 200, 50},
{2, 5, 200, 50},
{2, 4, 200, 50},
{2, 3, 200, 50},
{2, 2, 200, 50},
{2, 1, 200, 50},
{2, 0, 200, 50},
{3, 0, 200, 50},
{3, 1, 200, 50},
{3, 3, 200, 50},
{3, 4, 200, 50},
{3, 5, 200, 50},
{0, 7, 200, 50},
{1, 7, 200, 50},
{2, 7, 200, 50},
{3, 7, 200, 50},
{3, 6, 200, 50},
{3, 9, 200, 50},
{3, 8, 200, 50},
{2, 8, 200, 50},
{1, 8, 200, 50},
{0, 8, 200, 50},
{3, 10, 200, 50},
{3, 11, 200, 50},
{3, 12, 200, 50},
{3, 13, 200, 50},
{3, 14, 200, 50},
{3, 15, 200, 50},
{2, 15, 200, 50},
{2, 14, 200, 50},
{2, 13, 200, 50},
{2, 12, 200, 50},
{2, 11, 200, 50},
{2, 10, 200, 50},
{2, 9, 200, 50},
{1, 9, 200, 50},
{1, 10, 200, 50},
{1, 11, 200, 50},
{1, 12, 200, 50},
{1, 13, 200, 50},
{1, 14, 200, 50},
{1, 15, 200, 50},
{0, 15, 200, 50},
{0, 14, 200, 50},
{0, 13, 200, 50},
{0, 12, 200, 50},
{0, 11, 200, 50},
{0, 10, 200, 50},
{0, 9, 200, 50},
{0, 0, 0, 0}
{KeyAddr(3, 0), 200, 50},
{KeyAddr(3, 1), 200, 50},
{KeyAddr(3, 2), 200, 50},
{KeyAddr(3, 3), 200, 50},
{KeyAddr(3, 4), 200, 50},
{KeyAddr(3, 5), 200, 50},
{KeyAddr(2, 6), 200, 50},
{KeyAddr(2, 9), 200, 50},
{KeyAddr(3, 10), 200, 50},
{KeyAddr(3, 11), 200, 50},
{KeyAddr(3, 12), 200, 50},
{KeyAddr(3, 13), 200, 50},
{KeyAddr(3, 14), 200, 50},
{KeyAddr(3, 15), 200, 50},
{KeyAddr::none(), 0, 0}
};
KALEIDOSCOPE_INIT_PLUGINS(GhostInTheFirmware,
LEDControl,
StalkerEffect,
Macros,
EventDropper);

@ -0,0 +1,154 @@
/* -*- mode: c++ -*-
* Kaleidoscope-Macros Examples
* Copyright (C) 2021 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/>.
*/
#include <Kaleidoscope.h>
#include <Kaleidoscope-Macros.h>
#include <Kaleidoscope-OneShot.h>
// Macros
enum {
TOGGLE_ONESHOT,
};
// *INDENT-OFF*
KEYMAPS(
[0] = KEYMAP_STACKED
(___, M(1), M(2), M(3), M(4), M(5), ___,
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,
ShiftToLayer(1),
___, M(6), M(7), M(8), M(9), M(0), ___,
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_N, Key_M, Key_Comma, Key_Period, Key_Slash, Key_Minus,
Key_RightShift, Key_LeftAlt, Key_Spacebar, Key_RightControl,
ShiftToLayer(1)),
[1] = KEYMAP_STACKED
(
___, ___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___, ___,
___, ___, ___, ___,
___,
___, ___, ___, ___, ___, ___, ___,
___, ___, ___, ___, ___, ___, ___,
Key_UpArrow, Key_DownArrow, Key_LeftArrow, Key_RightArrow,___, ___,
___, ___, ___, ___, ___, ___, ___,
___, ___, ___, ___,
___),
)
// *INDENT-ON*
// Example macro for typing a string of characters.
void macroTypeString(KeyEvent &event) {
if (keyToggledOn(event.state)) {
Macros.type(PSTR("Hello, world!"));
}
}
// Example macro for macro step sequence.
const macro_t* macroSteps(KeyEvent &event) {
if (keyToggledOn(event.state)) {
// Note that the following sequence leaves two keys down (`Key_RightAlt` and
// `Key_C`). These virtual keys will remain in effect until the Macros key
// is released.
return MACRO(I(200), D(LeftShift), T(A), D(RightAlt), T(B), U(LeftShift), D(C));
}
return MACRO_NONE;
}
// Example macro that sets `event.key`.
const macro_t* macroNewSentence1(KeyEvent &event) {
if (keyToggledOn(event.state)) {
event.key = OSM(LeftShift);
return MACRO(Tc(Period), Tc(Spacebar), Tc(Spacebar));
}
return MACRO_NONE;
}
// Alternate example for above.
void macroNewSentence2(KeyEvent &event) {
if (keyToggledOn(event.state)) {
Macros.type(PSTR(". "));
event.key = OSM(LeftShift);
}
}
// Macro that calls `handleKeyEvent()`. This version works even if the OneShot
// plugin is registered before Macros in `KALEIDOSCOPE_INIT_PLUGINS()`.
void macroNewSentence3(KeyEvent &event) {
Macros.tap(Key_Period);
Macros.tap(Key_Spacebar);
Macros.tap(Key_Spacebar);
// Change the event into a OneShot key event.
event.key = OSM(LeftShift);
kaleidoscope::Runtime.handleKeyEvent(event);
// We can effectively erase the Macros key event, effectively aborting it.
event.key = Key_NoKey;
event.addr.clear();
}
// Macro that auto-repeats?
const macro_t* macroAction(uint8_t macro_id, KeyEvent &event) {
switch (macro_id) {
case 0:
macroTypeString(event);
break;
case 1:
return macroNewSentence1(event);
case 2:
macroNewSentence2(event);
break;
case 3:
macroNewSentence3(event);
break;
case 4:
return macroSteps(event);
default:
break;
}
return MACRO_NONE;
}
// For some of the above examples, it's important that Macros is registered
// before OneShot here.
KALEIDOSCOPE_INIT_PLUGINS(Macros, OneShot);
void setup() {
Kaleidoscope.setup();
}
void loop() {
Kaleidoscope.loop();
}

@ -0,0 +1,6 @@
{
"cpu": {
"fqbn": "keyboardio:avr:model01",
"port": ""
}
}

@ -21,27 +21,27 @@
// Macros
enum {
OSMALTCTRL,
TOGGLE_ONESHOT,
};
// *INDENT-OFF*
KEYMAPS(
[0] = KEYMAP_STACKED
(
Key_NoKey, Key_1, Key_2, Key_3, Key_4, Key_5, Key_NoKey,
M(TOGGLE_ONESHOT), Key_1, Key_2, Key_3, Key_4, Key_5, OneShot_MetaStickyKey,
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,
OSM(LeftControl), Key_Backspace, OSM(LeftGui), OSM(LeftShift),
M(OSMALTCTRL),
Key_Meh,
Key_skip, Key_6, Key_7, Key_8, Key_9, Key_0, Key_skip,
OneShot_ActiveStickyKey, Key_6, Key_7, Key_8, Key_9, Key_0, ___,
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_N, Key_M, Key_Comma, Key_Period, Key_Slash, Key_Minus,
OSM(RightShift), OSM(RightAlt), Key_Spacebar, OSM(RightControl),
Key_RightShift, Key_RightAlt, Key_Spacebar, Key_RightControl,
OSL(1)),
[1] = KEYMAP_STACKED
@ -64,14 +64,13 @@ KEYMAPS(
)
// *INDENT-ON*
void macroOneShotAltControl(uint8_t keyState) {
OneShot.inject(OSM(LeftAlt), keyState);
OneShot.inject(OSM(LeftControl), keyState);
void macroToggleOneShot() {
OneShot.toggleAutoOneShot();
}
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) {
if (macroIndex == OSMALTCTRL) {
macroOneShotAltControl(keyState);
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
if (macro_id == TOGGLE_ONESHOT) {
macroToggleOneShot();
}
return MACRO_NONE;

@ -49,10 +49,10 @@ KEYMAPS(
// *INDENT-ON*
// Defining a macro (on the "any" key: see above) to toggle Qukeys on and off
const macro_t *macroAction(uint8_t macro_index, uint8_t key_state) {
switch (macro_index) {
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
switch (macro_id) {
case MACRO_TOGGLE_QUKEYS:
if (keyToggledOn(key_state))
if (keyToggledOn(event.state))
Qukeys.toggle();
break;
}
@ -74,6 +74,7 @@ void setup() {
Qukeys.setOverlapThreshold(50);
Qukeys.setMinimumHoldTime(100);
Qukeys.setMinimumPriorInterval(80);
Qukeys.setMaxIntervalForTapRepeat(150);
Kaleidoscope.setup();
}

@ -49,10 +49,7 @@ void systerAction(kaleidoscope::plugin::Syster::action_t action, const char *sym
Unicode.type(0x2328);
break;
case kaleidoscope::plugin::Syster::EndAction:
handleKeyswitchEvent(Key_Backspace, UnknownKeyswitchLocation, IS_PRESSED | INJECTED);
Kaleidoscope.hid().keyboard().sendReport();
handleKeyswitchEvent(Key_Backspace, UnknownKeyswitchLocation, WAS_PRESSED | INJECTED);
Kaleidoscope.hid().keyboard().sendReport();
kaleidoscope::eraseChars(1);
break;
case kaleidoscope::plugin::Syster::SymbolAction:
Kaleidoscope.serialPort().print("systerAction: symbol=");

@ -52,10 +52,10 @@ static void unicode(uint32_t character, uint8_t keyState) {
}
}
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) {
switch (macroIndex) {
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
switch (macro_id) {
case MACRO_KEYBOARD_EMOJI:
unicode(0x2328, keyState);
unicode(0x2328, event.state);
break;
}
return MACRO_NONE;

@ -47,7 +47,7 @@ KALEIDOSCOPE_INIT_PLUGINS(LEDControl,
void setup() {
Kaleidoscope.setup();
ActiveModColorEffect.highlight_color = CRGB(0x00, 0xff, 0xff);
ActiveModColorEffect.setHighlightColor(CRGB(0x00, 0xff, 0xff));
}
void loop() {

@ -42,11 +42,11 @@ KEYMAPS(
)
// *INDENT-ON*
const macro_t *macroAction(uint8_t macro_index, uint8_t key_state) {
if (!keyToggledOn(key_state))
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
if (!keyToggledOn(event.state))
return MACRO_NONE;
if (macro_index == 0) {
if (macro_id == 0) {
for (uint8_t i = Key_A.getKeyCode(); i <= Key_0.getKeyCode(); i++) {
LEDControl.set_all_leds_to(0, 0, 0);
LEDControl.syncLeds();

@ -45,16 +45,16 @@ KALEIDOSCOPE_INIT_PLUGINS(LEDControl,
Macros,
LEDRainbowWaveEffect);
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) {
if (keyToggledOn(keyState)) {
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
if (keyToggledOn(event.state)) {
uint8_t brightness = LEDControl.getBrightness();
if (macroIndex == 0) {
if (macro_id == 0) {
if (brightness > 10)
brightness -= 10;
else
brightness = 0;
} else if (macroIndex == 1) {
} else if (macro_id == 1) {
if (brightness < 245)
brightness += 10;
else

@ -25,6 +25,7 @@ namespace kaleidoscope {
namespace plugin {
// --- state ---
Key Cycle::last_non_cycle_key_;
KeyAddr Cycle::cycle_key_addr_{KeyAddr::invalid_state};
uint8_t Cycle::current_modifier_flags_;
uint8_t Cycle::cycle_count_;
@ -35,15 +36,12 @@ uint8_t Cycle::cycle_count_;
// --- api ---
void Cycle::replace(Key key) {
handleKeyswitchEvent(Key_Backspace, UnknownKeyswitchLocation, IS_PRESSED | INJECTED);
kaleidoscope::Runtime.hid().keyboard().sendReport();
handleKeyswitchEvent(Key_Backspace, UnknownKeyswitchLocation, WAS_PRESSED | INJECTED);
kaleidoscope::Runtime.hid().keyboard().sendReport();
handleKeyswitchEvent(key, UnknownKeyswitchLocation, IS_PRESSED | INJECTED);
kaleidoscope::Runtime.hid().keyboard().sendReport();
handleKeyswitchEvent(key, UnknownKeyswitchLocation, WAS_PRESSED | INJECTED);
kaleidoscope::Runtime.hid().keyboard().sendReport();
if (cycle_key_addr_ == KeyAddr{KeyAddr::invalid_state})
return;
Runtime.handleKeyEvent(KeyEvent{cycle_key_addr_, IS_PRESSED | INJECTED, Key_Backspace});
Runtime.handleKeyEvent(KeyEvent{cycle_key_addr_, WAS_PRESSED | INJECTED, Key_Backspace});
Runtime.handleKeyEvent(KeyEvent{cycle_key_addr_, IS_PRESSED | INJECTED, key});
Runtime.handleKeyEvent(KeyEvent{cycle_key_addr_, WAS_PRESSED | INJECTED, key});
}
void Cycle::replace(uint8_t cycle_size, const Key cycle_steps[]) {
@ -57,35 +55,29 @@ EventHandlerResult Cycle::onNameQuery() {
return ::Focus.sendName(F("Cycle"));
}
EventHandlerResult Cycle::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
if (key_state & INJECTED)
EventHandlerResult Cycle::onKeyEvent(KeyEvent &event) {
if (event.state & INJECTED)
return EventHandlerResult::OK;
if (!keyIsPressed(key_state) && !keyWasPressed(key_state)) {
if (isCycle(mapped_key)) {
return EventHandlerResult::EVENT_CONSUMED;
}
return EventHandlerResult::OK;
}
if (!isCycle(mapped_key)) {
if (keyToggledOn(key_state)) {
current_modifier_flags_ |= toModFlag(mapped_key.getKeyCode());
last_non_cycle_key_.setKeyCode(mapped_key.getKeyCode());
if (!isCycle(event.key)) {
if (keyToggledOn(event.state)) {
current_modifier_flags_ |= toModFlag(event.key.getKeyCode());
last_non_cycle_key_.setKeyCode(event.key.getKeyCode());
last_non_cycle_key_.setFlags(current_modifier_flags_);
cycle_count_ = 0;
}
if (keyToggledOff(key_state)) {
current_modifier_flags_ &= ~toModFlag(mapped_key.getKeyCode());
if (keyToggledOff(event.state)) {
current_modifier_flags_ &= ~toModFlag(event.key.getKeyCode());
}
return EventHandlerResult::OK;
}
if (!keyToggledOff(key_state)) {
if (!keyToggledOff(event.state)) {
return EventHandlerResult::EVENT_CONSUMED;
}
++cycle_count_;
cycle_key_addr_ = event.addr;
cycleAction(last_non_cycle_key_, cycle_count_);
return EventHandlerResult::EVENT_CONSUMED;
}

@ -37,11 +37,12 @@ class Cycle : public kaleidoscope::Plugin {
static void replace(uint8_t cycle_size, const Key cycle_steps[]);
EventHandlerResult onNameQuery();
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
private:
static uint8_t toModFlag(uint8_t keyCode);
static Key last_non_cycle_key_;
static KeyAddr cycle_key_addr_;
static uint8_t cycle_count_;
static uint8_t current_modifier_flags_;
};

@ -25,34 +25,35 @@ namespace plugin {
uint16_t DynamicMacros::storage_base_;
uint16_t DynamicMacros::storage_size_;
uint16_t DynamicMacros::map_[];
static void playMacroKeyswitchEvent(Key key, uint8_t keyswitch_state, bool explicit_report) {
handleKeyswitchEvent(key, UnknownKeyswitchLocation, keyswitch_state | INJECTED);
if (explicit_report)
return;
kaleidoscope::Runtime.hid().keyboard().sendReport();
kaleidoscope::Runtime.hid().mouse().sendReport();
Key DynamicMacros::active_macro_keys_[];
// =============================================================================
// It might be possible to use Macros instead of reproducing it
void DynamicMacros::press(Key key) {
Runtime.handleKeyEvent(KeyEvent(KeyAddr::none(), IS_PRESSED | INJECTED, key));
for (Key &mkey : active_macro_keys_) {
if (mkey == Key_NoKey) {
mkey = key;
break;
}
}
}
static void playKeyCode(Key key, uint8_t keyStates, bool explicit_report) {
if (keyIsPressed(keyStates)) {
playMacroKeyswitchEvent(key, IS_PRESSED, explicit_report);
}
if (keyWasPressed(keyStates)) {
playMacroKeyswitchEvent(key, WAS_PRESSED, explicit_report);
void DynamicMacros::release(Key key) {
for (Key &mkey : active_macro_keys_) {
if (mkey == key) {
mkey = Key_NoKey;
}
}
Runtime.handleKeyEvent(KeyEvent(KeyAddr::none(), WAS_PRESSED | INJECTED, key));
}
static void readKeyCodeAndPlay(uint16_t pos, uint8_t flags, uint8_t keyStates, bool explicit_report) {
Key key(Runtime.storage().read(pos++), // key_code
flags);
playKeyCode(key, keyStates, explicit_report);
void DynamicMacros::tap(Key key) {
Runtime.handleKeyEvent(KeyEvent(KeyAddr::none(), IS_PRESSED | INJECTED, key));
Runtime.handleKeyEvent(KeyEvent(KeyAddr::none(), WAS_PRESSED | INJECTED, key));
}
void DynamicMacros::updateDynamicMacroCache(void) {
void DynamicMacros::updateDynamicMacroCache() {
uint16_t pos = storage_base_;
uint8_t current_id = 0;
macro_t macro = MACRO_ACTION_END;
@ -116,27 +117,22 @@ void DynamicMacros::updateDynamicMacroCache(void) {
}
}
// public
void DynamicMacros::play(uint8_t macro_id) {
macro_t macro = MACRO_ACTION_END;
uint8_t interval = 0;
uint8_t flags;
bool explicit_report = false;
uint16_t pos;
Key key;
pos = storage_base_ + map_[macro_id];
while (true) {
switch (macro = Runtime.storage().read(pos++)) {
case MACRO_ACTION_STEP_EXPLICIT_REPORT:
explicit_report = true;
break;
case MACRO_ACTION_STEP_IMPLICIT_REPORT:
explicit_report = false;
break;
case MACRO_ACTION_STEP_SEND_REPORT:
kaleidoscope::Runtime.hid().keyboard().sendReport();
kaleidoscope::Runtime.hid().mouse().sendReport();
break;
case MACRO_ACTION_STEP_INTERVAL:
interval = Runtime.storage().read(pos++);
break;
@ -145,46 +141,59 @@ void DynamicMacros::play(uint8_t macro_id) {
delay(wait);
break;
}
case MACRO_ACTION_STEP_KEYDOWN:
flags = Runtime.storage().read(pos++);
readKeyCodeAndPlay(pos++, flags, IS_PRESSED, explicit_report);
key.setFlags(Runtime.storage().read(pos++));
key.setKeyCode(Runtime.storage().read(pos++));
press(key);
break;
case MACRO_ACTION_STEP_KEYUP:
flags = Runtime.storage().read(pos++);
readKeyCodeAndPlay(pos++, flags, WAS_PRESSED, explicit_report);
key.setFlags(Runtime.storage().read(pos++));
key.setKeyCode(Runtime.storage().read(pos++));
release(key);
break;
case MACRO_ACTION_STEP_TAP:
flags = Runtime.storage().read(pos++);
readKeyCodeAndPlay(pos++, flags, IS_PRESSED | WAS_PRESSED, false);
key.setFlags(Runtime.storage().read(pos++));
key.setKeyCode(Runtime.storage().read(pos++));
tap(key);
break;
case MACRO_ACTION_STEP_KEYCODEDOWN:
readKeyCodeAndPlay(pos++, 0, IS_PRESSED, explicit_report);
key.setFlags(0);
key.setKeyCode(Runtime.storage().read(pos++));
press(key);
break;
case MACRO_ACTION_STEP_KEYCODEUP:
readKeyCodeAndPlay(pos++, 0, WAS_PRESSED, explicit_report);
key.setFlags(0);
key.setKeyCode(Runtime.storage().read(pos++));
release(key);
break;
case MACRO_ACTION_STEP_TAPCODE:
readKeyCodeAndPlay(pos++, 0, IS_PRESSED | WAS_PRESSED, false);
key.setFlags(0);
key.setKeyCode(Runtime.storage().read(pos++));
tap(key);
break;
case MACRO_ACTION_STEP_TAP_SEQUENCE: {
uint8_t keyCode;
do {
flags = Runtime.storage().read(pos++);
keyCode = Runtime.storage().read(pos++);
playKeyCode(Key(keyCode, flags), IS_PRESSED | WAS_PRESSED, false);
while (true) {
key.setFlags(0);
key.setKeyCode(pgm_read_byte(pos++));
if (key == Key_NoKey)
break;
tap(key);
delay(interval);
} while (!(flags == 0 && keyCode == 0));
}
break;
}
case MACRO_ACTION_STEP_TAP_CODE_SEQUENCE: {
uint8_t keyCode;
do {
keyCode = Runtime.storage().read(pos++);
playKeyCode(Key(keyCode, 0), IS_PRESSED | WAS_PRESSED, false);
while (true) {
key.setFlags(0);
key.setKeyCode(pgm_read_byte(pos++));
if (key.getKeyCode() == 0)
break;
tap(key);
delay(interval);
} while (keyCode != 0);
}
break;
}
@ -197,12 +206,23 @@ void DynamicMacros::play(uint8_t macro_id) {
}
}
EventHandlerResult DynamicMacros::onKeyswitchEvent(Key &mappedKey, KeyAddr key_addr, uint8_t keyState) {
if (mappedKey.getRaw() < ranges::DYNAMIC_MACRO_FIRST || mappedKey.getRaw() > ranges::DYNAMIC_MACRO_LAST)
bool isDynamicMacrosKey(Key key) {
return (key.getRaw() >= ranges::DYNAMIC_MACRO_FIRST &&
key.getRaw() <= ranges::DYNAMIC_MACRO_LAST);
}
// -----------------------------------------------------------------------------
EventHandlerResult DynamicMacros::onKeyEvent(KeyEvent &event) {
if (!isDynamicMacrosKey(event.key))
return EventHandlerResult::OK;
if (keyToggledOn(keyState)) {
play(mappedKey.getRaw() - ranges::DYNAMIC_MACRO_FIRST);
if (keyToggledOn(event.state)) {
uint8_t macro_id = event.key.getRaw() - ranges::DYNAMIC_MACRO_FIRST;
play(macro_id);
} else {
for (Key key : active_macro_keys_) {
release(key);
}
}
return EventHandlerResult::EVENT_CONSUMED;
@ -249,14 +269,14 @@ EventHandlerResult DynamicMacros::onFocusEvent(const char *command) {
return EventHandlerResult::EVENT_CONSUMED;
}
// public
void DynamicMacros::reserve_storage(uint16_t size) {
storage_base_ = ::EEPROMSettings.requestSlice(size);
storage_size_ = size;
updateDynamicMacroCache();
}
}
}
} // namespace plugin
} // namespace kaleidoscope
kaleidoscope::plugin::DynamicMacros DynamicMacros;

@ -24,15 +24,15 @@
#define DM(n) Key(kaleidoscope::ranges::DYNAMIC_MACRO_FIRST + n)
#define MAX_CONCURRENT_DYNAMIC_MACRO_KEYS 8
namespace kaleidoscope {
namespace plugin {
class DynamicMacros : public kaleidoscope::Plugin {
public:
DynamicMacros(void) {}
EventHandlerResult onKeyswitchEvent(Key &mappedKey, KeyAddr key_addr, uint8_t keyState);
EventHandlerResult onNameQuery();
EventHandlerResult onKeyEvent(KeyEvent &event);
EventHandlerResult onFocusEvent(const char *command);
static void reserve_storage(uint16_t size);
@ -43,10 +43,14 @@ class DynamicMacros : public kaleidoscope::Plugin {
static uint16_t storage_base_;
static uint16_t storage_size_;
static uint16_t map_[31];
static void updateDynamicMacroCache(void);
static void updateDynamicMacroCache();
static Key active_macro_keys_[MAX_CONCURRENT_DYNAMIC_MACRO_KEYS];
static void press(Key key);
static void release(Key key);
static void tap(Key key);
};
}
}
} // namespace plugin
} // namespace kaleidoscope
extern kaleidoscope::plugin::DynamicMacros DynamicMacros;

@ -19,6 +19,8 @@
#include <Kaleidoscope-EEPROM-Settings.h>
#include "Kaleidoscope-FocusSerial.h"
#include "kaleidoscope/Runtime.h"
#include "kaleidoscope/KeyEvent.h"
namespace kaleidoscope {
namespace plugin {
@ -72,14 +74,14 @@ bool DynamicTapDance::dance(uint8_t tap_dance_index, KeyAddr key_addr,
break;
case TapDance::Interrupt:
case TapDance::Timeout:
handleKeyswitchEvent(key, key_addr, IS_PRESSED | INJECTED);
Runtime.handleKeyEvent(KeyEvent(key_addr, IS_PRESSED, key));
break;
case TapDance::Hold:
handleKeyswitchEvent(key, key_addr, IS_PRESSED | WAS_PRESSED | INJECTED);
Runtime.handleKeyEvent(KeyEvent(key_addr, IS_PRESSED | WAS_PRESSED, key));
break;
case TapDance::Release:
kaleidoscope::Runtime.hid().keyboard().sendReport();
handleKeyswitchEvent(key, key_addr, WAS_PRESSED | INJECTED);
//kaleidoscope::Runtime.hid().keyboard().sendReport();
Runtime.handleKeyEvent(KeyEvent(key_addr, WAS_PRESSED, key));
break;
}

@ -53,27 +53,27 @@ void EEPROMKeymapProgrammer::cancel(void) {
state_ = INACTIVE;
}
EventHandlerResult EEPROMKeymapProgrammer::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
EventHandlerResult EEPROMKeymapProgrammer::onKeyEvent(KeyEvent &event) {
if (state_ == INACTIVE)
return EventHandlerResult::OK;
if (state_ == WAIT_FOR_KEY) {
if (keyToggledOn(key_state)) {
update_position_ = Layer.mostRecent() * Runtime.device().numKeys() + key_addr.toInt();
if (keyToggledOn(event.state)) {
update_position_ = Layer.mostRecent() * Runtime.device().numKeys() + event.addr.toInt();
}
if (keyToggledOff(key_state)) {
if ((uint16_t)(Layer.mostRecent() * Runtime.device().numKeys() + key_addr.toInt()) == update_position_)
if (keyToggledOff(event.state)) {
if ((uint16_t)(Layer.mostRecent() * Runtime.device().numKeys() + event.addr.toInt()) == update_position_)
nextState();
}
return EventHandlerResult::EVENT_CONSUMED;
}
if (state_ == WAIT_FOR_SOURCE_KEY) {
if (keyToggledOn(key_state)) {
new_key_ = Layer.getKeyFromPROGMEM(Layer.mostRecent(), key_addr);
if (keyToggledOn(event.state)) {
new_key_ = Layer.getKeyFromPROGMEM(Layer.mostRecent(), event.addr);
}
if (keyToggledOff(key_state)) {
if (new_key_ == Layer.getKeyFromPROGMEM(Layer.mostRecent(), key_addr))
if (keyToggledOff(event.state)) {
if (new_key_ == Layer.getKeyFromPROGMEM(Layer.mostRecent(), event.addr))
nextState();
}
return EventHandlerResult::EVENT_CONSUMED;
@ -81,18 +81,18 @@ EventHandlerResult EEPROMKeymapProgrammer::onKeyswitchEvent(Key &mapped_key, Key
// WAIT_FOR_CODE state
if (mapped_key < Key_1 || mapped_key > Key_0)
if (event.key < Key_1 || event.key > Key_0)
return EventHandlerResult::OK;
if (!keyToggledOn(key_state)) {
if (!keyToggledOn(event.state)) {
return EventHandlerResult::EVENT_CONSUMED;
}
uint8_t n;
if (mapped_key.getKeyCode() == Key_0.getKeyCode())
if (event.key.getKeyCode() == Key_0.getKeyCode())
n = 0;
else
n = mapped_key.getKeyCode() - Key_1.getKeyCode() + 1;
n = event.key.getKeyCode() - Key_1.getKeyCode() + 1;
new_key_.setRaw(new_key_.getRaw() * 10 + n);

@ -38,7 +38,7 @@ class EEPROMKeymapProgrammer : public kaleidoscope::Plugin {
static void nextState(void);
static void cancel(void);
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
EventHandlerResult onFocusEvent(const char *command);
private:

@ -27,7 +27,17 @@ The plugin only makes sense when using one-shot keys.
## Plugin methods
The plugin provides the `EscapeOneShot` object, which has no public methods.
The plugin provides the `EscapeOneShot` object, which has one public
configuration method:
### `.setCancelKey(key)`
> Changes the `Key` value that will trigger deactivation of one-shot
> (including sticky) keys. The default is to use `Key_Escape` (the
> normal `Esc` key), but if you would rather have a dedicated key (so
> that you can use `Key_Escape` in combination with one-shot
> modifiers), there is the special `OneShotCancelKey`, which will not
> have any side effects.
## Dependencies

@ -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,32 @@
#include <Kaleidoscope-OneShot.h>
#include <Kaleidoscope-Escape-OneShot.h>
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/layers.h"
namespace kaleidoscope {
namespace plugin {
bool EscapeOneShot::did_escape_;
EventHandlerResult EscapeOneShot::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState) {
if (mapped_key != Key_Escape || (keyState & INJECTED))
return EventHandlerResult::OK;
if (did_escape_)
mapped_key = Key_NoKey;
did_escape_ = !keyToggledOff(keyState);
if ((!::OneShot.isActive() || ::OneShot.isPressed()) && !::OneShot.isSticky()) {
return EventHandlerResult::OK;
Key EscapeOneShot::cancel_oneshot_key_{Key_Escape};
EventHandlerResult EscapeOneShot::onKeyEvent(KeyEvent &event) {
// We only act on an escape key (or `cancel_oneshot_key_`, if that has been
// set) 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 (event.key == cancel_oneshot_key_ &&
keyToggledOn(event.state) &&
(event.state & INJECTED) == 0 &&
::OneShot.isActive()) {
// Cancel all OneShot keys
::OneShot.cancel(true);
// Change the cancellation key to a blank key, and signal that event
// processing is complete.
event.key = Key_NoKey;
return EventHandlerResult::EVENT_CONSUMED;
}
::OneShot.cancel(true);
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
@ -19,16 +19,22 @@
#include "kaleidoscope/Runtime.h"
constexpr Key OneShotCancelKey {kaleidoscope::ranges::OS_CANCEL};
namespace kaleidoscope {
namespace plugin {
class EscapeOneShot : public kaleidoscope::Plugin {
public:
EscapeOneShot(void) {}
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState);
EventHandlerResult onKeyEvent(KeyEvent &event);
void setCancelKey(Key cancel_key) {
cancel_oneshot_key_ = cancel_key;
}
private:
static bool did_escape_;
static Key cancel_oneshot_key_;
};
}
}

@ -50,20 +50,22 @@ void FingerPainter::toggle(void) {
edit_mode_ = !edit_mode_;
}
EventHandlerResult FingerPainter::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
EventHandlerResult FingerPainter::onKeyEvent(KeyEvent &event) {
if (!Runtime.has_leds || !edit_mode_)
return EventHandlerResult::OK;
if (!keyToggledOn(key_state)) {
return EventHandlerResult::EVENT_CONSUMED;
if (keyToggledOff(event.state)) {
return EventHandlerResult::OK;
}
if (!key_addr.isValid())
return EventHandlerResult::EVENT_CONSUMED;
if (!event.addr.isValid())
return EventHandlerResult::OK;
// TODO(anyone): The following works only for keyboards with LEDs for each key.
uint8_t color_index = ::LEDPaletteTheme.lookupColorIndexAtPosition(color_base_, Runtime.device().getLedIndex(key_addr));
uint8_t color_index = ::LEDPaletteTheme
.lookupColorIndexAtPosition(color_base_,
Runtime.device().getLedIndex(event.addr));
// Find the next color in the palette that is different.
// But do not loop forever!
@ -80,7 +82,9 @@ EventHandlerResult FingerPainter::onKeyswitchEvent(Key &mapped_key, KeyAddr key_
new_color = ::LEDPaletteTheme.lookupPaletteColor(color_index);
}
::LEDPaletteTheme.updateColorIndexAtPosition(color_base_, Runtime.device().getLedIndex(key_addr), color_index);
::LEDPaletteTheme.updateColorIndexAtPosition(color_base_,
Runtime.device().getLedIndex(event.addr),
color_index);
return EventHandlerResult::EVENT_CONSUMED;
}

@ -32,7 +32,7 @@ class FingerPainter : public LEDMode {
static void toggle(void);
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
EventHandlerResult onFocusEvent(const char *command);
EventHandlerResult onSetup();
EventHandlerResult onNameQuery();

@ -32,7 +32,7 @@ void FocusSerial::drain(void) {
Runtime.serialPort().read();
}
EventHandlerResult FocusSerial::beforeReportingState() {
EventHandlerResult FocusSerial::afterEachCycle() {
if (Runtime.serialPort().available() == 0)
return EventHandlerResult::OK;

@ -91,7 +91,7 @@ class FocusSerial : public kaleidoscope::Plugin {
static constexpr char NEWLINE = '\n';
/* Hooks */
EventHandlerResult beforeReportingState();
EventHandlerResult afterEachCycle();
EventHandlerResult onFocusEvent(const char *command);
private:

@ -22,16 +22,16 @@ that.
#include <Kaleidoscope-GhostInTheFirmware.h>
#include <Kaleidoscope-Macros.h>
const macro_t *macroAction(uint8_t macro_index, uint8_t key_state) {
if (macro_index == 0 && keyToggledOn(key_state))
const macro_t *macroAction(uint8_t macro_id, KeyEvent& event) {
if (macro_id == 0 && keyToggledOn(event.state))
GhostInTheFirmware.activate();
return MACRO_NONE;
}
static const kaleidoscope::plugin::GhostInTheFirmware::GhostKey ghost_keys[] PROGMEM = {
{0, 0, 200, 50},
{0, 0, 0}
{KeyAddr(0, 0), 200, 50},
{KeyAddr::none(), 0, 0}
};
KALEIDOSCOPE_INIT_PLUGINS(GhostInTheFirmware,
@ -59,13 +59,14 @@ methods and properties:
### `.ghost_keys`
> Set this property to the sequence of keys to press, by assigning a sequence to
> this variable. Each element is a quartett of `row`, `column`, a `pressTime`,
> and a `delay`. Each of these will be pressed in different cycles, unlike
> macros which play back within a single cycle.
>
> The key at `row`, `column` will be held for `pressTime` milliseconds, and
> after an additional `delay` milliseconds, the plugin will move on to the next
> entry in the sequence.
> this variable. Each element is a `GhostKey` object, comprised of a `KeyAddr`
> (the location of a key on the keyboard), a duration of the key press (in
> milliseconds), and a delay after the key release until the next one is pressed
> (also in milliseconds).
> This `ghost_keys` array *MUST* end with the sentinal value of
> `{KeyAddr::none(), 0, 0}` to ensure that GhostInTheFirmware doesn't read past
> the end of the array.
>
> The sequence *MUST* reside in `PROGMEM`.

@ -18,54 +18,63 @@
#include "kaleidoscope/Runtime.h"
#include <Kaleidoscope-GhostInTheFirmware.h>
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/progmem_helpers.h"
namespace kaleidoscope {
namespace plugin {
const GhostInTheFirmware::GhostKey *GhostInTheFirmware::ghost_keys;
bool GhostInTheFirmware::is_active_;
bool GhostInTheFirmware::is_pressed_;
uint16_t GhostInTheFirmware::current_pos_;
uint32_t GhostInTheFirmware::start_time_;
uint16_t GhostInTheFirmware::press_timeout_;
uint16_t GhostInTheFirmware::delay_timeout_;
bool GhostInTheFirmware::is_active_ = false;
uint16_t GhostInTheFirmware::current_pos_ = 0;
uint16_t GhostInTheFirmware::start_time_;
void GhostInTheFirmware::activate(void) {
is_active_ = true;
}
EventHandlerResult GhostInTheFirmware::beforeReportingState() {
EventHandlerResult GhostInTheFirmware::afterEachCycle() {
if (!is_active_)
return EventHandlerResult::OK;
if (press_timeout_ == 0) {
press_timeout_ = pgm_read_word(&(ghost_keys[current_pos_].pressTime));
delay_timeout_ = pgm_read_word(&(ghost_keys[current_pos_].delay));
// This stores the current GhostKey in the active sequence.
static GhostKey ghost_key{KeyAddr::none(), 0, 0};
if (press_timeout_ == 0) {
// When a ghost key has finished playing, it sets its delay to zero,
// indicating that it's time to read the next one from memory.
if (ghost_key.delay == 0) {
// Read the settings for the key from PROGMEM:
loadFromProgmem(ghost_keys[current_pos_], ghost_key);
// The end of the sequence is marked by a GhostKey with an invalid KeyAddr
// value (i.e. KeyAddr::none()). If we read this sentinel value, reset and
// deactivate.
if (!ghost_key.addr.isValid()) {
current_pos_ = 0;
ghost_key.delay = 0;
is_active_ = false;
return EventHandlerResult::OK;
}
is_pressed_ = true;
// If we're not at the end of the sequence, send the first keypress event,
// and start the timer.
Runtime.handleKeyEvent(KeyEvent(ghost_key.addr, IS_PRESSED));
start_time_ = Runtime.millisAtCycleStart();
} else {
if (is_pressed_ && Runtime.hasTimeExpired(start_time_, press_timeout_)) {
is_pressed_ = false;
start_time_ = Runtime.millisAtCycleStart();
byte row = pgm_read_byte(&(ghost_keys[current_pos_].row));
byte col = pgm_read_byte(&(ghost_keys[current_pos_].col));
handleKeyswitchEvent(Key_NoKey, KeyAddr(row, col), WAS_PRESSED);
} else if (is_pressed_) {
byte row = pgm_read_byte(&(ghost_keys[current_pos_].row));
byte col = pgm_read_byte(&(ghost_keys[current_pos_].col));
handleKeyswitchEvent(Key_NoKey, KeyAddr(row, col), IS_PRESSED);
} else if (Runtime.hasTimeExpired(start_time_, delay_timeout_)) {
current_pos_++;
press_timeout_ = 0;
} else if (ghost_key.addr.isValid()) {
// If the ghost key's address is still valid, that means that the virtual
// key is still being held.
if (Runtime.hasTimeExpired(start_time_, ghost_key.press_time)) {
// The key press has timed out, so we send the release event.
Runtime.handleKeyEvent(KeyEvent(ghost_key.addr, WAS_PRESSED));
// Next, we invalidate the ghost key's address to prevent checking the
// hold timeout again, then restart the timer for checking the delay.
ghost_key.addr.clear();
start_time_ = Runtime.millisAtCycleStart();
}
} else if (Runtime.hasTimeExpired(start_time_, ghost_key.delay)) {
// The ghost key has been (virtually) pressed and released, and its delay
// has now elapsed, so we set the delay to zero and increment the index
// value to indicate that the next key should be loaded in the next cycle.
ghost_key.delay = 0;
++current_pos_;
}
return EventHandlerResult::OK;

@ -23,29 +23,23 @@ namespace kaleidoscope {
namespace plugin {
class GhostInTheFirmware : public kaleidoscope::Plugin {
public:
typedef struct {
byte row;
byte col;
uint16_t pressTime;
struct GhostKey {
KeyAddr addr;
uint16_t press_time;
uint16_t delay;
} GhostKey;
};
static const GhostKey *ghost_keys;
GhostInTheFirmware(void) {}
static void activate(void);
EventHandlerResult beforeReportingState();
EventHandlerResult afterEachCycle();
private:
static bool is_active_;
static bool is_pressed_;
static uint16_t current_pos_;
static uint32_t start_time_;
static uint16_t press_timeout_;
static uint16_t delay_timeout_;
static void loopHook(bool is_post_clear);
static uint16_t start_time_;
};
}
}

@ -27,10 +27,12 @@
#ifndef KALEIDOSCOPE_VIRTUAL_BUILD
#ifdef ARDUINO_AVR_ERGODOX
#include "kaleidoscope/Runtime.h"
#include <avr/wdt.h>
#include "kaleidoscope/device/ez/ErgoDox/ErgoDoxScanner.h"
#include "kaleidoscope/key_events.h"
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/KeyEvent.h"
#include "kaleidoscope/Runtime.h"
namespace kaleidoscope {
namespace device {
@ -112,10 +114,12 @@ void __attribute__((optimize(3))) ErgoDox::readMatrix() {
void __attribute__((optimize(3))) ErgoDox::actOnMatrixScan() {
for (byte row = 0; row < matrix_rows; row++) {
for (byte col = 0; col < matrix_columns; col++) {
uint8_t keyState = (bitRead(previousKeyState_[row], col) << 0) |
(bitRead(keyState_[row], col) << 1);
if (keyState)
handleKeyswitchEvent(Key_NoKey, KeyAddr(row, col), keyState);
uint8_t key_state = (bitRead(previousKeyState_[row], col) << 0) |
(bitRead(keyState_[row], col) << 1);
if (keyToggledOn(key_state) || keyToggledOff(key_state)) {
auto event = KeyEvent::next(KeyAddr(row, col), key_state);
kaleidoscope::Runtime.handleKeyswitchEvent(event);
}
}
previousKeyState_[row] = keyState_[row];
}

@ -133,36 +133,38 @@ void Heatmap::TransientLEDMode::resetMap() {
highest_ = 1;
}
EventHandlerResult Heatmap::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
// It may be better to use `onKeyswitchEvent()` here
EventHandlerResult Heatmap::onKeyEvent(KeyEvent &event) {
// If the keyboard has no LEDs, return
if (!Runtime.has_leds)
return EventHandlerResult::OK;
// this methode is called frequently by Kaleidoscope
// even if the module isn't activated
// If the event doesn't correspond to a physical key, skip it
if (!event.addr.isValid())
return EventHandlerResult::OK;
// if it is a synthetic key, skip it
if (key_state & INJECTED)
if (event.state & INJECTED)
return EventHandlerResult::OK;
// if the key is not toggled on, skip it
if (!keyToggledOn(key_state))
if (!keyToggledOn(event.state))
return EventHandlerResult::OK;
// if the LED mode is not current, skip it
if (::LEDControl.get_mode_index() != led_mode_id_)
return EventHandlerResult::OK;
return ::LEDControl.get_mode<TransientLEDMode>()
->onKeyswitchEvent(mapped_key, key_addr, key_state);
return ::LEDControl.get_mode<TransientLEDMode>()->onKeyEvent(event);
}
EventHandlerResult Heatmap::TransientLEDMode::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
EventHandlerResult Heatmap::TransientLEDMode::onKeyEvent(KeyEvent &event) {
// increment the heatmap_ value related to the key
heatmap_[key_addr.toInt()]++;
heatmap_[event.addr.toInt()]++;
// check highest_
if (highest_ < heatmap_[key_addr.toInt()]) {
highest_ = heatmap_[key_addr.toInt()];
if (highest_ < heatmap_[event.addr.toInt()]) {
highest_ = heatmap_[event.addr.toInt()];
// if highest_ (and so heatmap_ value related to the key)
// is close to overflow: call shiftStats

@ -33,7 +33,7 @@ class Heatmap : public Plugin,
static uint8_t heat_colors_length;
void resetMap(void);
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
EventHandlerResult beforeEachCycle();
// This class' instance has dynamic lifetime
@ -48,7 +48,7 @@ class Heatmap : public Plugin,
explicit TransientLEDMode(const Heatmap *parent);
void resetMap();
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
EventHandlerResult beforeEachCycle();
protected:

@ -49,8 +49,7 @@ EventHandlerResult IdleLEDs::beforeEachCycle() {
return EventHandlerResult::OK;
}
EventHandlerResult IdleLEDs::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
EventHandlerResult IdleLEDs::onKeyEvent(KeyEvent &event) {
if (idle_) {
::LEDControl.enable();
idle_ = false;

@ -33,7 +33,7 @@ class IdleLEDs: public kaleidoscope::Plugin {
static void setIdleTimeoutSeconds(uint32_t new_limit);
EventHandlerResult beforeEachCycle();
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
private:
static bool idle_;

@ -2,9 +2,8 @@
With this plugin, any active modifier on the keyboard will have the LED under it
highlighted. No matter how the modifier got activated (a key press, a macro,
anything else), the coloring will apply. Layer keys, be them layer toggles,
momentary switches, or one-shot layer keys count as modifiers as far as the
plugin is concerned.
anything else), the coloring will apply. Layer shift keys and OneShot layer keys
count as modifiers as far as the plugin is concerned.
## Using the plugin
@ -37,11 +36,20 @@ properties:
### `.highlight_color`
> The color to use for highlighting the modifiers. Defaults to a white color.
> The color to use for highlighting normal modifier keys and
> layer-shift keys. Defaults to a white color.
### `.oneshot_color`
> The color to use for highlighting active one-shot keys. These are
> the keys that will time out or deactivate when a subsequent key is
> pressed. Defaults to a yellow color.
### `.sticky_color`
> The color to use for highlighting one-shot modifiers when they are sticky. Defaults to a red color.
> The color to use for highlighting "sticky" one-shot keys. These keys
> will remain active until they are pressed again. Defaults to a red
> color.
## Plugin methods

@ -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
@ -17,79 +17,89 @@
#include <Kaleidoscope-LED-ActiveModColor.h>
#include <Kaleidoscope-OneShot.h>
#include "kaleidoscope/LiveKeys.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::onKeyEvent(KeyEvent &event) {
for (auto key_addr : KeyAddr::all()) {
Key k = Layer.lookupOnActiveLayer(key_addr);
// If `event.addr` is not a physical key address, ignore it:
if (! event.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(event.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 (event.key.isKeyboardModifier() ||
event.key.isLayerShift() ||
::OneShot.isOneShotKey(event.key) ||
::OneShot.isActive(event.addr)) {
mod_key_bits_.set(event.addr);
}
if (event.key == OneShot_ActiveStickyKey) {
for (KeyAddr entry_addr : KeyAddr::all()) {
// Get the entry from the live keys array
Key entry_key = live_keys[entry_addr];
// Skip empty entries
if (entry_key == Key_Transparent || entry_key == Key_NoKey) {
continue;
}
// Highlight everything else
mod_key_bits_.set(entry_addr);
}
}
} else { // if (keyToggledOff(event.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(event.addr) && !::OneShot.isActive(event.addr)) {
mod_key_bits_.clear(event.addr);
::LEDControl.refreshAt(event.addr);
}
}
return EventHandlerResult::OK;
}
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];
Key k = Layer.lookupOnActiveLayer(key_addr);
if (::OneShot.isOneShotKey(k)) {
if (::OneShot.isSticky(k))
::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))
::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);
// -----------------------------------------------------------------------------
EventHandlerResult ActiveModColorEffect::beforeSyncingLeds() {
// 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_) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
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 (highlight_normal_modifiers_) {
// Normal modifiers get a third color:
::LEDControl.setCrgbAt(key_addr, highlight_color);
}
#pragma GCC diagnostic pop
}
return EventHandlerResult::OK;
}
}
}
} // namespace plugin
} // namespace kaleidoscope
kaleidoscope::plugin::ActiveModColorEffect ActiveModColorEffect;

@ -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,33 +18,64 @@
#pragma once
#include "kaleidoscope/Runtime.h"
#include "kaleidoscope/KeyAddrBitfield.h"
#include <Kaleidoscope-LEDControl.h>
#define MAX_MODS_PER_LAYER 16
// =============================================================================
#define _DEPRECATED_MESSAGE_ACTIVEMODCOLOR_COLORS \
"The `ActiveModColorEffect` public class variables have been deprecated. \n" \
"Please use the following methods instead: \n" \
" - for `highlight_color` => `setHighlightColor(color)` \n" \
" - for `oneshot_color` => `setOneShotColor(color)` \n" \
" - for `sticky_color` => `setStickyColor(color)`"
namespace kaleidoscope {
namespace plugin {
class ActiveModColorEffect : public kaleidoscope::Plugin {
public:
ActiveModColorEffect(void) {}
// When removing access to these variables, don't delete them. Instead, make
// them private, and add trailing underscores here and in
// LED-ActiveModColor.cpp.
DEPRECATED(ACTIVEMODCOLOR_COLORS)
static cRGB highlight_color;
DEPRECATED(ACTIVEMODCOLOR_COLORS)
static cRGB oneshot_color;
DEPRECATED(ACTIVEMODCOLOR_COLORS)
static cRGB sticky_color;
static void setHighlightColor(cRGB color) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
highlight_color = color;
#pragma GCC diagnostic pop
}
static void setOneShotColor(cRGB color) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
oneshot_color = color;
#pragma GCC diagnostic pop
}
static void setOnestickyColor(cRGB color) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
sticky_color = color;
#pragma GCC diagnostic pop
}
static void highlightNormalModifiers(bool value) {
highlight_normal_modifiers_ = value;
}
EventHandlerResult beforeReportingState();
EventHandlerResult onLayerChange();
EventHandlerResult onSetup() {
return onLayerChange();
}
EventHandlerResult onKeyEvent(KeyEvent &event);
EventHandlerResult beforeSyncingLeds();
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_;
};
}
}

@ -61,40 +61,40 @@ void AlphaSquareEffect::TransientLEDMode::refreshAt(KeyAddr key_addr) {
::LEDControl.setCrgbAt(key_addr, CRGB(0, 0, 0));
}
EventHandlerResult AlphaSquareEffect::onKeyswitchEvent(Key &mappedKey, KeyAddr key_addr, uint8_t keyState) {
EventHandlerResult AlphaSquareEffect::onKeyEvent(KeyEvent &event) {
if (!Runtime.has_leds)
return EventHandlerResult::OK;
if (::LEDControl.get_mode_index() != led_mode_id_)
return EventHandlerResult::OK;
if (keyState & INJECTED)
if (event.state & INJECTED)
return EventHandlerResult::OK;
if (mappedKey < Key_A || mappedKey > Key_0)
if (event.key < Key_A || event.key > Key_0)
return EventHandlerResult::OK;
if (!keyIsPressed(keyState))
return EventHandlerResult::OK;
// if (!keyIsPressed(event.state))
// return EventHandlerResult::OK;
uint8_t display_col = 2;
auto this_led_mode = ::LEDControl.get_mode<TransientLEDMode>();
Key prev_key = this_led_mode->last_key_left_;
if (key_addr.col() < Runtime.device().matrix_columns / 2) {
this_led_mode->last_key_left_ = mappedKey;
if (event.addr.col() < Runtime.device().matrix_columns / 2) {
this_led_mode->last_key_left_ = event.key;
this_led_mode->start_time_left_ = Runtime.millisAtCycleStart();
} else {
prev_key = this_led_mode->last_key_right_;
this_led_mode->last_key_right_ = mappedKey;
this_led_mode->last_key_right_ = event.key;
this_led_mode->start_time_right_ = Runtime.millisAtCycleStart();
display_col = 10;
}
if (prev_key != mappedKey)
if (prev_key != event.key)
::AlphaSquare.clear(prev_key, display_col);
::AlphaSquare.display(mappedKey, display_col);
::AlphaSquare.display(event.key, display_col);
return EventHandlerResult::OK;
}

@ -30,7 +30,7 @@ class AlphaSquareEffect : public Plugin,
static uint16_t length;
EventHandlerResult onKeyswitchEvent(Key &mappedKey, KeyAddr key_addr, uint8_t keyState);
EventHandlerResult onKeyEvent(KeyEvent &event);
// This class' instance has dynamic lifetime
//

@ -34,19 +34,20 @@ StalkerEffect::TransientLEDMode::TransientLEDMode(const StalkerEffect *parent)
map_{}
{}
EventHandlerResult StalkerEffect::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState) {
EventHandlerResult StalkerEffect::onKeyEvent(KeyEvent &event) {
if (!Runtime.has_leds)
return EventHandlerResult::OK;
if (!key_addr.isValid())
if (!event.addr.isValid())
return EventHandlerResult::OK;
if (::LEDControl.get_mode_index() != led_mode_id_)
return EventHandlerResult::OK;
if (keyIsPressed(keyState)) {
::LEDControl.get_mode<TransientLEDMode>()->map_[key_addr.toInt()] = 0xff;
}
// The simplest thing to do is trigger on both press and release. The color
// will fade while the key is held, and get restored to full brightness when
// it's released.
::LEDControl.get_mode<TransientLEDMode>()->map_[event.addr.toInt()] = 0xff;
return EventHandlerResult::OK;
}

@ -39,7 +39,7 @@ class StalkerEffect : public Plugin,
static uint16_t step_length;
static cRGB inactive_color;
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState);
EventHandlerResult onKeyEvent(KeyEvent &event);
// This class' instance has dynamic lifetime
//

@ -45,20 +45,21 @@ WavepoolEffect::TransientLEDMode::TransientLEDMode(const WavepoolEffect *parent)
page_(0)
{}
EventHandlerResult WavepoolEffect::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
if (!key_addr.isValid())
EventHandlerResult WavepoolEffect::onKeyEvent(KeyEvent &event) {
if (!event.addr.isValid())
return EventHandlerResult::OK;
if (::LEDControl.get_mode_index() != led_mode_id_)
return EventHandlerResult::OK;
return ::LEDControl.get_mode<TransientLEDMode>()
->onKeyswitchEvent(mapped_key, key_addr, key_state);
return ::LEDControl.get_mode<TransientLEDMode>()->onKeyEvent(event);
}
EventHandlerResult WavepoolEffect::TransientLEDMode::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
if (keyIsPressed(key_state)) {
surface_[page_][pgm_read_byte(rc2pos + key_addr.toInt())] = 0x7f;
EventHandlerResult WavepoolEffect::TransientLEDMode::onKeyEvent(KeyEvent &event) {
// It might be better to trigger on both toggle-on and toggle-off, but maybe
// just the former.
if (keyIsPressed(event.state)) {
surface_[page_][pgm_read_byte(rc2pos + event.addr.toInt())] = 0x7f;
frames_since_event_ = 0;
}

@ -34,7 +34,7 @@ class WavepoolEffect : public Plugin,
public:
WavepoolEffect(void) {}
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
// ms before idle animation starts after last keypress
static uint16_t idle_timeout;
@ -53,7 +53,7 @@ class WavepoolEffect : public Plugin,
//
explicit TransientLEDMode(const WavepoolEffect *parent);
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
protected:

@ -29,7 +29,7 @@ TriColor::TriColor(cRGB base_color, cRGB mod_color, cRGB esc_color) {
void TriColor::TransientLEDMode::update(void) {
for (auto key_addr : KeyAddr::all()) {
Key k = Layer.lookup(key_addr);
Key k = Layer.lookupOnActiveLayer(key_addr);
// Special keys are always mod_color
if (k.getFlags() != 0) {

@ -58,9 +58,6 @@ The dictionary is made up of a list of keys, and an action callback. Using the
`LEADER_DICT` and `LEADER_SEQ` helpers is recommended. The dictionary *must* be
marked `PROGMEM`!
**Note** that we need to use the `Leader` object before any other that adds or
changes key behaviour! Failing to do so may result in unpredictable behaviour.
## Plugin methods
The plugin provides the `Leader` object, with the following methods and properties:

@ -20,12 +20,14 @@
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/key_events.h"
#include "kaleidoscope/KeyEventTracker.h"
namespace kaleidoscope {
namespace plugin {
// --- state ---
Key Leader::sequence_[LEADER_MAX_SEQUENCE_LENGTH + 1];
KeyEventTracker Leader::event_tracker_;
uint8_t Leader::sequence_pos_;
uint16_t Leader::start_time_ = 0;
uint16_t Leader::time_out = 1000;
@ -81,8 +83,9 @@ void Leader::reset(void) {
sequence_[0] = Key_NoKey;
}
// DEPRECATED
void Leader::inject(Key key, uint8_t key_state) {
onKeyswitchEvent(key, UnknownKeyswitchLocation, key_state);
Runtime.handleKeyEvent(KeyEvent(KeyAddr::none(), key_state | INJECTED, key));
}
// --- hooks ---
@ -90,61 +93,49 @@ EventHandlerResult Leader::onNameQuery() {
return ::Focus.sendName(F("Leader"));
}
EventHandlerResult Leader::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState) {
if (keyState & INJECTED)
EventHandlerResult Leader::onKeyswitchEvent(KeyEvent &event) {
// If the plugin has already processed and released this event, ignore it.
// There's no need to update the event tracker explicitly.
if (event_tracker_.shouldIgnore(event))
return EventHandlerResult::OK;
if (!isActive() && !isLeader(mapped_key))
if (keyToggledOff(event.state) || event.state & INJECTED)
return EventHandlerResult::OK;
if (!isActive()) {
// Must be a leader key!
if (keyToggledOff(keyState)) {
// not active, but a leader key = start the sequence on key release!
start_time_ = Runtime.millisAtCycleStart();
sequence_pos_ = 0;
sequence_[sequence_pos_] = mapped_key;
}
// If the sequence was not active yet, ignore the key.
return EventHandlerResult::EVENT_CONSUMED;
}
// active
int8_t action_index = lookup();
if (keyToggledOn(keyState)) {
sequence_pos_++;
if (sequence_pos_ > LEADER_MAX_SEQUENCE_LENGTH) {
reset();
if (!isLeader(event.key))
return EventHandlerResult::OK;
}
start_time_ = Runtime.millisAtCycleStart();
sequence_[sequence_pos_] = mapped_key;
action_index = lookup();
sequence_pos_ = 0;
sequence_[sequence_pos_] = event.key;
if (action_index >= 0) {
return EventHandlerResult::EVENT_CONSUMED;
}
} else if (keyIsPressed(keyState)) {
// held, no need for anything here.
return EventHandlerResult::EVENT_CONSUMED;
return EventHandlerResult::ABORT;
}
++sequence_pos_;
if (sequence_pos_ > LEADER_MAX_SEQUENCE_LENGTH) {
reset();
return EventHandlerResult::OK;
}
start_time_ = Runtime.millisAtCycleStart();
sequence_[sequence_pos_] = event.key;
int8_t action_index = lookup();
if (action_index == NO_MATCH) {
reset();
return EventHandlerResult::OK;
}
if (action_index == PARTIAL_MATCH) {
return EventHandlerResult::EVENT_CONSUMED;
return EventHandlerResult::ABORT;
}
action_t leaderAction = (action_t) pgm_read_ptr((void const **) & (dictionary[action_index].action));
(*leaderAction)(action_index);
reset();
return EventHandlerResult::EVENT_CONSUMED;
return EventHandlerResult::ABORT;
}
EventHandlerResult Leader::afterEachCycle() {

@ -17,8 +17,9 @@
#pragma once
#include "kaleidoscope/Runtime.h"
#include <Kaleidoscope-Ranges.h>
#include "kaleidoscope/KeyEventTracker.h"
#include "kaleidoscope/plugin.h"
#define LEADER_MAX_SEQUENCE_LENGTH 4
@ -47,11 +48,12 @@ class Leader : public kaleidoscope::Plugin {
void inject(Key key, uint8_t key_state);
EventHandlerResult onNameQuery();
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState);
EventHandlerResult onKeyswitchEvent(KeyEvent &event);
EventHandlerResult afterEachCycle();
private:
static Key sequence_[LEADER_MAX_SEQUENCE_LENGTH + 1];
static KeyEventTracker event_tracker_;
static uint8_t sequence_pos_;
static uint16_t start_time_;

@ -34,21 +34,24 @@ enum {
M(MACRO_MODEL01), M(MACRO_HELLO), M(MACRO_SPECIAL)
// later in the Sketch:
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) {
switch (macroIndex) {
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
switch (macro_id) {
case MACRO_MODEL01:
return MACRODOWN(I(25),
D(LeftShift), T(M), U(LeftShift), T(O), T(D), T(E), T(L),
T(Spacebar),
W(100),
T(0), T(1) );
if (keyToggledOn(event.state)) {
return MACRO(I(25),
D(LeftShift), T(M), U(LeftShift), T(O), T(D), T(E), T(L),
T(Spacebar),
W(100),
T(0), T(1) );
}
break;
case MACRO_HELLO:
if (keyToggledOn(keyState)) {
if (keyToggledOn(event.state)) {
return Macros.type(PSTR("Hello "), PSTR("world!"));
}
break;
case MACRO_SPECIAL:
if (keyToggledOn(keyState)) {
if (keyToggledOn(event.state)) {
// Do something special
}
break;
@ -101,22 +104,34 @@ The plugin provides a `Macros` object, with the following methods and properties
> easiest way to do that is to wrap the string in a `PSTR()` helper. See the
> program code at the beginning of this documentation for an example!
### `.row`, `.col`
### `.press(key)`/`.release(key)`
> The `row` and `col` properties describe the physical position a macro was
> triggered from if it was triggered by a key. The playback functions
> do not use these properties, but they are available, would one want to create
> a macro that needs to know which key triggered it.
> Used in `Macros.play()`, these methods press virtual keys in a small
> supplemental `Key` array for the purpose of keeping keys active for complex
> macro sequences where it's important to have overlapping key presses.
>
> When the macro was not triggered by a key the value of these properties are
> unspecified.
> `Macros.press(key)` sends a key press event, and will keep that virtual key
> active until either `Macros.release(key)` is called, or a Macros key is
> released. If you use `Macros.press(key)` in a macro, but also change the value
> of `event.key`, you will need to make sure to also call `Macros.release(key)`
> at some point to prevent that key from getting "stuck" on.
### `.clear()`
> Releases all virtual keys held by macros. This both empties the supplemental
> `Key` array (see above) and sends a release event for each key stored there.
### `.tap(key)`
> Sends an immediate press and release event for `key` with no delay, using an
> invalid key address.
## Macro helpers
Macros need to be able to simulate key down and key up events for any key - even
keys that may not be on the keymap otherwise. For this reason and others, we
need to define them in a special way, using the `MACRO` helper (or its
`MACRODOWN()` variant, see below):
need to define them in a special way, using the `MACRO` helper.
### `MACRO(steps...)`
@ -128,20 +143,6 @@ need to define them in a special way, using the `MACRO` helper (or its
> that end with END will still work correctly, but new code should not use END;
> usage of END is deprecated.
### `MACRODOWN(steps...)`
> The same as the `MACRO()` helper above, but it will create a special sequence,
> where the steps are only played back when the triggering key was just pressed.
> That is, the macro will not be performed when the key is released, or held, or
> not pressed at all.
>
> Use this over `MACRO()` when you only want to perform an action when the key
> actuates, and no action should be taken when it is held, released, or when it
> is not pressed at all. For a lot of macros that emit a sequence without any
> other side effects, `MACRODOWN()` is usually the better choice.
>
> Can only be used from the `macroAction()` overrideable method.
## `MACRO` steps
Macro steps can be divided into the following groups:
@ -164,7 +165,11 @@ In most cases, one is likely use normal keys for the steps, so the `D`, `U`, and
`T` steps apply the `Key_` prefix. This allows us to write `MACRO(T(X))` instead
of `MACRO(Tr(Key_X))` - making the macro definition shorter, and more readable.
The compact variant (`Dc`, `Uc`, and `Tc`) prefix the argument with `Key_` too,
The "raw" variants (`Dr`/`Ur`/`Tr`) use the full name of the `Key` object,
without adding the `Key_` prefix to the argument given. `Tr(Key_X)` is the same
as `T(X)`.
The "compact" variants (`Dc`/`Uc`/`Tc`) prefix the argument with `Key_` too,
but unlike `D`, `U`, and `T`, they ignore the `flags` component of the key, and
as such, are limited to ordinary keys. Mouse keys, consumer- or system keys are
not supported by this compact representation.
@ -188,26 +193,9 @@ them in order.
with `Key_`, and they ignore the `flags` component of a key, and as such, are
limited to ordinary keys.
### Controlling when to send reports
While the plugin will - by default - send a report after every step, that is not
always desirable. For this reason, we allow turning this implicit reporting off,
and switching to explicit reporting instead. Note that the tap steps (`T()`,
`Tr()`, and `Tc()`) will always send an implicit report, and so will
`Macros.type()`.
To control when to send reports, the following steps can be used:
* `WITH_EXPLICIT_REPORT`: Prevents the plugin from sending an implicit report
after every step. To send a report, one needs to have a `SEND_REPORT` step
too.
* `WITH_IMPLICIT_REPORT`: Enables sending an implicit report after every step
(the default).
* `SEND_REPORT`: Send a report.
## Overrideable methods
## Overrideable functions
### `macroAction(macroIndex, keyState)`
### `macroAction(uint8_t macro_id, KeyEvent &event)`
> The `macroAction` method is the brain of the macro support in Kaleidoscope:
> this function tells the plugin what sequence to play when given a macro index

@ -0,0 +1,215 @@
# Upgrading Macros code
This is a guide to upgrading existing Macros code to use the new version of
Kaleidoscope and the Macros plugin.
## New `macroAction()` function
There is a new version of the `macroAction()` function, which is the entry point
for user-defined Macros code. The old version takes two integer parameters, with
the following call signature:
```c++
const macro_t* macroAction(uint8_t macro_id, uint8_t key_state)
```
If your sketch has this function, with a `key_state` bitfield parameter, it
might still work as expected, but depending on the specifics of the code that
gets called from it, your macros might not work as expected. Either way, you
should update that function to the new version, which takes a `KeyEvent`
reference as its second parameter:
```c++
const macro_t* macroAction(uint8_t macro_id, KeyEvent &event)
```
For simple macros, it is a simple matter of replacing `key_state` in the body of
the `macroAction()` code with `event.state`. This covers most cases where all
that's done is a call to `Macros.type()`, or a `MACRO()` or `MACRODOWN()`
sequence is returned.
## Code that calls `handleKeyswitchEvent()` or `pressKey()`
It is very likely that if you have custom code that calls
`handleKeyswitchEvent()` or `pressKey()` directly, it will no longer function
properly after upgrading. To adapt this code to the new `KeyEvent` system
requires a deeper understanding of the changes to Kaleidoscope, but likely
results in much simpler Macros code.
The first thing that is important to understand is that the `macroAction()`
function will now only be called when a Macros `Key` toggles on or off, not once
per cycle while the key is held. This is because the new event handling code in
Kaleidoscope only calls plugin handlers in those cases, dealing with one event
at a time, in a single pass through the plugin event handlers (rather than one
pass per active key)--and only sends a keyboard HID report in response to those
events, not once per scan cycle.
This means that any Macros code that is meant to keep keycodes in the keyboard
HID report while the Macros key is held needs to be changed. For example, if a
macro contained the following code:
```c++
if (keyIsPressed(key_state)) {
Runtime.hid().keyboard().pressKey(Key_LeftShift);
}
```
...that wouldn't work quite as expected, because as soon as the next key is
pressed, a new report would be generated without ever calling `macroAction()`,
and therefore that change to the HID report would not take place, effectively
turning off the `shift` modifier immediately before sending the report with the
keycode that it was intended to modify.
Furthermore, that `shift` modifier would never even get sent in the first place,
because the HID report no longer gets cleared at the beginning of every
cycle. Now it doesn't get cleared until _after_ the plugin event handlers get
called (in the case of Macros, that's `onKeyEvent()`, which calls the
user-defined `macroAction()` function), so any changes made to the HID report
from that function will be discarded before it's sent.
Instead of the above, there are two new mechanisms for keeping keys active while
a Macros key is pressed:
### Alter the `event.key` value
If your macro only needs to keep a single `Key` value active after running some
code, and doesn't need to run any custom code when the key is released, the
simplest thing to do is to override the event's `Key` value:
```c++
if (keyToggledOn(event.state)) {
// do some macro action(s)
event.key = Key_LeftShift;
}
```
This will (temporarily) replace the Macros key with the value assigned (in this
case, `Key_LeftShift`), starting immediately after the `macroAction()` function
returns, and lasting until the key is released. This key value can include
modifier flags, or it can be a layer-shift, or any other valid `Key` value
(though it won't get processed by plugins that are initialized before Macros in
`KALEIDOSCOPE_INIT_PLUGINS()`, and Macros itself won't act on the value, if it
gets replaced by a different Macros key).
### Use the supplemental Macros `Key` array
The Macros plugin now contains a small array of `Key` values that will be
included when building HID reports triggered by subsequent, non-Macros
events. To use it, just call one (or more) of the following methods:
```c++
Macros.press(key);
Macros.release(key);
Macros.tap(key)
```
Each one of these functions generates a new artificial key event, and processes
it (including sending a HID report, if necessary). For `press()` and
`release()`, it also stores the specified key's value in the Macros supplemental
`Key` array. In the case of the `tap()` function, it generates matching press
and release events, but skips storing them, assuming that no plugin will
generate an intervening event. All of the events generated by these functions
will be marked `INJECTED`, which will cause Macros itself (and many other
plugins) to ignore them.
This will allow you to keep multiple `Key` values active while a Macros key is
held, while leaving the Macros key itself active, enabling more custom code to
be called on its release. Note that whenever a Macros key is released, the
supplemental key array is cleared to minimize the chances of keycodes getting
"stuck". It is still possible to write a macro that will cause values to persist
in this array, however, by combining both a sequence that uses key presses
without matched releases _and_ replacing `event.key` (see above) in the same
macro.
### Borrow an idle key (not recommended)
It's also possible to "borrow" one (or more) idle keys on the keyboard by
searching the `live_keys[]` array for an empty entry, and generating a new event
with the address of that key. This is not recommended because surprising things
can happen if that key is then pressed and released, but it's still an option
for people who like to live dangerously.
## Code that calls `sendReport()`
Calling `sendReport()` directly from a macro is now almost always unnecessary.
Instead, a call to `Runtime.handleKeyEvent()` will result in a keyboard HID
report being sent in response to the generated event without needing to make it
explicit.
## Code that uses `Macros.key_addr`
This variable is deprecated. Instead, using the new `macroAction(id, event)`
function, the address of the Macros key is available via the `event.addr`
variable.
## Working with other plugins
### Plugin-specific `Key` values
When the the Macros plugin generates events, it marks the event state as
`INJECTED` in order to prevent unbounded recursion (Macros ignores injected
events). This causes most other plugins to ignore the event, as well.
Therefore, including a plugin-specific key (e.g. a OneShot modifier such as
`OSM(LeftAlt)`) will most likely be ignored by the target plugin, and will
therefore not have the desired effect. This applies to any calls to
`Macros.play()` (including returning `MACRO()` from `macroAction()`),
`Macros.tap()`, `Macros.press()`, and `Macros.release()`.
### Physical event plugins
Macros cannot usefully produce events handled by plugins that implement the
`onKeyswitchEvent()` handler, such as Qukeys, TapDance, and Leader. To make
those plugins work with Macros, it's necessary to have the other plugin produce
a Macros key, not the other way around. A `macroAction()` function must not call
`Runtime.handleKeyswitchEvent()`.
### OneShot
This is one plugin that you might specifically want to use with a macro,
generally at the end of a sequence. For example, a macro for ending one
sentence and beginning the next one might print a period followed by a space
(`. `), then a OneShot shift key tap, so that the next character will be
automatically capitalized. The problem, as mentioned before is that the
following won't work:
```c++
MACRO(Tc(Period), Tc(Spacebar), Tr(OSM(LeftShift)))
```
...because OneShot will ignore the `INJECTED` event. One solution is to change
the value of `event.key`, turning the pressed Macros key into a OneShot
modifier. This will only work if Macros is registered before OneShot in
`KALEIDOSCOPE_INIT_PLUGINS()`:
```c++
const macro_t* macroNewSentence(KeyEvent &event) {
if (keyToggledOn(event.state)) {
event.key = OSM(LeftShift);
return MACRO(Tc(Period), Tc(Spacebar));
}
return MACRO_NONE;
}
```
A more robust solution is to explicitly call `Runtime.handleKeyEvent()`, but
this is more complex, because you'll need to prevent the Macros key from
clobbering the OneShot key in the `live_keys[]` array:
```c++
void macroNewSentence(KeyEvent &event) {
if (keyToggledOn(event.state)) {
Macros.tap(Key_Period);
Macros.tap(Key_Spacebar);
event.key = OSM(LeftShift);
kaleidoscope::Runtime.handleKeyEvent(event);
// Last, we invalidate the current event's key address to prevent the Macros
// key value from clobbering the OneShot shift.
event.key = Key_NoKey;
event.addr.clear();
}
}
```

@ -19,65 +19,112 @@
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/key_events.h"
// =============================================================================
// Default `macroAction()` function definitions
__attribute__((weak))
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState) {
const macro_t *macroAction(uint8_t macro_id, KeyEvent &event) {
return MACRO_NONE;
}
namespace kaleidoscope {
namespace plugin {
MacroKeyEvent Macros_::active_macros[];
byte Macros_::active_macro_count;
KeyAddr Macros_::key_addr;
#ifndef NDEPRECATED
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
__attribute__((weak))
const macro_t *macroAction(uint8_t macro_id, uint8_t key_state) {
return MACRO_NONE;
}
void playMacroKeyswitchEvent(Key key, uint8_t keyswitch_state, bool explicit_report) {
handleKeyswitchEvent(key, UnknownKeyswitchLocation, keyswitch_state | INJECTED);
const macro_t* deprecatedMacroDown(uint8_t key_state, const macro_t* macro_p) {
if (keyToggledOn(key_state))
return macro_p;
return MACRO_NONE;
}
#pragma GCC diagnostic pop
#endif
if (explicit_report)
return;
// =============================================================================
// `Macros` plugin code
namespace kaleidoscope {
namespace plugin {
kaleidoscope::Runtime.hid().keyboard().sendReport();
kaleidoscope::Runtime.hid().mouse().sendReport();
constexpr uint8_t press_state = IS_PRESSED | INJECTED;
constexpr uint8_t release_state = WAS_PRESSED | INJECTED;
// Initialized to zeroes (i.e. `Key_NoKey`)
Key Macros::active_macro_keys_[];
#ifndef NDEPRECATED
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
MacroKeyEvent Macros::active_macros[];
byte Macros::active_macro_count;
KeyAddr Macros::key_addr = KeyAddr::none();
#pragma GCC diagnostic pop
#endif
// -----------------------------------------------------------------------------
// Public helper functions
void Macros::press(Key key) {
Runtime.handleKeyEvent(KeyEvent{KeyAddr::none(), press_state, key});
// This key may remain active for several subsequent events, so we need to
// store it in the active macro keys array.
for (Key &macro_key : active_macro_keys_) {
if (macro_key == Key_NoKey) {
macro_key = key;
break;
}
}
}
static void playKeyCode(Key key, uint8_t keyStates, bool explicit_report) {
if (keyIsPressed(keyStates)) {
playMacroKeyswitchEvent(key, IS_PRESSED, explicit_report);
}
if (keyWasPressed(keyStates)) {
playMacroKeyswitchEvent(key, WAS_PRESSED, explicit_report);
void Macros::release(Key key) {
// Before sending the release event, we need to remove the key from the active
// macro keys array, or it will get inserted into the report anyway.
for (Key &macro_key : active_macro_keys_) {
if (macro_key == key) {
macro_key = Key_NoKey;
}
}
Runtime.handleKeyEvent(KeyEvent{KeyAddr::none(), release_state, key});
}
static void readKeyCodeAndPlay(const macro_t *macro_p, uint8_t flags, uint8_t keyStates, bool explicit_report) {
Key key(pgm_read_byte(macro_p++), // key_code
flags);
void Macros::clear() {
// Clear the active macro keys array.
for (Key &macro_key : active_macro_keys_) {
if (macro_key == Key_NoKey)
continue;
Runtime.handleKeyEvent(KeyEvent{KeyAddr::none(), release_state, macro_key});
macro_key = Key_NoKey;
}
}
playKeyCode(key, keyStates, explicit_report);
void Macros::tap(Key key) const {
// No need to call `press()` & `release()`, because we're immediately
// releasing the key after pressing it. It is possible for some other plugin
// to insert an event in between, but very unlikely.
Runtime.handleKeyEvent(KeyEvent{KeyAddr::none(), press_state, key});
Runtime.handleKeyEvent(KeyEvent{KeyAddr::none(), release_state, key});
}
void Macros_::play(const macro_t *macro_p) {
void Macros::play(const macro_t *macro_p) {
macro_t macro = MACRO_ACTION_END;
uint8_t interval = 0;
uint8_t flags;
bool explicit_report = false;
Key key;
if (!macro_p)
if (macro_p == MACRO_NONE)
return;
while (true) {
switch (macro = pgm_read_byte(macro_p++)) {
// These are unlikely to be useful now that we have KeyEvent. I think the
// whole `explicit_report` came about as a result of scan-order bugs.
case MACRO_ACTION_STEP_EXPLICIT_REPORT:
explicit_report = true;
break;
case MACRO_ACTION_STEP_IMPLICIT_REPORT:
explicit_report = false;
break;
case MACRO_ACTION_STEP_SEND_REPORT:
kaleidoscope::Runtime.hid().keyboard().sendReport();
kaleidoscope::Runtime.hid().mouse().sendReport();
break;
// End legacy macro step commands
// Timing
case MACRO_ACTION_STEP_INTERVAL:
interval = pgm_read_byte(macro_p++);
break;
@ -86,46 +133,59 @@ void Macros_::play(const macro_t *macro_p) {
delay(wait);
break;
}
case MACRO_ACTION_STEP_KEYDOWN:
flags = pgm_read_byte(macro_p++);
readKeyCodeAndPlay(macro_p++, flags, IS_PRESSED, explicit_report);
key.setFlags(pgm_read_byte(macro_p++));
key.setKeyCode(pgm_read_byte(macro_p++));
press(key);
break;
case MACRO_ACTION_STEP_KEYUP:
flags = pgm_read_byte(macro_p++);
readKeyCodeAndPlay(macro_p++, flags, WAS_PRESSED, explicit_report);
key.setFlags(pgm_read_byte(macro_p++));
key.setKeyCode(pgm_read_byte(macro_p++));
release(key);
break;
case MACRO_ACTION_STEP_TAP:
flags = pgm_read_byte(macro_p++);
readKeyCodeAndPlay(macro_p++, flags, IS_PRESSED | WAS_PRESSED, false);
key.setFlags(pgm_read_byte(macro_p++));
key.setKeyCode(pgm_read_byte(macro_p++));
tap(key);
break;
case MACRO_ACTION_STEP_KEYCODEDOWN:
readKeyCodeAndPlay(macro_p++, 0, IS_PRESSED, explicit_report);
key.setFlags(0);
key.setKeyCode(pgm_read_byte(macro_p++));
press(key);
break;
case MACRO_ACTION_STEP_KEYCODEUP:
readKeyCodeAndPlay(macro_p++, 0, WAS_PRESSED, explicit_report);
key.setFlags(0);
key.setKeyCode(pgm_read_byte(macro_p++));
release(key);
break;
case MACRO_ACTION_STEP_TAPCODE:
readKeyCodeAndPlay(macro_p++, 0, IS_PRESSED | WAS_PRESSED, false);
key.setFlags(0);
key.setKeyCode(pgm_read_byte(macro_p++));
tap(key);
break;
case MACRO_ACTION_STEP_TAP_SEQUENCE: {
uint8_t keyCode;
do {
flags = pgm_read_byte(macro_p++);
keyCode = pgm_read_byte(macro_p++);
playKeyCode(Key(keyCode, flags), IS_PRESSED | WAS_PRESSED, false);
while (true) {
key.setFlags(0);
key.setKeyCode(pgm_read_byte(macro_p++));
if (key == Key_NoKey)
break;
tap(key);
delay(interval);
} while (!(flags == 0 && keyCode == 0));
}
break;
}
case MACRO_ACTION_STEP_TAP_CODE_SEQUENCE: {
uint8_t keyCode;
do {
keyCode = pgm_read_byte(macro_p++);
playKeyCode(Key(keyCode, 0), IS_PRESSED | WAS_PRESSED, false);
while (true) {
key.setFlags(0);
key.setKeyCode(pgm_read_byte(macro_p++));
if (key.getKeyCode() == 0)
break;
tap(key);
delay(interval);
} while (keyCode != 0);
}
break;
}
@ -138,6 +198,26 @@ void Macros_::play(const macro_t *macro_p) {
}
}
const macro_t *Macros::type(const char *string) const {
while (true) {
uint8_t ascii_code = pgm_read_byte(string++);
if (ascii_code == 0)
break;
Key key = lookupAsciiCode(ascii_code);
if (key == Key_NoKey)
continue;
tap(key);
}
return MACRO_NONE;
}
// -----------------------------------------------------------------------------
// Translation from ASCII to keycodes
static const Key ascii_to_key_map[] PROGMEM = {
// 0x21 - 0x30
LSHIFT(Key_1),
@ -181,8 +261,7 @@ static const Key ascii_to_key_map[] PROGMEM = {
LSHIFT(Key_Backtick),
};
Key Macros_::lookupAsciiCode(uint8_t ascii_code) {
Key Macros::lookupAsciiCode(uint8_t ascii_code) const {
Key key = Key_NoKey;
switch (ascii_code) {
@ -224,67 +303,89 @@ Key Macros_::lookupAsciiCode(uint8_t ascii_code) {
return key;
}
const macro_t *Macros_::type(const char *string) {
while (true) {
uint8_t ascii_code = pgm_read_byte(string++);
if (!ascii_code)
break;
// -----------------------------------------------------------------------------
// Event handlers
Key key = lookupAsciiCode(ascii_code);
if (key == Key_NoKey)
continue;
playMacroKeyswitchEvent(key, IS_PRESSED, false);
playMacroKeyswitchEvent(key, WAS_PRESSED, false);
EventHandlerResult Macros::onKeyEvent(KeyEvent &event) {
// Ignore everything except Macros keys
if (!isMacrosKey(event.key))
return EventHandlerResult::OK;
#ifndef NDEPRECATED
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
// Set `Macros.key_addr` so that code in the `macroAction()` function can have
// access to it. This is not such a good solution, but it's done this way for
// backwards compatability. At some point, we should introduce a new
// `macroAction(KeyEvent)` function.
if (event.addr.isValid()) {
key_addr = event.addr;
} else {
key_addr = KeyAddr::none();
}
return MACRO_NONE;
}
bool Macros_::isMacroKey(Key key) {
if (key >= ranges::MACRO_FIRST && key <= ranges::MACRO_LAST)
return true;
return false;
}
EventHandlerResult Macros_::onNameQuery() {
return ::Focus.sendName(F("Macros"));
}
EventHandlerResult Macros_::onKeyswitchEvent(Key &mappedKey, KeyAddr key_addr, uint8_t keyState) {
if (! isMacroKey(mappedKey))
#pragma GCC diagnostic pop
#endif
// Decode the macro ID from the Macros `Key` value.
uint8_t macro_id = event.key.getRaw() - ranges::MACRO_FIRST;
// Call the new `macroAction(event)` function.
const macro_t* macro_ptr = macroAction(macro_id, event);
#ifndef NDEPRECATED
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
// If the new `macroAction()` returned nothing, try the legacy version.
if (macro_ptr == MACRO_NONE)
macro_ptr = macroAction(macro_id, event.state);
#pragma GCC diagnostic pop
#endif
// Play back the macro pointed to by `macroAction()`
play(macro_ptr);
if (keyToggledOff(event.state) || !isMacrosKey(event.key)) {
// If we toggled off or if the value of `event.key` has changed, send
// release events for any active macro keys. Simply clearing
// `active_macro_keys_` might be sufficient, but it's probably better to
// send the toggle off events so that other plugins get a chance to act on
// them.
clear();
// Return `OK` to let Kaleidoscope finish processing this event as
// normal. This is so that, if the user-defined `macroAction(id, &event)`
// function changes the value of `event.key`, it will take effect properly.
return EventHandlerResult::OK;
}
uint8_t macro_index = mappedKey.getRaw() - ranges::MACRO_FIRST;
addActiveMacroKey(macro_index, key_addr.toInt(), keyState);
return EventHandlerResult::EVENT_CONSUMED;
}
EventHandlerResult Macros_::afterEachCycle() {
active_macro_count = 0;
// No other plugin should be handling Macros keys, and there's nothing more
// for Kaleidoscope to do with a key press of a Macros key, so we return
// `EVENT_CONSUMED`, causing Kaleidoscope to update `live_keys[]` correctly,
// ensuring that the above block will clear `active_macro_keys_` on release,
// but not allowing any other plugins to change the `event.key` value, which
// would interfere.
//return EventHandlerResult::EVENT_CONSUMED;
return EventHandlerResult::OK;
}
EventHandlerResult Macros_::beforeReportingState() {
for (byte i = 0; i < active_macro_count; ++i) {
if (active_macros[i].key_id == 0xFF) {
key_addr = UnknownKeyswitchLocation;
} else {
key_addr = KeyAddr(active_macros[i].key_id);
}
const macro_t *m = macroAction(active_macros[i].key_code,
active_macros[i].key_state);
Macros.play(m);
EventHandlerResult Macros::beforeReportingState(const KeyEvent &event) {
// Do this in beforeReportingState(), instead of `onAddToReport()` because
// `live_keys` won't get updated until after the macro sequence is played from
// the keypress. This could be changed by either updating `live_keys` manually
// ahead of time, or by executing the macro sequence on key release instead of
// key press. This is probably the simplest solution.
for (Key key : active_macro_keys_) {
if (key != Key_NoKey)
Runtime.addToReport(key);
}
return EventHandlerResult::OK;
}
EventHandlerResult Macros::onNameQuery() {
return ::Focus.sendName(F("Macros"));
}
}
kaleidoscope::plugin::Macros_ Macros;
} // namespace plugin
} // namespace kaleidoscope
kaleidoscope::plugin::Macros Macros;

@ -23,68 +23,158 @@
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/key_events.h"
const macro_t *macroAction(uint8_t macroIndex, uint8_t keyState);
// =============================================================================
// Deprecated Macros code
#ifndef NDEPRECATED
#define _DEPRECATED_MESSAGE_MACROS_ACTIVE_MACRO_COUNT __NL__ \
"The `Macros.active_macro_count` variable is deprecated. It no longer has\n" __NL__ \
"any functional purpose, and can be safely removed from your code."
#define _DEPRECATED_MESSAGE_MACROS_ACTIVE_MACROS __NL__ \
"The `Macros.active_macros` array is deprecated. It no longer serves any\n" __NL__ \
"functional purpose, and can be safely removed from your code."
#define _DEPRECATED_MESSAGE_MACROS_ADD_ACTIVE_MACRO_KEY __NL__ \
"The `Macros.addActiveMacroKey()` function is deprecated. It no longer\n" __NL__ \
"has any functional purpose, and can be safely removed from your code."
#define _DEPRECATED_MESSAGE_MACRO_ACTION_FUNCTION_V1 __NL__ \
"The old `macroAction(macro_id, key_state)` is deprecated.\n" __NL__ \
"Please define the new `macroAction()` function instead:\n" __NL__ \
"\n" __NL__ \
"const macro_t* macroAction(uint8_t macro_id, KeyEvent &event);\n" __NL__ \
"\n" __NL__ \
"In the body of the new function, replace the `key_state` value with\n" __NL__ \
"`event.state`. Also, note that the new function gives you access to the\n" __NL__ \
"`KeyAddr` of the Macros key event (`event.addr`), and the `Key` value\n" __NL__ \
"(`event.key`). Because the event is passed by reference, it is now\n" __NL__ \
"possible to assign to `event.key` on a toggled-on event, causing that\n" __NL__ \
"`Key` value to persist after the macro finishes playing, leaving that\n" __NL__ \
"value active until the key is released."
#define _DEPRECATED_MESSAGE_MACROS_KEY_ADDR __NL__ \
"The `Macros.key_addr` public variable is deprecated.\n" __NL__ \
"Instead of using this to get the `KeyAddr` of the current macro from\n" __NL__ \
"`macroAction()`, please use the new version of `macroAction()`, which\n" __NL__ \
"uses a `KeyEvent` as its second parameter, giving access to the address\n" __NL__ \
"of the event in the `event.addr` member variable."
#define _DEPRECATED_MESSAGE_MACROS_MACRODOWN __NL__ \
"The `MACRODOWN()` preprocessor macro is deprecated. Please use `MACRO()`\n" __NL__ \
"with a test for `keyToggledOn(event.state)` instead."
DEPRECATED(MACROS_MACRODOWN)
const macro_t* deprecatedMacroDown(uint8_t key_state, const macro_t* macro_p);
#if !defined(MAX_CONCURRENT_MACROS)
#define MAX_CONCURRENT_MACROS 8
#endif
// =============================================================================
// Define this function in a Kaleidoscope sketch in order to trigger Macros.
const macro_t* macroAction(uint8_t macro_id, KeyEvent &event);
#ifndef NDEPRECATED
DEPRECATED(MACRO_ACTION_FUNCTION_V1)
const macro_t* macroAction(uint8_t macro_id, uint8_t key_state);
struct MacroKeyEvent {
byte key_code;
byte key_id;
byte key_state;
};
#endif
// The number of simultaneously-active `Key` values that a macro can have
// running during a call to `Macros.play()`. I don't know if it's actually
// possible to override this by defining it in a sketch before including
// "Kaleidoscope-Macros.h", but probably not.
#if !defined(MAX_CONCURRENT_MACRO_KEYS)
#define MAX_CONCURRENT_MACRO_KEYS 8
#endif
namespace kaleidoscope {
namespace plugin {
class Macros_ : public kaleidoscope::Plugin {
class Macros : public kaleidoscope::Plugin {
public:
Macros_(void) {}
static MacroKeyEvent active_macros[MAX_CONCURRENT_MACROS];
static byte active_macro_count;
static void addActiveMacroKey(byte key_code, byte key_id, byte key_state) {
// If we've got too many active macros, give up:
if (active_macro_count >= MAX_CONCURRENT_MACROS) {
return;
}
active_macros[active_macro_count].key_code = key_code;
active_macros[active_macro_count].key_id = key_id;
active_macros[active_macro_count].key_state = key_state;
++active_macro_count;
}
EventHandlerResult onNameQuery();
EventHandlerResult onKeyswitchEvent(Key &mappedKey, KeyAddr key_addr, uint8_t keyState);
EventHandlerResult beforeReportingState();
EventHandlerResult afterEachCycle();
void play(const macro_t *macro_p);
/* What follows below, is a bit of template magic that allows us to use
Macros.type() with any number of arguments, without having to use a
sentinel. See the comments on Runtime.use() for more details - this is
the same trick.
*/
inline const macro_t *type() {
/// Send a key press event from a Macro
///
/// Generates a new `KeyEvent` and calls `Runtime.handleKeyEvent()` with the
/// specified `key`, then stores that `key` in an array of active macro key
/// values. This allows the macro to press one key and keep it active when a
/// subsequent key event is sent as part of the same macro sequence.
void press(Key key);
/// Send a key release event from a Macro
///
/// Generates a new `KeyEvent` and calls `Runtime.handleKeyEvent()` with the
/// specified `key`, then removes that key from the array of active macro
/// keys (see `Macros.press()`).
void release(Key key);
/// Clear all virtual keys held by Macros
///
/// This function clears the active macro keys array, sending a release event
/// for each key stored there.
void clear();
/// Send a key "tap event" from a Macro
///
/// Generates two new `KeyEvent` objects, one each to press and release the
/// specified `key`, passing both in sequence to `Runtime.handleKeyEvent()`.
void tap(Key key) const;
/// Play a macro sequence of key events
void play(const macro_t* macro_ptr);
// Templates provide a `type()` function that takes a variable number of
// `char*` (string) arguments, in the form of a list of strings stored in
// PROGMEM, of the form `Macros.type(PSTR("Hello "), PSTR("world!"))`.
inline const macro_t* type() const {
return MACRO_NONE;
}
const macro_t *type(const char *string);
const macro_t* type(const char* string) const;
template <typename... Strings>
const macro_t *type(const char *first, Strings&&... strings) {
const macro_t* type(const char* first, Strings&&... strings) const {
type(first);
return type(strings...);
}
static KeyAddr key_addr;
// ---------------------------------------------------------------------------
// Event handlers
EventHandlerResult onNameQuery();
EventHandlerResult onKeyEvent(KeyEvent &event);
EventHandlerResult beforeReportingState(const KeyEvent &event);
private:
Key lookupAsciiCode(uint8_t ascii_code);
bool isMacroKey(Key key);
// An array of key values that are active while a macro sequence is playing
static Key active_macro_keys_[MAX_CONCURRENT_MACRO_KEYS];
// Translate and ASCII character value to a corresponding `Key`
Key lookupAsciiCode(uint8_t ascii_code) const;
// Test for a key that encodes a macro ID
bool isMacrosKey(Key key) const {
if (key >= ranges::MACRO_FIRST && key <= ranges::MACRO_LAST)
return true;
return false;
}
#ifndef NDEPRECATED
public:
DEPRECATED(MACROS_ACTIVE_MACROS)
static MacroKeyEvent active_macros[0];
DEPRECATED(MACROS_ACTIVE_MACRO_COUNT)
static uint8_t active_macro_count;
DEPRECATED(MACROS_ADD_ACTIVE_MACRO_KEY)
static void addActiveMacroKey(uint8_t macro_id, KeyAddr key_addr, uint8_t key_state) {}
DEPRECATED(MACROS_KEY_ADDR)
static KeyAddr key_addr;
#endif
};
}
}
} // namespace plugin
} // namespace kaleidosocpe
extern kaleidoscope::plugin::Macros_ Macros;
extern kaleidoscope::plugin::Macros Macros;

@ -1,3 +1,4 @@
// -*- mode: c++ -*-
/* Kaleidoscope-Macros - Macro keys for Kaleidoscope.
* Copyright (C) 2017-2018 Keyboard.io, Inc.
*

@ -1,3 +1,4 @@
// -*- mode: c++ -*-
/* Kaleidoscope-Macros - Macro keys for Kaleidoscope.
* Copyright (C) 2017-2019 Keyboard.io, Inc.
*
@ -41,8 +42,20 @@ typedef enum {
typedef uint8_t macro_t;
#define MACRO_NONE 0
#define MACRO(...) ({static const macro_t __m[] PROGMEM = { __VA_ARGS__, MACRO_ACTION_END }; &__m[0]; })
#define MACRODOWN(...) (keyToggledOn(keyState) ? MACRO(__VA_ARGS__) : MACRO_NONE)
#define MACRO(...) ( \
{ \
static const macro_t __m[] PROGMEM = { \
__VA_ARGS__, \
MACRO_ACTION_END \
}; \
&__m[0]; \
})
#ifndef NDEPRECATED
#define MACRODOWN(...) \
deprecatedMacroDown(event.state, MACRO(__VA_ARGS__));
#endif
#define I(n) MACRO_ACTION_STEP_INTERVAL, n
#define W(n) MACRO_ACTION_STEP_WAIT, n

@ -28,7 +28,7 @@ EventHandlerResult MagicCombo::onNameQuery() {
return ::Focus.sendName(F("MagicCombo"));
}
EventHandlerResult MagicCombo::beforeReportingState() {
EventHandlerResult MagicCombo::afterEachCycle() {
for (byte i = 0; i < magiccombo::combos_length; i++) {
bool match = true;
byte j;

@ -49,7 +49,7 @@ class MagicCombo : public kaleidoscope::Plugin {
static uint16_t min_interval;
EventHandlerResult onNameQuery();
EventHandlerResult beforeReportingState();
EventHandlerResult afterEachCycle();
private:
static uint16_t start_time_;

@ -24,8 +24,6 @@
namespace kaleidoscope {
namespace plugin {
uint8_t MouseKeys_::mouseMoveIntent;
uint8_t MouseKeys_::speed = 1;
uint16_t MouseKeys_::speedDelay = 1;
@ -39,6 +37,9 @@ uint16_t MouseKeys_::move_start_time_;
uint16_t MouseKeys_::accel_start_time_;
uint16_t MouseKeys_::wheel_start_time_;
// =============================================================================
// Configuration functions
void MouseKeys_::setWarpGridSize(uint8_t grid_size) {
MouseWrapper.warp_grid_size = grid_size;
}
@ -47,150 +48,199 @@ void MouseKeys_::setSpeedLimit(uint8_t speed_limit) {
MouseWrapper.speedLimit = speed_limit;
}
void MouseKeys_::scrollWheel(uint8_t keyCode) {
if (!Runtime.hasTimeExpired(wheel_start_time_, wheelDelay))
return;
// =============================================================================
// Key variant tests
wheel_start_time_ = Runtime.millisAtCycleStart();
bool MouseKeys_::isMouseKey(const Key& key) const {
return (key.getFlags() == (SYNTHETIC | IS_MOUSE_KEY));
}
if (keyCode & KEY_MOUSE_UP)
kaleidoscope::Runtime.hid().mouse().move(0, 0, wheelSpeed);
else if (keyCode & KEY_MOUSE_DOWN)
kaleidoscope::Runtime.hid().mouse().move(0, 0, -wheelSpeed);
else if (keyCode & KEY_MOUSE_LEFT)
kaleidoscope::Runtime.hid().mouse().move(0, 0, 0, -wheelSpeed);
else if (keyCode & KEY_MOUSE_RIGHT)
kaleidoscope::Runtime.hid().mouse().move(0, 0, 0, wheelSpeed);
bool MouseKeys_::isMouseButtonKey(const Key& key) const {
uint8_t variant = key.getKeyCode() & (KEY_MOUSE_BUTTON | KEY_MOUSE_WARP);
return variant == KEY_MOUSE_BUTTON;
}
EventHandlerResult MouseKeys_::onNameQuery() {
return ::Focus.sendName(F("MouseKeys"));
bool MouseKeys_::isMouseMoveKey(const Key& key) const {
uint8_t mask = (KEY_MOUSE_BUTTON | KEY_MOUSE_WARP | KEY_MOUSE_WHEEL);
uint8_t variant = key.getKeyCode() & mask;
return variant == 0;
}
EventHandlerResult MouseKeys_::afterEachCycle() {
kaleidoscope::Runtime.hid().mouse().sendReport();
kaleidoscope::Runtime.hid().mouse().releaseAllButtons();
mouseMoveIntent = 0;
bool MouseKeys_::isMouseWarpKey(const Key& key) const {
return (key.getKeyCode() & KEY_MOUSE_WARP) != 0;
}
return EventHandlerResult::OK;
bool MouseKeys_::isMouseWheelKey(const Key& key) const {
uint8_t mask = (KEY_MOUSE_BUTTON | KEY_MOUSE_WARP | KEY_MOUSE_WHEEL);
uint8_t variant = key.getKeyCode() & mask;
return variant == KEY_MOUSE_WHEEL;
}
EventHandlerResult MouseKeys_::beforeReportingState() {
if (mouseMoveIntent == 0) {
MouseWrapper.accelStep = 0;
return EventHandlerResult::OK;
}
// =============================================================================
// Event Handlers
if (!Runtime.hasTimeExpired(move_start_time_, speedDelay))
return EventHandlerResult::OK;
// -----------------------------------------------------------------------------
EventHandlerResult MouseKeys_::onNameQuery() {
return ::Focus.sendName(F("MouseKeys"));
}
move_start_time_ = Runtime.millisAtCycleStart();
// -----------------------------------------------------------------------------
EventHandlerResult MouseKeys_::onSetup(void) {
kaleidoscope::Runtime.hid().mouse().setup();
kaleidoscope::Runtime.hid().absoluteMouse().setup();
int8_t moveX = 0, moveY = 0;
return EventHandlerResult::OK;
}
// -----------------------------------------------------------------------------
EventHandlerResult MouseKeys_::afterEachCycle() {
// Check timeout for accel update interval.
if (Runtime.hasTimeExpired(accel_start_time_, accelDelay)) {
accel_start_time_ = Runtime.millisAtCycleStart();
// `accelStep` determines the movement speed of the mouse pointer, and gets
// reset to zero when no mouse movement keys is pressed (see below).
if (MouseWrapper.accelStep < 255 - accelSpeed) {
MouseWrapper.accelStep += accelSpeed;
}
accel_start_time_ = Runtime.millisAtCycleStart();
}
if (mouseMoveIntent & KEY_MOUSE_UP)
moveY -= speed;
if (mouseMoveIntent & KEY_MOUSE_DOWN)
moveY += speed;
// Check timeout for position update interval.
bool update_position = Runtime.hasTimeExpired(move_start_time_, speedDelay);
if (update_position) {
move_start_time_ = Runtime.millisAtCycleStart();
// Determine which mouse movement directions are active by searching through
// all the currently active keys for mouse movement keys, and adding them to
// a bitfield (`directions`).
uint8_t directions = 0;
int8_t vx = 0;
int8_t vy = 0;
for (Key key : live_keys.all()) {
if (isMouseKey(key) && isMouseMoveKey(key)) {
directions |= key.getKeyCode();
}
}
if (mouseMoveIntent & KEY_MOUSE_LEFT)
moveX -= speed;
if (mouseMoveIntent & KEY_MOUSE_RIGHT)
moveX += speed;
if (directions == 0) {
// If there are no mouse movement keys held, reset speed to zero.
MouseWrapper.accelStep = 0;
} else {
// For each active direction, add the mouse movement speed.
if (directions & KEY_MOUSE_LEFT)
vx -= speed;
if (directions & KEY_MOUSE_RIGHT)
vx += speed;
if (directions & KEY_MOUSE_UP)
vy -= speed;
if (directions & KEY_MOUSE_DOWN)
vy += speed;
// Prepare the mouse report.
MouseWrapper.move(vx, vy);
// Send the report.
Runtime.hid().mouse().sendReport();
}
}
// Check timeout for scroll report interval.
bool update_wheel = Runtime.hasTimeExpired(wheel_start_time_, wheelDelay);
if (update_wheel) {
wheel_start_time_ = Runtime.millisAtCycleStart();
// Determine which scroll wheel keys are active, and add their directions to
// a bitfield (`directions`).
uint8_t directions = 0;
int8_t vx = 0;
int8_t vy = 0;
for (Key key : live_keys.all()) {
if (isMouseKey(key) && isMouseWheelKey(key)) {
directions |= key.getKeyCode();
}
}
MouseWrapper.move(moveX, moveY);
if (directions != 0) {
// Horizontal scroll wheel:
if (directions & KEY_MOUSE_LEFT)
vx -= wheelSpeed;
if (directions & KEY_MOUSE_RIGHT)
vx += wheelSpeed;
// Vertical scroll wheel (note coordinates are opposite movement):
if (directions & KEY_MOUSE_UP)
vy += wheelSpeed;
if (directions & KEY_MOUSE_DOWN)
vy -= wheelSpeed;
// Add scroll wheel changes to HID report.
Runtime.hid().mouse().move(0, 0, vy, vx);
// Send the report.
Runtime.hid().mouse().sendReport();
}
}
return EventHandlerResult::OK;
}
EventHandlerResult MouseKeys_::onKeyswitchEvent(Key &mappedKey, KeyAddr key_addr, uint8_t keyState) {
if (mappedKey.getFlags() != (SYNTHETIC | IS_MOUSE_KEY))
// -----------------------------------------------------------------------------
EventHandlerResult MouseKeys_::onKeyEvent(KeyEvent &event) {
if (!isMouseKey(event.key))
return EventHandlerResult::OK;
if (mappedKey.getKeyCode() & KEY_MOUSE_BUTTON && !(mappedKey.getKeyCode() & KEY_MOUSE_WARP)) {
uint8_t button = mappedKey.getKeyCode() & ~KEY_MOUSE_BUTTON;
if (isMouseButtonKey(event.key)) {
sendMouseButtonReport(event);
if (keyIsPressed(keyState)) {
// Reset warp state on initial mouse button key-down only so we can use
// warp keys to drag-and-drop:
if (keyToggledOn(keyState)) {
MouseWrapper.reset_warping();
}
kaleidoscope::Runtime.hid().mouse().pressButtons(button);
} else if (keyToggledOff(keyState)) {
kaleidoscope::Runtime.hid().mouse().releaseButtons(button);
MouseWrapper.end_warping();
}
} else if (!(mappedKey.getKeyCode() & KEY_MOUSE_WARP)) {
if (keyToggledOn(keyState)) {
move_start_time_ = Runtime.millisAtCycleStart();
accel_start_time_ = Runtime.millisAtCycleStart();
wheel_start_time_ = Runtime.millisAtCycleStart() - wheelDelay;
} else if (isMouseWarpKey(event.key)) {
if (keyToggledOn(event.state)) {
sendMouseWarpReport(event);
}
if (keyIsPressed(keyState)) {
if (mappedKey.getKeyCode() & KEY_MOUSE_WHEEL) {
scrollWheel(mappedKey.getKeyCode());
} else {
mouseMoveIntent |= mappedKey.getKeyCode();
}
} else if (keyToggledOff(keyState)) {
/* If a mouse key toggles off, we want to explicitly stop moving (or
* scrolling) in that direction. We want to do this to support use-cases
* where we send multiple reports per cycle (such as macros), and can't
* rely on the main loop clearing the report for us. We do not want to
* clear the whole report either, because we want any other mouse keys
* to still have their desired effect. Therefore, we selectively stop
* movement or scrolling. */
mouseMoveIntent &= ~mappedKey.getKeyCode();
bool x = false, y = false, vWheel = false, hWheel = false;
if (mappedKey.getKeyCode() & KEY_MOUSE_UP ||
mappedKey.getKeyCode() & KEY_MOUSE_DOWN) {
if (mappedKey.getKeyCode() & KEY_MOUSE_WHEEL) {
vWheel = true;
} else {
y = true;
}
} else if (mappedKey.getKeyCode() & KEY_MOUSE_LEFT ||
mappedKey.getKeyCode() & KEY_MOUSE_RIGHT) {
if (mappedKey.getKeyCode() & KEY_MOUSE_WHEEL) {
hWheel = true;
} else {
x = true;
}
}
kaleidoscope::Runtime.hid().mouse().stop(x, y, vWheel, hWheel);
}
} else if (keyToggledOn(keyState)) {
if (mappedKey.getKeyCode() & KEY_MOUSE_WARP && mappedKey.getFlags() & IS_MOUSE_KEY) {
MouseWrapper.warp(((mappedKey.getKeyCode() & KEY_MOUSE_WARP_END) ? WARP_END : 0x00) |
((mappedKey.getKeyCode() & KEY_MOUSE_UP) ? WARP_UP : 0x00) |
((mappedKey.getKeyCode() & KEY_MOUSE_DOWN) ? WARP_DOWN : 0x00) |
((mappedKey.getKeyCode() & KEY_MOUSE_LEFT) ? WARP_LEFT : 0x00) |
((mappedKey.getKeyCode() & KEY_MOUSE_RIGHT) ? WARP_RIGHT : 0x00));
}
} else if (isMouseMoveKey(event.key)) {
// No report is sent here; that's handled in `afterEachCycle()`.
move_start_time_ = Runtime.millisAtCycleStart() - speedDelay;
accel_start_time_ = Runtime.millisAtCycleStart();
} else if (isMouseWheelKey(event.key)) {
// No report is sent here; that's handled in `afterEachCycle()`.
wheel_start_time_ = Runtime.millisAtCycleStart() - wheelDelay;
}
return EventHandlerResult::EVENT_CONSUMED;
}
EventHandlerResult MouseKeys_::onSetup(void) {
kaleidoscope::Runtime.hid().mouse().setup();
kaleidoscope::Runtime.hid().absoluteMouse().setup();
return EventHandlerResult::OK;
// =============================================================================
// HID report helper functions
// -----------------------------------------------------------------------------
void MouseKeys_::sendMouseButtonReport(const KeyEvent &event) const {
// Get ready to send a new mouse report by building it from live_keys. Note
// that this also clears the movement and scroll values, but since those are
// relative, that's what we want.
Runtime.hid().mouse().releaseAllButtons();
uint8_t buttons = 0;
for (KeyAddr key_addr : KeyAddr::all()) {
if (key_addr == event.addr)
continue;
Key key = live_keys[key_addr];
if (isMouseKey(key) && isMouseButtonKey(key)) {
buttons |= key.getKeyCode();
}
}
if (keyToggledOn(event.state))
buttons |= event.key.getKeyCode();
buttons &= ~KEY_MOUSE_BUTTON;
Runtime.hid().mouse().pressButtons(buttons);
Runtime.hid().mouse().sendReport();
}
// -----------------------------------------------------------------------------
void MouseKeys_::sendMouseWarpReport(const KeyEvent &event) const {
MouseWrapper.warp(
((event.key.getKeyCode() & KEY_MOUSE_WARP_END) ? WARP_END : 0x00) |
((event.key.getKeyCode() & KEY_MOUSE_UP) ? WARP_UP : 0x00) |
((event.key.getKeyCode() & KEY_MOUSE_DOWN) ? WARP_DOWN : 0x00) |
((event.key.getKeyCode() & KEY_MOUSE_LEFT) ? WARP_LEFT : 0x00) |
((event.key.getKeyCode() & KEY_MOUSE_RIGHT) ? WARP_RIGHT : 0x00));
}
}
} // namespace plugin
} // namespace kaleidoscope
kaleidoscope::plugin::MouseKeys_ MouseKeys;

@ -39,17 +39,23 @@ class MouseKeys_ : public kaleidoscope::Plugin {
EventHandlerResult onSetup();
EventHandlerResult onNameQuery();
EventHandlerResult beforeReportingState();
EventHandlerResult afterEachCycle();
EventHandlerResult onKeyswitchEvent(Key &mappedKey, KeyAddr key_addr, uint8_t keyState);
EventHandlerResult onKeyEvent(KeyEvent &event);
private:
static uint8_t mouseMoveIntent;
static uint16_t move_start_time_;
static uint16_t accel_start_time_;
static uint16_t wheel_start_time_;
static void scrollWheel(uint8_t keyCode);
bool isMouseKey(const Key &key) const;
bool isMouseButtonKey(const Key &key) const;
bool isMouseMoveKey(const Key &key) const;
bool isMouseWarpKey(const Key &key) const;
bool isMouseWheelKey(const Key &key) const;
void sendMouseButtonReport(const KeyEvent &event) const;
void sendMouseWarpReport(const KeyEvent &event) const;
};
}
}

@ -23,7 +23,7 @@ active if another one-shot of the same type is tapped, so `Ctrl, Alt, b` becomes
`Ctrl+Alt+b`, and `L1, L2, c` is turned into `L1+L2+c`. Furthermore, modifiers
and other layer keys do not cancel the one-shot effect, either.
## Using One-Shot Keys
## Using One-Shot keys
To enter one-shot mode, tap _quickly_ on a one-shot key. The next
normal (non-one-shot) key you press will have the modifier applied,
@ -40,11 +40,30 @@ modifier will now stay on until you press it again. Continuing the
Shift, d, e, f` will give you `ABCdef`.
This can be a bit tricky; combining this plugin with
[LED-ActiveModColor](Kaleidoscope-LED-ActiveModColor.md)
will help you understand what state your one-shot is in; when a
one-shot key is active, it will have a white LED highlight; when
sticky, a red highlight. (These colors are configurable.)
[LED-ActiveModColor](Kaleidoscope-LED-ActiveModColor.md) will help you
understand what state your one-shot is in; when a one-shot key is active, it
will have a yellow LED highlight; when sticky, a red highlight. When it is in a
"held" state, but will be deactivated when released like any non-one-shot key,
it will have a white highlight. (These colors are configurable.)
## Special OneShot keys
OneShot also comes with two special keys that can make any key on your keyboard
sticky: `OneShot_MetaStickyKey` & `OneShot_ActiveStickyKey`. These are both
`Key` values that can be used as entries in your sketch's keymap.
### `OneShot_MetaStickyKey`
This special OneShot key behaves like other OneShot keys, but its affect is to
make the next key pressed sticky. Tap `OneShot_MetaStickyKey`, then tap `X`, and
`X` will become sticky. Tap `X` again to deactivate it.
### `OneShot_ActiveStickyKey`
This special key doesn't act like a OneShot key, but instead makes any key(s)
currently held (or otherwise active) sticky. Press (and hold) `X`, tap
`OneShot_ActiveStickyKey`, then release `X`, and `X` will stay active until it
is tapped again to deactivate it.
## Using the plugin
@ -69,13 +88,13 @@ void setup() {
There are two macros the plugin provides:
### `OSM(mod)`
#### `OSM(mod)`
> A macro that takes a single argument, the name of the modifier: `LeftControl`,
> `LeftShift`, `LeftAlt`, `LeftGui` or their right-side variant. When marked up
> with this macro, the modifier will act as a one-shot modifier.
### `OSL(layer)`
#### `OSL(layer)`
> Takes a layer number as argument, and sets up the key to act as a one-shot
> layer key.
@ -83,85 +102,203 @@ There are two macros the plugin provides:
> Please note that while `Kaleidoscope` supports more, one-shot layers are
> limited to 8 layers only.
In addition, there is a special key:
#### `Key_MetaSticky`
> A key that behaves like a one-shot key, but while active, it makes
> other keys that are pressed become sticky, just like double-tapped
> one-shot keys.
## Plugin methods
The plugin provides one object, `OneShot`, which implements both one-shot
modifiers and one-shot layer keys. It has the following methods:
### `.isActive()`
### Configuration methods: Timeouts
> Returns if any one-shot key is in flight. This makes it possible to
> differentiate between having a modifier or layer active, versus having them
> active only until after the next key getting pressed. And this, in turn, is
> useful for macros that need to fiddle with either modifier or layer state: if
> one-shots are not active, they need not restore the original state.
#### `.setTimeout(timeout)`
### `.isPressed()`
> OneShot keys will remain active after they're pressed for `timeout`
> milliseconds (or until a subsequent non-oneshot key is pressed). The
> default value is 2500 (2.5 seconds).
> Returns true if any one-shot key is still held.
#### `.setHoldTimeout(timeout)`
### `.isSticky(key)`
> If a one-shot key is held for longer than `timeout` milliseconds, it
> will behave like a normal key, and won't remain active after it is
> released. The default value is 250 (1/4 seconds).
> Returns if the key is currently sticky.
#### `.setDoubleTapTimeout(timeout)`
### `.isModifierActive(key)`
> If a one-shot key is double-tapped (pressed twice in a row) in less
> than `timeout` milliseconds, it wil become sticky, and will remain
> active until it is pressed a third time. The default value is -1,
> which indicates that it should use the same timeout as
> `.setTimeout()`.
> Returns if the modifier `key` has a one-shot state active. Use this together
> with `Kaleidoscope.hid().keyboard().isModifierKeyActive` to catch cases where
> a one-shot modifier is active, but not registered yet.
### Configuration methods: Stickability
### `.cancel([with_stickies])`
#### `.enableStickability(key...)`
#### `.disableStickability(key...)`
> The `cancel()` method can be used to cancel any pending one-shot effects,
> useful when one changed their minds, and does not wish to wait for the
> timeout.
> Enables/disables stickability for all keys listed. The keys should
> all be OneShot keys, modifier keys, or layer-shift keys, as
> specified on the keymap. For example:
> `OneShot.enableStickability(OSM(LeftShift), OSL(1), Key_RightGUI)`.
> `OneShot.disableStickability(OSM(RighttAlt), OSL(2), ShiftToLayer(4))`.
>
> The optional `with_stickies` argument, if set to `true`, will also cancel
> sticky one-shot effects. If omitted, it defaults to `false`, and not canceling
> stickies.
> By default, all OneShot keys are stickable.
### `.inject(key, keyState)`
#### `.enableStickabilityForModifiers()`
#### `.enableStickabilityForLayers()`
#### `.disableStickabilityForModifiers()`
#### `.disableStickabilityForLayers()`
> Simulates a key event, specifically designed to inject one-shot keys into the
> event loop. The primary purpose of this method is to make it easier to trigger
> multiple one-shots at the same time.
>
> See the example sketch for more information about its use.
> Enables/disables stickability for all modifiers and layers,
> respectively. These are convenience methods for cases where one
> wants to enable stickability for a group of one-shot keys.
### `.enableStickability(key...)`
### Configuration methods: Automatic one-shot keys
> Enables stickability for all keys listed. The keys should all be OneShot keys,
> as if specified on the keymap. For example:
> `OneShot.enableStickability(OSM(LeftShift), OSL(1))`.
>
> By default, all oneshot keys are stickable.
#### `.enableAutoModifiers()`
#### `.disableAutoModifiers()`
#### `.toggleAutoModifiers()`
### `.enableStickabilityForModifiers()`
### `.enableStickabilityForLayers()`
> Enables/disables/toggles auto-oneshot functionality for modifier
> keys. When enabled, all normal modifier keys, including those with
> other modifier flags added to them (e.g. `LSHIFT(Key_LeftAlt)`,
> `Key_Meh`) will be automatically treated as one-shot keys, in
> addition to dedicated ones like `OSM(LeftGui)`.
> Enables stickability for all modifiers and layers, respectively. These are
> convenience methods for cases where one wants to enable stickability for a
> group of one-shot keys.
#### `.enableAutoLayers()`
#### `.disableAutoLayers()`
#### `.toggleAutoLayers()`
### `.disableStickability(key...)`
> Enables/disables/toggles auto-oneshot functionality for layer shift
> keys (see above).
> Disables stickability for all keys listed. The keys should all be OneShot keys,
> as if specified on the keymap. For example:
> `OneShot.disableStickability(OSM(LeftShift), OSL(1))`.
>
> By default, all oneshot keys are stickable.
#### `.enableAutoOneShot()`
#### `.disableAutoOneShot()`
#### `.toggleAutoOneShot()`
> Enables/disables/toggles auto-oneshot functionality for all
> modifiers and layer shift keys.
### Test methods
#### `.isActive(key_addr)`
> Returns `true` if the key at `key_addr` is in an active one-shot
> state. Note that if a key is still being held, but will be not
> remain active after it is released, it is not considered to be in a
> one-shot state, even if it had been earlier.
#### `.isTemporary(key_addr)`
> Returns `true` if the key at `key_addr` is in a temporary one-shot
> state. Such a key will eventually time out or get deactivated by a
> subsequent key press.
#### `.isSticky(key_addr)`
> Returns `true` if the key at `key_addr` is in a permanent one-shot
> state. Such a key will not be deactivated by subsequent keypresses,
> nor will it time out. It will only be deactivated by pressing it one
> more time, or by being cancelled by the `cancel()` method (see
> below).
#### `.isActive()`
> Returns `true` if there are any active one-shot keys. Note: it
> returns `false` if there are no currently active one-shot keys, but
> there are keys that were at one time in a one-shot state, but are
> still being held after that state has been cancelled.
#### `.isSticky()`
> Returns `true` if there are any sticky one-shot keys.
#### `.isStickable(key)`
> Returns `true` if a key of the specified value can be made sticky by
> double-tapping.
#### `.isModifier(key)`
> Returns `true` if the specified key is a modifier key. This does not
> include OneShot modifiers (e.g. `OSM(LeftShift)`), but it does
> include modifiers with additional modifier flags (e.g. `Key_Meh`,
> `LCTRL(Key_RightGui)`).
#### `.isLayerShift(key)`
> Returns `true` if the specified key is a layer-shift key
> (e.g. `ShiftToLayer(2)`). OneShot layer keys (e.g. `OSL(5)` are not
> included).
#### `.isOneShotKey(key)`
> Returns `true` if the specified key is a OneShot modifier or
> layer-shift key (e.g. `OSM(LeftAlt)`, `OSL(3)`).
### Other methods
#### `.cancel([with_stickies])`
> Immediately deactivates the one-shot status of any _temporary_
> one-shot keys. Any keys still being physically held will continue to
> function as normal modifier/layer-shift keys.
>
> If `with_stickies` is `true` (the default is `false`), _sticky_
> one-shot keys will also be deactivated, in the same way.
### Deprecated methods
The following methods have been deprecated, and should no longer be
used, if possible. These functions made more sense when OneShot was
based on `Key` values; it has since be rewritten to be based on
`KeyAddr` values.
#### `.inject(key, key_state)`
> Finds an idle key on the keyboard, and turns it into a one-shot
> key. When OneShot was based on `Key` values, this made more sense,
> as it didn't need a valid `KeyAddr` to work. Since the main purpose
> of this method was to enable the triggering of multiple one-shot
> modifiers with a single key, it is much better to use automatic
> one-shot modifiers, if possible, because then it's not necessary to
> use a Macro to configure.
#### `.isModifierActive(key)`
> Returns `true` if a keymap cache entry with the current value of
> `key` is active (one-shot, sticky, or held). This should be a
> function that is not specific to OneShot.
#### `.isActive(key)`
> Returns `true` if a keymap cache entry with the current value of
> `key` is in an active one-shot state. Please use
> `.isActive(key_addr)` instead.
#### `.isSticky(key)`
> Returns `true` if a keymap cache entry with the current value of
> `key` is in a sticky one-shot state. Please use
> `.isSticky(key_addr)` instead.
### `.disableStickabilityForModifiers()`
### `.disableStickabilityForLayers()`
#### `.isPressed()`
> Disables stickability for all modifiers and layers, respectively. These are
> convenience methods for cases where one wants to disable stickability for a
> group of one-shot keys.
> Returns `false`. OneShot doesn't need to keep track of whether or
> not a one-shot key is still pressed any more. This function was
> mainly used by LED-ActiveModColor, which no longer needs it.
## Plugin properties
## Plugin properties **[DEPRECATED]**
Along with the methods listed above, the `OneShot` object has the following
properties too:
Along with the methods listed above, the `OneShot` object has the
following properties, too. [Note: these have all been deprecated,
please use the `.set*Timeout()` methods above instead.]
### `.time_out`

@ -19,274 +19,488 @@
#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::timeout_ = 2500;
uint16_t OneShot::hold_timeout_ = 250;
int16_t OneShot::double_tap_timeout_ = -1;
// Deprecated
#ifndef NDEPRECATED
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;
#endif
// ----------------------------------------------------------------------------
// State variables
uint16_t OneShot::stickable_keys_ = -1;
bool OneShot::auto_modifiers_ = false;
bool OneShot::auto_layers_ = false;
KeyAddrBitfield OneShot::temp_addrs_;
KeyAddrBitfield OneShot::glue_addrs_;
uint16_t OneShot::start_time_ = 0;
KeyAddr OneShot::prev_key_addr_ = OneShot::invalid_key_addr;
#ifndef ONESHOT_WITHOUT_METASTICKY
KeyAddr OneShot::meta_sticky_key_addr_ {KeyAddr::invalid_state};
#endif
// ============================================================================
// Public interface
// ----------------------------------------------------------------------------
// Configuration functions
void OneShot::enableStickability(Key key) {
uint8_t n = getKeyIndex(key);
stickable_keys_ |= (1 << n);
}
void OneShot::disableStickability(Key key) {
uint8_t n = getKeyIndex(key);
stickable_keys_ &= ~(1 << n);
}
void OneShot::enableStickabilityForModifiers() {
stickable_keys_ |= stickable_modifiers_mask;
}
void OneShot::disableStickabilityForModifiers() {
stickable_keys_ &= ~stickable_modifiers_mask;
}
void OneShot::enableStickabilityForLayers() {
stickable_keys_ |= stickable_layers_mask;
}
void OneShot::disableStickabilityForLayers() {
stickable_keys_ &= ~stickable_layers_mask;
}
// ----------------------------------------------------------------------------
// Global tests for any OneShot key
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;
}
bool OneShot::isSticky() {
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT; i++) {
if (state_[i].sticky)
for (KeyAddr key_addr : glue_addrs_) {
if (! temp_addrs_.read(key_addr)) {
return true;
}
}
return false;
}
bool OneShot::isStickable(Key key) {
return state_[key.getRaw() - ranges::OS_FIRST].stickable;
}
// ----------------------------------------------------------------------------
// Key-specific OneShot key tests
// ---- OneShot stuff ----
void OneShot::injectNormalKey(uint8_t idx, uint8_t key_state) {
Key key;
// 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 (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);
bool OneShot::isStickable(Key key) {
int8_t n;
if (key.isKeyboardModifier()) {
n = key.getKeyCode() - Key_LeftControl.getKeyCode();
return bitRead(stickable_keys_, n);
} else if (key.isLayerShift()) {
n = oneshot_mod_count + key.getKeyCode() - LAYER_SHIFT_OFFSET;
if (n < oneshot_key_count) {
return bitRead(stickable_keys_, n);
}
#ifndef ONESHOT_WITHOUT_METASTICKY
} else if (key == OneShot_MetaStickyKey) {
return true;
#endif
}
return false;
}
handleKeyswitchEvent(key, UnknownKeyswitchLocation, key_state | INJECTED);
bool OneShot::isTemporary(KeyAddr key_addr) {
return temp_addrs_.read(key_addr);
}
void OneShot::activateOneShot(uint8_t idx) {
injectNormalKey(idx, IS_PRESSED);
bool OneShot::isSticky(KeyAddr key_addr) {
return (glue_addrs_.read(key_addr) && !temp_addrs_.read(key_addr));
}
void OneShot::cancelOneShot(uint8_t idx) {
state_[idx].active = false;
injectNormalKey(idx, WAS_PRESSED);
bool OneShot::isActive(KeyAddr key_addr) {
return (isTemporary(key_addr) || glue_addrs_.read(key_addr));
}
// ----------------------------------------------------------------------------
// Other functions
// 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);
}
}
for (KeyAddr key_addr : temp_addrs_) {
if (glue_addrs_.read(key_addr)) {
releaseKey(key_addr);
} else {
temp_addrs_.clear(key_addr);
}
}
}
// ----------------------------------------------------------------------------
// Plugin hook functions
EventHandlerResult OneShot::onNameQuery() {
return ::Focus.sendName(F("OneShot"));
}
EventHandlerResult OneShot::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState) {
uint8_t idx = mapped_key.getRaw() - ranges::OS_FIRST;
EventHandlerResult OneShot::onKeyEvent(KeyEvent& event) {
if (keyState & INJECTED)
// 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 (event.state & INJECTED)
return EventHandlerResult::OK;
if (!isActive()) {
if (!isOneShotKey_(mapped_key)) {
return EventHandlerResult::OK;
}
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;
bool temp = temp_addrs_.read(event.addr);
bool glue = glue_addrs_.read(event.addr);
activateOneShot(idx);
}
if (keyToggledOn(event.state)) {
return EventHandlerResult::EVENT_CONSUMED;
}
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;
// Make all held keys sticky if `OneShot_ActiveStickyKey` toggles on.
if (event.key == OneShot_ActiveStickyKey) {
// Skip the stickify key itself
for (KeyAddr entry_addr : KeyAddr::all()) {
if (entry_addr == event.addr) {
continue;
}
// Get the entry from the keyboard state array
Key entry_key = live_keys[entry_addr];
// Skip empty entries
if (entry_key == Key_Transparent || entry_key == Key_NoKey) {
continue;
}
// Make everything else sticky
temp_addrs_.clear(entry_addr);
glue_addrs_.set(entry_addr);
}
prev_key_addr_ = event.addr;
return EventHandlerResult::OK;
}
if (keyToggledOn(keyState)) {
state_[idx].pressed = true;
if (!temp && !glue) {
// The key is in the "normal" state. The first thing we need to do is
// convert OneShot keys to their equivalent values, and record the fact
// that the key that just toggled on should transition to the "pending"
// state.
bool is_oneshot = false;
if (isOneShotKey(event.key)) {
event.key = decodeOneShotKey(event.key);
is_oneshot = 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;
#ifndef ONESHOT_WITHOUT_METASTICKY
bool is_meta_sticky_key_active = meta_sticky_key_addr_.isValid();
if (is_meta_sticky_key_active) {
// If the meta key isn't sticky, release it
bool ms_temp = temp_addrs_.read(meta_sticky_key_addr_);
bool ms_glue = glue_addrs_.read(meta_sticky_key_addr_);
if (ms_temp) {
if (ms_glue) {
// The meta key is in the "one-shot" state; release it immediately.
releaseKey(meta_sticky_key_addr_);
} else {
// The meta key is in the "pending" state; cancel that, and let it
// deactivate on release.
temp_addrs_.clear(meta_sticky_key_addr_);
}
} else {
start_time_ = Runtime.millisAtCycleStart();
state_[idx].position = key_addr.toInt();
state_[idx].active = true;
prev_key_ = mapped_key;
activateOneShot(idx);
}
glue_addrs_.set(event.addr);
} else if (event.key == OneShot_MetaStickyKey) {
meta_sticky_key_addr_ = event.addr;
temp_addrs_.set(event.addr);
}
if (is_meta_sticky_key_active || (event.key == OneShot_MetaStickyKey)) {
prev_key_addr_ = event.addr;
start_time_ = Runtime.millisAtCycleStart();
return EventHandlerResult::OK;
}
#endif
if (is_oneshot ||
(auto_modifiers_ && event.key.isKeyboardModifier()) ||
(auto_layers_ && event.key.isLayerShift())) {
temp_addrs_.set(event.addr);
start_time_ = Runtime.millisAtCycleStart();
} else if (!event.key.isKeyboardModifier() &&
!event.key.isLayerShift()) {
// Only trigger release of temporary one-shot keys if the pressed key is
// neither a modifier nor a layer shift. We need the actual release of
// those keys to happen after the current event is finished, however, so
// we trigger it by back-dating the start time, so that the timeout
// check will trigger in the afterEachCycle() hook.
start_time_ -= timeout_;
}
}
return EventHandlerResult::EVENT_CONSUMED;
}
} else if (temp && glue) {
// temporary one-shot
temp_addrs_.clear(event.addr);
if (event.addr == prev_key_addr_ &&
isStickable(event.key) &&
!hasDoubleTapTimedOut()) {
// The same stickable key has been double-tapped within the double-tap
// timeout window. We cancel the second press event, emulating a single
// press-and-hold. This doesn't interfere with `prev_key_addr_`, since
// it's the same key again.
return EventHandlerResult::ABORT;
} else {
// A second tap that's not a double-tap cancels the one-shot state
glue_addrs_.clear(event.addr);
}
// ordinary key here, with some event
} else if (!temp && glue) {
// sticky state
temp_addrs_.clear(event.addr);
glue_addrs_.clear(event.addr);
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;
} else { /* if (temp && !glue) */
// A key has been pressed that is in the "pending" one-shot state. Since
// this key should have entered the "temporary" one-shot 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_ = event.addr;
return EventHandlerResult::OK;
}
} else {
EventHandlerResult OneShot::beforeReportingState() {
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT / 2; i++) {
if (state_[i].active) {
activateOneShot(i);
// Key toggled off
if (temp || glue) {
// Any key in the "pending" one-shot 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 execution speed to get a smaller binary.
glue_addrs_.set(event.addr);
// This is an active one-shot 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.
return EventHandlerResult::ABORT;
#ifndef ONESHOT_WITHOUT_METASTICKY
} else if (event.key == OneShot_MetaStickyKey) {
// Turn off the meta key if it's released in its "normal" state.
meta_sticky_key_addr_ = KeyAddr::none();
#endif
}
}
return EventHandlerResult::OK;
}
// ----------------------------------------------------------------------------
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;
}
}
if (oneshot_active && hasTimedOut())
cancel();
bool is_cancelled = false;
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;
}
} else if (state_[i].active && !state_[i].pressed) {
is_cancelled = true;
cancelOneShot(i);
}
bool oneshot_expired = hasTimedOut(timeout_);
bool hold_expired = hasTimedOut(hold_timeout_);
bool any_temp_keys = false;
for (KeyAddr key_addr : temp_addrs_) {
any_temp_keys = true;
if (glue_addrs_.read(key_addr)) {
// Release keys in "one-shot" state that have timed out or been cancelled
// by another key press.
if (oneshot_expired)
releaseKey(key_addr);
} else {
// Cancel "pending" state of keys held longer than the hold timeout.
if (hold_expired)
temp_addrs_.clear(key_addr);
}
}
if (is_cancelled) {
should_cancel_ = false;
should_cancel_stickies_ = false;
// Keep the start time from getting stale; if there are no keys waiting for a
// timeout, it's safe to advance the timer to the current time.
if (!any_temp_keys) {
start_time_ = Runtime.millisAtCycleStart();
}
// Temporary fix for deprecated variables
#ifndef NDEPRECATED
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
timeout_ = time_out;
hold_timeout_ = hold_time_out;
double_tap_timeout_ = double_tap_time_out;
#pragma GCC diagnostic pop
#endif
return EventHandlerResult::OK;
}
void OneShot::inject(Key mapped_key, uint8_t key_state) {
onKeyswitchEvent(mapped_key, UnknownKeyswitchLocation, key_state);
}
// ============================================================================
// Private functions, not exposed to other plugins
// --- glue code ---
// ----------------------------------------------------------------------------
// Helper functions for acting on OneShot key events
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;
}
return false;
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;
}
bool OneShot::isActive(Key key) {
uint8_t idx = key.getRaw() - ranges::OS_FIRST;
return (state_[idx].active && !hasTimedOut()) ||
state_[idx].pressed ||
state_[idx].sticky;
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 (key.isKeyboardModifier()) {
n = key.getKeyCode() - Key_LeftControl.getKeyCode();
} else if (key.isLayerShift()) {
n = oneshot_mod_count + key.getKeyCode() - LAYER_SHIFT_OFFSET;
}
return n;
}
bool OneShot::isSticky(Key key) {
uint8_t idx = key.getRaw() - ranges::OS_FIRST;
return state_[idx].sticky;
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);
}
}
bool OneShot::isModifierActive(Key key) {
if (key < Key_LeftControl || key > Key_RightGui)
return false;
// ------------------------------------------------------------------------------
// Helper functions for sending key events for keys in OneShot states
uint8_t idx = key.getKeyCode() - Key_LeftControl.getKeyCode();
return state_[idx].active;
void OneShot::pressKey(KeyAddr key_addr, Key key) {
if (isOneShotKey(key)) {
key = decodeOneShotKey(key);
}
prev_key_addr_ = key_addr;
start_time_ = Runtime.millisAtCycleStart();
temp_addrs_.set(key_addr);
KeyEvent event{key_addr, IS_PRESSED | INJECTED, key};
Runtime.handleKeyEvent(event);
}
void OneShot::cancel(bool with_stickies) {
should_cancel_ = true;
should_cancel_stickies_ = with_stickies;
void OneShot::holdKey(KeyAddr key_addr) {
KeyEvent event{key_addr, WAS_PRESSED | IS_PRESSED | INJECTED};
Runtime.handleKeyEvent(event);
}
void OneShot::enableStickability(Key key) {
if (key >= ranges::OS_FIRST && key <= ranges::OS_LAST)
state_[key.getRaw() - ranges::OS_FIRST].stickable = true;
}
void OneShot::releaseKey(KeyAddr key_addr) {
glue_addrs_.clear(key_addr);
temp_addrs_.clear(key_addr);
void OneShot::disableStickability(Key key) {
if (key >= ranges::OS_FIRST && key <= ranges::OS_LAST)
state_[key.getRaw() - ranges::OS_FIRST].stickable = false;
#ifndef ONESHOT_WITHOUT_METASTICKY
if (live_keys[key_addr] == OneShot_MetaStickyKey)
meta_sticky_key_addr_ = KeyAddr::none();
#endif
KeyEvent event{key_addr, WAS_PRESSED | INJECTED};
Runtime.handleKeyEvent(event);
}
void OneShot::enableStickabilityForModifiers() {
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT / 2; i++) {
state_[i].stickable = true;
// ------------------------------------------------------------------------------
// Deprecated functions
#ifndef NDEPRECATED
void OneShot::inject(Key key, uint8_t key_state) {
if (isOneShotKey(key)) {
key = decodeOneShotKey(key);
}
// Find an idle keyswitch to use for the injected OneShot key and activate
// it. This is an ugly hack, but it will work. It does mean that whatever key
// is used will be unavailable for its normal function until the injected
// OneShot key is deactivated, so use of `inject()` is strongly discouraged.
for (KeyAddr key_addr : KeyAddr::all()) {
if (live_keys[key_addr] == Key_Transparent) {
pressKey(key_addr, key);
glue_addrs_.set(key_addr);
break;
}
}
}
void OneShot::enableStickabilityForLayers() {
for (uint8_t i = ONESHOT_KEY_COUNT / 2; i < ONESHOT_KEY_COUNT; i++) {
state_[i].stickable = true;
bool OneShot::isModifierActive(Key key) {
// This actually works for any `Key` value, not just modifiers. Because we're
// just searching the keymap cache, it's also possible to return a false
// positive (a plugin might have altered the cache for an idle `KeyAddr`), or
// a false negative (a plugin might be inserting a modifier without a valid
// `KeyAddr`), but as this is a deprecated function, I think this is good
// enough.
for (KeyAddr key_addr : KeyAddr::all()) {
if (live_keys[key_addr] == key) {
return true;
}
}
return false;
}
void OneShot::disableStickabilityForModifiers() {
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT / 2; i++) {
state_[i].stickable = false;
bool OneShot::isActive(Key key) {
if (isOneShotKey(key)) {
key = decodeOneShotKey(key);
}
for (KeyAddr key_addr : glue_addrs_) {
if (live_keys[key_addr] == key) {
return true;
}
}
return false;
}
void OneShot::disableStickabilityForLayers() {
for (uint8_t i = ONESHOT_KEY_COUNT / 2; i < ONESHOT_KEY_COUNT; i++) {
state_[i].stickable = false;
bool OneShot::isSticky(Key key) {
if (isOneShotKey(key)) {
key = decodeOneShotKey(key);
}
for (KeyAddr key_addr : glue_addrs_) {
if (live_keys[key_addr] == key &&
!temp_addrs_.read(key_addr)) {
return true;
}
}
return false;
}
#endif
}
}
} // namespace plugin
} // namespace kaleidoscope
kaleidoscope::plugin::OneShot OneShot;

@ -19,34 +19,62 @@
#include "kaleidoscope/Runtime.h"
#include <Kaleidoscope-Ranges.h>
#include "kaleidoscope/key_events.h"
#include "kaleidoscope/KeyAddrBitfield.h"
// ----------------------------------------------------------------------------
// Deprecation warning messages
#define _DEPRECATED_MESSAGE_ONESHOT_TIMEOUT \
"The `OneShot.time_out` variable is deprecated. Please use the\n" \
"`OneShot.setTimeout()` function instead."
#define _DEPRECATED_MESSAGE_ONESHOT_HOLD_TIMEOUT \
"The `OneShot.hold_time_out` variable is deprecated. Please use the\n" \
"`OneShot.setHoldTimeout()` function instead."
#define _DEPRECATED_MESSAGE_ONESHOT_DOUBLE_TAP_TIMEOUT \
"The `OneShot.double_tap_time_out` variable is deprecated. Please use the\n" \
"`OneShot.setDoubleTapTimeout()` function instead."
#define _DEPRECATED_MESSAGE_ONESHOT_INJECT \
"The `OneShot.inject(key, key_state)` function has been deprecated."
#define _DEPRECATED_MESSAGE_ONESHOT_ISACTIVE_KEY \
"The `OneShot.isActive(key)` function is deprecated. Please use\n" \
"`OneShot.isActive(key_addr)` instead, if possible."
#define _DEPRECATED_MESSAGE_ONESHOT_ISSTICKY_KEY \
"The `OneShot.isSticky(key)` function is deprecated. Please use\n" \
"`OneShot.isSticky(key_addr)` instead, if possible."
#define _DEPRECATED_MESSAGE_ONESHOT_ISPRESSED \
"The `OneShot.isPressed()` function is deprecated. This function now\n" \
"always returns false."
#define _DEPRECATED_MESSAGE_ONESHOT_ISMODIFIERACTIVE \
"The `OneShot.isModifierActive()` function is deprecated."
// ----------------------------------------------------------------------------
// 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)
// ----------------------------------------------------------------------------
// Key constants
constexpr Key OneShot_MetaStickyKey {kaleidoscope::ranges::OS_META_STICKY};
constexpr Key OneShot_ActiveStickyKey {kaleidoscope::ranges::OS_ACTIVE_STICKY};
namespace kaleidoscope {
namespace plugin {
class OneShot : public kaleidoscope::Plugin {
public:
OneShot(void) {
for (uint8_t i = 0; i < ONESHOT_KEY_COUNT; i++) {
state_[i].stickable = true;
}
}
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);
// Constructor
OneShot() {}
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 +96,187 @@ class OneShot : public kaleidoscope::Plugin {
static void disableStickabilityForModifiers();
static void disableStickabilityForLayers();
static bool isStickable(Key key);
static void enableAutoModifiers() {
auto_modifiers_ = true;
}
static void enableAutoLayers() {
auto_layers_ = true;
}
static void enableAutoOneShot() {
enableAutoModifiers();
enableAutoLayers();
}
static void disableAutoModifiers() {
auto_modifiers_ = false;
}
static void disableAutoLayers() {
auto_layers_ = false;
}
static void disableAutoOneShot() {
disableAutoModifiers();
disableAutoLayers();
}
static void toggleAutoModifiers() {
auto_modifiers_ = ! auto_modifiers_;
}
static void toggleAutoLayers() {
auto_layers_ = ! auto_layers_;
}
static void toggleAutoOneShot() {
if (auto_modifiers_ || auto_layers_) {
disableAutoOneShot();
} else {
enableAutoOneShot();
}
}
// --------------------------------------------------------------------------
// Global test functions
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);
// --------------------------------------------------------------------------
// Deprecated functions
#ifndef NDEPRECATED
DEPRECATED(ONESHOT_INJECT)
void inject(Key key, uint8_t key_state);
DEPRECATED(ONESHOT_ISMODIFIERACTIVE)
static bool isModifierActive(Key key);
DEPRECATED(ONESHOT_ISACTIVE_KEY)
static bool isActive(Key oneshot_key);
DEPRECATED(ONESHOT_ISSTICKY_KEY)
static bool isSticky(Key oneshot_key);
DEPRECATED(ONESHOT_ISPRESSED)
static bool isPressed() {
return false;
}
#endif
// --------------------------------------------------------------------------
// Timeout onfiguration functions
static void setTimeout(uint16_t ttl) {
timeout_ = ttl;
#ifndef NDEPRECATED
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
time_out = ttl;
#pragma GCC diagnostic pop
#endif
}
static void setHoldTimeout(uint16_t ttl) {
hold_timeout_ = ttl;
#ifndef NDEPRECATED
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
hold_time_out = ttl;
#pragma GCC diagnostic pop
#endif
}
static void setDoubleTapTimeout(int16_t ttl) {
double_tap_timeout_ = ttl;
#ifndef NDEPRECATED
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
double_tap_time_out = ttl;
#pragma GCC diagnostic pop
#endif
}
// --------------------------------------------------------------------------
// Configuration variables (should probably be private)
#ifndef NDEPRECATED
DEPRECATED(ONESHOT_TIMEOUT)
static uint16_t time_out;
DEPRECATED(ONESHOT_HOLD_TIMEOUT)
static uint16_t hold_time_out;
DEPRECATED(ONESHOT_DOUBLE_TAP_TIMEOUT)
static int16_t double_tap_time_out;
#endif
// --------------------------------------------------------------------------
// Plugin hook functions
EventHandlerResult onNameQuery();
EventHandlerResult beforeReportingState();
EventHandlerResult onKeyEvent(KeyEvent &event);
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];
// --------------------------------------------------------------------------
// 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);
// --------------------------------------------------------------------------
// Configuration variables
static uint16_t timeout_;
static uint16_t hold_timeout_;
static int16_t double_tap_timeout_;
// --------------------------------------------------------------------------
// State variables
static uint16_t stickable_keys_;
static bool auto_modifiers_;
static bool auto_layers_;
static KeyAddrBitfield temp_addrs_;
static KeyAddrBitfield glue_addrs_;
static uint16_t start_time_;
static Key prev_key_;
static bool should_cancel_;
static bool should_cancel_stickies_;
static KeyAddr prev_key_addr_;
static void injectNormalKey(uint8_t idx, uint8_t key_state);
static void activateOneShot(uint8_t idx);
static void cancelOneShot(uint8_t idx);
#ifndef ONESHOT_WITHOUT_METASTICKY
static KeyAddr meta_sticky_key_addr_;
#endif
static bool isOneShotKey_(Key key) {
return key.getRaw() >= ranges::OS_FIRST && key.getRaw() <= ranges::OS_LAST;
// --------------------------------------------------------------------------
// Internal utility functions
static bool hasTimedOut(uint16_t ttl) {
return Runtime.hasTimeExpired(start_time_, ttl);
}
static bool hasTimedOut() {
return Runtime.hasTimeExpired(start_time_, time_out);
static bool hasDoubleTapTimedOut() {
// Derive the true double-tap timeout value if we're using the default.
uint16_t dtto = (double_tap_timeout_ < 0) ? timeout_ : double_tap_timeout_;
return hasTimedOut(dtto);
}
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;

@ -75,6 +75,22 @@ likely to generate errors and out-of-order events.
>
> Defaults to `250`.
### `.setMaxIntervalForTapRepeat(timeout)`
> Sets the time (in milliseconds) that limits the tap-repeat window. If the same
> qukey is pressed, released, and pressed again within this timeframe, then
> held, Qukeys will turn it into a single press and hold event, using the
> primary key value (which cannot otherwise be held). If the second press is
> also a tap, and the two _release_ events occur within the same timeframe, it
> will instead be treated as a double tap (of the primary key value).
>
> To effectively shut off the tap-repeat feature, set this value to `0`. The
> maximum value is `255`; anything higher than `250` could result in key repeat
> being triggered on the host before Qukeys determines whether it's a tap-repeat
> or a double-tap sequence, because most systems delay the key repeat by 500 ms.
>
> Defaults to `200`.
### `.setOverlapThreshold(percentage)`
> This sets a variable that allows the user to roll over from a qukey to a
@ -205,6 +221,16 @@ the given alternate value on all layers, regardless of what the primary value is
for that key on the top currently active layer.
### Tap-Repeat Behaviour
If a qukey is tapped, then immediately pressed and held, Qukeys will turn that
sequence of events into a single press and hold of the primary key value
(whereas merely holding the key yeilds the alternate value). This is
particularly useful on macOS apps that use Apple's Cocoa input system, where
holding a key gives the user access to a menu of accented characters, rather
than merely repeating the same character until the key is released.
## Design & Implementation
When a qukey is pressed, it doesn't immediately add a corresponding keycode to

@ -1,6 +1,6 @@
/* -*- mode: c++ -*-
* Kaleidoscope-Qukeys -- Assign two keycodes to a single key
* Copyright (C) 2017-2019 Michael Richters
* Copyright (C) 2017-2020 Michael Richters
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -23,7 +23,9 @@
#include <Kaleidoscope-FocusSerial.h>
#include "kaleidoscope/progmem_helpers.h"
#include "kaleidoscope/layers.h"
#include "kaleidoscope/KeyEvent.h"
#include "kaleidoscope/KeyEventTracker.h"
#include "kaleidoscope/KeyAddrEventQueue.h"
namespace kaleidoscope {
namespace plugin {
@ -34,68 +36,37 @@ EventHandlerResult Qukeys::onNameQuery() {
// This is the event handler. It ignores certain events, but mostly just adds
// them to the Qukeys event queue.
EventHandlerResult Qukeys::onKeyswitchEvent(Key& key, KeyAddr k, uint8_t key_state) {
// If k is not a physical key, ignore it; some other plugin injected it.
if (! k.isValid() || (key_state & INJECTED) != 0) {
EventHandlerResult Qukeys::onKeyswitchEvent(KeyEvent &event) {
// If the plugin has already processed and released this event, ignore it.
// There's no need to update the event tracker explicitly.
if (event_tracker_.shouldIgnore(event)) {
// We should never get an event that's in our queue here, but just in case
// some other plugin sends one, abort.
if (event_queue_.shouldAbort(event))
return EventHandlerResult::ABORT;
return EventHandlerResult::OK;
}
// If the key was injected (from the queue being flushed), we need to ignore
// it.
if (flushing_queue_) {
// If event.addr is not a physical key, ignore it; some other plugin injected it.
if (! event.addr.isValid() || (event.state & INJECTED) != 0) {
return EventHandlerResult::OK;
}
// If Qukeys is turned off, continue to next plugin.
if (! active_) {
if (isDualUseKey(key)) {
key = queue_head_.primary_key;
if (isDualUseKey(event.key)) {
event.key = queue_head_.primary_key;
}
return EventHandlerResult::OK;
}
// Deal with keyswitch state changes.
if (keyToggledOn(key_state) || keyToggledOff(key_state)) {
// If the user rolled over from a non-modifier key to a qukey, let the
// release event for that key skip the queue. This prevents unintended
// repeat characters for the tapped key, which would otherwise have its
// release event delayed.
if (keyToggledOff(key_state) && event_queue_.length() == 1 &&
k != event_queue_.addr(0) && !isModifierKey(key)) {
return EventHandlerResult::OK;
}
// If we can't trivially ignore the event, just add it to the queue.
event_queue_.append(k, key_state);
// In order to prevent overflowing the queue, process it now.
if (event_queue_.isFull()) {
processQueue();
}
// Any event that gets added to the queue gets re-processed later, so we
// need to abort processing now.
return EventHandlerResult::EVENT_CONSUMED;
}
// 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;
}
}
// Either this key doesn't have an event in the queue at all, or its first
// event in the queue is a release. We treat the key as a normal held key.
return EventHandlerResult::OK;
// If we can't trivially ignore the event, just add it to the queue.
event_queue_.append(event);
// In order to prevent overflowing the queue, process it now.
while (processQueue());
// Any event that gets added to the queue gets re-processed later, so we
// need to abort processing now.
return EventHandlerResult::ABORT;
}
@ -103,27 +74,8 @@ EventHandlerResult Qukeys::onKeyswitchEvent(Key& key, KeyAddr k, uint8_t key_sta
// queue is ready to be flushed. It only allows one event to be flushed per
// cycle, because the keyboard HID report can't store all of the information
// necessary to correctly handle all of the rollover corner cases.
EventHandlerResult Qukeys::beforeReportingState() {
// For keys that have been physically released, but whose release events are
// still waiting to be flushed from the queue, we need to restore them,
// because `handleKeyswitchEvent()` didn't get called for those KeyAddrs.
for (uint8_t i{0}; i < event_queue_.length(); ++i) {
if (event_queue_.isRelease(i)) {
KeyAddr k = event_queue_.addr(i);
// Now for the tricky bit. Before "restoring" this key hold, we need to
// make sure that its key press event has already been flushed from the
// queue, so we need to search for a matching key press event preceding
// this release event. If we find one, we need to ignore it.
if (isKeyAddrInQueueBeforeIndex(k, i)) {
continue;
}
flushing_queue_ = true;
handleKeyswitchEvent(Key_NoKey, k, IS_PRESSED | WAS_PRESSED);
flushing_queue_ = false;
}
}
// Next, if there hasn't been a keypress in a while, update the prior keypress
EventHandlerResult Qukeys::afterEachCycle() {
// If there hasn't been a keypress in a while, update the prior keypress
// timestamp to avoid integer overflow issues:
if (Runtime.hasTimeExpired(prior_keypress_timestamp_,
minimum_prior_interval_)) {
@ -131,12 +83,14 @@ EventHandlerResult Qukeys::beforeReportingState() {
Runtime.millisAtCycleStart() - (minimum_prior_interval_ + 1);
}
// If any events get flushed from the queue, stop there; we can only safely
// send the one report per cycle.
if (processQueue()) {
// If there's nothing in the queue, there's nothing more to do.
if (event_queue_.isEmpty()) {
return EventHandlerResult::OK;
}
// Process as many events as we can from the queue.
while (processQueue());
// If we get here, that means that the first event in the queue is a qukey
// press. All that's left to do is to check if it's been held long enough that
// it has timed out.
@ -153,7 +107,6 @@ EventHandlerResult Qukeys::beforeReportingState() {
// -----------------------------------------------------------------------------
// 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
@ -162,11 +115,9 @@ EventHandlerResult Qukeys::beforeReportingState() {
// 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 there's nothing in the queue, abort.
if (event_queue_.isEmpty()) {
return true;
return false;
}
// In other cases, we will want the KeyAddr of the first event in the queue.
@ -174,13 +125,28 @@ bool Qukeys::processQueue() {
// If that first event is a key release, it can be flushed right away.
if (event_queue_.isRelease(0)) {
flushEvent(Key_NoKey);
return true;
// We can't unconditionally flush the release event, because it might be
// second half of a tap-repeat event. If the queue is full, we won't bother to
// check, but otherwise, ift `tap_repeat_.addr` is set (and matches), we call
// `shouldWaitForTapRepeat()` to determine whether or not to flush the key
// release event.
if (event_queue_.isFull() ||
queue_head_addr != tap_repeat_.addr ||
!shouldWaitForTapRepeat()) {
flushEvent(Key_NoKey);
return true;
}
// We now know that we're waiting to determine if we're getting a tap-repeat
// sequence, so we can't flush the release event at the head of the queue.
// Warning: Returning false here is only okay because we already checked to
// make sure the queue isn't full.
return false;
}
// 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.
// Should be able to remove the `active_` check once `deactivate()` gets updated
if (! isQukey(queue_head_addr) || ! active_) {
flushEvent(queue_head_.primary_key);
return true;
@ -198,8 +164,8 @@ bool Qukeys::processQueue() {
// key, so we don't need to do it repeatedly later.
bool qukey_is_spacecadet = isModifierKey(queue_head_.primary_key);
// If the qukey press is followed a printable key too closely, it's not
// eligible to take on its alternate value unless it's a SpaceCadet-type key.
// If the qukey press followed a printable key too closely, it's not eligible
// to take on its alternate value unless it's a SpaceCadet-type key.
if (!Runtime.hasTimeExpired(prior_keypress_timestamp_,
minimum_prior_interval_) &&
!qukey_is_spacecadet) {
@ -237,6 +203,13 @@ bool Qukeys::processQueue() {
if (next_keypress_index == 0 || overlap_threshold_ == 0) {
Key event_key = qukey_is_spacecadet ?
queue_head_.alternate_key : queue_head_.primary_key;
// A qukey just got released in primary state; this might turn out to be
// the beginning of a tap-repeat sequence, so we set the tap-repeat
// address and start time to the time of the initial press event before
// flushing it from the queue. This will come into play when processing
// the corresponding release event later.
tap_repeat_.addr = queue_head_addr;
tap_repeat_.start_time = event_queue_.timestamp(0);
flushEvent(event_key);
return true;
}
@ -303,6 +276,7 @@ void Qukeys::flushEvent(Key event_key) {
// First we record the address and state of the event:
KeyAddr queue_head_addr = event_queue_.addr(0);
uint8_t keyswitch_state = event_queue_.isRelease(0) ? WAS_PRESSED : IS_PRESSED;
KeyEvent event{queue_head_addr, keyswitch_state, event_key, event_queue_.id(0)};
// If the flushed event is a keypress of a printable symbol, record its
// timestamp. This lets us suppress some unintended alternate values seen by
@ -314,12 +288,11 @@ void Qukeys::flushEvent(Key event_key) {
prior_keypress_timestamp_ = event_queue_.timestamp(0);
}
// Remove the head event from the queue:
// Remove the head event from the queue, then call `handleKeyswitchEvent()` to
// resume processing of the event. It's important to remove the event from the
// queue first; otherwise `onKeyswitchEvent()` will abort it.
event_queue_.shift();
// This ensures that the flushed event will be ignored by the event handler hook:
flushing_queue_ = true;
handleKeyswitchEvent(event_key, queue_head_addr, keyswitch_state);
flushing_queue_ = false;
Runtime.handleKeyswitchEvent(event);
}
@ -327,11 +300,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 = Runtime.lookupKey(k);
// Next, we check to see if this is a DualUse-type qukey (defined in the keymap)
if (isDualUseKey(key)) {
@ -421,6 +393,100 @@ bool Qukeys::isKeyAddrInQueueBeforeIndex(KeyAddr k, uint8_t index) const {
}
// This question gets called early in `processQueue()` if a key release event is
// at the head of the queue, and the `tap_repeat_.addr` is the same as that
// event's KeyAddr. It returns true if `processQueue()` should wait for either
// subsequent events or a timeout instead of proceeding to flush the key release
// event immediately, and false if it is still waiting. It assumes that
// `event_queue_[0]` is a release event, and that `event_queue_[0].addr ==
// tap_repeat_.addr`. (The latter should only be set to a valid KeyAddr if a qukey
// press event has been flushed with its primary Key value, and could still
// represent the start of a double-tap or tap-repeat sequeunce.)
bool Qukeys::shouldWaitForTapRepeat() {
// First, we set up a variable to store the queue index of a subsequent press
// of the same qukey addr, if any.
uint8_t second_press_index = 0;
// Next, we search the event queue (starting at index 1 because the first
// event in the queue is known), trying to find a matching sequeunce for
// either a double-tap, or a tap-repeat.
for (uint8_t i{1}; i < event_queue_.length(); ++i) {
if (event_queue_.isPress(i)) {
// Found a keypress event following the release of the initial primary
// qukey.
if (event_queue_.addr(i) == tap_repeat_.addr) {
// The same qukey toggled on twice in a row, and because of the timeout
// check below, we know it was quick enough that it could represent a
// tap-repeat sequence. Now we update the start time (which had been set
// to the timestamp of the first press event) to the timestamp of the
// first release event (currently at the head of the queue), because we
// want to compare the release times of the two taps to determine if
// it's actually a double-tap sequence instead (otherwise it could be
// too difficult to tap it fast enough).
tap_repeat_.start_time = event_queue_.timestamp(0);
// We also record the index of this second press event. If it turns out
// that we've got a tap-repeat sequence, we want to silently suppress the
// first release and second press by removing them from the queue
// without flushing them. We don't know yet whether we'll be doing so.
second_press_index = i;
} else {
// Some other key was pressed. For it to be a tap-repeat sequence, we
// require that the same key be pressed twice in a row, with no
// intervening presses of other keys. Therefore, we can return false to
// signal that the release event at the head of the queue can be
// flushed.
return false;
}
} else if (event_queue_.addr(i) == tap_repeat_.addr) {
// We've found a key release event in the queue, and it's the same key as
// the qukey at the head of the queue, so this is the second release that
// has occurred before timing out (see below for the timeout
// check). Therefore, this is a double-tap sequence (the second release
// happened very close to the first release), not a tap-repeat, so we
// clear the tap-repeat address, and return false to trigger the flush of
// the first release event.
tap_repeat_.addr = KeyAddr{KeyAddr::invalid_state};
return false;
}
}
// We've now searched the queue. Either we found only irrelevant key release
// events (for other keys that were pressed before the sequence began), or we
// found only a second key press event of the same qukey (but not a double
// tap). Next, we check the timeout. If we didn't find a second press event,
// the start time will still be that of the initial key press event (already
// flushed from the queue), but if the second press has been detected, the
// start time will be that of the key release event currently at the head of
// the queue.
if (Runtime.hasTimeExpired(tap_repeat_.start_time, tap_repeat_.timeout)) {
// Time has expired. The sequence represents either a single tap or a
// tap-repeat of the qukey's primary value. Either way, we can clear the
// stored address.
tap_repeat_.addr = KeyAddr{KeyAddr::invalid_state};
if (second_press_index > 0) {
// A second press was found (but it's release didn't come quick enough to
// be a double tap), so this is a tap-repeat event. To turn it into a
// single key press and hold, we need to remove the second press event and
// the first release event from the queue without flushing the
// events. Order matters here!
event_queue_.remove(second_press_index);
event_queue_.remove(0);
} else {
// The key was not pressed again, so the single tap has timed out. We
// return false to let the release event be flushed.
return false;
}
}
// We haven't found a double-tap sequence, and the timeout hasn't expired, so
// we return true to signal that we should just wait until we get another
// event, or until time runs out.
return true;
}
// -----------------------------------------------------------------------------
// This function is here to provide the test for a SpaceCadet-type qukey, which

@ -1,6 +1,6 @@
/* -*- mode: c++ -*-
* Kaleidoscope-Qukeys -- Assign two keycodes to a single key
* Copyright (C) 2017-2019 Michael Richters
* Copyright (C) 2017-2020 Michael Richters
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -20,7 +20,8 @@
#include "kaleidoscope/Runtime.h"
#include <Kaleidoscope-Ranges.h>
#include "kaleidoscope/plugin/Qukeys/KeyAddrEventQueue.h"
#include "kaleidoscope/KeyAddrEventQueue.h"
#include "kaleidoscope/KeyEventTracker.h"
// DualUse Key definitions for Qukeys in the keymap
#define MT(mod, key) Key( \
@ -81,6 +82,17 @@ class Qukeys : public kaleidoscope::Plugin {
hold_timeout_ = hold_timeout;
}
// Set the timeout (in milliseconds) for the tap-repeat feature. If a qukey is
// tapped twice in a row in less time than this amount, it will allow the user
// to hold the key with its primary value (unless it's a SpaceCadet type key,
// in which case it will repeat the alternate value instead). This requires a
// quick tap immediately followed by a press and hold, and will result in a
// single press-and-hold on the host system. If a double tap is done instead,
// it will result in two independent taps.
void setMaxIntervalForTapRepeat(uint8_t ttl) {
tap_repeat_.timeout = ttl;
}
// 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
@ -128,10 +140,8 @@ class Qukeys : public kaleidoscope::Plugin {
// Kaleidoscope hook functions.
EventHandlerResult onNameQuery();
EventHandlerResult onKeyswitchEvent(Key &mapped_key,
KeyAddr key_addr,
uint8_t key_state);
EventHandlerResult beforeReportingState();
EventHandlerResult onKeyswitchEvent(KeyEvent &event);
EventHandlerResult afterEachCycle();
private:
// An array of Qukey objects in PROGMEM.
@ -174,7 +184,7 @@ class Qukeys : public kaleidoscope::Plugin {
// 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};
KeyEventTracker event_tracker_;
// A cache of the current qukey's primary and alternate key values, so we
// don't have to keep looking them up from PROGMEM.
@ -190,6 +200,14 @@ class Qukeys : public kaleidoscope::Plugin {
bool isDualUseKey(Key key);
bool releaseDelayed(uint16_t overlap_start, uint16_t overlap_end) const;
bool isKeyAddrInQueueBeforeIndex(KeyAddr k, uint8_t index) const;
// Tap-repeat feature support.
struct {
KeyAddr addr{KeyAddr::invalid_state};
uint16_t start_time;
uint8_t timeout{200};
} tap_repeat_;
bool shouldWaitForTapRepeat();
};
// This function returns true for any key that we expect to be used chorded with

@ -17,6 +17,8 @@
#pragma once
#include <stdint.h> // for uint16_t
// Included for definition of legacy Macros plugin key range:
#include "kaleidoscope/key_defs.h"
@ -78,6 +80,9 @@ enum : uint16_t {
TURBO,
DYNAMIC_MACRO_FIRST,
DYNAMIC_MACRO_LAST = DYNAMIC_MACRO_FIRST + 31,
OS_META_STICKY,
OS_ACTIVE_STICKY,
OS_CANCEL,
SAFE_START,
KALEIDOSCOPE_SAFE_START = SAFE_START

@ -22,31 +22,20 @@
namespace kaleidoscope {
namespace plugin {
Key Redial::key_to_redial_;
Key Redial::last_key_;
bool Redial::redial_held_ = false;
EventHandlerResult Redial::onNameQuery() {
return ::Focus.sendName(F("Redial"));
}
EventHandlerResult Redial::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
if (mapped_key == Key_Redial) {
if (keyToggledOff(key_state))
key_to_redial_ = last_key_;
mapped_key = key_to_redial_;
redial_held_ = keyIsPressed(key_state);
return EventHandlerResult::OK;
EventHandlerResult Redial::onKeyEvent(KeyEvent &event) {
if (keyToggledOn(event.state)) {
if (event.key == Key_Redial) {
event.key = last_key_;
} else if (shouldRemember(event.key)) {
last_key_ = event.key;
}
}
if (keyToggledOn(key_state) && shouldRemember(mapped_key)) {
last_key_ = mapped_key;
if (!redial_held_)
key_to_redial_ = mapped_key;
}
return EventHandlerResult::OK;
}

@ -32,12 +32,10 @@ class Redial : public kaleidoscope::Plugin {
static bool shouldRemember(Key mappedKey);
EventHandlerResult onNameQuery();
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
private:
static Key key_to_redial_;
static Key last_key_;
static bool redial_held_;
};
}

@ -16,25 +16,19 @@
*/
#include <Kaleidoscope-ShapeShifter.h>
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/LiveKeys.h"
namespace kaleidoscope {
namespace plugin {
const ShapeShifter::dictionary_t *ShapeShifter::dictionary = NULL;
bool ShapeShifter::mod_active_;
const ShapeShifter::dictionary_t *ShapeShifter::dictionary = nullptr;
EventHandlerResult ShapeShifter::beforeReportingState() {
mod_active_ = kaleidoscope::Runtime.hid().keyboard().isModifierKeyActive(Key_LeftShift) ||
kaleidoscope::Runtime.hid().keyboard().isModifierKeyActive(Key_RightShift);
return EventHandlerResult::OK;
}
EventHandlerResult ShapeShifter::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
if (!dictionary)
EventHandlerResult ShapeShifter::onKeyEvent(KeyEvent &event) {
if (dictionary == nullptr)
return EventHandlerResult::OK;
// If Shift is not active, bail out early.
if (!mod_active_)
if (!dictionary)
return EventHandlerResult::OK;
Key orig, repl;
@ -45,17 +39,26 @@ EventHandlerResult ShapeShifter::onKeyswitchEvent(Key &mapped_key, KeyAddr key_a
orig = dictionary[i].original.readFromProgmem();
i++;
} while (orig != Key_NoKey &&
orig != mapped_key);
orig != event.key);
i--;
// If not found, bail out.
if (orig == Key_NoKey)
return EventHandlerResult::OK;
bool shift_detected = false;
for (KeyAddr k : KeyAddr::all()) {
if (live_keys[k].isKeyboardShift())
shift_detected = true;
}
if (! shift_detected)
return EventHandlerResult::OK;
repl = dictionary[i].replacement.readFromProgmem();
// If found, handle the alternate key instead
mapped_key = repl;
event.key = repl;
return EventHandlerResult::OK;
}

@ -32,11 +32,7 @@ class ShapeShifter : public kaleidoscope::Plugin {
static const dictionary_t *dictionary;
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult beforeReportingState();
private:
static bool mod_active_;
EventHandlerResult onKeyEvent(KeyEvent &event);
};
}

@ -21,261 +21,203 @@
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/key_events.h"
//#include <Kaleidoscope-Devel-ArduinoTrace.h>
namespace kaleidoscope {
namespace plugin {
//Constructor with input and output, and assume default timeout
SpaceCadet::KeyBinding::KeyBinding(Key input_, Key output_) {
input = input_;
output = output_;
}
// Constructor with input and output, and assume default timeout
SpaceCadet::KeyBinding::KeyBinding(Key input, Key output)
: input(input), output(output) {}
//Constructor with all three set
SpaceCadet::KeyBinding::KeyBinding(Key input_, Key output_, uint16_t timeout_) {
input = input_;
output = output_;
timeout = timeout_;
}
// Constructor with all three set
SpaceCadet::KeyBinding::KeyBinding(Key input, Key output, uint16_t timeout)
: input(input), output(output), timeout(timeout) {}
// =============================================================================
// Space Cadet class variables
// -----------------------------------------------------------------------------
// Plugin configuration variables
//Space Cadet
SpaceCadet::KeyBinding * SpaceCadet::map;
uint16_t SpaceCadet::time_out = 1000;
uint16_t SpaceCadet::time_out = 200;
// -----------------------------------------------------------------------------
// State variables
bool SpaceCadet::disabled = false;
// These variables are used to keep track of any pending unresolved SpaceCadet
// key that has been pressed. If `pending_map_index_` is negative, it means
// there is no such pending keypress. Otherwise, it holds the value of the index
// of that key in the array.
int8_t SpaceCadet::pending_map_index_ = -1;
KeyEventTracker SpaceCadet::event_tracker_;
// =============================================================================
// SpaceCadet functions
// Constructor
SpaceCadet::SpaceCadet() {
static SpaceCadet::KeyBinding initialmap[] = {
//By default, respect the default timeout
{Key_LeftShift, Key_LeftParen, 0}
, {Key_RightShift, Key_RightParen, 0}
//These may be uncommented, added, or set in the main sketch
/*,{Key_LeftGui,Key_LeftCurlyBracket, 250}
,{Key_RightAlt,Key_RightCurlyBracket, 250}
,{Key_LeftControl,Key_LeftBracket, 250}
,{Key_RightControl,Key_RightBracket, 250}*/
, SPACECADET_MAP_END
// By default, respect the default timeout
{Key_LeftShift, Key_LeftParen, 0},
{Key_RightShift, Key_RightParen, 0},
// These may be uncommented, added, or set in the main sketch
/*
{Key_LeftGui, Key_LeftCurlyBracket, 250},
{Key_RightAlt, Key_RightCurlyBracket, 250},
{Key_LeftControl, Key_LeftBracket, 250},
{Key_RightControl, Key_RightBracket, 250},
*/
SPACECADET_MAP_END
};
map = initialmap;
}
//Function to enable SpaceCadet behavior
// -----------------------------------------------------------------------------
// Function to determine whether SpaceCadet is active (useful for Macros and
// other plugins).
bool SpaceCadet::active() {
return !disabled;
}
// Function to enable SpaceCadet behavior
void SpaceCadet::enable() {
disabled = false;
}
//Function to disable SpaceCadet behavior
// Function to disable SpaceCadet behavior
void SpaceCadet::disable() {
disabled = true;
}
//Function to determine whether SpaceCadet is active (useful for Macros and other plugins)
bool SpaceCadet::active() {
return !disabled;
}
// =============================================================================
// Event handler hook functions
// -----------------------------------------------------------------------------
EventHandlerResult SpaceCadet::onNameQuery() {
return ::Focus.sendName(F("SpaceCadet"));
}
EventHandlerResult SpaceCadet::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
//Handle our synthetic keys for enabling and disabling functionality
if (mapped_key.getRaw() >= kaleidoscope::ranges::SC_FIRST &&
mapped_key.getRaw() <= kaleidoscope::ranges::SC_LAST) {
//Only fire the activate / deactivate on the initial press (not held or release)
if (keyToggledOn(key_state)) {
if (mapped_key == Key_SpaceCadetEnable) {
enable();
} else if (mapped_key == Key_SpaceCadetDisable) {
disable();
}
}
return EventHandlerResult::EVENT_CONSUMED;
// -----------------------------------------------------------------------------
EventHandlerResult SpaceCadet::onKeyswitchEvent(KeyEvent &event) {
// If SpaceCadet has already processed and released this event, ignore
// it. There's no need to update the event tracker in this case.
if (event_tracker_.shouldIgnore(event)) {
// We should never get an event that's in our queue here, but just in case
// some other plugin sends one, abort.
if (event_queue_.shouldAbort(event))
return EventHandlerResult::ABORT;
return EventHandlerResult::OK;
}
//if SpaceCadet is disabled, this was an injected key, it was NoKey,
//or if they key somehow came here without being either pressed or released,
//return the mapped key and just get out of here.
if (
disabled
|| (key_state & INJECTED)
|| mapped_key == Key_NoKey
|| (!keyIsPressed(key_state) && !keyWasPressed(key_state))
) {
// If event.addr is not a physical key, ignore it; some other plugin injected
// it. This check should be unnecessary.
if (!event.addr.isValid() || (event.state & INJECTED) != 0) {
return EventHandlerResult::OK;
}
// If a key has been just toggled on...
if (keyToggledOn(key_state)) {
//check to see if we found a valid key. Assume not valid.
bool valid_key = false;
bool other_mapped_key_flagged = false;
//Check the current map to see if any other key has been already flagged
//Exit condition is if we reach the special SPACECADET_MAP_END sentinel
for (
uint8_t i = 0 ;
!(
map[i].input == Key_NoKey
&& map[i].output == Key_NoKey
&& map[i].timeout == 0
) ;
++i
) {
if (map[i].flagged
&& map[i].input != mapped_key) {
other_mapped_key_flagged = true;
break;
}
}
//This will only set one key, and, if it isn't in our map, it clears everything for the non-pressed key
//Exit condition is if we reach the special SPACECADET_MAP_END sentinel
for (
uint8_t i = 0 ;
!(
map[i].input == Key_NoKey
&& map[i].output == Key_NoKey
&& map[i].timeout == 0
) ;
++i
) {
if (mapped_key == map[i].input) {
//Only activate this as part of the mapping if there isn't already a
//key waiting for timeout. This allows us to return OK later and for
//this loop to inject all the other flagged keys
if (!other_mapped_key_flagged) {
//The keypress was valid and a match. Mark it as flagged and reset the counter
map[i].flagged = true;
map[i].start_time = Runtime.millisAtCycleStart();
//yes, we found a valid key
valid_key = true;
}
} else {
//If the key entry we're looking at was flagged previously, add it to the
//report before we do anything else (this handles the situation where we
//hit another key after this -- if it's a modifier, we want the modifier
//key to be added to the report, for things like ctrl, alt, shift, etc)
if (map[i].flagged) {
handleKeyswitchEvent(map[i].input, UnknownKeyswitchLocation, IS_PRESSED | INJECTED);
}
//The keypress wasn't a match, so we need to mark it as not flagged and
//reset the timer for it to disable everything.
map[i].flagged = false;
map[i].start_time = 0;
}
// Turn SpaceCadet on or off.
if (keyToggledOn(event.state)) {
if (event.key == Key_SpaceCadetEnable) {
enable();
return EventHandlerResult::EVENT_CONSUMED;
}
//If we found a valid key in our map, we don't actually want to send anything.
//This gets around an issue in Windows if we map a SpaceCadet function on top
//of Alt -- sending Alt by itself activates the menubar. We don't want to send
//anything until we know that we're either sending the alternate key or we
//know for sure that we want to send the originally pressed key.
if (valid_key) {
if (event.key == Key_SpaceCadetDisable) {
disable();
return EventHandlerResult::EVENT_CONSUMED;
}
//this is all we need to do on keypress, let the next handler do its thing too.
//This case assumes we weren't a valid key that we were watching, so we don't
//need to do anything else.
return EventHandlerResult::OK;
}
// if the state is empty, that means that either an activating key wasn't pressed,
// or we used another key in the interim. in both cases, nothing special to do.
bool valid_key = false;
bool pressed_key_was_valid = false;
uint8_t index = 0;
//Look to see if any keys in our map are currently flagged.
//Exit condition is if we reach the special SPACECADET_MAP_END sentinel
for (
uint8_t i = 0 ;
!(
map[i].input == Key_NoKey
&& map[i].output == Key_NoKey
&& map[i].timeout == 0
);
++i
) {
//The key we're looking at was previously flagged (so perform action)
if (map[i].flagged) {
valid_key = true;
index = i;
}
// Do nothing if disabled, but keep the event tracker current.
if (disabled)
return EventHandlerResult::OK;
//the key we're looking at was valid (in the map)
if (map[i].input == mapped_key) {
pressed_key_was_valid = true;
if (!event_queue_.isEmpty()) {
// There's an unresolved SpaceCadet key press.
if (keyToggledOff(event.state)) {
if (event.addr == event_queue_.addr(0)) {
// SpaceCadet key released before timing out; send the event with the
// SpaceCadet key's alternate `Key` value before flushing the rest of
// the queue (see below).
flushEvent(true);
} else if (!event_queue_.isFull()) {
// Queue not full; add event and abort
event_queue_.append(event);
return EventHandlerResult::ABORT;
}
}
// Either a new key was pressed, or the SpaceCadet key was released and has
// been flushed (see above), or the queue is full and is about to overflow.
// In all cases, we fulsh the whole queue now.
flushQueue();
}
//If no valid mapped keys were pressed, simply return the key that
//was originally passed in.
if (!valid_key) {
return EventHandlerResult::OK;
}
//use the map index to find the local timeout for this key
uint16_t current_timeout = map[index].timeout;
//If that isn't set, use the global timeout setting.
if (current_timeout == 0) {
current_timeout = time_out;
// Event queue is now empty
if (keyToggledOn(event.state)) {
// Check for a SpaceCadet key
pending_map_index_ = getSpaceCadetKeyIndex(event.key);
if (pending_map_index_ >= 0) {
// A SpaceCadet key was pressed
event_queue_.append(event);
return EventHandlerResult::ABORT;
}
}
//Check to determine if we have surpassed our timeout for holding this key
if (Runtime.hasTimeExpired(map[index].start_time, current_timeout)) {
// if we timed out, that means we need to keep pressing the mapped
// key, but we won't need to send the alternative key in the end
map[index].flagged = false;
map[index].start_time = 0;
//Just return this key itself (we won't run alternative keys check)
return EventHandlerResult::OK;
}
return EventHandlerResult::OK;
}
// If the key that was pressed isn't one of our mapped keys, just
// return. This can happen when another key is released, and that should not
// interrupt us.
if (!pressed_key_was_valid) {
// -----------------------------------------------------------------------------
EventHandlerResult SpaceCadet::afterEachCycle() {
// If there's no pending event, return.
if (event_queue_.isEmpty())
return EventHandlerResult::OK;
}
// if a key toggled off (and that must be one of the mapped keys at this point),
// send the alternative key instead (if we were interrupted, we bailed out earlier).
if (keyToggledOff(key_state)) {
Key alternate_key = map[index].output;
// Get timeout value for the pending key.
uint16_t pending_timeout = time_out;
if (map[pending_map_index_].timeout != 0)
pending_timeout = map[pending_map_index_].timeout;
uint16_t start_time = event_queue_.timestamp(0);
//Since we are sending the actual key (no need for shift, etc),
//only need to send that key and not the original key.
if (Runtime.hasTimeExpired(start_time, pending_timeout)) {
// The timer has expired; release the pending event unchanged.
flushQueue();
}
return EventHandlerResult::OK;
}
//inject our new key
handleKeyswitchEvent(alternate_key, key_addr, IS_PRESSED | INJECTED);
// =============================================================================
// Private helper function(s)
//Unflag the key so we don't try this again.
map[index].flagged = false;
map[index].start_time = 0;
int8_t SpaceCadet::getSpaceCadetKeyIndex(Key key) const {
for (uint8_t i = 0; !map[i].isEmpty(); ++i) {
if (map[i].input == key) {
return i;
}
}
return -1;
}
//Special case here for if we had a valid key that's continuing to be held.
//If it's a valid key, and it's continuing to be held, return NoKey.
//This prevents us from accidentally triggering a keypress that we didn't
//mean to handle.
if (valid_key) {
return EventHandlerResult::EVENT_CONSUMED;
void SpaceCadet::flushQueue() {
while (!event_queue_.isEmpty()) {
flushEvent(false);
}
//Finally, as a final sanity check, simply return the passed-in key as-is.
return EventHandlerResult::OK;
}
void SpaceCadet::flushEvent(bool is_tap) {
KeyEvent event = event_queue_.event(0);
if (is_tap && pending_map_index_ >= 0) {
event.key = map[pending_map_index_].output;
}
event_queue_.shift();
Runtime.handleKeyswitchEvent(event);
}
}
} // namespace plugin
} // namespace kaleidoscope
kaleidoscope::plugin::SpaceCadet SpaceCadet;

@ -19,6 +19,8 @@
#pragma once
#include "kaleidoscope/Runtime.h"
#include "kaleidoscope/KeyEventTracker.h"
#include "kaleidoscope/KeyAddrEventQueue.h"
#include <Kaleidoscope-Ranges.h>
#ifndef SPACECADET_MAP_END
@ -33,44 +35,60 @@ namespace plugin {
class SpaceCadet : public kaleidoscope::Plugin {
public:
//Internal Class
//Declarations for the modifier key mapping
// Internal Class
// Declarations for the modifier key mapping
class KeyBinding {
public:
//Empty constructor; set the vars separately
KeyBinding(void) {}
//Constructor with input and output
KeyBinding(Key input_, Key output_);
//Constructor with all three set
KeyBinding(Key input_, Key output_, uint16_t timeout_);
//The key that is pressed
// Empty constructor; set the vars separately
KeyBinding() {}
// Constructor with input and output
KeyBinding(Key input, Key output);
// Constructor with all three set
KeyBinding(Key input, Key output, uint16_t timeout);
// The key that is pressed
Key input;
//the key that is sent
// the key that is sent
Key output;
//The timeout (default to global timeout)
// The timeout (default to global timeout)
uint16_t timeout = 0;
//The flag (set to 0)
bool flagged = false;
//the start time for this key press
uint16_t start_time = 0;
// to check for the end of a list (SPACECADET_MAP_END)
bool isEmpty() const {
return (input == Key_NoKey && output == Key_NoKey && timeout == 0);
}
};
SpaceCadet(void);
//Methods
// Methods
static void enable(void);
static void disable(void);
static bool active(void);
//Publically accessible variables
// Publically accessible variables
static uint16_t time_out; // The global timeout in milliseconds
static SpaceCadet::KeyBinding * map; // The map of key bindings
EventHandlerResult onNameQuery();
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyswitchEvent(KeyEvent &event);
EventHandlerResult afterEachCycle();
private:
static bool disabled;
static KeyEventTracker event_tracker_;
// The maximum number of events in the queue at a time.
static constexpr uint8_t queue_capacity_{4};
// The event queue stores a series of press and release events.
KeyAddrEventQueue<queue_capacity_> event_queue_;
static int8_t pending_map_index_;
int8_t getSpaceCadetKeyIndex(Key key) const;
void flushEvent(bool is_tap = false);
void flushQueue();
};
}

@ -30,17 +30,16 @@ EventHandlerResult GeminiPR::onNameQuery() {
return ::Focus.sendName(F("GeminiPR"));
}
EventHandlerResult GeminiPR::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState) {
if (mapped_key < geminipr::START ||
mapped_key > geminipr::END)
EventHandlerResult GeminiPR::onKeyEvent(KeyEvent &event) {
if (event.key < geminipr::START || event.key > geminipr::END)
return EventHandlerResult::OK;
if (keyToggledOn(keyState)) {
uint8_t key = mapped_key.getRaw() - geminipr::START;
if (keyToggledOn(event.state)) {
uint8_t key = event.key.getRaw() - geminipr::START;
++keys_held_;
state_[key / 7] |= 1 << (6 - (key % 7));
} else if (keyToggledOff(keyState)) {
} else {
--keys_held_;
if (keys_held_ == 0) {

@ -30,7 +30,8 @@ class GeminiPR : public kaleidoscope::Plugin {
GeminiPR(void) {}
EventHandlerResult onNameQuery();
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState);
EventHandlerResult onKeyEvent(KeyEvent &event);
private:
static uint8_t keys_held_;
static uint8_t state_[6];
@ -88,9 +89,9 @@ enum {
ZR,
END = ZR,
};
}
}
}
}
} // namespace geminipr
} // namespace steno
} // namespace plugin
} // namespace kaleidoscope
extern kaleidoscope::plugin::steno::GeminiPR GeminiPR;

@ -49,54 +49,81 @@ EventHandlerResult Syster::onNameQuery() {
return ::Focus.sendName(F("Syster"));
}
EventHandlerResult Syster::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState) {
EventHandlerResult Syster::onKeyEvent(KeyEvent &event) {
if (!is_active_) {
if (!isSyster(mapped_key))
// If Syster isn't actively matching an input sequence, we're only looking
// for the special Syster `Key` value; anything else gets passed through
// immediately.
if (!isSyster(event.key))
return EventHandlerResult::OK;
if (keyToggledOn(keyState)) {
// It's a Syster Key; activate the plugin as soon as it toggles on, so we
// process rollover correctly.
if (keyToggledOn(event.state)) {
is_active_ = true;
systerAction(StartAction, NULL);
systerAction(StartAction, nullptr);
}
return EventHandlerResult::EVENT_CONSUMED;
}
if (keyState & INJECTED)
// Always ignore events marked as artificially injected (it might actually be
// better to drop this, but it's not really clear).
if (event.state & INJECTED)
return EventHandlerResult::OK;
if (isSyster(mapped_key)) {
// If a Syster key gets pressed while we're reading an input sequence, ignore
// it. This could be turned into a "reset" where we erase the abandoned input.
if (isSyster(event.key)) {
return EventHandlerResult::EVENT_CONSUMED;
}
if (mapped_key == Key_Backspace && symbol_pos_ == 0) {
return EventHandlerResult::EVENT_CONSUMED;
// If the user presses a backspace key while at the beginning of the input
// string, suppress it to prevent erasing past the start of the sequence.
// Again, this could be changed to do a reset.
if (event.key == Key_Backspace && symbol_pos_ == 0) {
return EventHandlerResult::ABORT;
}
if (keyToggledOff(keyState)) {
if (mapped_key == Key_Spacebar) {
for (uint8_t i = 0; i <= symbol_pos_; i++) {
handleKeyswitchEvent(Key_Backspace, UnknownKeyswitchLocation, IS_PRESSED | INJECTED);
kaleidoscope::Runtime.hid().keyboard().sendReport();
handleKeyswitchEvent(Key_Backspace, UnknownKeyswitchLocation, WAS_PRESSED | INJECTED);
kaleidoscope::Runtime.hid().keyboard().sendReport();
}
systerAction(EndAction, NULL);
// We only pay attention to key press events while parsing input. If the user
// holds a key down long enough to trigger repeating characters in the OS,
// we'll end up erasing fewer characters than we should. This could be
// addressed by immediately sending the corresponding release event, but
// that's probably too much trouble to be worthwhile.
if (keyToggledOn(event.state)) {
// Pressing the spacebar ends the input sequence.
if (event.key == Key_Spacebar) {
// First, we erase all the typed characters in the symbol sequence.
eraseChars(symbol_pos_);
// Next, we call the user-defined end action.
systerAction(EndAction, nullptr);
// Then we null-terminate the `symbol_` string, and call the user-defined
// symbol action.
symbol_[symbol_pos_] = 0;
systerAction(SymbolAction, symbol_);
// Finally, we're done, so we reset, deactivating Syster until the next
// press of a Syster key.
reset();
return EventHandlerResult::EVENT_CONSUMED;
}
}
// Returning ABORT here stops the spacebar from activating. Alternatively,
// we could remove this return statement, and instead allow the spacebar
// to take effect, resulting in a space in the output, which would follow
// any symbols produced by the symbol action.
return EventHandlerResult::ABORT;
if (keyToggledOn(keyState)) {
if (mapped_key == Key_Backspace) {
} else if (event.key == Key_Backspace) {
// If the user erases any typos, we keep the Syster symbol string in sync
// with what's on the screen. Again, this doesn't account for a key held
// long enough to trigger repeat in the OS.
if (symbol_pos_ > 0)
symbol_pos_--;
--symbol_pos_;
} else {
const char c = keyToChar(mapped_key);
// An ordinary keypress, with Syster active. We add its corresponding
// character to the symbol string.
const char c = keyToChar(event.key);
if (c)
symbol_[symbol_pos_++] = c;
}
@ -105,8 +132,35 @@ EventHandlerResult Syster::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, u
return EventHandlerResult::OK;
}
} // namespace plugin
void eraseChars(int8_t n) {
// For the `event.addr`, we could track the address of the Syster key, but it
// might be on a layer that's no longer active by the time this gets
// called. We could search the active keymap for an existing `Key_Backspace`,
// but there might not be one. We could hijack the first idle key we find in
// the keymap, but we probably don't need to. Even if some other plugin reacts
// by inserting another event between these two, it's very unlikely that will
// cause a user-visible error.
auto event = KeyEvent(KeyAddr::none(), INJECTED, Key_Backspace);
while (n > 0) {
event.state = IS_PRESSED | INJECTED;
Runtime.handleKeyEvent(event);
event.state = WAS_PRESSED | INJECTED;
Runtime.handleKeyEvent(event);
--n;
}
Runtime.handleKeyEvent(event);
// Change the event from a press to a release, but use the same id. This does
// come with a small risk that another plugin will be tracking events, but
// also ignoring event ids that it has seen before, but it's more likely to
// avoid an error than to cause one.
event.state = WAS_PRESSED | INJECTED;
Runtime.handleKeyEvent(event);
}
}
} // namespace kaleidoscope
__attribute__((weak)) const char keyToChar(Key key) {
if (key.getFlags() != 0)

@ -35,25 +35,29 @@ class Syster : public kaleidoscope::Plugin {
SymbolAction
} action_t;
Syster(void) {}
Syster() {}
static void reset(void);
static void reset();
bool is_active(void);
bool is_active();
EventHandlerResult onNameQuery();
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState);
EventHandlerResult onKeyEvent(KeyEvent &event);
private:
static char symbol_[SYSTER_MAX_SYMBOL_LENGTH + 1];
static uint8_t symbol_pos_;
static bool is_active_;
};
}
}
} // namespace plugin
void eraseChars(int8_t n);
} // namespace kaleidoscope
const char keyToChar(Key key);
void systerAction(kaleidoscope::plugin::Syster::action_t action, const char *symbol);
extern kaleidoscope::plugin::Syster Syster;

@ -18,89 +18,50 @@
#include <Kaleidoscope-TapDance.h>
#include <Kaleidoscope-FocusSerial.h>
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/layers.h"
#include "kaleidoscope/KeyEventTracker.h"
namespace kaleidoscope {
namespace plugin {
// --- state ---
uint16_t TapDance::start_time_;
uint16_t TapDance::time_out = 200;
TapDance::TapDanceState TapDance::state_[TapDance::TAPDANCE_KEY_COUNT];
Key TapDance::last_tap_dance_key_ = Key_NoKey;
KeyAddr TapDance::last_tap_dance_addr_;
// --- actions ---
void TapDance::interrupt(KeyAddr key_addr) {
uint8_t idx = last_tap_dance_key_.getRaw() - ranges::TD_FIRST;
tapDanceAction(idx, last_tap_dance_addr_, state_[idx].count, Interrupt);
state_[idx].triggered = true;
// --- config ---
last_tap_dance_key_ = Key_NoKey;
Runtime.hid().keyboard().sendReport();
Runtime.hid().keyboard().releaseAllKeys();
if (state_[idx].pressed)
return;
release(idx);
}
void TapDance::timeout(void) {
uint8_t idx = last_tap_dance_key_.getRaw() - ranges::TD_FIRST;
uint16_t TapDance::time_out = 200;
uint8_t TapDance::tap_count_ = 0;
tapDanceAction(idx, last_tap_dance_addr_, state_[idx].count, Timeout);
state_[idx].triggered = true;
KeyEventTracker TapDance::event_tracker_;
if (state_[idx].pressed)
// --- api ---
void TapDance::actionKeys(uint8_t tap_count,
ActionType action,
uint8_t max_keys,
const Key tap_keys[]) {
if (event_queue_.isEmpty())
return;
last_tap_dance_key_ = Key_NoKey;
release(idx);
}
void TapDance::release(uint8_t tap_dance_index) {
last_tap_dance_key_ = Key_NoKey;
state_[tap_dance_index].pressed = false;
state_[tap_dance_index].triggered = false;
state_[tap_dance_index].release_next = true;
}
void TapDance::tap(void) {
uint8_t idx = last_tap_dance_key_.getRaw() - ranges::TD_FIRST;
if (tap_count > max_keys)
tap_count = max_keys;
state_[idx].count++;
start_time_ = Runtime.millisAtCycleStart();
KeyEvent event = event_queue_.event(0);
event.key = tap_keys[tap_count - 1].readFromProgmem();
tapDanceAction(idx, last_tap_dance_addr_, state_[idx].count, Tap);
if (action == Interrupt || action == Timeout) {
event_queue_.shift();
Runtime.handleKeyswitchEvent(event);
} else if (action == Tap && tap_count == max_keys) {
tap_count_ = 0;
event_queue_.clear();
Runtime.handleKeyswitchEvent(event);
}
}
// --- 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 = tap_keys[tap_count - 1].readFromProgmem();
switch (tap_dance_action) {
case Tap:
break;
case Interrupt:
case Timeout:
handleKeyswitchEvent(key, last_tap_dance_addr_, IS_PRESSED | INJECTED);
break;
case Hold:
handleKeyswitchEvent(key, last_tap_dance_addr_, IS_PRESSED | WAS_PRESSED | INJECTED);
break;
case Release:
kaleidoscope::Runtime.hid().keyboard().sendReport();
handleKeyswitchEvent(key, last_tap_dance_addr_, WAS_PRESSED | INJECTED);
break;
void TapDance::flushQueue(KeyAddr ignored_addr) {
while (! event_queue_.isEmpty()) {
KeyEvent queued_event = event_queue_.event(0);
event_queue_.shift();
if (queued_event.addr != ignored_addr)
Runtime.handleKeyswitchEvent(queued_event);
}
}
@ -110,99 +71,80 @@ EventHandlerResult TapDance::onNameQuery() {
return ::Focus.sendName(F("TapDance"));
}
EventHandlerResult TapDance::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState) {
if (keyState & INJECTED)
return EventHandlerResult::OK;
if (mapped_key.getRaw() < ranges::TD_FIRST || mapped_key.getRaw() > ranges::TD_LAST) {
if (last_tap_dance_key_ == Key_NoKey)
return EventHandlerResult::OK;
if (keyToggledOn(keyState)) {
interrupt(key_addr);
mapped_key = Key_NoKey;
}
EventHandlerResult TapDance::onKeyswitchEvent(KeyEvent &event) {
// If the plugin has already processed and released this event, ignore it.
// There's no need to update the event tracker explicitly.
if (event_tracker_.shouldIgnore(event)) {
// We should never get an event that's in our queue here, but just in case
// some other plugin sends one, abort.
if (event_queue_.shouldAbort(event))
return EventHandlerResult::ABORT;
return EventHandlerResult::OK;
}
uint8_t tap_dance_index = mapped_key.getRaw() - ranges::TD_FIRST;
if (keyToggledOff(keyState))
state_[tap_dance_index].pressed = false;
if (last_tap_dance_key_ != mapped_key) {
if (last_tap_dance_key_ == Key_NoKey) {
if (state_[tap_dance_index].triggered) {
if (keyToggledOff(keyState)) {
release(tap_dance_index);
}
return EventHandlerResult::EVENT_CONSUMED;
}
last_tap_dance_key_ = mapped_key;
last_tap_dance_addr_ = key_addr;
tap();
return EventHandlerResult::EVENT_CONSUMED;
} else {
if (keyToggledOff(keyState) && state_[tap_dance_index].count) {
release(tap_dance_index);
return EventHandlerResult::EVENT_CONSUMED;
}
if (!keyToggledOn(keyState)) {
return EventHandlerResult::EVENT_CONSUMED;
}
interrupt(key_addr);
}
// If event.addr is not a physical key, ignore it; some other plugin injected it.
if (! event.addr.isValid() || (event.state & INJECTED) != 0) {
return EventHandlerResult::OK;
}
// in sequence
if (keyToggledOff(keyState)) {
return EventHandlerResult::EVENT_CONSUMED;
if (keyToggledOff(event.state)) {
if (event_queue_.isEmpty())
return EventHandlerResult::OK;
event_queue_.append(event);
return EventHandlerResult::ABORT;
}
last_tap_dance_key_ = mapped_key;
last_tap_dance_addr_ = key_addr;
state_[tap_dance_index].pressed = true;
if (event_queue_.isEmpty() && !isTapDanceKey(event.key))
return EventHandlerResult::OK;
if (keyToggledOn(keyState)) {
tap();
return EventHandlerResult::EVENT_CONSUMED;
KeyAddr td_addr = event_queue_.addr(0);
Key td_key = Layer.lookupOnActiveLayer(td_addr);
uint8_t td_id = td_key.getRaw() - ranges::TD_FIRST;
if (! event_queue_.isEmpty() &&
event.addr != event_queue_.addr(0)) {
// Interrupt: Call `tapDanceAction()` first, so it will have access to the
// TapDance key press event that needs to be sent, then flush the queue.
tapDanceAction(td_id, td_addr, tap_count_, Interrupt);
flushQueue();
tap_count_ = 0;
// If the event isn't another TapDance key, let it proceed. If it is, fall
// through to the next block, which handles "Tap" actions.
if (! isTapDanceKey(event.key))
return EventHandlerResult::OK;
}
if (state_[tap_dance_index].triggered)
tapDanceAction(tap_dance_index, key_addr, state_[tap_dance_index].count, Hold);
return EventHandlerResult::EVENT_CONSUMED;
// Tap: First flush the queue, ignoring the previous press and release events
// for the TapDance key, then add the new tap to the queue (it becomes the
// first entry).
flushQueue(event.addr);
event_queue_.append(event);
tapDanceAction(td_id, td_addr, ++tap_count_, Tap);
return EventHandlerResult::ABORT;
}
EventHandlerResult TapDance::afterEachCycle() {
for (uint8_t i = 0; i < TAPDANCE_KEY_COUNT; i++) {
if (!state_[i].release_next)
continue;
tapDanceAction(i, last_tap_dance_addr_, state_[i].count, Release);
state_[i].count = 0;
state_[i].release_next = false;
}
if (last_tap_dance_key_ == Key_NoKey)
// If there's no active TapDance sequence, there's nothing to do.
if (event_queue_.isEmpty())
return EventHandlerResult::OK;
if (Runtime.hasTimeExpired(start_time_, time_out))
timeout();
// The first event in the queue is now guaranteed to be a TapDance key.
KeyAddr td_addr = event_queue_.addr(0);
Key td_key = Layer.lookupOnActiveLayer(td_addr);
uint8_t td_id = td_key.getRaw() - ranges::TD_FIRST;
// Check for timeout
uint16_t start_time = event_queue_.timestamp(0);
if (Runtime.hasTimeExpired(start_time, time_out)) {
tapDanceAction(td_id, td_addr, tap_count_, Timeout);
flushQueue();
tap_count_ = 0;
}
return EventHandlerResult::OK;
}
}
}
} // namespace plugin
} // namespace kaleidoscope
__attribute__((weak)) void tapDanceAction(uint8_t tap_dance_index, KeyAddr key_addr, uint8_t tap_count,
kaleidoscope::plugin::TapDance::ActionType tap_dance_action) {

@ -18,7 +18,11 @@
#pragma once
#include "kaleidoscope/Runtime.h"
#include "kaleidoscope/LiveKeys.h"
#include <Kaleidoscope-Ranges.h>
#include "kaleidoscope/KeyAddr.h"
#include "kaleidoscope/KeyAddrEventQueue.h"
#include "kaleidoscope/KeyEventTracker.h"
#define TD(n) Key(kaleidoscope::ranges::TD_FIRST + n)
@ -47,31 +51,32 @@ class TapDance : public kaleidoscope::Plugin {
void actionKeys(uint8_t tap_count, ActionType tap_dance_action, uint8_t max_keys, const Key tap_keys[]);
EventHandlerResult onNameQuery();
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t keyState);
EventHandlerResult onKeyswitchEvent(KeyEvent &event);
EventHandlerResult afterEachCycle();
static constexpr bool isTapDanceKey(Key key) {
return (key.getRaw() >= ranges::TD_FIRST &&
key.getRaw() <= ranges::TD_LAST);
}
private:
static constexpr uint8_t TAPDANCE_KEY_COUNT = 16;
struct TapDanceState {
bool pressed: 1;
bool triggered: 1;
bool release_next: 1;
uint8_t count;
};
static TapDanceState state_[TAPDANCE_KEY_COUNT];
static uint16_t start_time_;
static Key last_tap_dance_key_;
static KeyAddr last_tap_dance_addr_;
static void tap(void);
static void interrupt(KeyAddr key_addr);
static void timeout(void);
static void release(uint8_t tap_dance_index);
// The maximum number of events in the queue at a time.
static constexpr uint8_t queue_capacity_{8};
// The event queue stores a series of press and release events.
KeyAddrEventQueue<queue_capacity_> event_queue_;
static KeyEventTracker event_tracker_;
// The number of taps in the current TapDance sequence.
static uint8_t tap_count_;
void flushQueue(KeyAddr ignored_addr = KeyAddr::none());
};
}
}
} // namespace plugin
} // namespace kaleidoscope
void tapDanceAction(uint8_t tap_dance_index, KeyAddr key_addr, uint8_t tap_count,
kaleidoscope::plugin::TapDance::ActionType tap_dance_action);

@ -17,60 +17,77 @@
#include <Kaleidoscope-TopsyTurvy.h>
#include "kaleidoscope/keyswitch_state.h"
#include "kaleidoscope/LiveKeys.h"
namespace kaleidoscope {
namespace plugin {
uint8_t TopsyTurvy::last_pressed_position_;
bool TopsyTurvy::is_shifted_;
bool TopsyTurvy::is_active_;
KeyAddr TopsyTurvy::tt_addr_ = KeyAddr::none();
EventHandlerResult TopsyTurvy::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
if (mapped_key == Key_LeftShift ||
mapped_key == Key_RightShift) {
is_shifted_ = keyIsPressed(key_state);
if (is_active_)
return EventHandlerResult::EVENT_CONSUMED;
EventHandlerResult TopsyTurvy::onKeyEvent(KeyEvent &event) {
if (keyToggledOff(event.state)) {
if (event.addr == tt_addr_)
tt_addr_.clear();
return EventHandlerResult::OK;
}
if (mapped_key < ranges::TT_FIRST || mapped_key > ranges::TT_LAST) {
if (keyToggledOn(key_state) && (mapped_key < Key_LeftControl || mapped_key > Key_RightGui)) {
last_pressed_position_ = key_addr.toInt();
}
if (event.key.isKeyboardModifier())
return EventHandlerResult::OK;
if (isTopsyTurvyKey(event.key)) {
event.key.setRaw(event.key.getRaw() - ranges::TT_FIRST);
tt_addr_ = event.addr;
} else {
live_keys.activate(tt_addr_, Key_NoKey);
tt_addr_.clear();
}
is_active_ = keyIsPressed(key_state);
if (tt_addr_.isValid()) {
for (KeyAddr key_addr : KeyAddr::all()) {
if (key_addr == event.addr)
continue;
if (keyToggledOn(key_state)) {
last_pressed_position_ = key_addr.toInt();
} else {
if (last_pressed_position_ != key_addr.toInt()) {
return EventHandlerResult::EVENT_CONSUMED;
Key active_key = live_keys[key_addr];
if (active_key == Key_Transparent)
continue;
if (active_key.isKeyboardKey() && !active_key.isKeyboardModifier()) {
live_keys.activate(key_addr, Key_NoKey);
}
}
}
return EventHandlerResult::OK;
}
mapped_key.setRaw(mapped_key.getRaw() - ranges::TT_FIRST);
EventHandlerResult TopsyTurvy::beforeReportingState(const KeyEvent &event) {
// invert the shift state
if (!is_shifted_) {
mapped_key.setFlags(mapped_key.getFlags() | SHIFT_HELD);
if (!tt_addr_.isValid()) {
return EventHandlerResult::OK;
}
// If a TopsyTurvy key is being held, no other KeyboardKey should be able to
// toggle off, because those keys were masked. It's possible for other plugins
// to change that, but those types of complex plugin interactions can't be
// guaranteed to be safe, anyway. Therefore, we assume that if `tt_addr` is
// valid, it is also the last key pressed.
bool shift_detected = false;
for (KeyAddr key_addr : KeyAddr::all()) {
if (live_keys[key_addr].isKeyboardShift()) {
shift_detected = true;
break;
}
}
if (keyIsPressed(key_state)) {
kaleidoscope::Runtime.hid().keyboard().releaseKey(Key_LeftShift);
kaleidoscope::Runtime.hid().keyboard().releaseKey(Key_RightShift);
return EventHandlerResult::OK;
if (shift_detected) {
Runtime.hid().keyboard().releaseKey(Key_LeftShift);
Runtime.hid().keyboard().releaseKey(Key_RightShift);
} else {
Runtime.hid().keyboard().pressKey(Key_LeftShift);
}
return EventHandlerResult::EVENT_CONSUMED;
return EventHandlerResult::OK;
}
}
}
} // namespace plugin
} // namespace kaleidoscope
kaleidoscope::plugin::TopsyTurvy TopsyTurvy;

@ -29,13 +29,18 @@ class TopsyTurvy: public kaleidoscope::Plugin {
public:
TopsyTurvy(void) {}
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
EventHandlerResult beforeReportingState(const KeyEvent &event);
static bool isTopsyTurvyKey(Key key) {
return (key >= ranges::TT_FIRST &&
key <= ranges::TT_LAST);
}
private:
static uint8_t last_pressed_position_;
static bool is_shifted_;
static bool is_active_;
static KeyAddr tt_addr_;
};
}
}

@ -25,16 +25,14 @@ namespace kaleidoscope {
namespace plugin {
uint16_t Turbo::interval_ = 10;
uint16_t Turbo::flashInterval_ = 69;
uint16_t Turbo::flash_interval_ = 69;
bool Turbo::sticky_ = false;
bool Turbo::flash_ = true;
cRGB Turbo::activeColor_ = CRGB(160, 0, 0);
cRGB Turbo::active_color_ = CRGB(160, 0, 0);
bool Turbo::enable = false;
uint32_t Turbo::startTime = 0;
uint32_t Turbo::flashStartTime = 0;
KeyAddr Turbo::keyPositions[4];
uint16_t Turbo::numKeys = 0;
bool Turbo::active_ = false;
uint32_t Turbo::start_time_ = 0;
uint32_t Turbo::flash_start_time_ = 0;
uint16_t Turbo::interval() {
return interval_;
@ -44,10 +42,10 @@ void Turbo::interval(uint16_t newVal) {
}
uint16_t Turbo::flashInterval() {
return flashInterval_;
return flash_interval_;
}
void Turbo::flashInterval(uint16_t newVal) {
flashInterval_ = newVal;
flash_interval_ = newVal;
}
bool Turbo::sticky() {
@ -65,75 +63,91 @@ void Turbo::flash(bool newVal) {
}
cRGB Turbo::activeColor() {
return activeColor_;
return active_color_;
}
void Turbo::activeColor(cRGB newVal) {
activeColor_ = newVal;
active_color_ = newVal;
}
void Turbo::findKeyPositions() {
numKeys = 0;
for (auto key_addr : KeyAddr::all()) {
if (Layer.lookupOnActiveLayer(key_addr) == Key_Turbo) {
keyPositions[numKeys++] = key_addr;
}
EventHandlerResult Turbo::onKeyEvent(KeyEvent &event) {
if (active_ && flash_ && keyToggledOff(event.state)) {
if (event.key.isKeyboardKey())
LEDControl::refreshAt(event.addr);
}
}
EventHandlerResult Turbo::onSetup() {
Turbo::findKeyPositions();
return EventHandlerResult::OK;
}
EventHandlerResult Turbo::onNameQuery() {
return ::Focus.sendName(F("Turbo"));
}
EventHandlerResult Turbo::onLayerChange() {
Turbo::findKeyPositions();
return EventHandlerResult::OK;
}
if (event.key != Key_Turbo)
return EventHandlerResult::OK;
EventHandlerResult Turbo::onKeyswitchEvent(Key &key, KeyAddr key_addr, uint8_t key_state) {
if (key != Key_Turbo) return EventHandlerResult::OK;
enable = sticky_ ? (keyIsPressed(key_state) ? enable : !enable) : keyIsPressed(key_state);
if (!enable) {
for (uint16_t i = 0; i < numKeys; i++) {
LEDControl::refreshAt(KeyAddr(keyPositions[i]));
}
if (keyToggledOn(event.state)) {
active_ = true;
start_time_ = Runtime.millisAtCycleStart() - interval_;
} else {
active_ = false;
if (flash_)
LEDControl::refreshAll();
}
return EventHandlerResult::EVENT_CONSUMED;
}
EventHandlerResult Turbo::afterEachCycle() {
if (enable) {
if (Runtime.millisAtCycleStart() - startTime > interval_) {
kaleidoscope::Runtime.hid().keyboard().sendReport();
startTime = Runtime.millisAtCycleStart();
}
if (flash_) {
if (Runtime.millisAtCycleStart() - flashStartTime > flashInterval_ * 2) {
for (uint16_t i = 0; i < numKeys; i++) {
LEDControl::setCrgbAt(KeyAddr(keyPositions[i]), activeColor_);
if (active_) {
if (Runtime.hasTimeExpired(start_time_, interval_)) {
// Reset the timer.
start_time_ = Runtime.millisAtCycleStart();
// Clear the existing Keyboard HID report. It might be nice to keep the
// modifiers active, but I'll save that for another time.
Runtime.hid().keyboard().releaseAllKeys();
// Send the empty report to register the release of all the held keys.
Runtime.hid().keyboard().sendReport();
// Just in case the Turbo key has been wiped from `live_keys[]` without
// `onKeyEvent()` being called with a toggle-off:
active_ = false;
// Go through the `live_keys[]` array and add any Keyboard HID keys to the
// new report.
for (Key key : live_keys.all()) {
if (key == Key_Turbo) {
active_ = true;
}
flashStartTime = Runtime.millisAtCycleStart();
} else if (Runtime.millisAtCycleStart() - flashStartTime > flashInterval_) {
for (uint16_t i = 0; i < numKeys; i++) {
LEDControl::setCrgbAt(KeyAddr(keyPositions[i]), {0, 0, 0});
if (key.isKeyboardKey()) {
Runtime.addToReport(key);
}
}
LEDControl::syncLeds();
} else {
for (uint16_t i = 0; i < numKeys; i++) {
LEDControl::setCrgbAt(KeyAddr(keyPositions[i]), activeColor_);
// Send the re-populated keyboard report.
Runtime.hid().keyboard().sendReport();
}
}
return EventHandlerResult::OK;
}
EventHandlerResult Turbo::beforeSyncingLeds() {
if (flash_ && active_) {
static bool leds_on = false;
cRGB color = CRGB(0, 0, 0);
if (leds_on) {
color = active_color_;
}
if (Runtime.hasTimeExpired(flash_start_time_, flash_interval_)) {
flash_start_time_ = Runtime.millisAtCycleStart();
leds_on = !leds_on;
}
for (KeyAddr key_addr : KeyAddr::all()) {
Key key = live_keys[key_addr];
if (key.isKeyboardKey()) {
LEDControl::setCrgbAt(key_addr, color);
}
}
}
return EventHandlerResult::OK;
}
EventHandlerResult Turbo::onNameQuery() {
return ::Focus.sendName(F("Turbo"));
}
}
}

@ -21,7 +21,7 @@
#pragma once
#define Key_Turbo Key{kaleidoscope::ranges::TURBO }
#define Key_Turbo Key{kaleidoscope::ranges::TURBO}
namespace kaleidoscope {
namespace plugin {
@ -44,25 +44,21 @@ class Turbo : public kaleidoscope::Plugin {
cRGB activeColor();
void activeColor(cRGB newVal);
EventHandlerResult onSetup();
EventHandlerResult onNameQuery();
EventHandlerResult onLayerChange();
EventHandlerResult onKeyswitchEvent(Key &key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
EventHandlerResult afterEachCycle();
private:
void findKeyPositions();
EventHandlerResult beforeSyncingLeds();
private:
static uint16_t interval_;
static uint16_t flashInterval_;
static uint16_t flash_interval_;
static bool sticky_;
static bool flash_;
static cRGB activeColor_;
static cRGB active_color_;
static bool enable;
static uint32_t startTime;
static uint32_t flashStartTime;
static KeyAddr keyPositions[4];
static uint16_t numKeys;
static bool active_;
static uint32_t start_time_;
static uint32_t flash_start_time_;
};
}
}

@ -39,16 +39,21 @@ uint16_t TypingBreaks::left_hand_keys_;
uint16_t TypingBreaks::right_hand_keys_;
uint16_t TypingBreaks::settings_base_;
EventHandlerResult TypingBreaks::onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state) {
EventHandlerResult TypingBreaks::onKeyEvent(KeyEvent &event) {
uint32_t lock_length = settings.lock_length * 1000;
uint32_t idle_time_limit = settings.idle_time_limit * 1000;
uint32_t lock_time_out = settings.lock_time_out * 1000;
// Let key release events through regardless, so the last key pressed (and any
// other held keys) finish getting processed when they're released.
if (keyToggledOff(event.state))
return EventHandlerResult::OK;
// If we are locked...
if (keyboard_locked_) {
// ...and the lock has not expired yet
if (!Runtime.hasTimeExpired(lock_start_time_, lock_length)) {
return EventHandlerResult::EVENT_CONSUMED; // remain locked
return EventHandlerResult::ABORT;
}
// ...otherwise clear the lock
@ -90,14 +95,12 @@ EventHandlerResult TypingBreaks::onKeyswitchEvent(Key &mapped_key, KeyAddr key_a
// So it seems we did not need to lock up. In this case, lets increase key
// counters if need be.
if (event.addr.col() <= Runtime.device().matrix_columns / 2)
left_hand_keys_++;
else
right_hand_keys_++;
if (keyToggledOn(key_state)) {
if (key_addr.col() <= Runtime.device().matrix_columns / 2)
left_hand_keys_++;
else
right_hand_keys_++;
last_key_time_ = Runtime.millisAtCycleStart();
}
last_key_time_ = Runtime.millisAtCycleStart();
return EventHandlerResult::OK;
}

@ -37,7 +37,7 @@ class TypingBreaks : public kaleidoscope::Plugin {
static settings_t settings;
EventHandlerResult onNameQuery();
EventHandlerResult onKeyswitchEvent(Key &mapped_key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
EventHandlerResult onFocusEvent(const char *command);
EventHandlerResult onSetup();

@ -23,12 +23,13 @@ namespace plugin {
bool WinKeyToggle::enabled_;
EventHandlerResult WinKeyToggle::onKeyswitchEvent(Key &key, KeyAddr key_addr, uint8_t key_state) {
EventHandlerResult WinKeyToggle::onKeyEvent(KeyEvent &event) {
if (!enabled_)
return EventHandlerResult::OK;
if (key == Key_LeftGui || key == Key_RightGui)
return EventHandlerResult::EVENT_CONSUMED;
if (event.key == Key_LeftGui || event.key == Key_RightGui) {
return EventHandlerResult::ABORT;
}
return EventHandlerResult::OK;
}

@ -25,7 +25,7 @@ class WinKeyToggle: public kaleidoscope::Plugin {
public:
WinKeyToggle() {}
EventHandlerResult onKeyswitchEvent(Key &key, KeyAddr key_addr, uint8_t key_state);
EventHandlerResult onKeyEvent(KeyEvent &event);
void toggle() {
enabled_ = !enabled_;
}

@ -80,6 +80,7 @@ void setup();
#endif
#include "kaleidoscope/KeyAddr.h"
#include "kaleidoscope/KeyEvent.h"
#include "kaleidoscope/key_events.h"
#include "kaleidoscope/layers.h"
#include "kaleidoscope_internal/sketch_exploration/sketch_exploration.h"

@ -0,0 +1,187 @@
/* 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);
}
void clear() {
memset(data_, 0, sizeof(data_));
}
// 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

@ -1,6 +1,6 @@
// -*- mode: c++ -*-
/* Kaleidoscope - Firmware for computer input devices
* Copyright (C) 2013-2019 Keyboard.io, Inc.
* Copyright (C) 2013-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
@ -21,11 +21,11 @@
//#include <assert.h>
#include "kaleidoscope/Runtime.h"
#include "kaleidoscope/KeyEvent.h"
#include "kaleidoscope/KeyAddr.h"
#include "kaleidoscope/keyswitch_state.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
@ -44,7 +44,8 @@ class KeyAddrEventQueue {
private:
uint8_t length_{0};
KeyAddr addrs_[_capacity];
KeyEventId event_ids_[_capacity]; // NOLINT(runtime/arrays)
KeyAddr addrs_[_capacity]; // NOLINT(runtime/arrays)
_Timestamp timestamps_[_capacity]; // NOLINT(runtime/arrays)
_Bitfield release_event_bits_;
@ -62,6 +63,11 @@ class KeyAddrEventQueue {
// 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()`.
KeyEventId id(uint8_t index) const {
// assert(index < length_);
return event_ids_[index];
}
KeyAddr addr(uint8_t index) const {
// assert(index < length_);
return addrs_[index];
@ -83,26 +89,54 @@ class KeyAddrEventQueue {
// 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) {
void append(const KeyEvent& event) {
// assert(length_ < _capacity);
addrs_[length_] = k;
event_ids_[length_] = event.id();
addrs_[length_] = event.addr;
timestamps_[length_] = Runtime.millisAtCycleStart();
bitWrite(release_event_bits_, length_, keyToggledOff(keyswitch_state));
bitWrite(release_event_bits_, length_, keyToggledOff(event.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,
// Remove an event from the head of the queue, shifting the subsequent
// ones. 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);
void remove(uint8_t n = 0) {
// assert(length > n);
--length_;
for (uint8_t i{0}; i < length_; ++i) {
for (uint8_t i{n}; i < length_; ++i) {
event_ids_[i] = event_ids_[i + 1];
addrs_[i] = addrs_[i + 1];
timestamps_[i] = timestamps_[i + 1];
}
// mask = all ones for bits >= n, zeros otherwise
_Bitfield mask = _Bitfield(~0) << n;
// use the inverse mask to get just the low bits (that won't be shifted)
_Bitfield low_bits = release_event_bits_ & ~mask;
// shift the event bits
release_event_bits_ >>= 1;
// use the mask to zero the low bits, leaving only the shifted high bits
release_event_bits_ &= mask;
// add the low bits back in
release_event_bits_ |= low_bits;
}
void shift() {
remove(0);
}
void shift(uint8_t n) {
if (n >= length_) {
clear();
return;
}
length_ -= n;
for (uint8_t i{0}; i < length_; ++i) {
addrs_[i] = addrs_[i + n];
timestamps_[i] = timestamps_[i + n];
}
release_event_bits_ >>= n;
}
// Empty the queue entirely.
@ -110,7 +144,16 @@ class KeyAddrEventQueue {
length_ = 0;
release_event_bits_ = 0;
}
KeyEvent event(uint8_t i) const {
uint8_t state = isRelease(i) ? WAS_PRESSED : IS_PRESSED;
return KeyEvent{addr(i), state, Key_NoKey, id(i)};
}
// Only call this after `EventTracker::shouldIgnore()` returns `true`.
bool shouldAbort(const KeyEvent& event) const {
return (length_ != 0) && (event.id() - event_ids_[0] >= 0);
}
};
} // namespace plugin
} // namespace kaleidoscope

@ -0,0 +1,98 @@
/* Kaleidoscope - Firmware for computer input devices
* Copyright (C) 2013-2021 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 "kaleidoscope_internal/device.h"
#include "kaleidoscope/KeyAddr.h"
#include "kaleidoscope/key_defs.h"
namespace kaleidoscope {
// A `KeyAddrMap` is a collection of objects, indexed by `KeyAddr`, with one
// entry per key on the keyboard.
template<typename _ContentType, uint8_t _size>
class KeyAddrMap {
private:
_ContentType values_[_size]; // NOLINT(runtime/arrays)
public:
typedef KeyAddrMap<_ContentType, _size> ThisType;
// Return the number of `Key` entries in the array
static constexpr uint8_t size() {
return _size;
}
// To set the value of an entry:
// key_array[key_addr] = Key_X;
_ContentType& operator[](KeyAddr key_addr) {
return values_[key_addr.toInt()];
}
// To get the value of an entry:
// Key key = key_array[key_addr];
const _ContentType& operator[](KeyAddr key_addr) const {
return values_[key_addr.toInt()];
}
// ---------------------------------------------------------------------------
// The following code defines an iterator class for a class `KeyMap`, such
// that we can write the following code to get each entry in the array:
//
// typedef KeyAddrMap<Key> KeyMap;
//
// for (Key key : key_map) {...}
//
// Or, if we need write access to the entries in the array:
//
// for (Key &key : key_map) {...}
private:
class Iterator;
friend class ThisType::Iterator;
public:
Iterator begin() {
return {*this, KeyAddr(uint8_t(0))};
}
Iterator end() {
return {*this, KeyAddr(_size)};
}
private:
class Iterator {
public:
Iterator(ThisType &map, KeyAddr key_addr)
: map_(map), key_addr_(key_addr) {}
bool operator!=(const Iterator &other) const {
return key_addr_ != other.key_addr_;
}
_ContentType& operator*() const {
return map_[key_addr_];
}
Iterator& operator++() {
++key_addr_;
return *this;
}
private:
ThisType &map_;
KeyAddr key_addr_;
};
};
} // namespace kaleidoscope

@ -0,0 +1,23 @@
/* 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/>.
*/
#include "kaleidoscope/KeyEvent.h"
namespace kaleidoscope {
KeyEventId KeyEvent::last_id_ = 0;
} // namespace kaleidoscope

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save