diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md index 69f43dee..9c7ac354 100644 --- a/docs/UPGRADING.md +++ b/docs/UPGRADING.md @@ -16,6 +16,8 @@ If any of this does not make sense to you, or you have trouble updating your .in - [Bidirectional communication for plugins](#bidirectional-communication-for-plugins) - [Consistent timing](#consistent-timing) + [Breaking changes](#breaking-changes) + - [Macros](#macros) + - [Removed `kaleidoscope-builder`](#removed-kaleidoscope-builder) - [OneShot meta keys](#oneshot-meta-keys) - [git checkouts aren't compatible with Arduino IDE (GUI)]([#repository-rearchitecture) - [Layer system switched to activation-order](#layer-system-switched-to-activation-order) @@ -435,13 +437,354 @@ As a developer, one can continue using `millis()`, but migrating to `Kaleidoscop ## Breaking changes -### +### Macros + +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. + + +#### Using `MACRO()` and `MACRODOWN()` + +The preprocessor macro `MACRODOWN()` has been deprecated, because the event +handler for Macros is no longer called every cycle, but only when a key is +either pressed or released. Instead of using `return MACRODOWN()`, you should +test for a toggle-on event in `macroAction()` and use `MACRO()` instead. If you +previously had something like the following in your `macroAction()` function: + +```c++ +switch(macro_id) { +case MY_MACRO: + return MACRODOWN(T(X), T(Y), T(Z)); +} +``` + +...you should replace that with: + +```c++ +switch(macro_id) { +case MY_MACRO: + if (keyToggledOn(event.state)) + return MACRO(T(X), T(Y), T(Z)); +} +``` + +...or, for a group of macros that should only fire on keypress: + +```c++ +if (keyToggledOn(event.state)) { + switch(macro_id) { + case MY_MACRO: + return MACRO(T(X), T(Y), T(Z)); + case MY_OTHER_MACRO: + return MACRO(T(A), T(B), T(C)); + } +} +``` + +#### Releasing keys with `Macros.release()` or `U()`/`Ur()`/`Uc()` + +Macros now operates by manipulating keys on a small supplemental virtual +keyboard when using `Macros.press()` and `Macros.release()` (which are called by +`D()` and `U()`, _et al_, respectively). This means that it has no built-in +facility for releasing other keys that are held on the keyboard. For example, +if you had a Macro that removed `shift` keycodes from the HID report in the +past, it won't work. For example: + +```c++ + case KEY_COMMA: + if (keyToggledOn(event.state)) { + if (Kaleidoscope.hid().keyboard().wasModifierKeyActive(Key_LeftShift)) { + return MACRO(U(LeftShift), T(Comma), D(LeftShift)); + } else { + return MACRO(T(M)); + } + } +``` + +In this case, holding a physical `Key_LeftShift` and pressing `M(KEY_COMMA)` +will not cause the held `shift` to be released, and you'll get a `<` instead of +the intended `,` (depending on the OS keymap). To accomplish this, you'll need +a small plugin like the following in your sketch: + +```c++ +namespace kaleidoscope { +namespace plugin { + +// When activated, this plugin will suppress any `shift` key (including modifier +// combos with `shift` a flag) before it's added to the HID report. +class ShiftBlocker : public Plugin { + + public: + EventHandlerResult onAddToReport(Key key) { + if (active_ && key.isKeyboardShift()) + return EventHandlerResult::ABORT; + return EventHandlerResult::OK; + } + + void enable() { + active_ = true; + } + void disable() { + active_ = false; + } + + private: + bool active_{false}; + +}; + +} // namespace plugin +} // namespace kaleidoscope + +kaleidoscope::plugin::ShiftBlocker ShiftBlocker; +``` + +You may also need to define a function to test for held `shift` keys: + +```c++ +bool isShiftKeyHeld() { + for (Key key : kaleidoscope::live_keys.all()) { + if (key.isKeyboardShift()) + return true; + } + return false; +} +``` + +Then, in your `macroAction()` function: + +```c++ + if (keyToggledOn(event.state)) { + switch (macro_id) { + case MY_MACRO: + if (isShiftKeyHeld()) { + ShiftBlocker.enable(); + Macros.tap(Key_Comma); + ShiftBlocker.disable(); + } else { + Macros.tap(Key_M); + } + return MACRO_NONE; + } + } +``` + +In many simple cases, such as the above example, an even better solution is to +use the CharShift plugin instead of Macros. + +#### 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(); + } +} +``` + +### Removed `kaleidoscope-builder` `kaleidoscope-builder` has been removed. We replaced it 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. - ### OneShot meta keys The special OneShot keys `OneShot_MetaStickyKey` & `OneShot_ActiveStickyKey` are no longer handled by the OneShot plugin directly, but instead by a separate OneShotMetaKeys plugin. If you use these keys in your sketch, you will need to add the new plugin, and register it after OneShot in `KALEIDOSCOPE_INIT_PLUGINS()` for those keys to work properly. diff --git a/plugins/Kaleidoscope-Macros/README.md b/plugins/Kaleidoscope-Macros/README.md index 6b04e726..39c2062e 100644 --- a/plugins/Kaleidoscope-Macros/README.md +++ b/plugins/Kaleidoscope-Macros/README.md @@ -210,350 +210,3 @@ Due to technical and practical reasons, `Macros.type()` assumes a QWERTY layout on the host side, and so do all other parts that work with keycodes. If your operating system is set to a different layout, the strings and keycodes will need to be adjusted accordingly. - - -# Upgrading old 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. - - -## Using `MACRO()` and `MACRODOWN()` - -The preprocessor macro `MACRODOWN()` has been deprecated, because the event -handler for Macros is no longer called every cycle, but only when a key is -either pressed or released. Instead of using `return MACRODOWN()`, you should -test for a toggle-on event in `macroAction()` and use `MACRO()` instead. If you -previously had something like the following in your `macroAction()` function: - -```c++ -switch(macro_id) { -case MY_MACRO: - return MACRODOWN(T(X), T(Y), T(Z)); -} -``` - -...you should replace that with: - -```c++ -switch(macro_id) { -case MY_MACRO: - if (keyToggledOn(event.state)) - return MACRO(T(X), T(Y), T(Z)); -} -``` - -...or, for a group of macros that should only fire on keypress: - -```c++ -if (keyToggledOn(event.state)) { - switch(macro_id) { - case MY_MACRO: - return MACRO(T(X), T(Y), T(Z)); - case MY_OTHER_MACRO: - return MACRO(T(A), T(B), T(C)); - } -} -``` - -## Releasing keys with `Macros.release()` or `U()`/`Ur()`/`Uc()` - -Macros now operates by manipulating keys on a small supplemental virtual -keyboard when using `Macros.press()` and `Macros.release()` (which are called by -`D()` and `U()`, _et al_, respectively). This means that it has no built-in -facility for releasing other keys that are held on the keyboard. For example, -if you had a Macro that removed `shift` keycodes from the HID report in the -past, it won't work. For example: - -```c++ - case KEY_COMMA: - if (keyToggledOn(event.state)) { - if (Kaleidoscope.hid().keyboard().wasModifierKeyActive(Key_LeftShift)) { - return MACRO(U(LeftShift), T(Comma), D(LeftShift)); - } else { - return MACRO(T(M)); - } - } -``` - -In this case, holding a physical `Key_LeftShift` and pressing `M(KEY_COMMA)` -will not cause the held `shift` to be released, and you'll get a `<` instead of -the intended `,` (depending on the OS keymap). To accomplish this, you'll need -a small plugin like the following in your sketch: - -```c++ -namespace kaleidoscope { -namespace plugin { - -// When activated, this plugin will suppress any `shift` key (including modifier -// combos with `shift` a flag) before it's added to the HID report. -class ShiftBlocker : public Plugin { - - public: - EventHandlerResult onAddToReport(Key key) { - if (active_ && key.isKeyboardShift()) - return EventHandlerResult::ABORT; - return EventHandlerResult::OK; - } - - void enable() { - active_ = true; - } - void disable() { - active_ = false; - } - - private: - bool active_{false}; - -}; - -} // namespace plugin -} // namespace kaleidoscope - -kaleidoscope::plugin::ShiftBlocker ShiftBlocker; -``` - -You may also need to define a function to test for held `shift` keys: - -```c++ -bool isShiftKeyHeld() { - for (Key key : kaleidoscope:live_keys.all()) { - if (key.isKeyboardShift()) - return true; - } - return false; -} -``` - -Then, in your `macroAction()` function: - -```c++ - if (keyToggledOn(event.state)) { - switch (macro_id) { - case MY_MACRO: - if (isShiftKeyHeld()) { - ShiftBlocker.enable(); - Macros.tap(Key_Comma); - ShiftBlocker.disable(); - } else { - Macros.tap(Key_M); - } - return MACRO_NONE; - } - } -``` - -In many simple cases, such as the above example, an even better solution is to -use the CharShift plugin instead of Macros. - - -## 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(); - } -} -```