FancyMenu Wiki/自定义元素

关于

你可以通过FancyMenu API为FancyMenu编写拓展模组实现自定义元素。

准备开发环境

请参阅 准备工作区

添加新元素

每个自定义元素(或item,因为元素在内部被称为此)都需要封装到CustomizationItemContainer中。

在模组初始化时,这个容器需要注册到CustomizationItemRegistry

创建Item

实际上item的类是CustomizationItem。这个类是渲染它并存储其所有属性的项。

给你的项创建一个CustomizationItem的新子类,自己的孩子好好起名嗷。

在这里,我会将我的演示用项类命名为ExampleCustomizationItem

在这个类中有两个很重要的东西。

构造方法(Constructor)

第一个就是这个类的构造方法。

在这里,你需要反序列化 已保存/序列化 的项实例的属性。并需要把它设置为新的真实例。

保存到布局时,项会被序列化为PropertiesSections

你只关心(你自己)你的自定义项的值就行。每一项的基本的长宽高坐标部分啥的都默认让超类整完了。

//The constuctor is used to de-serialize the PropertiesSection and set all of its values to the new real item instance.
//构造方法用于反序列化PropertiesSection并将其所有值设置为新的真项实例。
public ExampleCustomizationItem(CustomizationItemContainer parentContainer, PropertiesSection item) {

    //The superclass will automatically get values like orientation, x pos, y pos, width and height and will set it to the real item instance.
    //超类会自动获取长宽高位置坐标等将它们设置到真项实例中。
    super(parentContainer, item);

    //Getting a custom property from the serialized item instance and set it to the real instance
    //从序列化的项实例中获取自定义属性并将其设置为真实例。
    String someProperty = item.getEntryValue("saved_property");
    if (someProperty != null) {
        this.someField = someProperty;
    }

}

render()

还有一件事,你得渲染你的项。

这个方法是你的项的灵魂,使用这个方法来改变你的项的可视部分,就那个一会你可以在菜单看见的。

所以就在这里做你的事,知道吗?

这里唯一真正必要的部分是将此方法中的所有内容封装到shouldRender()中。

这个方法检查可视化需求和其他重要内容,所以请别把这里忘了捏。

@Override
public void render(PoseStack matrix, Screen menu) throws IOException {

    //This is really important and should be in every item render method, to check for visibility requirements and more.
    //为了检查可视化需求这应该在所有的项渲染方法中。
    if (this.shouldRender()) {

        //Always use getPosX() and getPosY() to get the X and Y positions of the item.
        //就用getPosX()和getPosY()方法获取项的X、Y位置就行。
        //The fields posX and posY aren't the final position, just the base pos without the orientation!
        //这里的posX和posY字段不是指最终位置,而只是没有方向的基本pos!
        int x = this.getPosX(menu);
        int y = this.getPosY(menu);

        //We want to use placeholder text values for our 'someField' string, so we use the DynamicValueHelper to convert them,
        //我们想为我们的"someField"字符串使用占位符文本值,使用DynamicValueHelper来转换即可,
        //but they should look like placeholders in the editor, so we only convert them when not in the editor.
        //但在编辑器中它们应该看着像占位符,所以我们不在编辑器中时转换它们。
        String text;
        if (!isEditorActive()) {
            text = DynamicValueHelper.convertFromRaw(this.someField);
        } else {
            text = StringUtils.convertFormatCodes(this.someField, "&", "§");
        }

        //Always try to make your items' opacity changeable by setting the 'opacity' field!
        //你可以尝试设置"opacity"来改变项的不透明度。
        //This field is set by the "delay appearance" feature to control the fade-in opacity.
        //该字段由"delay appearance"功能设置以控制淡入不透明度。
        drawString(matrix, Minecraft.getInstance().font, text, x + 10, y + 10, -1 | Mth.ceil(this.opacity * 255.0F) << 24);

    }

}

完整示例

这里是完整的自定义项示例。

package de.keksuccino.fancymenu.api.item.example;

import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import de.keksuccino.fancymenu.api.item.CustomizationItem;
import de.keksuccino.fancymenu.api.item.CustomizationItemContainer;
import de.keksuccino.fancymenu.menu.fancy.DynamicValueHelper;
import de.keksuccino.konkrete.input.StringUtils;
import de.keksuccino.konkrete.properties.PropertiesSection;
import de.keksuccino.konkrete.rendering.RenderUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.util.Mth;

import java.awt.*;
import java.io.IOException;

public class ExampleCustomizationItem extends CustomizationItem {

    public String displayText = "placeholder";
    public String backgroundColorString = "#38ff38";
    public Color backgroundColor = new Color(56, 255, 56);

