mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 20:44:08 +00:00
463 lines
20 KiB
Java
463 lines
20 KiB
Java
package noppes.npcs.client.gui.builder;
|
|
|
|
import cpw.mods.fml.relauncher.Side;
|
|
import cpw.mods.fml.relauncher.SideOnly;
|
|
import net.minecraft.client.gui.FontRenderer;
|
|
import net.minecraft.client.gui.GuiButton;
|
|
import net.minecraft.util.StatCollector;
|
|
import noppes.npcs.client.gui.util.GuiNPCInterface;
|
|
import noppes.npcs.client.gui.util.GuiNpcButton;
|
|
import noppes.npcs.client.gui.util.GuiNpcButtonYesNo;
|
|
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
|
import noppes.npcs.client.gui.util.GuiNpcTextField;
|
|
import noppes.npcs.client.gui.util.GuiScrollWindow;
|
|
import noppes.npcs.client.gui.util.SubGuiInterface;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* General-purpose builder that converts a list of {@link FieldDef} into positioned
|
|
* GUI widgets inside a {@link GuiScrollWindow}.
|
|
* <p>
|
|
* Uses {@link FieldDef#row(FieldDef, FieldDef)} for two-column layouts.
|
|
* Subclasses can override {@link #buildField} to handle custom field types.
|
|
*/
|
|
@SideOnly(Side.CLIENT)
|
|
public class GuiFieldBuilder {
|
|
|
|
// Layout defaults (2-column mode)
|
|
protected int contentRight = 330;
|
|
protected int colLLabel = 5;
|
|
protected int colLField = 75;
|
|
protected int colLWidth = 70;
|
|
protected int colRLabel = 178;
|
|
protected int colRField = 248;
|
|
protected int colRWidth = 70;
|
|
protected int rowHeight = 24;
|
|
protected int labelPadding = 8;
|
|
|
|
// Dependencies
|
|
protected final GuiNPCInterface parent;
|
|
protected GuiScrollWindow sw;
|
|
protected final FontRenderer fontRenderer;
|
|
|
|
// Scroll window ID
|
|
protected int scrollWindowId = 0;
|
|
|
|
// ID counters
|
|
protected int widgetId;
|
|
protected int clearId;
|
|
protected int labelId;
|
|
|
|
// Starting Y
|
|
protected int startY = 5;
|
|
protected int lastBuildY = 0;
|
|
|
|
// Output maps
|
|
protected final Map<Integer, FieldDef> buttonFieldMap = new HashMap<>();
|
|
protected final Map<Integer, FieldDef> textFieldMap = new HashMap<>();
|
|
protected final Map<Integer, FieldDef> clearFieldMap = new HashMap<>();
|
|
|
|
public GuiFieldBuilder(GuiNPCInterface parent, FontRenderer fontRenderer) {
|
|
this.parent = parent;
|
|
this.fontRenderer = fontRenderer;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// CONFIG CHAINING
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
public GuiFieldBuilder scrollWindowId(int id) { this.scrollWindowId = id; return this; }
|
|
public GuiFieldBuilder contentRight(int v) { this.contentRight = v; return this; }
|
|
public GuiFieldBuilder startIds(int widget, int clear, int label) {
|
|
this.widgetId = widget; this.clearId = clear; this.labelId = label; return this;
|
|
}
|
|
public GuiFieldBuilder startY(int y) { this.startY = y; return this; }
|
|
public GuiFieldBuilder rowHeight(int h) { this.rowHeight = h; return this; }
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// BUILD
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Creates a {@link GuiScrollWindow} at the given position/size, builds all fields into it,
|
|
* and sets maxScrollY automatically. The returned window is ready to use.
|
|
*/
|
|
public GuiScrollWindow buildScrollWindow(List<FieldDef> fields, int x, int y, int width, int height) {
|
|
this.sw = new GuiScrollWindow(parent, x, y, width, height, 0);
|
|
sw.backgroundColor = 0x88000000;
|
|
// Register with parent first — this calls initGui() which clears widget lists,
|
|
// so we must add widgets AFTER this point
|
|
parent.addScrollableGui(scrollWindowId, sw);
|
|
int finalY = build(fields);
|
|
sw.maxScrollY = Math.max(finalY - height, 0);
|
|
return sw;
|
|
}
|
|
|
|
/**
|
|
* Processes all fields and adds widgets to the scroll window.
|
|
* @return the final Y position after all fields
|
|
*/
|
|
protected int build(List<FieldDef> fields) {
|
|
buttonFieldMap.clear();
|
|
textFieldMap.clear();
|
|
clearFieldMap.clear();
|
|
|
|
int y = startY;
|
|
|
|
for (int i = 0; i < fields.size(); i++) {
|
|
FieldDef def = fields.get(i);
|
|
|
|
// Let subclasses handle custom types; returns -1 if not handled
|
|
int customResult = buildField(def, y, fields, i);
|
|
if (customResult >= 0) {
|
|
// Subclass handled it, check if index was advanced
|
|
int newIndex = getLastHandledIndex();
|
|
if (newIndex > i) i = newIndex;
|
|
y = customResult;
|
|
continue;
|
|
}
|
|
|
|
// Skip invisible fields
|
|
if (!def.isVisible()) continue;
|
|
|
|
// Section header
|
|
if (def.getType() == FieldType.SECTION_HEADER) {
|
|
y += 3;
|
|
GuiNpcLabel sectionLabel = new GuiNpcLabel(labelId++, def.getLabel(), colLLabel, y + 2, 0xFFFF55);
|
|
if (def.getHoverText() != null && !def.getHoverText().isEmpty()) {
|
|
sectionLabel.setHoverText(def.getHoverText());
|
|
}
|
|
sw.addLabel(sectionLabel);
|
|
y += 15;
|
|
continue;
|
|
}
|
|
|
|
// ROW: explicit two-column pair
|
|
if (def.getType() == FieldType.ROW) {
|
|
FieldDef left = def.getLeftChild();
|
|
FieldDef right = def.getRightChild();
|
|
boolean leftVis = left != null && left.isVisible();
|
|
boolean rightVis = right != null && right.isVisible();
|
|
|
|
if (leftVis && rightVis) {
|
|
renderFieldAt(left, colLLabel, colLField, colLWidth, y);
|
|
renderFieldAt(right, colRLabel, colRField, colRWidth, y);
|
|
} else if (leftVis) {
|
|
renderFullWidth(left, y);
|
|
} else if (rightVis) {
|
|
renderFullWidth(right, y);
|
|
}
|
|
if (leftVis || rightVis) y += rowHeight;
|
|
continue;
|
|
}
|
|
|
|
// Full-width field
|
|
renderFullWidth(def, y);
|
|
y += rowHeight;
|
|
}
|
|
|
|
lastBuildY = y;
|
|
return y;
|
|
}
|
|
|
|
/**
|
|
* Hook for subclasses to handle custom field types (e.g. EFFECTS_LIST).
|
|
* Return the new Y position if handled, or -1 if not handled (base class will process it).
|
|
* If handling advances the field index, call {@link #setLastHandledIndex(int)}.
|
|
*/
|
|
protected int buildField(FieldDef def, int y, List<FieldDef> fields, int index) {
|
|
return -1;
|
|
}
|
|
|
|
private int lastHandledIndex = -1;
|
|
protected void setLastHandledIndex(int index) { this.lastHandledIndex = index; }
|
|
protected int getLastHandledIndex() { return lastHandledIndex; }
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// FIELD RENDERING
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
|
protected void renderFieldAt(FieldDef def, int labelX, int fieldX, int fieldW, int y) {
|
|
GuiNpcLabel fieldLabel = new GuiNpcLabel(labelId++, def.getLabel(), labelX, y + 5, 0xFFFFFF);
|
|
String hover = def.getHoverText();
|
|
if (hover != null && !hover.isEmpty()) {
|
|
fieldLabel.setHoverText(hover);
|
|
}
|
|
sw.addLabel(fieldLabel);
|
|
|
|
switch (def.getType()) {
|
|
case FLOAT: {
|
|
float fVal = def.getValue() instanceof Number ? ((Number) def.getValue()).floatValue() : 0f;
|
|
GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, String.valueOf(fVal));
|
|
tf.setFloatsOnly();
|
|
if (def.hasRange()) tf.setMinMaxDefaultFloat(def.getMin(), def.getMax(), fVal);
|
|
if (!def.isEnabled()) tf.setEnabled(false);
|
|
if (hover != null) tf.setHoverText(hover);
|
|
sw.addTextField(tf);
|
|
textFieldMap.put(widgetId, def);
|
|
widgetId++;
|
|
clearId++;
|
|
break;
|
|
}
|
|
case INT: {
|
|
int iVal = def.getValue() instanceof Number ? ((Number) def.getValue()).intValue() : 0;
|
|
GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, String.valueOf(iVal));
|
|
tf.setIntegersOnly();
|
|
if (def.hasRange()) tf.setMinMaxDefault((int) def.getMin(), (int) def.getMax(), iVal);
|
|
if (!def.isEnabled()) tf.setEnabled(false);
|
|
if (hover != null) tf.setHoverText(hover);
|
|
sw.addTextField(tf);
|
|
textFieldMap.put(widgetId, def);
|
|
widgetId++;
|
|
clearId++;
|
|
break;
|
|
}
|
|
case STRING: {
|
|
String sVal = def.getValue() != null ? def.getValue().toString() : "";
|
|
GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, sVal);
|
|
if (!def.isEnabled()) tf.setEnabled(false);
|
|
if (hover != null) tf.setHoverText(hover);
|
|
sw.addTextField(tf);
|
|
textFieldMap.put(widgetId, def);
|
|
widgetId++;
|
|
clearId++;
|
|
break;
|
|
}
|
|
case LABEL: {
|
|
String text = def.getValue() != null ? def.getValue().toString() : "";
|
|
sw.addLabel(new GuiNpcLabel(labelId++, text, fieldX, y + 5, 0xAAAAAA));
|
|
widgetId++;
|
|
clearId++;
|
|
break;
|
|
}
|
|
case BOOLEAN: {
|
|
boolean bVal = def.getValue() instanceof Boolean ? (Boolean) def.getValue() : false;
|
|
GuiNpcButtonYesNo btn = new GuiNpcButtonYesNo(widgetId, fieldX, y, fieldW, 20, bVal);
|
|
if (!def.isEnabled()) btn.setEnabled(false);
|
|
if (hover != null) btn.setHoverText(hover);
|
|
sw.addButton(btn);
|
|
buttonFieldMap.put(widgetId, def);
|
|
widgetId++;
|
|
clearId++;
|
|
break;
|
|
}
|
|
case ENUM: {
|
|
Class<? extends Enum<?>> enumClass = def.getEnumClass();
|
|
if (enumClass != null) {
|
|
Enum<?>[] constants = enumClass.getEnumConstants();
|
|
String[] names = new String[constants.length];
|
|
for (int i = 0; i < constants.length; i++) names[i] = constants[i].toString();
|
|
int selected = def.getValue() instanceof Enum ? ((Enum<?>) def.getValue()).ordinal() : 0;
|
|
GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, fieldW, 20, names, selected);
|
|
if (!def.isEnabled()) btn.setEnabled(false);
|
|
if (hover != null) btn.setHoverText(hover);
|
|
sw.addButton(btn);
|
|
buttonFieldMap.put(widgetId, def);
|
|
}
|
|
widgetId++;
|
|
clearId++;
|
|
break;
|
|
}
|
|
case STRING_ENUM: {
|
|
String[] values = def.getStringEnumValues();
|
|
if (values != null && values.length > 0) {
|
|
String curVal = def.getStringEnumValue();
|
|
int selected = 0;
|
|
for (int i = 0; i < values.length; i++) {
|
|
if (values[i].equals(curVal)) { selected = i; break; }
|
|
}
|
|
GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, fieldW, 20, values, selected);
|
|
if (!def.isEnabled()) btn.setEnabled(false);
|
|
if (hover != null) btn.setHoverText(hover);
|
|
sw.addButton(btn);
|
|
buttonFieldMap.put(widgetId, def);
|
|
}
|
|
widgetId++;
|
|
clearId++;
|
|
break;
|
|
}
|
|
case SUB_GUI: {
|
|
String btnText = def.getButtonLabel();
|
|
int btnW;
|
|
if (def.hasClearAction()) {
|
|
btnW = fieldW - 22;
|
|
GuiNpcButton clearBtn = new GuiNpcButton(clearId, fieldX + fieldW - 20, y, 20, 20, "X");
|
|
sw.addButton(clearBtn);
|
|
clearFieldMap.put(clearId, def);
|
|
} else {
|
|
btnW = fieldW;
|
|
}
|
|
GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, btnW, 20, btnText);
|
|
Integer textColor = def.getButtonTextColor();
|
|
if (textColor != null) btn.setTextColor(textColor);
|
|
if (!def.isEnabled()) btn.setEnabled(false);
|
|
if (hover != null) btn.setHoverText(hover);
|
|
sw.addButton(btn);
|
|
buttonFieldMap.put(widgetId, def);
|
|
widgetId++;
|
|
clearId++;
|
|
break;
|
|
}
|
|
default:
|
|
// Unknown type — skip, incrementing IDs to stay in sync
|
|
widgetId++;
|
|
clearId++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void renderFullWidth(FieldDef def, int y) {
|
|
int fieldW = getFullFieldWidth(def);
|
|
String translated = StatCollector.translateToLocal(def.getLabel());
|
|
int labelW = fontRenderer.getStringWidth(translated);
|
|
int fieldX = colLLabel + labelW + labelPadding;
|
|
renderFieldAt(def, colLLabel, fieldX, fieldW, y);
|
|
}
|
|
|
|
protected int getFullFieldWidth(FieldDef def) {
|
|
switch (def.getType()) {
|
|
case FLOAT:
|
|
case INT: return 60;
|
|
case STRING: return 200;
|
|
case BOOLEAN: return 50;
|
|
case LABEL: return 60;
|
|
case ENUM:
|
|
case STRING_ENUM: return 120;
|
|
case SUB_GUI: return def.hasClearAction() ? 175 : 200;
|
|
default: return 80;
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// EVENT HANDLING
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Handles button events for declarative fields.
|
|
* Call from parent GUI's buttonEvent(). Returns true if the event was handled.
|
|
* <p>
|
|
* For SUB_GUI fields, the sub-gui is opened directly via {@code parent.setSubGuiWithResult()},
|
|
* so the result handler survives initGui() rebuilds. After this returns true, check
|
|
* {@code parent.hasSubGui()} to determine if initGui() should be called.
|
|
*/
|
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
|
public boolean handleButtonEvent(int buttonId, GuiButton button) {
|
|
// Check clear buttons first
|
|
if (clearFieldMap.containsKey(buttonId)) {
|
|
FieldDef def = clearFieldMap.get(buttonId);
|
|
if (def != null && def.hasClearAction()) {
|
|
def.getClearAction().run();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Check field buttons
|
|
FieldDef def = buttonFieldMap.get(buttonId);
|
|
if (def == null) return false;
|
|
|
|
switch (def.getType()) {
|
|
case BOOLEAN:
|
|
def.setValue(((GuiNpcButton) button).getValue() == 1);
|
|
return true;
|
|
case ENUM: {
|
|
int idx = ((GuiNpcButton) button).getValue();
|
|
Class<? extends Enum<?>> ec = def.getEnumClass();
|
|
if (ec != null) {
|
|
Enum<?>[] constants = ec.getEnumConstants();
|
|
if (idx >= 0 && idx < constants.length) def.setValue(constants[idx]);
|
|
}
|
|
return true;
|
|
}
|
|
case STRING_ENUM: {
|
|
int idx = ((GuiNpcButton) button).getValue();
|
|
String[] values = def.getStringEnumValues();
|
|
if (values != null && idx >= 0 && idx < values.length) def.setStringEnumValue(values[idx]);
|
|
return true;
|
|
}
|
|
case SUB_GUI:
|
|
if (def.getSubGuiFactory() != null) {
|
|
parent.setSubGuiWithResult(def.getSubGuiFactory().get(), sub -> {
|
|
if (def.getSubGuiResultHandler() != null) {
|
|
def.getSubGuiResultHandler().accept(sub);
|
|
}
|
|
});
|
|
}
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles text field unfocus events for declarative fields.
|
|
* Call from parent GUI's unFocused(). Returns true if the value actually changed.
|
|
*/
|
|
public boolean handleTextFieldEvent(int textFieldId, GuiNpcTextField field) {
|
|
FieldDef def = textFieldMap.get(textFieldId);
|
|
if (def == null) return false;
|
|
|
|
switch (def.getType()) {
|
|
case FLOAT:
|
|
try {
|
|
float newVal = Float.parseFloat(field.getText());
|
|
Object old = def.getValue();
|
|
float oldVal = old instanceof Number ? ((Number) old).floatValue() : 0f;
|
|
if (newVal != oldVal) {
|
|
def.setValue(newVal);
|
|
return true;
|
|
}
|
|
} catch (NumberFormatException ignored) {}
|
|
return false;
|
|
case INT:
|
|
try {
|
|
int newVal = Integer.parseInt(field.getText());
|
|
Object old = def.getValue();
|
|
int oldVal = old instanceof Number ? ((Number) old).intValue() : 0;
|
|
if (newVal != oldVal) {
|
|
def.setValue(newVal);
|
|
return true;
|
|
}
|
|
} catch (NumberFormatException ignored) {}
|
|
return false;
|
|
case STRING:
|
|
String newVal = field.getText();
|
|
Object old = def.getValue();
|
|
String oldVal = old != null ? old.toString() : "";
|
|
if (!newVal.equals(oldVal)) {
|
|
def.setValue(newVal);
|
|
return true;
|
|
}
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles sub-gui close events. Call from parent GUI's subGuiClosed().
|
|
* <p>
|
|
* Note: SUB_GUI fields opened via handleButtonEvent use
|
|
* {@code parent.setSubGuiWithResult()}, so their results are handled
|
|
* automatically by {@code GuiNPCInterface.closeSubGui()}. This method
|
|
* only handles sub-guis opened through other means.
|
|
*/
|
|
public boolean handleSubGuiClosed(SubGuiInterface subgui) {
|
|
return false;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// GETTERS
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
public Map<Integer, FieldDef> getButtonFieldMap() { return buttonFieldMap; }
|
|
public Map<Integer, FieldDef> getTextFieldMap() { return textFieldMap; }
|
|
public Map<Integer, FieldDef> getClearFieldMap() { return clearFieldMap; }
|
|
public GuiScrollWindow getScrollWindow() { return sw; }
|
|
public int getLastBuildY() { return lastBuildY; }
|
|
public int getNextLabelId() { return labelId; }
|
|
}
|