예제 #1
0
  @Override
  public TerminalSize getPreferredSize(List<Component> components) {
    TerminalSize preferredSize = TerminalSize.ZERO;
    if (components.isEmpty()) {
      return preferredSize.withRelative(
          leftMarginSize + rightMarginSize, topMarginSize + bottomMarginSize);
    }

    Component[][] table = buildTable(components);
    table = eliminateUnusedRowsAndColumns(table);

    // Figure out each column first, this can be done independently of the row heights
    int preferredWidth = 0;
    int preferredHeight = 0;
    for (int width : getPreferredColumnWidths(table)) {
      preferredWidth += width;
    }
    for (int height : getPreferredRowHeights(table)) {
      preferredHeight += height;
    }
    preferredSize = preferredSize.withRelative(preferredWidth, preferredHeight);
    preferredSize =
        preferredSize.withRelativeColumns(
            leftMarginSize + rightMarginSize + (table[0].length - 1) * horizontalSpacing);
    preferredSize =
        preferredSize.withRelativeRows(
            topMarginSize + bottomMarginSize + (table.length - 1) * verticalSpacing);
    return preferredSize;
  }
예제 #2
0
 @Override
 public void scrollLines(int firstLine, int lastLine, int distance) {
   if (firstLine < 0) {
     firstLine = 0;
   }
   if (lastLine >= size.getRows()) {
     lastLine = size.getRows() - 1;
   }
   if (firstLine < lastLine) {
     if (distance > 0) {
       // scrolling up: start with first line as target:
       int curLine = firstLine;
       // copy lines from further "below":
       for (; curLine <= lastLine - distance; curLine++) {
         buffer[curLine] = buffer[curLine + distance];
       }
       // blank out the remaining lines:
       for (; curLine <= lastLine; curLine++) {
         buffer[curLine] = newBlankLine();
       }
     } else if (distance < 0) {
       // scrolling down: start with last line as target:
       int curLine = lastLine;
       distance = -distance;
       // copy lines from further "above":
       for (; curLine >= firstLine + distance; curLine--) {
         buffer[curLine] = buffer[curLine - distance];
       }
       // blank out the remaining lines:
       for (; curLine >= firstLine; curLine--) {
         buffer[curLine] = newBlankLine();
       }
     } /* else: distance == 0 => no-op */
   }
 }
 private void buttonMoveCursorActionPerformed(
     java.awt.event.ActionEvent evt) { // GEN-FIRST:event_buttonMoveCursorActionPerformed
   TerminalSize terminalSize = scrollingSwingTerminal.getTerminalSize();
   Random random = new Random();
   scrollingSwingTerminal.setCursorPosition(
       random.nextInt(terminalSize.getColumns()), random.nextInt(terminalSize.getRows()));
   scrollingSwingTerminal.flush();
 } // GEN-LAST:event_buttonMoveCursorActionPerformed