    //The constuctor is used to de-serialize the PropertiesSection and set all of its values to the new real item instance.
    //构造方法用于反序列化PropertiesSection并将其所有值设置为新的真项实例。
    public ExampleCustomizationItem(CustomizationItemContainer parentContainer, PropertiesSection item) {

        //The superclass will automatically get values like orientation, x pos, y pos, width and height and will set it to the real item instance.
        //超类会自动获取长宽高位置坐标等将它们设置到真项实例中。
        super(parentContainer, item);

        //Getting the background HEX color from the serialized item
        //从序列化项中获取背景HEX颜色。
        String backColorHex = item.getEntryValue("background_color");
        if (backColorHex != null) {
            Color c = RenderUtils.getColorFromHexString(backColorHex);
            if (c != null) {
                this.backgroundColor = c;
                this.backgroundColorString = backColorHex;
            }
        }

        //Getting the display text string from the serialized item
        //从序列化项中获取显示文本字符串。
        String dText = item.getEntryValue("display_text");
        if (dText != null) {
            this.displayText = dText;
        }

    }

    @Override
    public void render(PoseStack matrix, Screen menu) throws IOException {

        //This is really important and should be in every item render method, to check for visibility requirements and more.
        //为了检查可视化需求这应该在所有的项渲染方法中。
        if (this.shouldRender()) {

            //Always use getPosX() and getPosY() to get the X and Y positions of the item.
            //就用getPosX()和getPosY()方法获取项的X、Y位置就行。
            //The fields posX and posY aren't the final position, just the base pos without the orientation!
            //这里的posX和posY字段不是指最终位置,而只是没有方向的基本pos!
            int x = this.getPosX(menu);
            int y = this.getPosY(menu);

            RenderSystem.enableBlend();

            //Rendering the background color as background of the item.
            //将背景颜色渲染为项的背景。
            fill(matrix, x, y, x + this.getWidth(), y + this.getHeight(), this.backgroundColor.getRGB() | Mth.ceil(this.opacity * 255.0F) << 24);

            //Rendering the display text to the upper-left side of the item
            //将显示文本渲染到项的左上角。
            if (this.displayText != null) {
                //We want to use placeholder text values for the display text, so we use the DynamicValueHelper to convert them,
                //我们想使用占位符文本值作为显示文本,使用DynamicValueHelper来转换即可,
                //but they should look like placeholders in the editor, so we only convert them when not in the editor.
                //但在编辑器中它们应该看着像占位符,所以我们不在编辑器中时转换它们。
                String text;
                if (!isEditorActive()) {
                    text = DynamicValueHelper.convertFromRaw(this.displayText);
                } else {
                    text = StringUtils.convertFormatCodes(this.displayText, "&", "§");
                }
                //The 'opacity' field is used to set the fade-in opacity of the item when the "delay appearance" option is enabled for it.
                //'opacity'字段用于设置项在启用“delay appearance”选项时的淡入不透明度。
                //Always try to make your items' opacity changeable by setting the 'opacity' field! (I also used it in the fill method for the background)
                ////你可以尝试设置"opacity"来改变项的不透明度。(我还在背景的填充方法中使用了它)
                drawString(matrix, Minecraft.getInstance().font, text, x + 10, y + 10, -1 | Mth.ceil(this.opacity * 255.0F) << 24);
            }

        }

    }

}

创建编辑器元素

LayoutEditorElements包含在布局编辑器中对你的项重要的所有内容。

这包括与你的项相关的所有UI部分,例如说在编辑器中右键单击项时的右键菜单。

给你的项起个合适的名字并创建一个LayoutEditorElement的新子类。

接下来,我会将我的样例编辑器元素类命名为ExampleLayoutEditorElement

在这个类中有两种重要方法需要你留意一下。

init()

第一个,由编辑器调用以初始化你的编辑器元素(主要用于初始化UI)。

每一个编辑器元素都有rightclickMenu(右键菜单)字段。这是元素的右键右键菜单。

这里的右键菜单已包含所有的默认内容,比如设置方向、删除元素等。

要是你想在这编辑你的项的自定义值,那需要在添加一个新的entries到那个右键菜单并链接到你的项。

编辑器元素类的对象字段是你的实际CustomizationItem实例,只需将其转换为你的项的子类(在本例中为ExampleCustomizationItem)即可使用其自定义字段和方法。

