/* * 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.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.gson.JsonElement; import com.google.gson.JsonParser; import com.google.gson.stream.JsonReader; import org.codehaus.plexus.util.ExceptionUtils; 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.assets.management.AssetManager; import org.terasology.config.Config; import org.terasology.config.NUIEditorConfig; import org.terasology.engine.paths.PathManager; import org.terasology.registry.In; import org.terasology.rendering.nui.UIWidget; import org.terasology.rendering.nui.WidgetUtil; import org.terasology.rendering.nui.asset.UIElement; import org.terasology.rendering.nui.asset.UIFormat; import org.terasology.rendering.nui.databinding.Binding; import org.terasology.rendering.nui.databinding.ReadOnlyBinding; import org.terasology.rendering.nui.editor.systems.NUIEditorSystem; import org.terasology.rendering.nui.editor.utils.NUIEditorItemRenderer; import org.terasology.rendering.nui.editor.utils.NUIEditorMenuTreeBuilder; import org.terasology.rendering.nui.editor.utils.NUIEditorNodeUtils; import org.terasology.rendering.nui.editor.utils.NUIEditorTextEntryBuilder; import org.terasology.rendering.nui.itemRendering.ToStringTextRenderer; import org.terasology.rendering.nui.widgets.JsonEditorTreeView; import org.terasology.rendering.nui.widgets.UIBox; import org.terasology.rendering.nui.widgets.UIButton; import org.terasology.rendering.nui.widgets.UIDropdownScrollable; import org.terasology.rendering.nui.widgets.UILabel; 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 javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.LookAndFeel; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.filechooser.FileNameExtensionFilter; import java.awt.Component; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; /** * The main NUI editor screen. * Contains file selection, editing & preview widgets. */ @SuppressWarnings("unchecked") public final class NUIEditorScreen extends AbstractEditorScreen { public static final ResourceUrn ASSET_URI = new ResourceUrn("engine:nuiEditorScreen"); // Editor widget identifiers. private static final String AVAILABLE_ASSETS_ID = "availableAssets"; private static final String EDITOR_TREE_VIEW_ID = "editor"; private static final String SELECTED_SCREEN_ID = "selectedScreen"; private static final String CREATE_NEW_SCREEN = "New Screen"; private Logger logger = LoggerFactory.getLogger(NUIEditorScreen.class); /** * Used to retrieve & dispose of {@link UIElement} assets. */ @In private AssetManager assetManager; /** * Used to read from and write to {@link NUIEditorConfig} */ @In private Config config; /** * Used to toggle the editor screen on ESCAPE. */ @In private NUIEditorSystem nuiEditorSystem; /** * The box used to preview a NUI screen modified by the editor. */ private UIBox selectedScreenBox; /** * The Urn of the currently edited asset. */ private String selectedAsset; /** * The Urn of the asset that will be selected after a response to a user prompt. */ private String selectedAssetPending; /** * The path to the currently selected asset. Null if no path for the asset exists. */ private Path selectedAssetPath; /** * The widget used as an inline node editor. */ private UITextEntry<JsonTree> inlineEditorEntry; /** * An alternative locale to be used for screen rendering. */ private Locale alternativeLocale; @Override public void initialise() { // Retrieve the widgets based on their identifiers. UIDropdownScrollable<String> availableAssetDropdown = find(AVAILABLE_ASSETS_ID, UIDropdownScrollable.class); JsonEditorTreeView editor = find(EDITOR_TREE_VIEW_ID, JsonEditorTreeView.class); selectedScreenBox = find(SELECTED_SCREEN_ID, UIBox.class); super.setEditorSystem(nuiEditorSystem); super.setEditor(editor); // Populate the list of screens. List<String> availableAssetList = Lists.newArrayList(); availableAssetList.add(CREATE_NEW_SCREEN); availableAssetList.addAll(assetManager.getAvailableAssets(UIElement.class).stream().map(Object::toString).collect(Collectors.toList())); Collections.sort(availableAssetList); if (availableAssetDropdown != null) { availableAssetDropdown.setOptions(availableAssetList); availableAssetDropdown.bindSelection(new Binding<String>() { @Override public String get() { return selectedAsset; } @Override public void set(String value) { // Construct a new screen tree (or de-serialize from an existing asset) selectedAssetPending = value; if (CREATE_NEW_SCREEN.equals(value)) { selectedAssetPath = null; resetState(NUIEditorNodeUtils.createNewScreen()); } else { selectAsset(new ResourceUrn(value)); } } }); } if (editor != null) { editor.subscribeTreeViewUpdate(() -> { getEditor().addToHistory(); resetPreviewWidget(); updateConfig(); setUnsavedChangesPresent(true); updateAutosave(); }); editor.setContextMenuTreeProducer(node -> { NUIEditorMenuTreeBuilder nuiEditorMenuTreeBuilder = new NUIEditorMenuTreeBuilder(); nuiEditorMenuTreeBuilder.setManager(getManager()); nuiEditorMenuTreeBuilder.putConsumer(NUIEditorMenuTreeBuilder.OPTION_COPY, getEditor()::copyNode); nuiEditorMenuTreeBuilder.putConsumer(NUIEditorMenuTreeBuilder.OPTION_PASTE, getEditor()::pasteNode); nuiEditorMenuTreeBuilder.putConsumer(NUIEditorMenuTreeBuilder.OPTION_EDIT, this::editNode); nuiEditorMenuTreeBuilder.putConsumer(NUIEditorMenuTreeBuilder.OPTION_DELETE, getEditor()::deleteNode); nuiEditorMenuTreeBuilder.putConsumer(NUIEditorMenuTreeBuilder.OPTION_ADD_WIDGET, this::addWidget); nuiEditorMenuTreeBuilder.subscribeAddContextMenu(n -> { getEditor().fireUpdateListeners(); // Automatically edit a node that's been added. if (n.getValue().getType() == JsonTreeValue.Type.KEY_VALUE_PAIR) { getEditor().getModel().getNode(getEditor().getSelectedIndex()).setExpanded(true); getEditor().getModel().resetNodes(); getEditor().setSelectedIndex(getEditor().getModel().indexOf(n)); editNode(n); } }); return nuiEditorMenuTreeBuilder.createPrimaryContextMenu(node); }); editor.setEditor(this::editNode, getManager()); } UIButton save = find("save", UIButton.class); save.bindEnabled(new ReadOnlyBinding<Boolean>() { @Override public Boolean get() { return CREATE_NEW_SCREEN.equals(selectedAsset) || areUnsavedChangesPresent(); } }); save.subscribe(button -> { // Save the current look and feel. LookAndFeel currentLookAndFeel = UIManager.getLookAndFeel(); // (Temporarily) set the look and feel to the system default. try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | UnsupportedLookAndFeelException ignored) { } // Configure the file chooser. JFileChooser fileChooser = new JFileChooser() { @Override protected JDialog createDialog(Component parent) { JDialog dialog = super.createDialog(parent); dialog.setLocationByPlatform(true); dialog.setAlwaysOnTop(true); return dialog; } }; fileChooser.setSelectedFile(new File(CREATE_NEW_SCREEN.equals(selectedAsset) ? "untitled.ui" : selectedAsset.split(":")[1] + ".ui")); fileChooser.setFileFilter(new FileNameExtensionFilter("UI asset file (*.ui)", "ui")); if (fileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) { saveToFile(fileChooser.getSelectedFile()); deleteAutosave(); } // Reload the look and feel. try { UIManager.setLookAndFeel(currentLookAndFeel); } catch (UnsupportedLookAndFeelException ignored) { } }); UIButton override = find("override", UIButton.class); override.bindEnabled(new ReadOnlyBinding<Boolean>() { @Override public Boolean get() { return selectedAssetPath != null && areUnsavedChangesPresent(); } }); override.subscribe(button -> { saveToFile(selectedAssetPath); deleteAutosave(); }); // Set the handlers for the editor buttons. WidgetUtil.trySubscribe(this, "settings", button -> getManager().pushScreen(NUIEditorSettingsScreen.ASSET_URI, NUIEditorSettingsScreen.class)); WidgetUtil.trySubscribe(this, "copy", button -> copyJson()); WidgetUtil.trySubscribe(this, "paste", button -> pasteJson()); WidgetUtil.trySubscribe(this, "undo", button -> undo()); WidgetUtil.trySubscribe(this, "redo", button -> redo()); WidgetUtil.trySubscribe(this, "close", button -> nuiEditorSystem.toggleEditor()); updateConfig(); } /** * {@inheritDoc} */ @Override public void selectAsset(ResourceUrn urn) { boolean isLoaded = assetManager.isLoaded(urn, UIElement.class); Optional<UIElement> asset = assetManager.getAsset(urn, UIElement.class); if (asset.isPresent()) { UIElement element = asset.get(); if (!isLoaded) { asset.get().dispose(); } AssetDataFile source = element.getSource(); String content = null; try (JsonReader reader = new JsonReader(new InputStreamReader(source.openStream(), Charsets.UTF_8))) { reader.setLenient(true); content = new JsonParser().parse(reader).toString(); } catch (IOException e) { logger.error(String.format("Could not load asset source file for %s", urn.toString()), e); } if (content != null) { JsonTree node = JsonTreeConverter.serialize(new JsonParser().parse(content)); selectedAssetPending = urn.toString(); resetState(node); } } } /** * {@inheritDoc} */ @Override protected void resetStateInternal(JsonTree node) { getEditor().setTreeViewModel(node, true); resetPreviewWidget(); getEditor().clearHistory(); updateConfig(); selectedAsset = selectedAssetPending; try { ResourceUrn urn = new ResourceUrn(selectedAsset); setSelectedAssetPath(urn); } catch (InvalidUrnException ignored) { } } /** * {@inheritDoc} */ @Override public void resetPreviewWidget() { try { // Serialize the editor's contents and update the widget. JsonElement element = JsonTreeConverter.deserialize(getEditor().getRoot()); UIWidget widget = new UIFormat().load(element, alternativeLocale).getRootWidget(); selectedScreenBox.setContent(widget); } catch (Throwable t) { String truncatedStackTrace = Joiner.on(System.lineSeparator()) .join(Arrays.copyOfRange(ExceptionUtils.getStackFrames(t), 0, 10)); selectedScreenBox.setContent(new UILabel(truncatedStackTrace)); } } /** * {@inheritDoc} */ @Override protected void updateConfig() { NUIEditorConfig nuiEditorConfig = config.getNuiEditor(); setDisableAutosave(nuiEditorConfig.isDisableAutosave()); // Update the editor's item renderer. getEditor().setItemRenderer(nuiEditorConfig.isDisableIcons() ? new ToStringTextRenderer<>() : new NUIEditorItemRenderer(getEditor().getModel())); // Update the alternative locale. If it has been updated, change the preview widget's locale. if (nuiEditorConfig.getAlternativeLocale() != null && !nuiEditorConfig.getAlternativeLocale().equals(alternativeLocale)) { alternativeLocale = nuiEditorConfig.getAlternativeLocale(); if (selectedAsset != null) { resetPreviewWidget(); } } } /** * {@inheritDoc} */ @Override protected void editNode(JsonTree node) { Class nodeClass = null; try { Class parentClass = NUIEditorNodeUtils.getNodeInfo((JsonTree) node.getParent(), getManager()).getNodeClass(); if (parentClass != null) { nodeClass = parentClass.getDeclaredField(node.getValue().getKey()).getType(); } } catch (NullPointerException | NoSuchFieldException ignored) { } if (nodeClass != null && Enum.class.isAssignableFrom(nodeClass)) { // If the node is an enum, initialize and show the enum editor screen. getManager().pushScreen(EnumEditorScreen.ASSET_URI, EnumEditorScreen.class); EnumEditorScreen enumEditorScreen = (EnumEditorScreen) getManager() .getScreen(EnumEditorScreen.ASSET_URI); enumEditorScreen.setNode(node); enumEditorScreen.setEnumClass(nodeClass); enumEditorScreen.subscribeClose(() -> getEditor().fireUpdateListeners()); } else { JsonTreeValue.Type type = node.getValue().getType(); // Create the inline editor depending on the node's type. inlineEditorEntry = null; if (type == JsonTreeValue.Type.VALUE) { inlineEditorEntry = NUIEditorTextEntryBuilder.createValueEditor(); } else if (type == JsonTreeValue.Type.KEY_VALUE_PAIR) { inlineEditorEntry = NUIEditorTextEntryBuilder.createKeyValueEditor(); } else if (type == JsonTreeValue.Type.OBJECT && !(!node.isRoot() && node.getParent().getValue().getType() == JsonTreeValue.Type.ARRAY)) { inlineEditorEntry = NUIEditorTextEntryBuilder.createObjectEditor(); } else if (type == JsonTreeValue.Type.ARRAY) { inlineEditorEntry = NUIEditorTextEntryBuilder.createArrayEditor(); } if (inlineEditorEntry != null) { inlineEditorEntry.bindValue(new Binding<JsonTree>() { @Override public JsonTree get() { return node; } @Override public void set(JsonTree value) { if (value != null) { node.setValue(value.getValue()); getEditor().fireUpdateListeners(); } } }); getEditor().setAlternativeWidget(inlineEditorEntry); focusInlineEditor(node, inlineEditorEntry); } } } /** * {@inheritDoc} */ @Override protected void addWidget(JsonTree node) { getManager().pushScreen(WidgetSelectionScreen.ASSET_URI, WidgetSelectionScreen.class); // Initialise and show the widget selection screen. WidgetSelectionScreen widgetSelectionScreen = (WidgetSelectionScreen) getManager() .getScreen(WidgetSelectionScreen.ASSET_URI); widgetSelectionScreen.setNode(node); widgetSelectionScreen.subscribeClose(() -> { node.setExpanded(true); JsonTree widget = node.getChildAt(node.getChildren().size() - 1); widget.setExpanded(true); getEditor().fireUpdateListeners(); // Automatically edit the id of a newly added widget. getEditor().getModel().resetNodes(); getEditor().setSelectedIndex(getEditor().getModel().indexOf(widget.getChildWithKey("id"))); editNode(widget.getChildWithKey("id")); }); } /** * {@inheritDoc} */ @Override protected Path getAutosaveFile() { return PathManager.getInstance().getHomePath().resolve("nuiEditorAutosave.json"); } /** * {@inheritDoc} */ @Override protected String getSelectedAsset() { return selectedAsset; } /** * {@inheritDoc} */ @Override protected void setSelectedAsset(String selectedAsset) { this.selectedAsset = selectedAsset; // Also prevent the asset being reset. this.selectedAssetPending = selectedAsset; } /** * {@inheritDoc} */ @Override protected void setSelectedAssetPath(ResourceUrn urn) { boolean isLoaded = assetManager.isLoaded(urn, UIElement.class); Optional<UIElement> asset = assetManager.getAsset(urn, UIElement.class); if (asset.isPresent()) { UIElement element = asset.get(); if (!isLoaded) { asset.get().dispose(); } AssetDataFile source = element.getSource(); selectedAssetPath = getPath(source); } } }