Pull to refresh

Не хватает клавиш? (upd)

Level of difficultyEasy
Reading time23 min
Views7.2K

Сколько разных значений вы можете ввести нажатием одной клавиши? Так, на клавиатуре 33 клавиши в буквенном блоке, 13 в цифровом ряду, получается 46. А двумя нажатиями? Выходит 46×46, и ещё 46 – те же клавиши с Shift. Всего чуть больше двух тысяч, и это просто случайные сочетания букв, цифр и пунктуации.

Что если правильные ответы – сотни для одного нажатия, и десятки тысяч для двух? Это далеко не предел. И эти значения – не случайные пары символов, а кнопка "мой рабочий емейл" или "текущая дата", символы осо́бой пунктуации, специфичные языковые символы, кнопка для перевода с транслита, исправления регистра, запуска приложения, и даже "включить музыку через 20 минут" или "прогноз погоды". И для этого вам не нужно учить наизусть таблицу юникод или хитрые сочетания. Вы сами определяете, что и где будет находиться, никак не меняя базовую функциональность.

Нет, это не клавиатура с тысячью кнопок, и даже не прошиваемая механика. Самая обычная клавиатура, которая сейчас у вас под рукой. Как это возможно? Обо всём по порядку.


Предпосылки

В 2018 году в мире кастомных механических клавиатур (нет, статья совсем не о них) стала набирать популярность идея с забавным названием – Tap Dance.

В сообществе уже вполне устоявшейся была концепция мультинажатий, где у каждой клавиши может быть отдельное действие при двойном, тройном, и дальнейших нажатиях.
Также широко известна была и идея разделения обычных нажатий и нажатий с небольшим удержанием, с отдельным действием для каждого.

Объединив эти два подхода, получился Tap Dance, в котором комбинацией нажатий одной клавиши с опциональными удержаниями мы получаем огромное количество возможных назначений. Ведь всего на четырёх нажатиях с удержаниями мы можем разместить целую... азбуку Морзе? Кажется, идея не так нова, хотя стоит признать ценность её адаптации, особенно когда в массовом сознании имеется твёрдая убеждённость, что клавиша = символ, причём символ весьма конкретный.

Из-за того, что термин в себе уже объединяет две популярные концепции, и часто упоминается вместе с прочими соседями, вроде аккордного ввода, и не без метко заимствованного названия, впоследствии он также приобрёл значение просто общего стиля, подхода к использованию клавиатуры, включающий в себя весь этот "экзотический" функционал. Но так как реализован каноничный TapDance и его соседи на аппаратном уровне, подавляющему большинству он, увы, попросту недоступен. Был.

В общем, речь пойдёт об одном вольном программном переложении для самых обычных клавиатур всего описанного функционала, и даже чуть шире. Но не только на одной клавише, ведь, хоть эта идея звучит интересно, вряд ли вы всерьёз захотите вбивать морзянку ради одного назначения.

Добавим также аккордный ввод, кнопку перевода с транслита, переключение в эмодзи-режим (простите 😊), тот самый ввод азбукой Морзе, просто шутки ради, и всё, что ещё захотите. Если раньше вы не знали, как добавить новый символ на клавиатуру – через час не будете знать, чем же ещё можно занять это место.
Приступим.


Как работает – Структура дереваЛогика переходов
Что работает – GUIСлоиРеализация

Структура

tl;dr

Расширяя базовую идею Tap Dance, разрешаем добавлять назначения к последовательностям любых клавиш, а не только одной и той же, сохраняя возможность tap/hold ветвления.

Для этого реализуем древовидную структуру, где каждое нажатие меняет активную таблицу назначений по обозначенному переходу. Когда доходим до листа или прерываемся на узле – возвращаемся к корню (если пользователь не запретил возврат данному назначению).

Также добавляем поддержку пользовательских модификаторов, нажатие которых добавляет 1<<значение_модификатора к текущему узлу, не вызывая отдельный переход. Все сохраняемые назначения учитывают текущее значение модификатора, а hold рассматривается как значение_модификатора+1. Базовые назначения без модификаторов – 0 и 1 (для tap и hold), с модификатором 1 – 2 и 3, и т.д.

Итоговый узел/рабочая таблица/активная таблица назначений:
На первом уровне хэш-таблица с ключами сканкодами, на втором – хэш-таблица со значениями модификаторов, третий уровень – набор [поля_назначения*, вложенная_таблица_переходов].

Тип на удержании также может обозначать принадлежность к аккорду/комбо. Аккорды повторяют структуру таблицы назначений, только вместо ключей-сканкодов используют ключи-буферы_нажатий в hex представлении. Лежат рядом с таблицей сканкодов на каждом переходе.

У каждой раскладки собственное дерево, назначения в которых происходят раздельно. К каждому дереву конкретной раскладки на старте приложения добавляются глобальные назначения, собирая все назначения в единое дерево для каждой раскладки. При смене раскладки мы просто переходим к новому корню.

В итоге доступны любые комбинации описанных действий для добавления новых назначений:
q_base->w_hold->аккорд_123->модификатор3+e_hold->что-угодно->…

Дальше >

