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}