Table of Contents

= Работа с кнопками и клавишами в 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" <chg 3-31-98 RM>
#define	keyBitContrast		0x0200		// Contrast key

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

Очередь клавиш

События keyDownEvent хранятся в отдельной очереди. Это вызвано двумя причинами: экономией места и возможностью быстро добавлять клавиши из системного прерывания. Функция EvtAddEventToQueue кроме копирования эвента в очередь также преобразовывает координаты эвента screenX и screenY в дисплейные, что совершенно не нужно для клавиш. Экономия памяти достигается специальным кодированием клавиш в очереди. 8-битный символ без флагов и кода хранится как 1 байт. Совершенно аналогично используется очередь росчерков. Функция SysGetEvent начинает поиск событий с проверки очередей клавиш и росчерков, а уже потом переходит к обычной очереди эвентов.

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

Собираем все вместе. Как образуются keyDownEvent

Рассмотрим как нажатие на клавишу порождает эвент.

  1. Нажатие на клавишу вызывает системное прерывание. Обработчик прерывания получает маску нажатых сейчас клавиш, убирает клавиши, чья генерация кодов была запрещена вызовом KeySetMask. Оставшиеся после маскирования клавиши проверяются с предыдущим состоянием активных кнопок. Если была нажата новая клавиша, то вычисляется ее код и она добавляется в очередь клавиш. Если прерывание вызвано повтором, то с помощью параметров выставленных вызовом функции KeyRates проверяется необходимость генерации повторного кода с выставленным флагом autoRepeatKeyMask.
  2. Функция EvtGetEvent извлекает символ из очереди клавиш и заполняет структуру EventType. Полученный эвент отдается пользователю
  3. Функция SysHandleEvent проверяет эвент и если выставлен commandKeyMask, то пытается обработать знакомые коды.
  4. Функции 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.

Интересен смысл параметра autoShiftedP. В нем возвращается true, если шифт был вызван системой через вызов GrfProcessStroke(NULL, NULL, true) или через GrfSetState(caps, num, true). Если же был шифт был вызван росчерком, то в параметре возвращается false.

Мораль этой главы простая: к KeyManager состояния шифтов отношения не имеют.

Бедные разработчики клавиатурных драйверов.

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

Изыски лицензиатов

Во времена до 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 породит следующую последовательность эвентов:

Невозможность изменения времени до посылки keyHoldEvent и отсутствие его повторений оставляют место для использования флага autoRepeatKeyMask.

Эвенты keyDownEvent и keyHoldEvent выставляют новый флаг в modifiers:

#define willSendUpKeyMask  0x0800      // True if a keyUp event will be sent later

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

Boolean HsKeyEventIsFromKeyboard (EventPtr eventP);

. Что она проверяет?

Для поддержки клавиатуры было задействовано поле 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.