Чтобы не нарушать последовательность, оттолкнёмся от базовых составляющих концепции Tap Dance, но не привязывайтесь к идее одной клавиши, это лишь старт.

Каноничный Tap Dance

В простейшей реализации мультинажатий, как первой составляющей, мы по каждому нажатию запускаем ожидание следующего нажатия этой же клавиши, и в зависимости от того, произошло ли оно, отправляем или значение одиночного нажатия, или двойного. Для тройного повторяем ожидание ещё раз. И так далее, по необходимости. Хранить мы бы это стали в массиве. Угловато, но просто – клавиша[глубина]=действие.

Ещё проще дело обстоит с tap/hold, здесь у нас всего-то два значения, один if.

Но для их объединения простого массива будет недостаточно, и уже нужно говорить о вложенности и переходах. То есть мы говорим о бинарном дереве с переходами к следующему узлу по нажатиям или удержаниям. Выполнение действия, в простейшем случае – ввод, происходит на листьях, или в случае прерывания на каком-то узле.

Расширяем дерево

Итак, у нас есть таблица клавиш, которая хранит бинарные деревья переходов. Почему бы не объединить эти уровни? Что если наши переходы будут вести не просто к выбору "нажал или удержал", а к новой, полной таблице клавиш? Всего с одним уровнем вложенности и ветвлением tap/hold мы получим место под (количество_клавиш*2)^2 назначений. Уже в буквенной части поместятся все кандзи. На третьем уровне дважды поместятся все текущие назначения юникод. Заниматься мы этим, конечно, не будем, но простор открыт. ничего святого

Теперь наша структура выглядит как хэш-таблица клавиш (сканкодов – идентификаторов физических клавиш), по которым хранится два массива – для нажатий и удержаний, каждый из которых содержит значение действия и вложенную таблицу клавиш для перехода. Так мы можем не контролировать уровень глубины перехода, не создавать отдельную логику для обработки корневого значения и прочие связанные вещи. Мы просто храним ссылку на корень и на текущий узел – "рабочую таблицу", каждый раз рассматривая только её, безотносительно её положения.

Модификаторы

Хотелось бы также реализовать и пользовательские модификаторы. Но создавать им такие же вложенные переходы не получится, ведь это отразится на комбинируемых модификаторах – нужно будет дублировать переход для каждого из задействованных модификаторов и дублировать их самих на прочих переходах.

Более оптимальный вариант – если модификаторы не будут иметь собственного перехода, и все будут работать в рамках одной таблицы, просто изменяя общую переменную значения текущего модификатора. Для этого каждому назначаемому модификатору добавим числовое значение, на которое и будет увеличиваться/уменьшаться наше значение_модификатора.

По сканкодам у нас уже хранятся базовое значение и значение при удержании, как элементы 0 и 1, без модификаторов. 0 без модификаторов. Напрашивается, что 2, 3, и далее это и будут наши модификаторы. С двумя допущениями – это также нужно будет из массива перевести в хэш-таблицу, чтобы не создавать ограничение на последовательное добавление модификаторов; а также, чтобы было меньше пересечений на комбинируемых модификаторах, будем использовать не чистое значение модификатора, а сдвиг 1 << значение. Так у нас не получится, что модификаторы 2 и 3 вместе приведут к назначениям модификатора 5. К тому же, так мы сможем и для переменной текущего модификатора просто устанавливать/отключать биты, а не добавлять/убавлять, что избавит нас от потенциальных ошибок лишнего добавленного значения модификатора.

Модификаторы не отменяют tap/hold, а дополняют его. Так значения 2 и 3 – это те же tap/hold, только с добавленным модификатором 1. За счёт того, что мы используем не чистое значение модификатора, у нас теперь всегда (за исключением модификатора 0), значение нажатия это текущий_модификатор, а удержания – текущий_модификатор+1. Альтернативным вариантом было бы просто располагать их парами в массиве по каждому модификатору, но так пришлось бы добавить ещё один уровень к структуре.

Типы значений

Добавленные модификаторы подсказывают, что теперь нам нужно добавить что-то, что поможет нам отличать их от обычных удержаний. Хранить их отдельно не имеет смысла, если они отлично вписываются в существующую структуру – они не могут работать вместе с обычным действием при удержании, и логически ближе к ним. При этом они не мешают базовым нажатиям.

Значит мы можем просто описывать их вместо действия при удержании, но добавив подсказку типа. Так как типы нам всё равно понадобятся, это ни капли не усложняет структуру.

Почти финальный вид узла:

{
  <клавиша>: {
    <модификатор>: [тип, действие/значение, {следующий_узел}],
  },
}

Где все чётные "модификаторы" – базовые значения, нечётные – при удержании.

Аккорды

Ещё одна функциональность, которую хотелось бы реализовать и сделать частью нашей структуры – аккорды (также часто применяется термин "combo", но есть разные трактовки). Это также триггер действия, который срабатывает при одновременном нажатии заданного сочетания клавиш.

