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,16 +6,23 @@
6 package com.mumfrey.liteloader.modconfig; 6 package com.mumfrey.liteloader.modconfig;
7 7
8 import java.util.ArrayList; 8 import java.util.ArrayList;
  9 +import java.util.Arrays;
9 import java.util.List; 10 import java.util.List;
  11 +import java.util.regex.Pattern;
10 12
11 import org.lwjgl.input.Keyboard; 13 import org.lwjgl.input.Keyboard;
12 14
  15 +import com.mumfrey.liteloader.client.gui.GuiLiteLoaderPanel;
13 import com.mumfrey.liteloader.client.mixin.IGuiButton; 16 import com.mumfrey.liteloader.client.mixin.IGuiButton;
14 17
15 import net.minecraft.client.Minecraft; 18 import net.minecraft.client.Minecraft;
  19 +import net.minecraft.client.gui.FontRenderer;
16 import net.minecraft.client.gui.GuiButton; 20 import net.minecraft.client.gui.GuiButton;
17 import net.minecraft.client.gui.GuiLabel; 21 import net.minecraft.client.gui.GuiLabel;
18 import net.minecraft.client.gui.GuiScreen; 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 * A general-purpose base class for mod config panels which implements a lot of 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,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 this.label = label; 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 this.control = control; 163 this.control = control;
66 this.listener = listener; 164 this.listener = listener;
67 } 165 }
68 166
  167 + @Override
69 void draw(Minecraft minecraft, int mouseX, int mouseY, float partialTicks) 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 boolean mousePressed(Minecraft minecraft, int mouseX, int mouseY) 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 return true; 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 return false; 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 protected final Minecraft mc; 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 private int contentHeight = 0; 404 private int contentHeight = 0;
111 405
112 - private ConfigOption<?> selected; 406 + private ConfigOption selected;
113 407
114 public AbstractConfigPanel() 408 public AbstractConfigPanel()
115 { 409 {
@@ -170,7 +464,7 @@ public abstract class AbstractConfigPanel implements ConfigPanel @@ -170,7 +464,7 @@ public abstract class AbstractConfigPanel implements ConfigPanel
170 label.addLine(line); 464 label.addLine(line);
171 } 465 }
172 this.contentHeight = Math.max(y + height, this.contentHeight); 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,12 +479,33 @@ public abstract class AbstractConfigPanel implements ConfigPanel
185 if (control != null) 479 if (control != null)
186 { 480 {
187 this.contentHeight = Math.max(control.yPosition + ((IGuiButton)control).getButtonHeight(), this.contentHeight); 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 return control; 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 @Override 509 @Override
195 public void onPanelResize(ConfigPanelHost host) 510 public void onPanelResize(ConfigPanelHost host)
196 { 511 {
@@ -199,12 +514,16 @@ public abstract class AbstractConfigPanel implements ConfigPanel @@ -199,12 +514,16 @@ public abstract class AbstractConfigPanel implements ConfigPanel
199 @Override 514 @Override
200 public void onTick(ConfigPanelHost host) 515 public void onTick(ConfigPanelHost host)
201 { 516 {
  517 + for (ConfigOption configOption : this.options)
  518 + {
  519 + configOption.onTick();
  520 + }
202 } 521 }
203 522
204 @Override 523 @Override
205 public void drawPanel(ConfigPanelHost host, int mouseX, int mouseY, float partialTicks) 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 configOption.draw(this.mc, mouseX, mouseY, partialTicks); 528 configOption.draw(this.mc, mouseX, mouseY, partialTicks);
210 } 529 }
@@ -219,7 +538,7 @@ public abstract class AbstractConfigPanel implements ConfigPanel @@ -219,7 +538,7 @@ public abstract class AbstractConfigPanel implements ConfigPanel
219 return; 538 return;
220 } 539 }
221 540
222 - for (ConfigOption<?> configOption : this.options) 541 + for (ConfigOption configOption : this.options)
223 { 542 {
224 if (configOption.mousePressed(this.mc, mouseX, mouseY)) 543 if (configOption.mousePressed(this.mc, mouseX, mouseY))
225 { 544 {
@@ -251,5 +570,22 @@ public abstract class AbstractConfigPanel implements ConfigPanel @@ -251,5 +570,22 @@ public abstract class AbstractConfigPanel implements ConfigPanel
251 host.close(); 570 host.close();
252 return; 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,4 +91,6 @@ gui.error.copytoclipboard=Copy error to clipboard
91 gui.error.tooltip=%d mod startup error(s) detected (%d critical) 91 gui.error.tooltip=%d mod startup error(s) detected (%d critical)
92 92
93 gui.notifications.updateavailable=LiteLoader Update Available! 93 gui.notifications.updateavailable=LiteLoader Update Available!
94 -gui.notifications.newsnapshotavailable=!!New Snapshot Available:%nBuild #%s (%s)  
95 \ No newline at end of file 94 \ No newline at end of file
  95 +gui.notifications.newsnapshotavailable=!!New Snapshot Available:%nBuild #%s (%s)
  96 +
  97 +gui.invalidvalue=§cInvalid value!
96 \ No newline at end of file 98 \ No newline at end of file