QMK firmwareのTap Holdの動作を改善する

目的

 自作キーボードはQMK firmwareというソフトで動かすのが定石のようですが、このソフト細かい部分で直感的でない動作をするのでその改善方法を探してコードを書いたのでメモを残します。

 QMKではタップとホールドを切り分けていて、あるボタンを押して離すとタップ、そのボタンを押しながら他のボタンを押すとホールドとしています。これにより変換キーのみを推すと変換、変換キーを押しながらNキーを押すとBSキーにするとか、スペースキーを押しながら他のキーを押すとShiftキーとして動作するとかが出来てかなり便利なのですが、細かい反応に少し違和感があります。

 具体的な判定方法は特に設定をしないとキーを押してからTAPPING_TERM(デフォルトでは200ms)で設定した秒数が経過する前に離すとタップと以降はホールドとなります。そのため、ホールドキーとして動作させるためにはTAPPING_TERM分待つ必要があり、速く入力しようとすると誤入力が起きやすく、TAPPING_TERMを短くすると今度はタップを速く入力しないといけなくなります。

 PERMISSIVE_HOLDを定義するとTAPPING_TERM前でも他のキーを押すとホールドと認識してくれるのですがどうやらこれは次の順番でのみ動作し

ホールドキー押下- >他のキー押下->他のキー離す->ホールドキー離す

以下の順番では機能しないようなので、矢張り頻繁に誤入力が発生します。

ホールドキー押下- >他のキー押下->ホールドキー離す->他のキー離す

 また、RETRO_TAPPINGという設定もあり、これを有効にすると他のキーさえ押さなければTAPPING_TERMを越えてもタップと判定してくれますが、当然マウスの押下には反応しないので実用上不便です。

https://qiita.com/chesscommands/items/224f018229fa63b91162#permissive-hold

 なのでこの辺の改善をしようとするとQMKデフォルトの機能は使わず自前で実装するしかないです。以下のサイトで便利なサンプルを公開していただけていたのでこちらを参考にShiftキーのようなモディファイアでも動作するように改造したものを公開します。
https://okapies.hateblo.jp/entry/2019/02/02/133953

このコードにより以下を実現します。

  1. ボタンの押下順序、時間に関係なくホールドキーを押下しながら他のキーを押すとホールドと認識させる
  2. マウスとの連携のための一定時間以上押し続けるとホールドキーを押下する機能
  3. 2.と排他でホールドキー長押しでタップキーをリピートする機能
#include QMK_KEYBOARD_H
#include "keymap_jp.h"

enum custom_keycodes {
  RGBRST = SAFE_RANGE,
  TG_NODOKA,
  TO_DEF,
  LT2_MHEN,
  LT2_HENK,
  CTL_ESC, 
  CTL_APP, 
  ALT_KANA, 
  SFT_SPC, 
  C_S_ESP, 
  ALT_F4, 
  NEXTWIN, 
  MACRO1,
  MACRO2,
  MACRO3,
  MACRO4,
};

enum layer_number {
  _QWERTY = 0,
  _NODOKA,
  _FUNCTION,
  _TENKEY,
  _HENKAN,
  _TEMPLATE,
};

#define TT_TEN   TT(_TENKEY) 
#define TT_FUNC  TT(_FUNCTION) 
#define TG_TEN   TG(_TENKEY) 
#define TG_FUNC  TG(_FUNCTION) 