Казалось бы, что аккорды совсем не вписываются в структуру, ведь мы работаем с каждой отдельной клавишей по отдельному модификатору, и нам понадобится собирать все значения аккордовых клавиш. Но замените в нашей рабочей таблице слово <клавиша> на <аккорд>, и всё сойдётся. Конечно, просто заменять клавиши аккордами мы не будем, а вот повторить эту структуру в соседней таблице, почему бы и нет:

[
  {  // таблица сканкодов
    <клавиша>: {
      <модификатор>: [тип, действие/значение, {следующий_узел}],
    },
  },
  {  // таблица аккордов
    <сочетание>: {
      <модификатор>: [тип, действие/значение, {следующий_узел}],
    },
  }
]

Минимальные отличия в логике, абсолютно та же структура, лишь с другим типом ключа. Те же поддержки модификаторов, те же переходы к следующим узлам.

Проверка сочетаний – не самое дешёвое дело, потому не стоит выполнять её при каждом нажатии. Лучше бы указать где-то тип часть_аккорда задействованным клавишам, чтобы выполнять проверку только когда одна из клавиш группы срабатывает. И указать это можно, опять же, в типе по удержанию, ведь аккордовая клавиша логически не может выполнять какое-либо действие по удержанию или быть модификатором.

Языковые зависимости

И последнее дополнение, прежде чем мы закроем этот блок:

До сих пор мы говорили о единственном дереве, и оно вполне актуально, как дерево глобальных назначений, которые будут активны всегда. Но у нас может возникнуть потребность в зависимых от языка/раскладки назначениях. Тогда стоило бы объявить отдельные деревья для каждой из раскладок, и при смене системной раскладки менять ссылку на актуальный корень.

А чтобы не выполнять одновременно и глобальные и языковые переходы, с дублированием проверок в каждом из них, это было бы странно, на старте приложения просто будем дополнять каждое языковое дерево глобальными назначениями, и использовать только корни от конкретных раскладок.

Основная логика

tl;dr

Так как базовых события, от которых мы можем оттолкнуться всего два – key down и key up, – всю логику и взаимодействия нам нужно обвязывать вокруг них. При этом нагрузка на key up минимальная – он служит лишь для сброса состояния.

В процессе разбора нажатий нам часто придётся откладывать ввод в ожидании следующего нажатия – для этого нам понадобятся отдельные глобальные переменные для ожидающего значения цепочки и ожидающего значения перед удержанием.

Итого для основной функциональности между нажатиями у нас будут переходить:

  • уже обсуждённая активная рабочая таблица / узел

  • константная таблица корней от раскладок

  • текущее значение модификатора

  • во время ожидания следующего нажатия в цепочке – значение последнего нажатия

  • во время проверки удержания – значение базового нажатия

  • а также два битовых буфера – для нажатых модификаторов, и для всех остальных нажатий

Центральная логика при нажатии максимально проста: мы смотрим, есть ли в нашей активной таблице переходов пришедший сканкод и есть ли в нём ключ текущего модификатора – current_node[scancode][current_mod]. Если чего-то нет, необходимо передать/пропустить нативное нажатие, то есть не вмешиваться в процесс ввода.

Если значение есть, проверяем, есть ли назначение на удержании – current_node[scancode][current_mod + 1], если его нет – выходим в первую и основную ветку:

Базовая обработка без назначенного действия удержания

  • отправляем на обработку массив обычного нажатия

  • в обработке { проверяем, есть ли значения в таблице переходов пришедшего узла

  • если переходы есть
    • сохраняем узел как "последнее значение цепочки"
    • меняем ссылку активной таблицы переходов на вложенную
    • запускаем таймер передачи последнего значения/действия, который мы сбрасываем при каждом новом нажатии
    • когда/если таймер срабатывает – выполняем последнее действие (в простейшем случае – ввод символа), сбрасываем переменную, и возвращаем ссылку текущего рассматриваемого узла к корню по текущей раскладке

  • если переходов при обработке не нашлось – сразу выполняем действие и сбрасываем таблицу к корню. }

Таким образом, при каждом нажатии мы сбрасываем предыдущий таймер, переходим на следующий уровень цепочки, и так пока не дойдём до листа, или пока не прервёмся таймером, что в любом случае выполнит последнее действие и сбросит таблицу к корню.

Действие при удержании

Вторая ветка, когда у нас есть действие при удержании, не особо сложнее. Мы добавляем проверку удержания, и в зависимости от результата отправляем на обработку либо узел от базового нажатия, либо от удержания. Но с двумя дополнениями:

  • чтобы у нас не начиналось новое key_down событие от удержания пока мы проверяем его, включим бит по текущему сканкоду в буфере, а в начало key_down обработчика добавим проверку, что бит от пришедшего нажатия мы ещё не трогали. Снимаем бит только на key_up событии (и в некоторых специфичных случаях, но оставим совсем уж частности за скобками).

  • если во время ожидания у нас произойдёт нажатие другой клавиши, нам нужно завершить предыдущую обработку. Для этого перед началом проверки удержания сохраним текущее значение от нажатия в глобальную переменную pending. Если мы выполнили чистую проверку удержания, и отправили нужный узел на обработку, в конце мы сбросим данную переменную. И добавим новый блок в начало key_down обработчика: если у нас есть не пустой pending, то есть мы в ожидании удержания, но уже делаем следующее нажатие, – форсируем обработку по базовому значению из pending, и сбрасываем его, вместе с проверкой ожидания удержания. Проверка клавиши, нажатие которой вызвало этот блок, продолжается без изменений, но уже с обновлённой ссылкой на текущий узел.

