Как переопределить установленные сопоставления поведения?

В java-9 Skins стали общедоступными, а Behaviors остались в неведении, тем не менее, они значительно изменились, теперь для всех входных привязок используется InputMap.

CellBehaviorBase устанавливает привязки мыши, например:

InputMap.MouseMapping pressedMapping, releasedMapping;
addDefaultMapping(
    pressedMapping = new InputMap.MouseMapping(MouseEvent.MOUSE_PRESSED, this::mousePressed),
    releasedMapping = new InputMap.MouseMapping(MouseEvent.MOUSE_RELEASED, this::mouseReleased),
    new InputMap.MouseMapping(MouseEvent.MOUSE_DRAGGED, this::mouseDragged)
);

Конкретный XXSkin теперь устанавливает поведение в частном порядке:

final private BehaviorBase behavior; 
public TableCellSkin(TableCell control) {
    super(control);
    behavior = new TableCellBehavior(control);
    .... 
}

Требуется заменить поведение mousePressed (в контексте jdk9). Идея состоит в том, чтобы рефлексивно захватить поле super, удалить все его сопоставления и установить пользовательское поведение. По какой-то непонятной мне причине старые привязки по-прежнему активны (хотя старые привязки пусты!) и вызываются перед новыми привязками.

Ниже приведен работающий пример, с которым можно поиграться: сопоставление с mousePressed просто реализовано так, чтобы ничего не делать, в частности, чтобы не вызывать super. Чтобы увидеть старые привязки в действии, я установил условную точку останова отладки в CellBehaviorBase.mousePressed, например (в Eclipse):

System.out.println("mousePressed super");
new RuntimeException("whoIsCalling: " + getNode().getClass()).printStackTrace();
return false;

Запустите отладку и щелкните любую ячейку, после чего вы получите:

mousePressed super
java.lang.RuntimeException: whoIsCalling: class de.swingempire.fx.scene.control.cell.TableCellBehaviorReplace$PlainCustomTableCell
    at com.sun.javafx.scene.control.behavior.CellBehaviorBase.mousePressed(CellBehaviorBase.java:169)
    at com.sun.javafx.scene.control.inputmap.InputMap.handle(InputMap.java:274)
    at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
    at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)

//... lots more of event dispatching

// until finally the output in my custom cell behavior

Feb. 02, 2016 3:14:02 NACHM. de.swingempire.fx.scene.control.cell.TableCellBehaviorReplace$PlainCustomTableCellBehavior mousePressed
INFORMATION: short-circuit super: Bulgarisch

Я ожидал увидеть только самую последнюю часть, то есть распечатку моего пользовательского поведения. Такое ощущение, что я как-то принципиально не в себе, но не могу понять. Идеи?

Работающий код (извините за его длину, хотя большая часть шаблона):

public class TableCellBehaviorReplace extends Application {

    private final ObservableList<Locale> locales =
            FXCollections.observableArrayList(Locale.getAvailableLocales());

    private Parent getContent() {
        TableView<Locale> table = createLocaleTable();
        BorderPane content = new BorderPane(table);
        return content;
    }

    private TableView<Locale> createLocaleTable() {
        TableView<Locale> table = new TableView<>(locales);

        TableColumn<Locale, String> name = new TableColumn<>("Name");
        name.setCellValueFactory(new PropertyValueFactory<>("displayName"));
        name.setCellFactory(p -> new PlainCustomTableCell<>());

        TableColumn<Locale, String> lang = new TableColumn<>("Language");
        lang.setCellValueFactory(new PropertyValueFactory<>("displayLanguage"));
        lang.setCellFactory(p -> new PlainCustomTableCell<>());

        table.getColumns().addAll(name, lang);
        return table;
    }

    /**
     * Custom skin that installs custom Behavior. Note: this is dirty!
     * Access super's behavior, dispose to get rid off its handlers, install
     * custom behavior.
     */
    public static class PlainCustomTableCellSkin<S, T> extends TableCellSkin<S, T> {

        private BehaviorBase<?> replacedBehavior;
        public PlainCustomTableCellSkin(TableCell<S, T> control) {
            super(control);
            replaceBehavior();
        }

        private void replaceBehavior() {
            BehaviorBase<?> old = (BehaviorBase<?>) invokeGetField(TableCellSkin.class, this, "behavior");
            old.dispose();
            // at this point, InputMap mappings are empty:
            // System.out.println("old mappings: " + old.getInputMap().getMappings().size());
            replacedBehavior = new PlainCustomTableCellBehavior<>(getSkinnable());
        }

