From 7307c38a3c520408a722c088a12b8590ea7a853e Mon Sep 17 00:00:00 2001 From: Evan Debenham Date: Thu, 8 Jul 2021 12:38:45 -0400 Subject: [PATCH] v0.9.4: overhauled text input, now multiplatform using libGDX textField --- .../java/com/watabou/glscripts/Script.java | 17 +- .../java/com/watabou/noosa/TextInput.java | 149 ++++++++++++++++++ .../com/watabou/utils/PlatformSupport.java | 9 -- .../android/AndroidPlatformSupport.java | 16 -- core/src/main/assets/gdx/textfield.atlas | 21 +++ core/src/main/assets/gdx/textfield.json | 8 + core/src/main/assets/gdx/textfield.png | Bin 0 -> 81 bytes core/src/main/assets/interfaces/chrome.png | Bin 1119 -> 1164 bytes .../shatteredpixeldungeon/Chrome.java | 7 +- .../ui/WndTextInput.java | 105 ++++++++++++ .../desktop/DesktopPlatformSupport.java | 15 -- .../ios/IOSPlatformSupport.java | 5 - 12 files changed, 296 insertions(+), 56 deletions(-) create mode 100644 SPD-classes/src/main/java/com/watabou/noosa/TextInput.java create mode 100644 core/src/main/assets/gdx/textfield.atlas create mode 100644 core/src/main/assets/gdx/textfield.json create mode 100644 core/src/main/assets/gdx/textfield.png create mode 100644 core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/WndTextInput.java diff --git a/SPD-classes/src/main/java/com/watabou/glscripts/Script.java b/SPD-classes/src/main/java/com/watabou/glscripts/Script.java index c59866cb9..899a009b0 100644 --- a/SPD-classes/src/main/java/com/watabou/glscripts/Script.java +++ b/SPD-classes/src/main/java/com/watabou/glscripts/Script.java @@ -37,7 +37,7 @@ public class Script extends Program { @SuppressWarnings("unchecked") public synchronized static T use( Class c ) { - + if (c != curScriptClass) { Script script = all.get( c ); @@ -45,11 +45,7 @@ public class Script extends Program { script = Reflection.newInstance( c ); all.put( c, script ); } - - if (curScript != null) { - curScript.unuse(); - } - + curScript = script; curScriptClass = c; curScript.use(); @@ -58,6 +54,11 @@ public class Script extends Program { return (T)curScript; } + + public synchronized static void unuse(){ + curScript = null; + curScriptClass = null; + } public synchronized static void reset() { for (Script script:all.values()) { @@ -77,7 +78,5 @@ public class Script extends Program { link(); } - - public void unuse() { - } + } diff --git a/SPD-classes/src/main/java/com/watabou/noosa/TextInput.java b/SPD-classes/src/main/java/com/watabou/noosa/TextInput.java new file mode 100644 index 000000000..a401303e5 --- /dev/null +++ b/SPD-classes/src/main/java/com/watabou/noosa/TextInput.java @@ -0,0 +1,149 @@ +package com.watabou.noosa; + +import com.badlogic.gdx.Files; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.g2d.BitmapFont; +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.Stage; +import com.badlogic.gdx.scenes.scene2d.ui.Container; +import com.badlogic.gdx.scenes.scene2d.ui.Skin; +import com.badlogic.gdx.scenes.scene2d.ui.TextArea; +import com.badlogic.gdx.scenes.scene2d.ui.TextField; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import com.badlogic.gdx.utils.Align; +import com.badlogic.gdx.utils.viewport.ScreenViewport; +import com.watabou.glscripts.Script; +import com.watabou.glwrap.Blending; +import com.watabou.glwrap.Quad; +import com.watabou.glwrap.Texture; +import com.watabou.noosa.ui.Component; +import com.watabou.utils.FileUtils; +import com.watabou.utils.Point; + +//essentially contains a libGDX text input field, plus a PD-rendered background +public class TextInput extends Component { + + private Stage stage; + private Container container; + private TextField textField; + + private Skin skin; + + private NinePatch bg; + + public TextInput( NinePatch bg, boolean multiline ){ + super(); + this.bg = bg; + add(bg); + + stage = new Stage(new ScreenViewport()); + Game.inputHandler.addInputProcessor(stage); + + container = new Container(); + stage.addActor(container); + container.setTransform(true); + + skin = new Skin(FileUtils.getFileHandle(Files.FileType.Internal, "gdx/textfield.json")); + + int zoom = (int) Camera.main.zoom; + int textSize = multiline ? 6 : 9; + TextField.TextFieldStyle style = skin.get(TextField.TextFieldStyle.class); + style.font = Game.platform.getFont(textSize*zoom, "", false, false); + style.background = null; + textField = multiline ? new TextArea("", style) : new TextField("", style); + textField.setProgrammaticChangeEvents(true); + + if (!multiline) textField.setAlignment(Align.center); + + textField.addListener(new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + BitmapFont f = Game.platform.getFont(textSize*zoom, textField.getText(), false, false); + TextField.TextFieldStyle style = textField.getStyle(); + if (f != style.font){ + style.font = f; + textField.setStyle(style); + } + } + }); + container.setActor(textField); + stage.setKeyboardFocus(textField); + Gdx.input.setOnscreenKeyboardVisible(true); + } + + public void setText(String text){ + textField.setText(text); + textField.setCursorPosition(textField.getText().length()); + } + + public void setMaxLength(int maxLength){ + textField.setMaxLength(maxLength); + } + + public String getText(){ + return textField.getText(); + } + + @Override + protected void layout() { + super.layout(); + + float contX = x; + float contY = y; + float contW = width; + float contH = height; + + if (bg != null){ + bg.x = x; + bg.y = y; + bg.size(width, height); + + contX += bg.marginLeft(); + contY += bg.marginTop(); + contW -= bg.marginHor(); + contH -= bg.marginVer(); + } + + float zoom = Camera.main.zoom; + Camera c = camera(); + if (c != null){ + zoom = c.zoom; + Point p = c.cameraToScreen(contX, contY); + contX = p.x/zoom; + contY = p.y/zoom; + } + + container.align(Align.topLeft); + container.setPosition(contX*zoom, (Game.height-(contY*zoom))); + container.size(contW*zoom, contH*zoom); + } + + @Override + public void update() { + super.update(); + stage.act(Game.elapsed); + } + + @Override + public void draw() { + super.draw(); + Quad.releaseIndices(); + Script.unuse(); + Texture.clear(); + stage.draw(); + Quad.bindIndices(); + Blending.useDefault(); + } + + @Override + public synchronized void destroy() { + super.destroy(); + if (stage != null) { + stage.dispose(); + skin.dispose(); + Game.inputHandler.removeInputProcessor(stage); + Gdx.input.setOnscreenKeyboardVisible(false); + Game.platform.updateSystemUI(); + } + } +} diff --git a/SPD-classes/src/main/java/com/watabou/utils/PlatformSupport.java b/SPD-classes/src/main/java/com/watabou/utils/PlatformSupport.java index d8afd4519..1753ba722 100644 --- a/SPD-classes/src/main/java/com/watabou/utils/PlatformSupport.java +++ b/SPD-classes/src/main/java/com/watabou/utils/PlatformSupport.java @@ -37,15 +37,6 @@ public abstract class PlatformSupport { public abstract void updateSystemUI(); public abstract boolean connectedToUnmeteredNetwork(); - - //FIXME this is currently used because no platform-agnostic text input has been implemented. - //should look into doing that using either plain openGL or libgdx's libraries - public abstract void promptTextInput( String title, String hintText, int maxLen, boolean multiLine, - String posTxt, String negTxt, TextCallback callback); - - public static abstract class TextCallback { - public abstract void onSelect( boolean positive, String text ); - } public void vibrate( int millis ){ //regular GDX vibration by default diff --git a/android/src/main/java/com/shatteredpixel/shatteredpixeldungeon/android/AndroidPlatformSupport.java b/android/src/main/java/com/shatteredpixel/shatteredpixeldungeon/android/AndroidPlatformSupport.java index 1ddf773f2..35ff10492 100644 --- a/android/src/main/java/com/shatteredpixel/shatteredpixeldungeon/android/AndroidPlatformSupport.java +++ b/android/src/main/java/com/shatteredpixel/shatteredpixeldungeon/android/AndroidPlatformSupport.java @@ -160,22 +160,6 @@ public class AndroidPlatformSupport extends PlatformSupport { } } - @Override - public void promptTextInput(final String title, final String hintText, final int maxLen, final boolean multiLine, final String posTxt, final String negTxt, final TextCallback callback) { - Game.runOnRenderThread( new Callback() { - @Override - public void call() { - Game.scene().addToFront(new WndAndroidTextInput(title, hintText, maxLen, multiLine, posTxt, negTxt) { - @Override - protected void onSelect(boolean positive) { - callback.onSelect(positive, getText()); - } - }); - } - } - ); - } - /* FONT SUPPORT */ //droid sans / roboto, or a custom pixel font, for use with Latin and Cyrillic languages diff --git a/core/src/main/assets/gdx/textfield.atlas b/core/src/main/assets/gdx/textfield.atlas new file mode 100644 index 000000000..5a157c718 --- /dev/null +++ b/core/src/main/assets/gdx/textfield.atlas @@ -0,0 +1,21 @@ + +textfield.png +size: 4,4 +format: RGBA8888 +filter: Nearest,Nearest +repeat: none +cursor + rotate: false + xy: 0, 0 + size: 3, 3 + split: 1, 1, 1, 1 + orig: 3, 3 + offset: 0, 0 + index: -1 +selection + rotate: false + xy: 3, 3 + size: 1, 1 + orig: 1, 1 + offset: 0, 0 + index: -1 \ No newline at end of file diff --git a/core/src/main/assets/gdx/textfield.json b/core/src/main/assets/gdx/textfield.json new file mode 100644 index 000000000..e7d8c6ca1 --- /dev/null +++ b/core/src/main/assets/gdx/textfield.json @@ -0,0 +1,8 @@ +{ +Color: { + black: { a: 1, b: 0, g: 0, r: 0 }, +}, +TextFieldStyle: { + default: { selection: selection, fontColor: black, cursor: cursor } +} +} diff --git a/core/src/main/assets/gdx/textfield.png b/core/src/main/assets/gdx/textfield.png new file mode 100644 index 0000000000000000000000000000000000000000..d3ad630ac1a86c0b805042c8b3ee6f2596e8b422 GIT binary patch literal 81 zcmeAS@N?(olHy`uVBq!ia0vp^EFjFm1|(O0oL2{=Bs^UlLn;`P8JhkV@GwirSh%r2 dD)s-*%wTYov$*}l#(JO{22WQ%mvv4FO#q-Q6axSN literal 0 HcmV?d00001 diff --git a/core/src/main/assets/interfaces/chrome.png b/core/src/main/assets/interfaces/chrome.png index 46e12af3604e16d54dc92d3995638f4cce84a51e..c5f779d8d2989e7e30f13ecad55131e794572dd3 100644 GIT binary patch delta 907 zcmV;619be~2#g7kBmv}+B|{b7-rj~BEpBdjJ3Tk(=;%H`Ir8%Ik#vvH+vG<_JiL{SbL#X2^1XlBi_H_^8v2$O|s!WmVM;2uS@X(6((C z1yCLnP~1+r|hSlBd}*duU5fyH9ESg-(d zLxJULc~b%A#7}@ZUVu=B@lj`Xf%B&e1RTAHq~nzL&oluw3mH~1OkTy(amxE=n!p|; z;LrayV2yeDaSTKp0^_J;AJ|!bYE0Ey&`PY73ASo2YKmdhQ_1t9LcfaB8_rK3(rV3Bc z;Pfim?|V2UAnyVB(*bfGz`xD_@bdch{?5Nn2Ke~=`u^r$765WP0LK5B0LcB!e`Z5K zKF2>s0R3X50%&|O+#cY@5nG{#XnwiJSF&vZ?)Q%q#dAqwH?B{W0N8%H1pxQQAq#*Y z05TMm@Ix3c_Z|T57XZa14gzE)eGQb#pgofd{gs9CV!|l~0m|iaT9s2P8vsK8<4o#} zCf^lBam&XspjJI7z_0$tKVdH%e*AD0Q8hV?l%Sy zL0JU`^&Swd1&{_H6F}?%(P}_dj3pqx1MYwm1L~&+G)@g@3iH4*0ma`_1OB9mtbaT} zA_4|u05B8-m;oYbBpmbuVD(gN0pPMo7zyLSfC&!BFj9idVju}+YMk{{SuF2_t%dY? z>Bno}&{Hva!6LvD!am?=#Z)}B0g#gbEe8001_(o`Eg^B} hC)26e{^R-n`3FBt68?gqtET_}002ovPDHLkV1f(5p7HK~#7F#MTFn>>vz);b79s6v*`6?tjH+oOu>n5kzJS z-&RYq__vb85Wt2}4ggXFUe0~UNX~}Yytc8y1X zBcOk(uGgzBd*~6w2)JHv*DD5)C;_+o?N0;Phu;A2>w^>yiWT8gat4{0d)T_EDxN+uVXNBoBule0^ZL9OAF4v>lhf1`O^jy z(er?9{CNP}^5@GoIe_ke3(nyIeE!D)?uUQU-wW7mAjq!;C>1xLP%4QTAWVRW7m)mG zzb=ql31T3C{;#T@gM>NRSu?YJ{}V-{DzjwNhC68Nw=5?B|M~)eo7=nld;axlfQQGY z=V$)q0ibXKVE&&AfWpga4g~z?ON$r4ycpR4nqLf$2ZVXlR+u4LUv9aQod5{0Z=8QD zt|gCQUSBc-z+Ucy0n#70lSdH%WmJry49u7N3;_2lfNCB^fO5@V1GPHn%>0x7#zuKH z=Tt?2dc9t1>S@&-0MdWCl6I%dcV$`Zr341FnuP+=>c4zb_QE-!h5kSfS}iw#l&b)= zVDt~sf*T-O00J;F0)UDK5CH=<4)uQtknIJK1t1qdOn_`RpsB_a5dQ#wfKLXrKO4~b zY(Q6;2R;%|ef?~}n>LY+KMs(qfXN&HOw|BxfJ_?+Cu0HZnTkCC)jU&1!Z?{QApm8} zjG*i>kVu)CQ210VJ7I4lV_y3C8Yp@yrX=WXWz2xF>;m>wJoN!2L2o+@Nc}YoXkk#~ zR7@<;0OL4L{Z#l=Oq|eD5~ZK~r(*Aq=V#^}G_Urh$tdoN00000NkvXXu0mjf&X|gP diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Chrome.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Chrome.java index 5bd9e8820..8d065c4bf 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Chrome.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/Chrome.java @@ -28,6 +28,7 @@ public class Chrome { public enum Type { TOAST, TOAST_TR, + TOAST_WHITE, WINDOW, WINDOW_SILVER, RED_BUTTON, @@ -53,10 +54,12 @@ public class Chrome { case TOAST_TR: case GREY_BUTTON_TR: return new NinePatch( Asset, 20, 9, 9, 9, 4 ); + case TOAST_WHITE: + return new NinePatch( Asset, 29, 0, 9, 9, 4 ); case RED_BUTTON: - return new NinePatch( Asset, 29, 0, 6, 6, 2 ); + return new NinePatch( Asset, 38, 0, 6, 6, 2 ); case GREY_BUTTON: - return new NinePatch( Asset, 29, 6, 6, 6, 2 ); + return new NinePatch( Asset, 38, 6, 6, 6, 2 ); case TAG: return new NinePatch( Asset, 22, 18, 16, 14, 3 ); case GEM: diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/WndTextInput.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/WndTextInput.java new file mode 100644 index 000000000..9b1a0392b --- /dev/null +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ui/WndTextInput.java @@ -0,0 +1,105 @@ +package com.shatteredpixel.shatteredpixeldungeon.ui; + +import com.shatteredpixel.shatteredpixeldungeon.Chrome; +import com.shatteredpixel.shatteredpixeldungeon.scenes.PixelScene; +import com.watabou.noosa.TextInput; +import com.watabou.utils.DeviceCompat; + +public class WndTextInput extends Window { + + private static final int WIDTH = 120; + private static final int W_LAND_MULTI = 200; //in the specific case of multiline in landscape + private static final int MARGIN = 2; + private static final int BUTTON_HEIGHT = 16; + + public WndTextInput(final String title, final String initialValue, final int maxLength, + final boolean multiLine, final String posTxt, final String negTxt) { + super(); + + //need to offset to give space for the soft keyboard + if (!DeviceCompat.isDesktop()) { + if (PixelScene.landscape()) { + offset(-45); + } else { + offset(multiLine ? -60 : -45); + } + } + + final int width; + if (PixelScene.landscape() && multiLine) { + width = W_LAND_MULTI; //more editing space for landscape users + } else { + width = WIDTH; + } + + final RenderedTextBlock txtTitle = PixelScene.renderTextBlock(title, 9); + txtTitle.maxWidth(width); + txtTitle.hardlight(Window.TITLE_COLOR); + txtTitle.setPos((width - txtTitle.width()) / 2, 2); + add(txtTitle); + + TextInput textBox = new TextInput(Chrome.get(Chrome.Type.TOAST_WHITE), multiLine); + if (initialValue != null) textBox.setText(initialValue); + textBox.setMaxLength(maxLength); + + float pos = txtTitle.bottom() + 2 * MARGIN; + + //sets different height depending on whether this is a single or multi line input. + final float inputHeight; + if (multiLine) { + inputHeight = 64; //~8 lines of text + } else { + inputHeight = 16; + } + add(textBox); + textBox.setRect(MARGIN, pos, width-2*MARGIN, inputHeight); + + pos += inputHeight + MARGIN; + + final RedButton positiveBtn = new RedButton(posTxt) { + @Override + protected void onClick() { + onSelect(true, textBox.getText()); + hide(); + } + }; + + final RedButton negativeBtn; + if (negTxt != null) { + negativeBtn = new RedButton(negTxt) { + @Override + protected void onClick() { + onSelect(false, textBox.getText()); + hide(); + } + }; + } else { + negativeBtn = null; + } + + if (negTxt != null) { + positiveBtn.setRect(MARGIN, pos, (width - MARGIN * 3) / 2, BUTTON_HEIGHT); + add(positiveBtn); + negativeBtn.setRect(positiveBtn.right() + MARGIN, pos, (width - MARGIN * 3) / 2, BUTTON_HEIGHT); + add(negativeBtn); + } else { + positiveBtn.setRect(MARGIN, pos, width - MARGIN * 2, BUTTON_HEIGHT); + add(positiveBtn); + } + + pos += BUTTON_HEIGHT + MARGIN; + + //need to resize first before laying out the text box, as it depends on the window's camera + resize(width, (int) pos); + + textBox.setRect(MARGIN, textBox.top(), width-2*MARGIN, inputHeight); + + } + + public void onSelect(boolean positive, String text){ } + + @Override + public void onBackPressed() { + //Do nothing, prevents accidentally losing writing + } +} diff --git a/desktop/src/main/java/com/shatteredpixel/shatteredpixeldungeon/desktop/DesktopPlatformSupport.java b/desktop/src/main/java/com/shatteredpixel/shatteredpixeldungeon/desktop/DesktopPlatformSupport.java index c3dc73ba2..0da144921 100644 --- a/desktop/src/main/java/com/shatteredpixel/shatteredpixeldungeon/desktop/DesktopPlatformSupport.java +++ b/desktop/src/main/java/com/shatteredpixel/shatteredpixeldungeon/desktop/DesktopPlatformSupport.java @@ -66,21 +66,6 @@ public class DesktopPlatformSupport extends PlatformSupport { public boolean connectedToUnmeteredNetwork() { return true; //no easy way to check this in desktop, just assume user doesn't care } - - @Override - //FIXME tinyfd_inputBox isn't a full solution for this. No support for multiline, looks ugly. Ideally we'd have an opengl-based input box - public void promptTextInput(String title, String hintText, int maxLen, boolean multiLine, String posTxt, String negTxt, TextCallback callback) { - String result = TinyFileDialogs.tinyfd_inputBox(title, title, hintText); - if (result == null){ - callback.onSelect(false, ""); - } else { - if (result.contains("\r\n")) result = result.substring(0, result.indexOf("\r\n")); - if (result.contains("\n")) result = result.substring(0, result.indexOf("\n")); - if (result.length() > maxLen) result = result.substring(0, maxLen); - callback.onSelect(true, result.replace("\r\n", "").replace("\n", "")); - } - } - /* FONT SUPPORT */ //custom pixel font, for use with Latin and Cyrillic languages diff --git a/ios/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ios/IOSPlatformSupport.java b/ios/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ios/IOSPlatformSupport.java index dde2be7b2..7283aa1bc 100644 --- a/ios/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ios/IOSPlatformSupport.java +++ b/ios/src/main/java/com/shatteredpixel/shatteredpixeldungeon/ios/IOSPlatformSupport.java @@ -56,11 +56,6 @@ public class IOSPlatformSupport extends PlatformSupport { return !test.getFlags().contains(SCNetworkReachabilityFlags.IsWWAN); } - @Override - public void promptTextInput(String title, String hintText, int maxLen, boolean multiLine, String posTxt, String negTxt, TextCallback callback) { - //TODO need multiplat text input, this does nothing atm! - } - public void vibrate( int millis ){ //gives a short vibrate on iPhone 6+, no vibration otherwise AudioServices.playSystemSound(1520);