Модификатор

Проверку типа модификатор мы выполняем чуть иначе – без влияния прочих, уже активных модификаторов, то есть всегда от значения модификатора 1. Это нужно для комбинируемых модификаторов, иначе их придётся перекрёстно назначать в каждом сочетании.

Тем не менее, если тип удержания – модификатор, мы включаем бит в соответствующем буфере, и увеличиваем глобальную переменную текущего значения модификаторов. А вkey_up добавляем проверку бита по сканкоду, и если срабатывает, выключаем его и уменьшаем переменную значения модификаторов.

Но до этого мы также запоминаем последнее значение цепочки от обычного нажатия с уже активными прочими модификаторами, и добавляем таймер сброса этого значения. Также мы добавляем в key_up маленький блок: если есть последнее значение – отдай его на обработку и отмени таймер сброса.
Так наш модификатор не теряет функции обычного нажатия – если мы просто нажали модификатор, то отправится назначение от обычного нажатия. Если мы удержали модификатор, подумали, нужен ли он нам, и отпустили – просто ничего не произойдёт. А если через модификатор нажали хоть одну клавишу, последнее значение будет перезаписано и модификатор не отправит своё базовое значение, как бы быстро вы его не отжали.

Аккорд

И последняя ветка – если мы видим тип удержания часть_аккорда.

Так же включаем бит нажатия клавиши, проверяем вхождение всего буфера нажатий (без учёта модификаторов, у которых собственный буфер) в таблице аккордов. Если значение найдено – передаём его в обработку. Ничего необычного. Но буфер для хранения и проверок приходится переводить в hex-представление.

Так же, как и с модификаторами, мы сохраняем последнее_значение и запускаем таймер его сброса. Уже добавленный блок в key_up без изменений справится с передачей действия при базовом нажатии, если последнее_значение не будет перезаписано.

Дополнительные опции

При создании назначения у нас также есть несколько опциональных параметров, которые влияют на путь обработки (для этого они там и нужны 🙃).

Первый – невозвратное назначение. Если при обычном пути обработки мы возвращаемся к корню, когда доходим до листа, или если нас "сбросил" таймер ожидания следующего нажатия, то с этой опцией этого не происходит. Если мы дошли до невозвратного листа, мы просто останемся на текущем уровне переходов. Можем повторить это же нажатие с выполнением действия, или выполнить любые другие нажатия. Если у них не будет указана такая же невозвратная опция, через них мы уже вернёмся к корню.

Второй – моментальное/независимое действие. Опять же сравним с обычным поведением – когда мы идём по цепочке, мы не вызываем назначения каждого узла, а только листового (или сброшенного таймером). С данной опцией – вызываем. Это могут быть как переопределённые действия, так и базовые. Например, мы можем назначить цепочку "т->ь->с->я", без переопределённого действия, указав каждому узлу независимое исполнение, а листовому узлу добавим дополнительную функцию, которая будет нас спрашивать, нужен ли тут мягкий знак. Например. И, конечно, с этой опцией мы не выполняем действие, когда по таймеру доходим до обработки последнего_значения. Оно уже выполнено.

Третий – дополнительное действие при отжатии клавиши. Можно использовать просто как дополнительное действие, но логика именно в том, что в момент "принятия" узла в обработку мы дополнительно сохраняем действие при отжатии для данного сканкода, которое вызываем при key_up.

И последние элементы – изменённое время ожидания удержания и следующего нажатия. В обычном случае мы используем глобальные значения, но каждое назначение может иметь и собственные, которые мы и будем использовать в соответствующих строках.

Прочее

И из того, что не пришлось к слову в предыдущих пунктах, но также участвует в процессе:

  • Клавиши системных модификаторов стараемся пропускать максимально нативно, не вмешиваясь в процесс, чтобы не ломать системные/программные хоткеи. Они будут запрещены к переназначению по нажатию, но им можно будет дополнительно добавить значение пользовательского модификатора на удержании. Хотя это не рекомендуется.
    В целом это работает как и ранее: если назначения есть – отправляются они, если нет – нативное нажатие. Например, если вы определите ctrl модификатором, и на s назначите действие из-под его значения модификатора, данный хоткей сработает только как назначенное действие. Если ctrl не назначен модификатором, или назначен, но на "модифицированный" s нет действия – сработает системный хоткей.
    Но это всё равно остаётся потенциально опасным местом в различных непротестированных сочетаниях нажатий.

  • При нажатиях мы также сверяем текущую активную раскладку с последней, которую мы видели. Если они отличаются, форсируем передачу последнего значения по активной таблице и возвращаем её к корню уже по новой раскладке, прежде чем перейти к проверкам нового нажатия.

  • Если начальная проверка сканкода и модификатора в активной таблице неудачна – возвращаемся к корню, и выполняем проверку ещё раз, прежде чем уйти в ветку "нет назначений".

  • В обработке мы по умолчанию не сбрасываем активную таблицу к корню, если есть активные модификаторы. Сброс к корню в этом случае происходит при отпускании всех модификаторов, а пока любой из них активен – можно продолжать ввод на этом уровне переходов, добавлять новые модификаторы и т.д. Но это работает только пока мы не перейдём на уровень глубже (если он есть).

  • Модификаторы сохраняются между переходами только в случае, если у них есть точно такое же назначение на следующем уровне. Все остальные модификаторы сбрасываются между переходами, и их удержание никак не влияет на новый уровень.

