Commit 7b19b205199773d775ab7f7c42bfc7f9c5db7659

Authored by Mumfrey
1 parent 7940c007

Add text field support to AbstractConfigPanel, closes #32

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