package org.csstudio.ui.util.widgets; import java.text.NumberFormat; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; 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.Path; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; /** * Simple meter widget. * * @author Kay Kasemir * @author Gabriele Carcassi */ public class MeterWidget extends Canvas { /** Line width for scale outline (rest uses width 1) */ final private static int LINE_WIDTH = 5; /** Number of labels (and ticks) */ final private static int LABEL_COUNT = 5; final private static int startAngle = 140; final private static int endAngle = 40; final private static double scaleWidth = 0.35; final private Color backgroundColor= new Color(null, 255, 255, 255); final private Color faceColor = new Color(null, 20, 10, 10); final private Color needleColor = new Color(null, 20, 0, 200); final private Color okColor = new Color(null, 0, 200, 0); final private Color warningColor = new Color(null, 200, 200, 0); final private Color alarmColor = new Color(null, 250, 0, 0); /** Minimum value. */ private double min = -10.0; /** Lower alarm limit. */ private double lowAlarm = -5.0; /** Lower warning limit. */ private double lowWarning = -4.0; /** Upper warning limit. */ private double highWarning = 4.0; /** Upper alarm limit. */ private double highAlarm = 5.0; /** Maximum value. */ private double max = +10.0; /** Display precision. */ private int precision = 4; /** Current value. */ private double value = 1.0; /** Most recent scale image or <code>null</code>. */ private Image scaleImage; /** ClientRect for which the image was created. */ private Rectangle old_client_rect = new Rectangle(0, 0, 0, 0); /** X-coord of needle pivot point */ private int pivot_x; /** Y-coord of needle pivot point */ private int pivot_y; /** X-Radius of scale */ private int x_radius; /** Y-Radius of scale */ private int y_radius; /** Constructor */ public MeterWidget(final Composite parent, final int style) { // To reduce flicker, don't clear the background. // On Linux, however, that seems to corrupt the overall // widget layout, so we don't use that option. // super(parent, style | SWT.NO_BACKGROUND); super(parent, SWT.NO_BACKGROUND); addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { invalidateScale(); alarmColor.dispose(); warningColor.dispose(); okColor.dispose(); needleColor.dispose(); faceColor.dispose(); backgroundColor.dispose(); } }); addPaintListener(paintListener); } /** * Configure the meter. * * @param min Minimum value. * @param lowAlarm Lower alarm limit. * @param lowWarning Lower warning limit. * @param highWarning Upper warning limit. * @param highAlarm Upper alarm limit. * @param max Maximum value. * @param precision Display precision */ public void setLimits(final double min, final double lowAlarm, final double lowWarning, final double highWarning, final double highAlarm, final double max, final int precision) { if (this.min == min && this.lowAlarm == lowAlarm && this.lowWarning == lowWarning && this.highWarning == highWarning && this.highAlarm == highAlarm && this.max == max && this.precision == precision) { return; } if (min > max) { // swap this.min = min; this.max = max; } else if (min == max) { // Some fake default range this.min = min; this.max = min + 10.0; } else { // Set as given this.min = min; this.max = max; } // Check for limits that are outside the value range // or NaN (since EPICS R3.14.11) if (lowAlarm > this.min && lowAlarm < this.max) this.lowAlarm = lowAlarm; else this.lowAlarm = this.min; if (lowWarning > this.min && lowWarning < this.max) this.lowWarning = lowWarning; else this.lowWarning = this.lowAlarm; if (highAlarm > this.min && highAlarm < this.max) this.highAlarm = highAlarm; else this.highAlarm = this.max; if (highWarning > this.min && highWarning < this.max) this.highWarning = highWarning; else this.highWarning = this.highAlarm; this.precision = precision; invalidateScale(); redraw(); } /** Set current value. */ public void setValue(final double value) { if (this.value == value) return; this.value = value; if (!isDisposed()) { redraw(); } } @Override public void setEnabled(boolean enabled) { // When the widget is disabled, force the redraw. boolean oldEnabled = getEnabled(); super.setEnabled(enabled); if (oldEnabled != enabled) { invalidateScale(); redraw(); } } /** Reset the scale. * <p> * Clears the scale image, so it will be re-computed on redraw. */ private void invalidateScale() { if (scaleImage != null) { scaleImage.dispose(); scaleImage = null; } } /** @see org.eclipse.swt.widgets.Composite#computeSize(int, int, boolean) */ @Override public Point computeSize(final int wHint, final int hHint, final boolean changed) { int width, height; height = 100; width = 100; if (wHint != SWT.DEFAULT) { width = wHint; } if (hHint != SWT.DEFAULT) { height = hHint; } return new Point(width, height); } /** @return Angle in degrees for given value on scale. */ private double getAngle(final double value) { if (value <= min) { return startAngle; } if (value >= max) { return endAngle; } return endAngle + (startAngle - endAngle) * (max-value) / (max-min); } private PaintListener paintListener = new PaintListener() { @Override public void paintControl(PaintEvent e) { //long start = System.nanoTime(); final GC gc = e.gc; // Get the rectangle that exactly fills the 'inner' area // such that drawRectangle() will match. Rectangle displayArea = getClientArea(); //paintScale(client_rect, gc); // Background and border gc.setForeground(faceColor); gc.setBackground(backgroundColor); gc.setLineWidth(LINE_WIDTH); gc.setLineCap(SWT.CAP_ROUND); gc.setLineJoin(SWT.JOIN_ROUND); // To reduce flicker, the scale is drawn as a prepared image into // the widget whose background has not been cleared. createScaleImage(gc, displayArea); if (getEnabled()) { gc.drawImage(scaleImage, 0, 0); paintNeedle(gc); } else { // Not enabled final Image grayed = new Image(gc.getDevice(), scaleImage, SWT.IMAGE_DISABLE); gc.drawImage(grayed, 0, 0); grayed.dispose(); final String message = "No numeric display info"; final Point size = gc.textExtent(message); gc.drawString(message, (displayArea.width-size.x)/2, (displayArea.height-size.y)/2, true); } //System.out.println("MeterWidget paint: " + (System.nanoTime() - start)); } }; private void paintNeedle(final GC gc) { gc.setLineWidth(LINE_WIDTH); gc.setLineCap(SWT.CAP_ROUND); gc.setLineJoin(SWT.JOIN_ROUND); final double needle_angle = getAngle(value); final int needle_x_radius = (int)((1 - 0.5*scaleWidth)*x_radius); final int needle_y_radius = (int)((1 - 0.5*scaleWidth)*y_radius); gc.setForeground(needleColor); gc.drawLine(pivot_x, pivot_y, (int)(pivot_x + needle_x_radius*Math.cos(Math.toRadians(needle_angle))), (int)(pivot_y - needle_y_radius*Math.sin(Math.toRadians(needle_angle)))); } /** Create image of the scale (labels etc.) _if_needed_ */ private void createScaleImage(final GC gc, final Rectangle client_rect) { // Is there already a matching image? if ((scaleImage != null) && old_client_rect.equals(client_rect)) { return; } // Remember the client rect for the next call: old_client_rect = client_rect; // The area that one can use with drawRectangle() // is actually one pixel smaller... final Rectangle real_client_rect = new Rectangle(client_rect.x, client_rect.y, client_rect.width-1, client_rect.height-1); // Create image buffer, prepare GC for it. // In case there's old one, delete it. if (scaleImage != null) { scaleImage.dispose(); } scaleImage = new Image(gc.getDevice(), client_rect); final GC scale_gc = new GC(scaleImage); paintScale(real_client_rect, scale_gc); scale_gc.dispose(); } private void paintScale(final Rectangle displayArea, final GC scale_gc) { scale_gc.setForeground(faceColor); scale_gc.setBackground(backgroundColor); scale_gc.setLineWidth(LINE_WIDTH); scale_gc.setLineCap(SWT.CAP_ROUND); scale_gc.setLineJoin(SWT.JOIN_ROUND); // Background, border scale_gc.fillRectangle(displayArea); scale_gc.drawRectangle(displayArea); // Calculate meter area and center it. double ratio = 1.654; int meterWidth = (int) Math.min(displayArea.width, displayArea.height * ratio); int meterHeight = (int) Math.min(displayArea.height, displayArea.width / ratio); int meterX = (displayArea.width - meterWidth) / 2; int meterY = (displayArea.height - meterHeight) / 2; Rectangle meterArea = new Rectangle(meterX, meterY, meterWidth, meterHeight); pivot_x = meterArea.x + meterArea.width / 2; pivot_y = meterArea.y + meterArea.height; final NumberFormat fmt = NumberFormat.getNumberInstance(); fmt.setMaximumFractionDigits(precision); final Point min_size = scale_gc.textExtent(fmt.format(min)); final Point max_size = scale_gc.textExtent(fmt.format(max)); final int text_width_idea = Math.max(min_size.x/2, max_size.x/2); final int text_height_idea = min_size.y; // Labels should somehow fit around the outside of the scale final int tick_x_radius = meterArea.width/2 - text_width_idea; final int tick_y_radius = meterArea.height - 2*text_height_idea; y_radius = tick_y_radius - text_height_idea; x_radius = tick_x_radius * y_radius/tick_y_radius; // Inner radius of scale. final int x_radius2 = (int)((1-scaleWidth)*x_radius); final int y_radius2 = (int)((1-scaleWidth)*y_radius); // Lower end of ticks. final int tick_x_radius2 = (int)(0.6*x_radius2); final int tick_y_radius2 = (int)(0.6*y_radius2); // Path for outline of scale final Path scale_path = createSectionPath(scale_gc.getDevice(), pivot_x, pivot_y, x_radius, y_radius, x_radius2, y_radius2, startAngle, endAngle); // Fill scale with 'ok' color scale_gc.setBackground(okColor); scale_gc.fillPath(scale_path); // Border around the scale drawn later... // Colored alarm sections final int high_alarm_start = (int) getAngle(highAlarm); final Path high_alarm_path = createSectionPath(scale_gc.getDevice(), pivot_x, pivot_y, x_radius, y_radius, x_radius2, y_radius2, high_alarm_start, endAngle); scale_gc.setBackground(alarmColor); scale_gc.fillPath(high_alarm_path); high_alarm_path.dispose(); final int low_alarm_end = (int) getAngle(lowAlarm); final Path low_alarm_path = createSectionPath(scale_gc.getDevice(), pivot_x, pivot_y, x_radius, y_radius, x_radius2, y_radius2, startAngle, low_alarm_end); scale_gc.fillPath(low_alarm_path); low_alarm_path.dispose(); // Warning sections final int high_warning_start = (int) getAngle(highWarning); final Path high_warning_path = createSectionPath(scale_gc.getDevice(), pivot_x, pivot_y, x_radius, y_radius, x_radius2, y_radius2, high_warning_start, high_alarm_start); scale_gc.setBackground(warningColor); scale_gc.fillPath(high_warning_path); high_warning_path.dispose(); final int low_warning_end = (int) getAngle(lowWarning); final Path low_warning_path = createSectionPath(scale_gc.getDevice(), pivot_x, pivot_y, x_radius, y_radius, x_radius2, y_radius2, low_alarm_end, low_warning_end); scale_gc.fillPath(low_warning_path); low_warning_path.dispose(); // Scale outline scale_gc.drawPath(scale_path); scale_path.dispose(); // Labels and tick marks scale_gc.setLineWidth(1); for (int i=0; i<LABEL_COUNT; ++i) { final double label_value = min+(max-min)*i/(LABEL_COUNT-1); final double angle = getAngle(label_value); final double cos_angle = Math.cos(Math.toRadians(angle)); final double sin_angle = Math.sin(Math.toRadians(angle)); scale_gc.drawLine( (int)(pivot_x + tick_x_radius2*cos_angle), (int)(pivot_y - tick_y_radius2*sin_angle), (int)(pivot_x + tick_x_radius*cos_angle), (int)(pivot_y - tick_y_radius*sin_angle)); final String label_text = fmt.format(label_value); final Point size = scale_gc.textExtent(label_text); // Don't print the numbers if disabled if (getEnabled()) { scale_gc.drawString(label_text, (int)(pivot_x + tick_x_radius*cos_angle)-size.x/2, (int)(pivot_y - tick_y_radius*sin_angle)-size.y, true); } } } /** Create path for a section of the colored scale. * @param device Device * @param x0 Center of arcs * @param y0 Center of arcs * @param x_radius Outer arc radius * @param y_radius Outer arc radius * @param x_radius2 Inner arc radius * @param y_radius2 Inner arc radius * @param start_angle degrees * @param end_angle degrees * @return */ private Path createSectionPath(final Device device, final int x0, final int y0, final int x_radius, final int y_radius, final int x_radius2, final int y_radius2, final int start_angle, final int end_angle) { final Path path = new Path(device); // Right edge path.moveTo((float)(x0 + x_radius2*Math.cos(Math.toRadians(end_angle))), (float)(y0 - y_radius2*Math.sin(Math.toRadians(end_angle)))); // Upper edge path.addArc(x0 - x_radius, y0 - y_radius, 2*x_radius, 2*y_radius, end_angle, start_angle-end_angle); // Left edge path.lineTo((float)(x0 + x_radius2*Math.cos(Math.toRadians(start_angle))), (float)(y0 - y_radius2*Math.sin(Math.toRadians(start_angle)))); addClockwiseArc(path, x0, y0, x_radius2, y_radius2, start_angle, end_angle); path.close(); return path; } /** Add a clockwise arc from start to end angle to path. * @param path * @param x0 Center of arc * @param y0 Center of arc * @param x_radius X radius of arc * @param y_radius Y radius of arc * @param start_angle start degrees (0=east, 90=north) * @param end_angle end degrees */ private void addClockwiseArc(final Path path, final int x0, final int y0, final int x_radius, final int y_radius, final float start_angle, final float end_angle) { // TODO Would like to draw arc back, i.e. go clockwise, // but SWT didn't do that on all platforms, so we draw the arc ourselves. // Linux: OK // OS X : Rendering errors // if (false) // path.addArc(x0 - x_radius, y0 - y_radius, // 2*x_radius, 2*y_radius, // start_angle, end_angle-start_angle); // else // { final double d_rad = Math.toRadians(5); final double start_rad = Math.toRadians(start_angle); final double end_rad = Math.toRadians(end_angle); double rad=start_rad; while (rad >= end_rad) { path.lineTo((float)(x0 + x_radius*Math.cos(rad)), (float)(y0 - y_radius*Math.sin(rad))); rad -= d_rad; } path.lineTo((float)(x0 + x_radius*Math.cos(end_rad)), (float)(y0 - y_radius*Math.sin(end_rad))); // } } }