Демонстрация

Мы рассмотрели идею и принципы работы основной логической части проекта, возможно даже слишком детально. Теперь пора посмотреть на реализацию.

Но для начала, буквально 30 секунд, если позволите:

Tap Dance for Windows – это открытый проект для Windows (пока что), позволяющий наделять клавиши клавиатуры дополнительными функциями и комбинациями, подобно возможностям прошивок вроде QMK, но на программном уровне.

Проект представляет собой интерактивную среду для переназначений и поддерживает такие ключевые возможности, как:

• разделение поведения tap/hold
• классические и расширенные мультинажатия
• последовательности назначений с неограниченной глубиной
• аккорды/комбо
• назначение пользовательских модификаторов

… и всё это одновременно, в одном сценарии использования, если понадобится. Вы сами выбираете, что из возможностей, и в каких комбинациях использовать.

Всё это настраивается через GUI в реальном времени. Назначения могут быть привязаны к конкретным языковым раскладкам, а также объединены в слои, переключение которых возможно на лету, для динамической смены назначений прямо в процессе использования.

Добавляемые назначения поддерживают не только ввод текста и симуляцию клавиш, но и вызов ряда дополнительных функций, список которых вы легко можете дополнить сами, для любого нужного вам функционала.

Спасибо, это надо было сделать.

Обзор GUI

Для большей интерактивности можете клонировать репозиторий, и двигаться параллельно с дальнейшим описанием.

Основное окно приложения отображает клавиши, повторяющие клавиши обычной клавиатуры в вашей текущей раскладке. В правом списке отображены аккорды текущего уровня, в левом – список слоёв. Кроме этого, над клавишами-стрелками расположено выпадающее меню текущих языковых раскладок, для переключения и раздельного назначения на каждой из них, или в глобальном режиме, для общих назначений.

Буквенные клавиши отображаются точно как в вашей раскладке.                                                                                                                          На скриншотах именно изменённая системная раскладка, а не переназначения.
Буквенные клавиши отображаются точно как в вашей раскладке. На скриншотах именно изменённая системная раскладка, а не переназначения.

В настройках (🔧) вы можете изменить режим отображения – квадратный/широкий, масштаб интерфейса и шрифта, а также формат раскладки вашей клавиатуры (ANSI/ISO). Последняя настройка не только косметическая, она влияет на некоторые назначения, так как и некоторые сканкоды зависят от формата. В этом же окне задайте желаемое время для распознавания удержаний и для ожидания следующего нажатия перехода.

Широкий режим с ISO-форматом
Широкий режим с ISO-форматом

Из прочих пунктов настроек стоит отдельно отметить пункт Ignore inactive layers, который полезен (но совсем не обязателен) только во время назначений в GUI. Выключайте его если не задаёте новые назначения. Он значительно влияет на производительность при переключении слоёв.

Также отмечу последние три поля – все нажатия клавиш физической клавиатуры выполняют переход в gui по нажатой клавише. Но указанные в данных полях клавиши будут отвечать за заданные действия – возврат к предыдущему уровню, добавление базового назначения, добавление назначения на удержание. По умолчанию это сканкоды для num-, num+ и numEnter. Если захотите изменить эти значения – посмотреть, у каких клавиш какие сканкоды можно изменив здесь же тип отображения клавиш на Always use scancodes.

Все назначения добавляются на определённые слои (да, это не совсем те слои), которые вы можете раздельно переключать, редактировать, настраивать их относительный приоритет, как через GUI, так и через специальные назначения с других слоёв. Более подробно рассмотрим их позже, а пока добавим новый слой кнопкой new в левом списке, на котором немного поиграемся с назначениями. Отметим его галочкой как активный, и перейдём к редактированию двойным нажатием по нему, или кнопкой view, предварительно выбрав его в списке.

Первые назначения

Добавим наши первые назначения. Для этого кликом или нажатием физической кнопки клавиатуры перейдём к нужной клавише, и появившимися в верхней правой части кнопками зададим действия. Например, на базовое нажатие тильды добавим любой текст, а на удержание добавим модификатор 1, выбрав соответствующий тип. В этом же окне переходов ещё раз нажмём по тильде, и добавим назначения для базового нажатия – {Media_Play_Pause} с типом Key simulation, и для удержания назначим FunctionGetDateTime. После выбора данного типа действия у нас откроется вспомогательное окно, где мы можем выбрать нужную функцию и её параметры. Нажатием по assign значение подставится в поле. Можем вписать его и вручную, если понадобится. Параметры без кавычек.

