001package squidpony.squidgrid.gui.gdx;
002
003import com.badlogic.gdx.*;
004import com.badlogic.gdx.graphics.Color;
005import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
006import com.badlogic.gdx.math.MathUtils;
007import com.badlogic.gdx.scenes.scene2d.Actor;
008import com.badlogic.gdx.scenes.scene2d.InputEvent;
009import com.badlogic.gdx.scenes.scene2d.InputListener;
010import com.badlogic.gdx.scenes.scene2d.Stage;
011import com.badlogic.gdx.scenes.scene2d.actions.Actions;
012import squidpony.SquidTags;
013import squidpony.panel.IColoredString;
014import squidpony.panel.ICombinedPanel;
015import squidpony.panel.ISquidPanel;
016import squidpony.squidgrid.gui.gdx.UIUtil.CornerStyle;
017
018import java.util.*;
019
020/**
021 * A panel that layouts buttons vertically. It offers various features:
022 * 
023 * <ul>
024 * <li>It can display margins around buttons, with various {@link CornerStyle
025 * styles}.</li>
026 * <li>It returns a handler to detect mouse clicks on button or add it to
027 * {@code this} (if you want the handler to be called from the enclosing
028 * {@link Stage}).</li>
029 * <li>If {@link #shortcutCharacterColor configured}, the handler also handles
030 * key shortcuts. In that case this panel highlights the shortcuts in text
031 * automatically.</li>
032 * <li>To handle what happens when buttons are clicked, you should implement the
033 * abstract method {@link #selectedButton(int)}.</li>
034 * </ul>
035 * 
036 * <p>
037 * The panel is configured via its {@code public} fields. Configuration must
038 * happen in-between creation and the call to {@link #putAll(boolean, boolean)}.
039 * </p>
040 * 
041 * <p>
042 * This class has two different behaviors w.r.t to the backing panels. They can
043 * either be given at creation time or they can be created on the fly. If you're
044 * doing a full screen menu, you should likely give the panels at creation time
045 * (because you'll compute theirs sizes so that they fit the whole screen). If
046 * you're doing an in-game menu, that is quickly dispatched, you should likely
047 * let this class create the panels (it'll create panels as small as possible,
048 * yet that suffice to display the buttons correctly). There is a dedicated
049 * subtype for the first usage: {@link PreAllocatedPanels}.
050 * </p>
051 * 
052 * <p>
053 * Here's a full screen example of this class:
054 * 
055 * <br/>
056 * <br/>
057 * 
058 * <img src="http://i.imgur.com/AQgWeic.png"/>
059 * 
060 * <br/>
061 * <br/>
062 * 
063 * and this shows a non-full screen example (the Drink/Throw/Drop menu):
064 * 
065 * <br/>
066 * <br/>
067 * 
068 * <img src="http://i.imgur.com/dyd7IoN.png"/>
069 * </p>
070 * 
071 * @author smelC
072 * 
073 * @param <T>
074 *            The type of colors.
075 */
076public abstract class ButtonsPanel<T extends Color> extends GroupCombinedPanel<T> {
077
078        /**
079         * The margin, in number of cells, in-between buttons. Do not set a negative
080         * value. If set to {@code 0}, {@code this} will layout like:
081         * 
082         * <pre>
083         *    button1
084         * longer button
085         *    button2
086         * </pre>
087         * 
088         * If set to {@code 1}, {@code this} will layout like:
089         * 
090         * <pre>
091         *    button1
092         *    
093         * longer button
094         * 
095         *    button2
096         * </pre>
097         */
098        public int interButtonMargin;
099
100        /**
101         * The margin to use around the whole panel, in number of pixels. Do not set
102         * a negative value. Typical good looking values: cellWidth/cellHeight
103         * divided by 2, 3, or 4.
104         * 
105         * <p>
106         * As an example, this margin is in dark red in the Drink/Throw/Drop menu
107         * in:
108         * 
109         * <br/>
110         * <br/>
111         * <img src="http://i.imgur.com/dyd7IoN.png">small example</img>
112         * </p>
113         */
114        public int borderMargin;
115
116        /**
117         * The x-padding in-between a button's text and its margin, in number of
118         * cells. This padding is used at the left and the right. Do not set a
119         * negative value. If set to {@code 0}, {@code this} will layout each button
120         * like:
121         * 
122         * <pre>
123         * ---------
124         * |button1|
125         * --------
126         * </pre>
127         * 
128         * If set to {@code 2}, {@code this} will layout like:
129         * 
130         * <pre>
131         * -------------
132         * |  button1  |
133         * -------------
134         * </pre>
135         */
136        public int xpadding;
137
138        /**
139         * The y-padding in-between a button's text and its margin, in number of
140         * cells. This padding is used at the top and the bottom. Do not set a
141         * negative value. If set to {@code 0}, {@code this} will layout each button
142         * like:
143         * 
144         * <pre>
145         * ---------
146         * |button1|
147         * --------
148         * </pre>
149         * 
150         * If set to {@code 1}, {@code this} will layout like:
151         * 
152         * <pre>
153         * -------------
154         * |           |
155         * |  button1  |
156         * |           |
157         * -------------
158         * </pre>
159         * 
160         */
161        public int ypadding;
162
163        /**
164         * The margin to show around each button, in number of pixels. Do not set a
165         * negative value.
166         */
167        public int buttonMargin;
168
169        /**
170         * The style to use for the buttons' corners. Do not set to {@code null}.
171         */
172        public CornerStyle cornerStyle = CornerStyle.ROUNDED;
173
174        /**
175         * The color to use to paint the background (outside buttons). Or
176         * {@code null} to disable background coloring.
177         */
178        public /* @Nullable */ T bgColor;
179
180        /**
181         * The color to use to paint the background of the inside of buttons, or
182         * {@code null} to disable painting.
183         */
184        public /* @Nullable */ T insideButtonBGColor;
185
186        /**
187         * The color for the margin around buttons, or {@code null} to disable
188         * painting.
189         */
190        public /* @Nullable */ T buttonsMarginColor;
191
192        /**
193         * The color for the border around the whole panel, or {@code null} to
194         * disable painting.
195         */
196        public /* @Nullable */ T borderColor;
197
198        /**
199         * The color to use to highlight shortcuts of buttons. Or {@code null} to
200         * disable shortcuts.
201         */
202        public /* @Nullable */ T shortcutCharacterColor;
203
204        /**
205         * The style of borders, around the panel. Do not set it to {@code null}.
206         */
207        public CornerStyle borderStyle = CornerStyle.ROUNDED;
208
209        /**
210         * The alignment of buttons, -1 for left, 0 for center, 1 for right. If non-
211         * {@code null}, this array's length must be the number of buttons.
212         * 
213         * <p>
214         * The default is center.
215         * </p>
216         */
217        public /* @Nullable */ int[] buttonsAlignment;
218
219        /**
220         * Indexes of buttons that should not receive a shortcut, neither click
221         * events. Ignored if {@link #shortcutCharacterColor} is {@code null}.
222         */
223        public /* @Nullable */ Set<Integer> doNotBind;
224
225        /**
226         * If non-{@code null}, {@code this} will avoid characters in this set to
227         * bind keyboard presses to buttons. Note that this class already rules out
228         * characters that aren't {@link Character#isLetter(char)}, so you don't
229         * need it to fill this set with characters such as '#', '/', etc; but you
230         * need to put 'é', 'à', etc. if you want to rule out "complex" letters.
231         */
232        public /* @Nullable */ Set<Character> unbindable;
233
234        /**
235         * Whether this panel supports scrolling. This'll make this panel display
236         * '...' and '...' as the first and last entries if it cannot display the
237         * entirety of {@link #buttonsTexts}. This is only supported if
238         * {@link #interButtonMargin} is 0. This makes the {@link InputProcessor}
239         * returned by {@link #putAll(boolean, boolean)} handle scrolling with arrow
240         * down/arrow up/j/k ( the last two coming from vim) and with mouse clicks
241         * on '...'. Don't use that if your panel isn't at least of height 3 (i.e.
242         * supports:
243         * 
244         * <pre>
245         * ...
246         * item_n
247         * ...
248         * </pre>
249         * 
250         * )
251         * 
252         * <p>
253         * This flag require {@link SquidPanel}s to be preallocated (i.e. to be
254         * given at creation time).
255         * </p>
256         */
257        public boolean enableScrolling;
258
259        /** The text of the buttons to scroll */
260        public String scrollText = " ..."; // " …" <- not in fancy fonts
261
262        /**
263         * Really, if you're muting this beyond {@link #init(List)}, you're doing
264         * bad.
265         */
266        protected List<IColoredString<T>> buttonsTexts;
267
268        /** The positions of buttons, set by {@link #putAll(boolean, boolean)} */
269        protected /* @Nullable */ List<Rectangle> buttons;
270
271        /**
272         * The shortcuts to select the buttons. Keys are the shortcuts (always in
273         * lowercase) while values are indexes of {@link #buttonsTexts}. Or
274         * {@code null} if shortcuts are disabled.
275         * 
276         * <p>
277         * Initialized and filled in {@link #putShortcut(int, IColoredString)}.
278         * </p>
279         */
280        protected /* @Nullable */ Map<Character, Integer> shortcuts;
281
282        protected int hcells = -1;
283        protected int vcells = -1;
284
285        /**
286         * The indexes of buttons displayed. May not cover {@link #buttonsTexts}, if
287         * scrolling is possible.
288         */
289        protected /* @Nullable */ FirstAndLastButtonIndex firstLastButtonIndexes;
290
291        private int topMargin;
292
293        /**
294         * If you use this constructor, you can use {@link PreAllocatedPanels} to
295         * avoid having to define {@link #buildPanel(int, int)}.
296         * 
297         * @param bg
298         *            The backing background panel.
299         * @param fg
300         *            The backing foreground panel.
301         * @param buttonTexts
302         *            The text of buttons. It should not contain end of lines
303         *            (beware that this isn't checked). If {@code null}, it MUST be
304         *            set later on with {@link #init(List)}.
305         * @throws IllegalStateException
306         *             In various cases of errors regarding sizes of panels.
307         */
308        public ButtonsPanel(ISquidPanel<T> bg, ISquidPanel<T> fg,
309                        /* @Nullable */List<IColoredString<T>> buttonTexts) {
310                super(bg, fg);
311                if (buttonTexts != null)
312                        init(buttonTexts);
313        }
314
315        /**
316         * Constructor to use when you want the panels to be build using
317         * {@link #buildPanel(int, int)}.
318         * 
319         * @param buttonTexts
320         *            The text of buttons. It should not contain end of lines
321         *            (beware that this isn't checked). If {@code null}, it MUST be
322         *            set later on with {@link #init(List)}.
323         * @throws IllegalStateException
324         *             In various cases of errors regarding sizes of panels.
325         */
326        public ButtonsPanel(/* Nullable */ List<IColoredString<T>> buttonTexts) {
327                if (buttonTexts != null) {
328                        this.buttonsTexts = new ArrayList<>(buttonTexts.size());
329                        this.buttonsTexts.addAll(buttonTexts);
330                }
331        }
332
333        /**
334         * Sets the buttons' text. Use this method if you gave {@code null} at
335         * creation time. Beware that this method can be called from the
336         * constructor.
337         * 
338         * @param buttonTexts
339         * @return {@code this}
340         */
341        public ButtonsPanel<T> init(List<IColoredString<T>> buttonTexts) {
342                this.buttonsTexts = new ArrayList<>(buttonTexts);
343                return this;
344        }
345
346        /**
347         * Adds {@code i} to the set of button indexes that should not be bound to
348         * user input.
349         * 
350         * @param i
351         */
352        public void addDoNotBind(int i) {
353                if (this.doNotBind == null)
354                        this.doNotBind = new HashSet<>(4);
355                doNotBind.add(i);
356        }
357
358        /**
359         * Displays this panel. You should very likely call this method exactly
360         * once.
361         * 
362         * @param putBordersAndMargins
363         *            Whether to draw margins and the border. This should be
364         *            {@code true} if {@code this}'s position (I mean, in terms of
365         *            {@link #setPosition(float, float)}) is set already. If that's
366         *            not the case (for example, because you need this method to be
367         *            called to compute the position from {@link #getHCells()} and
368         *            {@link #getVCells()}), give {@code false}.
369         * @return The {@link InputProcessor} to plug if you want
370         *         {@link #selectedButton(int)} to be called.
371         * 
372         *         <p>
373         *         See {@link #y_gdxToSquid()} to configure the processor's
374         *         behavior.
375         *         </p>
376         * 
377         *         <p>
378         *         If this panel is behind a {@link Stage} (i.e. you're not setting
379         *         the returned processor to {@link Gdx#input}) and you want it to
380         *         receive keyboard events, don't forget to call
381         *         {@link Stage#setKeyboardFocus(Actor)} by giving {@code this}.
382         *         </p>
383         * @throws NullPointerException
384         *             If the text of buttons wasn't given at creation time, and
385         *             {@link #init(List)} wasn't called since then.
386         * @throws IllegalStateException
387         *             If {@link #enableScrolling} is ON but {@link #bg} isn't set.
388         */
389        public InputProcessor putAll(boolean addListener, boolean putBordersAndMargins) {
390                /*
391                 * The number of cells that this panel can span. It is taken from the
392                 * panels if they are preallocated. If that's the case, the panel will
393                 * draw vertical dots that allow to "scroll". This is only done if
394                 * {@link #interButtonMarginn} is 0.
395                 */
396                if (enableScrolling && bg == null)
397                        throw new IllegalStateException("Panels must be preallocated if scrolling is enabled");
398                final int nbVDisplayedCells = enableScrolling ? bg.gridHeight() : Integer.MAX_VALUE;
399                final int nbHDisplayedCells = enableScrolling ? bg.gridWidth() : Integer.MAX_VALUE;
400                if (nbVDisplayedCells < Integer.MAX_VALUE)
401                        Gdx.app.log(SquidTags.LAYOUT,
402                                        "Available rectangle (in cells): " + nbHDisplayedCells + "x" + nbVDisplayedCells);
403                {
404                        hcells = computeRequiredCellsWidth();
405                        vcells = enableScrolling ? nbVDisplayedCells : computeRequiredCellsHeight();
406
407                        if (bg == null) {
408                                /* Panels weren't given at creation time */
409                                assert fg == null;
410                                setPanels(buildPanel(hcells, vcells), buildPanel(hcells, vcells));
411                        }
412
413                        /*
414                         * We don't include buttons margins, because they are rendered using
415                         * ShapeRenderer, whereas the "important" space is the one for the
416                         * SquidPanel, i.e. the text of buttons, the paddings, and the inter
417                         * button margins.
418                         */
419                        /* The call to #cellWidth() requires #bg to be set */
420                        final int width = hcells * cellWidth();
421                        final int availableW = pixelsWidth();
422                        if (availableW < width)
423                                Gdx.app.log("layout",
424                                                "Cannot layout " + getClass().getSimpleName() + " correctly. Required pixels width: "
425                                                                + width + ". Pixels width available: " + availableW);
426
427                        /* The call to #cellHeight() requires #bg to be set */
428                        final int height = vcells * cellHeight();
429                        final int availableH = pixelsHeight();
430                        if (!enableScrolling && availableH < height)
431                                Gdx.app.log("layout",
432                                                "Cannot layout " + getClass().getSimpleName() + " correctly. Required pixels height: "
433                                                                + height + ". Pixels height available: " + availableH);
434                }
435
436                final int gridHeight = bg.gridHeight();
437
438                final int totalVerticalMargin = gridHeight - vcells;
439                /*
440                 * Inexact division is floored (3/2 = 1), hence this aligns to the top.
441                 */
442                this.topMargin = totalVerticalMargin / 2;
443
444                if (buttonsTexts == null)
445                        throw new NullPointerException(
446                                        "The text of buttons must be set before displaying a " + getClass().getSimpleName());
447
448                this.buttons = new ArrayList<>(buttonsTexts.size());
449
450                return putAll0(addListener, putBordersAndMargins, 0, false);
451        }
452
453        /**
454         * Missing doc: see {@link #putAll(boolean, boolean)}.
455         * 
456         * @param scroll
457         *            Whether this is a scrolling request.
458         */
459        private InputProcessor putAll0(boolean addListener, boolean putBordersAndMargins, int buttonStartingIndex,
460                        boolean scroll) {
461                if (bgColor != null) {
462                        /* Paint whole background */
463                        fill(ICombinedPanel.What.BG, bgColor);
464                }
465
466                if (scroll) {
467                        /*
468                         * Repaint whole foreground, to avoid leaving the end of longest
469                         * entries visible.
470                         */
471                        fill(ICombinedPanel.What.FG, bgColor);
472                }
473
474                final int nbButtons = buttonsTexts.size();
475                final int nbVDisplayedCells = enableScrolling ? bg.gridHeight() : Integer.MAX_VALUE;
476
477                int m = topMargin;
478                /* In number of cells */
479                final int buttonHeight = buttonCellsHeight();
480                int nbb = 0;
481                int i = buttonStartingIndex;
482                boolean first = true;
483                for (; i < nbButtons; i++, nbb++) {
484                        final int alignment;
485                        final int nextm = m + buttonHeight + interButtonMargin;
486                        if (buttonsAlignment != null && i < buttonsAlignment.length)
487                                alignment = buttonsAlignment[i];
488                        else
489                                /* The default: centering */
490                                alignment = 0;
491                        /*
492                         * Lhs: '...' for 'scroll up' (first button). Rhs: '...' for scoll
493                         * down (last button).
494                         */
495                        final boolean last = i == nbButtons - 1;
496                        final boolean firstDots = first && 0 < i;
497                        final boolean lastDots = !last && (nbVDisplayedCells <= nextm);
498                        putButton(i, m, alignment, putBordersAndMargins, firstDots || lastDots, nbb, scroll);
499                        if (firstDots || lastDots)
500                                i--;
501                        m = nextm;
502                        first = false;
503                        if (nbVDisplayedCells <= m)
504                                break;
505                }
506
507                firstLastButtonIndexes = new FirstAndLastButtonIndex(buttonStartingIndex, i);
508                if ((firstLastButtonIndexes.last - firstLastButtonIndexes.start) < buttonsTexts.size()) {
509                        Gdx.app.log(SquidTags.LAYOUT, "Displaying buttons " + firstLastButtonIndexes + ", out of [0,"
510                                        + (buttonsTexts.size() - 1) + "].");
511                }
512
513                if (putBordersAndMargins)
514                        putBorder();
515
516                final InputMultiplexer result = new InputMultiplexer();
517                /*
518                 * First put the key handler. It'd be nice to use SquidLib's KeyHandler
519                 * interface (it'd be more convenient in #selectedButton for complex
520                 * inputs), but it requires more work.
521                 */
522                result.addProcessor(buildKeyInputProcessor());
523                /*
524                 * Then the mouse handler. SquidInput is required here, since it does
525                 * the translation from libgdx's coordinates to SquidLib coordinates.
526                 */
527                result.addProcessor(new SquidInput(new SquidMouse(bg.cellWidth(), bg.cellHeight(), bg.gridWidth(),
528                                bg.gridHeight(), 0, 0, buildMouseInputProcessor())));
529
530                if (addListener) {
531                        addListener(new InputListener() {
532
533                                @Override
534                                public boolean keyDown(InputEvent event, int keycode) {
535                                        return result.keyDown(keycode);
536                                }
537
538                                @Override
539                                public boolean keyUp(InputEvent event, int keycode) {
540                                        return result.keyUp(keycode);
541                                }
542
543                                @Override
544                                public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
545                                        return result.touchDown(MathUtils.round(x), MathUtils.round(y), pointer, button);
546                                }
547
548                                @Override
549                                public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
550                                        result.touchUp(MathUtils.round(x), MathUtils.round(y), pointer, buttonHeight);
551                                }
552
553                                @Override
554                                public boolean keyTyped(InputEvent event, char character) {
555                                        return result.keyTyped(character);
556                                }
557
558                        });
559                }
560
561                putHook();
562
563                return result;
564        }
565
566        /**
567         * Draws the margins around the buttons and the border around the panel.
568         * Because this uses a {@link ShapeRenderer}, margins do not move if
569         * {@code this} is moved (such as with
570         * {@link Actions#moveTo(float, float, float)}). This method's purpose is to
571         * draw margins in such a case.
572         * 
573         * <p>
574         * Remember that, when using {@link SquidPanel}s; margins (more generally:
575         * anything done with {@link ShapeRenderer}) should always be drawn after
576         * everything else, so when sliding/moving, your code should be like:
577         * 
578         * <pre>
579         * stage.act()   <- if the panel is moving
580         * stage.draw(); <- of course, 'stage' is the Stage containing {@code this}
581         * buttonsPanel.putMarginsAndBorder();
582         * </pre>
583         * </p>
584         */
585        public void putButtonsMarginsAndBorder() {
586                if (buttons == null)
587                        /* Not set yet */
588                        return;
589
590                /* Margins around buttons */
591                final int bound = buttons.size();
592                /* index-based loop, to avoid allocating the Iterator */
593                for (int i = 0; i < bound; i++) {
594                        final Rectangle r = buttons.get(i);
595                        displayMarginsAround(r.botLeftX, r.botLeftY, r.width, r.height);
596                }
597
598                /* The border */
599                putBorder();
600        }
601
602        /**
603         * This method should only be called after {@link #putAll(boolean, boolean)}
604         * .
605         * 
606         * @return The number of cells that the buttons of {@code this} spans,
607         *         horizontally. Ignores margins, but not padding.
608         * @throws IllegalStateException
609         *             If {@link #putAll(boolean, boolean)} wasn't called yet.
610         */
611        public int getHCells() {
612                if (hcells == -1)
613                        throw new IllegalStateException("This method should be called after #putAll");
614                return hcells;
615        }
616
617        /**
618         * This method should only be called after {@link #putAll(boolean, boolean)}
619         * .
620         * 
621         * @return The number of cells that the buttons of {@code this} spans,
622         *         vertically. Ignores margins, but not padding.
623         * @throws IllegalStateException
624         *             If {@link #putAll(boolean, boolean)} wasn't called yet.
625         */
626        public int getVCells() {
627                if (vcells == -1)
628                        throw new IllegalStateException("This method should be called after #putAll");
629                return vcells;
630        }
631
632        protected InputProcessor buildMouseInputProcessor() {
633                return new InputAdapter() {
634
635                        @Override
636                        public boolean touchDown(int screenX, int screenY, int pointer, int button) {
637                                /* Because we wanna receive touchUp */
638                                return true;
639                        }
640
641                        @Override
642                        public boolean touchUp(int x, int gdxy, int pointer, int button) {
643                                final int y;
644                                if (y_gdxToSquid())
645                                        y = getGridHeight() - (gdxy + 1);
646                                else
647                                        y = gdxy;
648                                final int bound = buttons.size();
649                                for (int i = 0; i < bound; i++) {
650                                        final Rectangle r = buttons.get(i);
651                                        if (x < r.botLeftX)
652                                                /* Too much to the left */
653                                                continue;
654                                        if (y < r.botLeftY - r.height)
655                                                /* Too high */
656                                                continue;
657                                        if (r.botLeftY <= y)
658                                                /* Too low */
659                                                continue;
660                                        if (r.botLeftX + r.width <= x)
661                                                /* Too much to the right */
662                                                continue;
663
664                                        /* It's a hit! */
665
666                                        if (i == 0 && hasScrollUp()) {
667                                                final boolean b = scrollUp(Input.Keys.UP);
668                                                assert b;
669                                                return true;
670                                        } else if (i == scrollDownButtonIndex()) {
671                                                final boolean b = scrollDown(Input.Keys.DOWN);
672                                                assert b;
673                                                return true;
674                                        }
675
676                                        final int j = i + firstLastButtonIndexes.start + (hasScrollUp() ? -1 : 0);
677
678                                        if (doNotBind == null || !doNotBind.contains(j)) {
679                                                if (j < buttonsTexts.size()) {
680                                                        /* Send event */
681                                                        selectedButton(j);
682                                                } else {
683                                                        /* Should not happen */
684                                                        Gdx.app.log(SquidTags.LAYOUT,
685                                                                        "Skipping invalid button index in " + getClass().getSimpleName() + ": "
686                                                                                        + j + ". Maximum index is " + (buttons.size() - 1)
687                                                                                        + ". This is a bug.");
688                                                }
689                                        }
690
691                                        return true;
692                                }
693
694                                return false;
695                        }
696
697                        @Override
698                        public boolean touchDragged(int screenX, int screenY, int pointer) {
699                                /* Not handled for now */
700                                return false;
701                        }
702
703                        @Override
704                        public boolean mouseMoved(int screenX, int screenY) {
705                                /* Not handled for now */
706                                return false;
707                        }
708
709                        @Override
710                        public boolean scrolled(int amount) {
711                                /* Not handled for now */
712                                return false;
713                        }
714                };
715        }
716
717        protected InputProcessor buildKeyInputProcessor() {
718                return new InputAdapter() {
719
720                        @Override
721                        public boolean keyDown(int keycode) {
722                                return false;
723                        }
724
725                        @Override
726                        public boolean keyUp(int keycode) {
727                                if (scrollDown(keycode)) {
728                                        return true;
729                                } else if (scrollUp(keycode)) {
730                                        return true;
731                                } else
732                                        return super.keyDown(keycode);
733                        }
734
735                        @Override
736                        public boolean keyTyped(char character) {
737                                if (shortcuts == null)
738                                        return false;
739                                Integer buttonIndex = shortcuts.get(Character.toLowerCase(character));
740                                if (buttonIndex != null) {
741                                        selectedButton(buttonIndex);
742                                        return true;
743                                } else
744                                        return false;
745                        }
746                };
747        }
748
749        /**
750         * This method can be left unimplemented if you give the panels at
751         * construction time (constructor
752         * {@link #ButtonsPanel(ISquidPanel, ISquidPanel, List)}.
753         * 
754         * @param width
755         *            The width that the panel must have.
756         * @param height
757         *            The height that the panel must have.
758         * @return A freshly allocated {@link ISquidPanel}.
759         */
760        protected abstract ISquidPanel<T> buildPanel(int width, int height);
761
762        /**
763         * This method is called when the button at index {@code i} is hit.
764         * 
765         * @param i
766         *            The index of a button (starts at 0).
767         */
768        protected abstract void selectedButton(int i);
769
770        /**
771         * smelC: when I plug {@link #putAll(boolean, boolean)} result directly into
772         * {@link Gdx#input} (i.e. when I give {@code false} as the first argument
773         * to {@code putAll}), I leave this definition. When I give {@code putAll}
774         * {@code true}, and the listener is plugged behind a {@link Stage}, I
775         * redefine this method to return {@code true}.
776         * 
777         * @return {@code true} if the y-coordinate must be translated from libgdx
778         *         coordinates (0,0) at bottom left to squid coordinates (0,0) at
779         *         top left.
780         */
781        protected boolean y_gdxToSquid() {
782                return false;
783        }
784
785        /**
786         * @param botLeftX
787         *            The bottom left x cell of the button's inside, in squidlib's
788         *            coordinates ((0,0) is top left).
789         * @param botLeftY
790         *            The bottom left y cell of the button's inside, in squidlib's
791         *            coordinates ((0,0) is top left).
792         * @param width
793         *            The width of the button considered.
794         * @param height
795         *            The width of the button considered.
796         */
797        protected void displayMarginsAround(int botLeftX, int botLeftY, int width, int height) {
798                if (buttonMargin == 0 || buttonsMarginColor == null)
799                        /* Nothing to do */
800                        return;
801
802                /* Actor's bottom left */
803                final float x = getX();
804                final float y = getY();
805
806                final int cw = bg.cellWidth();
807                final int ch = bg.cellHeight();
808
809                /* Button button left, in libgdx's coordinates */
810                final float gdxx = x + botLeftX * cw;
811                final float gdxy = y + ((bg.gridHeight() - botLeftY) * ch);
812
813                /* Width of the button's inside */
814                final int w = width * cw;
815                /* Height of the button's inside */
816                final int h = height * ch;
817
818                UIUtil.drawMarginsAround(null, gdxx, gdxy, w, h, buttonMargin, buttonsMarginColor, cornerStyle);
819        }
820
821        protected int pixelsWidth() {
822                return bg.cellWidth() * bg.gridWidth();
823        }
824
825        protected int pixelsHeight() {
826                return bg.cellHeight() * bg.gridHeight();
827        }
828
829        /**
830         * Method that paint's the background of a button's inside.
831         * 
832         * @param xoff
833         *            The x offset
834         * @param yoff
835         *            The y offset
836         * @param width
837         *            The inside's width
838         * @param height
839         *            The inside's height
840         */
841        protected void putButtonInside(int xoff, int yoff, int width, int height) {
842                if (insideButtonBGColor == null)
843                        return;
844
845                for (int x = 0; x < width; x++) {
846                        for (int w = 0; w < height; w++)
847                                putBG(x + xoff, w + yoff, insideButtonBGColor);
848                }
849        }
850
851        /**
852         * Paints the border
853         */
854        protected void putBorder() {
855                if (0 < borderMargin && borderColor != null)
856                        UIUtil.drawMarginsAround(null, getX(), getY(), getHCells() * cellWidth(),
857                                        getVCells() * cellHeight(), borderMargin, borderColor, borderStyle);
858        }
859
860        /**
861         * @param buttonIndex
862         *            The index of the button for which the text is being built.
863         * @param text
864         * @return The text to display, with the coloring to highlight the shortcut.
865         */
866        protected IColoredString<T> putShortcut(int buttonIndex, IColoredString<T> text) {
867                assert 0 <= buttonIndex && buttonIndex < buttonsTexts.size();
868                if (shortcutCharacterColor == null || (doNotBind != null && doNotBind.contains(buttonIndex)))
869                        /*
870                         * Shortcuts are disabled or no shortcut should be put on this
871                         * button
872                         */
873                        return text;
874
875                /* @Nullable */ Character prevShortcut = null;
876
877                if (shortcuts == null)
878                        shortcuts = new HashMap<>();
879                else {
880                        for (Map.Entry<Character, Integer> entry : shortcuts.entrySet()) {
881                                if (entry.getValue() == buttonIndex) {
882                                        /* Shortcut was decided already, let's reuse it */
883                                        prevShortcut = entry.getKey();
884                                        break;
885                                }
886                        }
887                }
888
889                final IColoredString<T> result = new IColoredString.Impl<>();
890                boolean set = false;
891                for (IColoredString.Bucket<T> bucket : text) {
892                        final String bucketText = bucket.getText();
893                        final T bucketColor = bucket.getColor();
894                        if (set) {
895                                /* Append the bucket to the result */
896                                result.append(bucketText, bucketColor);
897                        } else {
898                                final int bound = bucketText.length();
899                                /* Iterate over the bucket's text */
900                                for (int i = 0; i < bound; i++) {
901                                        final char c = bucketText.charAt(i);
902                                        if (!set && prevShortcut != null && Character.toLowerCase(c) == prevShortcut) {
903                                                /* Shortcut was assigned already, let's display it */
904                                                result.append(c, shortcutCharacterColor);
905                                                /* To avoid coming here in next rolls */
906                                                set = true;
907                                        } else if (set || shortcuts.containsKey(Character.toLowerCase(c))
908                                                        || !canBeShortcut(c) || (unbindable != null && unbindable.contains(c))
909                                                        || (enableScrolling && (c == 'j' || c == 'k'))) {
910                                                /*
911                                                 * Shortcut already used or we went into the 'else'
912                                                 * already, or character is inadequate, or character can
913                                                 * be used to scroll (in vim-style).
914                                                 */
915                                                result.append(c, bucketColor);
916                                        } else {
917                                                /* Let's use 'c' as the shortcut! */
918                                                result.append(c, shortcutCharacterColor);
919                                                shortcuts.put(Character.toLowerCase(c), buttonIndex);
920                                                set = true;
921                                        }
922                                }
923                        }
924                }
925                return result;
926        }
927
928        /**
929         * Callback done when {@code this} is reput, because of a scrolling request
930         */
931        protected void putHook() {
932                /* Default implementation, you should override */
933        }
934
935        /**
936         * @param keycode
937         * @return Whether the request was handled.
938         */
939        protected boolean scrollDown(int keycode) {
940                if (enableScrolling
941                                && (keycode == Input.Keys.DOWN || keycode == Input.Keys.NUMPAD_2
942                                                || /* vim-style */ keycode == Input.Keys.J)
943                                && firstLastButtonIndexes.last < buttonsTexts.size() - 1) {
944                        /*
945                         * A request to scroll down && last displayed button is not last
946                         * button. If this is the first scroll, we straight go to the third
947                         * item (if possible, that's why there's the Math.min call), to
948                         * avoid just replacing the first one by '...'.
949                         */
950                        final int nbButtons = buttonsTexts.size();
951                        final int i = Math.min(firstLastButtonIndexes.start + (firstLastButtonIndexes.start == 0 ? 2 : 1),
952                                        nbButtons - 1);
953                        Gdx.app.log(SquidTags.LAYOUT, "Putting buttons of " + ButtonsPanel.this.getClass().getSimpleName()
954                                        + " from index " + i + " when scrolling down");
955                        ButtonsPanel.this.putAll0(false, false, i, true);
956                        return true;
957                } else
958                        return false;
959        }
960
961        protected boolean scrollUp(int keycode) {
962                if (enableScrolling && (keycode == Input.Keys.UP || keycode == Input.Keys.NUMPAD_8
963                                || /* vim-style */ keycode == Input.Keys.K) && 0 < firstLastButtonIndexes.start) {
964                        /*
965                         * A request to scroll up && first displayed button is not the first
966                         * button
967                         */
968                        final int i = firstLastButtonIndexes.start - 1;
969                        Gdx.app.log(SquidTags.LAYOUT, "Putting buttons of " + ButtonsPanel.this.getClass().getSimpleName()
970                                        + " from index " + i + " when scrolling up");
971                        ButtonsPanel.this.putAll0(false, false, i, true);
972                        return true;
973                } else
974                        return false;
975        }
976
977        /**
978         * @return The index of the scroll down button, if any. Otherwise -1.
979         */
980        protected int scrollDownButtonIndex() {
981                if (enableScrolling) {
982                        if (firstLastButtonIndexes.nbButtons() == buttonsTexts.size())
983                                /* No scrolling */
984                                return -1;
985                        else {
986                                if (firstLastButtonIndexes.start == 0)
987                                        return firstLastButtonIndexes.last + 1;
988                                else {
989                                        /**
990                                         * After scrolling down from:
991                                         * 
992                                         * <pre>
993                                         * i0
994                                         * i1
995                                         * ...
996                                         * </pre>
997                                         * 
998                                         * we have
999                                         * 
1000                                         * <pre>
1001                                         * ...
1002                                         * i2
1003                                         * i3
1004                                         * ...
1005                                         * </pre>
1006                                         * 
1007                                         * and start=2 and end=3
1008                                         */
1009                                        return firstLastButtonIndexes.nbButtons() + 2;
1010                                }
1011                        }
1012                } else
1013                        return -1;
1014        }
1015
1016        /**
1017         * @return Whether the scroll up button is being shown.
1018         */
1019        protected boolean hasScrollUp() {
1020                return enableScrolling && 0 < firstLastButtonIndexes.start;
1021        }
1022
1023        /**
1024         * @return Whether the scroll down button is being shown.
1025         */
1026        protected boolean hasScrollDown() {
1027                if (enableScrolling) {
1028                        return firstLastButtonIndexes.last < buttonsTexts.size();
1029                } else
1030                        return false;
1031        }
1032
1033        protected boolean canBeShortcut(char c) {
1034                return Character.isLetter(c);
1035        }
1036
1037        /**
1038         * @param i
1039         *            The button index with {@link #buttonsTexts}.
1040         * @param y
1041         *            The y offset from the panel's top.
1042         * @param alignment
1043         *            The alignment, in the format of {@link #buttonsAlignment}.
1044         * @param displayVDots
1045         *            if {@link #scrollText} must be displayed instead of the
1046         *            button's text.
1047         * @param nbb
1048         *            The index with {@link #buttons}.
1049         * @param scroll
1050         *            Whether this is a scrolling request.
1051         */
1052        private void putButton(int i, int y, int alignment, boolean putMargins, boolean displayVDots, int nbb,
1053                        boolean scroll) {
1054                final IColoredString<T> text = displayVDots ? IColoredString.Impl.<T> create(scrollText, null)
1055                                : buttonsTexts.get(i);
1056                final int textLength = text.length();
1057
1058                final int insideWidth = textLength + (xpadding * 2);
1059                final int insideHeight = 1 + (ypadding * 2);
1060
1061                final int gridWidth = getGridWidth();
1062                final int center = gridWidth / 2;
1063                /* The leftmost cell of the button's inside */
1064                final int left;
1065                /* The left most cell of the button's text */
1066                final int textLeft;
1067                if (alignment == -1) {
1068                        /* Align to left */
1069                        left = 0;
1070                        textLeft = 0;
1071                } else if (alignment == 1) {
1072                        /* Align to right */
1073                        left = gridWidth - insideWidth;
1074                        textLeft = gridWidth - textLength;
1075                } else {
1076                        /* Align to center */
1077                        left = center - (insideWidth / 2);
1078                        textLeft = center - textLength / 2;
1079                }
1080
1081                /* Paint the button's inside's background */
1082                if (insideButtonBGColor != null) {
1083                        for (int x = 0; x < insideWidth; x++) {
1084                                for (int w = 0; w < insideHeight; w++)
1085                                        putBG(x + left, w + y, insideButtonBGColor);
1086                        }
1087                }
1088
1089                /* Now place text */
1090                putFG(textLeft, y + (insideHeight / 2), putShortcut(i, text));
1091
1092                final int botLeftY = y + insideHeight;
1093
1094                if (putMargins)
1095                        /* Finally, paint margins */
1096                        displayMarginsAround(left, botLeftY, insideWidth, insideHeight);
1097
1098                if (this.buttons == null)
1099                        throw new NullPointerException(
1100                                        getClass().getSimpleName() + ": this.buttons should not be null at this moment");
1101
1102                if (scroll)
1103                        this.buttons.get(nbb).setWidth(insideWidth);
1104                else
1105                        this.buttons.add(new Rectangle(left, botLeftY, insideWidth, insideHeight));
1106        }
1107
1108        /**
1109         * This method's implementation should not inspect {@link #fg} or
1110         * {@link #bg}, because they may not have been set yet
1111         */
1112        private int computeRequiredCellsWidth() {
1113                int maxButtonWidth = 0;
1114                for (IColoredString<?> cs : buttonsTexts) {
1115                        final int local = cs.length();
1116                        if (maxButtonWidth < local)
1117                                maxButtonWidth = local;
1118                }
1119                final int nbCells = maxButtonWidth + (xpadding * 2);
1120                return nbCells;
1121        }
1122
1123        /**
1124         * This method's implementation should not only rely on {@link #fg} or
1125         * {@link #bg}, because they may not have been set yet
1126         */
1127        private int computeRequiredCellsHeight() {
1128                final int nbButtons = buttonsTexts.size();
1129                final int nbCells = nbButtons + (nbButtons * 2 * ypadding) + ((nbButtons - 1) * interButtonMargin);
1130                /*
1131                 * Call to max to avoid returning something negative (can happen if
1132                 * nbButtons == 0)
1133                 */
1134                return Math.max(nbCells, 0);
1135        }
1136
1137        /**
1138         * @return The height of one button, in number of cells.
1139         */
1140        private int buttonCellsHeight() {
1141                /* The button's text + the padding at the bottom and the top */
1142                return 1 + ypadding * 2;
1143        }
1144
1145        /**
1146         * A convenience subclass for people that give preallocated
1147         * {@link ISquidPanel}s. A single abstract method remains to be implemented:
1148         * what to do when a button is pressed, i.e.
1149         * {@link ButtonsPanel#selectedButton(int)}.
1150         * 
1151         * @author smelC
1152         */
1153        public static abstract class PreAllocatedPanels<T extends Color> extends ButtonsPanel<T> {
1154
1155                protected PreAllocatedPanels(ISquidPanel<T> bg, ISquidPanel<T> fg,
1156                                List<IColoredString<T>> buttonTexts) {
1157                        super(bg, fg, buttonTexts);
1158                }
1159
1160                @Override
1161                protected ISquidPanel<T> buildPanel(int width, int height) {
1162                        throw new IllegalStateException("This method should not be called");
1163                }
1164
1165        }
1166
1167        /**
1168         * @author smelC
1169         */
1170        protected static class Rectangle {
1171
1172                /**
1173                 * The bottom left coordinate of this rectangle (in number of cells), in
1174                 * SquidLib's coordinates.
1175                 */
1176                protected final int botLeftX;
1177                /**
1178                 * The bottom left coordinate of this rectangle (in number of cells), in
1179                 * SquidLib's coordinates.
1180                 */
1181                protected final int botLeftY;
1182
1183                /** The width of this rectangle, in number of cells */
1184                protected int width;
1185
1186                /** The height of this rectangle, in number of cells */
1187                protected final int height;
1188
1189                public Rectangle(int botLeftX, int botLeftY, int width, int height) {
1190                        this.botLeftX = check(botLeftX, "bottom left x");
1191                        this.botLeftY = check(botLeftY, "bottom left y");
1192                        this.width = check(width, "width");
1193                        this.height = check(height, "height");
1194                }
1195
1196                /**
1197                 * To call when scrolling happens, and a button's text changes.
1198                 * 
1199                 * @param width
1200                 */
1201                protected void setWidth(int width) {
1202                        this.width = width;
1203                }
1204
1205                private int check(int i, String s) {
1206                        if (i < 0)
1207                                Gdx.app.log("layout", "Invalid rectangle component (" + s + "): " + i);
1208                        return i;
1209                }
1210
1211                @Override
1212                public String toString() {
1213                        return "Rectangle at (" + botLeftX + "," + botLeftY + "), width: " + width + ", height:" + height;
1214                }
1215        }
1216
1217        /**
1218         * @author smelC
1219         */
1220        protected static class FirstAndLastButtonIndex {
1221
1222                protected final int start;
1223                protected final int last;
1224
1225                protected FirstAndLastButtonIndex(int start, int last) {
1226                        this.start = start;
1227                        this.last = last;
1228                }
1229
1230                int nbButtons() {
1231                        return last - start;
1232                }
1233
1234                @Override
1235                public String toString() {
1236                        return "[" + start + "," + last + "]";
1237                }
1238        }
1239}