= Работа с кнопками и клавишами в PalmOS =
===== Введение =====
PalmOS изначально разрабатывалась как ОС для бесклавиатурных машинок. Поэтому Key Manager, модуль работы с клавиатурой проектировался как обработчик пяти-шести кнопок. Прошло время и работа с клавишами на современных palm-устройствах превратилась в ад. Эта статья пытается провести Вас по кругам этого ада.
===== Классическая обработка кнопок =====
В PalmOS до 4 версии включительно работа с клавишами была унифицирована. Исключение составляли редкие дополнительные клавиши, типа Jog у Clie, но их было мало и никаких сложностей они не создавали.
==== KeyEvent ====
Самый высокий уровень работы с кнопками - это обработка соответствующих эвентов. Существует главное сообщение: keyDownEvent. При нажатии на кнопку PalmOS генерирует его. В сообщении передается структура _KeyDownEventType, где приведены специфические параметры сообщения.
struct _KeyDownEventType {
WChar chr; // ascii code
UInt16 keyCode; // virtual key code
UInt16 modifiers;
};
В структуре три поля: код символа, код клавиши и флаги.
Заметим, что события keyUpEvent нет. То есть отслеживаются только нажатия на кнопки.
=== Код символа ===
Код символа может быть ASCII-символом, символом из диапазона 128-255 (назначение символа зависит от текущей кодировки) и "виртуальным" символом с кодом > 255.
Виртуальные символы используются для кодирования нажатий на аппаратные кнопки, на кнопки, нарисованные на области граффити и для передачи информации между различными модулями PalmOS. Последний метод стал очень популярным и используется как отложенный триггер некоторых событий. Для того, чтобы символ воспринялся как виртуальный, нужно выставить в поле modifiers флаг commandKeyMask. Возможно, что если этот флаг не выставлен, то символы с кодом > 255 будет воспринят как символ из мультибайтовый кодировки.
Поясню мудреную фразу про триггер на примерах. Для активации сообщения о разряде батареи достаточно запостить в очередь символ vchrLowBattery. Функция SysHandleEvent обрабатывает этот эвент и показывает диалог о разряде. Вызов функции SysBatteryDialog вместо посылки эвента тоже работает, но а) он прерывает текущие выполняемые действия, в то время как в момент вызова SysHandleEvent приложение явно отдает управление PalmOS, б) эвент можно послать даже из прерывания, в) вызов в произвольное время с показам диалога может вызвать изменения в состоянии оконного интерфейса к которым прерываемая программа может быть не готова. Аналогично эвент vchrMenu показывает меню, а vchrAlarm запускает исполнение наступившего аларма. Если для взаимодействия модулей достаточно передать сам факт активации события, то такой механизм общения через сообщение подходит: не нужно выдумывать нового эвента, достаточно задействовать новый виртуальный код символа.
С аппаратными кнопками тоже все просто: классические пальмы посылают vchrHard1-vchrHard4 по нажатию на 4 кнопки. Все тот же SysHandleEvent перехватывает эти эвенты и запускает соответствующие приложения. Всем лицензиатам было выделено по диапазону из 256 кодов для расширенных кнопок.
=== Код клавиши ===
Коды клавиш не использовались. В этом поле можно передавать свою информацию. Стандартные кнопки и буквы после граффити никак не используют это поле, передавая там ноль. В принципе ничто не мешает при использовании виртуальных символов задействовать это поля для передачи информации.
=== Модификаторы (флаги) символов ===
В поле modifiers находятся флаги с разнообразной информацией о передаваемом символе.
// keyDownEvent modifers
#define shiftKeyMask 0x0001
#define capsLockMask 0x0002
#define numLockMask 0x0004
#define commandKeyMask 0x0008
#define optionKeyMask 0x0010
#define controlKeyMask 0x0020
#define autoRepeatKeyMask 0x0040 // True if generated due to auto-repeat
#define doubleTapKeyMask 0x0080 // True if this is a double-tap event
#define poweredOnKeyMask 0x0100 // True if this is a double-tap event
#define appEvtHookKeyMask 0x0200 // True if this is an app hook key
#define libEvtHookKeyMask 0x0400 // True if this is a library hook key
Самый важный флаг - это commandKeyMask. Этот флаг должен быть выставлен для всех виртуальных символов. Без этого флага виртуальные символы не будут обрабатываться. Это единственный флаг, который имеет смысл проверять в прикладных программах.
Часть флагов указывает на состояние граффити в момент ввода символа: shiftKeyMask, capsLockMask, numLockMask. Самое забавное, что эти флаги граффити не генерирует. Иногда их генерируют драйвера внешних клавиатур. Часть аналогичных флагов была зарезервирована на будущее: optionKeyMask, controlKeyMask. Важно понимать, что в отличии от флага commandKeyMask, эти флаги не влияют на смысл кода символа, они просто могут указывать на нажатые шифты. А могут и не указывать. Если мы действительно хотим узнать состояние флагов, то мы должны вызвать функцию GrfGetState.
Флаг autoRepeatKeyMask выставляется при удержании ( hold) клавиши. Похожий флаг doubleTapKeyMask не реализован. Флаг autoRepeatKeyMask используется моей программой TreoKeyHack для различения удержания от повторных нажатий. При удержании клавиши все последующие сгенерированные эвенты будут содержать флаг autoRepeatKeyMask, в то время как повторные нажатия обойдутся без него.
Флаг poweredOnKeyMask выставляется клавише, которая разбудила устройство. К сожалению этот флаг выставляют далеко не все пальмы (например, clie его не всегда выставляют), так что полезность этого флага сомнительна.
Забавный флаг appEvtHookKeyMask запускает приложение с creator id, составленным из chr и keyCode с launch code sysAppLaunchCmdEventHook. Не знаю, зачем это было сделано, но мой SilkMan использовал этот флаг для исполнения собственного кода по нажатию на экранную кнопку.
creator = eventP->data.keyDown.chr;
creator <<= 16;
creator |= eventP->data.keyDown.keyCode;
err = DmGetNextDatabaseByTypeCreator(true/*newSearch*/, &searchState, sysFileTApplication, creator,
true/*onlyLatestVers*/, &cardNo, &dbID);
if ( !err){
err = SysAppLaunch(cardNo, dbID, 0/*launchFlags*/, sysAppLaunchCmdEventHook, (MemPtr)eventP, &result);
}
Флаг libEvtHookKeyMask используется в Exchange manager. Аналогичен предыдущему флагу, но вызывает ExgLibHandleEvent(eventP->data.keyDown.keyCode,eventP);
==== Отслеживание кнопок на низком уровне ====
Для разработчиков игр получение клавиш через keyDownEvent не годится - очень медленно. Поэтому в PalmOS существует низкоуровневый способ обработки клавиш, функция KeyCurrentState. Функция возвращает 32-битовую маску нажатых сейчас кнопок.
#define keyBitPower 0x0001 // Power key
#define keyBitPageUp 0x0002 // Page-up
#define keyBitPageDown 0x0004 // Page-down
#define keyBitHard1 0x0008 // App #1
#define keyBitHard2 0x0010 // App #2
#define keyBitHard3 0x0020 // App #3
#define keyBitHard4 0x0040 // App #4
#define keyBitCradle 0x0080 // Button on cradle
#define keyBitAntenna 0x0100 // Antenna "key"
#define keyBitContrast 0x0200 // Contrast key
Этот способ позволяет получать статус кнопок "сейчас", а не спустя миллисекунды, требующиеся для прохождения через очередь событий. Также можно отслеживать отпускания кнопок и одновременное нажатие нескольких кнопок.
==== Очередь клавиш ====
События keyDownEvent хранятся в отдельной очереди. Это вызвано двумя причинами: экономией места и возможностью быстро добавлять клавиши из системного прерывания. Функция EvtAddEventToQueue кроме копирования эвента в очередь также преобразовывает координаты эвента screenX и screenY в дисплейные, что совершенно не нужно для клавиш. Экономия памяти достигается специальным кодированием клавиш в очереди. 8-битный символ без флагов и кода хранится как 1 байт.
Совершенно аналогично используется очередь росчерков.
Функция SysGetEvent начинает поиск событий с проверки очередей клавиш и росчерков, а уже потом переходит к обычной очереди эвентов.
Ничто не мешает программисту добавлять символы через EvtAddEventToQueue в обычную очередь, но это просто приведет к тому, что эвент будет занимать больше места в очереди эвентов (а не очереди клавиш) и будет отдан после исчерпания очереди клавиш.
==== Собираем все вместе. Как образуются keyDownEvent ====
Рассмотрим как нажатие на клавишу порождает эвент.
- Нажатие на клавишу вызывает системное прерывание. Обработчик прерывания получает маску нажатых сейчас клавиш, убирает клавиши, чья генерация кодов была запрещена вызовом KeySetMask. Оставшиеся после маскирования клавиши проверяются с предыдущим состоянием активных кнопок. Если была нажата новая клавиша, то вычисляется ее код и она добавляется в очередь клавиш. Если прерывание вызвано повтором, то с помощью параметров выставленных вызовом функции KeyRates проверяется необходимость генерации повторного кода с выставленным флагом autoRepeatKeyMask.
- Функция EvtGetEvent извлекает символ из очереди клавиш и заполняет структуру EventType. Полученный эвент отдается пользователю
- Функция SysHandleEvent проверяет эвент и если выставлен commandKeyMask, то пытается обработать знакомые коды.
- Функции MenuHandleEvent и FrmDispatchEvent обрабатывают те коды, которые относятся к текущему состоянию пользовательского интерфейса. Например, field control в фокусе добавит в свой текст буквенный символ.
==== Состояния шифтовых клавиш ====
Вся обработка shiftов происходит в GraffitiManager (функции GrfGetState и GrfSetState).
#define grfTempShiftPunctuation 1
#define grfTempShiftExtended 2
#define grfTempShiftUpper 3
#define grfTempShiftLower 4
Err GrfGetState(Boolean *capsLockP, Boolean *numLockP, UInt16 *tempShiftP, Boolean *autoShiftedP);
Err GrfSetState(Boolean capsLock, Boolean numLock, Boolean upperShift);
Все эти состояния появились из росчерков Graffiti 1.
* caps lock - это две вертикальные черты снизу вверх.
* num lock уже никогда не выставляется, он был создан в те времена, когда не было двух областей в зоне граффити и существовал росчерк для перехода в режим ввода цифр.
* temp shift punctuation выставляется после ввода точки
* temp shift extended выставляется после ввода росчерка из левого верхнего в правый нижний угол
* temp shift upper выставляется после ввода одной вертикальной черты снизу вверх
* temp shift lower похоже уже не выставляется и нужен был для перехода из numlock во временный ввод буквы.
Интересен смысл параметра autoShiftedP. В нем возвращается true, если шифт был вызван системой через вызов GrfProcessStroke(NULL, NULL, true) или через GrfSetState(caps, num, true). Если же был шифт был вызван росчерком, то в параметре возвращается false.
Мораль этой главы простая: к KeyManager состояния шифтов отношения не имеют.
==== Бедные разработчики клавиатурных драйверов. ====
Первыми с недостатками указанного интерфейса столкнулись разработчики клавиатурных драйверов. Обнаружились следующие факты:
* Вся обработка shiftов происходит в GraffitiManager и использовать ее для совмещения с клавиатурой не очень удобно. Либо приходится лезть в GraffitiManager на каждую клавишу, чтобы отследить прелести типа autoshift, либо вести раздельное состояние шифтов в граффити и клавиатуре. У каждого способа есть свои минусы.
* Клавиатура эмулирует только работу через keyDownEvent. Динамические игры, работающие через KeyCurrentState не могут управляться с клавиатуры.
* Отсутствует клавиатурная навигация по контролам в форме.
==== Изыски лицензиатов ====
Во времена до 5 оси лицензиаты не выходили сильно за рамки описанного. Максимум добавлялся JogDial и пара-тройка новых виртуальных кодов для них. Активнее использовались виртуальные символы для передачи внутренней информации.
===== PalmOS 5. Здравствуй новый год =====
PalmOS 5 и первое устройство Tungsten T внесли два новшества: нотификацию sysNotifyEventDequeuedEvent и 5-way navigator.
==== sysNotifyEventDequeuedEvent ====
На замену исчезнувшим хакам была предложена нотификиция sysNotifyEventDequeuedEvent. Эта нотификация рассылается функцией EvtGetEvent перед выдачей эвента наверх. Другая нотификаци sysNotifyVirtualCharHandlingEvent рассылается в том же месте, но только для виртуальных символов с поднятым флагом commandKeyMask. Эти нотификации позволили проверять и модифицировать очередь эвентов резидентными программами.
==== 5-Way Navigator ====
Вместо привычных кнопок веерх-вниз у TT появился джойстик. А у модераторов форумов по программированию под PalmOS появилась назойливая обязанность объяснять несколько раз на неделе, что коды, порождаемые джойстиком, нужно искать не в заголовочных файлах от PalmOS, а в SDK от компании PalmOne, производителя устройства. Разработчики устройства добавили единственный новый символ vchrNavChange (к существовавшим vchrPageUp и vchrPageDown) и задействовали поле keyCode под маску нажатых кнопок (вверх, вниз, влево, вправо, центр) и маску кнопок, изменивших состояние с предыдущего vchrNavChange. Также под новые кнопки добавили битов для KeyCurrentState. Практически каждый программист путался в keyBitNavLeft, navBitLeft и navChangeLeft. Но в целом новое API было удобным и мощным.
==== Sony ====
Клавиатурные КПК от Sony практически ничего не добавили к существовавшей раньше работе с клавиатурой.
==== Treo 600/650 ====
Большой шаг в добавлении клавиатуры к PalmOS сделала компания Handspring. Treo 600 отличается продуманной работой со встроенной клавиатурой. Это не могло не сказаться на работе с очередью клавиш.
=== Новые эвенты ===
Во-первых эти машинки генерируют эвенты keyUpEvent и keyHoldEvent. Информация, передаваемая в эвентах аналогична информации в keyDownEvent. Смысл keyUpEvent понятен, а вот keyHoldEvent посылается однократно через секунду после нажатия на клавишу и только если после этой клавиши ничего больше не нажималось. То есть удержание shift + A породит следующую последовательность эвентов:
* shift down
* A down
* A hold
* A up
* shift up
Невозможность изменения времени до посылки keyHoldEvent и отсутствие его повторений оставляют место для использования флага autoRepeatKeyMask.
Эвенты keyDownEvent и keyHoldEvent выставляют новый флаг в modifiers:
#define willSendUpKeyMask 0x0800 // True if a keyUp event will be sent later
По этому флагу можно определять то, что сообщение пришло от клавиатуры. Но лучше для такой проверки использовать готовую функцию Boolean HsKeyEventIsFromKeyboard (EventPtr eventP);
. Что она проверяет?
* Что тип события keyDownEvent, keyUpEvent или keyHoldEvent
* Что у событий keyDownEvent или keyHoldEvent выставлен бит willSendUpKeyMask
* Что у событий не выставлены биты appEvtHookKeyMask libEvtHookKeyMask
* Что в поле keyCode не ноль
Для поддержки клавиатуры было задействовано поле keyCode. В это поле заносится код клавиши, вызвавшей данный эвент. Обычно он совпадает с прописной буквой, нанесенной на клавиатуре.
=== Новые функции ====
Было добавлено много новых функций, расширяющих стандартный KeyManager. Низкоуровневые функции были расширены возможностью проверки бит для всех клавиш:
void HsKeyCurrentStateExt(UInt32 keys[3]);
void HsKeySetMaskExt (const UInt32 keyMaskNew[3], UInt32 keyMaskOld[3]);
UInt16 HsKeysPressed (UInt16 count, const UInt16 keyCodes[], Boolean pressed[]);
Err HsKeyStop (UInt16 keyCode);
Boolean HsKeyEnableKey (UInt16 keyCode, Boolean enabled);
Функция HsKeyCurrentStateExt расширила функцию KeyCurrentState до массива из 96 бит. Функция HsKeySetMaskExt пришла на замену KeySetMask.
Для упрощения программы вместо HsKeyCurrentStateExt можно использовать более простую функцию HsKeysPressed? которой передается массив кодов клавиш, состояние которых нужно проверить. Чаще это сделать проще, чем проверять биты.
Функция HsKeyEnableKey призвана упростить функцию HsKeySetMaskExt. Вместо выставления длинных битовых масок просто достаточно указать коды клавиш, которые не должны генерировать эвенты.
Функция HsKeyStop запрещает дальнейший автоповтор текущей нажатой клавиши. После отпускания клавиши автоповтор возобновляется.
Функция HsKeyEventIsFromKeyboard проверяет был ли текущий эвент получен от клавиатуры.
Появилась возможность переводить код клавиши в символ и наоборот:
UInt16 HsKeyChrCodeToKeyCode (UInt16 chrCode);
void HsKeyKeyCodeToChrCode (UInt16 keyCode, UInt16 modifiersIn, UInt16* chrP, UInt16* modifiersOutP);
Функция HsKeyChrCodeToKeyCode возвращает клавишу, которая могла бы сгенерировать такой код или 0, если такой код нельзя ввести с клавиатуры.
Функция HsKeyKeyCodeToChrCode по коду клавиши и входному полю modifiersIn определяет соответствующий код. Из битового поля modifiersIn используются только два флага: optionKeyMask и shiftKeyMask для определения из какой раскладки генерировать символ. В поле chrP записывается код символа, а в modifiersOutP записываются требуемые флаги символа. Требуемый флаг обычно один, наш любимый commandKeyMask. Он генерируется, например, для Opt+P, который порождает символ vchrBrightness. Флаги из modifiersIn никоим образом не копируются в modifiersOutP.