(41 – сканкод тильды)
(41 – сканкод тильды)

Чтобы это аккуратно смотрелось в GUI, можно добавить в последнем поле любой нужный текст, как здесь "ƒ Full date". Остальные параметры пока не будем трогать.

У нас готовы первые каноничные TapDance назначения с разными типами действий. Можете сразу опробовать в любом другом окне (в данном они будут приводить к новым переходам GUI). Одиночное нажатие тильды введёт заданный текст, двойное – переключит текущее воспроизведение медиа, а нажатие+нажатие с удержанием вызовет функцию, в данном случае возвращающую на ввод текущую дату. Saturday 17 May 2025 14:50 (вот и время обновления).

В отличие от классической идеи, мы не ограничены тильдой, и второе нажатие можем задавать не только на той же клавише, но и на любых других, в любых количествах, с любой вложенностью.

Назначение модификатора под одиночным удержанием пока ничего не даёт, ведь мы не задали назначения из-под этого модификатора. В верхнем меню пути нажмите на первый элемент, с названием слоя, который вернёт нас к первому уровню переходов. Вид тильды изменился, теперь она отображает назначенный текст по базовому нажатию, значение модификатора при удержании, и синей обводкой показывает, что выполняет роль модификатора. Также в верхнем углу мы видим индикацию, что клавиша имеет два дочерних назначения по базовому нажатию. Индикация для назначений по удержанию, когда есть о чём сигнализировать, отображается в соответствующей нижней части.

Теперь нажатием пкм или удержанием физической тильды активируем назначенный модификатор. Цвет обводки изменится на отображающий данное состояние – чёрный. Добавим пару новых назначений на этот уровень. Например, перенесём цифровой блок под правую руку с данным модификатором:

Помимо обводки, о текущем значении модификатора также говорит и вспомогательный текст в правой верхней части окна. При возврате к предыдущему уровню после добавленного назначения у нас сохраняется значение модификатора, с которым мы переходили отсюда к следующему уровню. Но если мы нажмём в пути по кнопке текущего уровня ещё раз, все активные модификаторы выключатся.

Проверим новые назначения – зажимаем тильду, и теперь под правой рукой у нас полный цифровой блок.

Протестируем глубину назначений. Назначим действие на 20 нажатий q. Хотя это, опять же, могут быть абсолютно любые клавиши, совершенно не обязательно одна и та же.

Проверим. It works. Хотя вызвать значение при удержании тут не очень просто.

Если снова вернёмся к первому уровню переходов, мы увидим индикацию дочернего назначения для q, даже при том, что под самим q никаких назначений нет. Если пройдём по отмеченному пути и сбросим назначенные действия последнего уровня, всё вернётся к прежнему состоянию, и даже самый первый q уже не показывает наличия дочерних переходов.

И проверим аккорды – нажмём в new под правым списком, нажатиями мышью или на клавиатуре выберем нужное сочетание, и подтвердим аккорд кнопкой save, назначив нужное действие. Клавиши аккордов обозначились жёлтым, а в правом списке появился наш аккорд, который мы так же можем сразу проверить.

Аккорды точно так же можно назначать из-под модификаторов, с других уровней, и задавать им собственные дочерние переходы. Для последнего – "зайдите" в аккорд двойным нажатием по его строке в списке, и определите вложенные назначения.

Верхнее меню пути показывает, какой тип перехода вы выполнили, от обычного нажатия , от удержания или по аккорду . Цифра перед стрелкой показывает значение модификатора перехода, если оно было.

Базовое значение также показывает назначение аккорда, когда мы заходим в него
Базовое значение также показывает назначение аккорда, когда мы заходим в него

И наконец рассмотрим дополнительные опции. Вернёмся к корню, и перейдём по обычному нажатию тильды на второй уровень, чтобы лучше всё увидеть.

Последовательно добавим на этот уровень (значения любые, можно просто с типом Default):

  • на 1 – невозвратную галочку Irrevocable;

  • на 2 – моментальное независимое исполнение Instant и любое дочернее действие из-под него (например ещё раз на 2);

  • на 3 – изменённое время удержания, и для проверки любое значение удержания здесь же;

  • на 4 – изменённое время ожидания дочернего нажатия и любое дочернее действие;

  • на 5 – любое действие, кроме Disabled, на отжатие клавиши.

Я ещё добавлю всё перечисленное на 6, и с удержанием, просто чтобы показать совмещённую индикацию:

Если вы задавали имя в GUI для одного из назначений тильды, там тоже будет соответствующий серебристый индикатор. Вся эта индикация отображается также и для кнопок Base/Hold.

Проверим новые назначения:

  • нажатие ~1 не вернёт нас к корню, мы останемся на этом же уровне переходов. Можем ещё сколько-угодно раз нажать 1 вызвав её действие, или любую другую клавишу, включая клавиши без назначений вовсе, чтобы выполнить её действие и всё же вернуться к первому уровню

  • нажатие ~22 выведет двойку сразу при ~2, и потом ещё действие вложенной двойки (если вы назначили её дочерней)

  • нажатие ~3 (с удержанием) потребует уже больше времени (или меньше) для определения ветвления tap/hold, строго как вы указали

  • нажатие ~4 даст большее окно для нажатия дочерне назначенной клавиши, прежде чем произойдёт сброс по таймеру

  • и ~5 выполнит дополнительное указанное действие.

