import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.nio.file.*;
import java.nio.charset.Charset;
import javax.xml.stream.*;

import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.input.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;
import javafx.scene.canvas.*;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.geometry.*;
import javafx.event.*;
import javafx.collections.*;

public class Edit extends Application {

  @Override
  public void start(Stage stage) throws Exception {
    BorderPane layout = new BorderPane();
    Scene scene = new Scene(layout);
    stage.setScene(scene);
    Scheme scheme = new Scheme(stage);
    test(scheme);
    scheme.read();
    layout.setCenter(scheme.getScrollPane());
    stage.setOnCloseRequest(event -> { if (!scheme.saveDialog()) event.consume(); });
    stage.show();
  }

  public static boolean validName(String name) { return name != null && name.matches("[a-zA-Z]|[a-zA-Z_][a-zA-Z0-9_]+"); }

  Future<?> future = null;
  int angle = 0;
  void test(Scheme scheme) {
    File file = new File(scheme.getXmlName().concat(".xml"));
    if (!file.exists()) {
      StringBuilder sb = new StringBuilder();
      sb.append("<?xml version=\"1.0\"?>\n");
      sb.append("<scheme width=\"500\" height=\"400\">\n");
      //sb.append(" <element clss=\"SETest\"/>\n");
      sb.append(" <schemeElement clss=\"Edit$SELabel\" name=\"L1\" layoutX=\"120.0\" layoutY=\"100.0\" text=\"Label\" bgFill=\"0xf6f6b6ff\"/>\n");
      sb.append(" <schemeElement clss=\"Edit$SEButton\" name=\"B1\" layoutX=\"250.0\" layoutY=\"150.0\" text=\"Button\"/>\n");
      sb.append("</scheme>\n");
      try { Files.write(file.toPath(), sb.toString().getBytes()); } catch (Exception e) { }
      scheme.setModifyed(true);
    }
    scheme.setOnAction((element, value) -> {
      if (!element.equals("B1.button")) return;
      if (future != null && !future.isDone()) {
        future.cancel(true);
      } else {
        ExecutorService executorService = Executors.newSingleThreadExecutor((runnable) -> {
          Thread thread = new Thread(runnable);
          thread.setDaemon(true);
          return thread;
        });
        future = executorService.submit(() -> {
          for (;;) {
            angle%=360;
            Platform.runLater(() -> {
              double x = 0, y = 0;
              try { x = Double.parseDouble(scheme.get("B1.layoutX")); } catch (Exception e) { }
              try { y = Double.parseDouble(scheme.get("B1.layoutY")); } catch (Exception e) { }
              scheme.set("L1.layoutX", Double.toString(x + 100 * Math.sin(Math.toRadians(angle))));
              scheme.set("L1.layoutY", Double.toString(y + 100 * Math.cos(Math.toRadians(angle))));
              scheme.set("L1.rotate", Double.toString(360 - angle));
              scheme.set("L1.text", Double.toString(angle));
            });
            angle++;
            try { Thread.sleep(15); } catch (Exception e) { break; }
          }
        });
        executorService.shutdown();
      }
    });
  }

  public class Scheme extends Region {
    protected ArrayList<String> listSE = new ArrayList<>(Arrays.asList(
      "Edit$SELabel",
      "Edit$SEButton"
    ));
    protected ObservableList<Label> labelSE;
    protected Stage stage;
    protected String name = null;
    protected Charset charset = Charset.defaultCharset(); //Charset.forName("US-ASCII");
    protected boolean modifyed = false;
    protected double gridSize = 10;
    protected ContextMenu contextMenu = new ContextMenu();
    protected MenuItem miNew, miSelect, miDelete, miToFront, miToBack, miProperty;
    protected MouseEvent cMouseEvent;
    protected SchemeElement editableElement;
    protected ScrollPane scrollPane;
    protected String xmlName = "scheme";

