/*******************************************************************************
* Copyright (c) Emil Crumhorn - Hexapixel.com - emil.crumhorn@gmail.com
* 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:
* emil.crumhorn@gmail.com - initial API and implementation
*******************************************************************************/
package com.hexapixel.widgets.ribbon;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.events.ShellListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Region;
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.Menu;
import org.eclipse.swt.widgets.Shell;
import com.hexapixel.widgets.generic.ColorCache;
import com.hexapixel.widgets.generic.CursorCache;
public class RibbonShell implements MouseListener, MouseMoveListener, KeyListener, ShellListener, ControlListener {
private Shell mShell;
private Point mMouseDownLoc;
private RibbonTabFolder mTabFolder;
private Color bgColor = ColorCache.getInstance().getColor(177, 200, 231);
private static final int SIDE_NONE = 0;
private static final int SIDE_TOP_LEFT = 1;
private static final int SIDE_LEFT = 2;
private static final int SIDE_BOTTOM_LEFT = 3;
private static final int SIDE_BOTTOM = 4;
private static final int SIDE_BOTTOM_RIGHT = 5;
private static final int SIDE_RIGHT = 6;
private static final int SIDE_TOP_RIGHT = 7;
private static final int SIDE_TOP = 8;
private Rectangle topLeftCorner;
private Rectangle leftSide;
private Rectangle bottomLeftCorner;
private Rectangle bottomSide;
private Rectangle bottomRightCorner;
private Rectangle rightSide;
private Rectangle topRightCorner;
private Rectangle topSide;
private Rectangle mArrowButtonBounds;
private int mArrowButtonState = AbstractButtonPaintManager.STATE_NONE;
public static final int CORNER_FLEX = 8;
public static final int SIDE_FLEX = 4;
private int sideCurrentlyResizing = SIDE_NONE;
private Point mShellSizeBeforeResize;
private boolean mMouseIsDown;
private Image buttonImage;
private QuickAccessShellToolbar mToolbar;
private Menu mBigButtonMenu;
private List<SelectionListener> mBigButtonListeners;
private RibbonTooltip mBigButtonTooltip;
private boolean mShellMaximized;
private Point mNonMaximizedLocation;
private Point mNonMaximizedSize;
public RibbonShell(Display parent) {
mShell = new Shell(parent, SWT.NO_TRIM | SWT.DOUBLE_BUFFERED );
init();
}
public RibbonShell(Display parent, Image buttonImage) {
this(parent);
this.buttonImage = buttonImage;
init();
}
public RibbonShell(Display parent, int style) {
mShell = new Shell(parent, style);
init();
}
public RibbonShell(Shell parent, int style) {
mShell = new Shell(parent, style);
init();
}
public RibbonShell(Shell parent, int style, Image buttonImage) {
this(parent, style);
this.buttonImage = buttonImage;
init();
}
public RibbonShell(Display parent, int style, Image buttonImage) {
this(parent, style);
this.buttonImage = buttonImage;
init();
}
public QuickAccessShellToolbar getToolbar() {
return mToolbar;
}
// internal
List<RibbonButton> getToolbarButtons() {
return mToolbar.getButtons();
}
public Image getButtonImage() {
return buttonImage;
}
public void setButtonImage(Image buttonImage) {
this.buttonImage = buttonImage;
}
public void setMaximized(boolean maximized) {
if (mShellMaximized != maximized) {
mShellMaximized = maximized;
if (mShellMaximized)
maximize();
else
restore();
}
}
private void maximize() {
mNonMaximizedLocation = mShell.getLocation();
mNonMaximizedSize = mShell.getSize();
Rectangle maxBounds = mShell.getMonitor().getClientArea();
mShell.setLocation(0, 0);
mShell.setSize(maxBounds.width, maxBounds.height);
}
private void restore() {
mShell.setSize(mNonMaximizedSize);
mShell.setLocation(mNonMaximizedLocation);
}
public boolean getMaximized() {
return mShellMaximized;
}
private void init() {
mBigButtonListeners = new ArrayList<SelectionListener>();
mBigButtonMenu = new Menu(mShell);
mToolbar = new QuickAccessShellToolbar(this);
updateCalculations();
mShell.setLayout(new RibbonShellLayout());
mTabFolder = new RibbonTabFolder(mShell, SWT.NONE, this);
mShell.addMouseListener(this);
mShell.addMouseMoveListener(this);
mShell.addShellListener(this);
mShell.addKeyListener(this);
mShell.addControlListener(this);
mShell.addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
drawBackground(e.gc);
}
});
}
private void drawBackground(GC gc) {
Rectangle bounds = mShell.getBounds();
if (!getMaximized()) {
Color borderColor = null;
if (Display.getDefault().getActiveShell() == mShell)
borderColor = AbstractShellPainter.outerBorderNonMaximized;
else
borderColor = AbstractShellPainter.outerBorderNonMaximized_Inactive;
gc.setBackground(borderColor);
gc.fillRectangle(0, 0, bounds.width, bounds.height);
gc.setBackground(bgColor);
gc.fillRectangle(1, 1, bounds.width-2, bounds.height-2);
// draw pretty borders, only bottoms, the top borders (as the menubar needs to connect for the big button to work)
// are drawn in a similar fashion in the RibbonTabFolder
gc.setForeground(borderColor);
// bottom left
gc.drawLine(1, bounds.height-4, 1, bounds.height-4);
gc.drawLine(1, bounds.height-3, 2, bounds.height-3);
gc.drawLine(2, bounds.height-2, 3, bounds.height-2);
// bottom right
gc.drawLine(bounds.width-4, bounds.height-2, bounds.width-3, bounds.height-2);
gc.drawLine(bounds.width-3, bounds.height-3, bounds.width-2, bounds.height-3);
gc.drawLine(bounds.width-2, bounds.height-4, bounds.width-2, bounds.height-4);
}
else {
gc.setBackground(bgColor);
gc.fillRectangle(bounds);
}
}
public Shell getShell() {
return mShell;
}
public void setSize(int w, int h) {
mShell.setSize(w, h);
}
public void setSize(Point size) {
mShell.setSize(size);
}
public void setText(String text) {
// this is for the OS
mShell.setText(text);
redrawContents();
}
public void redrawContents() {
mTabFolder.redraw();
}
public boolean isDisposed() {
return mShell.isDisposed();
}
void setArrowButtonBounds(Rectangle bounds) {
mArrowButtonBounds = bounds;
}
int getArrowButtonState() {
return mArrowButtonState;
}
Rectangle getArrowButtonBounds() {
return mArrowButtonBounds;
}
public void setBigButtonTooltip(RibbonTooltip tooltip) {
mBigButtonTooltip = tooltip;
}
public RibbonTooltip getBigButtonTooltip() {
return mBigButtonTooltip;
}
public void addBigButtonListener(SelectionListener listener) {
if (!mBigButtonListeners.contains(listener))
mBigButtonListeners.add(listener);
}
public void open() {
mShell.open();
// re-layout post shell open or the tabfolder will look unpopulated until some action happens
Display.getDefault().asyncExec(new Runnable() {
public void run() {
mTabFolder.layout(true, true);
mTabFolder.redraw();
mToolbar.updateBounds();
}
});
}
public void setVisible(boolean visible) {
mShell.setVisible(visible);
}
public String getText() {
return mShell.getText();
}
public RibbonTabFolder getRibbonTabFolder() {
return mTabFolder;
}
private void updateCalculations() {
Rectangle bounds = mShell.getBounds();
topLeftCorner = new Rectangle(0, 0, CORNER_FLEX, CORNER_FLEX);
leftSide = new Rectangle(0, CORNER_FLEX, SIDE_FLEX, bounds.height - (CORNER_FLEX * 2));
bottomLeftCorner = new Rectangle(0, bounds.height-CORNER_FLEX, CORNER_FLEX, CORNER_FLEX);
bottomSide = new Rectangle(CORNER_FLEX, bounds.height-SIDE_FLEX, bounds.width - (CORNER_FLEX * 2), SIDE_FLEX);
bottomRightCorner = new Rectangle(bounds.width-CORNER_FLEX, bounds.height-CORNER_FLEX, CORNER_FLEX, CORNER_FLEX);
rightSide = new Rectangle(bounds.width-SIDE_FLEX, CORNER_FLEX, SIDE_FLEX, bounds.height - (CORNER_FLEX * 2));
topRightCorner = new Rectangle(bounds.width-CORNER_FLEX, 0, CORNER_FLEX, CORNER_FLEX);
topSide = new Rectangle(CORNER_FLEX, 0, bounds.width-(CORNER_FLEX * 2), SIDE_FLEX);
}
private boolean isInside(int x, int y, Rectangle rect) {
if (rect == null) {
return false;
}
return x >= rect.x && y >= rect.y && x <= (rect.x + rect.width - 1) && y <= (rect.y + rect.height - 1);
}
private int getResizeSide(int x, int y) {
if (isInside(x, y, topLeftCorner))
return SIDE_TOP_LEFT;
else if (isInside(x, y, leftSide))
return SIDE_LEFT;
else if (isInside(x, y, bottomLeftCorner))
return SIDE_BOTTOM_LEFT;
else if (isInside(x, y, bottomSide))
return SIDE_BOTTOM;
else if (isInside(x, y, bottomRightCorner))
return SIDE_BOTTOM_RIGHT;
else if (isInside(x, y, rightSide))
return SIDE_RIGHT;
else if (isInside(x, y, topRightCorner))
return SIDE_TOP_RIGHT;
else if (isInside(x, y, topSide))
return SIDE_TOP;
return SIDE_NONE;
}
// internal callback for when the big button is clicked
void bigButtonClicked(MouseEvent me) {
Event e = new Event();
e.button = me.button;
e.data = this;
e.display = me.display;
e.stateMask = me.stateMask;
e.widget = me.widget;
e.x = me.x;
e.y = me.y;
SelectionEvent se = new SelectionEvent(e);
for (int i = 0; i < mBigButtonListeners.size(); i++) {
mBigButtonListeners.get(i).widgetSelected(se);
}
}
public void showBigButtonMenu() {
Rectangle bBounds = mTabFolder.getBigButtonBounds();
mBigButtonMenu.setLocation(mShell.toDisplay(bBounds.x, bBounds.height+bBounds.y));
mBigButtonMenu.setVisible(true);
}
public Menu getBigButtonMenu() {
return mBigButtonMenu;
}
public void mouseMove(MouseEvent e) {
Cursor cur = mShell.getCursor();
if (isInside(e.x, e.y, mToolbar.getBounds())) {
if (mToolbar.mouseMove(e))
redrawToolbar();
}
else {
if (mToolbar.dehover())
redrawToolbar();
}
if (mArrowButtonBounds != null) {
if (mArrowButtonState != AbstractButtonPaintManager.STATE_SELECTED) {
if (isInside(e.x, e.y, mArrowButtonBounds)) {
if (mArrowButtonState == AbstractButtonPaintManager.STATE_NONE) {
mArrowButtonState = AbstractButtonPaintManager.STATE_HOVER;
redrawArrowButton();
}
}
else {
if (mArrowButtonState != AbstractButtonPaintManager.STATE_NONE) {
mArrowButtonState = AbstractButtonPaintManager.STATE_NONE;
redrawArrowButton();
}
}
}
}
if (getMaximized()) {
if (cur != null)
mShell.setCursor(null);
return;
}
if (!mShellMaximized) {
int side = getResizeSide(e.x, e.y);
doResize(side, e);
}
}
private void redrawToolbar() {
Rectangle bounds = mToolbar.getBounds();
mTabFolder.redraw(bounds.x, bounds.y, bounds.width, bounds.height, false);
}
private void redrawArrowButton() {
mTabFolder.redraw(mArrowButtonBounds.x, mArrowButtonBounds.y, mArrowButtonBounds.width, mArrowButtonBounds.height, false);
}
private void doResize(int side, MouseEvent e) {
if (sideCurrentlyResizing == SIDE_NONE) {
Cursor cursor = null;
switch (side) {
case SIDE_TOP_LEFT:
case SIDE_BOTTOM_RIGHT:
cursor = CursorCache.getCursor(SWT.CURSOR_SIZENWSE);
break;
case SIDE_LEFT:
case SIDE_RIGHT:
cursor = CursorCache.getCursor(SWT.CURSOR_SIZEWE);
break;
case SIDE_BOTTOM_LEFT:
case SIDE_TOP_RIGHT:
cursor = CursorCache.getCursor(SWT.CURSOR_SIZENESW);
break;
case SIDE_TOP:
case SIDE_BOTTOM:
cursor = CursorCache.getCursor(SWT.CURSOR_SIZENS);
break;
}
mShell.setCursor(cursor);
}
// this clause deals with shell resizing as our shell has no borders but we still need to mimic it
// it's all just location and size pushing to simulate a mouse resize
if (sideCurrentlyResizing != SIDE_NONE) {
// do shell resize
Point point = Display.getDefault().map(mShell, null, e.x, e.y);
int deltaX = e.x - mMouseDownLoc.x;
int deltaY = e.y - mMouseDownLoc.y;
switch (sideCurrentlyResizing) {
case SIDE_LEFT:
mShell.setLocation(point.x - mMouseDownLoc.x, mShell.getLocation().y);
mShell.setSize(mShell.getSize().x - deltaX, mShell.getSize().y);
break;
case SIDE_BOTTOM:
mShell.setSize(mShell.getSize().x, mShellSizeBeforeResize.y + deltaY);
break;
case SIDE_RIGHT:
mShell.setSize(mShellSizeBeforeResize.x + deltaX, mShell.getSize().y);
break;
case SIDE_TOP:
mShell.setSize(mShellSizeBeforeResize.x, mShell.getSize().y - deltaY);
mShell.setLocation(mShell.getLocation().x, point.y - mMouseDownLoc.y);
break;
case SIDE_TOP_LEFT:
mShell.setLocation(point.x - mMouseDownLoc.x, point.y - mMouseDownLoc.y);
mShell.setSize(mShell.getSize().x - deltaX, mShell.getSize().y - deltaY);
break;
case SIDE_BOTTOM_LEFT:
mShell.setLocation(point.x - mMouseDownLoc.x, mShell.getLocation().y);
mShell.setSize(mShell.getSize().x - deltaX, mShellSizeBeforeResize.y + deltaY);
break;
case SIDE_BOTTOM_RIGHT:
mShell.setSize(mShellSizeBeforeResize.x + deltaX, mShellSizeBeforeResize.y + deltaY);
break;
case SIDE_TOP_RIGHT:
mShell.setLocation(mShell.getLocation().x, point.y - mMouseDownLoc.y);
mShell.setSize(mShellSizeBeforeResize.x + deltaX, mShell.getSize().y - deltaY);
break;
}
/*
// TODO: Make this pretty and flicker-less
if (mShell.getSize().x < 40)
mShell.setSize(40, mShell.getSize().y);
if (mShell.getSize().y < 40);
mShell.setSize(mShell.getSize().y, 40);*/
}
}
public void moveShell(MouseEvent e) {
if (mMouseDownLoc == null || mShellMaximized)
return;
Point point = Display.getDefault().map(mShell, null, e.x, e.y);
mShell.setLocation(point.x - mMouseDownLoc.x, point.y - mMouseDownLoc.y);
}
public void mouseDoubleClick(MouseEvent e) {
}
public void mouseDown(MouseEvent e) {
if (e.button == 1) {
mMouseIsDown = true;
mMouseDownLoc = new Point(e.x, e.y);
mShellSizeBeforeResize = mShell.getSize();
sideCurrentlyResizing = getResizeSide(e.x, e.y);
}
if (isInside(e.x, e.y, mToolbar.getBounds())) {
if (mToolbar.mouseDown(e))
redrawToolbar();
}
if (isInside(e.x, e.y, mArrowButtonBounds)) {
// if we click a selected button, deselect and switch to hover
if (mArrowButtonState == AbstractButtonPaintManager.STATE_SELECTED) {
mArrowButtonState = AbstractButtonPaintManager.STATE_HOVER;
redrawArrowButton();
}
else {
// click
mArrowButtonState = AbstractButtonPaintManager.STATE_SELECTED;
redrawArrowButton();
arrowButtonClicked();
}
}
else {
// reset state if clicked elsewhere
if (mArrowButtonState != AbstractButtonPaintManager.STATE_NONE) {
mArrowButtonState = AbstractButtonPaintManager.STATE_NONE;
redrawArrowButton();
}
}
}
private void arrowButtonClicked() {
// TODO: something
}
public void mouseUp(MouseEvent e) {
mMouseIsDown = false;
mMouseDownLoc = null;
sideCurrentlyResizing = SIDE_NONE;
// update side resize cursor
doResize(getResizeSide(e.x, e.y), e);
// don't let the shell disappear due to extremely small size
Point size = mShell.getSize();
if (size.x < 40 || size.y < 40) {
if (size.x < 40)
size.x = 40;
if (size.y < 40)
size.y = 40;
mShell.setSize(size.x, size.y);
}
}
public void shellActivated(ShellEvent e) {
redrawContents();
mShell.redraw();
}
public void shellClosed(ShellEvent e) {
}
public void shellDeactivated(ShellEvent e) {
redrawContents();
mShell.redraw();
}
public void shellDeiconified(ShellEvent e) {
redrawContents();
}
public void shellIconified(ShellEvent e) {
}
public void keyPressed(KeyEvent e) {
if (e.keyCode == SWT.F4 && e.stateMask == SWT.ALT)
mShell.dispose();
}
public void keyReleased(KeyEvent e) {
}
public void controlMoved(ControlEvent e) {
}
public void controlResized(ControlEvent e) {
updateCalculations();
updateShellStructure();
//not needed
// redrawContents();
}
private void updateShellStructure() {
Region region = new Region(mShell.getDisplay());
int maxX = mShell.getBounds().width;
int maxY = mShell.getBounds().height;
region.add(0, 0, maxX, maxY);
if (!getMaximized()) {
// top left
region.subtract(0, 0, 4, 1);
region.subtract(0, 1, 2, 1);
region.subtract(0, 2, 1, 1);
region.subtract(0, 3, 1, 1);
// bottom right
region.subtract(maxX-1, maxY-4, 1, 4);
region.subtract(maxX-2, maxY-2, 1, 2);
region.subtract(maxX-3, maxY-1, 1, 1);
region.subtract(maxX-4, maxY-1, 1, 1);
// bottom left
region.subtract(0, maxY-4, 1, 4);
region.subtract(1, maxY-2, 1, 2);
region.subtract(2, maxY-1, 1, 1);
region.subtract(3, maxY-1, 1, 1);
// top right
region.subtract(maxX-4, 0, 4, 1);
region.subtract(maxX-2, 1, 2, 1);
region.subtract(maxX-1, 2, 1, 1);
region.subtract(maxX-1, 3, 1, 1);
}
mShell.setRegion(region);
}
class RibbonShellLayout extends Layout {
private Point sizes [];
private void init(Composite composite) {
Control [] children = composite.getChildren();
sizes = new Point[children.length];
for (int i = 0; i < children.length; i++) {
sizes[i] = children[i].computeSize(SWT.DEFAULT, SWT.DEFAULT);
}
}
@Override
protected Point computeSize(Composite composite, int hint, int hint2,
boolean flushCache) {
if (flushCache) {
init(composite);
}
return null;
}
@Override
protected void layout(Composite composite, boolean flushCache) {
Control [] children = composite.getChildren();
if (sizes == null || flushCache || sizes.length != children.length)
init(composite);
int y = 0;
int x = getMaximized() ? 0 : 1; // border width
for (int i = 0; i < children.length; i++) {
Control child = children[i];
if (child instanceof RibbonTabFolder)
child.setBounds(new Rectangle(x, y, mShell.getBounds().width-2, sizes[i].y));
else
child.setBounds(new Rectangle(x, y, sizes[i].x-2, sizes[i].y));
y += sizes[i].y;
}
}
}
}