Commit 7a3302ea607753ffa64012c3b61a2923222647ba

Authored by Mumfrey
1 parent aa7b247d

Add text field support to AbstractConfigPanel, closes #32

src/client/java/com/mumfrey/liteloader/modconfig/AbstractConfigPanel.java
... ... @@ -6,16 +6,23 @@
6 6 package com.mumfrey.liteloader.modconfig;
7 7  
8 8 import java.util.ArrayList;
  9 +import java.util.Arrays;
9 10 import java.util.List;
  11 +import java.util.regex.Pattern;
10 12  
11 13 import org.lwjgl.input.Keyboard;
12 14  
  15 +import com.mumfrey.liteloader.client.gui.GuiLiteLoaderPanel;
13 16 import com.mumfrey.liteloader.client.mixin.IGuiButton;
14 17  
15 18 import net.minecraft.client.Minecraft;
  19 +import net.minecraft.client.gui.FontRenderer;
16 20 import net.minecraft.client.gui.GuiButton;
17 21 import net.minecraft.client.gui.GuiLabel;
18 22 import net.minecraft.client.gui.GuiScreen;
  23 +import net.minecraft.client.gui.GuiTextField;
  24 +import net.minecraft.client.renderer.RenderHelper;
  25 +import net.minecraft.client.resources.I18n;
19 26  
20 27 /**
21 28 * A general-purpose base class for mod config panels which implements a lot of
... ... @@ -42,74 +49,361 @@ public abstract class AbstractConfigPanel implements ConfigPanel
42 49 }
43 50  
44 51 /**
45   - * Struct which keeps a control together with its callback object
  52 + * A handle to an option text field, used to get and retrieve text and set
  53 + * the max length. It is possible to obtain the native text field as well,
  54 + * however caution should be used when doing so to avoid breaking the
  55 + * contract of the text field wrapper used in the config panel itself.
  56 + */
  57 + public interface ConfigTextField
  58 + {
  59 + /**
  60 + * Get the inner text field
  61 + */
  62 + public abstract GuiTextField getNativeTextField();
  63 +
  64 + /**
  65 + * Get the text field's text
  66 + */
  67 + public abstract String getText();
  68 +
  69 + /**
  70 + * Set the text field's text
  71 + *
  72 + * @param text text to set
  73 + * @return fluent interface
  74 + */
  75 + public abstract ConfigTextField setText(String text);
  76 +
  77 + /**
  78 + * Set a validation regex for this text box.
  79 + *
  80 + * @param regex Validation regex to use for this text field
  81 + * @param force If set to <tt>false</tt>, invalid values will only cause
  82 + * the text field to display an error when invalid text is present.
  83 + * If set to <tt>true</tt>, invalid values will be forcibly
  84 + * prohibited from being entered.
  85 + * @return fluent interfaces
  86 + */
  87 + public abstract ConfigTextField setRegex(String regex, boolean force);
  88 +
  89 + /**
  90 + * If the validation regex is not set, always returns true. Otherwise
  91 + * returns true if the current text value matches the validation regex.
  92 + *
  93 + * @return validation state of the current text value
  94 + */
  95 + public abstract boolean isValid();
  96 +
  97 + /**
  98 + * Set the max allowed string length, defaults to 32
46 99 *
47   - * @param <T> control type
  100 + * @param maxLength max string length to use
  101 + * @return fluent interface
  102 + */
  103 + public abstract ConfigTextField setMaxLength(int maxLength);
  104 + }
  105 +
  106 + /**
  107 + * Base for config option handle structs
48 108 */
49   - class ConfigOption<T extends GuiButton>
  109 + static abstract class ConfigOption
