Привет, Хабр!
Несколько месяцев назад я рассказывал о проекте tdfw, значительно расширяющим возможности взаимодействия с клавиатурой, вдохновившись функционалом прошиваемых клавиатур. Тогда у читателей мог возникнуть вопрос: а чем вообще программная реализация может быть лучше, если допустить, что она только повторяет (хотя это совсем не так) аппаратную?
Как минимум тем, что аппаратные решения строго привязаны к своему устройству, и выйти за его границы могут только через не слишком эффективные костыли надстройки. На программном же уровне мы не ограничены лишь одним устройством, потому можем объединить разные устройства ввода в единую, согласованную систему, используя все их возможности.
И поддержка жестового управления – одна из этих возможностей.

Подходы к распознаванию
Есть три основных способа распознавания жестов.
Простейший – работа с направлениями: задаём набор допустимых направлений, каждые n пикселей сдвига мыши фиксируем направление нового сегмента и сравниваем получившуюся последовательность с эталонными паттернами вида ↑ ↑ ↓ ↓ ← → ← →
.
Современный – машинное обучение, конечно. Это самостоятельная категория со своими алгоритмами и правилами.
Золотая середина – сравнение шаблонов. О ней и пойдёт речь дальше. Здесь мы представляем жест в виде набора точек, которые сравниваем с эталонными – чаще всего с помощью косинусного сходства или различных метрик расстояния.
Для корректного сравнения мы либо изначально каждый набор равномерно распределяем (ресемплируем) до заданного количества точек/векторов (обычно 64, +/-²), либо динамически выравниваем наборы на этапе сравнения.
Также есть опциональные стадии: центрирование (сдвиг всего набора к старту из начала координат); масштабирование до единого размера отрезков; и поворот по первому отрезку к определённому градусу.
Центрирование обычно включается в реализации, так как является более ожидаемым поведением, и вместе с тем упрощает распознавание, так как его выключение делает жесты чересчур зависимыми от начальной позиции. В последнем случае понадобятся дополнительные уточнения, вроде сдвига к ближайшей стартовой точке на этапе сравнения наборов.
Масштабирование обычно включено для реализаций с ресемплированием. Важно понимать, что динамическое выравнивание по сырому набору точек для нересемплированных вариантов это не то же самое, что геометрическое масштабирование, и его тоже нужно прописывать отдельно. В случае динамического выравнивания происходит приведение по времени/количеству точек, и результаты совпадут с масштабированием только если мы будем рисовать вдвое бо́льшие жесты за строго вдвое большее время.
Включение же поворота жестов, наоборот, является редкостью, так как чаще вы всё же предпочтёте различать жесты →
и ←
, хотя это может быть полезно для некоторых отдельных жестов, вроде круга. Но есть менее заметные варианты поворота, которые просто призваны убрать "ориентационный шум", когда поворот происходит не к строгому углу, а к ближайшему из, допустим, 8 направлений.
Эти пункты опциональны, и для разного поведения жестового ввода могут быть реализованы в разных сочетаниях.
Для своего проекта я взял ресемплирование жеста до 64 точек, с центрированием и масштабированием, и сравнением по косинусному сходству. Получилось замечательное расширение функционала, которое можно использовать и само по себе, на что и посмотрим ниже.
Как этим пользоваться / простые жесты
Пока вы клонируете/скачиваете репозиторий и открываете приложение, проговорим основы: режим рисования жестов для распознавания включается, пока вы удерживаете назначенную клавишу или кнопку мыши. Назначить можно сколько угодно жестов, на любое количество клавиш, и задать им любые действия, от ввода символа, до вызова кастомных функций произвольной сложности.
К тому же, назначенные клавиши/кнопки это не единые точки входа к общему набору жестов. Каждая клавиша имеет собственный набор жестов, поэтому вы можете назначить на разные кнопки группу жестов для управления медиа, группу для браузера, и так далее.
Итак, после считывания раскладок при первом запуске (это нужно для всей функциональности приложения) перед нами стартовое окно:

Для начала нам нужен слой, на который мы будем назначать жесты. Можно выбрать один из предустановленных, но, чтобы не путаться, добавим новый слой в нижнем левом углу и отметим его галочкой как активный.
Нажатием выберем любую клавишу или кнопку мыши (они справа, вокруг num-блока) для добавления к ней жестов. Так как режим рисования отключает стандартное поведение "перетаскивания", крайне не рекомендуется добавлять жесты под обычные кнопки мыши. Идеально использовать для этого дополнительные боковые XBM1
/XBM2
, если на вашей мыши они есть.
Я выберу для назначения q
. Теперь в нижнем центральном окне у нас активна кнопка New
, которая покажет нам окно для добавления назначения:

Сразу нажмём на Set gesture pattern
и нарисуем один эталонный жест удерживая ПКМ. Можете перерисовать, если выйдет не слишком эталонно, нажав эту кнопку ещё раз. Сложность жеста может быть любая, от простой прямой, до автографа (на всякий случай: данные никуда не передаются).
В первом списке мы можем выбрать, что произойдёт при срабатывании жеста – простой ввод заданного текста или символа; симуляция клавиши или сочетания в ahk-синтаксисе; выполнение пользовательской функции.
Могу предложить для тестового жеста простое переключение к прошлому окну через симуляцию Alt-Tab. Для этого выберем второй пункт в выпадающем списке, и в поле под ним введём !{Tab}
Если хотите протестировать с более необычным, но всё ещё простым назначением – выберите в выпадающем списке пункт
Function
. Появится вспомогательное окно, в котором выберем функциюWikiSummary
, в поле ниже укажем код русского языкаru
,Input: Selected
,Output: Tooltip
. Нажатием поAssign
функция подставится в поле окна назначения.
Теперь, когда сработает наш жест, во всплывающем окне появится краткое пояснение выделенного текста с википедии (если будет найдено). Api публичный, никаких ключей не нужно.
Также добавим в последнем поле имя нашему жесту и сохраним его. Другие пункты назначений пока оставим как есть.
Теперь наш жест появился в списке и готов к использованию.
Проверять стоит в другом окне, так как интерфейс приложения оставляет почти все нажатия себе, выполняя функцию перехода к следующему уровню. Если случайно нажмёте, вернитесь через верхний путь переходов ко второму элементу.
Если вы выбрали в качестве действия ввод текста, убедитесь, что у вас фокус на текстовом поле, иначе активное окно может распознать в этом тексте неожиданные хоткеи.
Итак, проверим жест: пока удерживаем кнопку/клавишу с назначенным жестом у нас включается режим рисования на оверлее, так же, как и при назначении. Если вы повторили жест достаточно близко к изначально заданному и он длиннее минимальной длины (по умолчанию задано 90% сходства и 150px длины, можно изменить в настройках), выполнится ваше действие. Во всех же остальных случаях в систему передастся обычное нажатие клавиши/кнопки.
Таким образом вы можете назначить жесты даже для буквенных клавиш, и это никак не будет влиять на набор текста. Просто нажатие клавиши – нажатие клавиши. Нажатие будет заменено другим действием только если с клавишей будет введён распознанный и сопоставленный жест.
К слову о прочих клавишах – можем проверить, что жесты на разных клавишах действительно не пересекаются: вернёмся к первому уровню назначений нажатием в верхнем меню переходов по самому левому элементу, здесь выберем другую клавишу/кнопку, и добавим новый жест с таким же рисунком. Проверим. Один жест, две точки входа, два назначения. Аналогично, если мы напротив, добавим другой жест под вторую клавишу, жест от первой на ней также не сработает.
Вложенные жесты
Здесь мы подошли к центральной идее основного проекта, и заодно выясним, что это за меню переходов сверху, и дополнительные настройки в назначениях.
Только что мы задавали назначения под разные клавиши. Если последовательность действий клавиша, жест
имела назначение, оно выполнялось. Если нет – выполнялось назначение самой клавиши. Но это работает не только для последовательности клавиша, жест
, а для любой цепочки, любой глубины, с любыми событиями-триггерами и назначениями.
Вернёмся к первому, корневому уровню переходов нажатием по первому элементу в верхнем меню. Перейдём к клавише в которую мы вложили жест, и в списке жестов нажмём по нему дважды. Мы "под" жестом. Сейчас здесь нет никаких назначений, поэтому он просто выполнял собственное действие. Здесь же выберем любую новую клавишу, и перейдём к ней ещё одним нажатием. Мы на четвёртом уровне переходов. Здесь можем задать новый жест, и он будет распознаваться только в тех сценариях использования, которые будут повторять этот путь – клавиша_1, жест_1, клавиша_2, жест_2
. Другими словами, с каждым нажатием мы переходим к новой, вложенной таблице назначений, если она не пустая.