const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
  [_QWERTY] = LAYOUT( \
  KC_ESC  , KC_SLCK , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , KC_INS  , KC_HOME , KC_END  , KC_BSPC   , \
  KC_ZKHK , KC_1    , KC_2    , KC_3    , KC_4    , KC_5    , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , KC_6    , KC_7    , KC_8    , KC_9    , KC_0    , JP_MINS , JP_CIRC , JP_YEN    , \
  KC_TAB  , KC_Q    , KC_W    , KC_E    , KC_R    , KC_T    , KC_WH_U , XXXXXXX , XXXXXXX , KC_WH_D , KC_Y    , KC_U    , KC_I    , KC_O    , KC_P    , JP_AT   , JP_LBRC , KC_ENT    , \
  KC_CAPS , KC_A    , KC_S    , KC_D    , KC_F    , KC_G    , KC_BTN2 , KC_BTN3 , KC_BTN3 , KC_BTN2 , KC_H    , KC_J    , KC_K    , KC_L    , JP_SCLN , JP_COLN , JP_RBRC , KC_PGUP   , \
  KC_LSFT , KC_Z    , KC_X    , KC_C    , KC_V    , KC_B    , TT_TEN  , KC_BTN1 , KC_BTN1 , TT_TEN  , KC_N    , KC_M    , KC_COMM , KC_DOT  , KC_SLSH , JP_BSLS , KC_RSFT , KC_PGDN   , \
  KC_LCTL , XXXXXXX , KC_LGUI , KC_LALT , KC_MHEN , KC_SPC  , XXXXXXX , TT_FUNC , TT_FUNC , XXXXXXX , KC_SPC  , KC_HENK , KC_KANA , KC_RCTL , XXXXXXX , KC_APP  , XXXXXXX , KC_DEL \
  )       ,
  [_NODOKA] = LAYOUT( \
  _______ , _______ , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , _______ , _______ , _______ , _______   , \
  _______ , _______ , _______ , _______ , _______ , _______ , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______   , \
  _______ , _______ , _______ , _______ , _______ , _______ , _______ , XXXXXXX , XXXXXXX , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______   , \
  _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______   , \
  _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______   , \
  CTL_ESC , XXXXXXX , _______ , _______ , LT2_MHEN, SFT_SPC , XXXXXXX , _______ , _______ , XXXXXXX , SFT_SPC , LT2_HENK, ALT_KANA, _______ , XXXXXXX , CTL_APP , XXXXXXX , _______ \
  )       ,
  [_HENKAN] = LAYOUT( \
  _______ , _______ , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , _______ , _______ , _______ , _______   , \
  _______ , KC_F1   , KC_F2   , KC_F3   , KC_F4   , KC_F5   , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , KC_F6   , KC_F7   , KC_F8   , KC_F9   , KC_F10  , KC_F11  , KC_F12  , _______   , \
  _______ , KC_1    , KC_2    , KC_3    , KC_4    , KC_5    , _______ , XXXXXXX , XXXXXXX , _______ , KC_6    , KC_7    , KC_8    , KC_9    , KC_0    , JP_MINS , JP_CIRC , JP_YEN    , \
  _______ , C_S_ESP , NEXTWIN , KC_INS  , ALT_F4  , KC_END  , _______ , _______ , _______ , _______ , KC_LEFT , KC_DOWN , KC_UP   , KC_RGHT , KC_PGDN , KC_PGUP , _______ , _______   , \
  _______ , _______ , _______ , _______ , _______ , KC_HOME , _______ , _______ , _______ , _______ , KC_BSPC , KC_DEL  , _______ , _______ , _______ , _______ , _______ , _______   , \
  _______ , XXXXXXX , _______ , _______ , _______ , _______ , XXXXXXX , _______ , _______ , XXXXXXX , _______ , _______ , XXXXXXX , _______ , XXXXXXX , _______ , XXXXXXX , _______\
  )       ,
  /* [_FUNCTION] = LAYOUT( \ */
  /* _______ , _______ , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , _______ , KC_MUTE , KC_VOLD , KC_VOLU   , \ */
  /* _______ , _______ , KC_F1   , KC_F2   , KC_F3   , KC_F4   , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , KC_PWR  , _______ , KC_SLEP , _______ , _______ , _______ , _______ , _______   , \ */
  /* _______ , _______ , KC_F5   , KC_F6   , KC_F7   , KC_F8   , TO_DEF  , XXXXXXX , XXXXXXX , _______ , KC_F1   , KC_F4   , KC_UP   , KC_F7   , KC_F11  , KC_INS  , KC_HOME , KC_PGUP   , \ */
  /* _______ , _______ , KC_F9   , KC_F10  , KC_F11  , KC_F12  , _______ , _______ , _______ , _______ , KC_F2   , _______ , KC_UP   , _______ , _______ , KC_DEL  , KC_END  , KC_PGDN   , \ */
  /* _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , KC_F3   , KC_LEFT , KC_DOWN , KC_RGHT , _______ , _______ , _______ , _______   , \ */
  /* _______ , XXXXXXX , _______ , _______ , _______ , _______ , XXXXXXX , _______ , _______ , XXXXXXX , _______ , _______ , XXXXXXX , _______ , XXXXXXX , _______ , XXXXXXX , _______\ */
  /* )       , */
  [_FUNCTION] = LAYOUT( \
  _______ , _______ , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , _______ , _______ , _______ , _______  , \
  _______ , _______ , _______ , _______ , _______ , _______ , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , _______ , _______ , _______ , _______ , _______ , KC_PSCR , KC_SLCK , KC_PAUS  , \
  _______ , KC_F1   , KC_F2   , KC_F3   , KC_F4   , KC_F5   , _______ , XXXXXXX , XXXXXXX , _______ , KC_F6   , KC_F7   , KC_F8   , KC_F9   , KC_F10  , KC_F11  , KC_F12  , _______  , \
  _______ , _______ , KC_PGUP , KC_UP   , KC_PGDN , KC_HOME , _______ , _______ , _______ , _______ , KC_HOME , KC_PGDN , KC_UP   , KC_PGUP , _______ , KC_INS  , KC_HOME , KC_PGUP  , \
  _______ , _______ , KC_LEFT , KC_DOWN , KC_RGHT , KC_END  , _______ , _______ , _______ , _______ , KC_END  , KC_LEFT , KC_DOWN , KC_RGHT , _______ , KC_DEL  , KC_END  , KC_PGDN  , \
  _______ , XXXXXXX , _______ , _______ , _______ , _______ , XXXXXXX , TG_FUNC , TG_FUNC , XXXXXXX , _______ , _______ , XXXXXXX , _______ , XXXXXXX , _______ , XXXXXXX , _______\
  )       ,
  [_TENKEY] = LAYOUT( \
  _______ , _______ , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , _______ , _______ , _______ , _______ , \
  _______ , _______ , MACRO1  , MACRO2  , MACRO3  , MACRO4  , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , _______ , KC_NLCK , KC_PSLS , KC_PAST , KC_BSPC , _______ , _______ , _______ , \
  _______ , _______ , _______ , _______ , _______ , _______ , _______ , XXXXXXX , XXXXXXX , _______ , _______ , KC_P7   , KC_P8   , KC_P9   , KC_PMNS , _______ , _______ , _______ , \
  _______ , _______ , _______ , KC_UP   , _______ , _______ , _______ , _______ , _______ ,TG_NODOKA, _______ , KC_P4   , KC_P5   , KC_P6   , KC_PPLS , _______ , _______ , _______ , \
  _______ , _______ , KC_LEFT , KC_DOWN , KC_RGHT , _______ , _______ , _______ , _______ , TG_TEN  , _______ , KC_P1   , KC_P2   , KC_P3   , KC_PENT , _______ , _______ , _______   , \
  _______ , XXXXXXX , _______ , _______ , _______ , _______ , XXXXXXX , _______ , _______ , XXXXXXX , _______ , KC_P0   , XXXXXXX , KC_PDOT , XXXXXXX , _______ , XXXXXXX , _______\
  )       ,
  [_TEMPLATE] = LAYOUT( \
  _______ , _______ , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , _______ , _______ , _______ , _______   , \
  _______ , _______ , _______ , _______ , _______ , _______ , XXXXXXX , XXXXXXX , XXXXXXX , XXXXXXX , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______   , \
  _______ , _______ , _______ , _______ , _______ , _______ , _______ , XXXXXXX , XXXXXXX , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______   , \
  _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______   , \
  _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______ , _______   , \
  _______ , XXXXXXX , _______ , _______ , _______ , _______ , XXXXXXX , _______ , _______ , XXXXXXX , _______ , _______ , XXXXXXX , _______ , XXXXXXX , _______ , XXXXXXX , _______\
  )
};