50 110 {
51   - final GuiLabel label;
52   - final T control;
53   - final ConfigOptionListener<T> listener;
  111 + void onTick()
  112 + {
  113 + }
  114 +
  115 + abstract void draw(Minecraft minecraft, int mouseX, int mouseY, float partialTicks);
  116 +
  117 + void mouseReleased(Minecraft minecraft, int mouseX, int mouseY)
  118 + {
  119 + }
  120 +
  121 + boolean mousePressed(Minecraft minecraft, int mouseX, int mouseY)
  122 + {
  123 + return false;
  124 + }
54 125  
55   - ConfigOption(GuiLabel label)
  126 + boolean keyPressed(Minecraft minecraft, char keyChar, int keyCode)
  127 + {
  128 + return false;
  129 + }
  130 + }
  131 +
  132 + /**
  133 + * Struct for labels
  134 + */
  135 + static class ConfigOptionLabel extends ConfigOption
  136 + {
  137 + private final GuiLabel label;
  138 +
  139 + ConfigOptionLabel(GuiLabel label)
56 140 {
57 141 this.label = label;
58   - this.control = null;
59   - this.listener = null;
60 142 }
61 143  
62   - ConfigOption(T control, ConfigOptionListener<T> listener)
  144 + @Override
  145 + void draw(Minecraft minecraft, int mouseX, int mouseY, float partialTicks)
  146 + {
  147 + this.label.drawLabel(minecraft, mouseX, mouseY);
  148 + }
  149 + }
  150 +
  151 + /**
  152 + * Struct which keeps a control together with its callback object
  153 + *
  154 + * @param <T> control type
  155 + */
  156 + static class ConfigOptionButton<T extends GuiButton> extends ConfigOption
  157 + {
  158 + private final T control;
  159 + private final ConfigOptionListener<T> listener;
  160 +
  161 + ConfigOptionButton(T control, ConfigOptionListener<T> listener)
63 162 {
64   - this.label = null;
65 163 this.control = control;
66 164 this.listener = listener;
67 165 }
68 166  
  167 + @Override
69 168 void draw(Minecraft minecraft, int mouseX, int mouseY, float partialTicks)
70 169 {
71   - if (this.label != null)
  170 + this.control.drawButton(minecraft, mouseX, mouseY);
  171 + }
  172 +
  173 + @Override
  174 + boolean mousePressed(Minecraft minecraft, int mouseX, int mouseY)
  175 + {
  176 + if (this.control.mousePressed(minecraft, mouseX, mouseY))
  177 + {
  178 + this.control.playPressSound(minecraft.getSoundHandler());
  179 + if (this.listener != null)
  180 + {
  181 + this.listener.actionPerformed(this.control);
  182 + }
  183 + return true;
  184 + }
  185 +
  186 + return false;
  187 + }
  188 +
  189 + @Override
  190 + void mouseReleased(Minecraft minecraft, int mouseX, int mouseY)
  191 + {
  192 + this.control.mouseReleased(mouseX, mouseY);
  193 + }
  194 + }
  195 +
  196 + /**
  197 + * Struct for text fields
  198 + */
  199 + class ConfigOptionTextField extends ConfigOption implements ConfigTextField
  200 + {
  201 + /**
  202 + * List for accessing via tab order
  203 + */
  204 + private final List<GuiConfigTextField> tabOrder;
  205 +
  206 + /**
  207 + * Tab index
  208 + */
  209 + private final int tabIndex;
  210 +
  211 + /**
  212 + * Inner text field
  213 + */
  214 + private final GuiConfigTextField textField;
  215 +
  216 + ConfigOptionTextField(List<GuiConfigTextField> tabOrder, GuiConfigTextField textField)
  217 + {
  218 + this.tabOrder = tabOrder;
  219 + this.tabIndex = tabOrder.indexOf(textField);
  220 + this.textField = textField;
  221 +
  222 + if (this.tabIndex == 0)
72 223 {
73   - this.label.drawLabel(minecraft, mouseX, mouseY);
  224 + textField.setFocused(true);
  225 + }
74 226 }
75 227  
76   - if (this.control != null)
  228 + @Override
  229 + void onTick()
77 230 {
78   - this.control.drawButton(minecraft, mouseX, mouseY);
  231 + this.textField.updateCursorCounter();
79 232 }
  233 +
  234 + @Override
  235 + void draw(Minecraft minecraft, int mouseX, int mouseY, float partialTicks)
  236 + {
  237 + this.textField.drawTextBox(mouseX, mouseY);
80 238 }
81 239  
  240 + @Override
82 241 boolean mousePressed(Minecraft minecraft, int mouseX, int mouseY)
83 242 {
84   - if (this.control != null && this.control.mousePressed(minecraft, mouseX, mouseY))
  243 + this.textField.mouseClicked(mouseX, mouseY, 0);
  244 + if (this.textField.isFocused())
85 245 {
86   - this.control.playPressSound(minecraft.getSoundHandler());
87   - if (this.listener != null)
  246 + // Unfocus all other text fields
  247 + for (GuiTextField textField : this.tabOrder)
88 248 {
89   - this.listener.actionPerformed(this.control);
  249 + if (textField != this.textField)
  250 + {
  251 + textField.setFocused(false);
  252 + }
90 253 }
  254 + }
  255 +
  256 + return false;
  257 + }
  258 +
  259 + @Override
  260 + boolean keyPressed(Minecraft minecraft, char keyChar, int keyCode)
  261 + {
  262 + if (!this.textField.isFocused())
  263 + {
  264 + return false;
  265 + }
  266 +
  267 + if (keyCode == Keyboard.KEY_TAB)
  268 + {
  269 + this.textField.setFocused(false);
  270 + int tabOrderSize = this.tabOrder.size();
  271 + this.tabOrder.get((this.tabIndex + (GuiScreen.isShiftKeyDown() ? -1 : 1) + tabOrderSize) % tabOrderSize).setFocused(true);
91 272 return true;
92 273 }
93 274  
  275 + return this.textField.textboxKeyTyped(keyChar, keyCode);
  276 + }
  277 +
  278 + @Override
  279 + public GuiTextField getNativeTextField()
  280 + {
  281 + return this.textField;
  282 + }
  283 +
  284 + @Override
  285 + public String getText()
  286 + {
  287 + return this.textField.getText();
  288 + }
  289 +
  290 + @Override
  291 + public ConfigTextField setText(String text)
  292 + {
  293 + this.textField.setText(text);
  294 + return this;
  295 + }
  296 +
  297 + @Override
  298 + public ConfigTextField setMaxLength(int maxLength)
  299 + {
  300 + this.textField.setMaxStringLength(maxLength);
  301 + return this;
  302 + }
  303 +
  304 + @Override
  305 + public ConfigTextField setRegex(String regex, boolean force)
  306 + {
  307 + this.textField.setRegex(Pattern.compile(regex), force);;
  308 + return this;
  309 + }
  310 +
  311 + @Override
  312 + public boolean isValid()
  313 + {
  314 + return this.textField.isValid();
  315 + }
  316 + }
  317 +
  318 + /**
  319 + * Custom text field which supports "soft" validation by regex (draws red
  320 + * border and error message when invalid)
  321 + */
  322 + class GuiConfigTextField extends GuiTextField
  323 + {
  324 + private final FontRenderer fontRenderer;
  325 + private final int width, height;
  326 + private Pattern regex;
  327 + private boolean valid, drawing;
  328 +
  329 + GuiConfigTextField(int id, FontRenderer fontRenderer, int x, int y, int width, int height)
  330 + {
  331 + super(id, fontRenderer, x, y, width, height);
  332 + this.fontRenderer = fontRenderer;
  333 + this.width = width;
  334 + this.height = height;
  335 + this.setRegex(null, false);
  336 + }
  337 +
  338 + void setRegex(Pattern regex, boolean restrict)
  339 + {
  340 + if (restrict && regex != null)
  341 + {
  342 + this.setValidator((text) -> regex.matcher(text).matches());
  343 + this.regex = null;
  344 + }
  345 + else
  346 + {
  347 + this.setValidator((text) -> {
  348 + this.validate(text);
  349 + return true;
  350 + });
  351 + this.regex = regex;
  352 + this.validate(this.getText());
  353 + }
  354 + }
  355 +
  356 + private boolean validate(String text)
  357 + {
  358 + this.valid = (this.regex == null || this.regex.matcher(text).matches());
  359 + return true;
  360 + }
  361 +
  362 + boolean isValid()
  363 + {
  364 + return this.valid;
  365 + }
  366 +
  367 + @Override
  368 + public boolean getEnableBackgroundDrawing()
  369 + {
  370 + boolean bg = super.getEnableBackgroundDrawing();
  371 + if (bg && this.drawing && !this.isValid())
  372 + {
  373 + drawRect(this.xPosition - 1, this.yPosition - 1, this.xPosition + this.width + 1, this.yPosition + this.height + 1, 0xFFFF5555);
  374 + drawRect(this.xPosition, this.yPosition, this.xPosition + this.width, this.yPosition + this.height, 0xFF000000);
94 375 return false;
95 376 }
96 377  
97   - void mouseReleased(Minecraft mc, int mouseX, int mouseY)
  378 + return bg;
  379 + }
  380 +
  381 + public void drawTextBox(int mouseX, int mouseY)
98 382 {
99   - if (this.control != null)
  383 + this.drawing = true;
  384 + super.drawTextBox();
  385 + if (!this.isValid())
  386 + {
  387 + drawRect(this.xPosition + this.width - 10, this.yPosition, this.xPosition + this.width, this.yPosition + this.height, 0x66000000);
  388 + this.fontRenderer.drawString("\247l!", this.xPosition + this.width - 6, this.yPosition + (this.height / 2) - 4, 0xFFFF5555);
  389 + if (mouseX >= this.xPosition && mouseX < this.xPosition + this.width && mouseY >= this.yPosition && mouseY < this.yPosition + this.height)
100 390 {
101   - this.control.mouseReleased(mouseX, mouseY);
  391 + AbstractConfigPanel.this.drawHoveringText(I18n.format("gui.invalidvalue"), mouseX, mouseY);
  392 + }
102 393 }
  394 + this.drawing = false;
103 395 }
104 396 }
105 397  
106 398 protected final Minecraft mc;
107 399  
108   - private final List<ConfigOption<?>> options = new ArrayList<ConfigOption<?>>();
  400 + private final List<ConfigOption> options = new ArrayList<ConfigOption>();
  401 +
  402 + private final List<GuiConfigTextField> textFields = new ArrayList<GuiConfigTextField>();