    public Scheme(Stage stage, SchemeElement... children) {
      this.stage = Objects.requireNonNull(stage, "Stage cannot be null.");
      addSE(children);
      stage.setFullScreenExitKeyCombination(KeyCombination.NO_MATCH);
      stage.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
        if (keyEvent.getCode() == KeyCode.F5) stage.setFullScreen(!stage.isFullScreen());
      });
      Paint background = Paint.valueOf("linear-gradient(from 0.0% 0.0% to 0.0% 100.0%, 0x90c1eaff 0.0%, 0x5084b0ff 100.0%)");
      setBackground(new Background(new BackgroundFill(background, null, null)));
      miNew = new MenuItem("New...");
      miNew.setOnAction(ae -> { newElement(cMouseEvent.getX(), cMouseEvent.getY()); });
      miSelect = new MenuItem("Select");
      miSelect.setOnAction(ae -> { setEditableElement(cMouseEvent.getSource()); });
      miDelete = new MenuItem("Delete");
      miDelete.setOnAction(ae -> { deleteSE((SchemeElement)cMouseEvent.getSource()); modifyed = true; });
      miToFront = new MenuItem("To Front");
      miToFront.setOnAction(ae -> { ((Node)cMouseEvent.getSource()).toFront(); });
      miToBack = new MenuItem("To Back");
      miToBack.setOnAction(ae -> { ((Node)cMouseEvent.getSource()).toBack(); });
      miProperty = new MenuItem("Property...");
      miProperty.setOnAction(ae -> { editProperty((SchemeElement)cMouseEvent.getSource()); });
      setOnMousePressed(me -> { mousePressed(me); });
    }

    public ScrollPane getScrollPane() {
      if (scrollPane == null) {
        scrollPane = new ScrollPane(this);
        scrollPane.viewportBoundsProperty().addListener((v, o, n) -> {
          Platform.runLater(() -> requestLayout());
        });
      }
      return scrollPane;
    }

    @Override protected double computePrefWidth(double width) {
      if (scrollPane == null) return super.computePrefWidth(width);
      return Math.max(scrollPane.getViewportBounds().getWidth(), super.computePrefWidth(width));
    }
    @Override protected double computePrefHeight(double height) {
      if (scrollPane == null) return super.computePrefHeight(height);
      return Math.max(scrollPane.getViewportBounds().getHeight(), super.computePrefHeight(height));
    }

    public void addSE(SchemeElement... children) {
      for (SchemeElement se : children) {
        if (se != null) {
          try {
            ElementProperty ep = se.getProp().get("name");
            if (ep != null) {
              String nn = ep.toString();
              AbstractMap.SimpleEntry<Integer, String> en = checkNameSE(nn);
              if (en.getKey().intValue() > 0) throw new RuntimeException(en.getValue());
            }
            getChildren().add(se);
            se.setScheme(this);
          } catch (Exception e) { e.printStackTrace(); }
        }
      }
    }

    public SchemeElement getSE(String name) {
      if (name != null && !name.isEmpty()) {
        for (Node node : getChildren()) {
          if (name.equals(((SchemeElement)node).getName())) return (SchemeElement)node;
        }
      }
      return null;
    }

    public boolean containsSE(String name) { return getSE(name) != null; }
    public void deleteSE(SchemeElement se) {
      getChildren().remove(se);
      se.setScheme(null);
    }
    //public void setNameSE(SchemeElement se, String name) { System.out.println("setNameSE " + name); }

    public void setEditableElement(Object element) {
      if (editableElement != null) {
        editableElement.setEditing(false);
        editableElement = null;
      }
      if (element != null && element instanceof SchemeElement) {
        editableElement = (SchemeElement)element;
        editableElement.setEditing(true);
        editableElement.toFront();
        modifyed = true;
      }
    }

    public void mousePressed(MouseEvent me) {
      cMouseEvent = me;
      if (contextMenu.isShowing()) contextMenu.hide();
      setEditableElement(me.isPrimaryButtonDown() ? me.getSource() : null);
      if (me.isSecondaryButtonDown()) {
        if (me.getSource() instanceof SchemeElement) {
          contextMenu.getItems().setAll(miSelect, miToFront, miToBack, miDelete, miProperty);
          miProperty.setDisable(((SchemeElement)me.getSource()).getProp().isEmpty());
          ArrayList<MenuItem> itemList = ((SchemeElement)me.getSource()).getItems();
          if (itemList != null && !itemList.isEmpty()) {
            contextMenu.getItems().add(new SeparatorMenuItem());
            contextMenu.getItems().addAll(itemList);
          }
        } else {
          contextMenu.getItems().setAll(miNew);
        }
        contextMenu.show((Node)me.getSource(), me.getScreenX(), me.getScreenY());
      }
      me.consume();
    }

    //@Override public ObservableList<Node> getChildren() { return super.getChildren(); }
    public Stage getStage() { return stage; }
    public String getName() { return name != null ? name : ""; }
    public void setName(String name) { if (Edit.validName(name)) this.name = name; }
    public ContextMenu getContextMenu() { return contextMenu; }
    public double getGridSize() { return gridSize; }
    public String getXmlName() { return xmlName; }
    public void setModifyed(boolean modifyed) { this.modifyed = modifyed; }
    public MouseEvent getCurrentMouseEvent() { return cMouseEvent; }

    public void write() {
      try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(xmlName.concat(".xml")), charset)) {
        XMLStreamWriter xmlw = XMLOutputFactory.newInstance().createXMLStreamWriter(writer);
        xmlw.writeStartDocument(); //writeStartDocument(charset.name(), "1.0");
        xmlw.writeCharacters("\n");
        xmlw.writeStartElement("scheme");
        xmlw.writeAttribute("name", getName());
        xmlw.writeAttribute("x", Double.toString(getStage().getX()));
        xmlw.writeAttribute("y", Double.toString(getStage().getY()));
        xmlw.writeAttribute("width", Double.toString(getStage().getWidth()));
        xmlw.writeAttribute("height", Double.toString(getStage().getHeight()));
        for (String sclss : listSE) {
          xmlw.writeCharacters("\n ");
          xmlw.writeEmptyElement("element");
          xmlw.writeAttribute("clss", sclss);
        }
        for (Node node : getChildren()) {
          xmlw.writeCharacters("\n ");
          xmlw.writeEmptyElement("schemeElement");
          xmlw.writeAttribute("clss", node.getClass().getName());
          Map<String, ElementProperty> prop = ((SchemeElement)node).getProp();
          for (ElementProperty ep: prop.values()) xmlw.writeAttribute(ep.getName(), ep.toString());
 	}
        xmlw.writeCharacters("\n");
        xmlw.writeEndElement();
        xmlw.writeEndDocument();
        xmlw.writeCharacters("\n");
        xmlw.flush();
        xmlw.close();
      } catch (Exception e) { e.printStackTrace(); }
    }

    public void read() {
      try (BufferedReader reader = Files.newBufferedReader(Paths.get(xmlName.concat(".xml")), charset)) {
        XMLStreamReader xmlr = XMLInputFactory.newInstance().createXMLStreamReader(reader);
        Map<String, String> attributeMap = new LinkedHashMap<>();
        while (xmlr.hasNext()) {
          xmlr.next();
          if (xmlr.isStartElement()) {
            attributeMap.clear();
            for (int i = 0; i < xmlr.getAttributeCount(); i++) attributeMap.put(xmlr.getAttributeLocalName(i), xmlr.getAttributeValue(i));
            String elementName = xmlr.getLocalName();
            if (elementName.equals("scheme")) {
              setName(attributeMap.get("name"));
              try { stage.setX(Double.parseDouble(attributeMap.get("x"))); } catch (Exception e) { }
              try { stage.setY(Double.parseDouble(attributeMap.get("y"))); } catch (Exception e) { }
              try { stage.setWidth(Double.parseDouble(attributeMap.get("width"))); } catch (Exception e) { }
              try { stage.setHeight(Double.parseDouble(attributeMap.get("height"))); } catch (Exception e) { }
            } else if (elementName.equals("element")) fillListSE(attributeMap);
            else if (elementName.equals("schemeElement")) {
              SchemeElement se = createSE(attributeMap);
              if (se != null) {
                se.init(0, 0);
                se.setAllProperty(attributeMap);
                addSE(se);
                fillListSE(attributeMap);
              }
            }
          }
        }
        xmlr.close();
      } catch (Exception e) { e.printStackTrace(); }
    }

    public void fillListSE(Map<String, String> map) {
      String sclss = map.get("clss");
      if(sclss != null && !sclss.isEmpty() && !listSE.contains(sclss)) listSE.add(sclss);
    }

    public void fillLabelSE() {
      if (labelSE == null) {
        Cursor cursor = getScene().getCursor();
        getScene().setCursor(Cursor.WAIT);
        labelSE = FXCollections.observableArrayList();
        Map<String, String> map = new LinkedHashMap<>();
        for (String sclss : listSE) {
          map.clear();
          map.put("clss", sclss);
          SchemeElement se = createSE(map);
          labelSE.add(se == null ? new Label(sclss) : new Label(se.getDescription(), se.getPicture(24)));
        }
        getScene().setCursor(cursor);
      }
    }

    public SchemeElement createSE(Map<String, String> map) {
      try {
        String className = map.get("clss");
        if (className == null || className.isEmpty()) throw new RuntimeException("Missing required attribute clss.");
        Class<?> cl = Class.forName(className);
        Object inst = className.startsWith("Edit$") ?
          cl.getConstructor(Edit.class).newInstance(Edit.this) :
          cl.getConstructor().newInstance();
        if (!(inst instanceof SchemeElement)) throw new RuntimeException("Is not SchemeElement.");
        return (SchemeElement)inst;
      } catch (Exception e) { e.printStackTrace(); }
      return null;
    }

    public boolean saveDialog() {
      if (!modifyed) return true;
      Dialog<ButtonType> dialog = new Dialog<>();
      dialog.initOwner(stage);
      dialog.initStyle(StageStyle.UTILITY);
      dialog.getDialogPane().getButtonTypes().addAll(ButtonType.YES, ButtonType.NO, ButtonType.CANCEL);
      dialog.setContentText("Save?");
      Optional<ButtonType> result = dialog.showAndWait();
      if (result.isPresent()) {
        if (result.get() == ButtonType.YES) { write(); return true; }
        if (result.get() == ButtonType.NO) return true;
      }
      return false;
    }

    public void editProperty(SchemeElement se) {
      Dialog<ButtonType> dialog = new Dialog<>();
      dialog.initOwner(stage);
      dialog.initStyle(StageStyle.UTILITY);
      dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
      GridPane grid = new GridPane();
      grid.setHgap(4);
      grid.setVgap(4);
      grid.setPadding(new Insets(20, 10, 10, 10));
      Map<String, ElementProperty> prop = se.getProp();
      int i = 0;
      for (ElementProperty ep: prop.values()) {
        if (ep.isEditable()) {
          grid.add(new Label(ep.getDescription()+":"), 0, i);
          Node editor = ep.editIni();
          grid.add(editor, 1, i);
          if (i == 0) Platform.runLater(() -> editor.requestFocus());
          i++;
        }
      }
      dialog.getDialogPane().setContent(grid);
      Button buttonOk = (Button)dialog.getDialogPane().lookupButton(ButtonType.OK);
      buttonOk.addEventFilter(ActionEvent.ACTION, ef -> {
        TextField textField = (TextField)prop.get("name").getEditor();
        String nn = textField.getText().trim();
        textField.setText(nn);
        if (!prop.get("name").toString().equals(nn)) {
          AbstractMap.SimpleEntry<Integer, String> en = checkNameSE(nn);
          if (en.getKey().intValue() > 0) { alert(en.getValue()); ef.consume(); return; }
        }
        for (ElementProperty ep: prop.values()) ep.editCommit();
        modifyed = true;
      });
      dialog.showAndWait();
    }

    public void newElement(double x, double y) {
      TextField textField = new TextField("");
      ListView<Label> listView = new ListView<>();
      listView.setPrefSize(200, 200);
      listView.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
      listView.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
      fillLabelSE();
      listView.setItems(labelSE);
      //listView.getSelectionModel().selectFirst();
      GridPane grid = new GridPane();
      grid.add(new Label("Name:"), 0, 0);
      grid.add(textField, 1, 0);
      grid.add(listView, 0, 1, 2, 1);
      grid.setHgap(10);
      grid.setVgap(10);
      Platform.runLater(() -> textField.requestFocus());
      Dialog<ButtonType> dialog = new Dialog<>();
      dialog.initOwner(stage);
      dialog.initStyle(StageStyle.UTILITY);
      dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
      dialog.getDialogPane().setContent(grid);
      Button buttonOk = (Button)dialog.getDialogPane().lookupButton(ButtonType.OK);
      buttonOk.addEventFilter(ActionEvent.ACTION, ef -> {
        if (!addEl(textField, listView, x, y)) ef.consume();
      });
      listView.setOnMouseClicked(me -> {
        if (me.getClickCount() == 2 && addEl(textField, listView, x, y)) dialog.close();
      });
      dialog.showAndWait();
    }

    public boolean addEl(TextField textField, ListView<Label> listView, double x, double y) {
      Map<String, String> map = new LinkedHashMap<>();
      int index = listView.getSelectionModel().getSelectedIndex();
      if (index < 0) return false;
      String nn = textField.getText().trim();
      textField.setText(nn);
      AbstractMap.SimpleEntry<Integer, String> en = checkNameSE(nn);
      if (en.getKey().intValue() > 0) { alert(en.getValue()); return false; }
      map.put("clss", listSE.get(index));
      map.put("name", nn);
      SchemeElement se = createSE(map);
      if (se == null) return false;
      se.init(x, y);
      se.setAllProperty(map);
      addSE(se);
      modifyed = true;
      return true;
    }

    public AbstractMap.SimpleEntry<Integer, String> checkNameSE(String name) {
      if (!name.isEmpty()) {
        if (!Edit.validName(name)) { return new AbstractMap.SimpleEntry<>(Integer.valueOf(1), "Element name " + name + " is not valid."); }
        if (containsSE(name)) { return new AbstractMap.SimpleEntry<>(Integer.valueOf(2), "Element name " + name + " already defined."); }
      }
      return new AbstractMap.SimpleEntry<>(Integer.valueOf(0), null);
    }

    public void alert(String s) {
      Alert alert = new Alert(Alert.AlertType.NONE, s, ButtonType.OK);
      alert.initOwner(stage);
      alert.initStyle(StageStyle.UTILITY);
      alert.showAndWait();
    }

    private BiConsumer<String, String> hAction = null;
    public void setOnAction(BiConsumer<String, String> hAction) { this.hAction = hAction; }
    public void action(String elementName, String value) { if (hAction != null) hAction.accept(elementName, value); }

    private BiConsumer<String, Exception> hMessage= (m, e) -> { System.out.println(m); };
    public void setOnMessage(BiConsumer<String, Exception> hMessage) { this.hMessage = hMessage; }
    public void message(String message) { message(message, null); }
    public void message(String message, Exception exception) { if (hMessage != null) hMessage.accept(message, exception); }

    public boolean set(String elementName, String value) {
      AbstractMap.SimpleEntry<String, String> en = splitName(elementName);
      return set(en.getKey(), en.getValue(), value);
    }
    public boolean set(String schemeElementName, String propertyName, String value) {
      ElementProperty ep = getEP(schemeElementName, propertyName);
      if (ep == null) return false;
      if (!ep.isChangeable()) { message("Property " + schemeElementName + "." + propertyName +" is not changeable."); return false; }
      boolean ret = ep.fromString(value);
      if (!ret) message("Property " + schemeElementName + "." + propertyName +" value is not valid.");
      return ret;
    }
    public String get(String elementName) {
      AbstractMap.SimpleEntry<String, String> en = splitName(elementName);
      return get(en.getKey(), en.getValue());
    }
    public String get(String schemeElementName, String propertyName) {
      ElementProperty ep = getEP(schemeElementName, propertyName);
      return ep != null ? ep.toString() : null;
    }
    public ElementProperty getEP(String schemeElementName, String propertyName) {
      if (schemeElementName == null || schemeElementName.isEmpty()) { message("Element name is empty."); return null; }
      SchemeElement se = getSE(schemeElementName);
      if (se == null) { message("Element " + schemeElementName + " not found."); return null; }
      if (propertyName == null || propertyName.isEmpty()) { message("Element " + schemeElementName + " property name is empty."); return null; }
      ElementProperty ep = se.getProp().get(propertyName);
      if (ep == null) { message("Property " + schemeElementName + "." + propertyName + " not found."); return null; }
      return ep;
    }
    public AbstractMap.SimpleEntry<String, String> splitName(String str) {
      int p = 0, c = 0;
      for (int i = 0; i < str.length(); i++) if (str.charAt(i) == '.') { p = i; c++; }
      if (c != 1) return new AbstractMap.SimpleEntry<>("", "");
      return new AbstractMap.SimpleEntry<>(str.substring(0, p), str.substring(p + 1, str.length()));
    }

  }  //end class Scheme

  public static class SchemeElement extends Group {

    protected StringProperty nameProperty = new SimpleStringProperty(null, "name");
    protected Scheme scheme = null;
    protected boolean editing = false;
    protected Map<String, ElementProperty> properties = new LinkedHashMap<>();
    protected ArrayList<Node> anchors;

    public SchemeElement() {
      setOnMousePressed(me -> { mousePressed(me); });
      addProperty(new ElementProperty(nameProperty()).setDescription("Name").setChangeable(false));
    }

    public void init(double x, double y) {
      layoutXProperty().set(x);
      layoutYProperty().set(y);
      addAllProperty(new ElementProperty(layoutXProperty()).setRestr().setEditable(false),
                     new ElementProperty(layoutYProperty()).setRestr().setEditable(false)
      );
    }

    public void childrenAddAll(Node... childrens) { getChildren().addAll(childrens); }
    public StringProperty nameProperty() { return nameProperty; }
    public String getName() { return nameProperty.get() != null ? nameProperty.get() : ""; }
    public void mousePressed(MouseEvent me) { if (scheme != null) scheme.mousePressed(me); }

    public void setEditing(boolean editing) {
      this.editing = editing;
      edit(editing);
    }
    public boolean isEditing()  { return editing; }
    //public Scheme getScheme() { return scheme; }
    public void setScheme(Scheme scheme) {
      if (this.scheme != null && scheme != null && this.scheme != scheme) throw new IllegalArgumentException("Scheme already defined.");
      this.scheme = scheme;
    }
    public Map<String, ElementProperty> getProp() { return properties; }
    public void addAllProperty(ElementProperty... elements) {
      for (ElementProperty ep : elements) addProperty(ep);
    }
    public void addProperty(ElementProperty ep) {
      if (!Edit.validName(ep.getName())) throw new IllegalArgumentException("Property name " + ep.getName() + " is not valid.");
      if (properties.containsKey(ep.getName())) throw new IllegalArgumentException("Property name " + ep.getName() + " already defined.");
      properties.put(ep.getName(), ep);
      ep.setSchemeElement(this);
    }
    public void setAllProperty(Map<String, String> map) {
      for (Map.Entry<String, String> entry : map.entrySet()) setProperty(entry.getKey(), entry.getValue());
    }
    public void setProperty(String propertyName, String value) {
      ElementProperty ep = properties.get(propertyName);
      if (ep != null) ep.fromString(value);
    }
    public String getProperty(String propertyName) {
      ElementProperty ep = properties.get(propertyName);
      return ep != null ? ep.toString() : "";
    }

    public void edit(boolean editing) {
      //if (scheme == null) throw new NullPointerException("Scheme cannot be null.");
      if (editing) {
        if (anchors == null) {
          anchors = new ArrayList<Node>(Arrays.asList(anchors()));
          getChildren().addAll(anchors);
        } else anchors.forEach(anchor -> anchor.setVisible(true));
      } else if (anchors != null) anchors.forEach(anchor -> anchor.setVisible(false));
    }

    public Node[] anchors() {
      return new Node[] { createAnchor(layoutXProperty(), layoutYProperty(), false) };
    }
    private double dx, dy;
    public Node createAnchor(DoubleProperty x, DoubleProperty y, boolean bind) {
      Circle anchor = new Circle(0, 0, 2);
      if (bind) {
        anchor.centerXProperty().bind(x);
        anchor.centerYProperty().bind(y);
      }
      anchor.setFill(Color.WHITE);
      anchor.setStroke(Color.BLACK);
      anchor.setStrokeWidth(2);
      anchor.setStrokeType(StrokeType.OUTSIDE);
      anchor.setOnMousePressed(me -> {
        dx = (bind ? x.get() : 0) - me.getX();
        dy = (bind ? y.get() : 0) - me.getY();
        me.consume();
      });
      anchor.setOnMouseDragged(me -> {
        x.set(me.getX() + dx + (bind ? 0 : x.get()));
        y.set(me.getY() + dy + (bind ? 0 : y.get()));
        me.consume();
      });
      anchor.setOnMouseReleased((me) -> { //snap to grid
        double gridSize = scheme.getGridSize();
        x.set(Math.round(x.get() / gridSize) * gridSize);
        y.set(Math.round(y.get() / gridSize) * gridSize);
        me.consume();
      });
      anchor.setOnMouseEntered((mouseEvent) -> { getScene().setCursor(Cursor.HAND); });
      anchor.setOnMouseExited((mouseEvent) -> { getScene().setCursor(Cursor.DEFAULT); });
      return anchor;
    }

    public Node createBoundLine(DoubleProperty startX, DoubleProperty startY, DoubleProperty endX, DoubleProperty endY) {
      Line boundLine = new Line();
      boundLine.startXProperty().bind(startX);
      boundLine.startYProperty().bind(startY);
      boundLine.endXProperty().bind(endX);
      boundLine.endYProperty().bind(endY);
      boundLine.setStrokeLineCap(StrokeLineCap.BUTT);
      boundLine.getStrokeDashArray().setAll(10.0, 5.0);
      return boundLine;
    }

    public ArrayList<MenuItem> getItems() { return null; }
    public String getDescription() { return getClass().getName(); }
    public Node getPicture(double size) {
      Canvas canvas = new Canvas(size, size);
      GraphicsContext gc = canvas.getGraphicsContext2D();
      gc.setStroke(Color.BLUE);
      gc.strokeRect(1, 1, size * .67, size * .67);
      gc.strokeRect(size * .33, size * .33, size * .6, size * .6);
      return canvas;
    }
  }  //end class SchemeElement

  public static class ElementProperty {

    protected String name = null;
    protected Node editor = null;
    protected SchemeElement se = null;
    protected String description = null;
    protected boolean editable = true;
    protected boolean changeable = true;
    protected Property<?> prop;
    protected Class<?> clss;

    public ElementProperty(Property<?> prop) { this(prop, prop.getName(), null); }
    public ElementProperty(Property<?> prop, String name) { this(prop, name, null); }
    public ElementProperty(Property<?> prop, Class<?> clss) { this(prop, prop.getName(), clss); }
    public ElementProperty(Property<?> prop, String name, Class<?> clss) {
      this.prop = Objects.requireNonNull(prop, "Property cannot be null.");
      this.prop = prop;
      this.name = name;
      this.clss = clss;
    }

    public String getName() { return name != null ? name : ""; }
    public ElementProperty setEditable(boolean editable) { this.editable = editable; return this; }
    public boolean isEditable() { return editable; }
    public ElementProperty setChangeable(boolean changeable) { this.changeable = changeable; return this; }
    public boolean isChangeable() { return changeable; }
    public ElementProperty setDescription(String description) { this.description = description; return this; }
    public String getDescription() { return description != null ? description : getName(); }

    protected ChangeListener<Number> doubleRestr = (v, o, n) -> { if (n.doubleValue() < 0) ((DoubleProperty)v).set(0); };
    public ElementProperty setRestr() {
      if (prop instanceof DoubleProperty) {
        ((DoubleProperty)prop).addListener(doubleRestr);
      }
      return this;
    }
    //public SchemeElement getSchemeElement() { return se; }
    public void setSchemeElement(SchemeElement se) {
      if (this.se != null && se != null && this.se != se) throw new IllegalArgumentException("SchemeElement already defined.");
      this.se = se;
    }
    public Property<?> getProperty() { return prop; }
    @Override public String toString() { return prop.getValue() == null ? "" : prop.getValue().toString(); }

    public Node getEditor() {
      if (!editable) throw new RuntimeException("Property is not editable.");
      if (editor == null) {
        if (prop instanceof StringProperty || prop instanceof DoubleProperty) {
          editor = new TextField();
        } else if (prop instanceof BooleanProperty) {
          editor = new CheckBox();
        } else if (prop instanceof ObjectProperty) {
          if (clss == null) throw new NullPointerException("Clss cannot be null.");
          if (clss.isAssignableFrom(Paint.class)) {
            editor = new ColorPicker();
          } else if (clss.isEnum()) {
            editor = new ComboBox<Object>(FXCollections.observableArrayList(clss.getEnumConstants()));
          }
        }
        if (editor == null) editor = new Label("undefined");
      }
      return editor;
    }

    @SuppressWarnings("unchecked")
    public Node editIni() {
      getEditor();
      if (editor instanceof TextField) ((TextField)editor).setText(toString());
      else if (editor instanceof CheckBox) ((CheckBox)editor).setSelected(((Boolean)prop.getValue()).booleanValue());
      else if (editor instanceof ColorPicker) ((ColorPicker)editor).setValue((Color)prop.getValue());
      else if (editor instanceof ComboBox) ((ComboBox<Object>)editor).getSelectionModel().select(prop.getValue());
      return editor;
    }

    @SuppressWarnings("unchecked")
    public void editCommit() {
      if (!editable || editor == null) return;
      if (editor instanceof TextField) fromString(((TextField)editor).getText());
      else if (editor instanceof CheckBox) ((BooleanProperty)prop).set(((CheckBox)editor).isSelected());
      else if (editor instanceof ColorPicker) ((ObjectProperty<Paint>)prop).set(((ColorPicker)editor).getValue());
      else if (editor instanceof ComboBox) ((ObjectProperty)prop).setValue(((ComboBox<Object>)editor).getSelectionModel().getSelectedItem());
    }

    @SuppressWarnings("unchecked")
    public boolean fromString(String s) {
      try { 
        if (prop instanceof StringProperty) ((StringProperty)prop).set(s);
        else if (prop instanceof DoubleProperty) {
          ((DoubleProperty)prop).set(Double.parseDouble(s));
        } else if (prop instanceof BooleanProperty) {
          ((BooleanProperty)prop).set(Boolean.parseBoolean(s));
        } else if (prop instanceof ObjectProperty) {
          if (clss.isAssignableFrom(Paint.class)) {
            ((ObjectProperty<Paint>)prop).set(Paint.valueOf(s));
          } else if (clss.isEnum()) {
            for (Object o : clss.getEnumConstants()) {
              if (s.equals(o.toString())) { ((ObjectProperty)prop).setValue(o); break; }
            }
          } else return false;
        } else return false;
      } catch (Exception e) { return false; }
      return true;
    }

  }  //end class ElementProperty

  public class SELabel extends SchemeElement {

    public SELabel() {
      super();
      Label label = new Label("Label");
      ObjectProperty<Paint> bgFillProperty = new SimpleObjectProperty<>(null, "bgFill", Color.TRANSPARENT);
      bgFillProperty.addListener((v, o, n) -> {
        label.setBackground(new Background(new BackgroundFill(n, CornerRadii.EMPTY, Insets.EMPTY)));
      });
      ObjectProperty<Paint> borderFillProperty = new SimpleObjectProperty<>(null, "borderFill", Color.TRANSPARENT);
      borderFillProperty.addListener((v, o, n) -> {
        label.setBorder(new Border(new BorderStroke(n, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(1))));
      });
      label.setPadding(new Insets(2));
      label.layoutXProperty().bind(label.widthProperty().divide(-2));
      label.layoutYProperty().bind(label.heightProperty().divide(-2));
      addAllProperty(new ElementProperty(label.textProperty()),
                     new ElementProperty(label.textFillProperty(), Paint.class),
                     new ElementProperty(bgFillProperty, Paint.class),
                     new ElementProperty(borderFillProperty, Paint.class),
                     new ElementProperty(label.minWidthProperty()),
                     new ElementProperty(label.alignmentProperty(), Pos.class),
                     new ElementProperty(label.rotateProperty())
      );
      childrenAddAll(label);
    }

    @Override public String getDescription() { return "Label"; }

  }  //end class SELabel

  public class SEButton extends SchemeElement {

    public SEButton() {
      super();
      Button button = new Button("Button");
      addEventFilter(MouseEvent.MOUSE_PRESSED, me -> { if (me.isSecondaryButtonDown()) mousePressed(me); });
      button.setOnAction(ae -> { scheme.action(getName().concat(".button"), "pressed"); });
      button.setPadding(new Insets(6));
      button.layoutXProperty().bind(button.widthProperty().divide(-2));
      button.layoutYProperty().bind(button.heightProperty().divide(-2));
      addAllProperty(new ElementProperty(button.textProperty()),
                     new ElementProperty(button.minWidthProperty())
      );
      childrenAddAll(button);
    }

    @Override public String getDescription() { return "Button"; }

  }  //end class SEButton

}  //end class Edit