static bool mhen_pressed = false;
static bool henk_pressed = false;
static bool ctl_esc_pressed = false;
static bool ctl_app_pressed = false;
static bool alt_kana_pressed = false;
static bool sft_spc_pressed = false;
static uint16_t mhen_pressed_time = 0;
static uint16_t henk_pressed_time = 0;
static uint16_t ctl_esc_pressed_time = 0;
static uint16_t ctl_app_pressed_time = 0;
static uint16_t alt_kana_pressed_time = 0;
static uint16_t sft_spc_pressed_time = 0;
static bool is_alt_tab_active = false;
static bool is_lctl_active = false;
/* static bool exceptionaly_lctl_layer_pressed = false; */
static uint16_t alt_tab_timer = 0;

// user_lt(record, ホールド時以降先レイヤー, タップ時のキーコード, モディファイアキー押下判定のための変数, trueならTAPPING_TERMより長押ししたときにタップと判定する)
static void user_lt(keyrecord_t *record, int layer, uint16_t keycode, bool *modifier_pressed, uint16_t *modifier_pressed_time, bool tapping_term_disable) {
        if (record->event.pressed) {
        *modifier_pressed = true;
        *modifier_pressed_time = record->event.time;

        layer_on(layer);
      } else {
        layer_off(layer);

        if (*modifier_pressed && (tapping_term_disable || (timer_elapsed(*modifier_pressed_time) < TAPPING_TERM))) {
          register_code(keycode);
          unregister_code(keycode);
        }
        *modifier_pressed = false;
      }
}

