1

在 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 没有抽象基础(如 fi ListCell)。

4

2 回答 2

1

可能是 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()。查找 eventHandlers 时 - by inputMap.handle(e),请参阅问题中显示的调试堆栈跟踪 - 旧处理程序位于内部簿记结构中。

早期实验的乐趣...... ;-)


最后,一个(非常肮脏,非常hacky!)的解决方案是接管 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);
}

顺便说一句:以防万一有人想知道 wtf - 没有,我在编辑单元格时对丢失的 commitOnFocusLost 的破解在 java-9 中停止工作。

于 2016-02-03T13:31:45.760 回答
0

尝试在 PlainCustomTableCellSkin 中继承抽象类 TableCellSkinBase 而不是 TableCellSkin。然后您可以调用超级构造函数,该构造函数将 TableCellBehaviorBase 对象作为附加参数。然后,您可以通过直接用正确的初始化它来节省替换它的时间。

只是为了更清楚: TableCellSkin extends TableCellSkinBase TableCellBehavior extends TableCellBehaviorBase

还有一件事。您还需要在构造函数中调用 super.init(tableCell)。以 TableCellSkin 类为参考。

于 2016-02-02T22:05:53.013 回答