package com.peterhi.ui;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
public final class ViewContainer extends Composite
implements Listener, ViewConstants {
private final SplitDock centerPanel;
private final AutohideBar leftBar;
private final AutohideBar topBar;
private final AutohideBar rightBar;
private final AutohideBar bottomBar;
private final List<Control> views;
private Control activeView;
public static void main(String[] args) throws Exception {
Display d = Display.getDefault();
Shell s = new Shell(d);
s.setLayout(new FillLayout());
ViewContainer v = new ViewContainer(s, SWT.NONE);
s.setSize(1152, 640);
Button v0 = new Button(v, SWT.NONE);
{
v0.setText("View 0");
ViewSpec vs0 = new ViewSpec(v, "view0");
vs0.setTitle("View 0");
v.add(v0, vs0);
}
Button v1 = new Button(v, SWT.NONE);
{
v1.setText("View 1");
ViewSpec vs1 = new ViewSpec(v, "view1");
vs1.setTitle("View 1");
v.add(v1, vs1);
}
Button v2 = new Button(v, SWT.NONE);
{
v2.setText("View 2");
ViewSpec vs2 = new ViewSpec(v, "view2");
vs2.setTitle("View 2");
vs2.setPosition(TAB_BEFORE);
v.add(v2, vs2);
}
Button v3 = new Button(v, SWT.NONE);
{
v3.setText("View 3");
ViewSpec vs3 = new ViewSpec(v, "view3");
vs3.setTitle("View 3");
vs3.setReference(v0);
vs3.setPosition(SPLIT_TOP);
vs3.setExtent(0.2f);
v.add(v3, vs3);
}
Button v4 = new Button(v, SWT.NONE);
{
v4.setText("View 4");
ViewSpec vs4 = new ViewSpec(v, "view4");
vs4.setTitle("View 4");
vs4.setReference(v0);
vs4.setPosition(SPLIT_LEFT);
vs4.setExtent(150);
v.add(v4, vs4);
}
Button v5 = new Button(v, SWT.NONE);
{
v5.setText("View 5");
ViewSpec vs5 = new ViewSpec(v, "view5");
vs5.setTitle("View 5");
vs5.setReference(v0);
vs5.setPosition(SPLIT_BOTTOM);
vs5.setExtent(150);
v.add(v5, vs5);
}
Button v6 = new Button(v, SWT.NONE);
{
v6.setText("View 6");
ViewSpec vs6 = new ViewSpec(v, "view6");
vs6.setTitle("View 6");
vs6.setReference(v0);
vs6.setExtent(200);
vs6.setPosition(SPLIT_RIGHT);
v.add(v6, vs6);
}
v.activate(v1);
s.open();
while (!s.isDisposed()) {
if (!d.readAndDispatch()) {
d.sleep();
}
}
}
public ViewContainer(Composite parent, int style) {
super(parent, style);
Display d = Display.getCurrent();
centerPanel = new SplitDock(this, SWT.CENTER);
leftBar = new AutohideBar(this, SWT.LEFT);
topBar = new AutohideBar(this, SWT.TOP);
rightBar = new AutohideBar(this, SWT.RIGHT);
bottomBar = new AutohideBar(this, SWT.BOTTOM);
centerPanel.setBackground(d.getSystemColor(SWT.COLOR_BLACK));
leftBar.setBackground(d.getSystemColor(SWT.COLOR_RED));
topBar.setBackground(d.getSystemColor(SWT.COLOR_YELLOW));
rightBar.setBackground(d.getSystemColor(SWT.COLOR_BLUE));
bottomBar.setBackground(d.getSystemColor(SWT.COLOR_GREEN));
views = new ArrayList<Control>();
addListener(SWT.Resize, this);
}
@Override
public void layout(boolean changed) {
Rectangle cli = getClientArea();
Point ls = leftBar.computeSize(SWT.DEFAULT, cli.height);
Point ts = topBar.computeSize(cli.width, SWT.DEFAULT);
Point rs = rightBar.computeSize(SWT.DEFAULT, cli.height);
Point bs = bottomBar.computeSize(cli.width, SWT.DEFAULT);
centerPanel.setBounds(cli.x + ls.x, cli.y + ts.y,
cli.width - ls.x - rs.x, cli.height - ts.y - bs.y);
leftBar.setBounds(cli.x, cli.y + ts.y,
ls.x, cli.height - ts.y - bs.y);
topBar.setBounds(cli.x + ls.x, cli.y,
cli.width - ls.x - rs.x, ts.y);
rightBar.setBounds(cli.x + cli.width - rs.x, cli.y + ts.y,
rs.x, cli.height - ts.y - bs.y);
bottomBar.setBounds(cli.x + ls.x, cli.y + cli.height - bs.y,
cli.width - ls.x - rs.x, bs.y);
}
@Override
public void setLayout(Layout layout) {
}
@Override
public Point computeSize(int wHint, int hHint, boolean changed) {
return super.computeSize(wHint, hHint, changed);
}
@Override
public void handleEvent(Event event) {
if (event.widget == this && event.type == SWT.Resize) {
layout();
}
}
public void add(Control view, ViewSpec vs) throws ViewException {
if (views.contains(view)) {
activate(view);
return;
}
if (views.isEmpty()) {
vs.setPosition(DEFAULT);
centerPanel.add(view, vs);
} else if (vs.getPosition() == DEFAULT) {
vs.setReference(getActiveView());
vs.setPosition(TAB_AFTER);
centerPanel.add(view, vs);
} else if (vs.isTabbed()) {
centerPanel.add(view, vs);
} else if (vs.isSplit()) {
centerPanel.add(view, vs);
}
view.setData(VIEW_SPEC, vs);
views.add(view);
activate(view);
}
public void activate(Control view) throws ViewException {
if (view == activeView) {
return;
}
deactivate(activeView);
if (view == null) {
view = getActiveView();
}
if (!views.contains(view)) {
return;
}
ViewSpec vs = (ViewSpec )view.getData(VIEW_SPEC);
if (vs == null) {
throw new ViewException();
}
Object item = vs.getItem();
if (item instanceof CTabItem) {
CTabItem ti = (CTabItem )item;
CTabFolder tf = ti.getParent();
tf.setSelection(ti);
ti.setFont(Ui.FONT_BOLD);
}
activeView = view;
}
private void deactivate(Control view) throws ViewException {
if (view == null) {
view = getActiveView();
}
if (!views.contains(view)) {
return;
}
ViewSpec vs = (ViewSpec )view.getData(VIEW_SPEC);
if (vs == null) {
throw new ViewException();
}
Object item = vs.getItem();
if (item instanceof CTabItem) {
CTabItem ti = (CTabItem )item;
ti.setFont(Ui.FONT_PLAIN);
}
}
public Control getActiveView() {
return activeView;
}
}
final class SplitDock extends Composite implements Listener, ViewConstants {
private final List<ViewGroup> groups;
private Rectangle oldClientArea;
private int gap;
public SplitDock(ViewContainer parent, int style) {
super(parent, style);
groups = new ArrayList<ViewGroup>();
gap = 10;
addListener(SWT.Resize, this);
}
public void add(Control view, ViewSpec vs) throws ViewException {
if (vs.getPosition() == DEFAULT) {
ViewGroup vg = new ViewGroup(this, SWT.NONE);
vg.setBounds(getClientArea());
vg.add(view, vs, -1);
groups.add(vg);
} else if (vs.isTabbed()) {
ViewGroup vg = findViewGroup(vs);
int index = findIndex(vs);
vg.add(view, vs, index);
} else if (vs.isSplit()) {
Control reference = vs.getReference();
ViewSpec rvs = (ViewSpec )reference.getData(VIEW_SPEC);
Point oldMinSize = rvs.getMinSize();
Point neoMinSize = vs.getMinSize();
ViewGroup vg = findViewGroup(vs);
Rectangle entire = vg.getBounds();
Rectangle old = new Rectangle(entire.x, entire.y,
entire.width, entire.height);
Rectangle neo = new Rectangle(entire.x, entire.y,
entire.width, entire.height);
int extent;
int minExtent;
if (vs.isVertical()) {
extent = vs.getExtent(entire.width - gap);
minExtent = neoMinSize.x;
} else {
extent = vs.getExtent(entire.height - gap);
minExtent = neoMinSize.y;
}
if (extent < minExtent) {
extent = minExtent;
}
if (vs.getPosition() == SPLIT_LEFT) {
neo.width = extent;
old.x += neo.width + gap;
old.width -= old.x;
} else if (vs.getPosition() == SPLIT_TOP) {
neo.height = extent;
old.y += neo.height + gap;
old.height -= old.y;
} else if (vs.getPosition() == SPLIT_BOTTOM) {
neo.height = extent;
old.height -= extent + gap;
neo.y += old.height + gap;
} else {
neo.width = extent;
old.width -= extent + gap;
neo.x += old.width + gap;
}
ViewGroup nvg = new ViewGroup(this, SWT.NONE);
nvg.add(view, vs, -1);
groups.add(nvg);
if (old.width < oldMinSize.x || old.height < oldMinSize.y) {
throw new ViewException(MessageFormat.format(
"Insufficient size to lay out the view, " +
"min = {0} but actual = {1}.", oldMinSize,
new Point(old.width, old.height)));
}
if (neo.width < neoMinSize.x || neo.height < neoMinSize.y) {
throw new ViewException(MessageFormat.format(
"Insufficient size to lay out the view, " +
"min = {0} but actual = {1}.", neoMinSize,
new Point(neo.width, neo.height)));
}
vg.setBounds(old);
nvg.setBounds(neo);
}
}
@Override
public void layout(boolean changed) {
if (groups.isEmpty()) {
return;
}
Rectangle newClientArea = getClientArea();
float[] scales = computeScales(newClientArea);
float xscale = scales[0];
float yscale = scales[1];
Map<ViewGroup, Rectangle> caches = new HashMap<ViewGroup, Rectangle>();
for (ViewGroup group : groups) {
Rectangle cache = group.getBounds();
caches.put(group, cache);
}
for (Rectangle cache : caches.values()) {
inflate(cache, oldClientArea, gap);
}
Snapshot2 snapshot = new Snapshot2(groups.toArray(
new ViewGroup[groups.size()]), oldClientArea, caches);
for (ViewGroup group : groups) {
Rectangle cache = caches.get(group);
cache.width = Math.round((float )cache.width * xscale);
cache.height = Math.round((float )cache.height * yscale);
}
ViewGroup upperLeft = snapshot.getUpperLeft();
lineUp(upperLeft, snapshot, caches);
adjustRightAndBottomCorners(snapshot, caches, newClientArea);
for (ViewGroup key : caches.keySet()) {
Rectangle cache = caches.get(key);
deflate(cache, newClientArea, gap);
key.setBounds(cache);
}
}
@Override
public void setLayout(Layout layout) {
}
@Override
public Point computeSize(int wHint, int hHint, boolean changed) {
return super.computeSize(wHint, hHint, changed);
}
@Override
public ViewContainer getParent() {
return (ViewContainer )super.getParent();
}
@Override
public boolean setParent(Composite parent) {
if (!(parent instanceof ViewContainer)) {
throw new IllegalArgumentException(
"Parent is not an instance of GrandPanel.");
}
return super.setParent(parent);
}
@Override
public void handleEvent(Event event) {
if (event.widget == this && event.type == SWT.Resize) {
if (oldClientArea != null) {
layout();
}
oldClientArea = getClientArea();
}
}
public int getGap() {
return gap;
}
public void setGap(int gap) {
if (gap <= 0) {
gap = 10;
}
this.gap = gap;
}
private float[] computeScales(Rectangle newClientArea) {
if (oldClientArea == null) {
return new float[] { 1.0f, 1.0f };
}
float[] factors = new float[] { 1.0f, 1.0f };
if (oldClientArea.width != 0 && newClientArea.width != 0) {
factors[0] = (float )newClientArea.width / oldClientArea.width;
}
if (oldClientArea.height != 0 && newClientArea.height != 0) {
factors[1] = (float )newClientArea.height / oldClientArea.height;
}
return factors;
}
private void deflate(Rectangle rect, Rectangle clientArea, int amt) {
int left = rect.x;
int top = rect.y;
int right = rect.x + rect.width;
int bottom = rect.y + rect.height;
int cleft = clientArea.x;
int ctop = clientArea.y;
int cright = clientArea.x + clientArea.width;
int cbottom = clientArea.y + clientArea.height;
if (left > cleft) {
left += amt / 2;
}
if (top > ctop) {
top += amt / 2;
}
if (right < cright) {
right -= amt / 2;
}
if (bottom < cbottom) {
bottom -= amt / 2;
}
rect.x = left;
rect.y = top;
rect.width = right - left;
rect.height = bottom - top;
}
private void inflate(Rectangle rect, Rectangle clientArea, int amt) {
int left = rect.x;
int top = rect.y;
int right = rect.x + rect.width;
int bottom = rect.y + rect.height;
int cleft = clientArea.x;
int ctop = clientArea.y;
int cright = clientArea.x + clientArea.width;
int cbottom = clientArea.y + clientArea.height;
left -= amt / 2;
top -= amt / 2;
right += amt / 2;
bottom += amt / 2;
if (left < cleft) {
left = cleft;
}
if (top < ctop) {
top = ctop;
}
if (right > cright) {
right = cright;
}
if (bottom > cbottom) {
bottom = cbottom;
}
rect.x = left;
rect.y = top;
rect.width = right - left;
rect.height = bottom - top;
}
private void lineUp(ViewGroup current, Snapshot2 snapshot,
Map<ViewGroup, Rectangle> caches) {
Rectangle cbounds = caches.get(current);
ViewGroup[] rights = snapshot.rightOf(current);
ViewGroup[] bottoms = snapshot.bottomOf(current);
for (ViewGroup right : rights) {
Rectangle rbounds = caches.get(right);
rbounds.x = cbounds.x + cbounds.width;
lineUp(right, snapshot, caches);
}
for (ViewGroup bottom : bottoms) {
Rectangle bbounds = caches.get(bottom);
bbounds.y = cbounds.y + cbounds.height;
lineUp(bottom, snapshot, caches);
}
}
private void adjustRightAndBottomCorners(Snapshot2 snapshot,
Map<ViewGroup, Rectangle> caches, Rectangle clientArea) {
for (ViewGroup key : caches.keySet()) {
Rectangle cache = caches.get(key);
if (snapshot.containsRightmost(key)) {
int right = cache.x + cache.width;
int cright = clientArea.x + clientArea.width;
int diff = cright - right;
cache.width += diff;
}
if (snapshot.containsBottommost(key)) {
int bottom = cache.y + cache.height;
int cbottom = clientArea.y + clientArea.height;
int diff = cbottom - bottom;
cache.height += diff;
}
}
}
private ViewGroup findViewGroup(ViewSpec vs) {
Control reference = vs.getReference();
ViewSpec rvs = (ViewSpec )reference.getData(VIEW_SPEC);
CTabItem item = (CTabItem )rvs.getItem();
if (item == null) {
return null;
}
CTabFolder folder = item.getParent();
if (folder == null) {
return null;
}
ViewGroup vg = (ViewGroup )folder.getParent();
return vg;
}
private int findIndex(ViewSpec vs) {
Control reference = vs.getReference();
ViewSpec rvs = (ViewSpec )reference.getData(VIEW_SPEC);
CTabItem item = (CTabItem )rvs.getItem();
if (item == null) {
return -1;
}
CTabFolder folder = item.getParent();
if (folder == null) {
return -1;
}
int index = folder.indexOf(item);
if (vs.getPosition() == TAB_AFTER) {
index++;
}
return index;
}
}
final class AutohideBar extends Composite implements ViewConstants {
public AutohideBar(ViewContainer parent, int style) {
super(parent, style);
}
@Override
public Point computeSize(int wHint, int hHint, boolean changed) {
return super.computeSize(isVertical() ? 10 : wHint,
isVertical() ? hHint : 10, changed);
}
@Override
public ViewContainer getParent() {
return (ViewContainer )super.getParent();
}
@Override
public boolean setParent(Composite parent) {
if (!(parent instanceof ViewContainer)) {
throw new IllegalArgumentException(
"Parent is not an instance of GrandPanel.");
}
return super.setParent(parent);
}
private boolean isVertical() {
int style = getStyle();
return (style & (SWT.LEFT | SWT.RIGHT)) != 0;
}
}
final class ViewGroup extends Composite implements ViewConstants {
private final CTabFolder tabPanel;
public ViewGroup(SplitDock parent, int style) {
super(parent, style);
setLayout(new FillLayout());
tabPanel = new CTabFolder(this, style);
tabPanel.setBorderVisible(true);
bindActivationListeners();
}
public void add(Control view, ViewSpec vs, int index) {
if (index < 0 || index > tabPanel.getItemCount()) {
index = tabPanel.getItemCount();
}
CTabItem item = new CTabItem(tabPanel, SWT.CLOSE, index);
item.setText(vs.getTitle());
item.setImage(vs.getIcon());
view.setParent(tabPanel);
item.setControl(view);
tabPanel.setSelection(item);
vs.setItem(item);
}
public SplitDock getParent() {
return (SplitDock )super.getParent();
}
public boolean setParent(Composite parent) {
if (!(parent instanceof SplitDock)) {
throw new IllegalArgumentException(
"Parent must be an instance of DockPanel.");
}
return super.setParent(parent);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(super.toString());
if (tabPanel.getItemCount() > 0) {
sb.append(" ");
CTabItem cti = tabPanel.getSelection();
if (cti == null) {
cti = tabPanel.getItem(0);
}
sb.append(cti.getText());
}
return sb.toString();
}
private ViewContainer getViewContainer() {
return (ViewContainer )getParent().getParent();
}
private void bindActivationListeners() {
Listener clickOnItemDirectlyListener = new Listener() {
@Override
public void handleEvent(Event event) {
CTabItem item = tabPanel.getItem(new Point(event.x, event.y));
if (item == null) {
item = tabPanel.getSelection();
}
try {
getViewContainer().activate(item.getControl());
} catch (Exception ex) {
ex.printStackTrace();
}
}
};
final Listener clickOnAnyControlInsideListener = new Listener() {
@Override
public void handleEvent(Event event) {
if (!(event.widget instanceof Control)) {
return;
}
Control control = (Control )event.widget;
if (isChildOf(control)) {
CTabItem ti = tabPanel.getSelection();
Control view = ti.getControl();
try {
getViewContainer().activate(view);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
private boolean isChildOf(Control control) {
if (control.getParent() == null) {
return false;
}
if (control.getParent() == tabPanel) {
return true;
}
return isChildOf(control.getParent());
}
};
final Listener removeFilterListener = new Listener() {
@Override
public void handleEvent(Event event) {
getDisplay().removeFilter(SWT.MouseDown,
clickOnAnyControlInsideListener);
}
};
tabPanel.addListener(SWT.MouseDown, clickOnItemDirectlyListener);
tabPanel.addListener(SWT.Dispose, removeFilterListener);
getDisplay().addFilter(SWT.MouseDown, clickOnAnyControlInsideListener);
}
}
final class Snapshot2 {
private final ViewGroup upperLeft;
private final SortedMap<ViewGroup, SortedSet<ViewGroup>> rights;
private final SortedMap<ViewGroup, SortedSet<ViewGroup>> bottoms;
private final SortedSet<ViewGroup> rightmost;
private final SortedSet<ViewGroup> bottommost;
public Snapshot2(ViewGroup[] groups, Rectangle clientArea,
Map<ViewGroup, Rectangle> caches) {
this.upperLeft = findUpperLeft(groups, caches);
this.rights = new TreeMap<ViewGroup, SortedSet<ViewGroup>>(
ViewGroupComparator.getHorizontal());
this.bottoms = new TreeMap<ViewGroup, SortedSet<ViewGroup>>(
ViewGroupComparator.getVertical());
this.rightmost = new TreeSet<ViewGroup>(
ViewGroupComparator.getVertical());
this.bottommost = new TreeSet<ViewGroup>(
ViewGroupComparator.getHorizontal());
for (ViewGroup group : groups) {
Rectangle bounds0 = caches.get(group);
if (bounds0.x + bounds0.width == clientArea.x + clientArea.width) {
rightmost.add(group);
}
if (bounds0.y + bounds0.height == clientArea.y + clientArea.height) {
bottommost.add(group);
}
}
for (ViewGroup group : groups) {
Rectangle bounds0 = caches.get(group);
SortedSet<ViewGroup> r = rights.get(group);
SortedSet<ViewGroup> b = bottoms.get(group);
if (r == null) {
r = new TreeSet<ViewGroup>(
ViewGroupComparator.getVertical());
rights.put(group, r);
}
if (b == null) {
b = new TreeSet<ViewGroup>(
ViewGroupComparator.getHorizontal());
bottoms.put(group, b);
}
for (ViewGroup other : groups) {
if (group == other) {
continue;
}
Rectangle bounds1 = caches.get(other);
if (bounds0.x + bounds0.width == bounds1.x) {
r.add(other);
}
if (bounds0.y + bounds0.height == bounds1.y) {
b.add(other);
}
}
}
}
public ViewGroup getUpperLeft() {
return upperLeft;
}
public ViewGroup[] rightOf(ViewGroup vg) {
SortedSet<ViewGroup> set = rights.get(vg);
if (set == null) {
return new ViewGroup[0];
}
return set.toArray(new ViewGroup[0]);
}
public ViewGroup[] bottomOf(ViewGroup vg) {
SortedSet<ViewGroup> set = bottoms.get(vg);
if (set == null) {
return new ViewGroup[0];
}
return set.toArray(new ViewGroup[0]);
}
public boolean containsRightmost(ViewGroup v) {
return rightmost.contains(v);
}
public boolean containsBottommost(ViewGroup v) {
return bottommost.contains(v);
}
public ViewGroup[] getRightmost() {
return rightmost.toArray(new ViewGroup[rightmost.size()]);
}
public ViewGroup[] getBottommost() {
return bottommost.toArray(new ViewGroup[bottommost.size()]);
}
private ViewGroup findUpperLeft(ViewGroup[] groups,
Map<ViewGroup, Rectangle> caches) {
for (ViewGroup group : groups) {
Rectangle bounds = caches.get(group);
if (bounds.x == 0 && bounds.y == 0) {
return group;
}
}
throw new IllegalArgumentException("No upper left group included.");
}
}
final class ViewGroupComparator implements Comparator<ViewGroup> {
private static Comparator<ViewGroup> horizontal;
private static Comparator<ViewGroup> vertical;
public static Comparator<ViewGroup> getHorizontal() {
if (horizontal == null) {
horizontal = new ViewGroupComparator(SWT.HORIZONTAL);
}
return horizontal;
}
public static Comparator<ViewGroup> getVertical() {
if (vertical == null) {
vertical = new ViewGroupComparator(SWT.VERTICAL);
}
return vertical;
}
private int style;
protected ViewGroupComparator(int style) {
this.style = style;
}
public boolean isVertical() {
return (style & SWT.VERTICAL) == SWT.VERTICAL;
}
@Override
public int compare(ViewGroup vg0, ViewGroup vg1) {
if (vg0 == vg1) {
return 0;
}
if (vg0 == null) {
return -1;
}
if (vg1 == null) {
return 1;
}
Rectangle b0 = vg0.getBounds();
Rectangle b1 = vg1.getBounds();
if (isVertical()) {
int dy = b0.y - b1.y;
if (dy != 0) {
return dy;
}
int dx = b0.x - b1.x;
if (dx != 0) {
return dx;
}
return vg0.hashCode() - vg1.hashCode();
} else {
int dx = b0.x - b1.x;
if (dx != 0) {
return dx;
}
int dy = b0.y - b1.y;
if (dy != 0) {
return dy;
}
return vg0.hashCode() - vg1.hashCode();
}
}
}