Всё это комбинируется в любых сочетаниях между собой, если отвечает вашим требованиям.

Если мы ещё добавим назначения под ~1, его нажатие приведёт к неограниченному ожиданию дочернего нажатия на этом уровне. А вот уже это дочернее нажатие вернёт нас к корню, если у него не будет также указан Irrevocable, конечно.

Что же, теперь вы знаете всё о назначениях в Tap-Dance-for-Windows. Перейдём дальше.

Слои и раскладки

Слоями в данном приложении называются отдельные .json файлы с собственными деревьями назначений, которые генерируются и редактируются нашим приложением, и рекурсивно собираются в единое дерево, используемое логической частью приложения, в соответствии со своим приоритетом.

При перекрёстных назначениях с разных активных слоёв к действию по приоритету выбирается первое отличное от Default (если есть), а дочерние переходы объединяются от всех назначений с таким же или стандартным действием.

Детальные примеры
  1. Есть значимое (не Default) назначение на слое с приоритетом 1, и стандартное назначение для того же события на слое с приоритетом 2. В этом случае их дочерние назначения объединятся, и к действию примется первое назначение.

  2. Если в прошлом примере поменять местами приоритет слоёв, всё поведение сохранится, так как стандартное назначение рассматривается только как путь к дочерним назначениям.

  3. Если назначение на обоих слоях совпадает по всем параметрам (имя в gui не рассматривается), они также объединяют свои дочерние переходы, и учитывается их "общее" действие при срабатывании.

  4. Но если эти значения будут значимыми и разными – и для действия при срабатывании, и для таблицы переходов будет использовано только назначение от слоя с наивысшим приоритетом.

И так для всех слоёв. Если у нас для одного события на одном уровне переходов есть назначения на разных активных слоях:
(Default), (Text 'Hello'), (Default), (Text 'Bye'),
(Function 'Test()'), (Text 'Hello'), (Default)
в порядке приоритета слоёв, в финальное дерево попадёт действие (Text 'Hello') и объединённая таблица дочерних переходов от первых трёх, и последних двух назначений.

Для большего контроля, все слои в списке, вне зависимости от текущей активности, отображают собственные назначения по данному пути и количество дочерних переходов, раздельно для tap и hold:

На корневом уровне, где нет назначений Base/Hold, отображается общее количество назначений для каждого слоя, для текущей выбранной раскладки/дерева и для всех остальных. Вы это уже могли видеть на некоторых прошлых изображениях:

Раскладки отображены выпадающим списком над клавишами-стрелками. Переключаясь между ними вы переходите в редактирование дерева конкретной раскладки. Напомню, что в GUI вы видите отдельно глобальные назначения, как раскладку Global и отдельно каждую другую, для удобства назначений. Но при работе и переходах используются только деревья от конкретных раскладок, к которым были добавлены глобальные назначения.

Каждый слой имеет собственный список раскладок, которые добавляются к общему списку при запуске. С включённой опцией Collect unfamiliar layouts, даже если у вас не установлена определённая раскладка, но она задействована на каком-то слое, даже неактивном, – вы сможете перейти в её дерево назначений через выпадающий список. Хотя использовать не сможете, пока не установите её, конечно.

В репозитории размещён ряд слоёв, которые вы можете опробовать, прежде чем определите собственные. Бо́льшая их часть это реальные назначения, которыми я сам пользуюсь уже более 7 лет, хотя особо прозелитировать не могу – они используют дополнительные назначения на клавишах системных модификаторов, что, как уже отмечалось ранее, не очень хорошо. Тем не менее, для ознакомления и шаблона отлично подходят.

Предустановленные слои

Default: слой с назначениями 83 пунктуационных и вспомогательных символов в буквенной части клавиатуры. Расположены под обычным удержанием, с alt, и alt с удержанием. И ещё неразрывный пробел под shift

Controlling Keys: слой с переназначениями управляющих клавиш, hjkl-стрелки, управление медиа на разные сочетания с BS, несколько повседневных функций, вроде увеличения/уменьшения числа под курсором, КАПС на одно слово, отложенный запуск/остановка музыки, и прочие мелочи. Задействует, как видно, все системные модификаторы.

Индикаторы количества назначений также работают и для модификаторов, хоть это и не дочерние переходы. Комбинируемые модификаторы тоже учитываются – на этом скриншоте на Shift показано количество назначений именно для Alt+Shift (mod 1+2), а не просто для Shift, где их только 4, что видно на прошлом изображении.
Индикаторы количества назначений также работают и для модификаторов, хоть это и не дочерние переходы. Комбинируемые модификаторы тоже учитываются – на этом скриншоте на Shift показано количество назначений именно для Alt+Shift (mod 1+2), а не просто для Shift, где их только 4, что видно на прошлом изображении.