// user_lt(record, ホールド時キーコードー, タップ時のキーコード, モディファイアキー押下判定のための変数, trueならTAPPING_TERMより長押ししたときにタップと判定する)
static void user_mt(keyrecord_t *record, uint16_t modcode, uint16_t keycode, bool *modifier_pressed, uint16_t *modifier_pressed_time, bool tapping_term_disable) {
        if (record->event.pressed) {
        *modifier_pressed = true;
        *modifier_pressed_time = record->event.time;
      } else {
	if (!*modifier_pressed) unregister_code(modcode);
        if (*modifier_pressed && (tapping_term_disable || (timer_elapsed(*modifier_pressed_time) < TAPPING_TERM))) {
          register_code(keycode);
          unregister_code(keycode);
        }
        *modifier_pressed = false;
      }
}

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  switch (keycode) {
    case LT2_MHEN:
      user_lt(record, _HENKAN, KC_ENT, &mhen_pressed, &mhen_pressed_time, true);
      return false;
      break;
    case LT2_HENK:
      user_lt(record, _HENKAN, KC_HENK, &henk_pressed, &henk_pressed_time, true);
      return false;
      break;
    case CTL_ESC:
      user_mt(record, KC_LCTL, KC_ESC, &ctl_esc_pressed, &ctl_esc_pressed_time, true);
      if (!record->event.pressed) is_lctl_active = false;
      return false;
      break;
    case CTL_APP:
      user_mt(record, KC_RCTL, KC_APP, &ctl_app_pressed, &ctl_app_pressed_time, true);
      return false;
      break;
    case ALT_KANA:
      user_mt(record, KC_RALT, KC_KANA, &alt_kana_pressed, &alt_kana_pressed_time, true);
      return false;
      break;
    case SFT_SPC:
      user_mt(record, KC_LSFT, KC_SPC, &sft_spc_pressed, &sft_spc_pressed_time, true);
      return false;
      break;
    default:
      if (record->event.pressed) {
        // reset the user_lt flags
        mhen_pressed = false;
        henk_pressed = false;

	// operate user_mt flags
        if (ctl_esc_pressed) {register_code(KC_LCTL); is_lctl_active = true;}
        ctl_esc_pressed  = false;
        if (ctl_app_pressed) register_code(KC_RCTL);
        ctl_app_pressed = false;
        if (alt_kana_pressed) register_code(KC_RALT);
        alt_kana_pressed = false;
        if (sft_spc_pressed) register_code(KC_LSFT);
        sft_spc_pressed  = false;

      }
      switch (keycode) {  // usr_lt, mtと併用したいキーはこちらで定義
        case TO_DEF:
          if (record->event.pressed) {
            layer_move(default_layer_state);
          }
          return false;
          break;
        case TG_NODOKA:
          if (record->event.pressed) {
            if (default_layer_state == _NODOKA) {
              default_layer_state = _QWERTY;
              layer_off(_NODOKA);
            } else {
              default_layer_state = _NODOKA;
              layer_on(_NODOKA);
            }
          }
	  return false;
          break;
        case KC_I: // LCTL + I = TAB
          if (record->event.pressed) {
            if (is_lctl_active && layer_state_is(_NODOKA)) {
              unregister_code(KC_LCTL);
              register_code(KC_TAB);
              register_code(KC_LCTL);
            } else {
              register_code(KC_I);
            }
          } else {
            if (is_lctl_active && layer_state_is(_NODOKA)) {
              unregister_code(KC_TAB);
            } else {
              unregister_code(KC_I);
            }
          }
          return false;
          break;
        case ALT_F4:
          if (record->event.pressed) {
            register_code16(A(KC_F4));
          } else {
            unregister_code16(A(KC_F4));
          }
          return false;
          break;
        case C_S_ESP:
          if (record->event.pressed) {
            register_code16(S(C(KC_ESC)));
          }
          return false;
          break;
        case MACRO1:
          if (record->event.pressed) {
             SEND_STRING(MACRO1_STRINGS);
          }
          return false;
          break;
        case MACRO2:
          if (record->event.pressed) {
             SEND_STRING(MACRO2_STRINGS);
          }
          return false;
          break;
        case MACRO3:
          if (record->event.pressed) {
             SEND_STRING(MACRO3_STRINGS);
          }
          return false;
          break;
        case MACRO4:
          if (record->event.pressed) {
             SEND_STRING(MACRO4_STRINGS);
          }
          return false;
      	case NEXTWIN: // ALT+TAB
        if (record->event.pressed) {
          if (!is_alt_tab_active) {
            is_alt_tab_active = true;
            register_code(KC_LALT);
          }
          alt_tab_timer = timer_read();
          register_code(KC_TAB);
        } else {
          unregister_code(KC_TAB);
        }
        break;
      }
  }
  return true;
}

