/* This file is part of leafdigital leafChat. leafChat is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. leafChat is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with leafChat. If not, see <http://www.gnu.org/licenses/>. Copyright 2012 Samuel Marshall. */ package com.leafdigital.ui; import java.awt.*; import java.util.*; import java.util.List; import javax.swing.JComponent; import com.leafdigital.ui.api.*; import leafchat.core.api.BugException; import static com.leafdigital.ui.api.BorderPanel.*; /** * Implements BorderPanel */ public class BorderPanelImp extends JComponent { // Conceptual grid and notation used // // <--L--> <--C--> <--R--> // // +-------+-------+-------+ // | | | | | // T | NW | N | NE | // | | | | | // +-------+-------+-------+ // | | | | | // M | W | CENTR | E | // | | | | | // +-------+-------+-------+ // | | | | | // B | SW | S | SE | // | | | | | // +-------+-------+-------+ /** Number of slots in BorderPanel */ private final static int SLOTS = 9; /** One of the CORNERS_xxx constants */ private int cornerHandling; /** Spacing between grid squares */ private int spacing = 0; /** Border at edge of grid */ private int border = 0; /** Keep record of held components */ private InternalWidget[] slots = new InternalWidget[SLOTS]; /** Constructor */ BorderPanelImp() { setLayout(null); setOpaque(false); } @Override public void setBounds(int x, int y, int width, int height) { super.setBounds(x, y, width, height); updateLayout(); } @Override public void validate() { super.validate(); updateLayout(); } @Override public Dimension getPreferredSize() { InternalWidget iw = (InternalWidget)publicInterface; int width = iw.getPreferredWidth(); return new Dimension(width, iw.getPreferredHeight(width)); } /** * @param slot Slot (NORTH, NORTHEAST, etc) * @return Preferred width of component at that slot, or 0 if none */ private int prefW(int slot) { if(slots[slot] == null || !slots[slot].isVisible()) { return 0; } return slots[slot].getPreferredWidth(); } /** * @param slot Slot (NORTH, NORTHEAST, etc) * @param width Available width * @return Preferred height of slot's component at that width, or 0 */ private int prefH(int slot, int width) { if(slots[slot] == null || !slots[slot].isVisible() || width == 0) { return 0; } return slots[slot].getPreferredHeight(width); } /** * @param slot Slot (NORTH, NORTHEAST, etc) * @return True if the slot is empty */ private boolean isEmpty(int slot) { return slots[slot] == null; } /** * Moves a component. * @param slot Slot of component to move * @param x X position * @param y Y position * @param width Width * @param height Height */ private void move(int slot, int x, int y, int width, int height) { if(slots[slot] == null) { return; } slots[slot].getJComponent().setBounds(x, y, width, height); } /** Move all components to their correct place in new layout */ private void updateLayout() { UISingleton.checkSwing(); // Get desired width and height, and modify to fit actual space available GridWidths gw = new GridWidths(); gw.fitWidth(getWidth() - border * 2); GridHeights gh = new GridHeights(gw); gh.fitHeight(getHeight() - border * 2); // Calculate positions int x1 = border, x2 = border + gw.l + gw.gutterLC, x3 = border + gw.l + gw.gutterLC + gw.c + gw.gutterCR; int y1 = border, y2 = border + gh.t + gh.gutterTM, y3 = border + gh.t + gh.gutterTM + gh.m + gh.gutterMB; // Corner components always go in the same places move(NORTHWEST, x1, y1, gw.l, gh.t); move(NORTHEAST, x3, y1, gw.r, gh.t); move(SOUTHWEST, x1, y3, gw.l, gh.b); move(SOUTHEAST, x3, y3, gw.r, gh.b); switch(cornerHandling) { case BorderPanel.CORNERS_LEAVEBLANK : { // Put all components in their grid squares move(NORTH, x2, y1, gw.c, gh.t); move(EAST, x3, y2, gw.r, gh.m); move(SOUTH, x2, y3, gw.c, gh.b); move(WEST, x1, y2, gw.l, gh.m); move(CENTRAL, x2, y2, gw.c, gh.m); break; } case BorderPanel.CORNERS_HORIZONTALFILL: { // E and W don't take up other slots move(EAST, x3, y2, gw.r, gh.m); move(WEST, x1, y2, gw.l, gh.m); int nStart = x2, nWidth = gw.c; if(isEmpty(NORTHWEST)) { nStart = x1; nWidth+= gw.l + gw.gutterLC; } if(isEmpty(NORTHEAST)) { nWidth += gw.gutterCR + gw.r; } move(NORTH, nStart, y1, nWidth, gh.t); int sStart = x2, sWidth = gw.c; if(isEmpty(SOUTHWEST)) { sStart = x1; sWidth += gw.l + gw.gutterLC; } if(isEmpty(SOUTHEAST)) { sWidth += gw.gutterCR + gw.r; } move(SOUTH, sStart, y3, sWidth, gh.b); int cStart = x2, cWidth = gw.c; if(isEmpty(WEST)) { cStart = x1; cWidth += gw.l + gw.gutterLC; } if(isEmpty(EAST)) { cWidth += gw.gutterCR + gw.r; } move(CENTRAL, cStart, y2, cWidth, gh.m); break; } case BorderPanel.CORNERS_VERTICALFILL: { // N and S don't take up other slots move(NORTH, x2, y1, gw.c, gh.t); move(SOUTH, x2, y3, gw.c, gh.b); int wStart = y2, wHeight = gh.m; if(isEmpty(NORTHWEST)) { wStart = y1; wHeight += gh.t + gh.gutterTM; } if(isEmpty(SOUTHWEST)) { wHeight += gh.gutterMB + gh.b; } move(WEST, x1, wStart, gw.l, wHeight); int eStart = y2, eHeight = gh.m; if(isEmpty(NORTHEAST)) { eStart = y1; eHeight += gh.t + gh.gutterTM; } if(isEmpty(SOUTHEAST)) { eHeight += gh.gutterMB + gh.b; } move(EAST, x3, eStart, gw.r, eHeight); int cStart = y2, cHeight = gh.m; if(isEmpty(NORTH)) { cStart = y1; cHeight += gh.t + gh.gutterTM; } if(isEmpty(SOUTH)) { cHeight += gh.gutterMB + gh.b; } move(CENTRAL, x2, cStart, gw.c, cHeight); break; } } repaint(); } /** @return Interface giving limited public access */ BorderPanel getInterface() { return publicInterface; } /** * @param i1 First number * @param i2 Second number * @param i3 Third number * @return Maximum of the three parameters */ private static int max(int i1, int i2, int i3) { if(i1 > i2) { return (i1 > i3) ? i1 : i3; } else { return (i2 > i3) ? i2 : i3; } } /** * Class manages the three widths in the grid. */ class GridWidths { int l, c, r; int gutterLC, gutterCR; /** @return Total width this represents */ int getTotalWidth() { return l + gutterLC + c + gutterCR + r; } /** Construct with desired widths */ GridWidths() { // Calculate L and R; these depend straightforwardly on the things in // those corners l = max(prefW(NORTHWEST), prefW(WEST), prefW(SOUTHWEST)); r = max(prefW(NORTHEAST), prefW(EAST), prefW(SOUTHEAST)); // Calculate gutters, depending on whether there's anything in the places gutterLC = (l == 0) ? 0 : spacing; gutterCR = (r == 0) ? 0 : spacing; // Calculate C, which depends on whether the N component expands or not if(cornerHandling == BorderPanel.CORNERS_HORIZONTALFILL) { int iTPref = prefW(NORTH) - (isEmpty(NORTHWEST) ? (l + gutterLC) : 0) - (isEmpty(NORTHEAST) ? (r + gutterCR) : 0), iMPref = prefW(CENTRAL) - (isEmpty(WEST) ? (l + gutterLC) : 0) - (isEmpty(EAST) ? (r + gutterCR) : 0), iBPref = prefW(SOUTH) - (isEmpty(SOUTHWEST) ? (l + gutterLC) : 0) - (isEmpty(SOUTHEAST) ? (r + gutterCR) : 0); c = max(iTPref, iMPref, iBPref); } else // CORNERS_LEAVEBLANK or .CORNERS_VERTICALFILL { c = max(prefW(NORTH), prefW(CENTRAL), prefW(SOUTH)); } } /** * Fit to a different available width. * @param availableWidth Required width */ void fitWidth(int availableWidth) { int extraSpace = availableWidth - getTotalWidth(); if(extraSpace>0) { c += extraSpace; } else if(extraSpace < 0) { c += extraSpace; if(c < 0) { int overflow = -c; c = 0; l -= overflow / 2; r -= (overflow + 1) / 2; // The +1 ensures that we use the complete total if(l < 0) { int overflowOverflow = -l; l = 0; r -= overflowOverflow; if(r < 0) { r = 0; } } else if(r < 0) { int overflowOverflow = -r; r = 0; l -= overflowOverflow; if(l < 0) { l = 0; } } } } } } /** * Manages the three heights in the grid. */ class GridHeights { int t, m, b; int gutterTM, gutterMB; int getTotalHeight() { return t + gutterTM + m + gutterMB + b; } /** * Find the desired grid heights for given widths. * @param gw Widths data */ GridHeights(GridWidths gw) { if(cornerHandling == BorderPanel.CORNERS_HORIZONTALFILL) { t = max( prefH(NORTHWEST, gw.l), prefH(NORTHEAST, gw.r), prefH(NORTH, gw.c + (isEmpty(NORTHWEST) ? (gw.l + gw.gutterLC) : 0) + (isEmpty(NORTHEAST) ? (gw.r + gw.gutterCR) : 0) ) ); b = max( prefH(SOUTHWEST, gw.l), prefH(SOUTHEAST, gw.r), prefH(SOUTH, gw.c + (isEmpty(SOUTHWEST) ? (gw.l + gw.gutterLC) : 0) + (isEmpty(SOUTHEAST) ? (gw.r + gw.gutterCR) : 0) ) ); } else // CORNERS_LEAVEBLANK or CORNERS_VERTICALFILL { t = max(prefH(NORTHWEST, gw.l), prefH(NORTH, gw.c), prefH(NORTHEAST, gw.r)); b = max(prefH(SOUTHWEST, gw.l), prefH(SOUTH, gw.c), prefH(SOUTHEAST, gw.r)); } // Calculate gutters, depending on whether there's anything in the places gutterTM = (t > 0) ? spacing : 0; gutterMB = (b > 0) ? spacing : 0; if(cornerHandling == BorderPanel.CORNERS_VERTICALFILL) { int iLPref = prefH(WEST, gw.l) - (isEmpty(NORTHWEST) ? (t + gutterTM) : 0) - (isEmpty(SOUTHWEST) ? (b + gutterMB) : 0), iCPref = prefH(CENTRAL, gw.c) - (isEmpty(NORTH) ? (t + gutterTM) : 0) - (isEmpty(SOUTH) ? (b + gutterMB) : 0), iRPref = prefH(EAST, gw.r) - (isEmpty(NORTHEAST) ? (t + gutterTM) : 0) - (isEmpty(SOUTHEAST) ? (b + gutterMB) : 0); m = max(iLPref, iCPref, iRPref); } else // CORNERS_LEAVEBLANK or CORNERS_HORIZONTALFILL { m = max(prefH(WEST, gw.l), prefH(CENTRAL, gw.c), prefH(EAST, gw.r)); } } /** * Fit to specified height. * @param availableHeight Height to be used */ void fitHeight(int availableHeight) { int extraSpace = availableHeight - getTotalHeight(); if(extraSpace>0) { m += extraSpace; } else if(extraSpace < 0) { m += extraSpace; if(m < 0) { int overflow = -m; m = 0; t -= overflow/2; t -= (overflow + 1)/2; // The +1 ensures that we use the complete total if(t < 0) { int overflowOverflow = -t; t = 0; b -= overflowOverflow; if(b < 0) { b = 0; } } else if(b < 0) { int overflowOverflow = -b; b = 0; t -= overflowOverflow; if(t < 0) { t = 0; } } } } } } /** Interface available to public */ private BorderPanel publicInterface = new BorderPanelInterface(); /** * Interface available to public. */ class BorderPanelInterface extends BasicWidget implements BorderPanel, InternalWidget { @Override public int getContentType() { return CONTENT_NAMEDSLOTS; } @Override public void set(final int slot, Widget w) { final InternalWidget iw = (InternalWidget)w; iw.setParent(this); UISingleton.runInSwing(new Runnable() { @Override public void run() { if(slots[slot] != null) { BorderPanelImp.this.remove(slots[slot].getJComponent()); slots[slot] = null; } if(iw != null) { add(iw.getJComponent()); iw.getJComponent().revalidate(); slots[slot] = iw; } updateLayout(); } }); } @Override public Widget get(int slot) { return slots[slot]; } @Override public Widget[] getWidgets() { List<Widget> all = new LinkedList<Widget>(); for(InternalWidget w : slots) { if(w != null) { all.add(w); } } return all.toArray(new Widget[all.size()]); } @Override public void setCornerHandling(final int iCorners) { UISingleton.runInSwing(new Runnable() { @Override public void run() { switch(iCorners) { case BorderPanel.CORNERS_HORIZONTALFILL: case BorderPanel.CORNERS_VERTICALFILL: case BorderPanel.CORNERS_LEAVEBLANK: { cornerHandling = iCorners; updateLayout(); return; } default: throw new IllegalArgumentException("Corner value not supported"); } } }); } @Override public void remove(Widget w) { final InternalWidget iw = (InternalWidget)w; UISingleton.runInSwing(new Runnable() { @Override public void run() { for(int i=0; i<slots.length; i++) { if(slots[i] == iw) { slots[i] = null; BorderPanelImp.this.remove(iw.getJComponent()); } } updateLayout(); } }); } @Override public void removeAll() { UISingleton.runInSwing(new Runnable() { @Override public void run() { for(int i=0; i<slots.length; i++) { if(slots[i] != null) { InternalWidget iw = slots[i]; slots[i] = null; BorderPanelImp.this.remove(iw.getJComponent()); } } updateLayout(); } }); } @Override public JComponent getJComponent() { return BorderPanelImp.this; } @Override public int getPreferredWidth() { GridWidths gw = new GridWidths(); return gw.getTotalWidth() + 2 * border; } @Override public int getPreferredHeight(int width) { if(width == 0) return 0; GridWidths gw = new GridWidths(); gw.fitWidth(width - 2 * border); GridHeights gh = new GridHeights(gw); int height = gh.getTotalHeight() + 2 * border; return height; } @Override public void setSpacing(final int spacing) { UISingleton.runInSwing(new Runnable() { @Override public void run() { BorderPanelImp.this.spacing = spacing; updateLayout(); } }); } @Override public void setBorder(final int border) { UISingleton.runInSwing(new Runnable() { @Override public void run() { BorderPanelImp.this.border = border; updateLayout(); } }); } @Override public void addXMLChild(String slotName, Widget child) { int slot; if(slotName.equals("north")) { slot = NORTH; } else if(slotName.equals("northeast")) { slot = NORTHEAST; } else if(slotName.equals("east")) { slot = EAST; } else if(slotName.equals("southeast")) { slot = SOUTHEAST; } else if(slotName.equals("south")) { slot = SOUTH; } else if(slotName.equals("southwest")) { slot = SOUTHWEST; } else if(slotName.equals("west")) { slot = WEST; } else if(slotName.equals("northwest")) { slot = NORTHWEST; } else if(slotName.equals("central")) { slot = CENTRAL; } else { throw new BugException( "Slot name invalid, expecting 'north', 'northeast', etc.: " + slotName); } set(slot, child); } @Override public void redoLayout() { updateLayout(); super.redoLayout(); } }; }