Extra Langs: слой с общей комбинируемой диакритикой под alt, а также назначениями для конкретных раскладок: цифровой ряд через Shift вводит специфичные языковые символы в двух регистрах (tap/hold). Версии для кириллических и латинских письменностей привязаны к стандартным йцукен и qwerty. Символы подобраны по максимальной ширине охвата.

При переключении раскладки в gui, для названий клавиш также подтягиваются символы из этой раскладки
При переключении раскладки в gui, для названий клавиш также подтягиваются символы из этой раскладки

Leader: поле для назначений пользовательских функций (о них ниже) через \ (дополнительная клавиша над Enter для ANSI, слева от Enter для ISO). Просто как шаблон.

Обратите внимание на индикацию на кнопке назначения Base
Обратите внимание на индикацию на кнопке назначения Base

Emoji: слой для попытки кликбейта 🙈. Почти четыре сотни эмодзи на всей клавиатуре в два-три нажатия. Можно и больше 🗿. Двойной клик по модификатору переводит в эмодзи режим без модификатора, пока не нажмёте ещё раз.

Morse: пусть и шуточный, но абсолютно рабочий слой с назначением четырёх уровней Пробела в комбинациях нажатий-удержаний для ввода латиницы. Попробовать забавно.

Шпаргалка
стартовая позиция
стартовая позиция

Angry: отчасти шуточный, отчасти нет, но хорошо демонстрирующий одно из применений Instant опции. Его назначение оставлю вам на исследование. Подсказка: он ориентирован на qwerty раскладку.

One word caps: вспомогательный слой, также для демонстрации одного из сценариев использования – с любого другого слоя вызывается включение этого слоя вместе с включением CapsLock, а при первом пробеле срабатывает дополнительное назначение на Space, которое выключает и Caps, и сам слой. Получается КАПС автоматически на одно слово.


Сценарии использования ограничены только фантазией. Создавайте любые назначения, какие только сможете придумать. А если просто назначений не будет хватать... об этом в следующем блоке, но с небольшим отступлением.

Реализация и дополнительный функционал

Текущая реализация написана на ahk v2, что хоть и довольно удобно, но налагает пару ограничений.

Например, в ахк мы должны заранее обозначить, будет ли заблокировано нативное нажатие, или нет. Потому в случаях, когда мы в key_down обошли всю логику, не нашли никаких назначений и ожиданий, и хотим передать базовое нажатие дальше – мы должны сами симулировать это нажатие. В подавляющем большинстве случаев этого более чем достаточно, но может некорректно работать в некоторых приложениях, в основном игровых.

Это же сказывается на системных модификаторах – лучшим найденным решением, в рамках ахк, на текущий момент, оказалось объявление на каждом новом (только новом) переходе отдельного хоткея на каждое переопределённое системно-модификаторное сочетание, с передачей отдельного аргумента, как значения переопределённого модификатора. Это вполне работает, и с кэшированием даёт малый оверхед, но всё же костыль.

Из-за этого, а также для общего повышения быстродействия, основная логика была переписана на C. Пока это лишь экспериментальное переложение без кастомизации, которое использует уже сгенерированное через основной скрипт дерево, но большая часть логики уже отлично работает. Начало положено.

А теперь о плю́сах.

Использование ахк позволяет нам использовать его родной синтаксис для симуляции клавиш. Пункт Key Simulation, в любом назначении, это он и есть. ^z для undo, {Volume_up} и все прочие команды, которые и понятно выглядят в gui, и удобно назначаются.

Кастомные функции

И ещё один приятный момент – мы можем задавать назначениям не просто передачу текста или симуляцию клавиши, но и исполнение любой функции (мы уже вызывали парочку в примерах), и их список может быть без проблем дополнен любым пользователем по своим потребностям, даже без опыта с ахк.

Эти функции совершенно не обязательно должны быть связаны с вводом или передавать значение в текстовое поле, вроде текущей даты или инкрементации числа под курсором. Любая функция. Обратитесь к внешнему api, чтобы получить курс валют или прогноз погоды; поставьте pomodoro-напоминание, которое сработает через 20 минут; задайте таймер включения/выключения музыки; нормализуйте текст со сбитым регистром или переведите выделенный текст с translita.

Или используйте назначенные функции чтобы менять активность слоёв приложения – переключать конкретные, или устанавливать определённый активный набор по одному нажатию.

Это из того, что уже добавлено для демонстрации, но вы всегда можете добавить новые, те, что нужны именно вам. Всё, что придумаете.

Заключение

Здесь должен был быть ещё раздел про применимость, где я думал рассказать о переложении типографских раскладок, использовании аккордного ввода, и прочих вариантах применения, но этот проект – инструмент, и как его использовать, решать вам. Для кнопки "my_email@gmail.com" или для отдѣльных новых символов, для кучи заготовленных шаблонов или для вызова десятков самописных функций, для своей вариации полнословной клавиатуры или для переназначения всего и вся? В любом случае, всегда буду рад почитать об интересных сценариях использования.

Спасибо за внимание.

Проект живёт на github. Все предложения, дополнения и замечания приветствуются.


Tags:
Hubs:
Total votes 18: ↑18 and ↓0+21
Comments72

Articles