ENGLISH | KOI | WIN | ALT | ISO
Мои программы

"Вариации на тему" переключатели "рус/лат" (и еще раз - "рус").

Предположим, что вы выполнили предыдущий пример или еще откуда-нибудь взяли (сделали) раскладку клавиатуры с тремя группами.

И вам не нравится способ переключения между группами - три положения у одного переключателя "лат/рус/рус".

Что можно сделать?

Ну, во-первых, давайте вообще забудем пока про символ ISO_Next_Group, к которому "прицеплено" такое неудобное "действие". (Вообще-то, семантику ISO_Next_Group можно и переделать, но пока отложим этот вопрос).

Для простоты рассмотрения будем считать, что переключение у вас делается одной клавишей (обычно - это CapsLock),а не комбинацией клавиш (типа - Shift+Shift или Alt+Shift).

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

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

xkb_symbols { include "en_US(pc104)" replace "new.symbols" };

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

Первый способ - простой и неудобный.

Самый тривиальный способ - сделать две клавиши-переключатели, каждая с двумя состояниями. Одна клавиша переключает "лат./старый рус.", другая - "лат/новый рус.". Или в терминах "номер группы", первая клавиша переключает "group1/group2", другая - "group1/group3".

Выберем два скан-кода и "подвесим" на них "действия" -

key <...> { actions[Group1]= [ LockGroup(group=2) ], actions[Group2]= [ LockGroup(group=1) ] }; key <...> { actions[Group1]= [ LockGroup(group=3) ], actions[Group3]= [ LockGroup(group=1) ] };

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

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

Тогда полное описание будет выглядеть как

key <...> { [NoSymbol],[NoSymbol],[NoSymbol], actions[Group1]= [ LockGroup(group=2) ], actions[Group2]= [ LockGroup(group=1) ], actions[Group3]= [ LockGroup(group=1) ] }; key <...> { [NoSymbol],[NoSymbol],[NoSymbol], actions[Group1]= [ LockGroup(group=3) ], actions[Group2]= [ LockGroup(group=1) ], actions[Group3]= [ LockGroup(group=1) ] };

Но, на мой взгляд, такой способ переключения еще более неудобный. (Я же не обещал "хороших" решений :-).
Поэтому, я его даже не пробовал и вам не предлагаю.

Второй способ (через модификатор).

Более удобным мне представляется способ, когда наш привычный перключатель (пусть это будет CapsLock) продолжает переключать "лат/рус". А вот - какой именно "рус" (вторую или третью группу) он выберет - будет определяться другой клавишей.

Естественно, при этом каждый из переключателей имеет два состояния. Основной переключатель - "лат/рус", дополнительный - "группа2/группа3".

Другими словами, "поведение" основного переключателя должно меняться в зависимости от того, нажимали ли мы дополнительный переключатель (и сколько раз - четное или нечетное количество).

Собственно эти два "поведения" я уже написал в предыдущем примере. Только там были две разные клавиши, а нам надо соместить это на одной. Кстати, можно заметить, что эти два описания отличаются только в одной строчке - "переход из состояния Group1", остальные части описания совпадают.

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

Первое, что приходит в голову - с помощью какого-нибудь модификатора. Напомню, что каждая группа может делиться на уровни (shift level), а выбор конкретного уровня зависит от состояния модификаторов. Причем, эту зависимость мы можем как угодно менять, сочиняя новые "типы" клавиш.

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

Естественно, "установку/сброс" модификатора "повесить" (с помощью соответствующих "действий") на второй переключатель.

Итак. Описание основного переключателя теперь будет иметь вид

key <...> { type[Group1]=".....", [NoSymbol, NoSymbol], [NoSymbol], [NoSymbol], actions[Group1]= [ LockGroup(group=2), LockGroup(group=3)], actions[Group2]= [ LockGroup(group=1) ], actions[Group3]= [ LockGroup(group=1) ] };

("тип" придумаем немного позже).

Теперь надо "сочинить" модификатор и новый тип. И здесь нас ждет "засада". Дело в том, что xkbcomp (по крайней мере в нынешнем состоянии) не позволяет объявить новый виртуальный модификатор. Можно использовать только те, которые в нем уже предопределены.

К счастью, среди его модификаторов уже есть подходящий - LevelThree. А подходящий он потому, что

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

Таким образом, берем модификатор LevelThree и тип "THREE_LEVEL" -

type "THREE_LEVEL" { modifiers = Shift+LevelThree; map[None] = Level1; map[Shift] = Level2; map[LevelThree] = Level3; map[Shift+LevelThree] = Level3; level_name[Level1] = "Base"; level_name[Level2] = "Shift"; level_name[Level3] = "Level3"; };

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

Таким образом, наш основной переключатель (давайте уже определимся, что это будет <CAPS>), приобретает вид

