Ответ 1
Проблема
Прежде чем предлагать решение, я думаю, что важно или, по крайней мере, интересно понять, почему наличие TextFormatter
, кажется, меняет поведение Dialog
. Если это не имеет значения для вас, смело переходите к концу ответа.
Отмена кнопок
Согласно документации Button
, кнопка отмены:
кнопка, которая получает клавиатуру VK_ESC, нажмите, если никакой другой узел в сцене ее не использует.
Конец этого предложения является важной частью. Кнопки отмены, а также кнопки по умолчанию реализуются путем регистрации ускорителя в Scene
, к которому принадлежит Button
. Эти ускорители вызываются только в том случае, если соответствующий KeyEvent
всплывает до Scene
. Если событие используется до того, как оно достигнет Scene
, ускоритель не вызывается.
Примечание. Чтобы больше узнать об обработке событий в JavaFX, особенно о таких терминах, как "пузыри" и "потребляются", я предлагаю прочитать это руководство.
Диалоги
Dialog
имеет определенные правила относительно того, как и когда он может быть закрыт. Эти правила описаны здесь, в разделе "Правила закрытия диалога". Достаточно сказать, что в основном все зависит от того, какой ButtonType
был добавлен в DialogPane
. В вашем примере вы используете один из предопределенных типов: ButtonType.CANCEL
. Если вы посмотрите документацию этого поля, вы увидите:
A pre-defined
ButtonType
that displays "Cancel" и has aButtonBar.ButtonData
ofButtonBar.ButtonData.CANCEL_CLOSE
.
И если вы посмотрите документацию ButtonData.CANCEL_CLOSE
, вы увидите:
Тег для кнопки "отмена" или "закрыть".
Is cancel button: True
По крайней мере для реализации по умолчанию это означает, что Button
, созданный для указанного ButtonType.CANCEL
, будет кнопкой отмены. Другими словами, Button
будет иметь свойство cancelButton
, установленное на true
. Это то, что позволяет закрыть Dialog
нажатием клавиши Esc.
Примечание. Это метод DialogPane#createButton(ButtonType)
, который отвечает за создание соответствующей кнопки (и может быть переопределен для настройки). Несмотря на то, что тип возвращаемого значения этого метода - Node
, как документировано, обычно возвращается экземпляр Button
.
TextFormatter
Каждый элемент управления в (основном) JavaFX имеет три компонента: класс элемента управления, класс скина и класс поведения. Последний класс отвечает за обработку пользовательского ввода, такого как события мыши и клавиши. В этом случае мы заботимся о TextInputControlBehavior
и TextFieldBehavior
; первый является суперклассом второго.
Примечание: В отличие от классов скинов, которые стали общедоступными API в JavaFX 9, классы поведения по-прежнему являются частными API с JavaFX 12.0.2. Многое из того, что описано ниже, является подробностями реализации.
Класс TextInputControlBehavior
регистрирует EventHandler
, который реагирует на нажатие клавиши Esc, вызывая метод cancelEdit(KeyEvent)
того же класса. Вся базовая реализация этого метода - пересылка KeyEvent
родительскому элементу TextInputControl
, если он есть, - что приводит к двум циклам отправки событий по неизвестной (для меня) причине. Однако класс TextFieldBehavior
переопределяет этот метод:
@Override protected void cancelEdit(KeyEvent event) { TextField textField = getNode(); if (textField.getTextFormatter() != null) { textField.cancelEdit(); event.consume(); } else { super.cancelEdit(event); } }
Как видите, присутствие TextFormatter
приводит к тому, что KeyEvent
безоговорочно потребляется. Это останавливает событие от достижения Scene
, кнопка отмены не срабатывает, и, таким образом, Dialog
не закрывается, когда клавиша Esc нажата, когда TextField
имеет фокус. Когда нет TextFormatter
, вызывается супер реализация, которая, как указано выше, просто пересылает событие родителю.
Причина такого поведения намекается на вызов TextInputControl#cancelEdit()
. Этот метод имеет "родственный метод" в виде TextInputControl#commitValue()
. Если вы посмотрите документацию этих двух методов, вы увидите:
Если поле в данный момент редактируется, этот вызов установит в тексте последнее зафиксированное значение.
И:
Зафиксируйте текущий текст и преобразуйте его в значение.
Соответственно. К сожалению, это мало что объясняет, но если вы посмотрите на реализацию, их цель станет ясна. TextFormatter
имеет свойство value
, которое не обновляется в режиме реального времени при вводе в TextField
. Вместо этого значение обновляется только тогда, когда оно зафиксировано (например, нажатием Enter). Обратное также верно; текущий текст можно вернуть к текущему значению, отменив редактирование (например, нажав Esc).
Примечание. Преобразование между String
и объектом произвольного типа обрабатывается StringConverter
, связанным с TextFormatter
.
Когда есть TextFormatter
, акт отмены редактирования считается сценарием с большим количеством событий. Это имеет смысл, я полагаю. Тем не менее, даже когда нет ничего, чтобы отменить событие, все еще потребляется - это не имеет особого смысла для меня.
Решение
Один из способов исправить это - копаться во внутренних органах, используя рефлексию, как показано в ответе Клеопатры. Другой вариант - добавить фильтр событий к TextField
или некоторому предку TextField
, который закрывает Dialog
при нажатии клавиши Esc.
textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ESCAPE) {
event.consume();
dialog.close();
}
});
Если вы хотите включить режим отмены редактирования (отмена без закрытия), вам следует закрывать Dialog
только в том случае, если нет редактирования для отмены. Взгляните на ответ клеопатры, чтобы увидеть, как можно определить, нужна ли отмена. Если есть что отменить, просто не используйте событие и не закрывайте Dialog
. Если отменить что-либо не нужно, просто сделайте то же, что и в коде выше (т.е. используйте и закройте).
Является ли использование фильтра событий "рекомендуемым способом"? Это конечно верный способ. JavaFX является управляемым событиями, как и большинство, если не все, основные наборы инструментов пользовательского интерфейса. В частности, для JavaFX это означает реагирование на Event
или наблюдение за Observable[Value]
на предмет недействительности/изменений. Фреймворк, построенный "поверх" JavaFX, может добавлять свои собственные механизмы. Поскольку проблема - это событие, которое потребляется, когда мы не хотим, чтобы оно было, допустимо добавить свои собственные обработчики для реализации желаемого поведения.