1
0
Fork 0

[Core] Convert Dynamic Macro to a Core Feature (#5948)

* Convert Dynamic Macro to a Core Feature

This imports the code from Dynamic Macro into the core code, and handles it, as such.

This deprecates the old method but does not remove it, for legacy support. This way, no existing user files need to be touched.

Additionally, this reorganizes the documentation to better reflect the changes.

Also, it adds user hooks to the feature so users can customize the existing functionality.

Based heavily on and closes #2976

* Apply suggestions from code review

Co-Authored-By: fauxpark <fauxpark@gmail.com>
Co-Authored-By: noroadsleft <18669334+noroadsleft@users.noreply.github.com>

* Cleanup based on feedback

* Add short-form keycodes and document them

- add short-form keycodes to quantum/quantum_keycodes.h
- document the new aliases in docs/feature_dynamic_macros.md

* Add Dynamic Macros section and keycodes to docs/keycodes.md

* Make anti-nesting optional

* Add documentation for DYNAMIC_MACRO_NO_NESTING option

* Fix Merge artifacts

* Fix formatting typo in docs

Co-Authored-By: James Young <18669334+noroadsleft@users.noreply.github.com>

* Remove DYNAMIC_MACRO_RANGE as it's not needed

* Fix includes and layer var type
This commit is contained in:
Drashna Jaelre 2019-11-04 22:59:13 -08:00 committed by James Young
parent 0e664f92c4
commit 542cb0a8ce
9 changed files with 384 additions and 59 deletions

View file

@ -407,8 +407,12 @@ ifeq ($(strip $(SPACE_CADET_ENABLE)), yes)
OPT_DEFS += -DSPACE_CADET_ENABLE OPT_DEFS += -DSPACE_CADET_ENABLE
endif endif
ifeq ($(strip $(DIP_SWITCH_ENABLE)), yes) ifeq ($(strip $(DIP_SWITCH_ENABLE)), yes)
SRC += $(QUANTUM_DIR)/dip_switch.c SRC += $(QUANTUM_DIR)/dip_switch.c
OPT_DEFS += -DDIP_SWITCH_ENABLE OPT_DEFS += -DDIP_SWITCH_ENABLE
endif endif
ifeq ($(strip $(DYNAMIC_MACRO_ENABLE)), yes)
SRC += $(QUANTUM_DIR)/process_keycode/process_dynamic_macro.c
OPT_DEFS += -DDYNAMIC_MACRO_ENABLE
endif

View file

@ -4,51 +4,45 @@ QMK supports temporary macros created on the fly. We call these Dynamic Macros.
You can store one or two macros and they may have a combined total of 128 keypresses. You can increase this size at the cost of RAM. You can store one or two macros and they may have a combined total of 128 keypresses. You can increase this size at the cost of RAM.
To enable them, first add a new element to the end of your `keycodes` enum — `DYNAMIC_MACRO_RANGE`: To enable them, first include `DYNAMIC_MACRO_ENABLE = yes` in your `rules.mk`. Then, add the following keys to your keymap:
```c |Key |Alias |Description |
enum keycodes { |------------------|----------|---------------------------------------------------|
QWERTY = SAFE_RANGE, |`DYN_REC_START1` |`DM_REC1` |Start recording Macro 1 |
COLEMAK, |`DYN_REC_START2` |`DM_REC2` |Start recording Macro 2 |
DVORAK, |`DYN_MACRO_PLAY1` |`DM_PLY1` |Replay Macro 1 |
PLOVER, |`DYN_MACRO_PLAY2` |`DM_PLY2` |Replay Macro 2 |
LOWER, |`DYN_REC_STOP` |`DM_RSTP` |Finish the macro that is currently being recorded. |
RAISE,
BACKLIT,
EXT_PLV,
DYNAMIC_MACRO_RANGE,
};
```
Your `keycodes` enum may have a slightly different name. You must add `DYNAMIC_MACRO_RANGE` as the last element because `dynamic_macros.h` will add some more keycodes after it. That should be everything necessary.
Below it, include the `dynamic_macro.h` header: To start recording the macro, press either `DYN_REC_START1` or `DYN_REC_START2`.
```c To finish the recording, press the `DYN_REC_STOP` layer button.
#include "dynamic_macro.h"`
```
Add the following keys to your keymap: To replay the macro, press either `DYN_MACRO_PLAY1` or `DYN_MACRO_PLAY2`.
* `DYN_REC_START1` — start recording the macro 1, It is possible to replay a macro as part of a macro. It's ok to replay macro 2 while recording macro 1 and vice versa but never create recursive macros i.e. macro 1 that replays macro 1. If you do so and the keyboard will get unresponsive, unplug the keyboard and plug it again. You can disable this completly by defining `DYNAMIC_MACRO_NO_NESTING` in your `config.h` file.
* `DYN_REC_START2` — start recording the macro 2,
* `DYN_MACRO_PLAY1` — replay the macro 1,
* `DYN_MACRO_PLAY2` — replay the macro 2,
* `DYN_REC_STOP` — finish the macro that is currently being recorded.
Add the following code to the very beginning of your `process_record_user()` function: ?> For the details about the internals of the dynamic macros, please read the comments in the `process_dynamic_macro.h` and `process_dynamic_macro.c` files.
```c ## Customization
if (!process_record_dynamic_macro(keycode, record)) {
return false;
}
```
That should be everything necessary. To start recording the macro, press either `DYN_REC_START1` or `DYN_REC_START2`. To finish the recording, press the `DYN_REC_STOP` layer button. To replay the macro, press either `DYN_MACRO_PLAY1` or `DYN_MACRO_PLAY2`. There are a number of options added that should allow some additional degree of customization
Note that it's possible to replay a macro as part of a macro. It's ok to replay macro 2 while recording macro 1 and vice versa but never create recursive macros i.e. macro 1 that replays macro 1. If you do so and the keyboard will get unresponsive, unplug the keyboard and plug it again. |Define |Default |Description |
|----------------------------|----------------|-----------------------------------------------------------------------------------------------------------------|
|`DYNAMIC_MACRO_SIZE` |128 |Sets the amount of memory that Dynamic Macros can use. This is a limited resource, dependent on the controller. |
|`DYNAMIC_MACRO_USER_CALL` |*Not defined* |Defining this falls back to using the user `keymap.c` file to trigger the macro behavior. |
|`DYNAMIC_MACRO_NO_NESTING` |*Not Defined* |Defining this disables the ability to call a macro from another macro (nested macros). |
For users of the earlier versions of dynamic macros: It is still possible to finish the macro recording using just the layer modifier used to access the dynamic macro keys, without a dedicated `DYN_REC_STOP` key. If you want this behavior back, use the following snippet instead of the one above:
If the LEDs start blinking during the recording with each keypress, it means there is no more space for the macro in the macro buffer. To fit the macro in, either make the other macro shorter (they share the same buffer) or increase the buffer size by adding the `DYNAMIC_MACRO_SIZE` define in your `config.h` (default value: 128; please read the comments for it in the header).
### DYNAMIC_MACRO_USER_CALL
For users of the earlier versions of dynamic macros: It is still possible to finish the macro recording using just the layer modifier used to access the dynamic macro keys, without a dedicated `DYN_REC_STOP` key. If you want this behavior back, add `#define DYNAMIC_MACRO_USER_CALL` to your `config.h` and insert the following snippet at the beginning of your `process_record_user()` function:
```c ```c
uint16_t macro_kc = (keycode == MO(_DYN) ? DYN_REC_STOP : keycode); uint16_t macro_kc = (keycode == MO(_DYN) ? DYN_REC_STOP : keycode);
@ -58,6 +52,15 @@ For users of the earlier versions of dynamic macros: It is still possible to fin
} }
``` ```
If the LEDs start blinking during the recording with each keypress, it means there is no more space for the macro in the macro buffer. To fit the macro in, either make the other macro shorter (they share the same buffer) or increase the buffer size by setting the `DYNAMIC_MACRO_SIZE` preprocessor macro (default value: 128; please read the comments for it in the header). ### User Hooks
For the details about the internals of the dynamic macros, please read the comments in the `dynamic_macro.h` header. There are a number of hooks that you can use to add custom functionality and feedback options to Dynamic Macro feature. This allows for some additional degree of customization.
Note, that direction indicates which macro it is, with `1` being Macro 1, `-1` being Macro 2, and 0 being no macro.
* `dynamic_macro_record_start_user(void)` - Triggered when you start recording a macro.
* `dynamic_macro_play_user(int8_t direction)` - Triggered when you play back a macro.
* `dynamic_macro_record_key_user(int8_t direction, keyrecord_t *record)` - Triggered on each keypress while recording a macro.
* `dynamic_macro_record_end_user(int8_t direction)` - Triggered when the macro recording is stopped.
Additionally, you can call `dynamic_macro_led_blink()` to flash the backlights if that feature is enabled.

