001package squidpony.squidgrid.gui.gdx;
002
003import com.badlogic.gdx.Gdx;
004import com.badlogic.gdx.Input;
005import com.badlogic.gdx.InputAdapter;
006import com.badlogic.gdx.graphics.Color;
007import com.badlogic.gdx.math.MathUtils;
008import com.badlogic.gdx.scenes.scene2d.Stage;
009import com.badlogic.gdx.utils.CharArray;
010import com.badlogic.gdx.utils.viewport.StretchViewport;
011
012import java.util.Map;
013import java.util.TreeMap;
014
015/**
016 * Acts like SquidInput but displays available keys as visible buttons if the device has no physical keyboard.
017 * Only relevant if you want keyboard input to always be available, so this doesn't implement the SquidInput
018 * constructors that don't handle any keys (the buttons this shows will act like keys even on Android and iOS).
019 * <br>
020 * This doesn't currently work well when used with a SquidPanel or SquidLayers that pans a camera over an area
021 * that is larger than one screen (you only need to worry about this in "standard" SquidLib usage if you use
022 * the {@link SquidPanel#SquidPanel(int, int, TextCellFactory, squidpony.IColorCenter, float, float, char[][])}) or
023 * {@link SquidLayers#SquidLayers(int, int, int, int, TextCellFactory, SquidColorCenter, SquidColorCenter, char[][])}
024 * constructors with a larger 2D char array than the given gridWidth and/or gridHeight). It works fine if the
025 * area that can be seen on one screen is as big as it gets, such as in the normal text-based mode of games like
026 * Rogue and Brogue. It can also work if you manually handle movement over a larger area without using libGDX
027 * Viewport and Camera code to accomplish that, since this class needs to handle the Viewport to some extent.
028 * <br>
029 * Created by Tommy Ettinger on 4/15/2016.
030 */
031public class VisualInput extends SquidInput {
032
033    public SquidPanel left, right;
034    protected TextCellFactory tcfLeft, tcfRight;
035    protected TreeMap<Character, String> availableKeys;
036    protected SquidMouse mouseLeft, mouseRight;
037    private int sectionWidth, sectionHeight;
038    private float screenWidth = -1, screenHeight = -1;
039    protected boolean initialized;
040    public Color color = Color.WHITE;
041    protected CharArray clicks;
042    public boolean eightWay = true, forceButtons;
043    public Stage stage;
044    protected ShrinkPartViewport spv;
045    private void ready(TextCellFactory font)
046    {
047        initialized = true;
048        sectionWidth = Gdx.graphics.getWidth() / 8;
049        sectionHeight = Gdx.graphics.getHeight();
050
051        tcfLeft = font.copy().width(sectionWidth / 4).height(sectionHeight / 16).initBySize();
052        tcfRight = font.copy().width(sectionWidth / 12).height(sectionHeight / 24).initBySize();
053
054        left = new SquidPanel(4, 16, tcfLeft);
055        if(eightWay) {
056            left.put(0, 7, new char[][]{
057                    new char[]{'\\', '-', '/'},
058                    new char[]{'|', 'O', '|'},
059                    new char[]{'/', '-', '\\'},
060            }, color);
061        }
062        else
063        {
064            left.put(0, 7, new char[][]{
065                    new char[]{' ', SquidInput.LEFT_ARROW, ' '},
066                    new char[]{SquidInput.UP_ARROW, 'O', SquidInput.DOWN_ARROW},
067                    new char[]{' ', SquidInput.RIGHT_ARROW, ' '},
068            }, color);        }
069        right = new SquidPanel(12, 24, tcfRight, null, Gdx.graphics.getWidth() - sectionWidth, 0);
070
071        mouseLeft = new SquidMouse(left.cellWidth(), left.cellHeight(), left.gridWidth, left.gridHeight,
072                0, 0, new InputAdapter()
073        {
074            @Override
075            public boolean touchUp(int screenX, int screenY, int pointer, int button) {
076                if(screenX < 3 && screenY >= 5 && screenY <= 7 && left.contents[screenX][screenY + 2] != 0 &&
077                        left.contents[screenX][screenY + 2] != ' ')
078                {
079                    switch ((screenY - 5) * 3 + screenX)
080                    {
081                        case 0: queue.add(SquidInput.UP_LEFT_ARROW);
082                            break;
083                        case 1: queue.add(SquidInput.UP_ARROW);
084                            break;
085                        case 2: queue.add(SquidInput.UP_RIGHT_ARROW);
086                            break;
087                        case 3: queue.add(SquidInput.LEFT_ARROW);
088                            break;
089                        case 4: queue.add(SquidInput.CENTER_ARROW);
090                            break;
091                        case 5: queue.add(SquidInput.RIGHT_ARROW);
092                            break;
093                        case 6: queue.add(SquidInput.DOWN_LEFT_ARROW);
094                            break;
095                        case 7: queue.add(SquidInput.DOWN_ARROW);
096                            break;
097                        case 8: queue.add(SquidInput.DOWN_RIGHT_ARROW);
098                            break;
099                        default:
100                            return false;
101                    }
102                    queue.add('\u0000');
103                    return true;
104                }
105                else
106                    return false;
107            }
108        });
109        //if(mouse != null)
110          //  mouse.setOffsetX(-sectionWidth);
111        stage = new Stage(new StretchViewport(Gdx.graphics.getWidth(), Gdx.graphics.getHeight()));
112        stage.addActor(left);
113        stage.addActor(right);
114    }
115    private void fillActions()
116    {
117        if(!initialized)
118            return;
119        int y = 0;
120        clicks = new CharArray(right.getGridHeight());
121        for(Map.Entry<Character, String> kv : availableKeys.entrySet())
122        {
123            switch (kv.getKey())
124            {
125                case SquidInput.UP_LEFT_ARROW:
126                case SquidInput.UP_ARROW:
127                case SquidInput.UP_RIGHT_ARROW:
128                case SquidInput.LEFT_ARROW:
129                case SquidInput.CENTER_ARROW:
130                case SquidInput.RIGHT_ARROW:
131                case SquidInput.DOWN_LEFT_ARROW:
132                case SquidInput.DOWN_ARROW:
133                case SquidInput.DOWN_RIGHT_ARROW:
134                    break;
135                default:
136                    right.put(1, y, kv.getValue(), color);
137                    clicks.add(kv.getKey());
138                    y++;
139            }
140            if(y > right.getGridHeight())
141                break;
142        }
143        mouseRight = new SquidMouse(right.cellWidth(), right.cellHeight(), right.gridWidth(), right.gridHeight(),
144                Gdx.graphics.getWidth() - sectionWidth, Math.round(sectionHeight - right.getHeight()),
145                new InputAdapter()
146        {
147            @Override
148            public boolean touchUp(int screenX, int screenY, int pointer, int button) {
149                if(screenY < clicks.size)
150                {
151                    queue.add(clicks.get(screenY));
152                    queue.add('\u0000');
153                    return true;
154                }
155                return false;
156            }
157        });
158    }
159    /**
160     * Convenience method that does essentially the same thing as init(Map&lt;Character, String&gt;).
161     * This assumes that each String passed to this is an action, and the first letter of a String is the character that
162     * a keypress would generate to perform the action named by the String. For example, calling this with
163     * {@code init("fire", "Fortify")} would register "fire" under 'f' and "Fortify" under 'F', displaying the Strings
164     * instead of the characters if possible. The first char of each String should be unique in the arguments.
165     * <br>
166     * This also initializes the displayed buttons if there is no hardware keyboard available. This uses the color and
167     * eightWay fields to determine what color the buttons will be drawn with and if directions should be 8-way or
168     * 4-way, respectively. These fields should be set before calling init() if you don't want the defaults, which are
169     * white buttons and 8-way directions. The font with this overload of init is set to Inconsolata-LGC-Custom, which
170     * is normally available via {@link DefaultResources#getStretchableFont()} and requires the .fnt file and .png file
171     * listed in the documentation for that method. If you don't have those files, but have some other font (preferably
172     * some kind of distance field font, since this resizes the font significantly), you can use
173     * {@link #init(TextCellFactory, String...)} or {@link #init(TextCellFactory, Map)} to have this use your font.
174     * @param enabled an array or vararg of Strings that name actions; the first char of each String should be unique
175     */
176    public void init(String... enabled) {
177        init(DefaultResources.getStretchableFont(), enabled);
178    }
179    /**
180     * Convenience method that does essentially the same thing as init(Map&lt;Character, String&gt;).
181     * This assumes that each String passed to this is an action, and the first letter of a String is the character that
182     * a keypress would generate to perform the action named by the String. For example, calling this with
183     * {@code init("fire", "Fortify")} would register "fire" under 'f' and "Fortify" under 'F', displaying the Strings
184     * instead of the characters if possible. The first char of each String should be unique in the arguments.
185     * <br>
186     * This also initializes the displayed buttons if there is no hardware keyboard available. This uses the color and
187     * eightWay fields to determine what color the buttons will be drawn with and if directions should be 8-way or
188     * 4-way, respectively. These fields should be set before calling init() if you don't want the defaults, which are
189     * white buttons and 8-way directions.
190     * @param font the TextCellFactory to use for the buttons, usually a distance field font from DefaultResources
191     * @param enabled an array or vararg of Strings that name actions; the first char of each String should be unique
192     */
193    public void init(TextCellFactory font, String... enabled)
194    {
195        if(!forceButtons && Gdx.input.isPeripheralAvailable(Input.Peripheral.HardwareKeyboard))
196            return;
197        ready(font);
198        availableKeys = new TreeMap<>();
199        if(enabled == null)
200            return;
201        for (int i = 0; i < enabled.length; i++) {
202            if(enabled[i] != null && !enabled[i].isEmpty())
203                availableKeys.put(enabled[i].charAt(0), enabled[i]);
204        }
205        fillActions();
206    }
207    /**
208     * For each char and String in available, registers each keyboard key (as a char, such as 'A' or
209     * SquidInput.LEFT_ARROW) with a String that names the action that key is used to perform, and makes these Strings
210     * available as buttons on the right side of the screen if on a device with no hardware keyboard. Arrows will be
211     * provided on the left side of the screen for directions.
212     * <br>
213     * This also initializes the displayed buttons if there is no hardware keyboard available. This uses the color and
214     * eightWay fields to determine what color the buttons will be drawn with and if directions should be 8-way or
215     * 4-way, respectively. These fields should be set before calling init() if you don't want the defaults, which are
216     * white buttons and 8-way directions. The font with this overload of init is set to Inconsolata-LGC-Custom, which
217     * is normally available via {@link DefaultResources#getStretchableFont()} and requires the .fnt file and .png file
218     * listed in the documentation for that method. If you don't have those files, but have some other font (preferably
219     * some kind of distance field font, since this resizes the font significantly), you can use
220     * {@link #init(TextCellFactory, String...)} or {@link #init(TextCellFactory, Map)} to have this use your font.
221     * @param available a Map of Character keys representing keyboard keys and Strings for the actions they trigger
222     */
223    public void init(Map<Character, String> available)
224    {
225        init(DefaultResources.getStretchableFont(), available);
226    }
227    /**
228     * For each char and String in available, registers each keyboard key (as a char, such as 'A' or
229     * SquidInput.LEFT_ARROW) with a String that names the action that key is used to perform, and makes these Strings
230     * available as buttons on the right side of the screen if on a device with no hardware keyboard. Arrows will be
231     * provided on the left side of the screen for directions.
232     * <br>
233     * This uses the color and eightWay fields to determine what color the buttons will be drawn with and if directions
234     * should be 8-way or 4-way, respectively. These fields should be set before calling init() if you don't want the
235     * defaults, which are white buttons and 8-way directions.
236     * @param font the TextCellFactory to use for the buttons, usually a distance field font from DefaultResources
237     * @param available a Map of Character keys representing keyboard keys and Strings for the actions they trigger
238     */
239    public void init(TextCellFactory font, Map<Character, String> available)
240    {
241        if(!forceButtons && Gdx.input.isPeripheralAvailable(Input.Peripheral.HardwareKeyboard))
242            return;
243        ready(font);
244        if(available != null) {
245            availableKeys = new TreeMap<>(available);
246            fillActions();
247        }
248    }
249    private VisualInput() {
250
251    }
252
253    public VisualInput(KeyHandler keyHandler) {
254        super(keyHandler);
255    }
256
257    public VisualInput(KeyHandler keyHandler, boolean ignoreInput) {
258        super(keyHandler, ignoreInput);
259    }
260
261    public VisualInput(KeyHandler keyHandler, SquidMouse mouse) {
262        super(keyHandler, mouse);
263    }
264
265    public VisualInput(KeyHandler keyHandler, SquidMouse mouse, boolean ignoreInput) {
266        super(keyHandler, mouse, ignoreInput);
267    }
268
269    @Override
270    public boolean touchDown(int screenX, int screenY, int pointer, int button) {
271        if (initialized && mouseLeft.onGrid(screenX, screenY))
272            return mouseLeft.touchDown(screenX, screenY, pointer, button);
273        else if (initialized && mouseRight.onGrid(screenX, screenY))
274            return mouseRight.touchDown(screenX, screenY, pointer, button);
275        if(spv != null) {
276            screenX *= spv.getScreenWidth() / (spv.getScreenWidth() - spv.barWidth);
277        }
278        return (!initialized || (mouse != null && mouse.onGrid(screenX, screenY))) && super.touchDown(screenX, screenY, pointer, button);
279    }
280
281    @Override
282    public boolean touchUp(int screenX, int screenY, int pointer, int button) {
283        if (initialized && mouseLeft.onGrid(screenX, screenY))
284            return mouseLeft.touchUp(screenX, screenY, pointer, button);
285        else if (initialized && mouseRight.onGrid(screenX, screenY))
286            return mouseRight.touchUp(screenX, screenY, pointer, button);
287        if(spv != null) {
288            screenX *= spv.getScreenWidth() / (spv.getScreenWidth() - spv.barWidth);
289        }
290
291        return (!initialized || (mouse != null && mouse.onGrid(screenX, screenY))) && super.touchUp(screenX, screenY, pointer, button);
292    }
293
294    @Override
295    public boolean touchDragged(int screenX, int screenY, int pointer) {
296        if (initialized && mouseLeft.onGrid(screenX, screenY))
297            return mouseLeft.touchDragged(screenX, screenY, pointer);
298        else if (initialized && mouseRight.onGrid(screenX, screenY))
299            return mouseRight.touchDragged(screenX, screenY, pointer);
300        if(spv != null) {
301            screenX *= spv.getScreenWidth() / (spv.getScreenWidth() - spv.barWidth);
302        }
303        return (!initialized || (mouse != null && mouse.onGrid(screenX, screenY))) && super.touchDragged(screenX, screenY, pointer);
304
305    }
306    @Override
307    public boolean mouseMoved (int screenX, int screenY) {
308        if(spv != null) {
309            screenX *= spv.getScreenWidth() / (spv.getScreenWidth() - spv.barWidth);
310        }
311
312        if(ignoreInput || mouse == null || !mouse.onGrid(screenX, screenY)) return false;
313        return mouse.mouseMoved(screenX, screenY);
314    }
315    public void reinitialize(float cellWidth, float cellHeight, float gridWidth, float gridHeight,
316                             int offsetX, int offsetY, float screenWidth, float screenHeight)
317    {
318        if(!initialized)
319        {
320            if(mouse != null) 
321                mouse.reinitialize(cellWidth, cellHeight, gridWidth, gridHeight, offsetX, offsetY);
322            return;
323        }
324        if(this.screenWidth > 0)
325            sectionWidth *= screenWidth / this.screenWidth;
326        else
327            sectionWidth *= screenWidth / Gdx.graphics.getWidth();
328        if(this.screenHeight > 0)
329            sectionHeight *= screenHeight / this.screenHeight;
330        else
331            sectionHeight *= screenHeight / Gdx.graphics.getHeight();
332        cellWidth /= screenWidth / (screenWidth - sectionWidth * 0.75f);
333        float leftWidth = screenWidth / 32f, rightWidth = screenWidth / 96f,
334                leftHeight = screenHeight / 12f, rightHeight = screenHeight / 24f;
335        if(mouse != null) 
336            mouse.reinitialize(cellWidth, cellHeight, gridWidth, gridHeight,
337                offsetX - MathUtils.round((screenWidth * 0.125f) * (screenWidth / (screenWidth - sectionWidth)) + cellWidth * 0.5f), offsetY);
338        mouseLeft.reinitialize(leftWidth, leftHeight, 4, 16, offsetX, offsetY);
339        mouseRight.reinitialize(rightWidth, rightHeight, 12, 24,
340                MathUtils.ceil(offsetX - (screenWidth - sectionWidth)),
341                MathUtils.round(offsetY - rightHeight * 0.5f + (right.getGridHeight() * rightHeight - screenHeight)));
342        this.screenWidth = screenWidth;
343        this.screenHeight = screenHeight;
344        if(spv != null)
345            spv.barWidth = sectionWidth;
346    }
347    public ShrinkPartViewport resizeInnerStage(Stage insides)
348    {
349        if(!initialized)
350            return null;
351        /*
352        insides.getViewport().setWorldWidth(insides.getViewport().getWorldWidth() - screenWidth * 2);
353        insides.getViewport().setScreenX(screenWidth);
354        insides.getViewport().setScreenY(0);
355        */
356        spv = new ShrinkPartViewport(insides.getWidth(), insides.getHeight(), sectionWidth);
357        insides.setViewport(spv);
358        return spv;
359    }
360
361    public int getSectionWidth() {
362        return sectionWidth;
363    }
364
365    public int getSectionHeight() {
366        return sectionHeight;
367    }
368
369    public void update(int width, int height, boolean centerCamera) {
370        if(initialized)
371        {
372            stage.getViewport().update(width, height, centerCamera);
373        }
374    }
375
376    public void show() {
377        if(initialized) {
378            stage.getViewport().apply(true);
379            stage.draw();
380            stage.act();
381        }
382    }
383}