예제 #4
0
  @Override
  public void copyTo(
      TextImage destination,
      int startRowIndex,
      int rows,
      int startColumnIndex,
      int columns,
      int destinationRowOffset,
      int destinationColumnOffset) {

    // If the source image position is negative, offset the whole image
    if (startColumnIndex < 0) {
      destinationColumnOffset += -startColumnIndex;
      columns += startColumnIndex;
      startColumnIndex = 0;
    }
    if (startRowIndex < 0) {
      startRowIndex += -startRowIndex;
      rows = startRowIndex;
      startRowIndex = 0;
    }
    // Make sure we can't copy more than is available
    columns = Math.min(buffer[0].length - startColumnIndex, columns);
    rows = Math.min(buffer.length - startRowIndex, rows);

    // Adjust target lengths as well
    columns = Math.min(destination.getSize().getColumns() - destinationColumnOffset, columns);
    rows = Math.min(destination.getSize().getRows() - destinationRowOffset, rows);

    if (columns <= 0 || rows <= 0) {
      return;
    }

    TerminalSize destinationSize = destination.getSize();
    if (destination instanceof BasicTextImage) {
      int targetRow = destinationRowOffset;
      for (int y = startRowIndex;
          y < startRowIndex + rows && targetRow < destinationSize.getRows();
          y++) {
        System.arraycopy(
            buffer[y],
            startColumnIndex,
            ((BasicTextImage) destination).buffer[targetRow++],
            destinationColumnOffset,
            columns);
      }
    } else {
      // Manually copy character by character
      for (int y = startRowIndex; y < startRowIndex + rows; y++) {
        for (int x = startColumnIndex; x < startColumnIndex + columns; x++) {
          destination.setCharacterAt(
              x - startColumnIndex + destinationColumnOffset,
              y - startRowIndex + destinationRowOffset,
              buffer[y][x]);
        }
      }
    }
  }
  @Override
  public void onAdded(WindowBasedTextGUI textGUI, Window window, List<Window> allWindows) {
    WindowDecorationRenderer decorationRenderer = getWindowDecorationRenderer(window);
    TerminalSize expectedDecoratedSize =
        decorationRenderer.getDecoratedSize(window, window.getPreferredSize());
    window.setDecoratedSize(expectedDecoratedSize);

    if (window.getHints().contains(Window.Hint.FIXED_POSITION)) {
      // Don't place the window, assume the position is already set
    } else if (allWindows.isEmpty()) {
      window.setPosition(TerminalPosition.OFFSET_1x1);
    } else if (window.getHints().contains(Window.Hint.CENTERED)) {
      int left = (lastKnownScreenSize.getColumns() - expectedDecoratedSize.getColumns()) / 2;
      int top = (lastKnownScreenSize.getRows() - expectedDecoratedSize.getRows()) / 2;
      window.setPosition(new TerminalPosition(left, top));
    } else {
      TerminalPosition nextPosition =
          allWindows.get(allWindows.size() - 1).getPosition().withRelative(2, 1);
      if (nextPosition.getColumn() + expectedDecoratedSize.getColumns()
              > lastKnownScreenSize.getColumns()
          || nextPosition.getRow() + expectedDecoratedSize.getRows()
              > lastKnownScreenSize.getRows()) {
        nextPosition = TerminalPosition.OFFSET_1x1;
      }
      window.setPosition(nextPosition);
    }

    // Finally, run through the usual calculations so the window manager's usual prepare method can
    // have it's say
    prepareWindow(lastKnownScreenSize, window);
  }
예제 #6
0
  @Override
  public TextGraphics drawImage(
      TerminalPosition topLeft,
      TextImage image,
      TerminalPosition sourceImageTopLeft,
      TerminalSize sourceImageSize) {

    // If the source image position is negative, offset the whole image
    if (sourceImageTopLeft.getColumn() < 0) {
      topLeft = topLeft.withRelativeColumn(-sourceImageTopLeft.getColumn());
      sourceImageSize = sourceImageSize.withRelativeColumns(sourceImageTopLeft.getColumn());
      sourceImageTopLeft = sourceImageTopLeft.withColumn(0);
    }
    if (sourceImageTopLeft.getRow() < 0) {
      topLeft = topLeft.withRelativeRow(-sourceImageTopLeft.getRow());
      sourceImageSize = sourceImageSize.withRelativeRows(sourceImageTopLeft.getRow());
      sourceImageTopLeft = sourceImageTopLeft.withRow(0);
    }

    // cropping specified image-subrectangle to the image itself:
    int fromRow = Math.max(sourceImageTopLeft.getRow(), 0);
    int untilRow =
        Math.min(
            sourceImageTopLeft.getRow() + sourceImageSize.getRows(), image.getSize().getRows());
    int fromColumn = Math.max(sourceImageTopLeft.getColumn(), 0);
    int untilColumn =
        Math.min(
            sourceImageTopLeft.getColumn() + sourceImageSize.getColumns(),
            image.getSize().getColumns());

    // difference between position in image and position on target:
    int diffRow = topLeft.getRow() - sourceImageTopLeft.getRow();
    int diffColumn = topLeft.getColumn() - sourceImageTopLeft.getColumn();

    // top/left-crop at target(TextGraphics) rectangle: (only matters, if topLeft has a negative
    // coordinate)
    fromRow = Math.max(fromRow, -diffRow);
    fromColumn = Math.max(fromColumn, -diffColumn);

    // bot/right-crop at target(TextGraphics) rectangle: (only matters, if topLeft has a negative
    // coordinate)
    untilRow = Math.min(untilRow, getSize().getRows() - diffRow);
    untilColumn = Math.min(untilColumn, getSize().getColumns() - diffColumn);

    if (fromRow >= untilRow || fromColumn >= untilColumn) {
      return this;
    }
    for (int row = fromRow; row < untilRow; row++) {
      for (int column = fromColumn; column < untilColumn; column++) {
        setCharacter(column + diffColumn, row + diffRow, image.getCharacterAt(column, row));
      }
    }
    return this;
  }