109 403  
110 404 private int contentHeight = 0;
111 405  
112   - private ConfigOption<?> selected;
  406 + private ConfigOption selected;
113 407  
114 408 public AbstractConfigPanel()
115 409 {
... ... @@ -170,7 +464,7 @@ public abstract class AbstractConfigPanel implements ConfigPanel
170 464 label.addLine(line);
171 465 }
172 466 this.contentHeight = Math.max(y + height, this.contentHeight);
173   - this.options.add(new ConfigOption<GuiButton>(label));
  467 + this.options.add(new ConfigOptionLabel(label));
174 468 }
175 469  
176 470 /**
... ... @@ -185,12 +479,33 @@ public abstract class AbstractConfigPanel implements ConfigPanel
185 479 if (control != null)
186 480 {
187 481 this.contentHeight = Math.max(control.yPosition + ((IGuiButton)control).getButtonHeight(), this.contentHeight);
188   - this.options.add(new ConfigOption<T>(control, listener));
  482 + this.options.add(new ConfigOptionButton<T>(control, listener));
189 483 }
190 484  
191 485 return control;
192 486 }
193 487  
  488 + /**
  489 + * Add a text field to the panel, returns a handle through which the created
  490 + * text field can be accessed
  491 + *
  492 + * @param id control id
  493 + * @param x text field x position
  494 + * @param y text field y position
  495 + * @param width text field width
  496 + * @param height text field height
  497 + * @return text field handle
  498 + */
  499 + protected ConfigTextField addTextField(int id, int x, int y, int width, int height)
  500 + {
  501 + GuiConfigTextField textField = new GuiConfigTextField(id, this.mc.fontRendererObj, x + 2, y, width, height);
  502 + this.textFields.add(textField);
  503 +
  504 + ConfigOptionTextField configOption = new ConfigOptionTextField(this.textFields, textField);
  505 + this.options.add(configOption);
  506 + return configOption;
  507 + }
  508 +
