/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
package illarion.easyquest.gui;
import com.mxgraph.io.mxCodecRegistry;
import com.mxgraph.io.mxObjectCodec;
import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxGraphModel;
import com.mxgraph.model.mxICell;
import com.mxgraph.model.mxIGraphModel;
import com.mxgraph.swing.handler.mxKeyboardHandler;
import com.mxgraph.swing.handler.mxRubberband;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.util.*;
import com.mxgraph.util.mxEventSource.mxIEventListener;
import com.mxgraph.util.mxUndoableEdit.mxUndoableChange;
import com.mxgraph.view.mxGraphSelectionModel;
import com.mxgraph.view.mxStylesheet;
import illarion.easyquest.EditorKeyboardHandler;
import illarion.easyquest.Lang;
import illarion.easyquest.QuestIO;
import illarion.easyquest.quest.Position;
import illarion.easyquest.quest.Status;
import illarion.easyquest.quest.Trigger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@SuppressWarnings("serial")
public final class Editor extends mxGraphComponent {
@Nullable
private Path questFile;
private boolean savedSinceLastChange;
private int questID = 10000;
@Nonnull
@SuppressWarnings("unused")
private final mxKeyboardHandler keyboardHandler;
@Nonnull
@SuppressWarnings("unused")
private final mxRubberband rubberband;
@Nonnull
private final mxUndoManager undoManager;
private final mxIEventListener undoHandler = new mxIEventListener() {
@Override
public void invoke(Object source, @Nonnull mxEventObject evt) {
undoManager.undoableEditHappened((mxUndoableEdit) evt.getProperty("edit"));
}
};
Editor(@Nonnull Graph graph) {
super(graph);
mxICell root = (mxCell) graph.getModel().getRoot();
Object value = root.getValue();
if (value != null) {
String txt = value.toString();
try {
questID = Integer.parseInt(txt);
} catch (NumberFormatException e) {
}
}
getConnectionHandler().getMarker().setHotspot(0.5f);
setToolTips(true);
setCellEditor(new CellEditor(this));
keyboardHandler = new EditorKeyboardHandler(this);
rubberband = new mxRubberband(this);
undoManager = new mxUndoManager();
mxCodecRegistry.register(new mxObjectCodec(new Status()));
mxCodecRegistry.addPackage(Status.class.getPackage().getName());
mxCodecRegistry.register(new mxObjectCodec(new Trigger()));
mxCodecRegistry.addPackage(Trigger.class.getPackage().getName());
mxCodecRegistry.register(new mxObjectCodec(new Position()));
mxCodecRegistry.addPackage(Position.class.getPackage().getName());
graph.getModel().addListener(mxEvent.UNDO, undoHandler);
graph.getView().addListener(mxEvent.UNDO, undoHandler);
mxIEventListener undoHandler = (source, evt) -> {
List<mxUndoableChange> changes = ((mxUndoableEdit) evt.getProperty("edit")).getChanges();
graph.setSelectionCells(graph.getSelectionCellsForChanges(changes));
};
undoManager.addListener(mxEvent.UNDO, undoHandler);
undoManager.addListener(mxEvent.REDO, undoHandler);
Editor editor = this;
getGraphControl().addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(@Nonnull MouseEvent e) {
if ((e.getClickCount() == 2) || ((e.getClickCount() == 1) &&
(MainFrame.getInstance().getCreateType() == MainFrame.CREATE_STATUS))) {
Object cell = getCellAt(e.getX(), e.getY());
if (cell == null) {
Object parent = graph.getDefaultParent();
graph.getModel().beginUpdate();
try {
Status status = new Status();
status.setName("New Quest Status");
status.setStart(false);
graph.insertVertex(parent, null, status, e.getX() - 60, e.getY() - 15, 120, 30);
} finally {
graph.getModel().endUpdate();
}
e.consume();
}
} else if ((e.getClickCount() == 1) &&
(MainFrame.getInstance().getCreateType() == MainFrame.CREATE_TRIGGER)) {
Object cell = getCellAt(e.getX(), e.getY());
if (cell == null) {
Object parent = graph.getDefaultParent();
graph.getModel().beginUpdate();
try {
Trigger trigger = new Trigger();
trigger.setName("New Quest Trigger");
mxICell edge = (mxCell) graph.insertEdge(parent, null, trigger, null, null);
edge.getGeometry().setSourcePoint(new mxPoint(e.getX() - 60, e.getY() - 15));
edge.getGeometry().setTargetPoint(new mxPoint(e.getX() + 60, e.getY() + 15));
editor.labelChanged(edge, trigger, null);
} finally {
graph.getModel().endUpdate();
}
e.consume();
}
}
}
});
}
private static void setup(@Nonnull Graph graph) {
mxStylesheet stylesheet = graph.getStylesheet();
Map<String, Object> nodeStyle = stylesheet.getDefaultVertexStyle();
nodeStyle.put(mxConstants.STYLE_SHAPE, mxConstants.SHAPE_RECTANGLE);
nodeStyle.put(mxConstants.STYLE_ROUNDED, true);
nodeStyle.put(mxConstants.STYLE_OPACITY, 50);
nodeStyle.put(mxConstants.STYLE_FILLCOLOR, "#EFEFFF");
nodeStyle.put(mxConstants.STYLE_GRADIENTCOLOR, "#AFAFFF");
nodeStyle.put(mxConstants.STYLE_FONTCOLOR, "#000000");
stylesheet.setDefaultVertexStyle(nodeStyle);
Map<String, Object> edgeStyle = stylesheet.getDefaultEdgeStyle();
edgeStyle.put(mxConstants.STYLE_EDGE, mxConstants.EDGESTYLE_ELBOW);
edgeStyle.put(mxConstants.STYLE_STROKEWIDTH, 2.0);
edgeStyle.put(mxConstants.STYLE_ROUNDED, true);
stylesheet.setDefaultEdgeStyle(edgeStyle);
Map<String, Object> startStyle = new HashMap<>();
startStyle.put(mxConstants.STYLE_STROKEWIDTH, 3.0);
startStyle.put(mxConstants.STYLE_STROKECOLOR, "#0000F0");
stylesheet.putCellStyle("StartStyle", startStyle);
}
@Nonnull
public mxUndoManager getUndoManager() {
return undoManager;
}
public int getSelectedStatusNumber() throws IllegalStateException {
Graph graph = (Graph) getGraph();
mxGraphSelectionModel model = graph.getSelectionModel();
Object[] nodes = model.getCells();
int count = 0;
mxCell status = null;
for (Object node : nodes) {
mxCell cell = (mxCell) node;
if (cell.getValue() instanceof Status) {
status = cell;
count += 1;
}
}
if (count == 1) {
return ((Status) status.getValue()).isStart() ? 0 : Integer.parseInt(status.getId());
} else {
throw new IllegalStateException(String.valueOf(count));
}
}
public int getQuestID() {
return questID;
}
public void setQuestID(int questID) {
if (this.questID != questID) {
this.questID = questID;
mxIGraphModel model = getGraph().getModel();
model.beginUpdate();
try {
mxICell root = (mxCell) model.getRoot();
root.setValue(questID);
} finally {
model.endUpdate();
}
}
}
@Nullable
public Path getQuestFile() {
return questFile;
}
public void setQuestFile(@Nullable Path file) {
if (file != null) {
questFile = file;
}
}
public void saved() {
savedSinceLastChange = false;
}
public boolean changedSinceSave() {
return savedSinceLastChange;
}
public void changedQuest() {
savedSinceLastChange = true;
}
@Nonnull
public static Editor loadQuest(@Nullable Path quest) {
mxIGraphModel model;
if (quest == null) {
model = new mxGraphModel();
} else {
try {
model = QuestIO.loadGraphModel(quest);
} catch (IOException e) {
model = new mxGraphModel();
}
}
Graph graph = new Graph(model);
setup(graph);
return new Editor(graph);
}
public boolean validQuest() {
Graph graph = (Graph) getGraph();
mxGraphSelectionModel model = graph.getSelectionModel();
model.clear();
Object parent = graph.getDefaultParent();
Object[] edges = graph.getChildEdges(parent);
Object[] nodes = graph.getChildVertices(parent);
int countStart = 0;
for (Object node : nodes) {
mxICell cell = (mxICell) node;
Status status = (Status) cell.getValue();
if (status.isStart()) {
countStart += 1;
}
}
String errors = "";
if (countStart != 1) {
errors = errors + Lang.getMsg(Editor.class, "startNumberError") + ' ' + countStart + ".\n";
}
int countUnconnected = 0;
int countNoContent = 0;
for (Object edge : edges) {
mxCell cell = (mxCell) edge;
mxCell source = (mxCell) cell.getSource();
mxCell target = (mxCell) cell.getTarget();
if ((source == null) || (target == null)) {
countUnconnected += 1;
}
Trigger trigger = (Trigger) cell.getValue();
if ((trigger.getType() == null) || (trigger.getObjectId() == null) ||
((trigger.getObjectId() instanceof Long) && ((Long) trigger.getObjectId() == 0)) ||
(trigger.getParameters() == null) || (trigger.getConditions() == null)) {
model.addCell(cell);
countNoContent += 1;
}
}
if (countUnconnected > 0) {
errors = errors + Lang.getMsg(Editor.class, "unconnectedError") + ' ' + countUnconnected + ".\n";
}
if (countNoContent > 0) {
errors = errors + Lang.getMsg(Editor.class, "noContentError") + ' ' + countNoContent + ".\n";
}
if (!errors.isEmpty()) {
JOptionPane.showMessageDialog(MainFrame.getInstance(), errors, Lang.getMsg(Editor.class, "exportFailed"),
JOptionPane.ERROR_MESSAGE);
return false;
}
return true;
}
}