How to override installed mappings of Behavior?

孤街浪徒 提交于 2019-12-24 11:56:31

问题


In java-9 Skins made it into public scope, while Behaviors are left in the dark - nevertheless changed considerably, in now using InputMap for all input bindings.

CellBehaviorBase installs mouse bindings like:

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)
);

A concrete XXSkin now installs the behavior privately:

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

The requirement is replace the mousePressed behavior (in jdk9 context). The idea is to grab super's field reflectively, dispose all its mappings and install the custom behavior. For some reason that I don't understand, the old bindings are still active (though the old mappings are empty!) and are invoked before the new bindings.

Below is a runnable example to play with: the mapping to mousePressed is simply implemented to do nothing, particularly to not invoke super. To see the old bindings at work, I set a conditional debug breakpoint at CellBehaviorBase.mousePressed like (in Eclipse):

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

Run a debug and click into any cell, then the output is:

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

I would expect to only see the very last part, that is the printout by my custom behavior. It feels like I'm somehow fundamentally off - but can't nail it. Ideas?

The runnable code (sorry for its length, most is boiler-plate, though):

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());
}

Edit

The suggestion inherit from the abstract skin XXSkinBase instead of the concrete XXSkin (then you are free to install whatever behavior you want, dude :-) is very reasonable and should be the first option. In the particular case of XX being TableCell, that's currently not possible, as the base class contains abstract package-private methods. Also, there are XX that don't have an abstract base (like f.i. ListCell).


回答1:


Might be a bug in InputMap:

Digging into the sources I found some internal book-keeping (eventTypeMappings) parallel to mappings (these are the handlers). InputMap is listening to changes in mappings and updates the internal book-keeping on changes

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
}

Meaning that the internal structure is never cleaned, particularly not when the mappings are removed in behavior.dispose(). When looking up eventHandlers - by inputMap.handle(e), see debug stacktrace shown in the question - the old handler is found in the internal book-keeping structure.

Joys of early experiments ... ;-)


At the end, a (very dirty, very hacky!) solution is to take over InputMap's job and force a cleanup of the internals:

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);
}

BTW: just in case anybody is wondering wtf - without, my hack around the missing commitOnFocusLost when editing a cell stopped working in java-9.




回答2:


Try in PlainCustomTableCellSkin to inherit from the abstract class TableCellSkinBase rather than from TableCellSkin. Then you can call the super constructor, which takes an TableCellBehaviorBase object as additional param. Then you can save your time replacing it, by initializing it directly with the right one.

Just for more claryfication: TableCellSkin extends TableCellSkinBase TableCellBehavior extends TableCellBehaviorBase

One more thing. You need to also call super.init(tableCell) in your constructor. Take the TableCellSkin class as reference.



来源:https://stackoverflow.com/questions/35156039/how-to-override-installed-mappings-of-behavior

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!