package com.peterhi.ui.bars;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.jdesktop.core.animation.timing.Animator;
import org.jdesktop.core.animation.timing.Interpolator;
import org.jdesktop.core.animation.timing.TimingSource;
import org.jdesktop.core.animation.timing.TimingTarget;
import org.jdesktop.core.animation.timing.TimingTargetAdapter;
import org.jdesktop.core.animation.timing.interpolators.AccelerationInterpolator;
import org.jdesktop.swt.animation.timing.sources.SWTTimingSource;
import com.peterhi.ui.UIImageResource;
public final class StripBar extends Canvas implements Listener {
public static final int SPACE = 5;
private final List<StripItemController> controllers = new ArrayList<StripItemController>();
private StripItemController focusedController;
private StripItemController pressedController;
private final Set<StripItemController> selectedControllers = new HashSet<StripItemController>();
private StripItemController draggedController;
private StripItemDragContext dragContext;
public StripBar(Composite parent, int style) {
super(parent, style);
addListener(SWT.SetData, this);
addListener(SWT.Paint, this);
addListener(SWT.PaintItem, this);
addListener(SWT.MouseEnter, this);
addListener(SWT.MouseExit, this);
addListener(SWT.MouseDown, this);
addListener(SWT.MouseMove, this);
addListener(SWT.MouseUp, this);
addListener(SWT.Dispose, this);
}
@Override
public void handleEvent(Event event) {
if (equals(event.widget)) {
if (event.type == SWT.SetData) {
onSetData(event);
} else if (event.type == SWT.Paint) {
onPaint(event);
} else if (event.type == SWT.PaintItem) {
onPaintItem(event);
} else if (event.type == SWT.MouseEnter) {
onMouseEnter(event);
} else if (event.type == SWT.MouseExit) {
onMouseExit(event);
} else if (event.type == SWT.MouseDown) {
onMouseDown(event);
} else if (event.type == SWT.MouseMove) {
onMouseMove(event);
} else if (event.type == SWT.MouseUp) {
onMouseUp(event);
} else if (event.type == SWT.Dispose) {
onDispose(event);
}
}
}
@Override
public Point computeSize(int wHint, int hHint, boolean changed) {
Point size = new Point(0, 0);
for (StripItemController controller : controllers) {
Rectangle bounds = controller.getItemBounds();
size.x += bounds.width;
size.y = Math.max(size.y, bounds.height);
}
size.x += SPACE * (getItemCount() + 1);
size.y += SPACE * 2;
Rectangle boundsWithTrim = computeTrim(0, 0, size.x, size.y);
size.x = boundsWithTrim.width;
size.y = boundsWithTrim.height;
return size;
}
public boolean isMultiSelection() {
int style = getStyle();
boolean multi = (style & SWT.MULTI) == SWT.MULTI;
return multi;
}
public int indexOfItem(StripItem item) {
if (item == null) {
throw new NullPointerException();
}
StripItemController controller = getController(item);
if (controller == null) {
return -1;
}
return controllers.indexOf(controller);
}
public boolean containsItem(StripItem item) {
int index = indexOfItem(item);
if (index < 0) {
return false;
}
return true;
}
public boolean hasItemImage(StripItem item) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
Image image = getItemImage(item);
return image != null;
}
public boolean hasItemText(StripItem item) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
String text = getItemText(item);
if (text == null) {
return false;
}
if (text.isEmpty()) {
return false;
}
return true;
}
public Image getItemImage(StripItem item) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
return item.getImage();
}
public String getItemText(StripItem item) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
return item.getText();
}
public Image getItemCloseButtonImage() {
Image closeButtonImage = UIImageResource.T.closeNormal10();
return closeButtonImage;
}
public void setItemImage(StripItem item, Image value) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
item.setImage(value);
}
public void setItemText(StripItem item, String value) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
item.setText(value);
}
public Point getItemImageSize(StripItem item) {
Point size = new Point(0, 0);
if (hasItemImage(item)) {
Image image = getItemImage(item);
Rectangle bounds = image.getBounds();
size.x = bounds.width;
size.y = bounds.height;
}
return size;
}
public Point getItemTextSize(StripItem item) {
Point size = new Point(0, 0);
if (hasItemText(item)) {
String text = getItemText(item);
GC gc = new GC(this);
size = gc.stringExtent(text);
gc.dispose();
}
return size;
}
public Point getItemCloseButtonSize() {
Image closeButtonImage = getItemCloseButtonImage();
Rectangle closeButtonImageBounds = closeButtonImage.getBounds();
return new Point(closeButtonImageBounds.width, closeButtonImageBounds.height);
}
public Rectangle getItemBounds(StripItem item) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
Rectangle bounds = new Rectangle(SPACE, SPACE, 0, 0);
for (StripItemController controller : controllers) {
if (controller.hasItemImage()) {
Point imageSize = controller.getItemImageSize();
bounds.height = Math.max(bounds.height, imageSize.y);
}
if (controller.hasItemText()) {
Point textSize = controller.getItemTextSize();
bounds.height = Math.max(bounds.height, textSize.y);
}
Point closeButtonSize = getItemCloseButtonSize();
bounds.height = Math.max(bounds.height, closeButtonSize.y);
}
bounds.height += SPACE * 2;
for (StripItemController controller : controllers) {
int extent = SPACE;
if (controller.hasItemImage()) {
Point imageSize = controller.getItemImageSize();
extent += imageSize.x;
extent += SPACE;
}
if (controller.hasItemText()) {
Point textSize = controller.getItemTextSize();
extent += textSize.x;
extent += SPACE;
}
Point closeButtonSize = getItemCloseButtonSize();
extent += closeButtonSize.x;
extent += SPACE;
if (controller.itemEquals(item)) {
bounds.width = extent;
break;
} else {
bounds.x += extent;
bounds.x += SPACE;
}
}
return bounds;
}
public Rectangle getItemImageBounds(StripItem item) {
Rectangle bounds = getItemBounds(item);
bounds.y += bounds.height / 2;
bounds.width = 0;
bounds.height = 0;
if (hasItemImage(item)) {
Point imageSize = getItemImageSize(item);
bounds.x += SPACE;
bounds.y -= imageSize.y / 2;
bounds.width = imageSize.x;
bounds.height = imageSize.y;
}
return bounds;
}
public Rectangle getItemTextBounds(StripItem item) {
Rectangle bounds = getItemImageBounds(item);
bounds.x += bounds.width;
bounds.y += bounds.height / 2;
bounds.width = 0;
bounds.height = 0;
if (hasItemText(item)) {
Point textSize = getItemTextSize(item);
bounds.x += SPACE;
bounds.y -= textSize.y / 2;
bounds.width = textSize.x;
bounds.height = textSize.y;
}
return bounds;
}
public Rectangle getItemCloseButtonBounds(StripItem item) {
Rectangle bounds = getItemTextBounds(item);
Point closeButtonSize = getItemCloseButtonSize();
bounds.x += bounds.width + SPACE;
bounds.y += bounds.height / 2 - closeButtonSize.y / 2;
bounds.width = closeButtonSize.x;
bounds.height = closeButtonSize.y;
return bounds;
}
public boolean isEmpty() {
return getItemCount() == 0;
}
public int getItemCount() {
return controllers.size();
}
public StripItem[] getItems() {
List<StripItem> items = new ArrayList<StripItem>();
for (StripItemController controller : controllers) {
items.add(controller.getItem());
}
return items.toArray(new StripItem[items.size()]);
}
public StripItem getItem(int index) {
StripItemController controller = controllers.get(index);
if (controller == null) {
return null;
}
return controller.getItem();
}
public StripItem getItem(int x, int y) {
StripItemController controller = getController(x, y);
if (controller == null) {
return null;
}
return controller.getItem();
}
public StripItemState getItemState(StripItem item) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
StripItemController controller = getController(item);
if (controller == null) {
throw new IllegalArgumentException();
}
if (controller.equals(focusedController)) {
return StripItemState.FOCUSED;
}
if (controller.equals(pressedController)) {
return StripItemState.PRESSED;
}
if (controller.equals(draggedController)) {
return StripItemState.DRAGGED;
}
if (selectedControllers.contains(controller)) {
return StripItemState.SELECTED;
}
return StripItemState.DEFAULT;
}
public boolean hasFocusedItem() {
return focusedController != null;
}
public boolean hasPressedItem() {
return pressedController != null;
}
public boolean hasDraggedItem() {
return draggedController != null;
}
public boolean hasSelectedItems() {
return !selectedControllers.isEmpty();
}
public StripItem getFocusedItem() {
if (hasFocusedItem()) {
return focusedController.getItem();
}
return null;
}
public StripItem getPressedItem() {
if (hasPressedItem()) {
return pressedController.getItem();
}
return null;
}
public StripItem getDraggedItem() {
if (hasDraggedItem()) {
return draggedController.getItem();
}
return null;
}
public StripItem[] getSelectedItems() {
Set<StripItem> items = new HashSet<StripItem>();
for (StripItemController controller : selectedControllers) {
items.add(controller.getItem());
}
return items.toArray(new StripItem[items.size()]);
}
public boolean isItemFocused(StripItem item) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
return hasFocusedItem() && focusedController.itemEquals(item);
}
public boolean isItemPressed(StripItem item) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
return hasPressedItem() && pressedController.itemEquals(item);
}
public boolean isItemDragged(StripItem item) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
return hasDraggedItem() && draggedController.itemEquals(item);
}
public boolean isItemSelected(StripItem item) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
StripItemController controller = getController(item);
if (controller == null) {
throw new IllegalArgumentException();
}
return selectedControllers.contains(controller);
}
protected void onSetData(Event event) {
StripItem item = (StripItem )event.item;
if (event.detail == SWT.INSERT) {
insertItem(item, event.index);
} else if (event.detail == SWT.DEL) {
removeItem(item);
}
}
protected void onPaint(Event event) {
for (StripItemController controller : controllers) {
if (isItemDragged(controller.getItem())) {
continue;
}
firePaintItemEvent(controller, event.gc);
}
if (draggedController != null) {
firePaintItemEvent(draggedController, event.gc);
}
}
protected void onPaintItem(Event event) {
GC gc = event.gc;
StripItem item = (StripItem )event.item;
StripItemController controller = getController(item);
paintItemBackground(gc, controller);
paintItemImage(gc, controller);
paintItemText(gc, controller);
paintItemCloseButton(gc, controller);
paintItemFrame(gc, controller);
}
protected void onMouseEnter(Event event) {
processFocus(getController(event.x, event.y));
}
protected void onMouseExit(Event event) {
processFocus(getController(event.x, event.y));
}
protected void onMouseDown(Event event) {
if (event.button != 1) {
return;
}
StripItemController controller = getController(event.x, event.y);
if (controller == null) {
return;
}
StripItemState state = StripItemState.PRESSED;
controller.setForeground(state.getForeground());
controller.setBackground(state.getBackground());
Rectangle bounds = controller.getItemBounds();
redraw(bounds.x, bounds.y, bounds.width, bounds.height, true);
dragContext = new StripItemDragContext();
dragContext.setMouseDownLocation(event.x, event.y);
dragContext.setMouseDownOffset(event.x - bounds.x, event.y - bounds.y);
dragContext.setMouseMoveLocation(event.x, event.y);
pressedController = controller;
}
protected void onMouseMove(Event event) {
processFocus(getController(event.x, event.y));
if (dragContext == null) {
return;
}
Point mouseDownDistance = dragContext.calculateMouseDownDistance(event.x, event.y);
Point mouseMoveDistance = dragContext.calculateMouseMoveDistance(event.x, event.y);
dragContext.setMouseMoveLocation(event.x, event.y);
if (draggedController == null && Math.abs(mouseDownDistance.x) > SPACE) {
draggedController = pressedController;
pressedController = null;
}
if (draggedController == null) {
return;
}
Rectangle oldBounds = draggedController.getItemBounds();
oldBounds.x += draggedController.getOffset();
draggedController.offset(mouseMoveDistance.x);
Rectangle newBounds = draggedController.getItemBounds();
newBounds.x += draggedController.getOffset();
redraw(oldBounds.x, oldBounds.y, oldBounds.width, oldBounds.height, true);
redraw(newBounds.x, newBounds.y, newBounds.width, newBounds.height, true);
}
protected void onMouseUp(Event event) {
if (event.button != 1) {
return;
}
dragContext = null;
if (pressedController != null) {
StripItemState state;
if (pressedController.isItemSelected()) {
if (isMultiSelection()) {
state = StripItemState.FOCUSED;
} else {
state = StripItemState.SELECTED;
}
} else {
state = StripItemState.SELECTED;
}
pressedController.setForeground(state.getForeground());
pressedController.setBackground(state.getBackground());
Rectangle bounds = pressedController.getItemBounds();
redraw(bounds.x, bounds.y, bounds.width, bounds.height, true);
if (pressedController.isItemSelected()) {
if (isMultiSelection()) {
selectedControllers.remove(pressedController);
}
} else {
if (!isMultiSelection()) {
StripItemController[] controllers = selectedControllers.toArray(
new StripItemController[selectedControllers.size()]);
selectedControllers.clear();
for (StripItemController aController : controllers) {
StripItemState aState = StripItemState.DEFAULT;
aController.setForeground(aState.getForeground());
animateBackground(aController, aState.getBackground());
}
}
selectedControllers.add(pressedController);
}
pressedController = null;
}
if (draggedController != null) {
StripItemState state;
Rectangle bounds = draggedController.getItemBounds();
animateOffset(draggedController, bounds.x);
draggedController = null;
}
}
protected void onDispose(Event event) {
StripItem[] items = getItems();
for (StripItem item : items) {
item.dispose();
}
}
private void firePaintItemEvent(StripItemController controller, GC gc) {
Rectangle bounds = controller.getItemBounds();
Event redirEvent = new Event();
redirEvent.gc = gc;
redirEvent.time = (int )(System.currentTimeMillis() & 0xffffffffL);
redirEvent.widget = this;
redirEvent.item = controller.getItem();
redirEvent.x = bounds.x;
redirEvent.y = bounds.y;
redirEvent.width = bounds.width;
redirEvent.height = bounds.height;
notifyListeners(SWT.PaintItem, redirEvent);
}
private void processFocus(StripItemController newController) {
StripItemController oldController = focusedController;
if (oldController == null && newController == null) {
return;
}
if (oldController != null && oldController.equals(newController)) {
return;
}
if (newController != null && newController.equals(oldController)) {
return;
}
if (hasDraggedItem()) {
return;
}
if (oldController != null) {
if (!oldController.isItemPressed() && !oldController.isItemSelected()) {
StripItemState state = StripItemState.DEFAULT;
oldController.setForeground(state.getForeground());
animateBackground(oldController, state.getBackground());
}
focusedController = null;
}
if (newController != null) {
if (!newController.isItemPressed() && !newController.isItemSelected()) {
StripItemState state = StripItemState.FOCUSED;
newController.setForeground(state.getForeground());
animateBackground(newController, state.getBackground());
}
focusedController = newController;
}
}
private void insertItem(StripItem item, int index) {
if (containsItem(item)) {
throw new IllegalArgumentException();
}
if (index < 0 || index > getItemCount()) {
index = getItemCount();
}
StripItemController controller = new StripItemController(item);
controllers.add(index, controller);
}
private void removeItem(StripItem item) {
if (!containsItem(item)) {
throw new IllegalArgumentException();
}
StripItemController controller = getController(item);
controllers.remove(controller);
}
private void paintItemBackground(GC gc, StripItemController controller) {
if (!controller.hasBackground()) {
return;
}
Color oldBackground = gc.getBackground();
Color newBackground = newColor(controller.getBackground());
gc.setBackground(newBackground);
Rectangle bounds = controller.getItemBounds();
bounds.x += controller.getOffset();
gc.fillRectangle(bounds);
gc.setBackground(oldBackground);
newBackground.dispose();
}
private void paintItemImage(GC gc, StripItemController controller) {
if (!controller.hasItemImage()) {
return;
}
Image image = controller.getItemImage();
Rectangle bounds = controller.getItemImageBounds();
bounds.x += controller.getOffset();
gc.drawImage(image, bounds.x, bounds.y);
}
private void paintItemText(GC gc, StripItemController controller) {
if (!controller.hasItemText()) {
return;
}
Color oldForeground = gc.getForeground();
Color newForeground = newColor(controller.getForeground());
gc.setForeground(newForeground);
String text = controller.getItemText();
Rectangle bounds = controller.getItemTextBounds();
bounds.x += controller.getOffset();
gc.drawText(text, bounds.x, bounds.y, true);
gc.setForeground(oldForeground);
newForeground.dispose();
}
private void paintItemCloseButton(GC gc, StripItemController controller) {
Image closeButtonImage = getItemCloseButtonImage();
Rectangle bounds = controller.getItemCloseButtonBounds();
bounds.x += controller.getOffset();
gc.drawImage(closeButtonImage, bounds.x, bounds.y);
}
private void paintItemFrame(GC gc, StripItemController controller) {
Display display = getDisplay();
Color foreground = gc.getForeground();
gc.setForeground(display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
Rectangle bounds = controller.getItemBounds();
bounds.x += controller.getOffset();
gc.drawRectangle(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1);
gc.setForeground(foreground);
}
private StripItemController getController(int x, int y) {
for (StripItemController controller : controllers) {
StripItem item = controller.getItem();
Rectangle bounds = item.getBounds();
if (bounds.contains(x, y)) {
return controller;
}
}
return null;
}
private StripItemController getController(StripItem item) {
for (StripItemController controller : controllers) {
if (controller.itemEquals(item)) {
return controller;
}
}
return null;
}
private Color newColor(RGB rgb) {
if (rgb == null) {
throw new NullPointerException();
}
Display display = getDisplay();
return new Color(display, rgb);
}
private void animateOffset(final StripItemController controller, final int to) {
if (controller == null) {
throw new NullPointerException();
}
if (controller.hasOffsetAnimator()) {
Animator animator = controller.getOffsetAnimator();
animator.cancel();
controller.setOffsetAnimator(null);
}
Rectangle bounds = controller.getItemBounds();
bounds.x += controller.getOffset();
final int from = bounds.x;
TimingTarget tt = new TimingTargetAdapter() {
@Override
public void timingEvent(Animator source, double fraction) {
int distance = (int )Math.round(((double )to - (double )from) * fraction);
Rectangle bounds = controller.getItemBounds();
int oldOffset = controller.getOffset();
int newOffset = from + distance - bounds.x;
controller.setOffset(newOffset);
if (!hasDraggedItem()) {
Point mouseLocation = getDisplay().getCursorLocation();
mouseLocation = toControl(mouseLocation);
Rectangle testBounds = controller.getItemBounds();
testBounds.x += controller.getOffset();
if (testBounds.contains(mouseLocation)) {
processFocus(controller);
} else {
processFocus(null);
}
}
redraw(bounds.x + oldOffset, bounds.y, bounds.width, bounds.height, true);
redraw(bounds.x + newOffset, bounds.y, bounds.width, bounds.height, true);
}
@Override
public void end(Animator source) {
Rectangle oldBounds = controller.getItemBounds();
oldBounds.x += controller.getOffset();
Rectangle newBounds = controller.getItemBounds();
int newOffset = to - newBounds.x;
controller.setOffset(newOffset);
newBounds.x += newOffset;
redraw(oldBounds.x, oldBounds.y, oldBounds.width, oldBounds.height, true);
redraw(newBounds.x, newBounds.y, newBounds.width, newBounds.height, true);
}
};
Interpolator interpol = new AccelerationInterpolator(0, 0.9);
Animator animator = new Animator.Builder().addTarget(tt).setDuration(160, TimeUnit.MILLISECONDS).setInterpolator(interpol).build();
controller.setOffsetAnimator(animator);
animator.start();
}
private void animateBackground(final StripItemController controller, final RGB to) {
if (controller == null) {
throw new NullPointerException();
}
if (controller.hasBackgroundAnimator()) {
Animator animator = controller.getBackgroundAnimator();
animator.cancel();
controller.setBackgroundAnimator(null);
}
final RGB from = controller.getBackground();
TimingTarget tt = new TimingTargetAdapter() {
@Override
public void timingEvent(Animator source, double fraction) {
double r = to.red - from.red;
double g = to.green - from.green;
double b = to.blue - from.blue;
r *= fraction;
g *= fraction;
b *= fraction;
r += from.red;
g += from.green;
b += from.blue;
int red = (int )Math.round(r);
int green = (int )Math.round(g);
int blue = (int )Math.round(b);
RGB rgb = new RGB(red, green, blue);
controller.setBackground(rgb);
Rectangle bounds = controller.getItemBounds();
redraw(bounds.x, bounds.y, bounds.width, bounds.height, true);
}
@Override
public void end(Animator source) {
controller.setBackground(to);
Rectangle bounds = controller.getItemBounds();
redraw(bounds.x, bounds.y, bounds.width, bounds.height, true);
}
};
Interpolator interpol = new AccelerationInterpolator(0, 0.9);
Animator animator = new Animator.Builder().addTarget(tt).setDuration(160, TimeUnit.MILLISECONDS).setInterpolator(interpol).build();
controller.setBackgroundAnimator(animator);
animator.start();
}
public static void main(String[] args) {
Display display = Display.getDefault();
TimingSource timingSource = new SWTTimingSource(16, TimeUnit.MILLISECONDS, display);
timingSource.init();
Animator.setDefaultTimingSource(timingSource);
Shell shell = new Shell(display);
GridLayout layout = new GridLayout();
layout.horizontalSpacing = 0;
layout.verticalSpacing = 0;
layout.marginWidth = 0;
layout.marginHeight = 0;
shell.setLayout(layout);
StripBar bar = new StripBar(shell, SWT.DOUBLE_BUFFERED | SWT.BORDER | SWT.MULTI);
GridData data = new GridData();
data.horizontalAlignment = GridData.FILL;
data.verticalAlignment = GridData.FILL;
data.grabExcessHorizontalSpace = true;
bar.setLayoutData(data);
StripItem item1 = new StripItem(bar, SWT.NONE);
item1.setText("Item 1");
StripItem item2 = new StripItem(bar, SWT.NONE);
item2.setImage(UIImageResource.T.addContact16());
StripItem item3 = new StripItem(bar, SWT.NONE);
StripItem item0 = new StripItem(bar, SWT.NONE, 0);
item0.setText("Item 0");
item0.setImage(UIImageResource.T.about16());
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
}
}