/******************************************************************************* * Copyright (c) 2011 Laurent CARON * 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: * Laurent CARON (laurent.caron at gmail dot com) - initial API and implementation *******************************************************************************/ package org.mihalis.opal.infinitePanel; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.mihalis.opal.utils.SWTGraphicUtil; /** * Instances of this class are controls located on the top of a shell. They * display a ticker that indicates to the user that a long task operation is * running. The design is inspired by Romain Guy's work * (http://www.curious-creature.org) */ public class InfiniteProgressPanel { private static final String INFINITE_PANEL_KEY = "org.mihalis.opal.InfinitePanel.InfiniteProgressPanel"; private static final int NUMBER_OF_STEPS = 10; private final Shell parent; private Shell shellHover; private String text; private Font textFont; private Color textColor; private float fps; private int barsCount; private int lineWidth; private int alpha; private Color defaultColor; private Color selectionColor; private int currentPosition; private Thread animatorThread; private Canvas canvas; private boolean fadeIn; private boolean fadeOut; private int fadeOutCounter; /** * Constructs a new instance of this class given its parent. * * @param shell a shell that will be the parent of the new instance (cannot * be null) * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the parent is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_WIDGET_DISPOSED - if the parent has been * disposed</li> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * thread that created the parent</li> * </ul> */ private InfiniteProgressPanel(final Shell shell) { if (shell == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } if (shell.isDisposed()) { SWT.error(SWT.ERROR_INVALID_ARGUMENT); } parent = shell; if (shell.getData(INFINITE_PANEL_KEY) != null) { throw new IllegalArgumentException("This shell has already an infinite panel attached on it !"); } text = null; textFont = null; barsCount = 14; fps = 15.0f; lineWidth = 16; alpha = 200; fadeIn = false; fadeOut = false; fadeOutCounter = 0; shell.setData(INFINITE_PANEL_KEY, this); parent.addListener(SWT.Activate, new Listener() { @Override public void handleEvent(final Event e) { if (shellHover != null && // !shellHover.isDisposed() && !shellHover.isVisible()) { shellHover.setVisible(true); shellHover.setActive(); } } }); } /** * Starts the ticker */ public void start() { if (parent.isDisposed()) { SWT.error(SWT.ERROR_WIDGET_DISPOSED); } currentPosition = 0; fadeIn = true; fadeOut = false; fadeOutCounter = 0; if (defaultColor == null) { defaultColor = SWTGraphicUtil.getDefaultColor(parent, 200, 200, 200); } if (selectionColor == null) { selectionColor = parent.getDisplay().getSystemColor(SWT.COLOR_BLACK); } createShell(); createAndRunAnimatorThread(); } private void createShell() { shellHover = new Shell(parent, SWT.APPLICATION_MODAL | SWT.NO_TRIM | SWT.ON_TOP); shellHover.setLayout(new FillLayout()); shellHover.setAlpha(0); shellHover.addListener(SWT.KeyUp, new Listener() { @Override public void handleEvent(final Event event) { event.doit = false; } }); shellHover.addListener(SWT.Deactivate, new Listener() { @Override public void handleEvent(final Event arg0) { shellHover.setVisible(false); } }); shellHover.setBounds(shellHover.getDisplay().map(parent, null, parent.getClientArea())); canvas = new Canvas(shellHover, SWT.NO_BACKGROUND | SWT.DOUBLE_BUFFERED); canvas.addPaintListener(new PaintListener() { @Override public void paintControl(final PaintEvent e) { InfiniteProgressPanel.this.paintCanvas(e); } }); shellHover.open(); } private void createAndRunAnimatorThread() { animatorThread = new Thread() { /** * @see java.lang.Thread#run() */ @Override public void run() { while (!Thread.interrupted()) { currentPosition = (currentPosition + 1) % barsCount; if (fadeOut) { fadeOutCounter++; } shellHover.getDisplay().asyncExec(new Runnable() { @Override public void run() { canvas.redraw(); } }); try { sleep(fadeOut ? 20 : (long) (1000 / fps)); } catch (final InterruptedException e) { break; } } } }; animatorThread.start(); } /** * Paint the canvas that holds the ticker * * @param e */ private void paintCanvas(final PaintEvent e) { // Paint the panel final Rectangle clientArea = ((Canvas) e.widget).getClientArea(); final GC gc = e.gc; handleFadeIn(); handleFadeOut(); drawBackground(clientArea, gc); drawTicker(clientArea, gc); drawText(clientArea, gc); } /** * Handle the fade in effect of the hover shell */ private void handleFadeIn() { if (fadeIn) { if (currentPosition == NUMBER_OF_STEPS) { fadeIn = false; shellHover.setAlpha(alpha); } else { shellHover.setAlpha(currentPosition * alpha / NUMBER_OF_STEPS); } } } /** * Handle the fade out effect of the hover shell */ private void handleFadeOut() { if (fadeOut) { if (fadeOutCounter >= NUMBER_OF_STEPS) { if (animatorThread != null) { animatorThread.interrupt(); animatorThread = null; } if (!shellHover.isDisposed()) { shellHover.getDisplay().asyncExec(new Runnable() { @Override public void run() { if (!shellHover.isDisposed()) { shellHover.dispose(); } } }); } } shellHover.setAlpha(255 - fadeOutCounter * alpha / NUMBER_OF_STEPS); } } /** * Draw the background of the panel * * @param gc GC on with the background is drawn * @param clientArea client area of the canvas */ private void drawBackground(final Rectangle clientArea, final GC gc) { gc.setBackground(shellHover.getDisplay().getSystemColor(SWT.COLOR_WHITE)); gc.fillRectangle(clientArea); } /** * Draw the ticker * * @param gc GC on with the ticker is drawn * @param clientArea client area of the canvas */ private void drawTicker(final Rectangle clientArea, final GC gc) { final int centerX = clientArea.width / 2; final int centerY = clientArea.height / 2; final int maxRay = (int) (Math.min(clientArea.width, clientArea.height) * 0.6f) / 2; final int minRay = (int) (maxRay * 0.5f); double angle = Math.PI / 2; gc.setLineCap(SWT.CAP_ROUND); gc.setLineWidth(lineWidth); gc.setAntialias(SWT.ON); final double angleStep = 2 * Math.PI / barsCount; for (int i = 0; i < barsCount; i++) { if (i == currentPosition) { gc.setForeground(selectionColor); } else { gc.setForeground(defaultColor); } gc.drawLine((int) (centerX + minRay * Math.cos(angle)), // (int) (centerY - minRay * Math.sin(angle)), // (int) (centerX + maxRay * Math.cos(angle)), // (int) (centerY - maxRay * Math.sin(angle))); angle -= angleStep; } } /** * Draw the text over the ticker * * @param gc GC on with the text is drawn * @param clientArea client area of the canvas */ private void drawText(final Rectangle clientArea, final GC gc) { if (text == null || "".equals(text)) { return; } final Font font; if (textFont == null) { font = parent.getDisplay().getSystemFont(); } else { font = textFont; } final Color color; if (textColor == null) { color = parent.getDisplay().getSystemColor(SWT.COLOR_BLACK); } else { color = textColor; } gc.setForeground(color); gc.setFont(font); gc.setTextAntialias(SWT.ON); final Point textSize = gc.textExtent(text, SWT.DRAW_TRANSPARENT); final int textWidth = textSize.x; final int textHeight = textSize.y; gc.drawString(text, (clientArea.width - textWidth) / 2, (clientArea.height - textHeight) / 2, true); } /** * Stop the animation and dispose the panel */ public void stop() { if (shellHover.isDisposed() || shellHover.getDisplay().isDisposed()) { return; } fadeOut = true; } /** * Returns the infinite progress panel for the shell. If no infinite panel * has been declared, returns null. * * @param shell the shell for which we are trying to get the associated * progess panel * @return the progress panel associated to shell, or null if there is no * progress panel */ public static InfiniteProgressPanel getInfiniteProgressPanelFor(final Shell shell) { if (shell == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } if (shell.isDisposed()) { SWT.error(SWT.ERROR_WIDGET_DISPOSED); } if (shell.getDisplay().isDisposed()) { SWT.error(SWT.ERROR_DEVICE_DISPOSED); } final InfiniteProgressPanel[] temp = new InfiniteProgressPanel[1]; shell.getDisplay().syncExec(new Runnable() { @Override public void run() { final Object data = shell.getData(INFINITE_PANEL_KEY); if (data != null && data instanceof InfiniteProgressPanel) { temp[0] = (InfiniteProgressPanel) data; } } }); if (temp[0] == null) { return new InfiniteProgressPanel(shell); } else { return temp[0]; } } /** * Check if a shell has an associated progress panel * * @param shell the shell * @return <code>true</code> if the shell has an associated panel, * <code>false</code> otherwise */ public static boolean hasInfiniteProgressPanel(final Shell shell) { return getInfiniteProgressPanelFor(shell) != null; } // ------------------------------------------------- Getters and Setters /** * @return the alpha value of the panel */ public int getAlpha() { return alpha; } /** * @param alpha the alpha value of the panel, between 0 and 255 * * @exception IllegalArgumentException * <ul> * <li>ERROR_INVALID_ARGUMENT - if the animation is running * </li> * </ul> */ public void setAlpha(final int alpha) { if (alpha < 0 || alpha > 255) { SWT.error(SWT.ERROR_INVALID_ARGUMENT); } checkIfAnimationIsRunning(); this.alpha = alpha; } /** * Check if the animation is running */ private void checkIfAnimationIsRunning() { if (animatorThread != null) { SWT.error(SWT.ERROR_INVALID_ARGUMENT, null, "Can not change this value when an animation is running"); } } /** * @return the number of bars displayed in the ticker */ public int getBarsCount() { return barsCount; } /** * @param barsCount the number of bars displayed in the ticker * * @exception IllegalArgumentException * <ul> * <li>ERROR_INVALID_ARGUMENT - if the animation is running * </li> * </ul> */ public void setBarsCount(final int barsCount) { checkIfAnimationIsRunning(); this.barsCount = barsCount; } /** * @return the default color for the ticker's bars */ public Color getDefaultColor() { return defaultColor; } /** * @param defaultColor the new default color for the ticker's bars. Please * notice that the previous color is disposed. * * @exception IllegalArgumentException * <ul> * <li>ERROR_INVALID_ARGUMENT - if the animation is running * </li> * </ul> */ public void setDefaultColor(final Color defaultColor) { checkIfAnimationIsRunning(); SWTGraphicUtil.safeDispose(this.defaultColor); this.defaultColor = defaultColor; } /** * @return the number of frame per second for the animation */ public float getFps() { return fps; } /** * @param fps the new frame per second value * * @exception IllegalArgumentException * <ul> * <li>ERROR_INVALID_ARGUMENT - if the animation is running * </li> * </ul> */ public void setFps(final float fps) { checkIfAnimationIsRunning(); this.fps = fps; } /** * @return the line width of the bars that compose the ticker */ public int getLineWidth() { return lineWidth; } /** * @param lineWidth the line width of the bars that compose the ticker * * @exception IllegalArgumentException * <ul> * <li>ERROR_INVALID_ARGUMENT - if the animation is running * </li> * </ul> */ public void setLineWidth(final int lineWidth) { checkIfAnimationIsRunning(); this.lineWidth = lineWidth; } /** * @return the selection color of the ticker's bars */ public Color getSelectionColor() { return selectionColor; } /** * @param selectionColor the new selection color for the ticker's bars. * Please notice that the previous color is disposed. * * @exception IllegalArgumentException * <ul> * <li>ERROR_INVALID_ARGUMENT - if the animation is running * </li> * </ul> */ public void setSelectionColor(final Color selectionColor) { checkIfAnimationIsRunning(); this.selectionColor = selectionColor; } /** * @return the displayed text */ public String getText() { return text; } /** * @param text set the text to display * * @exception IllegalArgumentException * <ul> * <li>ERROR_INVALID_ARGUMENT - if the animation is running * </li> * </ul> */ public void setText(final String text) { checkIfAnimationIsRunning(); this.text = text; } /** * @return the text color */ public Color getTextColor() { return textColor; } /** * @param textColor the text color. Please notice that the previous color is * disposed. * * @exception IllegalArgumentException * <ul> * <li>ERROR_INVALID_ARGUMENT - if the animation is running * </li> * </ul> */ public void setTextColor(final Color textColor) { checkIfAnimationIsRunning(); this.textColor = textColor; } /** * @return the text font */ public Font getTextFont() { return textFont; } /** * @param textFont the new text font. Please notice that the previous font * set is disposed. * * @exception IllegalArgumentException * <ul> * <li>ERROR_INVALID_ARGUMENT - if the animation is running * </li> * </ul> */ public void setTextFont(final Font textFont) { checkIfAnimationIsRunning(); this.textFont = textFont; } }