예제 #7
0
 @Override
 public void drawComponent(TextGUIGraphics graphics) {
   TerminalSize area = graphics.getSize();
   String absolutePath = directory.getAbsolutePath();
   if (area.getColumns() < absolutePath.length()) {
     absolutePath = absolutePath.substring(absolutePath.length() - area.getColumns());
     absolutePath = "..." + absolutePath.substring(Math.min(absolutePath.length(), 3));
   }
   setText(absolutePath);
   super.drawComponent(graphics);
 }
예제 #8
0
 @Override
 public BasicTextImage resize(TerminalSize newSize, TextCharacter filler) {
   if (newSize == null || filler == null) {
     throw new IllegalArgumentException(
         "Cannot resize BasicTextImage with null " + (newSize == null ? "newSize" : "filler"));
   }
   if (newSize.getRows() == buffer.length
       && (buffer.length == 0 || newSize.getColumns() == buffer[0].length)) {
     return this;
   }
   return new BasicTextImage(newSize, buffer, filler);
 }
예제 #9
0
 @Override
 public TextGraphics newTextGraphics(TerminalPosition topLeftCorner, TerminalSize size)
     throws IllegalArgumentException {
   TerminalSize writableArea = getSize();
   if (topLeftCorner.getColumn() + size.getColumns() <= 0
       || topLeftCorner.getColumn() >= writableArea.getColumns()
       || topLeftCorner.getRow() + size.getRows() <= 0
       || topLeftCorner.getRow() >= writableArea.getRows()) {
     // The area selected is completely outside of this TextGraphics, so we can return a "null"
     // object that doesn't
     // do anything because it is impossible to change anything anyway
     return new NullTextGraphics(size);
   }
   return new SubTextGraphics(this, topLeftCorner, size);
 }
예제 #10
0
 public TextBox(TerminalSize preferredSize) {
   this(
       preferredSize,
       (preferredSize != null && preferredSize.getRows() > 1)
           ? Style.MULTI_LINE
           : Style.SINGLE_LINE);
 }
예제 #11
0
 @Override
 public TerminalSize getPreferredSize(final ComboBox<V> comboBox) {
   TerminalSize size =
       TerminalSize.ONE.withColumns(
           (comboBox.getItemCount() == 0 ? CJKUtils.getColumnWidth(comboBox.getText()) : 0) + 2);
   synchronized (comboBox) {
     for (int i = 0; i < comboBox.getItemCount(); i++) {
       V item = comboBox.getItem(i);
       size =
           size.max(
               new TerminalSize(
                   CJKUtils.getColumnWidth(item.toString()) + 2 + 1,
                   1)); // +1 to add a single column of space
     }
   }
   return size;
 }
예제 #12
0
 public TextBox(TerminalSize preferredSize, String initialContent) {
   this(
       preferredSize,
       initialContent,
       (preferredSize != null && preferredSize.getRows() > 1) || initialContent.contains("\n")
           ? Style.MULTI_LINE
           : Style.SINGLE_LINE);
 }
예제 #13
0
 @Override
 public String toString() {
   StringBuilder sb = new StringBuilder(size.getRows() * (size.getColumns() + 1) + 50);
   sb.append('{')
       .append(size.getColumns())
       .append('x')
       .append(size.getRows())
       .append('}')
       .append('\n');
   for (TextCharacter[] line : buffer) {
     for (TextCharacter tc : line) {
       sb.append(tc.getCharacter());
     }
     sb.append('\n');
   }
   return sb.toString();
 }
예제 #14
0
 private int shrinkWidthToFitArea(TerminalSize area, int[] columnWidths) {
   int totalWidth = 0;
   for (int width : columnWidths) {
     totalWidth += width;
   }
   if (totalWidth > area.getColumns()) {
     int columnOffset = 0;
     do {
       if (columnWidths[columnOffset] > 0) {
         columnWidths[columnOffset]--;
         totalWidth--;
       }
       if (++columnOffset == numberOfColumns) {
         columnOffset = 0;
       }
     } while (totalWidth > area.getColumns());
   }
   return totalWidth;
 }