@Override
public void init() {

    //The superclass adds basic stuff to the right-click context menu, like visibility requirement controls, delete controls, orientation, etc.
    //这里的超类为右键菜单添加了些基础功能,比如可视化需求控件,删除控件,方向等。
    super.init();

    //The 'object' field holds the CustomizationItem instance of this element.
    //"object"字段包含这个元素的CustomizationItem实例。
    //Cast it to your own item class, to get and set your own fields.
    //把他转换成你自己的项类,以便于获取/设置你自己的字段。
    ExampleCustomizationItem i = ((ExampleCustomizationItem)this.object);

    //This button will be part of the right-click context menu of the element and is used to change the 'someField' field of the CustomizationItem subclass.
    //这个按钮将会成为元素右键菜单的一部分,以便于更改CustomizationItem子类的'someField'字段。
    AdvancedButton someFieldButton = new AdvancedButton(0, 0, 0, 0, "Set Some Field", (press) -> {
        //This is the basic input popup for text content, used in many parts of FancyMenu.
        //这是文本内容的基本输入弹出框,在FancyMenu的很多部分都有使用。
        FMTextInputPopup pop = new FMTextInputPopup(new Color(0, 0, 0, 0), "Set Some Field Content", null, 240, (callback) -> {
            //The callback of popups will be null, when pressing ESC in it to force-close it.
            //当按下ESC强制关闭窗口时,窗口回调值为空。
            if (callback != null) {
                if (!callback.equals(i.someField)) {
                    //Create a snapshot before every change, so you can undo the change in the editor (using CTRL + Z)
                    //在每次更改前生成一个快照,顺便你也可以回退在编辑器中的更改(Ctrl+Z)
                    this.handler.history.saveSnapshot(this.handler.history.createSnapshot());
                    //Now set the new value to the item instance
                    //现在捏,给项实例设置新值。
                    i.someField = callback;
                }
            }
        });
        //Set the current value as default text of the text input popup
        //设置现有的值为文本输入框的默认文本。
        if (i.someField != null) {
            pop.setText(i.someField);
        }
        //Open the popup
        PopupHandler.displayPopup(pop);
    });
    someFieldButton.setDescription("This is just an example button tooltip.");
    //Add the button to the right-click context menu content
    //添加按钮到右键菜单内容中。
    this.rightclickMenu.addContent(someFieldButton);

}

serializeItem()

简单但也重要,第二个重要方法在保存布局时被调用。

在这里,你的项实例(对象字段)被序列化为SimplePropertiesSection以将其属性保存在布局文件中。

你只关心(你自己)你的自定义项的值就行。每一项的基本的长宽高坐标部分啥的都默认添加到了序列化实例中。

@Override
public SimplePropertiesSection serializeItem() {

    ExampleCustomizationItem i = ((ExampleCustomizationItem)this.object);

    SimplePropertiesSection sec = new SimplePropertiesSection();

    //Add your custom item values here, so they get saved and can later be de-serialized again.
    //在这添加你的自定义项的值,以至于它们被保存后可以再次反序列化。
    sec.addEntry("saved_property", i.someField);

    return sec;

}

完整样例

这里是LayoutEditorElement的完整样例。

package de.keksuccino.fancymenu.api.item.example;

import de.keksuccino.fancymenu.api.item.LayoutEditorElement;
import de.keksuccino.fancymenu.menu.fancy.helper.DynamicValueInputPopup;
import de.keksuccino.fancymenu.menu.fancy.helper.layoutcreator.LayoutEditorScreen;
import de.keksuccino.fancymenu.menu.fancy.helper.ui.popup.FMTextInputPopup;
import de.keksuccino.konkrete.gui.content.AdvancedButton;
import de.keksuccino.konkrete.gui.screens.popup.PopupHandler;
import de.keksuccino.konkrete.rendering.RenderUtils;

import java.awt.*;

public class ExampleLayoutEditorElement extends LayoutEditorElement {

    public ExampleLayoutEditorElement(ExampleCustomizationItemContainer parentContainer, ExampleCustomizationItem customizationItemInstance, LayoutEditorScreen handler) {
        super(parentContainer, customizationItemInstance, true, handler, true);
    }

