Модель обработки ввода и событий GTK+
Модель обработки ввода и событий GTK+ — подробное описание обработки ввода и событий в GTK+
Обзор обработки ввода и событий в GTK+
В этой главе подробно описано, как GTK+ обрабатывает ввод. Если вас интересует, что происходит при преобразовании нажатия клавиши или движения мыши пользователем в изменение виджета GTK+, вам следует прочитать эту главу. Эти знания также будут полезны, если вы решите реализовать свои собственные виджеты.
Устройства и события
Наиболее базовые устройства ввода, с которыми каждый пользователь компьютера взаимодействовал, — это клавиатура и мышь; помимо них, GTK+ поддерживает тачпады, сенсорные экраны и более экзотические устройства ввода, такие как графические планшеты. Внутри GTK+ каждое такое устройство ввода представлено объектом GdkDevice.
Для упрощения работы с вариативностью между этими устройствами ввода, GTK+ имеет понятие мастер- и slave-устройств. Конкретные физические устройства, имеющие множество различных характеристик (мыши могут иметь 2, 3 или 8 кнопок, клавиатуры имеют различные раскладки и могут или не могут иметь отдельный блок цифр и т. д.) представляются как slave-устройства. Каждое slave-устройство связано с виртуальным master-устройством. Master-устройства всегда представлены парами «указатель/клавиатура» — такую пару можно рассматривать как «место».
Виджеты GTK+ обычно работают с мастер-устройствами и, таким образом, могут использоваться с любым устройством указания или клавиатурой.
Когда пользователь взаимодействует с устройством ввода (например, перемещает мышь или нажимает клавишу на клавиатуре), GTK+ получает события от системы окон. Эти события обычно направляются на конкретное окно — для событий указателя — это окно под указателем (захваты усложняют это), для событий клавиатуры — окно с фокусом клавиатуры.
GDK преобразует эти исходные события системы окон в GdkEvents. Типичные события ввода:
| GdkEventButton |
| GdkEventMotion |
| GdkEventCrossing |
| GdkEventKey |
| GdkEventFocus |
| GdkEventTouch |
Кроме того, GDK/GTK синтезирует другие сигналы, чтобы сообщить, блокируют ли захват (системный или внутри приложения) ввод:
| GdkEventGrabBroken |
| “grab-notify” |
При инициализации GTK+ он устанавливает функцию обработчика событий с помощью gdk_event_handler_set(), которая получает все эти события ввода (а также другие, например, связанные с управлением окнами).
Распространение событий
Для виджетов, у которых установлен GdkWindow, события поступают из системы окон и передаются в gtk_main_do_event(). Подробности о его работе см. в документации: сжатие событий enter/leave, идентификация виджета, получающего событие, помещение события в стек для gtk_get_current_event() и распространение события к виджету.
Когда GDK-бекенд генерирует событие ввода, оно привязано к GdkDevice и GdkWindow, которые, в свою очередь, представляют поверхность системы окон в бэкенде. Если виджет захватил текущее устройство ввода или все устройства ввода, событие распространяется на этот GtkWidget. В противном случае оно распространяется на GtkWidget, который вызвал gtk_widget_register_window() на GdkWindow, получившем событие.
Захваты реализованы для каждого устройства ввода и глобально. Захват для конкретного устройства ввода (gtk_device_grab_add()) получает события в приоритете перед глобальным захватом (gtk_grab_add()). Захваты ввода действуют только в пределах GtkWindowGroup, содержащем GtkWidget, который зарегистрировал GdkWindow события. Если этот GtkWidget является дочерним элементом виджета захвата, событие распространяется на дочерний элемент — это основа для распространения событий в модальных диалоговых окнах.
Событие распространяется на виджет с помощью gtk_propagate_event(). Распространение различается в зависимости от типа события: события нажатия клавиши (GDK_KEY_PRESS, GDK_KEY_RELEASE) доставляются в верхнеуровневое GtkWindow; другие события распространяются вниз и вверх по иерархии виджетов в трех фазах (см. GtkPropagationPhase).
Для событий нажатия клавиши обработчики по умолчанию верхнеуровневого окна для сигналов “key-press-event” и “key-release-event” обрабатывают мнемоники и акселераторы в первую очередь. Затем другие нажатия клавиш передаются в gtk_window_propagate_key_event(), которое распространяет событие вверх от текущего фокусного виджета окна (gtk_window_get_focus()) до верхнего уровня.
Для других событий в первой фазе (фазе «захвата») событие доставляется каждому виджету сверху вниз (например, верхнеуровневому GtkWindow или виджету захвата) до целевого GtkWidget. Жесты, присоединенные с помощью GTK_PHASE_CAPTURE, получают возможность отреагировать на событие.
После фазы «захвата» виджет, который должен был стать объектом назначения события, выполнит присоединенные к нему жесты с помощью GTK_PHASE_TARGET. Это известна как фаза «цель» и происходит только на этом виджете.
Далее, посылается сигнал “event”, а затем соответствующий сигнал для данного события, например “motion-notify-event”. Обработка этих сигналов была основным способом обработки ввода в виджетах GTK+ до появления жестов. Если виджет реализован, посылается сигнал “event-after”. Сигналы посылаются от целевого виджета к верхнему уровню в рамках фазы «пузырька».
Обработчики по умолчанию для сигналов событий отправляют событие жестам, которые присоединены с помощью GTK_PHASE_BUBBLE. Следовательно, жесты в фазе «пузырька» используются только в том случае, если у виджета нет собственных обработчиков событий или он заботится о передаче вверх по умолчанию GtkWidget обработчиков.
События не доставляются виджету, который нечувствителен или не отображен.
В любой момент во время фазы распространения виджет может указать, что полученное событие было обработано, и распространение следует остановить. В традиционных обработчиках событий это намекает на возврат GDK_EVENT_STOP. Если используются жесты, это может произойти, когда виджет сообщает жесту о том, что он берет событие для своей обработки. Подробнее см. раздел «состояния жестов» ниже.
Маски событий
Каждый экземпляр виджета имеет основную маску событий и другую, специфичную для устройства ввода, которые определяют типы событий ввода, которые он получает. Каждая настройка маски событий для виджета добавляется к соответствующей (основной или по устройству) маске событий для окна GdkWindow виджета и всех дочерних GdkWindows.
Если виджет бессоветный (gtk_widget_get_has_window() возвращает FALSE) и приложение хочет получать пользовательские события на нём, его необходимо поместить внутрь GtkEventBox для получения событий, и соответствующая маска событий должна быть установлена для этой области. При реализации виджета используйте GDK_INPUT_ONLY окна GdkWindow для получения событий вместо этого.
Фильтрация событий по маске событий происходит внутри окна GdkWindow, которое предоставляет маски событий для системного окна, чтобы уменьшить количество событий, которые GDK получает от него. При получении события, оно фильтруется по маске окна GdkWindow для устройства ввода, если она установлена. В противном случае, оно фильтруется по основной маске событий окна GdkWindow.
Это означает, что виджеты должны добавлять в маску событий каждый тип события, который они ожидают получить, используя gtk_widget_set_events() или gtk_widget_add_events() для сохранения существующей маски. Виджеты, которые работают с плавающими устройствами, должны использовать gtk_widget_set_device_events() или gtk_widget_add_device_events(), и должны явно включить устройство, используя gtk_widget_set_device_enabled(). Смотрите документацию по GdkDeviceManager для получения дополнительной информации.
Все стандартные виджеты устанавливают маску событий для всех событий, которые они ожидают получить, и изменять её не требуется. Маски следует устанавливать при реализации нового виджета.
События касания
События касания генерируются как события типа GDK_TOUCH_BEGIN, GDK_TOUCH_UPDATE или GDK_TOUCH_END, эти события содержат «последовательность событий», которая однозначно идентифицирует физическое касание до тех пор, пока оно не будет отпущено с устройства.
На некоторых платформах системных окон, устройства мультитач выполняют эмуляцию указателя, это работает, предоставляя подсказку «эмуляция указателя» одной из текущих взаимодействующих последовательностей касаний, которая будет передаваться в каждом событии GdkEventTouch из этой последовательности. По умолчанию, если виджет не запросил события касания, установив GDK_TOUCH_MASK в своей маске событий и не переопределил сигнал «touch-event», GTK+ преобразует эти события «эмуляции указателя» в семантически подобные события GdkEventButton и GdkEventMotion. В зависимости от того, находится ли GDK_TOUCH_MASK в маске событий или нет, последовательности, не эмулирующие указатель, всё равно могут запускать жесты или просто отфильтровываться, независимо от того, обрабатывает ли это виджет напрямую.
Если виджет устанавливает GDK_TOUCH_MASK в своей маске событий и не обрабатывает сигнал «touch-event», будут получены только события касания, и эмуляция указателя не будет выполняться.
Захваты
Захваты — это метод захвата всех событий ввода с устройства, они происходят либо неявно на устройствах указателя и касания, либо явно. Неявные захваты происходят при взаимодействии пользователя, когда происходит событие GdkEventButtonPress, все события, начиная с этого момента, и до соответствующего события GdkEventButtonRelease, будут переданы виджету, который получил первое событие. Аналогично, при событиях касания каждая GdkEventSequence будет передавать только события виджету, который получил событие GDK_TOUCH_BEGIN.
Явные захваты происходят программно (как активация, так и деактивация) и могут быть глобальными (захваты GDK) или прикладными (захваты GTK). На платформах системных окон, которые их поддерживают, захваты GDK будут препятствовать любому взаимодействию с любым другим приложением/окном/виджетом, кроме захватывающего, в то время как захваты GTK будут эффективны только внутри приложения (через все его окна), всё ещё позволяя взаимодействовать с другими приложениями.
Однако важный аспект захватов заключается в том, что они могут потенциально произойти в любой момент где-то ещё, даже когда указатель/устройство касания уже захвачено. Это делает необходимым для виджетов обрабатывать отмену любого текущего взаимодействия. В зависимости от того, является ли причиной этого захват GTK или GDK, виджет соответственно получит сигнал «grab-notify» или событие GdkEventGrabBroken.
В случае жестов эти сигналы обрабатываются автоматически, что приводит к отмене всех отслеживаемых событий указателя/касания и сигнализации о завершении распознавания.
Ввод с клавиатуры
Контроллеры событий и жесты
Контроллеры событий — это автономные объекты, которые могут выполнять определённые действия при получении событий GdkEvents. Они привязаны к GtkWidget и могут быть проинформированы о фазе распространения события, в которой они будут управлять событиями.
Жесты — это набор специфичных контроллеров, предназначенных для обработки событий указателя и/или касания. Каждая реализация жеста пытается распознать определённые действия из полученных событий, информируя о состоянии/прогрессе соответствующим образом, чтобы виджет мог отреагировать на них. В случае мультитач-жестов каждая последовательность взаимодействующих касаний будет отслеживаться независимо.
Поскольку жесты являются «простыми» единицами, их часто объединяют для выполнения действий более высокого уровня. Групповые жесты обрабатывают одни и те же последовательности событий одновременно, а эти последовательности разделяют одно и то же состояние во всех объединённых жестах. Примеры группировки могут быть:
| Жесты «перетаскивание» и «проведение» могут потребовать группировки. Первый будет сообщать о событиях, пока происходит перетаскивание, второй сообщит скорости свайпа по осям X/Y только после завершения жеста. |
| Группировка жеста «перетаскивание» с жестом «панорамирование» позволит перетаскивать только в направлении панорамирования, поскольку оба жеста разделяют состояние. |
| Если одновременно нужны жесты «нажатие» и «длинное нажатие», они должны быть сгруппированы. |
Состояния жестов
Жесты имеют понятие «состояния» для каждой отдельной последовательности касания. При первом получении событий от последовательности касаний состояние последовательности будет «нет», это означает, что последовательность касания обрабатывается жестом для возможного запуска действий, но распространение события не останавливается.
Когда жест входит в распознавание или в более поздний момент, виджет может выбрать захват последовательностей касаний (по отдельности или в группе), тем самым останавливая распространение события после того, как событие пройдёт через каждый жест в этом виджете и фазе распространения. В любой момент, когда это происходит, последовательности касаний отменяются вниз по цепочке распространения, чтобы сообщить об этом, что дальнейшие события не будут отправлены.
В качестве альтернативы или в более поздний момент виджет может выбрать отказ от последовательностей касаний, позволяя им пройти через процесс распространения событий ещё раз. Когда это происходит на стадии захвата, и если нет других захватывающих жестов в виджете, будет эмулировано и распространено событие GDK_TOUCH_BEGIN/GDK_BUTTON_PRESS, чтобы сохранить согласованность.
Групповые жесты всегда разделяют одно и то же состояние для данной последовательности касаний, поэтому установка состояния на одном из них передаёт состояние другим. Они также взаимно исключают друг друга, в пределах виджета может быть только одна группа жестов, захватывающая данную последовательность. Если другая группа жестов позднее захватит эту же последовательность, первая группа откажется от неё.
© 2005–2020 The GNOME Project
Licensed under the GNU Lesser General Public License version 2.1 or later.
https://developer.gnome.org/gtk3/3.20/chap-input-handling.html