예제 #15
0
 private int shrinkHeightToFitArea(TerminalSize area, int[] rowHeights) {
   int totalHeight = 0;
   for (int height : rowHeights) {
     totalHeight += height;
   }
   if (totalHeight > area.getRows()) {
     int rowOffset = 0;
     do {
       if (rowHeights[rowOffset] > 0) {
         rowHeights[rowOffset]--;
         totalHeight--;
       }
       if (++rowOffset == rowHeights.length) {
         rowOffset = 0;
       }
     } while (totalHeight > area.getRows());
   }
   return totalHeight;
 }
예제 #16
0
 private int grabExtraVerticalSpace(
     TerminalSize area, int[] rowHeights, Set<Integer> expandableRows, int totalHeight) {
   for (int rowIndex : expandableRows) {
     rowHeights[rowIndex]++;
     totalHeight++;
     if (area.getColumns() == totalHeight) {
       break;
     }
   }
   return totalHeight;
 }
예제 #17
0
 private int grabExtraHorizontalSpace(
     TerminalSize area, int[] columnWidths, Set<Integer> expandableColumns, int totalWidth) {
   for (int columnIndex : expandableColumns) {
     columnWidths[columnIndex]++;
     totalWidth++;
     if (area.getColumns() == totalWidth) {
       break;
     }
   }
   return totalWidth;
 }
예제 #18
0
  /**
   * Creates a new BasicTextImage by copying a region of a two-dimensional array of TextCharacter:s.
   * If the area to be copied to larger than the source array, a filler character is used.
   *
   * @param size Size to create the new BasicTextImage as (and size to copy from the array)
   * @param toCopy Array to copy initial data from
   * @param initialContent Filler character to use if the source array is smaller than the requested
   *     size
   */
  private BasicTextImage(
      TerminalSize size, TextCharacter[][] toCopy, TextCharacter initialContent) {
    if (size == null || toCopy == null || initialContent == null) {
      throw new IllegalArgumentException(
          "Cannot create BasicTextImage with null "
              + (size == null ? "size" : (toCopy == null ? "toCopy" : "filler")));
    }
    this.size = size;

    int rows = size.getRows();
    int columns = size.getColumns();
    buffer = new TextCharacter[rows][];
    for (int y = 0; y < rows; y++) {
      buffer[y] = new TextCharacter[columns];
      for (int x = 0; x < columns; x++) {
        if (y < toCopy.length && x < toCopy[y].length) {
          buffer[y][x] = toCopy[y][x];
        } else {
          buffer[y][x] = initialContent;
        }
      }
    }
  }
예제 #19
0
  @Override
  public void doLayout(TerminalSize area, List<Component> components) {
    EnumMap<Location, Component> layout = makeLookupMap(components);
    int availableHorizontalSpace = area.getColumns();
    int availableVerticalSpace = area.getRows();

    // We'll need this later on
    int topComponentHeight = 0;
    int leftComponentWidth = 0;

    // First allocate the top
    if (layout.containsKey(Location.TOP)) {
      Component topComponent = layout.get(Location.TOP);
      topComponentHeight =
          Math.min(topComponent.getPreferredSize().getRows(), availableVerticalSpace);
      topComponent.setPosition(TerminalPosition.TOP_LEFT_CORNER);
      topComponent.setSize(new TerminalSize(availableHorizontalSpace, topComponentHeight));
      availableVerticalSpace -= topComponentHeight;
    }

    // Next allocate the bottom
    if (layout.containsKey(Location.BOTTOM)) {
      Component bottomComponent = layout.get(Location.BOTTOM);
      int bottomComponentHeight =
          Math.min(bottomComponent.getPreferredSize().getRows(), availableVerticalSpace);
      bottomComponent.setPosition(new TerminalPosition(0, area.getRows() - bottomComponentHeight));
      bottomComponent.setSize(new TerminalSize(availableHorizontalSpace, bottomComponentHeight));
      availableVerticalSpace -= bottomComponentHeight;
    }

    // Now divide the remaining space between LEFT, CENTER and RIGHT
    if (layout.containsKey(Location.LEFT)) {
      Component leftComponent = layout.get(Location.LEFT);
      leftComponentWidth =
          Math.min(leftComponent.getPreferredSize().getColumns(), availableHorizontalSpace);
      leftComponent.setPosition(new TerminalPosition(0, topComponentHeight));
      leftComponent.setSize(new TerminalSize(leftComponentWidth, availableVerticalSpace));
      availableHorizontalSpace -= leftComponentWidth;
    }
    if (layout.containsKey(Location.RIGHT)) {
      Component rightComponent = layout.get(Location.RIGHT);
      int rightComponentWidth =
          Math.min(rightComponent.getPreferredSize().getColumns(), availableHorizontalSpace);
      rightComponent.setPosition(
          new TerminalPosition(area.getColumns() - rightComponentWidth, topComponentHeight));
      rightComponent.setSize(new TerminalSize(rightComponentWidth, availableVerticalSpace));
      availableHorizontalSpace -= rightComponentWidth;
    }
    if (layout.containsKey(Location.CENTER)) {
      Component centerComponent = layout.get(Location.CENTER);
      centerComponent.setPosition(new TerminalPosition(leftComponentWidth, topComponentHeight));
      centerComponent.setSize(new TerminalSize(availableHorizontalSpace, availableVerticalSpace));
    }

    // Set the remaining components to 0x0
    for (Component component : components) {
      if (!layout.values().contains(component)) {
        component.setPosition(TerminalPosition.TOP_LEFT_CORNER);
        component.setSize(TerminalSize.ZERO);
      }
    }
  }