key <CAPS> { type[Group1]="THREE_LEVEL", [NoSymbol, NoSymbol, NoSymbol], [NoSymbol], [NoSymbol], actions[Group1]= [ LockGroup(group=2), NoAction(), LockGroup(group=3)], actions[Group2]= [ LockGroup(group=1)], actions[Group3]= [ LockGroup(group=1)] };

Теперь можно заняться второй клавишей, которая будет "поднимать/опускать" модификатор LevelThree. Конечно, это может быть не одиночная клавиша, а какая-нибудь комбинация клавиш. Но для простоты, сначала возьмем отдельную клавишу. Я на своей клавиатуре ("Микрософтовской") использовал для этого клавишу "Menu" (скан-код <MENU>). Но, если у вас такой клавиши нет, то можно задействовать одну из парных клавиш (Shift, Control, Alt).

Итак. Сочиняем "поведение" клавиши <MENU>.
Прежде всего - я думаю, что вам не хочется держать ее все время нажатой, пока это нужно. То есть, нам надо, чтобы по первому нажатию она "подняла" модификатор, а по второму - опустила.

Как раз это делает "действие" LockMods (в переменной locked modifiers). Напомню, что SetMods и LatchMods "держат" модификаторы только пока нажата клавиша.

Итак, используем "действие" LockMods -

key <MENU> { [ NoSymbol ], actions[Group1]=[ LockMods(modifiers=LevelThree) ]};

(Описывать все три группы не обязательно. Если номер группы больше, чем допустимо для клавиши, XKB будет его "выравнивать", пока не попадет в допустимый диапазон.)

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

Поэтому, последним шагом будет - связать с клавишей какой-нибудь реальный модификатор (с помошью объявления modifier_map). И вот здесь нас ждет вторая "засада". В полной конфигурации все модификаторы (даже "безымянные" - Mod1-Mod5) уже расписаны. То есть, каждый уже соединен с какой-нибудь клавишей и, соответственно, каким-нибудь символом.

Самое неприятное то, что клиентские приложения могут реагировать на эти модификаторы и менять свое поведение. Поэтому, если мы свяжем наш LevelTree, например, с модификатором Mod5, который соответствует символу Scroll_Lock, то, при "поднятии" модификатора LevelThree, приложения будут считать, что нажата кнопка ScrollLock. А вот как они будут вести себя при этом - кто как.

Здесь надо немного пояснить - как реагируют программы на "безымянные" модификаторы. Поскольку этими модификаторам явно никакая функция не соответствует, программы ориентируются на символы, которые с ними связаны.

То есть, если программа обнаружит, что в "состоянии" модификаторов взведен модификатор Mod1, она попытается найти соответствующий ему символ. Обычно это - Alt_L и Alt_R, то есть любая из клавиш Alt. Соответственно, программа считает, что нажата клавиша Alt и ведет себя в соответствии со своим пониманием этой клавиши.

Поэтому, чтобы избежать всяких "побочных" эффектов, нам надо не только связать модификатор с нашей клавишей <MENU>, но и "отцепить" этот реальный модификатор от других. Как я уже говорил в примере "добавление новой группы", "отцепить" модификатор, который был объявлен где-то "в глубинах" includ'ов, очень трудно. но, к счастью, они там привязываются не к скан-кодам, а к символам. Поэтому, если мы в своей конфигурации просто уберем эти символы из описаний скан-кодов, то связка "модификатор-символ" просто "повиснет в воздухе" и таким образом нам удасться "отцепить" модификатор.

Итак. Давайте возьмем реальный модификатор Mod5. Он используется только для символа Scroll_Lock, а этот символ, в свою очередь, "подвешен" только на клавишу ScrollLock (скан-код <SCLK>). Да и клавиша эта не самая "часто используемая".

Теперь нам надо добавить в описание клавиши <MENU> виртуальный модификатор LevelThree. Добавить инструкцию modifier_map, который свяжет Mod5 с нашей клавишей. И, наконец, переопределить клавишу <SCLK>, чтобы в ней не было упоминания и символе Scroll_Lock.

key <MENU> { virtualMods = LevelThree, [ NoSymbol], actions[Group1]=[ LockMods(modifiers=LevelThree) ]}; modifier_map Mod5 { <MENU> }; key SCLK { [NoSymbol] };

Вот теперь можно пробовать - что получилось.

Конечно, это не самое лучшее решение.

  • Во-первых, из-за того, что мы заняли реальный модификатор Mod5 (кстати, и отмена ScrollLock не во всех случаях спасает).
  • Во-вторых, наш дополнительный переключатель сам группы не меняет, а только "машет флагом". Поэтому, если переключение "из лат. в рус" работает всегда, то для переключения "из одного рус. в другой рус." надо не только нажать дополнительный преключатель, но и "щелкнуть" основным.
  • Ну и, наконец, если мы захотим задействовать все четыре группы, то решение будет еще сложнее (понадобится как минимум два виртуальных модификатора, с которыми и так "напряженка").