// 定期実行される関数
void matrix_scan_user(void) {
   // Alt + tab用
  if (is_alt_tab_active) {
    if (timer_elapsed(alt_tab_timer) > 600) {
      unregister_code(KC_LALT);
      is_alt_tab_active = false;
    }
  }
   // TAPPING_TERM以降はモディファイアキーを押下
  if (ctl_esc_pressed && timer_elapsed(ctl_esc_pressed_time) > TAPPING_TERM) {register_code(KC_LCTL); is_lctl_active = true; ctl_esc_pressed = false;}
  if (ctl_app_pressed && timer_elapsed(ctl_app_pressed_time) > TAPPING_TERM) {register_code(KC_RCTL); ctl_app_pressed  = false;}
  if (alt_kana_pressed && timer_elapsed(alt_kana_pressed_time) > TAPPING_TERM) {register_code(KC_RALT); alt_kana_pressed = false;}
  if (sft_spc_pressed && timer_elapsed(sft_spc_pressed_time) > TAPPING_TERM) {register_code(KC_LSFT); sft_spc_pressed  = false;}
}

custom_keycodes とレイヤーにホールドタップで動作させたいキーを追加します。ここではLT2_HENKとCTL_ESCを例に説明します。

enum custom_keycodes {
  RGBRST = SAFE_RANGE,
  TG_NODOKA,
  TO_DEF,
  LT2_MHEN,
  LT2_HENK,
  CTL_ESC, 
  CTL_APP, 
  ALT_KANA, 
  SFT_SPC, 
  C_S_ESP, 
  ALT_F4, 
  NEXTWIN, 
  MACRO1,
  MACRO2,
  MACRO3,
  MACRO4,
};

 次に変換、LCtlが押されていて押している最中に他のキーが押されていないならばTrueとなるフラグhenk_pressedとctl_esc_pressed を定義します。TAPPING_TERMを動作させるため、henk_pressed_time と ctl_esc_pressed_time も定義します。

static bool henk_pressed = false;
static bool ctl_esc_pressed = false;
static uint16_t henk_pressed_time = 0;
static uint16_t ctl_esc_pressed_time = 0;

 process_record_user関数で使用するための関数を定義します。レイヤーを移行するホールドタップキーはuser_lt, Ctrlなどのモディファイアキーでホールドタップする場合はuser_mt関数を使用します。

// user_lt(record, ホールド時以降先レイヤー, タップ時のキーコード, モディファイアキー押下判定のための変数, TAPPING_TERMより長押ししたときもタップと判定するか)
static void user_lt(keyrecord_t *record, int layer, uint16_t keycode, bool *modifier_pressed, uint16_t *modifier_pressed_time, bool tapping_term_disable) {
        if (record->event.pressed) {
        *modifier_pressed = true;
        *modifier_pressed_time = record->event.time;

        layer_on(layer);
      } else {
        layer_off(layer);

        if (*modifier_pressed && (tapping_term_disable || (timer_elapsed(*modifier_pressed_time) < TAPPING_TERM))) {
          register_code(keycode);
          unregister_code(keycode);
        }
        *modifier_pressed = false;
      }
}