예제 #20
0
 private TextCharacter[] newBlankLine() {
   TextCharacter[] line = new TextCharacter[size.getColumns()];
   Arrays.fill(line, TextCharacter.DEFAULT_CHARACTER);
   return line;
 }
  /**
   * Called by {@link DefaultWindowManager} when iterating through all windows to decide their size
   * and position. If you override {@link DefaultWindowManager} to add your own logic to how windows
   * are placed on the screen, you can override this method and selectively choose which window to
   * interfere with. Note that the two key properties that are read by the GUI system after
   * preparing all windows are the position and decorated size. Your custom implementation should
   * set these two fields directly on the window. You can infer the decorated size from the content
   * size by using the window decoration renderer that is attached to the window manager.
   *
   * @param screenSize Size of the terminal that is available to draw on
   * @param window Window to prepare decorated size and position for
   */
  protected void prepareWindow(TerminalSize screenSize, Window window) {
    WindowDecorationRenderer decorationRenderer = getWindowDecorationRenderer(window);
    TerminalSize contentAreaSize;
    if (window.getHints().contains(Window.Hint.FIXED_SIZE)) {
      contentAreaSize = window.getSize();
    } else {
      contentAreaSize = window.getPreferredSize();
    }
    TerminalSize size = decorationRenderer.getDecoratedSize(window, contentAreaSize);
    TerminalPosition position = window.getPosition();

    if (window.getHints().contains(Window.Hint.FULL_SCREEN)) {
      position = TerminalPosition.TOP_LEFT_CORNER;
      size = screenSize;
    } else if (window.getHints().contains(Window.Hint.EXPANDED)) {
      position = TerminalPosition.OFFSET_1x1;
      size =
          screenSize.withRelative(
              -Math.min(4, screenSize.getColumns()), -Math.min(3, screenSize.getRows()));
      if (!size.equals(window.getDecoratedSize())) {
        window.invalidate();
      }
    } else if (window.getHints().contains(Window.Hint.FIT_TERMINAL_WINDOW)
        || window.getHints().contains(Window.Hint.CENTERED)) {
      // If the window is too big for the terminal, move it up towards 0x0 and if that's not enough
      // then shrink
      // it instead
      while (position.getRow() > 0 && position.getRow() + size.getRows() > screenSize.getRows()) {
        position = position.withRelativeRow(-1);
      }
      while (position.getColumn() > 0
          && position.getColumn() + size.getColumns() > screenSize.getColumns()) {
        position = position.withRelativeColumn(-1);
      }
      if (position.getRow() + size.getRows() > screenSize.getRows()) {
        size = size.withRows(screenSize.getRows() - position.getRow());
      }
      if (position.getColumn() + size.getColumns() > screenSize.getColumns()) {
        size = size.withColumns(screenSize.getColumns() - position.getColumn());
      }
      if (window.getHints().contains(Window.Hint.CENTERED)) {
        int left = (lastKnownScreenSize.getColumns() - size.getColumns()) / 2;
        int top = (lastKnownScreenSize.getRows() - size.getRows()) / 2;
        position = new TerminalPosition(left, top);
      }
    }

    window.setPosition(position);
    window.setDecoratedSize(size);
  }