    @Override
    public void init() {

        //The superclass adds basic stuff to the right-click context menu, like visibility requirement controls, delete controls, orientation, etc.
        //这里的超类为右键菜单添加了些基础功能,比如可视化需求控件,删除控件,方向等。
        super.init();

        //The 'object' field holds the CustomizationItem instance of this element.
        //"object"字段包含这个元素的CustomizationItem实例。
        //Cast it to your own item class, to get and set your own fields.
        //把他转换成你自己的项类,以便于获取/设置你自己的字段。
        ExampleCustomizationItem i = ((ExampleCustomizationItem)this.object);

        //This button will be part of the right-click context menu of the element and is uses to change the background color value of the item.
        //这个按钮将成为元素右键菜单的一部分,用于更改项的背景颜色值。
        AdvancedButton backgroundColorButton = new AdvancedButton(0, 0, 0, 0, "Background Color", (press) -> {
            //This is the basic input popup for text content, used in many parts of FancyMenu.
            //这是文本内容的基本输入弹出框,在FancyMenu的很多部分都有使用。
            FMTextInputPopup pop = new FMTextInputPopup(new Color(0, 0, 0, 0), "Background Color HEX", null, 240, (callback) -> {
                //The callback of popups will be null, when pressing ESC in it to force-close it.
                //当按下ESC强制关闭窗口时,窗口回调值为空。
                if (callback != null) {
                    if (!callback.equals(i.backgroundColorString)) {
                        Color c = RenderUtils.getColorFromHexString(callback);
                        if (c != null) {
                            //Create a snapshot before every change, so you can undo the change in the editor (using CTRL + Z)
                            //在每次更改前生成一个快照,顺便你也可以回退在编辑器中的更改(Ctrl+Z)
                            this.handler.history.saveSnapshot(this.handler.history.createSnapshot());
                            //Now set the new values to the item instance
                            //现在捏,给项实例设置新值。
                            i.backgroundColorString = callback;
                            i.backgroundColor = c;
                        }
                    }
                }
            });
            //Set the current value as default text of the text input popup
            //设置现有的值为文本输入框的默认文本。
            if (i.backgroundColorString != null) {
                pop.setText(i.backgroundColorString);
            }
            //Open the popup
            //打开弹出框。
            PopupHandler.displayPopup(pop);
        });
        backgroundColorButton.setDescription("This is just an example button tooltip.");
        //Add the button to the right-click context menu content
        //添加按钮到右键菜单内容中。
        this.rightclickMenu.addContent(backgroundColorButton);

        //This is the button to change the display text of the item. Will also be part of the right-click context menu.
        //这个是更改项显示的文本的按钮,也会成为右键菜单的一部分。
        AdvancedButton displayTextButton = new AdvancedButton(0, 0, 0, 0, "Display Text", (press) -> {
            //This is also a text input popup, but with placeholder text value support (the little icon at the right side of the input field)
            //这也是文本弹出框,但是支持占位符文本值。
            DynamicValueInputPopup pop = new DynamicValueInputPopup(new Color(0, 0, 0, 0), "Set Display Text", null, 240, (callback) -> {
                if (callback != null) {
                    if (!callback.equals(i.displayText)) {
                        //Again, save a snapshot before changing something!
                        //再来一次,在改变之前保存快照。
                        this.handler.history.saveSnapshot(this.handler.history.createSnapshot());
                        //Setting the new display text value
                        //设置新的显示文本值。
                        i.displayText = callback;
                    }
                }
            });
            if (i.displayText != null) {
                pop.setText(i.displayText);
            }
            PopupHandler.displayPopup(pop);
        });
        this.rightclickMenu.addContent(displayTextButton);

    }

    @Override
    public SimplePropertiesSection serializeItem() {

        ExampleCustomizationItem i = ((ExampleCustomizationItem)this.object);

        SimplePropertiesSection sec = new SimplePropertiesSection();

        //Add your custom item values here, so they get saved and can later be de-serialized again.
        sec.addEntry("background_color", i.backgroundColorString);
        sec.addEntry("display_text", i.displayText);

        return sec;

    }

}

创建Container

我们需要的最后的类是CustomizationItemContainer的子类。

这个容器是CustomizationItemsLayoutEditorElements的基础实例Builder。

给你的项创建CustomizationItemContainer子类并起合适的名字。

接下来,我会将我的示例容器类命名为ExampleCustomizationItemContainer

Item Identifier

项容器的构造方法需要唯一的标识符。(像是sfz)

至于怎么让你的标识符唯一,我也不赘述了。

你应该在子类构造方法里直接设置标识符。

public ExampleCustomizationItemContainer() {
    super("example_item_identifier");
}

Instance Builders

项容器用于构建你的CustomizationItem子类和LayoutEditorElement子类的实例。

你需要设置Builder的方法,以便容器可以构建子类的实例。