        @Override
        public void dispose() {
            replacedBehavior.dispose();
            super.dispose();
        }

    }

    /**
     * Custom behavior that's meant to override basic handlers. Here: short-circuit
     * mousePressed.
     */
    public static class PlainCustomTableCellBehavior<S, T> extends TableCellBehavior<S, T> {

        public PlainCustomTableCellBehavior(TableCell<S, T> control) {
            super(control);
        }

        @Override
        public void mousePressed(MouseEvent e) {
            if (true) {
                LOG.info("short-circuit super: " + getNode().getItem());
                return;
            }
            super.mousePressed(e);
        }

    }


    /**
     * C&P of default tableCell in TableColumn. Extended to install custom
     * skin.
     */
    public static class PlainCustomTableCell<S, T> extends TableCell<S, T> {

        public PlainCustomTableCell() {
        }

        @Override protected void updateItem(T item, boolean empty) {
            if (item == getItem()) return;

            super.updateItem(item, empty);

            if (item == null) {
                super.setText(null);
                super.setGraphic(null);
            } else if (item instanceof Node) {
                super.setText(null);
                super.setGraphic((Node)item);
            } else {
                super.setText(item.toString());
                super.setGraphic(null);
            }
        }

        @Override
        protected Skin<?> createDefaultSkin() {
            return new PlainCustomTableCellSkin<>(this);
        }

    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setScene(new Scene(getContent(), 400, 200));
        primaryStage.setTitle(FXUtils.version());
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    /**
     * Reflectively access super field.
     */
    public static Object invokeGetField(Class source, Object target, String name) {
        try {
            Field field = source.getDeclaredField(name);
            field.setAccessible(true);
            return field.get(target);
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(TableCellBehaviorReplace.class.getName());
}

Изменить

Предложение наследовать от абстрактного скина XXSkinBase вместо конкретного XXSkin (тогда вы можете установить любое поведение, которое хотите, чувак :-) очень разумно и должно быть первым вариантом. В конкретном случае, когда XX является TableCell, это в настоящее время невозможно, поскольку класс содержит абстрактные частные методы пакета. Кроме того, есть XX, которые не имеют абстрактной базы (например, ListCell).


person kleopatra    schedule 02.02.2016    source источник


Ответы (2)


Может быть ошибка в InputMap:

Покопавшись в исходниках, я обнаружил некоторую внутреннюю бухгалтерию (eventTypeMappings), параллельную сопоставлениям (это обработчики). InputMap прослушивает изменения в сопоставлениях и обновляет внутреннюю отчетность об изменениях.

mappings.addListener((ListChangeListener<Mapping<?>>) c -> {
    while (c.next()) {
        // TODO handle mapping removal
        if (c.wasRemoved()) {
            for (Mapping<?> mapping : c.getRemoved()) {
                removeMapping(mapping);
            }
        }

// removeMapping
private void removeMapping(Mapping<?> mapping) {
    // TODO
}

Это означает, что внутренняя структура никогда не очищается, особенно когда сопоставления удаляются в behavior.dispose(). При поиске обработчиков событий - по inputMap.handle(e) см. трассировку стека отладки, показанную в вопросе - старый обработчик найден во внутренней структуре бухгалтерского учета.

Радости ранних экспериментов... ;-)


В конце концов, (очень грязное, очень хакерское!) Решение состоит в том, чтобы взять на себя работу InputMap и принудительно очистить внутренности:

private void replaceBehavior() {
    BehaviorBase<?> old = (BehaviorBase<?>) invokeGetField(TableCellSkin.class, this, "behavior");
    old.dispose();
    cleanupInputMap(old.getInputMap());
    // at this point, InputMap mappings are empty:
    // System.out.println("old mappings: " + old.getInputMap().getMappings().size());
    replacedBehavior = new PlainCustomTableCellBehavior<>(getSkinnable());
}

/**
 * This is a hack around InputMap not cleaning up internals on removing mappings.
 * We remove MousePressed/MouseReleased/MouseDragged mappings from the internal map.
 * Beware: obviously this is dirty!
 * 
 * @param inputMap
 */
private void cleanupInputMap(InputMap<?> inputMap) {
    Map eventTypeMappings = (Map) invokeGetField(InputMap.class, inputMap, "eventTypeMappings");
    eventTypeMappings.remove(MouseEvent.MOUSE_PRESSED);
    eventTypeMappings.remove(MouseEvent.MOUSE_RELEASED);
    eventTypeMappings.remove(MouseEvent.MOUSE_DRAGGED);
}

Кстати: на всякий случай, если кому-то интересно, без чего, мой хак вокруг отсутствующего commitOnFocusLost при редактировании ячейки перестал работать в java-9.

person kleopatra    schedule 03.02.2016
comment
В вашем вопросе (имеющем дело с TableCell) первым классом в цепочке наследования (начиная с TableCellBehavior внизу), имеющим метод dispose(), является BehaviorBase. В контексте ListView и реализации «переопределения» для KeyEvent цепочка начинается с ListViewBehavior (также происходящего от BehaviorBase), у которого есть собственный метод dispose(), который вызывает super.dispose() (т.е. для BehaviorBase). Цель состоит в том, чтобы удалить и заменить сопоставления KeyEvent по умолчанию ..НО .. сохранить функцию InputMap, которая позволяет настраивать сопоставления ... [продолжение следует] - person jfr; 04.06.2020
comment
[продолжение предыдущего комментария] ... будет добавлено через «дочерние» InputMap, которые проверяются и обрабатываются методом handle() в InputMap при запуске события. Имея это в виду, должен ли я вызывать BehaviorBase.dispose() (вероятно, да) или ListViewBehavior.dispose() (вероятно, нет)? - person jfr; 04.06.2020
comment
хм... не совсем понял, что ты имеешь в виду ;) Кстати, эта ошибка уже давно исправлена, акробатика больше не нужна - хотя замена поведения по-прежнему не поддерживается (закрытое конечное поле и внутренний API). Для пользовательских сопоставлений просто возьмите поведение (грязный отражающий доступ) и замените сопоставления по мере необходимости. Что касается удаления: это метод экземпляра, полиморфизм позаботится о том, какой метод является правильным :) Если вы реализуете собственное поведение для пользовательского скина и вам нужно что-то очистить - не забудьте вызвать super в dispose. - person kleopatra; 04.06.2020
comment
спасибо за ваш ответ, что я имел в виду: BehaviorBase.dispose() удаляет только сопоставления, ListViewBehavior делает то же самое и немного больше. Меня беспокоит то, что «больше» имеет последствия, которые (по крайней мере, для меня) неясны. Если побочный ущерб от ListViewBehavior.dispose() безвреден, то это предпочтительнее; в противном случае код из BehaviorBase.dispose() необходимо скопировать и вызвать рефлексивно… [продолжение следует] - person jfr; 04.06.2020
comment
[продолжение] … (в обход ListViewBehavior.dispose() добраться до BehaviorBase.dispose() невозможно). Под «акробатикой» я предполагаю, что вы имеете в виду внутреннюю очистку, описанную в вашем первоначальном ответе, пожалуйста, подтвердите. Наконец, есть ли какие-либо планы обнародовать поведение, и если да, то каковы временные рамки? Рефлексия в лучшем случае утомительна (очень) и… [продолжение следует] - person jfr; 04.06.2020
comment
[продолжение] … всегда грязно. Ссылки на «дочернюю карту ввода» в InputMap, кажется, учитывают эту возможность, в противном случае они не имеют смысла (если, конечно, вы не вернетесь к рефлексии). - person jfr; 04.06.2020
comment
до сих пор не понимаю - а комментарии - не лучшая среда для обсуждения кода ;) Понятия не имею, вероятно, не в обозримом будущем (есть проблемы с дизайном, которые так и не были решены, и, на самом деле, нет никого вокруг, кто мог бы это сделать - может быть готов дать это попытка, если бы у меня был спонсор .. подсказка, подсказка ‹g›) но тогда, это полностью открытый исходный код, если это вас действительно беспокоит, собственный образ с пользовательскими улучшениями относительно легко создать. - person kleopatra; 04.06.2020

Попробуйте в PlainCustomTableCellSkin наследовать от абстрактного класса TableCellSkinBase, а не от TableCellSkin. Затем вы можете вызвать суперконструктор, который принимает объект TableCellBehaviorBase в качестве дополнительного параметра. Затем вы можете сэкономить свое время, заменив его, инициализировав его непосредственно правильным.

Просто для большей ясности: TableCellSkin расширяет TableCellSkinBase TableCellBehavior расширяет TableCellBehaviorBase

Еще кое-что. Вам также нужно вызвать super.init(tableCell) в вашем конструкторе. Возьмите класс TableCellSkin в качестве эталона.

person B. Schüss    schedule 02.02.2016
comment
спасибо за ваш вклад - но это jdk9, скины перемещены в публичный доступ, API изменен :-) - person kleopatra; 03.02.2016
comment
k все еще застрял в 8 :) - person B. Schüss; 03.02.2016