/******************************************************************************* * Copyright (c) 2006 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation ******************************************************************************/ package org.eclipse.ui.internal; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CBanner; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.CoolBar; import org.eclipse.swt.widgets.CoolItem; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Layout; import org.eclipse.ui.internal.layout.TrimCommonUIHandle; /** * This layout implements the handling necessary to support the positioning of * all of the 'trim' elements defined for the workbench. * <p> * NOTE: This class is a part of a 'work in progress' and should not be used * without consulting the Platform UI group. No guarantees are made as to the * stability of the API. * </p> * * @since 3.2 * */ public class WorkbenchLayout extends Layout { private static int defaultMargin = 5; /** * This is a convenience class that caches information for a single 'tiled' * line of trim. * * @since 3.2 * */ private class TrimLine { /** * Teh list of controls in this trim line */ List controls = new ArrayList(); /** * A cache of the previously computed sizes of each trim control */ List computedSizes = new ArrayList(); /** * In horizontal terms this is the 'height' of the tallest control. */ int minorMax = 0; /** * The number of controls in the line that want to 'grab' extra space. * Any unused space in a trim line is shared equally among these * controls */ int resizableCount = 0; /** * The amount of unused space in the line */ int extraSpace = 0; } /** * This layout is used to capture the CBanner's calls to 'computeSize' for * the left trim (which is used to determine the height of the CBanner) so * that it will compute its own height to be the max of either the left or * the right control. * <p> * NOTE: This class is expected to be removed once the CBanner mods are in. * </p> * * @since 3.2 * */ private class LeftBannerLayout extends Layout { /* * (non-Javadoc) * * @see org.eclipse.swt.widgets.Layout#computeSize(org.eclipse.swt.widgets.Composite, * int, int, boolean) */ protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) { // 'topMax' is the maximum height of both the left and // the right side return new Point(wHint, WorkbenchLayout.this.topMax); } /* * (non-Javadoc) * * @see org.eclipse.swt.widgets.Layout#layout(org.eclipse.swt.widgets.Composite, * boolean) */ protected void layout(Composite composite, boolean flushCache) { } } // Trim area 'ids' public static final String TRIMID_CMD_PRIMARY = "Command Primary"; //$NON-NLS-1$ public static final String TRIMID_CMD_SECONDARY = "Command Secondary"; //$NON-NLS-1$ public static final String TRIMID_VERTICAL1 = "vertical1"; //$NON-NLS-1$ public static final String TRIMID_VERTICAL2 = "vertical2"; //$NON-NLS-1$ public static final String TRIMID_STATUS = "Status"; //$NON-NLS-1$ public static final String TRIMID_CENTER = "Center"; //$NON-NLS-1$ // 'CBanner' info public CBanner banner; private int topMax; // 'Center' composite public Composite centerComposite; // inter-element spacing private int spacing = 0; // Trim Areas private TrimArea cmdPrimaryTrimArea; private TrimArea cmdSecondaryTrimArea; private TrimArea leftTrimArea; private TrimArea rightTrimArea; private TrimArea bottomTrimArea; // Drag handle info private int horizontalHandleSize = -1; private int verticalHandleSize = -1; private List dragHandles; // statics used in the layout private static Composite layoutComposite; private static Rectangle clientRect; /** * Construct a new layout. This defines the trim areas that trim can be * placed into. */ public WorkbenchLayout() { super(); // Add the trim areas into the layout cmdPrimaryTrimArea = new TrimArea(TRIMID_CMD_PRIMARY, SWT.TOP, defaultMargin); cmdSecondaryTrimArea = new TrimArea(TRIMID_CMD_SECONDARY, SWT.TOP, defaultMargin); leftTrimArea = new TrimArea(TRIMID_VERTICAL1, SWT.LEFT, defaultMargin); rightTrimArea = new TrimArea(TRIMID_VERTICAL2, SWT.RIGHT, defaultMargin); bottomTrimArea = new TrimArea(TRIMID_STATUS, SWT.BOTTOM, defaultMargin); // init the list that has the drag handle cache dragHandles = new ArrayList(); } /** * Create the CBanner control used to control the horizontal span of the * primary and secondary command areas. * * @param workbenchComposite * The workbench acting as the parent of the CBanner */ public void createCBanner(Composite workbenchComposite) { banner = new CBanner(workbenchComposite, SWT.NONE); banner.setSimple(false); banner.setRightWidth(175); banner.setLocation(0, 0); // Create the left composite and override its 'computeSize' // to delegate to the 'primary' command trim area Composite bannerLeft = new Composite(banner, SWT.NONE) { /* * (non-Javadoc) * * @see org.eclipse.swt.widgets.Composite#computeSize(int, int, * boolean) */ public Point computeSize(int wHint, int hHint, boolean changed) { // If we're doing a 'real' workbench layout then delegate to the // appropriate trim area if (WorkbenchLayout.layoutComposite != null) { return WorkbenchLayout.this.computeSize(TRIMID_CMD_PRIMARY, wHint); } return super.computeSize(wHint, hHint, changed); } }; bannerLeft.setLayout(new LeftBannerLayout()); bannerLeft.setBackground(workbenchComposite.getDisplay() .getSystemColor(SWT.COLOR_DARK_BLUE)); banner.setLeft(bannerLeft); // Create the right hand part of the CBanner Composite bannerRight = new Composite(banner, SWT.NONE) { /* * (non-Javadoc) * * @see org.eclipse.swt.widgets.Composite#computeSize(int, int, * boolean) */ public Point computeSize(int wHint, int hHint, boolean changed) { // If we're doing a 'real' workbench layout then delegate to the // appropriate trim area if (WorkbenchLayout.layoutComposite != null) { return WorkbenchLayout.this.computeSize( TRIMID_CMD_SECONDARY, wHint); } return super.computeSize(wHint, hHint, changed); } }; bannerRight.setBackground(workbenchComposite.getDisplay() .getSystemColor(SWT.COLOR_DARK_BLUE)); banner.setRight(bannerRight); // If the right banner control changes size it's because // the 'swoop' moved. bannerRight.addControlListener(new ControlListener() { public void controlMoved(ControlEvent e) { } public void controlResized(ControlEvent e) { Composite leftComp = (Composite) e.widget; leftComp.getShell().layout(true); } }); // Place the CBanner on the 'bottom' of the z-order banner.moveBelow(null); } /* * (non-Javadoc) * * @see org.eclipse.swt.widgets.Layout#computeSize(org.eclipse.swt.widgets.Composite, * int, int, boolean) * * Note that this is arbitrary since the we're a top level shell (so * computeSize won't be called. */ protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) { Point size = new Point(wHint, hHint); if (size.x == SWT.DEFAULT) { size.x = 300; } if (size.y == SWT.DEFAULT) { size.y = 300; } return size; } /* * (non-Javadoc) * * @see org.eclipse.swt.widgets.Layout#layout(org.eclipse.swt.widgets.Composite, * boolean) * * TODO: Supply a full description of the layout mechanicsS */ protected void layout(Composite composite, boolean flushCache) { layoutComposite = composite; clientRect = composite.getClientArea(); // reset all the drag handles to be invisible resetDragHandles(); // Compute the proper size for each trim area // Do Top Right, Top Left, Left, Right then Bottom so the math works out if (useCBanner()) { banner.moveBelow(null); // NOTE: calling 'computeSize' here will, in turn, call the // 'computeSize' for the left/right areas, each with the // 'correct' width hint... Point bannerSize = banner .computeSize(clientRect.width, SWT.DEFAULT); // set the amount of space used by the 'cmd' areas for use later topMax = bannerSize.y; banner.setSize(bannerSize); } else { Point c1Size = computeSize(TRIMID_CMD_PRIMARY, clientRect.width); Point c2Size = computeSize(TRIMID_CMD_SECONDARY, clientRect.width); // set the amount of space used by the 'cmd' areas for use later topMax = c1Size.y + c2Size.y; } // Now do the vertical areas; their 'major' is whatever is left // vertically once the top areas have been computed Point v1Size = computeSize(TRIMID_VERTICAL1, clientRect.height - topMax); Point v2Size = computeSize(TRIMID_VERTICAL2, clientRect.height - topMax); // Finally, the status area's 'major' is whatever is left over // horizontally once the vertical areas have been computed. computeSize(TRIMID_STATUS, clientRect.width - (v1Size.x + v2Size.x)); // Now, layout the trim within each area // Command primary area if (useCBanner()) { Point leftLoc = banner.getLeft().getLocation(); cmdPrimaryTrimArea.areaBounds.x = leftLoc.x; cmdPrimaryTrimArea.areaBounds.y = leftLoc.y; } else { cmdPrimaryTrimArea.areaBounds.x = 0; cmdPrimaryTrimArea.areaBounds.y = 0; } layoutTrim(cmdPrimaryTrimArea, cmdPrimaryTrimArea.areaBounds); // Command secondary area if (useCBanner()) { Point rightLoc = banner.getRight().getLocation(); cmdSecondaryTrimArea.areaBounds.x = rightLoc.x; cmdSecondaryTrimArea.areaBounds.y = rightLoc.y; } else { cmdSecondaryTrimArea.areaBounds.x = 0; cmdSecondaryTrimArea.areaBounds.y = cmdPrimaryTrimArea.areaBounds.height; } layoutTrim(cmdSecondaryTrimArea, cmdSecondaryTrimArea.areaBounds); leftTrimArea.areaBounds.x = 0; leftTrimArea.areaBounds.y = topMax; layoutTrim(leftTrimArea, leftTrimArea.areaBounds); rightTrimArea.areaBounds.x = clientRect.width - rightTrimArea.areaBounds.width; rightTrimArea.areaBounds.y = topMax; layoutTrim(rightTrimArea, rightTrimArea.areaBounds); bottomTrimArea.areaBounds.x = leftTrimArea.areaBounds.width; bottomTrimArea.areaBounds.y = clientRect.height - bottomTrimArea.areaBounds.height; layoutTrim(bottomTrimArea, bottomTrimArea.areaBounds); // Set the center control's bounds to fill the space remaining // after the trim has been arranged layoutCenter(); } /** * Indicates whether or not the layout should use the CBanner or tile the * primary and secondary trim areas one above the other. * * @return <code>true</code> iff the layout should use the CBanner. */ private boolean useCBanner() { return (banner != null && banner.getVisible()); } /** * @param areaId * The identifier for the TrimArea to return * @return The TrimArea that matches the given areaId */ private TrimArea getTrimArea(String areaId) { if (TRIMID_CMD_PRIMARY.equals(areaId)) { return cmdPrimaryTrimArea; } if (TRIMID_CMD_SECONDARY.equals(areaId)) { return cmdSecondaryTrimArea; } if (TRIMID_VERTICAL1.equals(areaId)) { return leftTrimArea; } if (TRIMID_VERTICAL2.equals(areaId)) { return rightTrimArea; } if (TRIMID_STATUS.equals(areaId)) { return bottomTrimArea; } return null; } /** * @param areaId * The TrimArea that we want to get the controls for * @return The list of controls whose TrimLayoutData's areaId matches the * given areaId parameter */ private List getTrimContents(String areaId) { List trimContents = new ArrayList(); Control[] children = layoutComposite.getChildren(); for (int i = 0; i < children.length; i++) { // Skip any disposed or invisible widgets if (children[i].isDisposed() || !children[i].getVisible()) { continue; } // Only accept children that want to be layed out in a particular // trim area if (children[i].getLayoutData() instanceof TrimLayoutData) { TrimLayoutData tlData = (TrimLayoutData) children[i] .getLayoutData(); if (tlData.areaId.equals(areaId)) { trimContents.add(children[i]); } } } return trimContents; } /** * Lays out the center composite once the outer trim areas have all been * done. */ private void layoutCenter() { if (centerComposite != null) { // The center is the 'inner' bounding box of the other trim areas Rectangle areaBounds = new Rectangle( leftTrimArea.areaBounds.x + leftTrimArea.areaBounds.width, topMax, clientRect.width - (leftTrimArea.areaBounds.width + rightTrimArea.areaBounds.width), clientRect.height - (topMax + bottomTrimArea.areaBounds.height)); centerComposite.setBounds(areaBounds); } } /** * Computes the size of a trim area given the length of its major dimension. * Depending on whether the trim area is horizontal or vertical one of the * two value will always match the supplied 'majorHint' ('x' if it's * horizontal). * <p> * Effectively, this computes the length of the minor dimension by tiling * the trim area's controls into multiple lines based on the length of the * major dimension. * </p> * * @param areaId * The area id to compute the size for * @param majorHint * The length of the major dimension * * @return The computed size */ private Point computeSize(String areaId, int majorHint) { TrimArea trimArea = getTrimArea(areaId); boolean horizontal = trimArea.orientation == SWT.TOP || trimArea.orientation == SWT.BOTTOM; // Gather up all the controls for this area and tile them out trimArea.trimContents = getTrimContents(trimArea.areaId); // Split the controls list into a list of TrimLines that each fit into // the trim area trimArea.trimLines = computeWrappedTrim(trimArea, majorHint); // Now, calculate the max between the default and the contents int minorMax = 0; for (Iterator iter = trimArea.trimLines.iterator(); iter.hasNext();) { TrimLine curLine = (TrimLine) iter.next(); minorMax += curLine.minorMax; } minorMax = Math.max(minorMax, trimArea.defaultMinor); // Store the size in the area'a cache... Point computedSize = new Point(0, 0); if (horizontal) { computedSize.x = trimArea.areaBounds.width = majorHint; computedSize.y = trimArea.areaBounds.height = minorMax; } else { computedSize.x = trimArea.areaBounds.width = minorMax; computedSize.y = trimArea.areaBounds.height = majorHint; } // ...and return it return computedSize; } /** * This is where the information required to lay the controls belonging to a * particular trim area out. * <p> * Tile the controls in the trim area into one or more lines. Each line is * guaranteed to take up less than or equal to the 'majorHint' in the major * dimension. The result is a complete cache of the information needed to * lay the controls in the trim area out. * </p> * * @param trimArea The trim area to create the cache info for * @param majorHint The length of the major dimension * * @return A List of <code>TrimLine</code> elements */ private List computeWrappedTrim(TrimArea trimArea, int majorHint) { boolean horizontal = trimArea.orientation == SWT.TOP || trimArea.orientation == SWT.BOTTOM; // Return a list of 'lines' to tile the control into... List lines = new ArrayList(); TrimLine curLine = new TrimLine(); lines.add(curLine); curLine.minorMax = trimArea.defaultMinor; // Init the tilePos to force a 'new' line int tilePos = 0; for (Iterator iter = trimArea.trimContents.iterator(); iter.hasNext();) { Control control = (Control) iter.next(); TrimLayoutData td = (TrimLayoutData) control.getLayoutData(); Point prefSize; if (horizontal) { //prefSize = control.computeSize(majorHint, SWT.DEFAULT); prefSize = control.computeSize(SWT.DEFAULT, SWT.DEFAULT); } else { prefSize = control.computeSize(SWT.DEFAULT, majorHint); } // Will this control fit onto the current line? int curTileSize = horizontal ? prefSize.x : prefSize.y; // every control except the first one needs to include the 'spacing' // in the calc if (curLine.controls.size() > 0) { curTileSize += spacing; } // If the control can be re-positioned then we have to add a drag // handle to it // we have to include the space that it'll occupy in the calcs if (td.listener != null) { curTileSize += horizontal ? horizontalHandleSize : verticalHandleSize; } // Place the control into the 'current' line if it'll fit (or if // it's the // -first- control (this handles the case where a control is too // large to // fit into the current TrimArea's 'major' size. if ((tilePos + curTileSize) <= majorHint || curLine.controls.size() == 0) { curLine.controls.add(control); // Cache the maximum amount of 'minor' space needed int minorSize = horizontal ? prefSize.y : prefSize.x; if (minorSize > curLine.minorMax) { curLine.minorMax = minorSize; } tilePos += curTileSize; } else { // Remember how much space was left on the current line curLine.extraSpace = majorHint - tilePos; // We need a new line... curLine = new TrimLine(); lines.add(curLine); curLine.controls.add(control); // Cache the maximum amount of 'minor' space needed int minorSize = horizontal ? prefSize.y : prefSize.x; if (minorSize > curLine.minorMax) { curLine.minorMax = minorSize; } tilePos = curTileSize; } // Count how many 'resizable' controls there are if ((td.flags & TrimLayoutData.GROWABLE) != 0) { curLine.resizableCount++; } } // Remember how much space was left on the current line curLine.extraSpace = majorHint - tilePos; return lines; } private void layoutTrim(TrimArea trimArea, Rectangle areaBounds) { boolean horizontal = trimArea.orientation == SWT.TOP || trimArea.orientation == SWT.BOTTOM; // How much space do we have to 'tile' into? int areaMajor = horizontal ? areaBounds.width : areaBounds.height; // Get the tiled 'List of Lists' for the trim List tileAreas = trimArea.trimLines; // Tile each 'line' into the trim int tilePosMinor = horizontal ? areaBounds.y : areaBounds.x; for (Iterator areaIter = tileAreas.iterator(); areaIter.hasNext();) { TrimLine curLine = (TrimLine) areaIter.next(); // Compute how much space to give to 'resizable' controls. Note that // we can end up computing -negative- 'extraSpace' values if the // control's // preferred 'major' size is actually > the 'major' size for the // trim area int resizePadding = 0; if (curLine.resizableCount > 0 && curLine.extraSpace > 0) { resizePadding = curLine.extraSpace / curLine.resizableCount; } // Tile each line int tilePosMajor = horizontal ? areaBounds.x : areaBounds.y; for (Iterator iter = curLine.controls.iterator(); iter.hasNext();) { Control control = (Control) iter.next(); TrimLayoutData td = (TrimLayoutData) control.getLayoutData(); Point prefSize = control.computeSize(SWT.DEFAULT, SWT.DEFAULT); int major = horizontal ? prefSize.x : prefSize.y; int minor = horizontal ? prefSize.y : prefSize.x; // Ensure that controls that are too wide for the area get // 'clipped' if (major > areaMajor) { major = areaMajor; } // If desired, extend the 'minor' size of the control to fill // the area if ((td.flags & TrimLayoutData.GRAB_EXCESS_MINOR) != 0) { minor = curLine.minorMax; } // If desired, extend the 'major' size of the control to fill // the area if ((td.flags & TrimLayoutData.GROWABLE) != 0) { major += resizePadding; } // If we have to show a drag handle then do it here if (td.listener != null && createHandles()) { TrimCommonUIHandle handle = getDragHandle(trimArea.orientation); // handle.setControl(control); if (horizontal) { handle.setBounds(tilePosMajor, tilePosMinor, getHandleSize(true), curLine.minorMax); } else { handle.setBounds(tilePosMinor, tilePosMajor, curLine.minorMax, getHandleSize(false)); } tilePosMajor += horizontal ? getHandleSize(true) : getHandleSize(false); } // Now, lay out the actual control switch (trimArea.orientation) { case SWT.TOP: control.setBounds(tilePosMajor, tilePosMinor, major, minor); break; case SWT.LEFT: control.setBounds(tilePosMinor, tilePosMajor, minor, major); break; case SWT.RIGHT: // Here we want to tile the control to the RIGHT edge int rightEdge = tilePosMinor + curLine.minorMax; control.setBounds(rightEdge - minor, tilePosMajor, minor, major); break; case SWT.BOTTOM: // Here we want to tile the control to the RIGHT edge int bottomEdge = tilePosMinor + curLine.minorMax; control.setBounds(tilePosMajor, bottomEdge - minor, major, minor); break; } // Ensure that the control is above the trim control tilePosMajor += major + spacing; } tilePosMinor += curLine.minorMax; tilePosMajor = horizontal ? areaBounds.x : areaBounds.y; } } /** * HACK>>>Remove if found in the wild... * @return */ private boolean createHandles() { return false; } private void resetDragHandles() { for (Iterator iter = dragHandles.iterator(); iter.hasNext();) { // TrimCommonUIHandle handle = (TrimCommonUIHandle) iter.next(); // handle.setControl(null); } } private TrimCommonUIHandle getDragHandle(int orientation) { // boolean horizontal = orientation == SWT.TOP // || orientation == SWT.BOTTOM; for (Iterator iter = dragHandles.iterator(); iter.hasNext();) { TrimCommonUIHandle handle = (TrimCommonUIHandle) iter.next(); // if (handle.toDrag == null && handle.horizontal == horizontal) return handle; } // If we get here then we haven't found a new drag handle so create one System.out.println("new Handle"); //$NON-NLS-1$ // TrimCommonUIHandle newHandle = new // TrimCommonUIHandle(layoutComposite, // horizontal); // dragHandles.add(newHandle); // return newHandle; return null; } /** * Return the SWT side that the trim area is on * * @param areaId The id of the area to get the orientation of * * @return The SWT side corresponding that the given area */ public static int getOrientation(String areaId) { if (TRIMID_CMD_PRIMARY.equals(areaId)) { return SWT.TOP; } if (TRIMID_CMD_SECONDARY.equals(areaId)) { return SWT.TOP; } if (TRIMID_VERTICAL1.equals(areaId)) { return SWT.LEFT; } if (TRIMID_VERTICAL2.equals(areaId)) { return SWT.RIGHT; } if (TRIMID_STATUS.equals(areaId)) { return SWT.BOTTOM; } return SWT.NONE; } /** * Calculate a size for the handle that will be large enough to show the * CoolBar's drag affordance. * * @return The size that the handle has to be, based on the orientation */ private int getHandleSize(boolean horizontal) { // Do we already have a 'cached' value? if (horizontal && horizontalHandleSize != -1) { return horizontalHandleSize; } if (!horizontal && verticalHandleSize != -1) { return verticalHandleSize; } // Must be the first time, calculate the value CoolBar bar = new CoolBar(layoutComposite, horizontal ? SWT.HORIZONTAL : SWT.VERTICAL); CoolItem item = new CoolItem(bar, SWT.NONE); Label ctrl = new Label(bar, SWT.PUSH); ctrl.setText("Button 1"); //$NON-NLS-1$ Point size = ctrl.computeSize(SWT.DEFAULT, SWT.DEFAULT); Point ps = item.computeSize(size.x, size.y); item.setPreferredSize(ps); item.setControl(ctrl); bar.pack(); // OK, now the difference between the location of the CB and the // location of the Point bl = ctrl.getLocation(); Point cl = bar.getLocation(); // Toss them now... ctrl.dispose(); item.dispose(); bar.dispose(); // The 'size' is the difference between the start of teh CoolBar and // start of its first control int length; if (horizontal) { length = bl.x - cl.x; horizontalHandleSize = length; } else { length = bl.y - cl.y; verticalHandleSize = length; } return length; } }