예제 #22
0
  public FileDialog(
      String title,
      String description,
      String actionLabel,
      TerminalSize dialogSize,
      boolean showHiddenFilesAndDirs,
      File selectedObject) {
    super(title);
    this.selectedFile = null;
    this.showHiddenFilesAndDirs = showHiddenFilesAndDirs;

    if (selectedObject == null || !selectedObject.exists()) {
      selectedObject = new File("").getAbsoluteFile();
    }
    selectedObject = selectedObject.getAbsoluteFile();

    Panel contentPane = new Panel();
    contentPane.setLayoutManager(new GridLayout(2));

    if (description != null) {
      new Label(description)
          .setLayoutData(
              GridLayout.createLayoutData(
                  GridLayout.Alignment.BEGINNING, GridLayout.Alignment.CENTER, false, false, 2, 1))
          .addTo(contentPane);
    }

    int unitWidth = dialogSize.getColumns() / 3;
    int unitHeight = dialogSize.getRows();

    new FileSystemLocationLabel()
        .setLayoutData(
            GridLayout.createLayoutData(
                GridLayout.Alignment.FILL, GridLayout.Alignment.CENTER, true, false, 2, 1))
        .addTo(contentPane);

    fileListBox = new ActionListBox(new TerminalSize(unitWidth * 2, unitHeight));
    fileListBox
        .withBorder(Borders.singleLine())
        .setLayoutData(
            GridLayout.createLayoutData(
                GridLayout.Alignment.BEGINNING, GridLayout.Alignment.CENTER, false, false))
        .addTo(contentPane);
    directoryListBox = new ActionListBox(new TerminalSize(unitWidth, unitHeight));
    directoryListBox.withBorder(Borders.singleLine()).addTo(contentPane);

    fileBox =
        new TextBox()
            .setLayoutData(
                GridLayout.createLayoutData(
                    GridLayout.Alignment.FILL, GridLayout.Alignment.CENTER, true, false, 2, 1))
            .addTo(contentPane);

    new Separator(Direction.HORIZONTAL)
        .setLayoutData(
            GridLayout.createLayoutData(
                GridLayout.Alignment.FILL, GridLayout.Alignment.CENTER, true, false, 2, 1))
        .addTo(contentPane);

    okButton = new Button(actionLabel, new OkHandler());
    Panels.grid(2, okButton, new Button("Cancel", new CancelHandler()))
        .setLayoutData(
            GridLayout.createLayoutData(
                GridLayout.Alignment.END, GridLayout.Alignment.CENTER, false, false, 2, 1))
        .addTo(contentPane);

    if (selectedObject.isFile()) {
      directory = selectedObject.getParentFile();
      fileBox.setText(selectedObject.getName());
    } else if (selectedObject.isDirectory()) {
      directory = selectedObject;
    }

    reloadViews(directory);
    setComponent(contentPane);
  }