//This will construct a default instance of your CustomizationItem without any customizations made to it.
//这里将会构造一个你的CustomizationItem默认实例,而且不对其进行任何自定义。
@Override
public CustomizationItem constructDefaultItemInstance() {
    //Just use an empty properties section here.
    //这里只是用了空属性section。
    //Make sure that your CustomizationItem accepts empty sections without throwing errors!
    //请确保你的CustomizationItem能接受空section还不报错。
    ExampleCustomizationItem i = new ExampleCustomizationItem(this, new PropertiesSection("dummy"));
    //The default size of 10x10 would be a bit too small for the item, so I set a new default size of 100x100 to the default instance.
    //默认10*10的大小可能对于项来说有点过小了,所以我把10*10拉到了100*100。
    //This means that now every new item of this type will have a size of 100x100 by default.
    //就是说,咱现在所有这类型的新项默认大小都是100*100了。
    i.width = 100;
    i.height = 100;
    return i;
}
//This will construct a customized instance of your CustomizationItem, using the given PropertiesSection (serialized item) to set all customizations.
//这将构造您的CustomizationItem 的自定义实例,使用给定的PropertiesSection(序列化项)来设置所有自定义项。
@Override
public CustomizationItem constructCustomizedItemInstance(PropertiesSection serializedItem) {
    return new ExampleCustomizationItem(this, serializedItem);
}

//This will construct a new instance of your LayoutEditorElement, used in the layout editor.
//这里将会为你的LayoutEditorElement构建用在布局编辑器的实例。
@Override
public LayoutEditorElement constructEditorElementInstance(CustomizationItem item, LayoutEditorScreen handler) {
    return new ExampleLayoutEditorElement(this, (ExampleCustomizationItem) item, handler);
}

Display Name

你应该为你的项设置显示名称。

这个显示名会用在布局编辑器中。

这个方法允许你填入本地化键作为显示名称。

@Override
public String getDisplayName() {
    return "Example Item";
}

描述

将鼠标悬停在按钮上以在布局编辑器中添加此类型的新项时,会显示项的描述。

@Override
public String[] getDescription() {
    return new String[] {
        "This is a description",
        "with 2 lines of text."
    };
}

完整示例

这里是CustomizationItemContainer的一个完整样例。

package de.keksuccino.fancymenu.api.item.example;

import de.keksuccino.fancymenu.api.item.CustomizationItem;
import de.keksuccino.fancymenu.api.item.CustomizationItemContainer;
import de.keksuccino.fancymenu.api.item.LayoutEditorElement;
import de.keksuccino.fancymenu.menu.fancy.helper.layoutcreator.LayoutEditorScreen;
import de.keksuccino.konkrete.properties.PropertiesSection;

//This needs to be registered to the CustomizationItemRegistry at mod init
//在模组初始化这里需要被注册到CustomizationItemRegistry。
public class ExampleCustomizationItemContainer extends CustomizationItemContainer {

    public ExampleCustomizationItemContainer() {
        super("example_item_identifier");
    }

    @Override
    public CustomizationItem constructDefaultItemInstance() {
        ExampleCustomizationItem i = new ExampleCustomizationItem(this, new PropertiesSection("dummy"));
        //The default size of 10x10 would be a bit too small for the item, so I set a new default size of 100x100 to the default instance.
        //默认10*10的大小可能对于项来说有点过小了,所以我把10*10拉到了100*100。
        //This means that now every new item of this type will have a size of 100x100 by default.
        //就是说,咱现在所有这类型的新项默认大小都是100*100了。
        i.width = 100;
        i.height = 100;
        return i;
    }

    @Override
    public CustomizationItem constructCustomizedItemInstance(PropertiesSection serializedItem) {
        return new ExampleCustomizationItem(this, serializedItem);
    }

    @Override
    public LayoutEditorElement constructEditorElementInstance(CustomizationItem item, LayoutEditorScreen handler) {
        return new ExampleLayoutEditorElement(this, (ExampleCustomizationItem) item, handler);
    }

    @Override
    public String getDisplayName() {
        return "Example Item";
    }

    @Override
    public String[] getDescription() {
        return new String[] {
                "This is a description",
                "with 2 lines of text."
        };
    }

}

注册Container

你就快完成了,嗯...最后一步,人类的一大步。

FancyMenu需要认识你的自定义项,所以你需要让你的拓展模组初始化时注册你的CustomizationItemContainerCustomizationItemRegistery

package de.keksuccino.fancymenu;

import net.minecraftforge.fml.common.Mod;
import de.keksuccino.fancymenu.api.item.CustomizationItemRegistry;

@Mod("modid")
public class ExampleModMainClass {

    public ExampleModMainClass() {
        try {

            //Register your CustomizationItemContainer to the CustomizationItemRegistry at mod init.
            //初始化时注册你的CustomizationItemContainer到CustomizationItemRegistery。
            CustomizationItemRegistry.registerItem(new ExampleCustomizationItemContainer());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

现在你可以使用你自己的布局元素了!