package com.baselet.element;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.baselet.control.SharedUtils;
import com.baselet.control.basics.geom.Dimension;
import com.baselet.control.basics.geom.DimensionDouble;
import com.baselet.control.basics.geom.Line;
import com.baselet.control.basics.geom.Point;
import com.baselet.control.basics.geom.PointDouble;
import com.baselet.control.basics.geom.Rectangle;
import com.baselet.control.config.SharedConfig;
import com.baselet.control.constants.SharedConstants;
import com.baselet.control.enums.AlignHorizontal;
import com.baselet.control.enums.Direction;
import com.baselet.control.enums.ElementStyle;
import com.baselet.control.enums.LineType;
import com.baselet.diagram.draw.DrawHandler;
import com.baselet.diagram.draw.helper.ColorOwn;
import com.baselet.diagram.draw.helper.ColorOwn.Transparency;
import com.baselet.element.facet.Facet;
import com.baselet.element.facet.KeyValueFacet;
import com.baselet.element.facet.PropertiesParserState;
import com.baselet.element.facet.Settings;
import com.baselet.element.facet.common.GroupFacet;
import com.baselet.element.facet.common.LayerFacet;
import com.baselet.element.interfaces.Component;
import com.baselet.element.interfaces.CursorOwn;
import com.baselet.element.interfaces.DrawHandlerInterface;
import com.baselet.element.interfaces.GridElement;
import com.baselet.element.interfaces.GridElementDeprecatedAddons;
import com.baselet.element.sticking.PointChange;
import com.baselet.element.sticking.Stickable;
import com.baselet.element.sticking.StickableMap;
import com.baselet.element.sticking.Stickables;
import com.baselet.element.sticking.StickingPolygon;
import com.baselet.gui.AutocompletionText;
public abstract class NewGridElement implements GridElement {
private final Logger log = LoggerFactory.getLogger(NewGridElement.class);
private DrawHandler drawer; // this is the drawer for element specific stuff
private DrawHandler metaDrawer; // this is a separate drawer to draw stickingborder, selection-background etc.
private Component component;
private DrawHandlerInterface handler;
private List<String> panelAttributes;
protected PropertiesParserState state;
protected final UndoHistory undoStack = new UndoHistory();
public void init(Rectangle bounds, String panelAttributes, String additionalAttributes, Component component, DrawHandlerInterface handler) {
this.component = component;
drawer = component.getDrawHandler();
metaDrawer = component.getMetaDrawHandler();
setPanelAttributesHelper(panelAttributes);
setRectangle(bounds);
this.handler = handler;
state = new PropertiesParserState(createSettings(), drawer);
setAdditionalAttributes(additionalAttributes);
}
@Override
public String getPanelAttributes() {
return SharedUtils.listToString("\n", panelAttributes);
}
@Override
public List<String> getPanelAttributesAsList() {
return panelAttributes;
}
@Override
public void setPanelAttributes(String panelAttributes) {
setPanelAttributesHelper(panelAttributes);
updateModelFromText();
}
public void setPanelAttributesHelper(String panelAttributes) {
this.panelAttributes = Arrays.asList(panelAttributes.split("\n", -1)); // split with -1 to retain empty lines at the end
}
/**
* ugly workaround to avoid that the Resize().execute() call which calls setSize() on this model updates the model during the
* calculated model update from autoresize. Otherwise the drawer cache would get messed up (it gets cleaned up 2 times in a row and afterwards everything gets drawn 2 times).
* Best testcase is an autoresize element with a background. Write some text and everytime autresize triggers, the background is drawn twice.
*/
private boolean autoresizePossiblyInProgress = false;
@Override
public void updateModelFromText() {
autoresizePossiblyInProgress = true;
drawer.clearCache();
drawer.resetStyle(); // must be set before actions which depend on the fontsize (otherwise a changed fontsize would be recognized too late)
try {
PropertiesParser.parsePropertiesAndHandleFacets(this, state);
} catch (Exception e) {
log.info("Cannot parse Properties Text", e);
drawer.resetStyle();
String localizedMessage = e.getLocalizedMessage();
if (localizedMessage == null) {
localizedMessage = e.toString();
}
drawError(drawer, localizedMessage);
}
autoresizePossiblyInProgress = false;
component.afterModelUpdate();
}
protected void drawError(DrawHandler drawer, String errorText) {
drawer.setEnableDrawing(true);
drawer.setForegroundColor(ColorOwn.RED);
drawer.setBackgroundColor(ColorOwn.RED.transparency(Transparency.SELECTION_BACKGROUND));
drawer.setLineWidth(0.2);
drawer.drawRectangle(0, 0, getRealSize().width, getRealSize().height); // draw dotted rect (to enforce background color even if element has no border)
resetAndDrawMetaDrawerContent(metaDrawer);
// y coordinate specifies the bottom line of the first text line therefore
// count the new line chars and calculate offset to place the message in the center
String[] errorLines = errorText.split("\n");
double y = getRealSize().height * 0.5 - errorLines.length * drawer.textHeightMaxWithSpace() / 2.0 + drawer.textHeightMax();
for (int i = 0; i < errorLines.length; i++, y += drawer.textHeightMaxWithSpace()) {
drawer.print(errorLines[i], 3, y, AlignHorizontal.LEFT);
}
}
void resetMetaDrawerAndDrawCommonContent(PropertiesParserState state, boolean resetMetaDrawer) {
drawCommonContent(state);
if (resetMetaDrawer) {
resetAndDrawMetaDrawerContent(metaDrawer);
}
}
protected abstract void drawCommonContent(PropertiesParserState state);
protected void resetAndDrawMetaDrawerContent(DrawHandler drawer) {
drawer.clearCache();
drawer.setForegroundColor(ColorOwn.TRANSPARENT);
drawer.setBackgroundColor(ColorOwn.SELECTION_BG);
drawer.drawRectangle(0, 0, getRealSize().width, getRealSize().height);
if (SharedConfig.getInstance().isDev_mode()) {
drawer.setForegroundColor(ColorOwn.BLACK);
drawer.setFontSize(10.5);
drawer.print(getId().toString(), new PointDouble(getRealSize().width - 3, getRealSize().height - 2), AlignHorizontal.RIGHT);
}
drawer.resetColorSettings();
if (SharedConfig.getInstance().isShow_stickingpolygon()) {
drawStickingPolygon(drawer);
}
}
@Override
public void setProperty(String key, Object newValue) {
StringBuilder sb = new StringBuilder("");
for (String line : getPanelAttributesAsList()) {
if (!line.startsWith(key)) {
sb.append(line).append("\n");
}
}
if (sb.length() > 0) { // remove last linebreak
sb.setLength(sb.length() - 1);
}
if (newValue != null) {
sb.append("\n").append(key).append(KeyValueFacet.SEP).append(newValue.toString()); // null will not be added as a value
}
setPanelAttributes(sb.toString());
}
@Override
public String getSetting(String key) {
for (String line : getPanelAttributesAsList()) {
if (line.startsWith(key + KeyValueFacet.SEP)) {
String[] split = line.split(KeyValueFacet.SEP, 2);
if (split.length > 1) {
return split[1];
}
}
}
return null;
}
@Override
public String getAdditionalAttributes() {
return ""; // usually GridElements have no additional attributes
}
@Override
public void setAdditionalAttributes(String additionalAttributes) {
// usually GridElements have no additional attributes
}
@Override
public boolean isInRange(Rectangle rect1) {
return rect1.contains(getRectangle());
}
@Override
public Set<Direction> getResizeArea(int x, int y) {
Set<Direction> returnSet = new HashSet<Direction>();
if (state.getElementStyle() == ElementStyle.NORESIZE || state.getElementStyle() == ElementStyle.AUTORESIZE) {
return returnSet;
}
if (x <= 5 && x >= 0) {
returnSet.add(Direction.LEFT);
}
else if (x <= getRectangle().width && x >= getRectangle().width - 5) {
returnSet.add(Direction.RIGHT);
}
if (y <= 5 && y >= 0) {
returnSet.add(Direction.UP);
}
else if (y <= getRectangle().height && y >= getRectangle().height - 5) {
returnSet.add(Direction.DOWN);
}
return returnSet;
}
/**
* @deprecated use {@link #generateStickingBorder()} instead, because typically the stickingpolygon is created for the own Rectangle, and the other method guarantees that the correct zoom level is applied (important to make alternative StickingPolygonGenerators like PointDoubleStickingPolygonGenerator work)
*/
@Deprecated
@Override
public final StickingPolygon generateStickingBorder(Rectangle rect) {
return state.getStickingPolygonGenerator().generateStickingBorder(rect);
}
/**
* generates the StickingPolygon of the element using the rectangle as if the zoomlevel would be 100% (this is IMPORTANT because the sticking-calculation doesn't calculate the zoomlevel (see Issue 229 and 231)
* Should never be overwritten; if a specific StickingPolygon should be created, instead overwrite the StickingPolygonGenerator in PropertiesParserState eg: Class uses different Generators based on which facets are active (see Class.java)
*/
@Override
public final StickingPolygon generateStickingBorder() {
return generateStickingBorder(getRealRectangle()); // ALWAYS generate the stickingBorder as if zoom were 100%
}
private final void drawStickingPolygon(DrawHandler drawer) {
Rectangle rect = new Rectangle(0, 0, getRealSize().width, getRealSize().height);
StickingPolygon poly = this.generateStickingBorder(rect);
drawer.setLineType(LineType.DASHED);
drawer.setForegroundColor(ColorOwn.STICKING_POLYGON);
Vector<? extends Line> lines = poly.getStickLines();
drawer.drawLines(lines.toArray(new Line[lines.size()]));
drawer.setLineType(LineType.SOLID);
drawer.resetColorSettings();
}
@Override
public void setRectangle(Rectangle bounds) {
component.setBoundsRect(bounds);
}
@Override
public void setLocationDifference(int diffx, int diffy) {
setLocation(getRectangle().x + diffx, getRectangle().y + diffy);
}
@Override
public void setLocation(int x, int y) {
Rectangle rect = getRectangle();
rect.setLocation(x, y);
component.setBoundsRect(rect);
}
@Override
public void setSize(int width, int height) {
if (width != getRectangle().width || height != getRectangle().height) { // only change size if it is really different
Rectangle rect = getRectangle();
rect.setSize(width, height);
setRectangle(rect);
if (!autoresizePossiblyInProgress) {
updateModelFromText();
}
}
}
@Override
public Rectangle getRectangle() {
return component.getBoundsRect();
}
@Override
public void repaint() {
component.repaintComponent();
}
/**
* @see com.baselet.element.interfaces.GridElement#getRealSize()
*/
@Override
public Dimension getRealSize() {
return new Dimension(zoom(getRectangle().width), zoom(getRectangle().height));
}
public Rectangle getRealRectangle() {
return new Rectangle(zoom(getRectangle().x), zoom(getRectangle().y), zoom(getRectangle().width), zoom(getRectangle().height));
}
private int zoom(int val) {
return val * SharedConstants.DEFAULT_GRID_SIZE / getGridSize();
}
@Override
public Component getComponent() {
return component;
}
protected abstract Settings createSettings();
@Override
public List<AutocompletionText> getAutocompletionList() {
List<AutocompletionText> returnList = new ArrayList<AutocompletionText>();
addAutocompletionTexts(returnList, state.getSettings().getFacetsForFirstRun());
addAutocompletionTexts(returnList, state.getSettings().getFacetsForSecondRun());
return returnList;
}
private void addAutocompletionTexts(List<AutocompletionText> returnList, List<? extends Facet> facets) {
for (Facet f : facets) {
for (AutocompletionText t : f.getAutocompletionStrings()) {
returnList.add(t);
}
}
}
@Override
public Integer getLayer() {
return state.getFacetResponse(LayerFacet.class, LayerFacet.DEFAULT_VALUE);
}
@Override
public Integer getGroup() {
return state.getFacetResponse(GroupFacet.class, null);
}
public void handleAutoresize(DimensionDouble necessaryElementDimension, AlignHorizontal alignHorizontal) {
Dimension realSize = getRealSize();
double diffw = necessaryElementDimension.getWidth() - realSize.width;
double diffh = necessaryElementDimension.getHeight() - realSize.height;
int diffwInt = SharedUtils.realignTo(false, unzoom(diffw), true, getGridSize());
int diffhInt = SharedUtils.realignTo(false, unzoom(diffh), true, getGridSize());
List<Direction> directions = null;
if (alignHorizontal == AlignHorizontal.LEFT) {
directions = Arrays.asList(Direction.RIGHT, Direction.DOWN);
}
else if (alignHorizontal == AlignHorizontal.RIGHT) {
diffwInt = -diffwInt;
directions = Arrays.asList(Direction.LEFT, Direction.DOWN);
}
else if (alignHorizontal == AlignHorizontal.CENTER) {
diffwInt = SharedUtils.realignTo(false, diffwInt / 2.0, true, getGridSize()) * 2;
directions = Arrays.asList(Direction.RIGHT, Direction.LEFT, Direction.DOWN);
}
drag(directions, diffwInt, diffhInt, new Point(0, 0), false, true, handler.getStickableMap(), false);
}
private double unzoom(double diffw) {
return diffw / SharedConstants.DEFAULT_GRID_SIZE * getGridSize();
}
@Override
public void setRectangleDifference(int diffx, int diffy, int diffw, int diffh, boolean firstDrag, StickableMap stickables, boolean undoable) {
Rectangle oldRect = getRectangle();
StickingPolygon stickingPolygonBeforeLocationChange = generateStickingBorder();
String oldAddAttr = getAdditionalAttributes();
setRectangle(new Rectangle(oldRect.x + diffx, oldRect.y + diffy, oldRect.getWidth() + diffw, oldRect.getHeight() + diffh));
moveStickables(stickables, undoable, oldRect, stickingPolygonBeforeLocationChange, oldAddAttr);
}
@Override
public void drag(Collection<Direction> resizeDirection, int diffX, int diffY, Point mousePosBeforeDrag, boolean isShiftKeyDown, boolean firstDrag, StickableMap stickables, boolean undoable) {
Rectangle oldRect = getRectangle();
StickingPolygon stickingPolygonBeforeLocationChange = generateStickingBorder();
String oldAddAttr = getAdditionalAttributes();
if (resizeDirection.isEmpty()) { // Move GridElement
setLocationDifference(diffX, diffY);
}
else { // Resize GridElement
Rectangle rect = getRectangle();
if (isShiftKeyDown && diagonalResize(resizeDirection)) { // Proportional Resize
boolean mouseToRight = diffX > 0 && diffX > diffY;
boolean mouseDown = diffY > 0 && diffY > diffX;
boolean mouseLeft = diffX < 0 && diffX < diffY;
boolean mouseUp = diffY < 0 && diffY < diffX;
if (mouseToRight || mouseLeft) {
diffY = diffX;
}
if (mouseDown || mouseUp) {
diffX = diffY;
}
}
if (resizeDirection.contains(Direction.LEFT) && resizeDirection.contains(Direction.RIGHT)) {
rect.setX(rect.getX() - diffX / 2);
rect.setWidth(Math.max(rect.getWidth() + diffX, minSize()));
}
else if (resizeDirection.contains(Direction.LEFT)) {
int newWidth = rect.getWidth() - diffX;
if (newWidth >= minSize()) {
rect.setX(rect.getX() + diffX);
rect.setWidth(newWidth);
}
}
else if (resizeDirection.contains(Direction.RIGHT)) {
rect.setWidth(Math.max(rect.getWidth() + diffX, minSize()));
}
if (resizeDirection.contains(Direction.UP)) {
int newHeight = rect.getHeight() - diffY;
if (newHeight >= minSize()) {
rect.setY(rect.getY() + diffY);
rect.setHeight(newHeight);
}
}
if (resizeDirection.contains(Direction.DOWN)) {
rect.setHeight(Math.max(rect.getHeight() + diffY, minSize()));
}
setRectangle(rect);
if (!autoresizePossiblyInProgress) {
updateModelFromText();
}
}
moveStickables(stickables, undoable, oldRect, stickingPolygonBeforeLocationChange, oldAddAttr);
}
private void moveStickables(StickableMap stickables, boolean undoable, Rectangle oldRect, StickingPolygon stickingPolygonBeforeLocationChange, String oldAddAttr) {
Map<Stickable, List<PointChange>> stickableChanges = Stickables.moveStickPointsBasedOnPolygonChanges(stickingPolygonBeforeLocationChange, generateStickingBorder(), stickables, getGridSize());
if (undoable) {
undoStack.add(new UndoInformation(getRectangle(), oldRect, stickableChanges, getGridSize(), oldAddAttr, getAdditionalAttributes()));
}
}
@Override
public void dragEnd() {
// only used by some specific elements like Relations
}
@Override
public boolean isSelectableOn(Point point) {
return getRectangle().contains(point);
}
private boolean diagonalResize(Collection<Direction> resizeDirection) {
return resizeDirection.contains(Direction.UP) && resizeDirection.contains(Direction.RIGHT) ||
resizeDirection.contains(Direction.UP) && resizeDirection.contains(Direction.LEFT) ||
resizeDirection.contains(Direction.DOWN) && resizeDirection.contains(Direction.LEFT) ||
resizeDirection.contains(Direction.DOWN) && resizeDirection.contains(Direction.RIGHT);
}
protected DrawHandlerInterface getHandler() {
return handler;
}
public int getGridSize() {
return getHandler().getGridSize();
}
private int minSize() {
return handler.getGridSize() * 2;
}
@Override
public void undoDrag() {
execUndoInformation(true);
}
private void execUndoInformation(boolean undo) {
UndoInformation undoInfo = undoStack.get(undo);
if (undoInfo != null) {
setRectangle(getRectangle().add(undoInfo.getDiffRectangle(getGridSize(), undo)));
Stickables.applyChanges(undoInfo.getStickableMoves(undo), null);
setAdditionalAttributes(undoInfo.getAdditionalAttributes(undo));
}
}
@Override
public void redoDrag() {
execUndoInformation(false);
}
@Override
public void mergeUndoDrag() {
UndoInformation undoInfoA = undoStack.remove();
UndoInformation undoInfoB = undoStack.remove();
undoStack.add(undoInfoA.merge(undoInfoB));
}
@Override
public GridElementDeprecatedAddons getDeprecatedAddons() {
return GridElementDeprecatedAddons.NONE;
}
@Override
public CursorOwn getCursor(Point point, Set<Direction> resizeDirections) {
return getCursorStatic(this, point, resizeDirections);
}
public static CursorOwn getCursorStatic(GridElement e, Point point, Set<Direction> resizeDirections) {
if (!e.isSelectableOn(point)) {
return CursorOwn.DEFAULT;
}
else {
if (resizeDirections.isEmpty()) {
return CursorOwn.HAND;
}
else if (resizeDirections.contains(Direction.UP) && resizeDirections.contains(Direction.RIGHT)) {
return CursorOwn.NE;
}
else if (resizeDirections.contains(Direction.UP) && resizeDirections.contains(Direction.LEFT)) {
return CursorOwn.NW;
}
else if (resizeDirections.contains(Direction.DOWN) && resizeDirections.contains(Direction.LEFT)) {
return CursorOwn.SW;
}
else if (resizeDirections.contains(Direction.DOWN) && resizeDirections.contains(Direction.RIGHT)) {
return CursorOwn.SE;
}
else if (resizeDirections.contains(Direction.UP)) {
return CursorOwn.N;
}
else if (resizeDirections.contains(Direction.RIGHT)) {
return CursorOwn.E;
}
else if (resizeDirections.contains(Direction.DOWN)) {
return CursorOwn.S;
}
else if (resizeDirections.contains(Direction.LEFT)) {
return CursorOwn.W;
}
return CursorOwn.DEFAULT;
}
}
}