blob: a2d047418e8f5424c185f25853f20023952bf016 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.ui;
import com.android.tools.idea.ui.ProportionalLayout.ColumnDefinition.Type;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.util.List;
import java.util.Map;
/**
* A layout manager which makes it easy to specify proportional grid layouts by hand.
* <p/>
* Unlike {@link GridBagLayout}, which requires setting complex constraints and hard to reason
* about weights, {@link ProportionalLayout} works by setting column definitions up front. A column
* can be fixed, fit-to-size, or proportional. When a layout is requested, fixed and fit-to-size
* columns are calculated first, and all remaining space is split by the proportional columns.
* Rows, in contrast, are always fit-to-size.
* <p/>
* When you register components with a panel using this layout, you need to associate it with a
* constraint telling it which row and column it should fit within. You should only associate one
* element per cell, and (unless that cell is sized to fit) the element will be stretched to fully
* contain the cell.
* <p/>
* Columns are pre-allocated, so it is an error to specify a cell whose column index is out of
* bounds. However, rows are unbounded - you can add a component at row 0 and then another at row
* 1000. Still, if a row doesn't have any components inside of it, it will simply be skipped during
* layout (meaning sparse layouts are collapsed).
*/
public final class ProportionalLayout implements LayoutManager2 {
/**
* A definition for a single column, indicating how its width will be calculated during a layout.
*/
public static final class ColumnDefinition {
public enum Type {
/**
* Shrink this column as small as possible to perfectly fit all its contents.
* <p/>
* For fit columns, {@link #getValue()} has no meaning.
*/
Fit,
/**
* Set this column to a fixed width in pixels.
* <p/>
* For fixed columns, {@link #getValue()} is a width in pixels.
*/
Fixed,
// TODO: Add a new definition for spacing based on number of characters, inspired by
// (but not quite the same as) Android's 'sp' type. See:
// http://developer.android.com/guide/topics/resources/more-resources.html#Dimension
/**
* Have this column eat up any remaining space (split with all other proportional columns).
* <p/>
* For proportional columns, {@link #getValue()} is an integer value which should be compared
* with all other proportional columns to determine how much space it gets. For example, if
* column A is set to 1 and column B is set to 3, column A gets 25% of all remaining space
* and column B gets 75%.
*/
Proportional,
}
private final Type myType;
private final int myValue; // Value's meaning depends on this constraint's type
public ColumnDefinition(Type type) {
this(type, 0);
}
public ColumnDefinition(Type type, int value) {
myType = type;
myValue = value;
}
public Type getType() {
return myType;
}
public int getValue() {
return myValue;
}
}
/**
* Constraints which specify which cell the element is slotted into.
*/
public static final class Constraint {
private final int myRow;
private final int myCol;
public Constraint(int row, int col) {
myRow = row;
myCol = col;
}
public int getRow() {
return myRow;
}
public int getCol() {
return myCol;
}
}
/**
* Create a {@link ProportionalLayout} from a comma-delimited string, where each value represents
* either a Fit, Fixed, or Proportional column.
* <p/>
* A Fit column is represented by the string "Fit"
* A Fixed column is represented by an integer + "px" (e.g. 100px)
* A Proportional column is represented by an integer value followed by a *
* <p/>
* Examples:
* "Fit,*,*" - First column fits to size, remaining two columns share leftover space equally
* "3*,*" - First column gets 75% of space, second column gets 25% of space
* "75*,25*" - Same as above
* "50px,*,100px" - First column gets 50 pixels, last column gets 100, middle gets remaining
*/
public static ProportionalLayout fromString(@NotNull String columnDefinitions) throws IllegalArgumentException {
List<String> splits = Lists.newArrayList(Splitter.on(',').split(columnDefinitions));
int numColumns = splits.size();
ColumnDefinition[] definitions = new ColumnDefinition[numColumns];
try {
for (int i = 0; i < splits.size(); i++) {
String s = splits.get(i);
if (s.equals("Fit")) {
definitions[i] = new ColumnDefinition(Type.Fit);
}
else if (s.endsWith("px")) {
int value = Integer.parseInt(s.substring(0, s.length() - 2)); // e.g. "30px" -> "30"
definitions[i] = new ColumnDefinition(Type.Fixed, value);
}
else if (s.equals("*")) {
definitions[i] = new ColumnDefinition(Type.Proportional, 1);
}
else if (s.endsWith("*")) {
int value = Integer.parseInt(s.substring(0, s.length() - 1)); // e.g. "3*" -> "3"
definitions[i] = new ColumnDefinition(Type.Proportional, value);
}
else {
throw new IllegalArgumentException(String.format("Bad column definition: \"%1$s\" in \"%2$s\"", s, columnDefinitions));
}
}
}
catch (NumberFormatException ex) {
throw new IllegalArgumentException(String.format("Bad column definition: \"%s\"", columnDefinitions));
}
return new ProportionalLayout(definitions);
}
private final ColumnDefinition[] myDefinitions;
private final float[] myPercentages;
private final Map<Component, Constraint> myConstraints = Maps.newHashMap();
public ProportionalLayout(ColumnDefinition... definitions) {
myDefinitions = definitions;
myPercentages = new float[myDefinitions.length];
float total = 0;
for (ColumnDefinition constraint : myDefinitions) {
if (constraint.getType() == Type.Proportional) {
total += constraint.getValue();
}
}
if (total > 0) {
for (int i = 0; i < myDefinitions.length; i++) {
ColumnDefinition constraint = definitions[i];
if (constraint.getType() == Type.Proportional) {
myPercentages[i] = constraint.getValue() / total;
}
}
}
}
public int getNumColumns() {
return myDefinitions.length;
}
@Override
public void addLayoutComponent(Component comp, Object constraint) {
myConstraints.put(comp, (Constraint)constraint);
}
@Override
public Dimension maximumLayoutSize(Container target) {
return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
@Override
public float getLayoutAlignmentX(Container target) {
return 0;
}
@Override
public float getLayoutAlignmentY(Container target) {
return 0.5f;
}
@Override
public void invalidateLayout(Container target) {
// Do nothing
}
@Override
public void addLayoutComponent(String name, Component comp) {
// Do nothing
}
@Override
public void removeLayoutComponent(Component comp) {
myConstraints.remove(comp);
}
@Override
public Dimension preferredLayoutSize(Container parent) {
int[] widths = new int[myDefinitions.length];
int[] heights;
for (int i = 0; i < myDefinitions.length; i++) {
ColumnDefinition defn = myDefinitions[i];
if (defn.getType() == Type.Fixed) {
widths[i] = defn.getValue();
}
}
synchronized (parent.getTreeLock()) {
Insets insets = parent.getInsets();
int componentCount = parent.getComponentCount();
int numRows = 1;
for (int i = 0; i < componentCount; i++) {
Component comp = parent.getComponent(i);
int row = myConstraints.get(comp).getRow();
numRows = Math.max(row + 1, numRows);
}
heights = new int[numRows];
// Calculate leftover space which, if we had, would fit all proportional columns.
int maxDesiredSpace = 0;
for (int i = 0; i < componentCount; i++) {
Component comp = parent.getComponent(i);
Dimension d = comp.getPreferredSize();
int row = myConstraints.get(comp).getRow();
heights[row] = Math.max(heights[row], d.height);
int col = myConstraints.get(comp).getCol();
ColumnDefinition defn = myDefinitions[col];
if (defn.getType() == Type.Fit) {
widths[col] = Math.max(widths[col], d.width);
}
else if (defn.getType() == Type.Proportional) {
// Calculate how much total leftover space would be needed to fit this cell
// after it takes its percentage cut
int desiredSpace = Math.round(d.width / myPercentages[col]);
maxDesiredSpace = Math.max(maxDesiredSpace, desiredSpace);
}
}
int h = 0;
for (int height : heights) {
h += height;
}
int w = maxDesiredSpace;
for (int width : widths) {
w += width;
}
return new Dimension(insets.left + insets.right + w, insets.top + insets.bottom + h);
}
}
@Override
public Dimension minimumLayoutSize(Container parent) {
int[] widths = new int[myDefinitions.length];
int[] heights;
for (int i = 0; i < myDefinitions.length; i++) {
ColumnDefinition defn = myDefinitions[i];
if (defn.getType() == Type.Fixed) {
widths[i] = defn.getValue();
}
}
synchronized (parent.getTreeLock()) {
Insets insets = parent.getInsets();
int componentCount = parent.getComponentCount();
int numRows = 1;
for (int i = 0; i < componentCount; i++) {
Component comp = parent.getComponent(i);
int row = myConstraints.get(comp).getRow();
numRows = Math.max(row + 1, numRows);
}
heights = new int[numRows];
for (int i = 0; i < componentCount; i++) {
Component comp = parent.getComponent(i);
Dimension d = comp.getMinimumSize();
int row = myConstraints.get(comp).getRow();
heights[row] = Math.max(heights[row], d.height);
int col = myConstraints.get(comp).getCol();
ColumnDefinition defn = myDefinitions[col];
if (defn.getType() == Type.Fit) {
widths[col] = Math.max(widths[col], d.width);
}
}
int h = 0;
for (int height : heights) {
h += height;
}
int w = 0;
for (float width : widths) {
w += width;
}
return new Dimension(insets.left + insets.right + w, insets.top + insets.bottom + h);
}
}
@Override
public void layoutContainer(Container parent) {
int numCols = myDefinitions.length;
int[] colXs = new int[numCols];
int[] colWs = new int[numCols];
int[] rowYs;
int[] rowHs;
for (int i = 0; i < myDefinitions.length; i++) {
ColumnDefinition defn = myDefinitions[i];
if (defn.getType() == Type.Fixed) {
colWs[i] = defn.getValue();
}
}
synchronized (parent.getTreeLock()) {
Insets insets = parent.getInsets();
int componentCount = parent.getComponentCount();
int numRows = 1;
for (int i = 0; i < componentCount; i++) {
Component comp = parent.getComponent(i);
int row = myConstraints.get(comp).getRow();
numRows = Math.max(numRows, row + 1);
}
rowYs = new int[numRows];
rowHs = new int[numRows];
for (int i = 0; i < componentCount; i++) {
Component comp = parent.getComponent(i);
Dimension d = comp.getMinimumSize();
int row = myConstraints.get(comp).getRow();
rowHs[row] = Math.max(rowHs[row], d.height);
int col = myConstraints.get(comp).getCol();
ColumnDefinition defn = myDefinitions[col];
if (defn.getType() == Type.Fit) {
colWs[col] = Math.max(colWs[col], d.width);
}
}
int leftoverWidth = parent.getWidth() - insets.right - insets.left;
for (int colW : colWs) {
leftoverWidth -= colW;
}
if (leftoverWidth > 0) {
for (int i = 0; i < numCols; i++) {
if (myPercentages[i] > 0) {
colWs[i] = Math.round(myPercentages[i] * leftoverWidth);
}
}
}
rowYs[0] = insets.top;
for (int i = 1; i < rowYs.length; i++) {
rowYs[i] = rowYs[i - 1] + rowHs[i - 1];
}
colXs[0] = insets.left;
for (int i = 1; i < colXs.length; i++) {
colXs[i] = colXs[i - 1] + colWs[i - 1];
}
for (int i = 0; i < componentCount; i++) {
Component comp = parent.getComponent(i);
int col = myConstraints.get(comp).getCol();
int row = myConstraints.get(comp).getRow();
comp.setBounds(colXs[col], rowYs[row], colWs[col], rowHs[row]);
}
}
}
}