예제 #23
0
  @Override
  public void doLayout(TerminalSize area, List<Component> components) {
    // Sanity check, if the area is way too small, just return
    Component[][] table = buildTable(components);
    table = eliminateUnusedRowsAndColumns(table);

    if (area.equals(TerminalSize.ZERO)
        || table.length == 0
        || area.getColumns()
            <= leftMarginSize + rightMarginSize + ((table[0].length - 1) * horizontalSpacing)
        || area.getRows()
            <= bottomMarginSize + topMarginSize + ((table.length - 1) * verticalSpacing)) {
      return;
    }

    // Adjust area to the margins
    area = area.withRelative(-leftMarginSize - rightMarginSize, -topMarginSize - bottomMarginSize);

    Map<Component, TerminalSize> sizeMap = new IdentityHashMap<Component, TerminalSize>();
    Map<Component, TerminalPosition> positionMap =
        new IdentityHashMap<Component, TerminalPosition>();

    // Figure out each column first, this can be done independently of the row heights
    int[] columnWidths = getPreferredColumnWidths(table);

    // Take notes of which columns we can expand if the usable area is larger than what the
    // components want
    Set<Integer> expandableColumns = getExpandableColumns(table);

    // Next, start shrinking to make sure it fits the size of the area we are trying to lay out on.
    // Notice we subtract the horizontalSpacing to take the space between components into account
    TerminalSize areaWithoutHorizontalSpacing =
        area.withRelativeColumns(-horizontalSpacing * (table[0].length - 1));
    int totalWidth = shrinkWidthToFitArea(areaWithoutHorizontalSpacing, columnWidths);

    // Finally, if there is extra space, make the expandable columns larger
    while (areaWithoutHorizontalSpacing.getColumns() > totalWidth && !expandableColumns.isEmpty()) {
      totalWidth =
          grabExtraHorizontalSpace(
              areaWithoutHorizontalSpacing, columnWidths, expandableColumns, totalWidth);
    }

    // Now repeat for rows
    int[] rowHeights = getPreferredRowHeights(table);
    Set<Integer> expandableRows = getExpandableRows(table);
    TerminalSize areaWithoutVerticalSpacing =
        area.withRelativeRows(-verticalSpacing * (table.length - 1));
    int totalHeight = shrinkHeightToFitArea(areaWithoutVerticalSpacing, rowHeights);
    while (areaWithoutVerticalSpacing.getRows() > totalHeight && !expandableRows.isEmpty()) {
      totalHeight =
          grabExtraVerticalSpace(
              areaWithoutVerticalSpacing, rowHeights, expandableRows, totalHeight);
    }

    // Ok, all constraints are in place, we can start placing out components. To simplify, do it
    // horizontally first
    // and vertically after
    TerminalPosition tableCellTopLeft = TerminalPosition.TOP_LEFT_CORNER;
    for (int y = 0; y < table.length; y++) {
      tableCellTopLeft = tableCellTopLeft.withColumn(0);
      for (int x = 0; x < table[y].length; x++) {
        Component component = table[y][x];
        if (component != null && !positionMap.containsKey(component)) {
          GridLayoutData layoutData = getLayoutData(component);
          TerminalSize size = component.getPreferredSize();
          TerminalPosition position = tableCellTopLeft;

          int availableHorizontalSpace = 0;
          int availableVerticalSpace = 0;
          for (int i = 0; i < layoutData.horizontalSpan; i++) {
            availableHorizontalSpace += columnWidths[x + i] + (i > 0 ? horizontalSpacing : 0);
          }
          for (int i = 0; i < layoutData.verticalSpan; i++) {
            availableVerticalSpace += rowHeights[y + i] + (i > 0 ? verticalSpacing : 0);
          }

          // Make sure to obey the size restrictions
          size = size.withColumns(Math.min(size.getColumns(), availableHorizontalSpace));
          size = size.withRows(Math.min(size.getRows(), availableVerticalSpace));

          switch (layoutData.horizontalAlignment) {
            case CENTER:
              position =
                  position.withRelativeColumn((availableHorizontalSpace - size.getColumns()) / 2);
              break;
            case END:
              position = position.withRelativeColumn(availableHorizontalSpace - size.getColumns());
              break;
            case FILL:
              size = size.withColumns(availableHorizontalSpace);
              break;
            default:
              break;
          }
          switch (layoutData.verticalAlignment) {
            case CENTER:
              position = position.withRelativeRow((availableVerticalSpace - size.getRows()) / 2);
              break;
            case END:
              position = position.withRelativeRow(availableVerticalSpace - size.getRows());
              break;
            case FILL:
              size = size.withRows(availableVerticalSpace);
              break;
            default:
              break;
          }

          sizeMap.put(component, size);
          positionMap.put(component, position);
        }
        tableCellTopLeft = tableCellTopLeft.withRelativeColumn(columnWidths[x] + horizontalSpacing);
      }
      tableCellTopLeft = tableCellTopLeft.withRelativeRow(rowHeights[y] + verticalSpacing);
    }

    // Apply the margins here
    for (Component component : components) {
      component.setPosition(positionMap.get(component).withRelative(leftMarginSize, topMarginSize));
      component.setSize(sizeMap.get(component));
    }
    this.changed = false;
  }