// user_mt(record, ホールド時キーコードー, タップ時のキーコード, モディファイアキー押下判定のための変数, キー押下時間判定のための変数, TAPPING_TERMより長押ししたときもタップと判定するか)
static void user_mt(keyrecord_t *record, uint16_t modcode, uint16_t keycode, bool *modifier_pressed, uint16_t *modifier_pressed_time, bool tapping_term_disable) {
        if (record->event.pressed) {
        *modifier_pressed = true;
        *modifier_pressed_time = record->event.time;
      } else {
	if (!*modifier_pressed) unregister_code(modcode);
        if (*modifier_pressed && (tapping_term_disable || (timer_elapsed(*modifier_pressed_time) < TAPPING_TERM))) {
          register_code(keycode);
          unregister_code(keycode);
        }
        *modifier_pressed = false;
      }
}

 process_record _user関数はキー押し離しされるごとに呼び出されるのでここでホールドタップの処理をします。
 初めのユーザー定義のキーに対してレイヤー移行する場合はuser_ltは(record, ホールド時の以降先レイヤー, タップ時のキーコード, キー押下判定のための変数, キー押下時間判定のための変数、TAPPING_TERMより長押ししたときもタップと判定するか)を順に指定して呼び出します。このときキー押下判定のための変数, キー押下時間判定のための変数は先頭に&を付けてポインタを渡してください。
 モディファイアキーの場合もuser_mtに(record, ホールド時キーコード, タップ時のキーコード, モディファイアキー押下判定のための変数, キー押下時間判定のための変数 TAPPING_TERMより長押ししたときもタップと判定するか)の順に指定して呼び出します。やはりキー押下判定のための変数, キー押下時間判定のための変数は先頭に&を付けてポインタを渡してください。

 つづいてdefautつまり他のキーが押された場合はもうタップとしては動作しないのでレイヤー移行する場合のフラグはfalseにします。
 モディファイアの場合は他のキーを押されたらモディファイアを押下してからリセットします。 このdefaultではreturn, breakをしないようにしてください。

 続いてもう一つswitch構文を使ってホールドタップ以外のキーの場合の処理をします。これはこの順番にしないとホールドタップキーのホールドを後に 続くキーに使えないからです。
 例えばLCTLを押しながらKC_Iを押すとTabになるようにしているのですが、この順番でないとLCTLキーを押していると判定できませんし、SPACEキーやAltキーでホールドタップを利用している場合も併用できません

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  switch (keycode) {
    case LT2_HENK:
      user_lt(record, _HENKAN, KC_HENK, &henk_pressed, &henk_pressed_time, true);
      return false;
      break;
    case CTL_ESC:
      user_mt(record, KC_LCTL, KC_ESC, &ctl_esc_pressed, &ctl_esc_pressed_time, true);
      if (!record->event.pressed) is_lctl_active = false;
      return false;
      break;
    default:
      if (record->event.pressed) {
        // 他のキーを押されたらリセット
        henk_pressed = false;

        // モディファイアの場合は他のキーを押されたらモディファイアを発行してからリセット
        if (ctl_esc_pressed) {register_code(KC_LCTL) is_lctl_active = true;}
        ctl_esc_pressed  = false;

      }
      switch (keycode) {  // usr_lt, mtと併用したいキーはこちらで定義
        case KC_I: // LCTL + I = TAB
          if (record->event.pressed) {
            if (is_lctl_active && layer_state_is(_NODOKA)) {
              unregister_code(KC_LCTL);
              register_code(KC_TAB);
              register_code(KC_LCTL);
            } else {
              register_code(KC_I);
            }
          } else {
            if (is_lctl_active && layer_state_is(_NODOKA)) {
              unregister_code(KC_TAB);
            } else {
              unregister_code(KC_I);
            }
          }
          return false;
          break;
        case ALT_F4:
          if (record->event.pressed) {
            register_code16(A(KC_F4));
          } else {
            unregister_code16(A(KC_F4));
          }
          return false;
          break;

 マウスとの連携のため、TAPPING_TERM以降もボタン押しつづけたらモディファイアを押下するようにします。これはレイヤー移行の方では特にやりたくなければやらなくてよいでしょう。matrix_scan_user関数を定義すると定期的に実行してくれます

// 定期実行される関数
void matrix_scan_user(void) {
   // Alt + tab用
  if (is_alt_tab_active) {
    if (timer_elapsed(alt_tab_timer) > 600) {
      unregister_code(KC_LALT);
      is_alt_tab_active = false;
    }
  }
   // TAPPING_TERM以降はモディファイアキーを押下
  if (ctl_esc_pressed && timer_elapsed(ctl_esc_pressed_time) > TAPPING_TERM) {register_code(KC_LCTL); is_lctl_active = true; ctl_esc_pressed = false;}
}

 マウスとの連携は出来なくなりますが、もし長押しでタップキーをリピートしたい場合はコードの先頭に次ぎの定義を追加し、

#define REPEAT_START_TIME 500  // リピート開始までの長押し時間(ms)
#define REPEAT_CYCLE 30  // リピートサイクル(ms)
static int last_ctrl_pressed_time = 0;

先ほどのコードを次ぎのように変更するとctrl長押しでescをリピートするようになります。

// 定期実行される関数
void matrix_scan_user(void) {
   // Alt + tab用
  if (is_alt_tab_active) {
    if (timer_elapsed(alt_tab_timer) > 600) {
      unregister_code(KC_LALT);
      is_alt_tab_active = false;
    }
  }
   // REPEAT_START_TIME以降はタップキーをリピート
  if (ctl_esc_pressed && timer_elapsed(ctl_esc_pressed_time) > REPEAT_START_TIME) {
    if (timer_elapsed(last_ctrl_pressed_time) > REPEAT_CYCLE){
      register_code(KC_ESC);
      unregister_code(KC_ESC);
      last_ctrl_pressed_time = ctl_esc_pressed_time+ timer_elapsed(ctl_esc_pressed_time);
    }
  }

}

追記 2021/07/10
 上記のコードでは長押し時のタップホールドのキー同士で上手く動作しなかったのでprocess_record_user関数の最初の部分を以下のように変更してください。最初のif (record->event.pressed)の部分で押下キーがタップホールドで定義したキーでなければ対応する押下のフラグをfalseにします。ホールドキーがモディファイアキーの場合は同時にそのモディファイアキーをregister_code関数で押下します。例では is_lctl_activeもTrueにしていますが、これはvimにLCtrl+Iでタブになる挙動を再現するためで必要なければいりません。その後のswitchから先の部分で上述のようにタップホールドのキーの処理をします。

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
  if (record->event.pressed) {
    // reset the user_lt & user_tt flags
    if (keycode != LT2_MHEN) {mhen_pressed = false;}

    // operate user_mt flags
    if (keycode != CTL_ESC){
      if (ctl_esc_pressed) {register_code(KC_LCTL); is_lctl_active = true;}
      ctl_esc_pressed  = false;
    }
  }

  switch (keycode) {
    case LT2_MHEN:
      user_lt(record, _HENKAN, KC_ENT, &mhen_pressed, &mhen_pressed_time, true);
      return false;
      break;
    case CTL_ESC:
      user_mt(record, KC_LCTL, KC_ESC, &ctl_esc_pressed, &ctl_esc_pressed_time, true);
      if (!record->event.pressed) is_lctl_active = false;
      return false;
      break;

    case KC_I: // LCTL + I = TAB
      if (record->event.pressed) {
        if (is_lctl_active && layer_state_is(_NODOKA)) {
          unregister_code(KC_LCTL);
          register_code(KC_TAB);
          register_code(KC_LCTL);
        } else {
          register_code(KC_I);
        }
      } else {
        if (is_lctl_active && layer_state_is(_NODOKA)) {
          unregister_code(KC_TAB);
        } else {
          unregister_code(KC_I);
        }
      }
      return false;
      break;

}

“QMK firmwareのTap Holdの動作を改善する” への2件の返信

  1. ピンバック: タップホールドの挙動を修正する – blog-dog

  2. 詳細な説明ありがとうございます。
    qmkのデフォルト機能では細かい部分で使いづらさを感じていたので、非常に助かりました。

コメントを残す

メールアドレスが公開されることはありません。