package org.gudy.azureus2.ui.swt.components.shell;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.*;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.ui.swt.Utils;
import org.gudy.azureus2.ui.swt.mainwindow.IMainWindow;
import com.aelitis.azureus.ui.swt.UIFunctionsManagerSWT;
import com.aelitis.azureus.ui.swt.UIFunctionsSWT;
public class LightBoxShell
{
private Shell lbShell = null;
private Shell parentShell = null;
private Rectangle fadedAreaExtent = null;
private int insetTop = 0;
private int insetBottom = 0;
private int insetLeft = 0;
private int insetRight = 0;
private boolean closeOnESC = false;
private boolean isAlreadyOpened = false;
private Display display;
private UIFunctionsSWT uiFunctions;
private boolean isAlphaSupported = true;
public LightBoxShell() {
this(false);
}
/**
* Creates a LightBoxShell without opening it
* @param closeOnESC if <code>true</code> then the ESC key can be used to dismiss the lightbox
*/
public LightBoxShell(boolean closeOnESC) {
this.closeOnESC = closeOnESC;
uiFunctions = UIFunctionsManagerSWT.getUIFunctionsSWT();
if (null == uiFunctions) {
throw new NullPointerException(
"An initialized instance of UIFunctionsSWT is required to create a LightBoxShell");
}
parentShell = uiFunctions.getMainShell();
if (null == parentShell) {
return;
}
IMainWindow mainWindow = uiFunctions.getMainWindow();
Rectangle r = mainWindow.getMetrics(IMainWindow.WINDOW_ELEMENT_STATUSBAR);
setInsets(0, r.height, 0, 0);
createControls();
}
public LightBoxShell(Shell parentShell) {
this.parentShell = parentShell;
createControls();
}
public void setInsets(int top, int bottom, int left, int right) {
this.insetTop = top;
this.insetBottom = bottom;
this.insetLeft = left;
this.insetRight = right;
}
private void createControls() {
lbShell = new Shell(parentShell, SWT.NO_TRIM | SWT.APPLICATION_MODAL);
/*
* Try and set the alpha; if an exception is thrown then set isAlphaSupported to false
*/
try {
lbShell.setAlpha(255);
} catch (Throwable t) {
isAlphaSupported = false;
}
/*
* Black mask with 30% transparency
*/
lbShell.setBackground(new Color(parentShell.getDisplay(), 0, 0, 0));
setAlpha(lbShell, 178);
display = parentShell.getDisplay();
/*
* Trap and prevent the ESC key from closing the shell
*/
if (false == closeOnESC) {
lbShell.addListener(SWT.Traverse, new Listener() {
public void handleEvent(Event e) {
if (e.detail == SWT.TRAVERSE_ESCAPE) {
e.doit = false;
}
}
});
}
/*
* For OSX add this listener to make sure that the parent shell and
* the lighbox shell behave like they are sandwiched together; without this
* then external applications can slide in between the parent shell and the
* lightbox which creates a strange visual effect
*/
if (true == Constants.isOSX) {
lbShell.addShellListener(new ShellAdapter() {
public void shellActivated(ShellEvent e) {
if (null != parentShell && false == parentShell.isDisposed()) {
parentShell.forceActive();
}
}
});
}
}
public void setAlpha(Shell shell, int alpha) {
if (true == isAlphaSupported && null != shell) {
shell.setAlpha(alpha);
}
}
public void open() {
if (null != lbShell && false == lbShell.isDisposed()) {
lbShell.setBounds(getTargetArea());
isAlreadyOpened = true;
lbShell.open();
}
}
public void close() {
Utils.execSWTThread(new AERunnable() {
public void runSupport() {
if (null != lbShell && false == lbShell.isDisposed()) {
lbShell.close();
}
}
});
}
/**
* Returns the effective area for the lightbox
* @return
*/
private Rectangle getTargetArea() {
if (null == fadedAreaExtent) {
/*
* Not entirely sure why this has to be done this way but it seems
* the Windows' shell has a 4 pixel border whereas the OSX's shell has none;
* this offset is used to shift the image to fit the client area exactly
*/
int xyOffset = (true == Constants.isOSX) ? 0 : 4;
fadedAreaExtent = parentShell.getClientArea();
Point parentLocation = parentShell.getLocation();
fadedAreaExtent.x = parentLocation.x + xyOffset + insetLeft;
fadedAreaExtent.y = parentLocation.y + parentShell.getSize().y
- fadedAreaExtent.height - xyOffset + insetTop;
fadedAreaExtent.width -= insetRight + insetLeft;
fadedAreaExtent.height -= insetTop + insetBottom;
}
return fadedAreaExtent;
}
/**
* Creates a stylized shell with pre-defined look and feel
* @param closeLightboxOnExit
* @return
*/
public StyledShell createStyledShell(int borderWidth,
boolean closeLightboxOnExit) {
StyledShell newShell = new StyledShell(borderWidth);
if (true == closeLightboxOnExit) {
newShell.addListener(SWT.Close, new Listener() {
public void handleEvent(Event event) {
close();
}
});
}
return newShell;
}
/**
* Centers and opens the given shell and closes the light box when the given shell is closed
* @param shellToOpen
*/
public void open(StyledShell shellToOpen) {
if (null != shellToOpen && null != lbShell) {
if (false == isAlreadyOpened) {
open();
}
if (false == shellToOpen.isAlreadyOpened()) {
shellToOpen.open();
}
}
}
public void setCursor(Cursor cursor) {
if (null != lbShell && false == lbShell.isDisposed()) {
lbShell.setCursor(cursor);
}
}
public void setData(String key, Object value) {
if (null != lbShell && false == lbShell.isDisposed()) {
lbShell.setData(key, value);
}
}
public class StyledShell
{
private Shell styledShell;
private Composite borderedBackground;
private Composite content;
private int borderWidth;
private boolean isAlreadyOpened = false;
private int alpha = 230;
private boolean isAnimating = false;
private StyledShell(int borderWidth) {
this.borderWidth = borderWidth;
styledShell = new Shell(lbShell, getShellStyle(SWT.NONE));
LightBoxShell.this.setAlpha(styledShell, 0);
if (true == Constants.isOSX) {
uiFunctions.createMainMenu(styledShell);
}
FillLayout fillLayout = new FillLayout();
fillLayout.marginHeight = borderWidth;
fillLayout.marginWidth = borderWidth;
styledShell.setLayout(fillLayout);
borderedBackground = new Composite(styledShell, SWT.NONE);
fillLayout = new FillLayout();
fillLayout.marginHeight = borderWidth;
fillLayout.marginWidth = borderWidth;
borderedBackground.setLayout(fillLayout);
content = new Composite(borderedBackground, SWT.NONE);
borderedBackground.addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
Rectangle bounds = borderedBackground.getClientArea();
int r = StyledShell.this.borderWidth;
int d = r * 2;
try {
e.gc.setAntialias(SWT.ON);
} catch (Throwable t) {
//Do nothing if it's not supported
}
/*
* Fills the four corners with the StyleShell background color so it blends in with the shell
*/
e.gc.setBackground(styledShell.getBackground());
e.gc.fillRectangle(0, 0, r, r);
e.gc.fillRectangle(bounds.width - r, 0, r, r);
e.gc.fillRectangle(bounds.width - r, bounds.height - r, r, r);
e.gc.fillRectangle(0, bounds.height - r, r, r);
/*
* Then paint in the rounded-corner rectangle
*/
e.gc.setBackground(content.getBackground());
/*
* Paint the 4 circles for the rounded corners; these circles will partially overlap
* on top of the four corners drawn above to give the look of a rounded corner
*/
e.gc.fillPolygon(circle(r, r, r));
e.gc.fillPolygon(circle(r, r, bounds.height - r));
e.gc.fillPolygon(circle(r, bounds.width - r, r));
e.gc.fillPolygon(circle(r, bounds.width - r, bounds.height - r));
/*
* Rectangle connecting between the top-left and top-right circles
*/
e.gc.fillRectangle(new Rectangle(r, 0, bounds.width - d, r));
/*
* Rectangle connecting between the bottom-left and bottom-right circles
*/
e.gc.fillRectangle(new Rectangle(r, bounds.height - r, bounds.width
- d, r));
/*
* Rectangle to fill the area between the 2 bars created above
*/
e.gc.fillRectangle(new Rectangle(0, r, bounds.width, bounds.height
- d));
}
});
Listener l = new Listener() {
int startX, startY;
public void handleEvent(Event e) {
if (e.type == SWT.KeyDown && e.character == SWT.ESC) {
styledShell.dispose();
}
if (e.type == SWT.MouseDown && e.button == 1) {
startX = e.x;
startY = e.y;
}
if (e.type == SWT.MouseMove && (e.stateMask & SWT.BUTTON1) != 0) {
Point p = styledShell.toDisplay(e.x, e.y);
p.x -= startX;
p.y -= startY;
styledShell.setLocation(p);
}
}
};
styledShell.addListener(SWT.KeyDown, l);
styledShell.addListener(SWT.MouseDown, l);
styledShell.addListener(SWT.MouseMove, l);
styledShell.setCursor(display.getSystemCursor(SWT.CURSOR_HAND));
}
/**
* Returns the bit mask for the proper shell style
* @param style
* @return
*/
private int getShellStyle(int style) {
/*
* If there are any other shell on top that also has a title then bring this shell on top of that
* so it is not obscured by the other shell(s); conversely DO NOT bring this shell on top if the
* above condition is false so that it will not obscure other windows like external browser, etc...
*/
if (true == Utils.anyShellHaveStyle(SWT.ON_TOP | SWT.TITLE)) {
UIFunctionsSWT uiFunctions = UIFunctionsManagerSWT.getUIFunctionsSWT();
if (uiFunctions != null && uiFunctions.getMainShell() != null) {
style |= SWT.ON_TOP;
}
}
/*
* On non-osx we must make this shell application modal so that it can not be hidden
* by the embedded media player
*
* At the same time we can not make it modal on OSX or else the screen positioning is all wrong;
* I'll find a fix for it later KN
*
* Additionally on non-osx set the NO_TRIM flag and on OSX ONLY set the NO_TRIM flag if setAlpha()
* is also supported. Versions of SWT on OSX that do not support setAlpha() also can not render
* the enmedded web page properly if the NO_TRIM flag is set; the NO_TRIM flag allows us to draw
* a round-cornered shell. Without this flag the shell corners would just be the normal square angle.
*/
if (true == Constants.isOSX) {
if (true == isAlphaSupported) {
style |= SWT.NO_TRIM;
}
} else {
style |= SWT.APPLICATION_MODAL;
style |= SWT.NO_TRIM;
}
return style;
}
private Region getRoundedRegion(Rectangle bounds) {
int r = borderWidth;
int d = r * 2;
Region region = new Region();
/*
* Add the 4 circles for the rounded corners
*/
region.add(circle(r, r, r));
region.add(circle(r, r, bounds.height - r));
region.add(circle(r, bounds.width - r, r));
region.add(circle(r, bounds.width - r, bounds.height - r));
/*
* Rectangle connecting between the top-left and top-right circles
*/
region.add(new Rectangle(r, 0, bounds.width - d, r));
/*
* Rectangle connecting between the bottom-left and bottom-right circles
*/
region.add(new Rectangle(r, bounds.height - r, bounds.width - d, r));
/*
* Rectangle to fill the area between the 2 bars created above
*/
region.add(new Rectangle(0, r, bounds.width, bounds.height - d));
return region;
}
private int[] circle(int r, int offsetX, int offsetY) {
int[] polygon = new int[8 * r + 4];
//x^2 + y^2 = r^2
for (int i = 0; i < 2 * r + 1; i++) {
int x = i - r;
int y = (int) Math.sqrt(r * r - x * x);
polygon[2 * i] = offsetX + x;
polygon[2 * i + 1] = offsetY + y;
polygon[8 * r - 2 * i - 2] = offsetX + x;
polygon[8 * r - 2 * i - 1] = offsetY - y;
}
return polygon;
}
public void addListener(int eventType, Listener listener) {
if (true == isAlive()) {
styledShell.addListener(eventType, listener);
}
}
private void open() {
if (true == isAlive()) {
styledShell.open();
isAlreadyOpened = true;
}
}
public void forceActive() {
if (true == isAlive()) {
styledShell.setVisible(true);
styledShell.forceActive();
}
}
public void pack() {
if (true == isAlive()) {
styledShell.pack();
}
}
public void pack(boolean changed) {
if (true == isAlive()) {
styledShell.pack(changed);
}
}
public void setSize(int width, int height) {
/*
* If the shell is opened already then, by default, resizing should not try to center the shell
*/
setSize(width, height, false == isAlreadyOpened);
}
public void setSize(int width, int height, boolean centersShell) {
if (true == isAlive()) {
Rectangle outerBounds = styledShell.getBounds();
/*
* Compensating since the 2 outer borders extends beyond the content area
*/
width += borderWidth * 4;
height += borderWidth * 4;
if (outerBounds.width != width || outerBounds.height != height) {
outerBounds.width = width;
outerBounds.height = height;
/*
* Centers the the StyleShell relative to the parent shell
*/
if (true == centersShell) {
Utils.centerRelativeTo(outerBounds, parentShell.getBounds());
}
/*
* Adjust the new bounds if the shell does not fully fit on the screen
*/
Utils.makeVisibleOnCursor(outerBounds);
styledShell.setRegion(getRoundedRegion(outerBounds));
styledShell.setBounds(outerBounds);
styledShell.forceActive();
}
}
}
public void animateFade(final int milliSeconds) {
if (false == isAlive() || true == isAnimating
|| false == isAlphaSupported) {
return;
}
Utils.execSWTThreadLater(0, new AERunnable() {
public void runSupport() {
if (!isAlive()) {
return;
}
isAnimating = true;
try {
int seconds = milliSeconds;
int currentAlpha = 0;
int delay = 3;
int sleepIncrement = milliSeconds / (10 + delay);
if (true == isAlive()) {
LightBoxShell.this.setAlpha(styledShell, currentAlpha);
styledShell.setVisible(true);
}
while (seconds > 0) {
Thread.sleep(sleepIncrement);
seconds -= (sleepIncrement);
if (true == isAlive()) {
/*
* We don't update the alpha for a few cycles to allow the shell to initialize it's content
* while still remaining invisible
*/
if (delay <= 0) {
LightBoxShell.this.setAlpha(styledShell, Math.min(
currentAlpha, alpha));
currentAlpha += 20;
}
delay--;
} else {
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (true == isAlive()) {
LightBoxShell.this.setAlpha(styledShell, alpha);
}
isAnimating = false;
styledShell.forceActive();
}
}
});
}
public void setVisible(boolean visible) {
if (true == isAlive()) {
styledShell.setVisible(visible);
}
}
public void removeListener(int eventType, Listener listener) {
if (true == isAlive()) {
styledShell.removeListener(eventType, listener);
}
}
public void setCursor(Cursor cursor) {
if (true == isAlive()) {
styledShell.setCursor(cursor);
}
if (null != lbShell && false == lbShell.isDisposed()) {
lbShell.setCursor(cursor);
}
}
public void setData(String key, Object value) {
if (true == isAlive()) {
styledShell.setData(key, value);
}
}
public boolean isAlive() {
if (null == styledShell || true == styledShell.isDisposed()) {
return false;
}
return true;
}
public Composite getContent() {
return content;
}
public Shell getShell() {
return styledShell;
}
public boolean isAlreadyOpened() {
return isAlreadyOpened;
}
public void setBackground(Color color) {
styledShell.setBackground(color);
}
public int getAlpha() {
return alpha;
}
public void setAlpha(int alpha) {
this.alpha = alpha;
}
public void hideShell(boolean value) {
if (true == value) {
LightBoxShell.this.setAlpha(styledShell, 0);
} else {
LightBoxShell.this.setAlpha(styledShell, alpha);
}
}
}
public Display getDisplay() {
return display;
}
public boolean isAlreadyOpened() {
return isAlreadyOpened;
}
}