/*
* Copyright 2016 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.rendering.nui.editor.layers;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.google.gson.stream.JsonReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.assets.ResourceUrn;
import org.terasology.assets.exceptions.InvalidUrnException;
import org.terasology.assets.format.AssetDataFile;
import org.terasology.engine.module.ModuleManager;
import org.terasology.input.Keyboard;
import org.terasology.input.device.KeyboardDevice;
import org.terasology.module.PathModule;
import org.terasology.naming.Name;
import org.terasology.registry.In;
import org.terasology.rendering.nui.Canvas;
import org.terasology.rendering.nui.CoreScreenLayer;
import org.terasology.rendering.nui.editor.systems.AbstractEditorSystem;
import org.terasology.rendering.nui.events.NUIKeyEvent;
import org.terasology.rendering.nui.layers.mainMenu.ConfirmPopup;
import org.terasology.rendering.nui.widgets.JsonEditorTreeView;
import org.terasology.rendering.nui.widgets.UITextEntry;
import org.terasology.rendering.nui.widgets.treeView.JsonTree;
import org.terasology.rendering.nui.widgets.treeView.JsonTreeConverter;
import org.terasology.rendering.nui.widgets.treeView.JsonTreeValue;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
/**
* A base screen for the NUI screen/skin editors.
*/
public abstract class AbstractEditorScreen extends CoreScreenLayer {
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* Used to get the {@link Path} of an asset.
*/
@In
private ModuleManager moduleManager;
/**
* The editor system.
*/
private AbstractEditorSystem editorSystem;
/**
* The editor widget.
*/
private JsonEditorTreeView editor;
/**
* Whether unsaved changes in the editor are present.
*/
private boolean unsavedChangesPresent;
/**
* Whether the autosave has been loaded.
*/
private boolean autosaveLoaded;
/**
* Whether autosaving (&loading autosaved files) should be disabled.
*/
private boolean disableAutosave;
/**
* Selects the current asset to be edited.
*
* @param urn The urn of the asset.
*/
public abstract void selectAsset(ResourceUrn urn);
/**
* Resets the editor's state based on a tree representation of an asset.
*
* @param node The node based on which the editor's state is to be reset.
*/
protected abstract void resetStateInternal(JsonTree node);
/**
* Resets the preview widget based on the editor's current state.
*/
protected abstract void resetPreviewWidget();
/**
* Updates the editor after state or configuration changes.
*/
protected abstract void updateConfig();
/**
* Initialises the widgets, screens etc. used to edit a specified node.
*
* @param node The node to be edited.
*/
protected abstract void editNode(JsonTree node);
/**
* Adds a widget selected by the user to the specified node.
*
* @param node The node to add a new widget to.
*/
protected abstract void addWidget(JsonTree node);
/**
* @return The path to the backup autosave file.
*/
protected abstract Path getAutosaveFile();
/**
* @return The currently selected asset (or alternative state, i.e. new screen)
*/
protected abstract String getSelectedAsset();
/**
* Sets the selected asset to a specific value.
*
* @param selectedAsset The value to set the selected asset to.
*/
protected abstract void setSelectedAsset(String selectedAsset);
/**
* Reset the stored path to an asset file based on an asset urn.
*
* @param urn The asset urn.
*/
protected abstract void setSelectedAssetPath(ResourceUrn urn);
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
// If the autosave is loaded in initialise(), the preview widget is updated before interface component
// sizes are set, which breaks the editor's layout.
// Therefore, the autosave is loaded after the first onDraw() call.
if (!autosaveLoaded) {
loadAutosave();
autosaveLoaded = true;
}
}
@Override
public boolean onKeyEvent(NUIKeyEvent event) {
if (event.isDown()) {
int id = event.getKey().getId();
KeyboardDevice keyboard = event.getKeyboard();
boolean ctrlDown = keyboard.isKeyDown(Keyboard.KeyId.RIGHT_CTRL) || keyboard.isKeyDown(Keyboard.KeyId.LEFT_CTRL);
if (id == Keyboard.KeyId.ESCAPE) {
editorSystem.toggleEditor();
return true;
} else if (ctrlDown && id == Keyboard.KeyId.Z) {
undo();
return true;
} else if (ctrlDown && id == Keyboard.KeyId.Y) {
redo();
return true;
} else {
return false;
}
}
return false;
}
@Override
public boolean isEscapeToCloseAllowed() {
// Escape to close is handled in onKeyEvent() to pass the editor's state to NUIEditorSystem.
return false;
}
/**
* Sets the editor's state based on the previous item in the editor widget's history.
*
* @see JsonEditorTreeView#undo()
*/
protected void undo() {
if (editor.undo()) {
resetPreviewWidget();
updateConfig();
}
}
/**
* Sets the editor's state based on the next item in the editor widget's history.
*
* @see JsonEditorTreeView#redo()
*/
protected void redo() {
if (editor.redo()) {
resetPreviewWidget();
updateConfig();
}
}
/**
* Resets the editor's state based on a specified {@link JsonTree}.
*
* @param node The {@link JsonTree} to reset the state from.
*/
protected void resetState(JsonTree node) {
if (unsavedChangesPresent) {
ConfirmPopup confirmPopup = getManager().pushScreen(ConfirmPopup.ASSET_URI, ConfirmPopup.class);
confirmPopup.setMessage("Unsaved changes!", "It looks like you've been editing something!" +
"\r\nAll unsaved changes will be lost. Continue anyway?");
confirmPopup.setOkHandler(() -> {
setUnsavedChangesPresent(false);
deleteAutosave();
resetStateInternal(node);
});
} else {
resetStateInternal(node);
}
}
/**
* Saves the contents of the editor as a JSON string to a specified file.
*
* @param file The file to save to.
*/
protected void saveToFile(File file) {
try (BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(file))) {
saveToFile(outputStream);
setUnsavedChangesPresent(false);
} catch (IOException e) {
logger.warn("Could not save asset", e);
}
}
/**
* Saves the contents of the editor as a JSON string to a specified file.
*
* @param path The path to save to.
*/
protected void saveToFile(Path path) {
try (BufferedOutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(path))) {
saveToFile(outputStream);
setUnsavedChangesPresent(false);
} catch (IOException e) {
logger.warn("Could not save asset", e);
}
}
/**
* Updates the autosave file with the current state of the tree.
*/
protected void updateAutosave() {
if (!disableAutosave) {
try (BufferedOutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(getAutosaveFile()))) {
JsonElement editorContents = JsonTreeConverter.deserialize(getEditor().getModel().getNode(0).getRoot());
JsonObject autosaveObject = new JsonObject();
autosaveObject.addProperty("selectedAsset", getSelectedAsset());
autosaveObject.add("editorContents", editorContents);
String jsonString = new GsonBuilder().setPrettyPrinting().create().toJson(autosaveObject);
outputStream.write(jsonString.getBytes());
} catch (IOException e) {
logger.warn("Could not save to autosave file", e);
}
}
}
/**
* Resets the editor based on the state of the autosave file.
*/
protected void loadAutosave() {
if (!disableAutosave) {
try (JsonReader reader = new JsonReader(new InputStreamReader(Files.newInputStream(getAutosaveFile())))) {
reader.setLenient(true);
String autosaveString = new JsonParser().parse(reader).toString();
JsonObject autosaveObject = new JsonParser().parse(autosaveString).getAsJsonObject();
String selectedAsset = autosaveObject.get("selectedAsset").getAsString();
setSelectedAsset(selectedAsset);
try {
ResourceUrn urn = new ResourceUrn(selectedAsset);
setSelectedAssetPath(urn);
} catch (InvalidUrnException ignored) {
}
JsonTree editorContents = JsonTreeConverter.serialize(autosaveObject.get("editorContents"));
resetState(editorContents);
setUnsavedChangesPresent(true);
} catch (NoSuchFileException ignored) {
} catch (IOException e) {
logger.warn("Could not load autosaved info", e);
}
}
}
/**
* Deletes the autosave file.
*/
protected void deleteAutosave() {
try {
Files.delete(getAutosaveFile());
} catch (NoSuchFileException ignored) {
} catch (IOException e) {
logger.warn("Could not delete autosave file", e);
}
}
private void saveToFile(BufferedOutputStream outputStream) throws IOException {
// Serialize tree contents and save to selected file.
JsonElement json = JsonTreeConverter.deserialize(getEditor().getModel().getNode(0).getRoot());
String jsonString = new GsonBuilder().setPrettyPrinting().create().toJson(json);
outputStream.write(jsonString.getBytes());
}
protected Path getPath(AssetDataFile source) {
List<String> path = source.getPath();
Name moduleName = new Name(path.get(0));
if (moduleManager.getEnvironment().get(moduleName) instanceof PathModule) {
path.add(source.getFilename());
String[] pathArray = path.toArray(new String[path.size()]);
// Copy all the elements after the first to a separate array for getPath().
String first = pathArray[0];
String[] more = Arrays.copyOfRange(pathArray, 1, pathArray.length);
return moduleManager.getEnvironment().getFileSystem().getPath(first, more);
}
return null;
}
/**
* De-serializes the current state of the editor and copies it to the system clipboard.
*/
protected void copyJson() {
if (getEditor().getModel() != null) {
// Deserialize the state of the editor to a JSON string.
JsonElement json = JsonTreeConverter.deserialize(getEditor().getModel().getNode(0).getRoot());
String jsonString = new GsonBuilder().setPrettyPrinting().create().toJson(json);
// Set the contents of the system clipboard to it.
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
try {
clipboard.setContents(new StringSelection(jsonString), null);
} catch (IllegalStateException e) {
logger.warn("Clipboard inaccessible.", e);
}
}
}
/**
* Attempts to serialize the system clipboard's contents - if successful,
* sets the current state of the editor to the serialized {@link JsonTree}.
*/
protected void pasteJson() {
// Get the clipboard contents.
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
Transferable t = clipboard.getContents(null);
// Attempt to convert them to a string.
String clipboardContents = null;
try {
if (t != null) {
clipboardContents = (String) t.getTransferData(DataFlavor.stringFlavor);
}
} catch (UnsupportedFlavorException | IOException e) {
logger.warn("Could not fetch clipboard contents.", e);
}
if (clipboardContents != null) {
try {
// Attempt to serialize them to a JsonTree and reset the editor state.
JsonElement json = new JsonParser().parse(clipboardContents);
JsonTree node = JsonTreeConverter.serialize(json);
resetState(node);
} catch (JsonSyntaxException | NullPointerException e) {
logger.warn("Could not construct a valid tree from clipboard contents.", e);
}
}
}
/**
* Sets the interface focus to the inline editor widget and selects a subset of its' contents.
*
* @param node The node that is currently being edited.
* @param inlineEditorEntry The inline editor widget.
*/
protected void focusInlineEditor(JsonTree node, UITextEntry inlineEditorEntry) {
getManager().setFocus(inlineEditorEntry);
inlineEditorEntry.resetValue();
if (node.getValue().getType() == JsonTreeValue.Type.KEY_VALUE_PAIR) {
// If the node is a key/value pair, select the value of the node.
if (node.getValue().getValue() instanceof String) {
inlineEditorEntry.setCursorPosition(node.getValue().getKey().length() + "\"\":\"".length(), true);
inlineEditorEntry.setCursorPosition(inlineEditorEntry.getText().length() - "\"".length(), false);
} else {
inlineEditorEntry.setCursorPosition(node.getValue().getKey().length() + "\"\":".length(), true);
inlineEditorEntry.setCursorPosition(inlineEditorEntry.getText().length(), false);
}
} else {
// Otherwise fully select the contents of the node.
inlineEditorEntry.setCursorPosition(0, true);
inlineEditorEntry.setCursorPosition(inlineEditorEntry.getText().length(), false);
}
}
protected void setEditorSystem(AbstractEditorSystem editorSystem) {
this.editorSystem = editorSystem;
}
protected JsonEditorTreeView getEditor() {
return this.editor;
}
protected void setEditor(JsonEditorTreeView editor) {
this.editor = editor;
}
protected boolean areUnsavedChangesPresent() {
return unsavedChangesPresent;
}
protected void setUnsavedChangesPresent(boolean unsavedChangesPresent) {
this.unsavedChangesPresent = unsavedChangesPresent;
}
protected void setDisableAutosave(boolean disableAutosave) {
this.disableAutosave = disableAutosave;
if (disableAutosave) {
deleteAutosave();
}
}
}