На примере выше я задал два простых жеста →
и ?
под «ключом» q
, и в жест →
вложил ещё один →
, через тот же q
. В конце видно, что все жесты, включая "двойной", работают как и полагается, и как простое нажатие q
без жеста сразу же возвращает q
.
Надписи q (from third level)
, это то, о чём мы только что говорили: нажатия клавиш это такие же события-триггеры, как и жесты, и им также можно задавать назначения, что я и сделал для этого примера – событие нажатия q
на третьем уровне данного сценария имеет действие ввода заданного текста.
Жесты в данной статье чуть забирают акцент, но в целом проект начинался именно с базовых клавишных последовательностей в разных комбинациях событий-триггеров (их чуть больше, чем просто нажатия), и они и сейчас являются основными, но теперь дополнились замечательным расширением.
Возвращаясь к примеру, триггер q
третьего уровня у нас сработал в двух сценариях:
• q→q
, точно так же, как и с простым q
– не было ввода жеста, и выполнилось собственное назначение;
• q→q?
аналогично предыдущему, только здесь сбросом цепочки послужил нераспознанный жест, так как ?
мы назначали в другом узле.
Этому вложенному q
можно также задать любые назначения, в том числе и отключить его действие вовсе, если оно лишнее. На проход по цепочке это никак не повлияет, единственная разница будет исключительно в финальном действии для этих двух сценариев.
И последнее, что вы могли подметить на этом примере – задержка ввода в сценарии q→
. Жест уже закончился, но текст ввёлся только через секунду.
Всё потому, что у нас здесь есть возможное дочернее/вложенное событие, и система ещё не знает, что ей делать – исполнять действие текущего узла, или переходить к следующему. Помимо ввода следующего события, которое поможет определиться, есть также сброс по таймеру – если следующего события нет n мс, считаем, что данный узел финальный и выполняем его действие.
И здесь есть много опций для тонкой настройки. Их описания чуть душные, извините, но они добавляют максимальную вариативность для любых сценариев использования.
самое базовое – можно задать глобальное значение данного таймера в настройках (по умолчанию 200мс), а также указать персональные значения любым назначениям
если вы хотите, чтобы действие выполнялось не в момент "решения", а в любом сценарии, как только мы дошли до этого назначения – включите для него опцию
Instant
. Так в одной цепочке можно выполнить сразу несколько назначенных действий. Например, если добавить это поведение для первого свайпа в примереq→q→
– на ввод будут переданы тексты как от первого, так и от второго жестов, за один сценарий. Действие выполняется, но цепочка не сбрасываетсясосед прошлой опции в каждом назначении –
Irrevocable
. Он фиксирует нас на текущем уровне, и сброс цепочки не происходит ни по таймеру, ни от дочерних листовых узлов. Если всё в тот же сценарийq→q→
добавить эту опцию для второгоq
, все последующие жесты вправо будут вызывать именно действие от второго, вложенного жеста, не возвращаясь к корню. Сброс в этом сценарии произойдёт либо через событие с собственными вложенными назначениями (проход по цепочке к другому узлу), либо через неназначенное дочернее событие, в стандартном поведении. К слову о последнем:при "незнакомом" системе событии происходит принятие решения по текущему узлу и сброс цепочки к корневому уровню. Это поведение по умолчанию, у которого тоже есть несколько альтернатив, включающиеся в выпадающем списке
Unassigned child behavior
для каждого назначения. Можно просто возвращаться к корню без действия самого узла («незнакомый сценарий? Ничего не делай»); можно искать на предыдущем уровне переходов, так же с действием или без него; или же можно вовсе игнорировать все незнакомые вложенные события, без действия, без сброса цепочки, без переходов (но таймер сброса всё так же работает).
Это всё, что нужно знать для добавления действий к целым последовательностям жестов. Только не прячьте в них свои пароли. Назначения всё равно хранятся простым текстом в .json ?
Жесты в контексте всей функциональности
Привязка к языковым раскладкам
Ещё больше вариативности добавляет возможность привязки назначений к конкретным языковым раскладкам (для этого при первом старте вам надо было показать приложению, какие у вас включены).
В примерах ранее мы добавляли глобальные назначения, которые работали на всех раскладках, но можно из-под одного и того же «ключа» указать на йцукен жесту влево одно назначение, на qwerty другое, и так далее.
Переключаться между раскладками для назначений можем через выпадающий список рядом с кнопкой настроек.
В интерфейсе глобальные назначения не подмешиваются к конкретным раскладкам, чтобы не вызывать путаницы, но при работе приложения к каждой раскладке добавляются глобальные назначения, с приоритетом для первых, если пересекаются. Пересечения одинаковых назначений с разных слоёв решается приоритетом слоёв.
События для назначений
Как уже упоминалось ранее, событий-триггеров больше, чем жесты и простые нажатия. Помимо них ещё есть удержания, аккорды/комбо, и пользовательские модификаторы. Каждое из этих событий ведёт к своему действию и ветке вложенных назначений, и жесты никак не меняют их функциональность.
Нажатия и удержания (tap/hold)
Если у нас есть q
с дочерним жестом и с назначением при удержании, например, ввод ~
, будет работать сразу всё: пока клавиша не будет отпущена, будет активен режим ввода жеста, после сопоставления жеста будет принято решение о действии:
жест распознан – переход к назначению жеста;
жест не распознан и время зажатия клавиши было меньше порога удержания – назначение простого нажатия (и действием может быть задан не только ввод
q
);жест не распознан и порог удержания преодолён – назначение удержания.
Порог удержания также настраивается и глобально в настройках, и отдельно может быть указан для каждого назначения.
Базовое назначение и при удержании добавляются кнопками в правом верхнем углу для текущего элемента (последнего в пути переходов).
Также этим назначениям можно указывать дополнительные действия при отпускании клавиши (одно в момент "решения", другое при up
событии).
Аккорды/комбо
Аккорд – комбинация одновременно нажатых клавиш с собственным переходом и действием.
Всё точно так же: если у клавиши, которая является частью аккорда, назначен дочерний жест – при удержании будет активен режим ввода жеста. Если добавить к этому нажатия клавиш для полного аккорда, сработает последний.
Под целым аккордом также можно назначать дочерние события, включая новые аккорды, но не жесты, так как им нужна конкретная клавиша-ключ.
Пользовательские модификаторы
Модификаторы (не те, которые системные, но можно задать и для них) – особый тип удержания, который позволяет задавать новые назначения прочим клавишам, которые срабатывают пока модификатор удерживается. Комбинированные модификаторы также предусмотрены.
Поведение с жестами такое же – пока нажата клавиша, у которой указаны и модификатор на удержании, и дочерние жесты – доступны и нажатия из-под модификатора, и рисование жестов.
Так как "модификатор" это именно тип при удержании, а не всей клавиши, базовое назначение всё ещё доступно, если с нажатым модификатором не было никакого события, а время удержания было ниже порога определения. Назначения из-под модификатора доступны сразу же, не требуя определения tap-hold.
GUI
Немного об интерфейсе в целом, что не упоминалось ранее.
Верхнее меню переходов показывает тип для каждого из них: •
жест, ➤
обычное нажатие, ▲
удержание, и ▼
аккорд. Если переход был с модификатором (они не является отдельными элементами переходов, а именно модифицируют другие) – его числовое значение отображается перед типом перехода.
Переходы "под" аккорды и жесты выполняются двойным кликом по ним в списках. Для переходов по базовым нажатиям и удержаниям обычных клавиш – ЛКМ
и ПКМ
соответственно, или с клавиатуры (как раз нажатием или удержанием). Если на удержании уже указан модификатор – ПКМ
/удержание активирует его.
Тексты на клавишах отображают действия от текущего пути переходов. Сверху для базового нажатия, под ним действие при удержании, если имеется.
Цвет обводки клавиш может показывать есть ли под ней вложенные жесты (соответствует цвету указанному для режима рисования); является ли она частью аккорда (жёлтый); или модификатором (синий). Текущие активные модификаторы имеют чёрную границу.
Комбинированное значение всех активных модификаторов показано в углу от num-блока.
Счётчики в правых углах клавиш (сверху для базового назначения, снизу для удержания) отображают количество всех дочерних назначений всех типов, включая жесты. Индикаторы в левых углах указывают на дополнительные опции у назначения. В порядке отображения индикаторов:
серебристый – изменённое имя;
серый –
irrevocable
;сине-зелёный –
instant
;синий – с дополнительным
up
действием;фиолетовый – изменённое время удержания;
розовый – изменённое время ожидания дочернего события.
Список жестов показывает заданное имя, значение, количество вложенных назначений, и слой, на котором он задан. Но если по текущему пути жесты не могут быть назначены (на корневом уровне; сразу под жестом, без клавиши-ключа; под аккордом; под системным модификатором), список отображает, под какими клавишами от текущего пути есть вложенные жесты, и их количество.
Слои
Все назначения можно группировать по "слоям" – отдельным файлам, которые можно переключать как через интерфейс, так и специальными назначениями, через действие с функцией SetActiveLayers
или ToggleLayers
.
Элементы уже присутствующие в списке – предустановленные слои для демонстрации, в том числе и Gestures
. На этом демонстрационном слое представлены две группы назначений, дублирующиеся для клавиатурного и мышиного старта жестов.
Первая группа – взаимодействие с браузером. Расположена на XBM2
(второй боковой кнопке мыши) и на CapsLock
. 17 жестов, практически полностью базовых форм, из одной-двух линий. Три из них имеют вложенные логические назначения.
Вторая группа – мультимедиа и вспомогательные функции. Расположена на XBM1
и ~
/ё
. Здесь и быстрое пояснение выделенного слова с википедии, и текущая погода в заданном городе, генерация случайного пароля, и так далее.
Можно использовать как шаблон или пример для собственных назначений, но если приглянётся большая часть, можете просто изменить существующий под себя.
…опять уносит в техническую документацию, значит пора заканчивать кофе и статью.
Я давний пользователь Gesturefy для Firefox, и жестовое управление уже стало таким родным и повседневным, что даже становится сложно представить, а как всё это делать без жестов. Которых ещё всегда не хватало за пределами FF, но теперь это исправлено.
Здесь ещё есть небольшая полянка для улучшений. Как минимум, нужно будет поиграться с визуальной частью рисования, попробовать разные дополнительные опции распознавания и добавить подсказки с живым распознаванием во время рисования.
Потрогать всё описанное и следить, что будет дальше, можно здесь.
Как и всегда – все замечания, дополнения и предложения приветствуются.
