/*******************************************************************************
* Copyright (c) 2014 Mentor Graphics and others.
* 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:
* Mentor Graphics - initial API and implementation
*******************************************************************************/
package com.codesourcery.internal.installer.ui;
import java.util.ArrayList;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Device;
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.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
/**
* A control that displays a sequence of steps.
* ---------------------------------
* | Step 1 > Step 2 > Step 3 > ... >
* ---------------------------------
*
* Use {@link #addStep(String)} to add steps to the control.
* Use {@link #setCurrentStep(String)} to set the current step.
*/
public class StepsControl extends Canvas {
/** Step colors */
private enum StepColor {
/** Foreground color of step before current step */
BEFORE_CURRENT_FOREGROUND,
/** Background color of step before current step */
BEFORE_CURRENT_BACKGROUND,
/** Foreground color of current step */
CURRENT_FOREGROUND,
/** Background color of current step */
CURRENT_BACKGROUND,
/** Foreground color of step after current step */
AFTER_CURRENT_FOREGROUND,
/** Background color of step after current step */
AFTER_CURRENT_BACKGROUND
};
/** Text drawing flags */
private final static int TEXT_FLAGS = SWT.DRAW_TRANSPARENT | SWT.DRAW_MNEMONIC;
/** Default leader width default */
private final static int LEADER_WIDTH_DEFAULT = 7;
/** Horizontal margin default */
private final static int HORIZONTAL_MARGIN_DEFAULT = 2;
/** Vertical margin default */
private final static int VERTICAL_MARGIN_DEFAULT = 2;
/** Text horizontal margin default */
private final static int TEXT_HORIZONTAL_MARGIN_DEFAULT = 4;
/** Text vertical margin default */
private final static int TEXT_VERTICAL_MARGIN_DEFAULT = 4;
/** Step spacing default */
private final static int STEP_SPACING_DEFAULT = 2;
/** Shadow offset */
private final static int SHADOW_OFFSET = 2;
/** <code>true</code> to double-buffer drawing */
private boolean doubleBuffer = true;
/** Current step */
private String currentStep;
/** Scroll offset */
private int scrollOffset = 0;
/** Leader width */
private int leaderWidth = LEADER_WIDTH_DEFAULT;
/** Horizontal margin */
private int horizontalMargin = HORIZONTAL_MARGIN_DEFAULT;
/** Vertical margin */
private int verticalMargin = VERTICAL_MARGIN_DEFAULT;
/** Text horizontal margin */
private int textHorizontalMargin = TEXT_HORIZONTAL_MARGIN_DEFAULT;
/** Text vertical margin */
private int textVerticalMargin = TEXT_VERTICAL_MARGIN_DEFAULT;
/** Step spacing */
private int stepSpacing = STEP_SPACING_DEFAULT;
/** <code>true</code> to show shadow */
private boolean showShadow = true;
/** Control colors */
private Color[] colors = new Color[StepColor.values().length];
/** Steps */
private ArrayList<String> steps = new ArrayList<String>();
/**
* Constructor
*
* @param parent Parent
* @param style Style flags
*/
public StepsControl(Composite parent, int style) {
super(parent, style & SWT.NO_BACKGROUND);
// Add paint listener
addPaintListener(new PaintListener() {
@Override
public void paintControl(PaintEvent e) {
onPaint(e);
}
});
// Add size listener
addControlListener(new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
// Recompute scroll offset
scrollOffset = -1;
redraw();
}
});
// Create control colors
createColors(getDisplay());
}
/**
* Adds a step. If the step has already been added, this method does
* nothing.
*
* @param stepName Step name
*/
public void addStep(String stepName) {
if (!steps.contains(stepName)) {
steps.add(stepName);
redraw();
}
}
/**
* Sets the current step.
* This method will ensure the current step is visible.
*
* @param currentStep Step to set current
*/
public void setCurrentStep(String currentStep) {
this.currentStep = currentStep;
/** Recompute scroll */
scrollOffset = -1;
redraw();
}
/**
* Returns the current step.
*
* @return Current step or <code>null</code>
*/
public String getCurrentStep() {
return currentStep;
}
/**
* Sets the control to use a buffer when painting to avoid flicker.
*
* @param doubleBuffer <code>true</code> to enable double-buffering
*/
public void setDoubleBuffered(boolean doubleBuffer) {
this.doubleBuffer = doubleBuffer;
}
/**
* Returns if the control will use a buffer to paint.
*
* @return <code>true</code> if double-buffering is enabled
*/
public boolean isDoubleBuffered() {
return doubleBuffer;
}
/**
* Sets the width of the step leader.
*
* @param leaderWidth Leader width
*/
public void setLeaderWidth(int leaderWidth) {
this.leaderWidth = leaderWidth;
}
/**
* Returns the leader width.
*
* @return Leader width
*/
public int getLeaderWidth() {
return leaderWidth;
}
/**
* Sets the horizontal margin.
*
* @param horizontalMargin Horizontal margin
*/
public void setHorizontalMargin(int horizontalMargin) {
this.horizontalMargin = horizontalMargin;
}
/**
* Returns the horizontal margin.
*
* @return Horizontal margin
*/
public int getHorizontalMargin() {
return horizontalMargin;
}
/**
* Sets the vertical margin.
*
* @param verticalMargin Vertical margin
*/
public void setVerticalMargin(int verticalMargin) {
this.verticalMargin = verticalMargin;
}
/**
* Returns the vertical margin.
*
* @return Vertical margin
*/
public int getVerticalMargin() {
return verticalMargin;
}
/**
* Sets the text horizontal margin.
*
* @param textHorizontalMargin Text horizontal margin
*/
public void setTextHorizontalMargin(int textHorizontalMargin) {
this.textHorizontalMargin = textHorizontalMargin;
}
/**
* Returns the text horizontal margin.
*
* @return Text horizontal margin
*/
public int getTextHorizontalMargin() {
return textHorizontalMargin;
}
/**
* Sets the text vertical margin.
*
* @param textVerticalMargin Text vertical margin
*/
public void setTextVerticalMargin(int textVerticalMargin) {
this.textVerticalMargin = textVerticalMargin;
}
/**
* Returns the text vertical margin.
*
* @return Text vertical margin
*/
public int getTextVerticalMargin() {
return textVerticalMargin;
}
/**
* Sets the step spacing.
*
* @param stepSpacing Step spacing
*/
public void setStepSpacing(int stepSpacing) {
this.stepSpacing = stepSpacing;
}
/**
* Returns the step spacing.
*
* @return Step spacing
*/
public int getStepSpacing() {
return stepSpacing;
}
/**
* Enables/disables drawing a shadow.
*
* @param showShadow <code>true</code> to enable show
*/
public void setShadowEnabled(boolean showShadow) {
this.showShadow = showShadow;
}
/**
* Returns if a shadown will be drawn.
*
* @return <code>true</code> if shadow will be drawn
*/
public boolean isShadowEnabled() {
return showShadow;
}
/**
* Creates control colors.
*
* @param device Device
*/
protected void createColors(Device device) {
// Colors of current step
colors[StepColor.CURRENT_BACKGROUND.ordinal()] =
new Color(device, getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION).getRGB());
colors[StepColor.CURRENT_FOREGROUND.ordinal()] =
new Color(device, getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT).getRGB());
// Colors of steps before current step
colors[StepColor.BEFORE_CURRENT_BACKGROUND.ordinal()] =
new Color(device, blendRGB(
getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION).getRGB(),
getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND).getRGB(),
60));
colors[StepColor.BEFORE_CURRENT_FOREGROUND.ordinal()] =
new Color(device, getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT).getRGB());
// Colors of steps after current step
colors[StepColor.AFTER_CURRENT_BACKGROUND.ordinal()] =
new Color(device, blendRGB(
getDisplay().getSystemColor(SWT.COLOR_LIST_FOREGROUND).getRGB(),
getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND).getRGB(),
5));
colors[StepColor.AFTER_CURRENT_FOREGROUND.ordinal()] =
new Color(device, blendRGB(
getDisplay().getSystemColor(SWT.COLOR_LIST_FOREGROUND).getRGB(),
getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND).getRGB(),
30));
}
/**
* Returns a color.
*
* @param color Color to return
* @return Color
*/
protected Color getColor(StepColor color) {
return colors[color.ordinal()];
}
/**
* Computes text size.
*
* @param text Text to compute
* @return Text size
*/
protected Point getTextSize(GC gc, String text) {
Point textSize;
if (text == null) {
textSize = new Point(0, 0);
}
else {
gc.setFont(getFont());
textSize = gc.textExtent(text, TEXT_FLAGS);
}
return textSize;
}
@Override
public Point computeSize(int wHint, int hHint, boolean changed)
{
return computeSize(wHint, hHint);
}
@Override
public Point computeSize(int wHint, int hHint)
{
if (wHint < 0) wHint = 0;
if (hHint < 0) hHint = 0;
GC gc = new GC(getShell());
gc.setFont(getFont());
int width = getHorizontalMargin() + getHorizontalMargin() + (isShadowEnabled() ? SHADOW_OFFSET : 0);
int height = getVerticalMargin() + getTextVerticalMargin() + gc.getFontMetrics().getHeight()
+ getTextVerticalMargin() + getVerticalMargin() + (isShadowEnabled() ? SHADOW_OFFSET : 0);
for (String step : steps) {
Point textSize = getTextSize(gc, step);
/* | | | | \ */
/* |text horizontal margin|text size|text horizontal margin|leader width> */
/* | | | | / */
width += getTextHorizontalMargin() + textSize.x + getTextHorizontalMargin() + getLeaderWidth();
}
gc.dispose();
int minWidth = Math.max(wHint, width);
int minHeight = Math.max(hHint, height);
return new Point(minWidth, minHeight);
}
/**
* Returns the extent of a step.
*
* @param gc Graphics context
* @param step Step to return extent for
* @return Text extent. The 'x' coordinate will contain the step offset.
* The 'y' coordinate will contain the step width.
*/
private Point getStepExtent(GC gc, String step) {
Point extent = null;
if (step != null) {
int offset = getHorizontalMargin();
for (String s : steps) {
int stepWidth = getTextHorizontalMargin() + getTextSize(gc, s).x + getTextHorizontalMargin() + getLeaderWidth();
if (s.equals(step)) {
extent = new Point(offset, stepWidth);
break;
}
else {
offset += stepWidth;
}
}
}
return extent;
}
/**
* Returns the next step.
*
* @param currentStep Step
* @return Step after <code>currentStep</code> or <code>null</code>
*/
private String getNextStep(String currentStep) {
String nextStep = null;
for (int index = 0; index < steps.size(); index ++) {
if (steps.get(index).equals(currentStep)) {
if (index + 1 < steps.size()) {
nextStep = steps.get(index + 1);
break;
}
}
}
return nextStep;
}
/**
* Called to paint the control.
*
* @param event Paint event
*/
private void onPaint(PaintEvent event) {
// Get the client area
Rectangle clientArea = getClientArea();
Image bufferImage = null;
GC gc;
// If double-buffering is enabled, create image for graphics context
if (isDoubleBuffered()) {
bufferImage = new Image(getDisplay(), clientArea.width, clientArea.height);
gc = new GC(bufferImage);
gc.setBackground(getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
gc.fillRectangle(clientArea);
}
// Else paint graphics context directly
else {
gc = event.gc;
}
// Set the font
gc.setFont(getFont());
// Get height of text
int textHeight = gc.getFontMetrics().getHeight();
// Use advanced graphics if available
gc.setAdvanced(true);
gc.setAntialias(SWT.ON);
// If advanced graphics available, draw panel shadow
if (gc.getAdvanced() && isShadowEnabled()) {
gc.setBackground(getDisplay().getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW));
gc.setAlpha(40);
for (int offset = SHADOW_OFFSET; offset > 0; offset--) {
gc.fillRoundRectangle(
SHADOW_OFFSET + offset,
SHADOW_OFFSET + offset,
clientArea.width - SHADOW_OFFSET - offset - offset,
clientArea.height - SHADOW_OFFSET - offset - offset,
3, 3);
}
gc.setAlpha(0xFF);
// Adjust client area to exclude shadow
clientArea.width -= SHADOW_OFFSET;
clientArea.height -= SHADOW_OFFSET;
}
// Does scroll offset require computing?
if (scrollOffset == -1) {
// Compute extent of current step
Point stepExtent = getStepExtent(gc, getCurrentStep());
// No scroll offset
if (stepExtent == null) {
scrollOffset = 0;
}
else {
// Current step extent off client area
if (stepExtent.x + stepExtent.y > clientArea.width) {
// If there is a next step, scroll so it is half visible
String nextStep = getNextStep(getCurrentStep());
if (nextStep != null) {
Point nextExtent = getStepExtent(gc, nextStep);
scrollOffset = clientArea.width - nextExtent.x - (nextExtent.y / 2);
}
// Otherwise scroll so entire current step is visible
else {
scrollOffset = clientArea.width - stepExtent.x - stepExtent.y;
}
}
}
}
// Draw panel
gc.setBackground(getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
gc.fillRoundRectangle(0, 0, clientArea.width, clientArea.height, 3, 3);
// Adjust clipping
gc.setClipping(clientArea.x + getHorizontalMargin(), clientArea.y + getVerticalMargin(), clientArea.width - getHorizontalMargin() - getHorizontalMargin(), clientArea.height - getVerticalMargin() - getVerticalMargin());
// Compute widths of steps text
int[] textWidths = new int[steps.size()];
for (int index = 0; index < steps.size(); index ++) {
String step = steps.get(index);
Point textSize = gc.textExtent(step, TEXT_FLAGS);
textWidths[index] = textSize.x;
}
boolean afterCurrentStep = false;
// Drawing off starts past horizontal margin, adjusted for scrolling
int offset = getHorizontalMargin() + scrollOffset;
// Draw steps
for (int index = 0; index < steps.size(); index++) {
String step = steps.get(index);
Color stepForeground;
Color stepBackground;
// Current step color
if (step.equals(getCurrentStep())) {
stepForeground = getColor(StepColor.CURRENT_FOREGROUND);
stepBackground = getColor(StepColor.CURRENT_BACKGROUND);
afterCurrentStep = true;
}
// After current step color
else if (afterCurrentStep) {
stepForeground = getColor(StepColor.AFTER_CURRENT_FOREGROUND);
stepBackground = getColor(StepColor.AFTER_CURRENT_BACKGROUND);
}
// Before current step color
else {
stepForeground = getColor(StepColor.BEFORE_CURRENT_FOREGROUND);
stepBackground = getColor(StepColor.BEFORE_CURRENT_BACKGROUND);
}
gc.setBackground(stepBackground);
gc.setForeground(stepBackground);
int stepWidth = getTextHorizontalMargin() + textWidths[index] + getTextHorizontalMargin();
int stepHeight = getTextVerticalMargin() + textHeight + getTextVerticalMargin();
int[] stepPoints;
// First step in sequence
if (index == 0) {
stepPoints = new int[] {
offset, getVerticalMargin(),
offset + stepWidth, getVerticalMargin(),
offset + stepWidth + getLeaderWidth(), getVerticalMargin() + getTextVerticalMargin() + textHeight / 2,
offset + stepWidth, getVerticalMargin() + stepHeight,
offset, getVerticalMargin() + stepHeight
};
}
// Last step in sequence
else if (index == steps.size() - 1) {
stepPoints = new int[] {
offset - getLeaderWidth() + getStepSpacing(), getVerticalMargin(),
clientArea.width - getHorizontalMargin(), getVerticalMargin(),
clientArea.width - getHorizontalMargin(), getVerticalMargin() + stepHeight,
offset - getLeaderWidth() + getStepSpacing(), getVerticalMargin() + stepHeight,
offset + getStepSpacing(), getVerticalMargin() + getTextVerticalMargin() + textHeight / 2
};
}
// Step in sequence
else {
stepPoints = new int[] {
offset - getLeaderWidth() + getStepSpacing(), getVerticalMargin(),
offset + stepWidth, getVerticalMargin(),
offset + stepWidth + getLeaderWidth(), getVerticalMargin() + getTextVerticalMargin() + textHeight / 2,
offset + stepWidth, getVerticalMargin() + stepHeight,
offset - getLeaderWidth() + getStepSpacing(), getVerticalMargin() + stepHeight,
offset + getStepSpacing(), getVerticalMargin() + getTextVerticalMargin() + textHeight / 2
};
}
gc.fillPolygon(stepPoints);
gc.setForeground(stepForeground);
gc.drawText(step, offset + getTextHorizontalMargin(), clientArea.height / 2 - textHeight / 2, TEXT_FLAGS);
offset += stepWidth + getLeaderWidth();
}
if (bufferImage != null) {
event.gc.drawImage(bufferImage, 0, 0);
bufferImage.dispose();
}
}
/**
* Blends two RGB values using the provided ratio.
*
* @param c1 First RGB value
* @param c2 Second RGB value
* @param ratio Percentage of the first RGB to blend with
* second RGB (0-100)
*
* @return The RGB value of the blended color
*/
public static RGB blendRGB(RGB c1, RGB c2, int ratio) {
ratio = Math.max(0, Math.min(255, ratio));
int r = Math.max(0, Math.min(255, (ratio * c1.red + (100 - ratio) * c2.red) / 100));
int g = Math.max(0, Math.min(255, (ratio * c1.green + (100 - ratio) * c2.green) / 100));
int b = Math.max(0, Math.min(255, (ratio * c1.blue + (100 - ratio) * c2.blue) / 100));
return new RGB(r, g, b);
}
@Override
public void dispose() {
// Dispose of colors
for (Color color : colors) {
color.dispose();
colors = null;
}
super.dispose();
}
}