View file

@ -297,6 +297,16 @@ This is a reference only. Each group of keys links to the page documenting their
|`OUT_USB` |USB only | |`OUT_USB` |USB only |
|`OUT_BT` |Bluetooth only | |`OUT_BT` |Bluetooth only |
## [Dynamic Macros](feature_dynamic_macros.md)
|Key |Alias |Description |
|-----------------|---------|--------------------------------------------------|
|`DYN_REC_START1` |`DM_REC1`|Start recording Macro 1 |
|`DYN_REC_START2` |`DM_REC2`|Start recording Macro 2 |
|`DYN_MACRO_PLAY1`|`DM_PLY1`|Replay Macro 1 |
|`DYN_MACRO_PLAY2`|`DM_PLY2`|Replay Macro 2 |
|`DYN_REC_STOP` |`DM_RSTP`|Finish the macro that is currently being recorded.|
## [Layer Switching](feature_advanced_keycodes.md#switching-and-toggling-layers) ## [Layer Switching](feature_advanced_keycodes.md#switching-and-toggling-layers)
|Key |Description | |Key |Description |

View file

@ -15,8 +15,10 @@
*/ */
/* Author: Wojciech Siewierski < wojciech dot siewierski at onet dot pl > */ /* Author: Wojciech Siewierski < wojciech dot siewierski at onet dot pl > */
#ifndef DYNAMIC_MACROS_H #pragma once
#define DYNAMIC_MACROS_H
/* Warn users that this is now deprecated and they should use the core feature instead. */
#pragma message "Dynamic Macros is now a core feature. See updated documentation to see how to configure it: https://docs.qmk.fm/#/feature_dynamic_macros"
#include "action_layer.h" #include "action_layer.h"
@ -33,18 +35,6 @@
# define DYNAMIC_MACRO_SIZE 128 # define DYNAMIC_MACRO_SIZE 128
#endif #endif
/* DYNAMIC_MACRO_RANGE must be set as the last element of user's
* "planck_keycodes" enum prior to including this header. This allows
* us to 'extend' it.
*/
enum dynamic_macro_keycodes {
DYN_REC_START1 = DYNAMIC_MACRO_RANGE,
DYN_REC_START2,
DYN_REC_STOP,
DYN_MACRO_PLAY1,
DYN_MACRO_PLAY2,
};
/* Blink the LEDs to notify the user about some event. */ /* Blink the LEDs to notify the user about some event. */
void dynamic_macro_led_blink(void) { void dynamic_macro_led_blink(void) {
#ifdef BACKLIGHT_ENABLE #ifdef BACKLIGHT_ENABLE
@ -272,5 +262,3 @@ bool process_record_dynamic_macro(uint16_t keycode, keyrecord_t *record) {
#undef DYNAMIC_MACRO_CURRENT_SLOT #undef DYNAMIC_MACRO_CURRENT_SLOT
#undef DYNAMIC_MACRO_CURRENT_LENGTH #undef DYNAMIC_MACRO_CURRENT_LENGTH
#undef DYNAMIC_MACRO_CURRENT_CAPACITY #undef DYNAMIC_MACRO_CURRENT_CAPACITY
#endif

View file

@ -0,0 +1,257 @@
/* Copyright 2016 Jack Humbert
* Copyright 2019 Drashna Jael're (@drashna, aka Christopher Courtney)
*
* 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, either version 2 of the License, or
* (at your option) any later version.
*
* 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/>.
*/
/* Author: Wojciech Siewierski < wojciech dot siewierski at onet dot pl > */
#include "process_dynamic_macro.h"
// default feedback method
void dynamic_macro_led_blink(void) {
#ifdef BACKLIGHT_ENABLE
backlight_toggle();
wait_ms(100);
backlight_toggle();
#endif
}
/* User hooks for Dynamic Macros */
__attribute__((weak)) void dynamic_macro_record_start_user(void) { dynamic_macro_led_blink(); }
__attribute__((weak)) void dynamic_macro_play_user(int8_t direction) { dynamic_macro_led_blink(); }
__attribute__((weak)) void dynamic_macro_record_key_user(int8_t direction, keyrecord_t *record) { dynamic_macro_led_blink(); }
__attribute__((weak)) void dynamic_macro_record_end_user(int8_t direction) { dynamic_macro_led_blink(); }
/* Convenience macros used for retrieving the debug info. All of them
* need a `direction` variable accessible at the call site.
*/
#define DYNAMIC_MACRO_CURRENT_SLOT() (direction > 0 ? 1 : 2)
#define DYNAMIC_MACRO_CURRENT_LENGTH(BEGIN, POINTER) ((int)(direction * ((POINTER) - (BEGIN))))
#define DYNAMIC_MACRO_CURRENT_CAPACITY(BEGIN, END2) ((int)(direction * ((END2) - (BEGIN)) + 1))
/**
* Start recording of the dynamic macro.
*
* @param[out] macro_pointer The new macro buffer iterator.
* @param[in] macro_buffer The macro buffer used to initialize macro_pointer.
*/
void dynamic_macro_record_start(keyrecord_t **macro_pointer, keyrecord_t *macro_buffer) {
dprintln("dynamic macro recording: started");
dynamic_macro_record_start_user();
clear_keyboard();
layer_clear();
*macro_pointer = macro_buffer;
}
/**
* Play the dynamic macro.
*
* @param macro_buffer[in] The beginning of the macro buffer being played.
* @param macro_end[in] The element after the last macro buffer element.
* @param direction[in] Either +1 or -1, which way to iterate the buffer.
*/
void dynamic_macro_play(keyrecord_t *macro_buffer, keyrecord_t *macro_end, int8_t direction) {
dprintf("dynamic macro: slot %d playback\n", DYNAMIC_MACRO_CURRENT_SLOT());
layer_state_t saved_layer_state = layer_state;
clear_keyboard();
layer_clear();
while (macro_buffer != macro_end) {
process_record(macro_buffer);
macro_buffer += direction;
}
clear_keyboard();
layer_state = saved_layer_state;
dynamic_macro_play_user(direction);
}
/**
* Record a single key in a dynamic macro.
*
* @param macro_buffer[in] The start of the used macro buffer.
* @param macro_pointer[in,out] The current buffer position.
* @param macro2_end[in] The end of the other macro.
* @param direction[in] Either +1 or -1, which way to iterate the buffer.
* @param record[in] The current keypress.
*/
void dynamic_macro_record_key(keyrecord_t *macro_buffer, keyrecord_t **macro_pointer, keyrecord_t *macro2_end, int8_t direction, keyrecord_t *record) {
/* If we've just started recording, ignore all the key releases. */
if (!record->event.pressed && *macro_pointer == macro_buffer) {
dprintln("dynamic macro: ignoring a leading key-up event");
return;
}
/* The other end of the other macro is the last buffer element it
* is safe to use before overwriting the other macro.
*/
if (*macro_pointer - direction != macro2_end) {
**macro_pointer = *record;
*macro_pointer += direction;
} else {
dynamic_macro_record_key_user(direction, record);
}
dprintf("dynamic macro: slot %d length: %d/%d\n", DYNAMIC_MACRO_CURRENT_SLOT(), DYNAMIC_MACRO_CURRENT_LENGTH(macro_buffer, *macro_pointer), DYNAMIC_MACRO_CURRENT_CAPACITY(macro_buffer, macro2_end));
}
/**
* End recording of the dynamic macro. Essentially just update the
* pointer to the end of the macro.
*/
void dynamic_macro_record_end(keyrecord_t *macro_buffer, keyrecord_t *macro_pointer, int8_t direction, keyrecord_t **macro_end) {
dynamic_macro_record_end_user(direction);
/* Do not save the keys being held when stopping the recording,
* i.e. the keys used to access the layer DYN_REC_STOP is on.
*/
while (macro_pointer != macro_buffer && (macro_pointer - direction)->event.pressed) {
dprintln("dynamic macro: trimming a trailing key-down event");
macro_pointer -= direction;
}
dprintf("dynamic macro: slot %d saved, length: %d\n", DYNAMIC_MACRO_CURRENT_SLOT(), DYNAMIC_MACRO_CURRENT_LENGTH(macro_buffer, macro_pointer));
*macro_end = macro_pointer;
}
/* Handle the key events related to the dynamic macros. Should be
* called from process_record_user() like this:
*
* bool process_record_user(uint16_t keycode, keyrecord_t *record) {
* if (!process_record_dynamic_macro(keycode, record)) {
* return false;
* }
* <...THE REST OF THE FUNCTION...>
* }
*/
bool process_dynamic_macro(uint16_t keycode, keyrecord_t *record) {
/* Both macros use the same buffer but read/write on different
* ends of it.
*
* Macro1 is written left-to-right starting from the beginning of
* the buffer.
*
* Macro2 is written right-to-left starting from the end of the
* buffer.
*
* &macro_buffer macro_end
* v v
* +------------------------------------------------------------+
* |>>>>>> MACRO1 >>>>>> <<<<<<<<<<<<< MACRO2 <<<<<<<<<<<<<|
* +------------------------------------------------------------+
* ^ ^
* r_macro_end r_macro_buffer
*
* During the recording when one macro encounters the end of the
* other macro, the recording is stopped. Apart from this, there
* are no arbitrary limits for the macros' length in relation to
* each other: for example one can either have two medium sized
* macros or one long macro and one short macro. Or even one empty
* and one using the whole buffer.
*/
static keyrecord_t macro_buffer[DYNAMIC_MACRO_SIZE];
/* Pointer to the first buffer element after the first macro.
* Initially points to the very beginning of the buffer since the
* macro is empty. */
static keyrecord_t *macro_end = macro_buffer;
/* The other end of the macro buffer. Serves as the beginning of
* the second macro. */
static keyrecord_t *const r_macro_buffer = macro_buffer + DYNAMIC_MACRO_SIZE - 1;
/* Like macro_end but for the second macro. */
static keyrecord_t *r_macro_end = r_macro_buffer;
/* A persistent pointer to the current macro position (iterator)
* used during the recording. */
static keyrecord_t *macro_pointer = NULL;
/* 0 - no macro is being recorded right now
* 1,2 - either macro 1 or 2 is being recorded */
static uint8_t macro_id = 0;
if (macro_id == 0) {
/* No macro recording in progress. */
if (!record->event.pressed) {
switch (keycode) {
case DYN_REC_START1:
dynamic_macro_record_start(&macro_pointer, macro_buffer);
macro_id = 1;
return false;
case DYN_REC_START2:
dynamic_macro_record_start(&macro_pointer, r_macro_buffer);
macro_id = 2;
return false;
case DYN_MACRO_PLAY1:
dynamic_macro_play(macro_buffer, macro_end, +1);
return false;
case DYN_MACRO_PLAY2:
dynamic_macro_play(r_macro_buffer, r_macro_end, -1);
return false;
}
}
} else {
/* A macro is being recorded right now. */
switch (keycode) {
case DYN_REC_STOP:
/* Stop the macro recording. */
if (record->event.pressed) { /* Ignore the initial release
* just after the recoding
* starts. */
switch (macro_id) {
case 1:
dynamic_macro_record_end(macro_buffer, macro_pointer, +1, &macro_end);
break;
case 2:
dynamic_macro_record_end(r_macro_buffer, macro_pointer, -1, &r_macro_end);
break;
}
macro_id = 0;
}
return false;
#ifdef DYNAMIC_MACRO_NO_NESTING
case DYN_MACRO_PLAY1:
case DYN_MACRO_PLAY2:
dprintln("dynamic macro: ignoring macro play key while recording");
return false;
#endif
default:
/* Store the key in the macro buffer and process it normally. */
switch (macro_id) {
case 1:
dynamic_macro_record_key(macro_buffer, &macro_pointer, r_macro_end, +1, record);
break;
case 2:
dynamic_macro_record_key(r_macro_buffer, &macro_pointer, macro_end, -1, record);
break;
}
return true;
break;
}
}
return true;
}

View file

@ -0,0 +1,41 @@
/* Copyright 2016 Jack Humbert
* Copyright 2019 Drashna Jael're (@drashna, aka Christopher Courtney)
*
* 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, either version 2 of the License, or
* (at your option) any later version.
*
* 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/>.
*/
/* Author: Wojciech Siewierski < wojciech dot siewierski at onet dot pl > */
#pragma once
#include "quantum.h"
/* May be overridden with a custom value. Be aware that the effective
* macro length is half of this value: each keypress is recorded twice
* because of the down-event and up-event. This is not a bug, it's the
* intended behavior.
*
* Usually it should be fine to set the macro size to at least 256 but
* there have been reports of it being too much in some users' cases,
* so 128 is considered a safe default.
*/
#ifndef DYNAMIC_MACRO_SIZE
# define DYNAMIC_MACRO_SIZE 128
#endif
void dynamic_macro_led_blink(void);
bool process_dynamic_macro(uint16_t keycode, keyrecord_t *record);
void dynamic_macro_record_start_user(void);
void dynamic_macro_play_user(int8_t direction);
void dynamic_macro_record_key_user(int8_t direction, keyrecord_t *record);
void dynamic_macro_record_end_user(int8_t direction);

View file

@ -222,6 +222,10 @@ bool process_record_quantum(keyrecord_t *record) {
// Must run first to be able to mask key_up events. // Must run first to be able to mask key_up events.
process_key_lock(&keycode, record) && process_key_lock(&keycode, record) &&
#endif #endif
#if defined(DYNAMIC_MACRO_ENABLE) && !defined(DYNAMIC_MACRO_USER_CALL)
// Must run asap to ensure all keypresses are recorded.
process_dynamic_macro(keycode, record) &&
#endif
#if defined(AUDIO_ENABLE) && defined(AUDIO_CLICKY) #if defined(AUDIO_ENABLE) && defined(AUDIO_CLICKY)
process_clicky(keycode, record) && process_clicky(keycode, record) &&
#endif // AUDIO_CLICKY #endif // AUDIO_CLICKY

View file

@ -149,6 +149,10 @@ extern layer_state_t layer_state;
#include "dip_switch.h" #include "dip_switch.h"
#endif #endif
#ifdef DYNAMIC_MACRO_ENABLE
#include "process_dynamic_macro.h"
#endif
// Function substitutions to ease GPIO manipulation // Function substitutions to ease GPIO manipulation
#if defined(__AVR__) #if defined(__AVR__)

View file

@ -505,6 +505,13 @@ enum quantum_keycodes {
MAGIC_EE_HANDS_LEFT, MAGIC_EE_HANDS_LEFT,
MAGIC_EE_HANDS_RIGHT, MAGIC_EE_HANDS_RIGHT,
// Dynamic Macros
DYN_REC_START1,
DYN_REC_START2,
DYN_REC_STOP,
DYN_MACRO_PLAY1,
DYN_MACRO_PLAY2,
// always leave at the end // always leave at the end
SAFE_RANGE SAFE_RANGE
}; };
@ -757,4 +764,11 @@ enum quantum_keycodes {
# define SH_OFF (QK_SWAP_HANDS | OP_SH_OFF) # define SH_OFF (QK_SWAP_HANDS | OP_SH_OFF)
#endif #endif
// Dynamic Macros aliases
#define DM_REC1 DYN_REC_START1
#define DM_REC2 DYN_REC_START2
#define DM_RSTP DYN_REC_STOP
#define DM_PLY1 DYN_MACRO_PLAY1
#define DM_PLY2 DYN_MACRO_PLAY2
#endif // QUANTUM_KEYCODES_H #endif // QUANTUM_KEYCODES_H