Поэтому рассмотрим еще один способ -

Третий способ (через дополнительную переменную номера группы).

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

Но ведь можно "запомнить" состояние дополнительного переключателя нет только с помощью модификатора.

Напомню, что номер группы может храниться в трех внутренних переменных XKB - locked, latched и base group, значение которых можно менять независимо. Причем для выбора символа у клавиши используется суммарное значение этих переменных - effective group.

Напомню также, что благодаря "методу выравнивания номера группы" (см. "Внутренности":Метод выравнивания..."), если суммарное значение окажется больше, чем количество существующих групп на единицу, то опять получится первая группа и т.д.

Таким образом, можно заставить дополнительный переключатель "запоминать" номер "альтернативной" группы в дополнительной "групповой" переменной, например - base group. А основной переключатель пусть манипулирует значением в другой переменной, например - locked group.

Итак. Пусть дополнительный переключатель запоминает - какая из "русских" групп нам требуется (2 или 3), например, в переменной base group.

Тогда основной переключатель, чтобы перейти из "лат" в из "рус" раскладок должен просто обнулить locked group. А для того, чтобы вернуться обратно в "лат", надо в locked group записать "добавку", достаточно большую, чтобы "метод выравнивания" "завернул" значение обратно к "лат" раскладке.

Таким образом мы нашу проблему (переход из Group1 в два разных состояния) решаем. Правда теперь у на появляется проблема - как вернуться обратно из разных состояний в одно и ту же группу. Но она то, решается очень просто. Поскольку клавиатура при этом находится в разных состояниях (Group2 и Group3), то мы без проблем "подвешиваем" на клавишу два разных "действия", каждое со своим значением "добавки".

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

Надо сказать об одной тонкости - хотя в "конфигах" группы нумеруются - 1,2,3..., внутри XKB это означает - 0,1,2...
То есть, group1 - на самом деле 0, group2 - 1 и т.д.

Итак. Давайте сначала рассмотрим задачу во внутренних значениях XKB (0,1,2).
Тогда у нас

  • первая группа ("лат") - 0
  • вторая группа ("старый рус.") - 1
  • третья группа ("новый рус.") - 2
  • максимальный номер группы - 2
При этом
  • если к 1 добавить 2 - получится снова 0 (3 на единицу больше, чем "максимальный номер группы")
  • аналогично, если к 2 добавить 1 - снова вернемся к группе 0.

Тогда алгоритм работы переключателей мог бы быть таким -

  • в дополнительной переменной (base group) запоминается номер одной из русских раскладок - то есть, 1 или 2;
  • для того, чтобы перейти из состояния, когда текущая группа "лат", в выбранную "рус", надо просто в locked group записать 0;
  • а для того, чтобы вернуться в "лат" из состояния "рус." надо в переменную base group записать -
    • 2, если текущая группа 1;
    • 1, если текущая группа 2.

Однако, не все так просто. Если мы попытаемся воплотить это в конфигурацию клавиши, то нарвемся на очередную "засаду".

Дело в том, что значения номера группы "запоминаются" только в locked group. В других переменных (base и latched) они "держаться" только пока соответствующая клавиша нажата и "испаряются" оттуда, если клавишу отпустить.

Правда, у нас есть одна "лазейка". Мы можем изменить поведение клавиши (той, что будет менять base group) и сделать ее "залипающей". Тогда XKB при первом нажатии/отжатии клавиши выполнит только ту задачу, которая выполняется при нажатии клавиши, а при повторном нажатии/отжатии наоборот - только то, что выполняется при отжатии клавиши.

Только обратите внимание, что когда мы ее все-таки "отожмем" там получится 0. То есть, мы не сможем держать там "2 или 3", а только "2 или 0", "1 или 0" и т.п.

Ну и ладно. Придется в ней держать не "номер алтернативной раскладки", а только "добавку" к номеру. То есть - не "2 или 3", а "0 или 1".

Тoгда нам придется слегка подправить наш алгоритм -

  • в дополнительной переменной (base group) запоминается "смещение" от первой "русской" раскладки - 0 или 1;
  • для того, чтобы перейти из состояния "лат", в нужную "рус", надо в locked group записать 1;
  • для того, чтобы вернуться в "лат" из состояния "рус." надо в переменную base group записать -
    • если текущая группа 1 (в base 0) - 0;
    • если текущая группа 2 (в base 1) - 2.

Теперь, переходя к обозначениям из "конфигов" (group1, group2 и т.д.), получим -

  • дополнительный переключатель:
    • ну, 0 или Group1 там получится автоматически при "отжатии" клавиши
    • "запоминать" в base group надо Group2,
  • основной переключатель:
    • "действие" для Group1 - записать в locked group значение Group2
    • для Group2 - записать Group1
    • для Group3 - записать Group3

Сочиняем описание клавиш (пусть это будут те же <CAPS> и <MENU>)

key <CAPS> { [NoSymbol],[NoSymbol],[NoSymbol], actions[Group1]= [ LockGroup(group=2) ], actions[Group2]= [ LockGroup(group=1) ], actions[Group3]= [ LockGroup(group=3) ] }; key <MENU> { [ NoSymbol ], locks= yes actions[Group1]=[ SetGroup(group=2) ]};

(обратите внимание - мы для дополнительного переключателя опять описываем только одну группу. Все равно он может "перещелкивать" только два состояния, и делать его "чувствительным" к текущей группе нет смысла.)

Можно пробовать.

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

Но вот если "пощелкать" им, когда основной переключатель стоит в состоянии "лат", то результаты будут несколько "странные" (вы можете сами их "просчитать"), поскольку "состояние лат." на самом деле - "хитро" подобранная сумма двух переменных (base и locked).

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

Замечу также, что это не единственный вариант переключения "с помощью двух переменных group". Можно, например, сделать так, чтобы дополнительный переключатель работал с locked group, а основной - с base group (если посмотреть исходный алгоритм, то там у основного переключателя есть состояние когда он "держит" в "своей" переменной 0). Но этот вариант не лучше в смысле "побочных эффектов", а в описании, пожалуй, сложнее.

Поэтому, пока не этом и остановимся.
Тем более, что у нас остался еще -

"Заключительный аккорд" - "отцепляемся" от скан-кодов.

Как я уже говорил - не очень хорошо, когда "действия" назначаются прямо в описании клавиши (в файлах xkb_symbols).

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

Обычно "хорошим тоном" является - привязать "действия" к каким-нибудь сиволам (через "интерпретации"), а в описании клавиш использовать только символы.

Давайте проделаем это для любого из примеров, например, последнего.

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

К счастью, в наборе символов есть группа кодов, которые должны использоваться только для внутренних нужд XKB и игнорироваться прикладными программами. Вы можете найти их в файле {XROOT}/include/X11/keysymdef.h их названия начинаются на XK_ISO_. Например,

XK_ISO_First_Group XK_ISO_First_Group_Lock XK_ISO_Last_Group XK_ISO_pv_Group_Lock

и т.п.

Конечно, там нет символов из названия которых следует, что по этому символу "записать в locked group число 3". Но мы можем изменить семантику любого из символов (тем более, что и не для каждого из этих символов задана семантика в "конфигах" XKB).

Давайте сначала выясним - сколько нам надо символов. Для этого посчитаем - сколько у нас различных действий в описании обоих переключателей.

LockGroup(group=1) LockGroup(group=2) LockGroup(group=3) SetGroup(group=2)

Итого - 4 штуки.

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

Обратите внимание, чтов названиях присутствуют - First_Group, Last_Group, Next_Group и pv_Group.

Давайте считать, что для нашего случая

  • First_Group - group=1
  • Next_Group - group=2
  • Last_Group - group=3

Тогда логично выбрать

  • для LockGroup(group=1) - ISO_First_Group_Lock
  • для LockGroup(group=2) - ISO_Next_Group_Lock
  • для LockGroup(group=3) - ISO_Last_Group_Lock
  • для SetGroup(group=2) - ISO_Group_Lock

Составим соответствующие "интерпретации". Напомню, что это нужно делать в файле типа xkb_compat и, соответственно, "приплюсовать" это файл к строчке в "полной" конфигурации, которая описывает именно xkb_compat (а не xkb_symbols).

Итак, наша "добавка" к xkb_compat (не забудьте ее "приплюсовать" куда надо) -

xkb_compat { interpt ISO_First_Group_Lock { action=LockGroup(group=1); }; interpt ISO_Next_Group_Lock { action=LockGroup(group=2); }; interpt ISO_Last_Group_Lock { action=LockGroup(group=3); }; interpt ISO_Group_Lock { action=SetGroup(group=2); locking = True;}; group 2 = AltGr; group 3 = AltGr; };

(Последнии две инструкции, "по идее" не нужны, поскольку уже должны быть в "общей конфигурации". Но почему-то они иногда "теряются".)

А описание символов (в xkb_symbols) теперь будут выглядеть как

key <CAPS> { [ ISO_Next_Group_Lock ], [ ISO_First_Group_Lock ], [ ISO_Last_Group_Lock ] }; key <MENU> { [ ISO_Group_Lock ] };

Можно пробовать.

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

Еще несколько "переключателей".