194 509 @Override
195 510 public void onPanelResize(ConfigPanelHost host)
196 511 {
... ... @@ -199,12 +514,16 @@ public abstract class AbstractConfigPanel implements ConfigPanel
199 514 @Override
200 515 public void onTick(ConfigPanelHost host)
201 516 {
  517 + for (ConfigOption configOption : this.options)
  518 + {
  519 + configOption.onTick();
  520 + }
202 521 }
203 522  
204 523 @Override
205 524 public void drawPanel(ConfigPanelHost host, int mouseX, int mouseY, float partialTicks)
206 525 {
207   - for (ConfigOption<?> configOption : this.options)
  526 + for (ConfigOption configOption : this.options)
208 527 {
209 528 configOption.draw(this.mc, mouseX, mouseY, partialTicks);
210 529 }
... ... @@ -219,7 +538,7 @@ public abstract class AbstractConfigPanel implements ConfigPanel
219 538 return;
220 539 }
221 540  
222   - for (ConfigOption<?> configOption : this.options)
  541 + for (ConfigOption configOption : this.options)
223 542 {
224 543 if (configOption.mousePressed(this.mc, mouseX, mouseY))
225 544 {
... ... @@ -251,5 +570,22 @@ public abstract class AbstractConfigPanel implements ConfigPanel
251 570 host.close();
252 571 return;
253 572 }
  573 +
  574 + for (ConfigOption configOption : this.options)
  575 + {
  576 + if (configOption.keyPressed(this.mc, keyChar, keyCode))
  577 + {
  578 + break;
  579 + }
  580 + }
  581 + }
  582 +
  583 + protected final void drawHoveringText(String text, int x, int y)
  584 + {
  585 + if (this.mc.currentScreen != null)
  586 + {
  587 + GuiLiteLoaderPanel.drawTooltip(this.mc.fontRendererObj, text, x, y, Integer.MAX_VALUE, Integer.MAX_VALUE, 0xFFFF5555, 0xB0000000);
  588 + RenderHelper.disableStandardItemLighting();
  589 + }
254 590 }
255 591 }
... ...
src/main/resources/assets/liteloader/lang/en_us.lang
... ... @@ -91,4 +91,6 @@ gui.error.copytoclipboard=Copy error to clipboard
91 91 gui.error.tooltip=%d mod startup error(s) detected (%d critical)
92 92  
93 93 gui.notifications.updateavailable=LiteLoader Update Available!
94   -gui.notifications.newsnapshotavailable=!!New Snapshot Available:%nBuild #%s (%s)
95 94 \ No newline at end of file
  95 +gui.notifications.newsnapshotavailable=!!New Snapshot Available:%nBuild #%s (%s)
  96 +
  97 +gui.invalidvalue=§cInvalid value!
96 98 \ No newline at end of file
... ...