/* * 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.utils; import com.google.common.collect.Queues; import com.google.gson.annotations.SerializedName; import org.reflections.ReflectionUtils; import org.terasology.persistence.ModuleContext; import org.terasology.rendering.nui.NUIManager; import org.terasology.rendering.nui.UILayout; import org.terasology.rendering.nui.UIWidget; import org.terasology.rendering.nui.editor.layers.NUIEditorScreen; import org.terasology.rendering.nui.editor.layers.NUISkinEditorScreen; import org.terasology.rendering.nui.layouts.relative.RelativeLayout; import org.terasology.rendering.nui.skin.UIStyleFragment; import org.terasology.rendering.nui.widgets.treeView.JsonTree; import org.terasology.rendering.nui.widgets.treeView.JsonTreeValue; import org.terasology.utilities.ReflectionUtil; import java.lang.reflect.Field; import java.util.Deque; import java.util.List; import java.util.Optional; import java.util.Set; @SuppressWarnings("unchecked") public final class NUIEditorNodeUtils { private static final String SAMPLE_LABEL_TEXT = "Welcome to the Terasology NUI editor!\r\n" + "Visit https://github.com/Terasology/TutorialNui/wiki for a quick overview of the editor,\r\n" + "as well as the NUI framework itself."; private NUIEditorNodeUtils() { } /** * @return The {@link JsonTree} to be used as an initial screen template within {@link NUIEditorScreen}. */ public static JsonTree createNewScreen() { JsonTree tree = new JsonTree(new JsonTreeValue(null, null, JsonTreeValue.Type.OBJECT)); tree.addChild(new JsonTreeValue("type", "PlaceholderScreen", JsonTreeValue.Type.KEY_VALUE_PAIR)); tree.addChild(new JsonTreeValue("skin", "engine:default", JsonTreeValue.Type.KEY_VALUE_PAIR)); JsonTree layout = new JsonTree(new JsonTreeValue("contents", null, JsonTreeValue.Type.OBJECT)); layout.addChild(new JsonTreeValue("type", "RelativeLayout", JsonTreeValue.Type.KEY_VALUE_PAIR)); JsonTree contents = new JsonTree(new JsonTreeValue("contents", null, JsonTreeValue.Type.ARRAY)); JsonTree label = createNewWidget("UILabel", "sampleLabel", true); label.addChild(new JsonTreeValue("text", SAMPLE_LABEL_TEXT, JsonTreeValue.Type.KEY_VALUE_PAIR)); contents.addChild(label); layout.addChild(contents); tree.addChild(layout); return tree; } /** * @return The {@link JsonTree} to be used as an initial skin template within {@link NUISkinEditorScreen}. */ public static JsonTree createNewSkin() { JsonTree tree = new JsonTree(new JsonTreeValue(null, null, JsonTreeValue.Type.OBJECT)); tree.addChild(new JsonTreeValue("inherit", "default", JsonTreeValue.Type.KEY_VALUE_PAIR)); tree.addChild(new JsonTreeValue("elements", null, JsonTreeValue.Type.OBJECT)); tree.addChild(new JsonTreeValue("families", null, JsonTreeValue.Type.OBJECT)); return tree; } /** * @param type The type of the widget. * @param id The id of the widget. * @param addLayoutInfo Whether a few layout settings from {@link RelativeLayout} should be added. * @return The {@link JsonTree} with the given type/id to be used as an empty widget template within {@link NUIEditorScreen}. */ public static JsonTree createNewWidget(String type, String id, boolean addLayoutInfo) { JsonTree widget = new JsonTree(new JsonTreeValue(null, null, JsonTreeValue.Type.OBJECT)); widget.addChild(new JsonTreeValue("type", type, JsonTreeValue.Type.KEY_VALUE_PAIR)); widget.addChild(new JsonTreeValue("id", id, JsonTreeValue.Type.KEY_VALUE_PAIR)); JsonTree layoutInfo = new JsonTree(new JsonTreeValue("layoutInfo", null, JsonTreeValue.Type.OBJECT)); if (addLayoutInfo) { layoutInfo.addChild(new JsonTreeValue("width", 500, JsonTreeValue.Type.KEY_VALUE_PAIR)); JsonTree hPosition = new JsonTree(new JsonTreeValue("position-horizontal-center", null, JsonTreeValue.Type .OBJECT)); JsonTree vPosition = new JsonTree(new JsonTreeValue("position-vertical-center", null, JsonTreeValue.Type .OBJECT)); layoutInfo.addChild(hPosition); layoutInfo.addChild(vPosition); } widget.addChild(layoutInfo); return widget; } private static Deque<JsonTree> getPathToRoot(JsonTree node) { Deque<JsonTree> pathToRoot = Queues.newArrayDeque(); // Create a stack with the root node at the top and the argument at the bottom. JsonTree currentNode = node; while (!currentNode.isRoot()) { pathToRoot.push(currentNode); currentNode = (JsonTree) currentNode.getParent(); } pathToRoot.push(currentNode); return pathToRoot; } /** * @param node A node in an asset tree. * @param nuiManager The {@link NUIManager} to be used for widget type resolution. * @return The info about this node's type. */ public static NodeInfo getNodeInfo(JsonTree node, NUIManager nuiManager) { Deque<JsonTree> pathToRoot = getPathToRoot(node); // Start iterating from top to bottom. Class currentClass = null; Class activeLayoutClass = null; for (JsonTree n : pathToRoot) { if (n.isRoot()) { // currentClass is not set - set it to the screen type. String type = (String) n.getChildWithKey("type").getValue().getValue(); currentClass = nuiManager .getWidgetMetadataLibrary() .resolve(type, ModuleContext.getContext()) .getType(); } else { if (List.class.isAssignableFrom(currentClass) && n.getValue().getKey() == null && "contents".equals(n.getParent().getValue().getKey())) { // Transition from a "contents" list to a UIWidget. currentClass = UIWidget.class; } else { // Retrieve the type of an unspecified UIWidget. if (currentClass == UIWidget.class && n.hasSiblingWithKey("type")) { String type = (String) n.getSiblingWithKey("type").getValue().getValue(); currentClass = nuiManager .getWidgetMetadataLibrary() .resolve(type, ModuleContext.getContext()) .getType(); } // If the current class is a layout, remember its' value (but do not set until later on!) Class layoutClass = null; if (UILayout.class.isAssignableFrom(currentClass)) { layoutClass = currentClass; } if (UILayout.class.isAssignableFrom(currentClass) && "contents".equals(n.getValue().getKey())) { // "contents" fields of a layout are always (widget) lists. currentClass = List.class; } else if (UIWidget.class.isAssignableFrom(currentClass) && "layoutInfo".equals(n.getValue().getKey())) { // Set currentClass to the layout hint type for the active layout. currentClass = (Class) ReflectionUtil.getTypeParameter(activeLayoutClass.getGenericSuperclass(), 0); } else { String value = n.getValue().toString(); Set<Field> fields = ReflectionUtils.getAllFields(currentClass); Optional<Field> newField = fields .stream().filter(f -> f.getName().equalsIgnoreCase(value)).findFirst(); if (newField.isPresent()) { currentClass = newField.get().getType(); } else { Optional<Field> serializedNameField = fields .stream() .filter(f -> f.isAnnotationPresent(SerializedName.class) && f.getAnnotation(SerializedName.class).value().equals(value)).findFirst(); if (serializedNameField.isPresent()) { currentClass = serializedNameField.get().getType(); } else { return null; } } } // Set the layout class value. if (layoutClass != null) { activeLayoutClass = layoutClass; } } } } // If the final result is a generic UIWidget, attempt to retrieve its' type. if (currentClass == UIWidget.class && node.hasChildWithKey("type")) { String type = (String) node.getChildWithKey("type").getValue().getValue(); currentClass = nuiManager .getWidgetMetadataLibrary() .resolve(type, ModuleContext.getContext()) .getType(); } return new NodeInfo(currentClass, activeLayoutClass); } /** * @param node A node in an asset tree. * @return The info about this node's type. */ public static NodeInfo getSkinNodeInfo(JsonTree node) { Deque<JsonTree> pathToRoot = getPathToRoot(node); // Start iterating from top to bottom. Class nodeClass = null; for (JsonTree n : pathToRoot) { if (n.isRoot()) { nodeClass = UIStyleFragment.class; } else { if ("elements".equals(n.getValue().getKey()) || "families".equals(n.getValue().getKey())) { nodeClass = null; } else if (n.getParent().getValue().getKey() != null && ("elements".equals(n.getParent().getValue().getKey()) || "families".equals(n.getParent().getValue().getKey()))) { nodeClass = UIStyleFragment.class; } else { String value = n.getValue().toString(); Set<Field> fields = ReflectionUtils.getAllFields(nodeClass); Optional<Field> newField = fields .stream().filter(f -> f.getName().equalsIgnoreCase(value)).findFirst(); if (newField.isPresent()) { nodeClass = newField.get().getType(); } else { Optional<Field> serializedNameField = fields .stream() .filter(f -> f.isAnnotationPresent(SerializedName.class) && f.getAnnotation(SerializedName.class).value().equals(value)).findFirst(); if (serializedNameField.isPresent()) { nodeClass = serializedNameField.get().getType(); } else { return null; } } } } } return new NodeInfo(nodeClass, null); } /** * Contains information about a node's types. */ public static class NodeInfo { /** * The type of the field this node represents. */ private Class nodeClass; /** * The type of the layout this node is a part of. null if it's not a part of a layout. */ private Class layoutClass; public NodeInfo(Class nodeClass, Class layoutClass) { this.nodeClass = nodeClass; this.layoutClass = layoutClass; } public Class getNodeClass() { return nodeClass; } public Class getLayoutClass() { return layoutClass; } } }