如何覆盖已安装的行为映射?
How to override installed mappings of Behavior?
在 java-9 中,皮肤进入了 public 范围,而行为则留在黑暗中 - 尽管如此发生了很大变化,现在所有输入绑定都使用 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 只是实现什么都不做,特别是 not 调用 super。为了查看旧绑定的工作情况,我在 CellBehaviorBase.mousePressed 处设置了一个条件调试断点,例如(在 Eclipse 中):
System.out.println("mousePressed super");
new RuntimeException("whoIsCalling: " + getNode().getClass()).printStackTrace();
return false;
运行一个debug点击任意单元格,则输出为:
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 的特定情况下,即 currently not possible,因为基础 class 包含抽象包私有方法。此外,还有一些没有抽象基础的 XX(如 f.i.ListCell)。
尝试在 PlainCustomTableCellSkin 中继承抽象 class TableCellSkinBase 而不是 TableCellSkin。
然后你可以调用超级构造函数,它将 TableCellBehaviorBase 对象作为附加参数。
然后你可以通过直接用正确的初始化它来节省你替换它的时间。
澄清一下:
TableCellSkin 扩展了 TableCellSkinBase
TableCellBehavior 扩展了 TableCellBehaviorBase
还有一件事。您还需要在构造函数中调用 super.init(tableCell) 。
以TableCellSkinclass为参考
可能是 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)
,请参阅问题中显示的调试堆栈跟踪 - 在内部簿记结构中找到旧处理程序。
早期实验的乐趣……;-)
最后,一个(非常肮脏,非常 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 中停止工作。
在 java-9 中,皮肤进入了 public 范围,而行为则留在黑暗中 - 尽管如此发生了很大变化,现在所有输入绑定都使用 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 只是实现什么都不做,特别是 not 调用 super。为了查看旧绑定的工作情况,我在 CellBehaviorBase.mousePressed 处设置了一个条件调试断点,例如(在 Eclipse 中):
System.out.println("mousePressed super");
new RuntimeException("whoIsCalling: " + getNode().getClass()).printStackTrace();
return false;
运行一个debug点击任意单元格,则输出为:
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 的特定情况下,即 currently not possible,因为基础 class 包含抽象包私有方法。此外,还有一些没有抽象基础的 XX(如 f.i.ListCell)。
尝试在 PlainCustomTableCellSkin 中继承抽象 class TableCellSkinBase 而不是 TableCellSkin。 然后你可以调用超级构造函数,它将 TableCellBehaviorBase 对象作为附加参数。 然后你可以通过直接用正确的初始化它来节省你替换它的时间。
澄清一下: TableCellSkin 扩展了 TableCellSkinBase TableCellBehavior 扩展了 TableCellBehaviorBase
还有一件事。您还需要在构造函数中调用 super.init(tableCell) 。 以TableCellSkinclass为参考
可能是 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)
,请参阅问题中显示的调试堆栈跟踪 - 在内部簿记结构中找到旧处理程序。
早期实验的乐趣……;-)
最后,一个(非常肮脏,非常 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 中停止工作。