diff --git a/examples/Qukeys/Qukeys.ino b/examples/Qukeys/Qukeys.ino index 3f53c8e1..b1caf1c0 100644 --- a/examples/Qukeys/Qukeys.ino +++ b/examples/Qukeys/Qukeys.ino @@ -1 +1,45 @@ -// -*- mode: c++ -*- \ No newline at end of file +// -*- mode: c++ -*- + +#include +#include + +const Key keymaps[][ROWS][COLS] PROGMEM = { + [0] = KEYMAP_STACKED + ( + Key_NoKey, Key_1, Key_2, Key_3, Key_4, Key_5, Key_NoKey, + Key_Backtick, Key_Q, Key_W, Key_E, Key_R, Key_T, Key_Tab, + Key_PageUp, Key_A, Key_S, Key_D, Key_F, Key_G, + Key_PageDown, Key_Z, Key_X, Key_C, Key_V, Key_B, Key_Escape, + + Key_LeftControl, Key_Backspace, Key_LeftGui, Key_LeftShift, + ___, + + Key_skip, Key_6, Key_7, Key_8, Key_9, Key_0, Key_skip, + Key_Enter, Key_Y, Key_U, Key_I, Key_O, Key_P, Key_Equals, + Key_H, Key_J, Key_K, Key_L, Key_Semicolon, Key_Quote, + Key_skip, Key_N, Key_M, Key_Comma, Key_Period, Key_Slash, Key_Minus, + + Key_RightShift, Key_RightAlt, Key_Spacebar, Key_RightControl, + ___), +}; + + +// Define the Qukeys map +QUKEYS( + Qukey(QWERTY, 2, 1, Key_LeftShift), + Qukey(QWERTY, 2, 2, Key_LeftControl), + Qukey(QWERTY, 2, 3, Key_LeftAlt), + Qukey(QWERTY, 2, 4, Key_LeftGui) + ) + +void setup() { + // Use Qukeys + Kaleidoscope.use(&Qukeys); + Qukeys.init(qukey_list, qukey_count); + + Kaleidoscope.setup(); +} + +void loop() { + Kaleidoscope.loop(); +} diff --git a/src/Kaleidoscope/Qukeys.cpp b/src/Kaleidoscope/Qukeys.cpp index 9d6353c1..48f9942d 100644 --- a/src/Kaleidoscope/Qukeys.cpp +++ b/src/Kaleidoscope/Qukeys.cpp @@ -28,8 +28,191 @@ namespace kaleidoscope { -// constructor -Qukeys::Qukeys(void) { +Qukey::Qukey(int8_t layer, byte row, byte col, Key alt_keycode) { + this->layer = layer; + this->addr = addr::addr(row, col); + this->alt_keycode = alt_keycode; + this->state = QUKEY_STATE_UNDETERMINED; } +Qukeys::Qukeys() {} + +Qukeys::init(Qukey *qukeys, uint8_t qukeys_count) { + qukeys_ = qukeys; + qukeys_count_ = qukeys_count; +} + +int8_t Qukeys::lookupQukey(uint8_t key_addr) { + if (key_addr == QUKEY_UNKNOWN_ADDR) + return QUKEY_NOT_FOUND; + for (int8_t i; i < qukeys_count_; i++) { + Qukey qukey = qukeys_[i]; + if (qukey.addr == key_addr) { + byte row = addr::row(key_addr); + byte col = addr::col(key_addr); + if ((qukey.layer == QUKEY_ALL_LAYERS) || + (qukey.layer == Layer.lookupActiveLayer(row, col))) { + return i; + } + } + } + return QUKEY_NOT_FOUND; +} + +void Qukeys::enqueue(uint8_t key_addr) { + if (key_queue_length_ == QUKEYS_QUEUE_MAX) { + flushKey(QUKEY_STATE_PRIMARY); + } + key_queue_[key_queue_length_].addr = key_addr; + key_queue_[key_queue_length_].flush_time = millis() + time_limit_; + key_queue_length_++; } + +int8_t Qukeys::searchQueue(uint8_t key_addr) { + for (int8_t i = 0; i < key_queue_length_; i++) { + if (key_queue_[i].addr == key_addr) + return i; + } + return -1; +} + +// flush a single entry from the head of the queue +void Qukeys::flushKey(int8_t state) { + int8_t qukey_index = Qukeys.lookupQukey(key_addr); + if (qukey_index != QUKEY_NOT_FOUND) { + qukeys_[qukey_index].state = state; + } + byte row = addr::row(key_queue[0].addr); + byte col = addr::col(key_queue[0].addr); + Key keycode = Key_NoKey; + if (state == QUKEY_STATE_ALTERNATE && qukey_index != QUKEY_NOT_FOUND) { + keycode = qukeys_[qukey_index].alt_keycode; + } else { + keycode = Layer.lookup(row, col); + } + // Since we're in the middle of the key scan, we don't necessarily + // have a full HID report, and we don't want to accidentally turn + // off keys that the scan hasn't reached yet, so we force the + // current report to be the same as the previous one, then proceed + hid::copyPrevKeyboardReport(); + // Instead of just calling pressKey here, we start processing the + // key again, as if it was just pressed, and mark it as injected, so + // we can ignore it and don't start an infinite loop. It would be + // nice if we could use key_state to also indicate which plugin + // injected the key. + handleKeySwitchEvent(keycode, row, col, IS_PRESSED | INJECTED); + // Now we send the report (if there were any changes) + hid::sendKeyboardReport(); + + // Shift the queue, so key_queue[0] is always the first key that gets processed + for (byte i = 0; i < key_queue_length_; i++) { + key_queue_[i] = key_queue_[i + 1]; + } + key_queue_length_--; +} + +void Qukeys::flushQueue(int8_t state, int8_t index) { + for (int8_t i = 0; i <= index; i++) { + if (key_queue_length_ == 0) + break; + flushKey(state); + } +} + +Key Qukeys::keyScanHook(Key mapped_key, byte row, byte col, uint8_t key_state) { + + // If Qukeys is turned off, continue to next plugin + if (!active) + return mapped_key; + + // If the key was injected (from the queue being flushed), continue to next plugin + if (key_state & INJECTED) + return mapped_key; + + // If the key isn't active, and didn't just toggle off, continue to next plugin + if (!keyIsPressed(key_state) && !keyWasPressed(key_state)) + return mapped_key; + + // get key addr & qukey (if any) + uint8_t key_addr = addr::addr(row, col); + int8_t qukey_index = Qukeys.lookupQukey(key_addr); + + // If the key was just pressed: + if (keyToggledOn(key_state)) { + // I think I may need to call maskKey() somewhere here, but I'm not sure + if (key_queue_length) { + enqueue(key_addr); + } else { + if (qukey_index == QUKEY_NOT_FOUND) + return mapped_key; + enqueue(key_addr); + } + } + + // In all other cases, we need to know if the key is queued already + int8_t queue_index = searchQueue(key_addr); + + // If the key was just released: + if (keyToggledOff(key_state)) { + if (queue_index == QUKEY_NOT_FOUND) + return mapped_key; + flushQueue(QUKEY_STATE_ALTERNATE, queue_index); + return Key_NoKey; + } + + // Otherwise, the key is still pressed + + // If the key is not a qukey: + if (qukey_index == QUKEY_NOT_FOUND) { + // If the key was pressed before the keys in the queue: + if (queue_index == QUKEY_NOT_FOUND) { + return mapped_key; + } else { + // suppress this keypress; it's still in the queue + return Key_NoKey; + } + } + + // qukeys that have already decided their keycode + if (qukeys_[qukey_index].state == QUKEY_STATE_PRIMARY) + return mapped_key; + if (qukeys_[qukey_index].state == QUKEY_STATE_ALTERNATE) + return qukeys_[qukey_index].alt_keycode; + // else state is undetermined; block. I could check timeouts here, + // but I'd rather do that in the pre-report hook + return Key_NoKey; +} + +void preReportHook(void) { + // If the qukey has been held longer than the time limit, set its + // state to the alternate keycode and add it to the report + uint32_t current_time = millis(); + for (int8_t i = 0; i < key_queue_length_; i++) { + if (current_time > key_queue_[i].flush_time) { + flushKey(QUKEY_STATE_ALTERNATE); + } else { + break; + } + } +} + +void loopHook(bool post_clear) { + if (!post_clear) + return preReportHook(); +} + +void Qukeys::begin() { + // initializing the key_queue seems unnecessary, actually + for (int8_t i = 0; i < QUKEYS_QUEUE_MAX; i++) { + key_queue_[i].addr = QUKEY_UNKNOWN_ADDR; + key_queue_[i].flush_time = 0; + } + key_queue_length_ = 0; + + Kaleidoscope.useEventHandlerHook(keyScanHook); + Kaleidoscope.useLoopHook(loopHook); +} + +} // namespace kaleidoscope { + +kaleidoscope::Qukeys Qukeys; diff --git a/src/Kaleidoscope/Qukeys.h b/src/Kaleidoscope/Qukeys.h index 92da0b98..ccb27898 100644 --- a/src/Kaleidoscope/Qukeys.h +++ b/src/Kaleidoscope/Qukeys.h @@ -19,19 +19,89 @@ #pragma once #include +#include +// Maximum number of qukeys allowed ­ not actually used +#define QUKEYS_MAX 64 +// Maximum length of the pending queue +#define QUKEYS_QUEUE_MAX 8 + +// Maybe it's better to use an enum for these state values? +#define QUKEY_STATE_UNDETERMINED 0 +#define QUKEY_STATE_PRIMARY 1 +#define QUKEY_STATE_ALTERNATE -1 + +// Initialization addr value for empty key_queue. This seems to be +// unnecessary, because we rely on keeping track of the lenght of the +// queue, anyway. +#define QUKEY_UNKNOWN_ADDR 0xFF +// Value to return when no match is found in Qukeys.dict. A successful +// match returns an index in the array, so this must be negative. Also +// used for failed search of the key_queue. +#define QUKEY_NOT_FOUND -1 +// Wildcard value; this matches any layer +#define QUKEY_ALL_LAYERS -1 namespace kaleidoscope { -class Qukeys : public KaleidoscopePlugin { +// Data structure for an individual qukey +struct Qukey { + public: + Qukey(void) {} + Qukey(int8_t layer, byte row, byte col, Key alt_keycode); + + int8_t layer; + uint8_t addr; + Key alt_keycode; + int8_t state; +}; + +// Data structure for an entry in the key_queue +struct QueueItem { + uint8_t addr; // keyswitch coordinates + uint32_t flush_time; // time past which a qukey gets flushed +}; +// The plugin itself +class Qukeys : public KaleidoscopePlugin { + // I could use a bitfield to get the state values, but then we'd + // have to check the key_queue (there are three states). Or use a + // second bitfield for the indeterminite state. Using a bitfield + // would enable storing the qukey list in PROGMEM, but I don't know + // if the added complexity is worth it. public: Qukeys(void); - void begin(void) final; + static void begin(void) final; + static void activate(void) { + active = true; + } + static void deactivate(void) { + active = false; + } + static int8_t lookupQukey(uint8_t key_addr); private: - -} + static Qukey * qukeys_; + static uint8_t qukeys_count_; + + static bool active_; + static uint16_t time_limit_; + static queue_item_t key_queue_[QUKEYS_QUEUE_MAX]; + static uint8_t key_queue_length_; + + static Key keyScanHook(Key mapped_key, byte row, byte col, uint8_t key_state); + static void preReportHook(void); + static void postReportHook(void) {} + static void loopHook(bool post_clear); +}; + +} // namespace kaleidoscope { + +extern kaleidoscope::Qukeys Qukeys; -} +// macro for use in sketch file to simplify definition of qukeys +#define QUKEYS(qukey_defs...) \ + static kaleidoscope::Qukey qukeys[] = { qukey_defs... }; \ + uint8_t qukeys_count = sizeof(qukeys) / sizeof(kaleidoscope::Qukey); \ + Qukeys.init(qukeys, qukeys_count);