diff --git a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/ResizingBehaviourApp.java b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/ResizingBehaviourApp.java index f3f2bc7e..7eb5f54a 100644 --- a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/ResizingBehaviourApp.java +++ b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/ResizingBehaviourApp.java @@ -3,20 +3,30 @@ import com.dlsc.gemsfx.util.ResizingBehaviour; import fr.brouillard.oss.cssfx.CSSFX; import javafx.application.Application; +import javafx.beans.InvalidationListener; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Group; import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.Label; +import javafx.scene.control.Spinner; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.stage.Stage; +import javafx.util.StringConverter; +import org.controlsfx.control.CheckComboBox; public class ResizingBehaviourApp extends Application { + private ResizingBehaviour resizingSupport; + @Override public void start(Stage stage) { Label content = new Label("Content"); - content.setMouseTransparent(false); content.setStyle("-fx-background-color: orange;"); content.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); content.setAlignment(Pos.CENTER); @@ -27,14 +37,37 @@ public void start(Stage stage) { stackPane.setPrefSize(250, 250); stackPane.setMaxSize(950, 850); - ResizingBehaviour resizingSupport = ResizingBehaviour.install(stackPane); + resizingSupport = ResizingBehaviour.install(stackPane); resizingSupport.setResizable(true); resizingSupport.setOnResize((width, height) -> System.out.println("width: " + width + ", height: " + height)); + Label installLabel = new Label("ResizingBehaviour already installed."); + + CheckComboBox supportedOperationsBox = createOperationCheckComboBox(); + + CheckBox resizeableBox = new CheckBox("Resizable"); + resizeableBox.selectedProperty().bindBidirectional(resizingSupport.resizableProperty()); + + Spinner offsetSpinner = new Spinner<>(5, 20, 5, 5); + resizingSupport.offsetProperty().bind(offsetSpinner.valueProperty()); + + Button uninstallButton = new Button("Uninstall"); + uninstallButton.setOnAction(e -> { + if (resizingSupport.isInstalled()) { + resizingSupport.uninstall(); + installLabel.setText("Tips: ResizingBehaviour already uninstalled."); + } + }); + + HBox controlsBox = new HBox(10, uninstallButton, supportedOperationsBox, resizeableBox, new Label("Edge Offset:"), offsetSpinner); + controlsBox.setAlignment(Pos.CENTER_LEFT); + VBox controlsContainer = new VBox(10, controlsBox, installLabel); + controlsContainer.setAlignment(Pos.CENTER_LEFT); + controlsContainer.setPadding(new Insets(10)); Group group = new Group(stackPane); - StackPane container = new StackPane(group); - container.setAlignment(Pos.CENTER); + BorderPane container = new BorderPane(group); + container.setBottom(controlsContainer); Scene scene = new Scene(container); CSSFX.start(scene); @@ -47,6 +80,43 @@ public void start(Stage stage) { stage.show(); } + private CheckComboBox createOperationCheckComboBox() { + CheckComboBox supportedOperationsBox = new CheckComboBox<>(); + supportedOperationsBox.getItems().addAll(ResizingBehaviour.Operation.values()); + supportedOperationsBox.setConverter(createOperationStringConverter()); + supportedOperationsBox.getCheckModel().checkAll(); + supportedOperationsBox.getCheckModel().getCheckedItems().addListener((InvalidationListener) (c) -> { + resizingSupport.setSupportedOperations(supportedOperationsBox.getCheckModel().getCheckedItems()); + }); + return supportedOperationsBox; + } + + private StringConverter createOperationStringConverter() { + return new StringConverter<>() { + @Override + public String toString(ResizingBehaviour.Operation operation) { + if (operation == null) { + return ""; + } + return switch (operation) { + case RESIZE_E -> "Right"; + case RESIZE_W -> "Left"; + case RESIZE_N -> "Top"; + case RESIZE_NE -> "Top Right"; + case RESIZE_NW -> "Top Left"; + case RESIZE_S -> "Bottom"; + case RESIZE_SE -> "Bottom Right"; + case RESIZE_SW -> "Bottom Left"; + }; + } + + @Override + public ResizingBehaviour.Operation fromString(String string) { + return null; + } + }; + } + public static void main(String[] args) { launch(); } diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/util/ResizingBehaviour.java b/gemsfx/src/main/java/com/dlsc/gemsfx/util/ResizingBehaviour.java index 68a8cd2a..1ec96b0e 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/util/ResizingBehaviour.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/util/ResizingBehaviour.java @@ -1,27 +1,46 @@ package com.dlsc.gemsfx.util; -import javafx.beans.property.*; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.event.EventHandler; import javafx.scene.Cursor; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Region; +import java.util.Objects; import java.util.function.BiConsumer; /** - * A stack pane that can be resized interactively by the user. Resize operations (mouse pressed and - * dragged) modify the preferred width and height of the pane and also the layout x and y coordinates. + * This class implements interactive resizing behavior for a {@link Region}. It allows + * the user to resize the region by pressed and dragging the region's edges. The resizing + * behavior can be attached to any Region, modifying its preferred width and height as well + * as its layout coordinates based on user interactions. */ public class ResizingBehaviour { + private static final String RESIZE_BEHAVIOUR_INSTALLED = "resizeBehaviourInstalled"; + + private final EventHandler mouseMovedHandler; + private final EventHandler mousePressedHandler; + private final EventHandler mouseReleasedHandler; + private final EventHandler mouseDraggedHandler; + + private final Region region; + private double startX; private double startY; /* * List of possible resizing operations. */ - private enum Operation { - NONE, + public enum Operation { RESIZE_N, RESIZE_S, RESIZE_W, @@ -32,7 +51,7 @@ private enum Operation { RESIZE_SE } - private Operation operation = Operation.NONE; + private Operation operation; /** * Installs the resizing behaviour on the given region. Once installed @@ -43,254 +62,276 @@ private enum Operation { * @return the installed behaviour */ public static ResizingBehaviour install(Region region) { - return new ResizingBehaviour(region); + Objects.requireNonNull(region, "Region cannot be null."); + + if (isInstalled(region)) { + throw new IllegalStateException("ResizingBehaviour is already installed on this region."); + } + + ResizingBehaviour behaviour = new ResizingBehaviour(region); + region.getProperties().put(RESIZE_BEHAVIOUR_INSTALLED, Boolean.TRUE); + return behaviour; } /** * Constructs a new pane with the given children. */ private ResizingBehaviour(Region region) { - EventHandler mouseMovedHandler = evt -> { - if (!isResizable()) { - return; - } + this.region = region; - double x = evt.getX(); - double y = evt.getY(); - - final double offset = getOffset(); - - if (x < offset) { - if (y < offset) { - region.setCursor(Cursor.NW_RESIZE); - } else if (y > region.getHeight() - offset) { - region.setCursor(Cursor.SW_RESIZE); - } else { - region.setCursor(Cursor.W_RESIZE); - } - } else if (x > region.getWidth() - offset) { - if (y < offset) { - region.setCursor(Cursor.NE_RESIZE); - } else if (y > region.getHeight() - offset) { - region.setCursor(Cursor.SE_RESIZE); - } else { - region.setCursor(Cursor.E_RESIZE); - } - } else if (y < offset) { - region.setCursor(Cursor.N_RESIZE); - } else if (y > region.getHeight() - offset) { - region.setCursor(Cursor.S_RESIZE); - } else { - region.setCursor(Cursor.DEFAULT); - } - }; + mouseMovedHandler = this::onMouseMove; + mousePressedHandler = this::onMousePressed; + mouseReleasedHandler = evt -> operation = null; + mouseDraggedHandler = this::onMouseDragged; region.addEventFilter(MouseEvent.MOUSE_MOVED, mouseMovedHandler); region.addEventFilter(MouseEvent.MOUSE_ENTERED, mouseMovedHandler); region.addEventFilter(MouseEvent.MOUSE_ENTERED_TARGET, mouseMovedHandler); - EventHandler mousePressedHandler = evt -> { - if (!isResizable()) { - return; - } + region.addEventFilter(MouseEvent.MOUSE_PRESSED, mousePressedHandler); + region.addEventFilter(MouseEvent.MOUSE_RELEASED, mouseReleasedHandler); + region.addEventFilter(MouseEvent.MOUSE_DRAGGED, mouseDraggedHandler); + } + + private void onMouseDragged(MouseEvent evt) { + if (operation == null) { + return; + } + + double x = evt.getScreenX(); + double y = evt.getScreenY(); + + double deltaX = (evt.getScreenX() - startX) * 4; + double deltaY = (evt.getScreenY() - startY) * 4; + + double width = region.getWidth(); + double height = region.getHeight(); + + double minHeight = region.minHeight(width); + double maxHeight = region.maxHeight(width); - startX = evt.getScreenX(); - startY = evt.getScreenY(); + double minWidth = region.minWidth(height); + double maxWidth = region.maxWidth(height); - double x = evt.getX(); - double y = evt.getY(); + double newHeight; + double newWidth; - final double offset = getOffset(); + switch (operation) { + case RESIZE_N: + newHeight = height - deltaY; + if (newHeight >= minHeight && newHeight <= maxHeight) { + region.setLayoutY(y); + region.setPrefHeight(Math.min(maxHeight, Math.max(minHeight, newHeight))); + startX = x; + startY = y; + } + evt.consume(); + break; + case RESIZE_S: + newHeight = height + deltaY; + if (newHeight >= minHeight && newHeight <= maxHeight) { + region.setPrefHeight(Math.min(maxHeight, Math.max(minHeight, newHeight))); + startX = x; + startY = y; + } + evt.consume(); + break; + case RESIZE_W: + newWidth = width - deltaX; + if (newWidth >= minWidth && newWidth <= maxWidth) { + region.setLayoutX(x); + region.setPrefWidth(Math.min(maxWidth, Math.max(minWidth, newWidth))); + startX = x; + startY = y; + } + evt.consume(); + break; + case RESIZE_E: + newWidth = width + deltaX; + if (newWidth >= minWidth && newWidth <= maxWidth) { + region.setPrefWidth(Math.min(maxWidth, Math.max(minWidth, newWidth))); + startX = x; + startY = y; + } + evt.consume(); + break; + case RESIZE_NW: + newWidth = width - deltaX; + if (newWidth >= minWidth && newWidth <= maxWidth) { + region.setLayoutX(x); + region.setPrefWidth(Math.min(maxWidth, Math.max(minWidth, newWidth))); + startX = x; + } + newHeight = height - deltaY; + if (newHeight >= minHeight && newHeight <= maxHeight) { + region.setLayoutY(y); + region.setPrefHeight(Math.min(maxHeight, Math.max(minHeight, newHeight))); + startY = y; + } + evt.consume(); + break; + case RESIZE_NE: + newWidth = width + deltaX; + if (newWidth >= minWidth && newWidth <= maxWidth) { + region.setPrefWidth(Math.min(maxWidth, Math.max(minWidth, newWidth))); + startX = x; + } - if (x < offset) { - if (y < offset) { - operation = Operation.RESIZE_NW; - } else if (y > region.getHeight() - offset) { - operation = Operation.RESIZE_SW; - } else { - operation = Operation.RESIZE_W; + newHeight = height - deltaY; + if (newHeight >= minHeight && newHeight <= maxHeight) { + region.setLayoutY(y); + region.setPrefHeight(Math.min(maxHeight, Math.max(minHeight, height - deltaY))); + startY = y; + } + evt.consume(); + break; + case RESIZE_SW: + newWidth = width - deltaX; + if (newWidth >= minWidth && newWidth <= maxWidth) { + region.setLayoutX(x); + region.setPrefWidth(Math.min(maxWidth, Math.max(minWidth, width - deltaX))); + startX = x; } - } else if (x > region.getWidth() - offset) { - if (y < offset) { - operation = Operation.RESIZE_NE; - } else if (y > region.getHeight() - offset) { - operation = Operation.RESIZE_SE; - } else { - operation = Operation.RESIZE_E; + + newHeight = height + deltaY; + if (newHeight >= minHeight && newHeight <= maxHeight) { + region.setPrefHeight(Math.min(newHeight, Math.max(minHeight, height + deltaY))); + startY = y; } - } else if (y < offset) { - operation = Operation.RESIZE_N; + evt.consume(); + break; + case RESIZE_SE: + newWidth = width + deltaX; + + if (newWidth >= minWidth && newWidth <= maxWidth) { + region.setLayoutX(x); + region.setPrefWidth(Math.min(maxWidth, Math.max(minWidth, newWidth))); + startX = x; + } + + newHeight = height + deltaY; + + if (newHeight >= minHeight && newHeight <= maxHeight) { + region.setPrefHeight(Math.max(minHeight, newHeight)); + startY = y; + } + evt.consume(); + break; + } + + BiConsumer onResize = getOnResize(); + if (onResize != null) { + onResize.accept(region.getWidth(), region.getHeight()); + } + } + + private void onMousePressed(MouseEvent evt) { + if (!isResizable()) { + return; + } + + startX = evt.getScreenX(); + startY = evt.getScreenY(); + + double x = evt.getX(); + double y = evt.getY(); + + final double offset = getOffset(); + + if (x < offset) { + if (y < offset) { + setOperationIfSupported(Operation.RESIZE_NW); } else if (y > region.getHeight() - offset) { - operation = Operation.RESIZE_S; + setOperationIfSupported(Operation.RESIZE_SW); } else { - operation = Operation.NONE; + setOperationIfSupported(Operation.RESIZE_W); } - }; - - region.addEventFilter(MouseEvent.MOUSE_PRESSED, mousePressedHandler); - region.addEventFilter(MouseEvent.MOUSE_RELEASED, evt -> operation = Operation.NONE); - region.addEventFilter(MouseEvent.MOUSE_DRAGGED, evt -> { - double x = evt.getScreenX(); - double y = evt.getScreenY(); - - double deltaX = (evt.getScreenX() - startX) * 4; - double deltaY = (evt.getScreenY() - startY) * 4; - - double width = region.getWidth(); - double height = region.getHeight(); - - double minHeight = region.minHeight(width); - double maxHeight = region.maxHeight(width); - - double minWidth = region.minWidth(height); - double maxWidth = region.maxWidth(height); - - double newHeight; - double newWidth; - - switch (operation) { - - case NONE: - break; - case RESIZE_N: - newHeight = height - deltaY; - if (newHeight >= minHeight && newHeight <= maxHeight) { - region.setLayoutY(y); - region.setPrefHeight(Math.min(maxHeight, Math.max(minHeight, newHeight))); - startX = x; - startY = y; - } - evt.consume(); - break; - case RESIZE_S: - newHeight = height + deltaY; - if (newHeight >= minHeight && newHeight <= maxHeight) { - region.setPrefHeight(Math.min(maxHeight, Math.max(minHeight, newHeight))); - startX = x; - startY = y; - } - evt.consume(); - break; - case RESIZE_W: - newWidth = width - deltaX; - if (newWidth >= minWidth && newWidth <= maxWidth) { - region.setLayoutX(x); - region.setPrefWidth(Math.min(maxWidth, Math.max(minWidth, newWidth))); - startX = x; - startY = y; - } - evt.consume(); - break; - case RESIZE_E: - newWidth = width + deltaX; - if (newWidth >= minWidth && newWidth <= maxWidth) { - region.setPrefWidth(Math.min(maxWidth, Math.max(minWidth, newWidth))); - startX = x; - startY = y; - } - evt.consume(); - break; - case RESIZE_NW: - newWidth = width - deltaX; - if (newWidth >= minWidth && newWidth <= maxWidth) { - region.setLayoutX(x); - region.setPrefWidth(Math.min(maxWidth, Math.max(minWidth, newWidth))); - startX = x; - } - newHeight = height - deltaY; - if (newHeight >= minHeight && newHeight <= maxHeight) { - region.setLayoutY(y); - region.setPrefHeight(Math.min(maxHeight, Math.max(minHeight, newHeight))); - startY = y; - } - evt.consume(); - break; - case RESIZE_NE: - newWidth = width + deltaX; - if (newWidth >= minWidth && newWidth <= maxWidth) { - region.setPrefWidth(Math.min(maxWidth, Math.max(minWidth, newWidth))); - startX = x; - } - - newHeight = height - deltaY; - if (newHeight >= minHeight && newHeight <= maxHeight) { - region.setLayoutY(y); - region.setPrefHeight(Math.min(maxHeight, Math.max(minHeight, height - deltaY))); - startY = y; - } - - evt.consume(); - break; - case RESIZE_SW: - newWidth = width - deltaX; - if (newWidth >= minWidth && newWidth <= maxWidth) { - region.setLayoutX(x); - region.setPrefWidth(Math.min(maxWidth, Math.max(minWidth, width - deltaX))); - startX = x; - } - - newHeight = height + deltaY; - if (newHeight >= minHeight && newHeight <= maxHeight) { - region.setPrefHeight(Math.min(newHeight, Math.max(minHeight, height + deltaY))); - startY = y; - } - - evt.consume(); - break; - case RESIZE_SE: - newWidth = width + deltaX; - - if (newWidth >= minWidth && newWidth <= maxWidth) { - region.setLayoutX(x); - region.setPrefWidth(Math.min(maxWidth, Math.max(minWidth, newWidth))); - startX = x; - } - - newHeight = height + deltaY; - - if (newHeight >= minHeight && newHeight <= maxHeight) { - region.setPrefHeight(Math.max(minHeight, newHeight)); - startY = y; - } - - evt.consume(); - break; + } else if (x > region.getWidth() - offset) { + if (y < offset) { + setOperationIfSupported(Operation.RESIZE_NE); + } else if (y > region.getHeight() - offset) { + setOperationIfSupported(Operation.RESIZE_SE); + } else { + setOperationIfSupported(Operation.RESIZE_E); } + } else if (y < offset) { + setOperationIfSupported(Operation.RESIZE_N); + } else if (y > region.getHeight() - offset) { + setOperationIfSupported(Operation.RESIZE_S); + } else { + operation = null; + } + } + + private void setOperationIfSupported(Operation resizeNw) { + operation = getSupportedOperations().contains(resizeNw) ? resizeNw : null; + } + + private void onMouseMove(MouseEvent evt) { + if (!isResizable()) { + return; + } - BiConsumer onResize = getOnResize(); - if (onResize != null) { - onResize.accept(region.getWidth(), region.getHeight()); + double x = evt.getX(); + double y = evt.getY(); + + final double offset = getOffset(); + + if (x < offset) { + if (y < offset) { + setCursorIfOperationSupported(Operation.RESIZE_NW, Cursor.NW_RESIZE); + } else if (y > region.getHeight() - offset) { + setCursorIfOperationSupported(Operation.RESIZE_SW, Cursor.SW_RESIZE); + } else { + setCursorIfOperationSupported(Operation.RESIZE_W, Cursor.W_RESIZE); } - }); + } else if (x > region.getWidth() - offset) { + if (y < offset) { + setCursorIfOperationSupported(Operation.RESIZE_NE, Cursor.NE_RESIZE); + } else if (y > region.getHeight() - offset) { + setCursorIfOperationSupported(Operation.RESIZE_SE, Cursor.SE_RESIZE); + } else { + setCursorIfOperationSupported(Operation.RESIZE_E, Cursor.E_RESIZE); + } + } else if (y < offset) { + setCursorIfOperationSupported(Operation.RESIZE_N, Cursor.N_RESIZE); + } else if (y > region.getHeight() - offset) { + setCursorIfOperationSupported(Operation.RESIZE_S, Cursor.S_RESIZE); + } else { + region.setCursor(Cursor.DEFAULT); + } } - // resize callback + private void setCursorIfOperationSupported(Operation operation, Cursor cursor) { + region.setCursor(getSupportedOperations().contains(operation) ? cursor : Cursor.DEFAULT); + } - private final ObjectProperty> onResize = new SimpleObjectProperty<>(this, "onResize"); + private ObjectProperty> onResize; public final BiConsumer getOnResize() { - return onResize.get(); + return onResize == null ? null : onResize.get(); } /** - * A callback used to inform interested parties when the width or height of the - * dialog was changed interactively by the user. + * A callback that will be invoked whenever the region is resized. * - * @see #resizableProperty() - * @return the callback / the consumer of the new width and height + * @return the onResize callback */ public final ObjectProperty> onResizeProperty() { + if (onResize == null) { + onResize = new SimpleObjectProperty<>(this, "onResize"); + } return onResize; } public final void setOnResize(BiConsumer onResize) { - this.onResize.set(onResize); + onResizeProperty().set(onResize); } - private final DoubleProperty offset = new SimpleDoubleProperty(this, "offset", 5); + private DoubleProperty offset; public final double getOffset() { - return offset.get(); + return offset == null ? 5 : offset.get(); } /** @@ -298,29 +339,96 @@ public final double getOffset() { * and drag to resize the pane. Default is 5. */ public final DoubleProperty offsetProperty() { + if (offset == null) { + offset = new SimpleDoubleProperty(this, "offset", 5); + } return offset; } public final void setOffset(double offset) { - this.offset.set(offset); + offsetProperty().set(offset); } - private final BooleanProperty resizable = new SimpleBooleanProperty(this, "resizable", true); + private BooleanProperty resizable; public final boolean isResizable() { - return resizable.get(); + return resizable == null || resizable.get(); } /** * Determines if the pane can currently be resized or not. + *

+ * Default is true. * * @return true if the pane is resizable */ public final BooleanProperty resizableProperty() { + if (resizable == null) { + resizable = new SimpleBooleanProperty(this, "resizable", true); + } return resizable; } public final void setResizable(boolean resizable) { - this.resizable.set(resizable); + resizableProperty().set(resizable); + } + + private SimpleListProperty supportedOperations; + + /** + * The list of supported operations for resizing the region. + *

+ * By default, all operations are supported. + * + * @return the list of supported operations + */ + public final SimpleListProperty supportedOperationsProperty() { + if (supportedOperations == null) { + supportedOperations = new SimpleListProperty<>(this, "supportedOperations", FXCollections.observableArrayList(Operation.values())); + } + return supportedOperations; } + + public final ObservableList getSupportedOperations() { + return supportedOperations == null ? FXCollections.observableArrayList(Operation.values()) : supportedOperations.get(); + } + + public final void setSupportedOperations(ObservableList supportedOperations) { + supportedOperationsProperty().set(supportedOperations); + } + + /** + * Checks if a ResizingBehaviour is installed on the provided region. + * + * @param region the region to check for installation + * @return true if the behavior is installed, false otherwise + */ + public static boolean isInstalled(Region region) { + return Boolean.TRUE.equals(region.getProperties().get(RESIZE_BEHAVIOUR_INSTALLED)); + } + + /** + * Returns true if this ResizingBehaviour is installed on the region. + * + * @return true if installed, false otherwise + */ + public boolean isInstalled() { + return isInstalled(region); + } + + /** + * Uninstalls this ResizingBehaviour from the region, cleaning up all event handlers. + */ + public void uninstall() { + region.removeEventFilter(MouseEvent.MOUSE_MOVED, mouseMovedHandler); + region.removeEventFilter(MouseEvent.MOUSE_ENTERED, mouseMovedHandler); + region.removeEventFilter(MouseEvent.MOUSE_ENTERED_TARGET, mouseMovedHandler); + + region.removeEventFilter(MouseEvent.MOUSE_PRESSED, mousePressedHandler); + region.removeEventFilter(MouseEvent.MOUSE_RELEASED, mouseReleasedHandler); + region.removeEventFilter(MouseEvent.MOUSE_DRAGGED, mouseDraggedHandler); + + region.getProperties().remove(RESIZE_BEHAVIOUR_INSTALLED); + } + }