Author: rwhitcomb
Date: Fri Feb 23 21:35:57 2018
New Revision: 1825172

URL: http://svn.apache.org/viewvc?rev=1825172&view=rev
Log:
PIVOT-1031: Pretty much complete implementation of the "Gauge" component,
along with tests of the listeners, and a sample application.
Note: there are still a couple of TODO: items in the skin, which may be
resolved in a future submission, but for now this is pretty complete.

Added:
    pivot/trunk/tests/src/org/apache/pivot/tests/GaugeTest.java
    pivot/trunk/tests/src/org/apache/pivot/tests/gauge_test.bxml
    
pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraGaugeSkin.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/Gauge.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/GaugeListener.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/Origin.java
    pivot/trunk/wtk/test/org/apache/pivot/wtk/test/GaugeTest.java
Modified:
    pivot/trunk/core/src/org/apache/pivot/util/StringUtils.java
    pivot/trunk/core/src/org/apache/pivot/util/Utils.java
    pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraTheme.java

Modified: pivot/trunk/core/src/org/apache/pivot/util/StringUtils.java
URL: 
http://svn.apache.org/viewvc/pivot/trunk/core/src/org/apache/pivot/util/StringUtils.java?rev=1825172&r1=1825171&r2=1825172&view=diff
==============================================================================
--- pivot/trunk/core/src/org/apache/pivot/util/StringUtils.java (original)
+++ pivot/trunk/core/src/org/apache/pivot/util/StringUtils.java Fri Feb 23 
21:35:57 2018
@@ -16,6 +16,10 @@
  */
 package org.apache.pivot.util;
 
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+
 /**
  * A set of static methods that perform various string manipulation
  * functions.
@@ -67,5 +71,57 @@ public class StringUtils {
         return builder.toString();
     }
 
+    /**
+     * Convert a string to a {@link Number}, with a possible known type to
+     * convert to.
+     * <p> If there isn't a known type, this is tricky, so go with Integer 
first,
+     * then try Double, or finally BigDecimal.
+     *
+     * @param <T> The numeric type to return.
+     * @param string The string representation of a number.
+     * @param type The desired numeric type of this value, or {@code null} to
+     * figure out the appropriate type ourselves.
+     * @return Either an {@link Integer}, {@link Double} or {@link BigDecimal}
+     * value, depending on the format of the input string (if the input type
+     * is {@code null}), or a value of the given type.
+     * @throws NumberFormatException if the input string doesn't contain a 
value
+     * parseable by one of these methods.
+     */
+    public static <T extends Number> Number toNumber(String string, Class<? 
extends Number> type) {
+        Utils.checkNullOrEmpty(string, "string");
+
+        if (type == null) {
+            try {
+                return Integer.valueOf(string);
+            } catch (NumberFormatException nfe) {
+                try {
+                    return Double.valueOf(string);
+                } catch (NumberFormatException nfe2) {
+                    return new BigDecimal(string);
+                }
+            }
+        } else {
+            if (type == Byte.class || type == byte.class) {
+                return Byte.valueOf(string);
+            } else if (type == Short.class || type == short.class) {
+                return Short.valueOf(string);
+            } else if (type == Integer.class || type == int.class) {
+                return Integer.valueOf(string);
+            } else if (type == Long.class || type == long.class) {
+                return Long.valueOf(string);
+            } else if (type == Float.class || type == float.class) {
+                return Float.valueOf(string);
+            } else if (type == Double.class || type == double.class) {
+                return Double.valueOf(string);
+            } else if (type == BigDecimal.class) {
+                return new BigDecimal(string);
+            } else if (type == BigInteger.class) {
+                return new BigInteger(string);
+            }
+            // TODO: maybe throw exception
+            return null;
+        }
+    }
+
 }
 

Modified: pivot/trunk/core/src/org/apache/pivot/util/Utils.java
URL: 
http://svn.apache.org/viewvc/pivot/trunk/core/src/org/apache/pivot/util/Utils.java?rev=1825172&r1=1825171&r2=1825172&view=diff
==============================================================================
--- pivot/trunk/core/src/org/apache/pivot/util/Utils.java (original)
+++ pivot/trunk/core/src/org/apache/pivot/util/Utils.java Fri Feb 23 21:35:57 
2018
@@ -190,6 +190,28 @@ public class Utils {
     }
 
     /**
+     * Check if the input argument is positive (greater than zero), and throw 
an
+     * {@link IllegalArgumentException} if not, with or without a descriptive 
message,
+     * depending on the {@code argument} supplied.
+     *
+     * @param value The value to check.
+     * @param argument A description for the argument, used to
+     * construct a message like {@code "xxx must be positive."}.
+     * Can be {@code null} or an empty string, in which case a plain
+     * {@link IllegalArgumentException} is thrown without any detail message.
+     * @throws IllegalArgumentException if the value is negative.
+     */
+    public static void checkPositive(float value, String argument) {
+        if (value <= 0.0f) {
+            if (isNullOrEmpty(argument)) {
+                throw new IllegalArgumentException();
+            } else {
+                throw new IllegalArgumentException(argument + " must be 
positive.");
+            }
+        }
+    }
+
+    /**
      * Check that the given value falls within the range of a non-negative 
"short" value, that is
      * between 0 and 0x7FFF (inclusive).
      *

Added: pivot/trunk/tests/src/org/apache/pivot/tests/GaugeTest.java
URL: 
http://svn.apache.org/viewvc/pivot/trunk/tests/src/org/apache/pivot/tests/GaugeTest.java?rev=1825172&view=auto
==============================================================================
--- pivot/trunk/tests/src/org/apache/pivot/tests/GaugeTest.java (added)
+++ pivot/trunk/tests/src/org/apache/pivot/tests/GaugeTest.java Fri Feb 23 
21:35:57 2018
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.tests;
+
+import java.awt.Color;
+
+import org.apache.pivot.beans.BXMLSerializer;
+import org.apache.pivot.collections.Map;
+import org.apache.pivot.wtk.Application;
+import org.apache.pivot.wtk.ApplicationContext;
+import org.apache.pivot.wtk.Component;
+import org.apache.pivot.wtk.DesktopApplicationContext;
+import org.apache.pivot.wtk.Display;
+import org.apache.pivot.wtk.Gauge;
+import org.apache.pivot.wtk.PushButton;
+import org.apache.pivot.wtk.Theme;
+import org.apache.pivot.wtk.Window;
+
+public class GaugeTest implements Application {
+    private Window window;
+    private PushButton gasPedal;
+    private PushButton brakePedal;
+    private Gauge<Integer> speedGauge;
+    private int speed;
+    private Color textColor;
+    private Color warningColor;
+    private Color criticalColor;
+
+    private int randomInt(int bound) {
+        double variant = Math.random();
+        int value;
+        if (variant >= 0.5) {
+            value = (int)Math.floor((variant - 0.5) * (double)bound);
+        } else {
+            value = (int)Math.ceil((variant * -1.0) * (double)bound);
+        }
+        return value;
+    }
+
+    private void setSpeed(int value) {
+        speed = Math.min(value, speedGauge.getMaxValue());
+        speed = Math.max(speed, speedGauge.getMinValue());
+        speedGauge.setValue(speed);
+        Color color = textColor;
+        if (speed >= speedGauge.getCriticalLevel()) {
+            color = criticalColor;
+        } else if (speed > speedGauge.getWarningLevel()) {
+            color = warningColor;
+        }
+        speedGauge.getStyles().put("textColor", color);
+        speedGauge.setText(Integer.toString(speed) + " mph");
+    }
+
+    private void hitTheGas() {
+        setSpeed(speed + 5 + randomInt(2));
+        System.out.println("Gas pedal -> " + speed);
+    }
+
+    private void hitTheBrakes() {
+        setSpeed(speed - (10 + randomInt(3)));
+        System.out.println("Brake pedal -> " + speed);
+    }
+
+    private void varyTheSpeed() {
+        if (speed > 0) {
+            setSpeed(speed + randomInt(5));
+            System.out.println("Varying speed -> " + speed);
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void startup(Display display, Map<String, String> properties) 
throws Exception {
+        BXMLSerializer bxmlSerializer = new BXMLSerializer();
+        window = (Window) 
bxmlSerializer.readObject(getClass().getResource("gauge_test.bxml"));
+        gasPedal = (PushButton)bxmlSerializer.getNamespace().get("gasPedal");
+        brakePedal = 
(PushButton)bxmlSerializer.getNamespace().get("brakePedal");
+        speedGauge = 
(Gauge<Integer>)bxmlSerializer.getNamespace().get("speedGauge");
+        warningColor = speedGauge.getStyles().getColor("warningColor");
+        criticalColor = speedGauge.getStyles().getColor("criticalColor");
+        textColor = Theme.getTheme().getColor(6);
+        setSpeed(speedGauge.getValue());
+        gasPedal.getButtonPressListeners().add((button) -> hitTheGas());
+        brakePedal.getButtonPressListeners().add((button) -> hitTheBrakes());
+        ApplicationContext.scheduleRecurringCallback(() -> varyTheSpeed(), 
500L);
+        window.open(display);
+    }
+
+    public static void main(String[] args) {
+        DesktopApplicationContext.main(GaugeTest.class, args);
+    }
+}

Added: pivot/trunk/tests/src/org/apache/pivot/tests/gauge_test.bxml
URL: 
http://svn.apache.org/viewvc/pivot/trunk/tests/src/org/apache/pivot/tests/gauge_test.bxml?rev=1825172&view=auto
==============================================================================
--- pivot/trunk/tests/src/org/apache/pivot/tests/gauge_test.bxml (added)
+++ pivot/trunk/tests/src/org/apache/pivot/tests/gauge_test.bxml Fri Feb 23 
21:35:57 2018
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to you under the Apache License,
+Version 2.0 (the "License"); you may not use this file except in
+compliance with the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<Window
+    xmlns:bxml="http://pivot.apache.org/bxml";
+    xmlns="org.apache.pivot.wtk">
+    <TablePane>
+        <columns>
+            <TablePane.Column width="1*"/>
+        </columns>
+        <rows>
+            <TablePane.Row height="-1">
+                <Border title="Speed" styles="{backgroundColor:10}">
+                    <Gauge bxml:id="speedGauge" origin="SOUTH" type="Integer" 
minValue="0" maxValue="200" value="0" warningLevel="120" criticalLevel="180"
+                        styles="{gaugeColor:8, backgroundColor:10, 
color:'green', warningColor:20, criticalColor:23, font:{size:20}}, 
thickness:10.0"/>
+                </Border>
+            </TablePane.Row>
+            <TablePane.Row height="-1">
+                <BoxPane orientation="horizontal" 
styles="{padding:{left:20,right:20,top:4,bottom:4},spacing:20}">
+                    <PushButton bxml:id="gasPedal" preferredWidth="80" 
buttonData="Accelerate"/>
+                    <PushButton bxml:id="brakePedal" preferredWidth="80" 
buttonData="Brake"/>
+                </BoxPane>
+            </TablePane.Row>
+        </rows>
+    </TablePane>
+</Window>

Added: 
pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraGaugeSkin.java
URL: 
http://svn.apache.org/viewvc/pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraGaugeSkin.java?rev=1825172&view=auto
==============================================================================
--- 
pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraGaugeSkin.java 
(added)
+++ 
pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraGaugeSkin.java 
Fri Feb 23 21:35:57 2018
@@ -0,0 +1,442 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.wtk.skin.terra;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.font.FontRenderContext;
+import java.awt.font.LineMetrics;
+import java.awt.geom.Arc2D;
+import java.awt.geom.Rectangle2D;
+
+import org.apache.pivot.collections.Dictionary;
+import org.apache.pivot.collections.Sequence;
+import org.apache.pivot.util.StringUtils;
+import org.apache.pivot.util.Utils;
+import org.apache.pivot.wtk.Component;
+import org.apache.pivot.wtk.Dimensions;
+import org.apache.pivot.wtk.Gauge;
+import org.apache.pivot.wtk.GaugeListener;
+import org.apache.pivot.wtk.GraphicsUtilities;
+import org.apache.pivot.wtk.Insets;
+import org.apache.pivot.wtk.Origin;
+import org.apache.pivot.wtk.Platform;
+import org.apache.pivot.wtk.Theme;
+import org.apache.pivot.wtk.skin.ComponentSkin;
+
+
+public class TerraGaugeSkin<T extends Number> extends ComponentSkin implements 
GaugeListener<T> {
+    private static final float STROKE_WIDTH = 6.0f;
+
+    private Color backgroundColor;
+    /** This is the color of the circle part of the gauge, where the value "is 
not". */
+    private Color gaugeColor;
+    private Color textColor;
+    /** This is the color for the "value" if it is below the warning or 
critical levels. */
+    private Color color;
+    private Color warningColor;
+    private Color criticalColor;
+    private boolean showTickMarks = false;
+    private Insets padding;
+    private Font font;
+    private float thickness = STROKE_WIDTH;
+    private float textAscent;
+
+    private static final RenderingHints renderingHints = new 
RenderingHints(null);
+
+    static {
+        renderingHints.put(RenderingHints.KEY_ANTIALIASING, 
RenderingHints.VALUE_ANTIALIAS_ON);
+        renderingHints.put(RenderingHints.KEY_STROKE_CONTROL, 
RenderingHints.VALUE_STROKE_PURE);
+        renderingHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, 
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
+    };
+
+    public TerraGaugeSkin() {
+        // TODO: set the rest of the default stuff (colors, etc.)
+        font = currentTheme().getFont().deriveFont(Font.BOLD, 24.0f);
+        backgroundColor = defaultBackgroundColor();
+        padding = Insets.NONE;
+    }
+
+    @Override
+    public void install(Component component) {
+        super.install(component);
+
+        @SuppressWarnings("unchecked")
+        Gauge<T> gauge = (Gauge<T>)component;
+        gauge.getGaugeListeners().add(this);
+    }
+
+    @Override
+    public boolean isFocusable() {
+        return false;
+    }
+
+    @Override
+    public void layout() {
+        // Nothing to do because we have no child components
+    }
+
+    @Override
+    public int getPreferredHeight(int width) {
+        return 128;  // Note: same as TerraActivityIndicatorSkin
+    }
+
+    @Override
+    public int getPreferredWidth(int height) {
+        return 128;  // Note: same as TerraActivityIndicatorSkin
+    }
+
+    /**
+     * Do the transformation of the arcs to the normal cartesian coordinates, 
and for the specified origin,
+     * then draw the arc in the given color.  Assumes the rendering hints and 
the stroke have already been set.
+     * <p> Start and extent are in the 0-360 range
+     */
+    private void drawArc(Graphics2D graphics, Rectangle2D rect, Origin origin, 
float arcStart, float arcExtent, Color color) {
+        float newStart = origin.getOriginAngle() - arcStart - arcExtent;
+        Arc2D arc = new Arc2D.Float(rect, newStart, arcExtent, Arc2D.OPEN);
+        graphics.setPaint(color);
+        graphics.draw(arc);
+    }
+
+    @Override
+    public void paint(Graphics2D graphics) {
+        @SuppressWarnings("unchecked")
+        Gauge<T> gauge = (Gauge<T>)getComponent();
+        // NOTE: sanity check:  warning level > min && < max, warning < 
critical if both set
+        // also critical > main && < max, critical > warning if both set
+
+        String text = gauge.getText();
+        T value = gauge.getValue();
+        T minLevel = gauge.getMinValue();
+        T maxLevel = gauge.getMaxValue();
+        T warningLevel = gauge.getWarningLevel();
+        T criticalLevel = gauge.getCriticalLevel();
+
+        Dimensions size = gauge.getSize();
+        Origin origin = gauge.getOrigin();
+
+        float diameter = (float)(Math.min(size.width - padding.getWidth(), 
size.height - padding.getHeight()))
+            - (thickness * 2.0f);
+        float x = ((float)size.width - diameter) / 2.0f;
+        float y = ((float)size.height - diameter) / 2.0f;
+
+        Rectangle2D rect = new Rectangle2D.Float(x, y, diameter, diameter);
+
+        float minValue = minLevel.floatValue();
+        float maxValue = maxLevel.floatValue();
+        float fullRange = maxValue - minValue;
+        float toAngle = 360.0f / fullRange;
+        float activeValue = value.floatValue() - minValue;
+        float activeAngle = activeValue * toAngle;
+
+        if (backgroundColor != null) {
+            graphics.setColor(backgroundColor);
+            graphics.fillRect(0, 0, size.width, size.height);
+        }
+
+        graphics.setRenderingHints(renderingHints);
+        graphics.setStroke(new BasicStroke(thickness, BasicStroke.CAP_BUTT, 
BasicStroke.JOIN_BEVEL));
+
+        // Note: presume that critical > warning if both are set
+        if (warningLevel != null && warningColor != null) {
+            float warningValue = warningLevel.floatValue() - minValue;
+            if (activeValue >= warningValue) {
+                float warningAngle = warningValue * toAngle;
+                if (criticalLevel != null && criticalColor != null) {
+                    float criticalValue = criticalLevel.floatValue() - 
minValue;
+                    if (activeValue >= criticalValue) {
+                        // Three segments here: min->warning (normal color), 
warning->critical (warning color), critical->active (critical color)
+                        float criticalAngle = criticalValue * toAngle;
+                        drawArc(graphics, rect, origin, 0.0f, warningAngle, 
color);
+                        drawArc(graphics, rect, origin, warningAngle, 
criticalAngle - warningAngle, warningColor);
+                        drawArc(graphics, rect, origin, criticalAngle, 
activeAngle - criticalAngle, criticalColor);
+                    } else {
+                        // Two segments here: min->warning (normal), 
warning->active (warning)
+                        drawArc(graphics, rect, origin, 0.0f, warningAngle, 
color);
+                        drawArc(graphics, rect, origin, warningAngle, 
activeAngle - warningAngle, warningColor);
+                    }
+                } else {
+                    // Two segments here: min->warning (normal), 
warning->active (warning color)
+                    drawArc(graphics, rect, origin, 0.0f, warningAngle, color);
+                    drawArc(graphics, rect, origin, warningAngle, activeAngle 
- warningAngle, warningColor);
+                }
+            } else {
+                // Just one segment, the normal value
+                drawArc(graphics, rect, origin, 0.0f, activeAngle, color);
+            }
+        } else if (criticalLevel != null && criticalColor != null) {
+            float criticalValue = criticalLevel.floatValue() - minValue;
+            if (activeValue > criticalValue) {
+                // Two here: min->critical (normal color), critical->active 
(critical color)
+                float criticalAngle = criticalValue * toAngle;
+                drawArc(graphics, rect, origin, 0.0f, criticalAngle, color);
+                drawArc(graphics, rect, origin, criticalAngle, activeAngle - 
criticalAngle, criticalColor);
+            } else {
+                // One, min->active (normal color)
+                drawArc(graphics, rect, origin, 0.0f, activeAngle, color);
+            }
+        } else {
+            // Else just one segment (min->active, normal color)
+            drawArc(graphics, rect, origin, 0.0f, activeAngle, color);
+        }
+
+        // Now draw the "inactive" part the rest of the way
+        if (activeAngle < 360.0f) {
+            drawArc(graphics, rect, origin, activeAngle, 360.0f - activeAngle, 
gaugeColor);
+        }
+
+        // Draw the text in the middle (if any)
+        if (!Utils.isNullOrEmpty(text)) {
+            FontRenderContext fontRenderContext = 
GraphicsUtilities.prepareForText(graphics, font, textColor);
+
+            Rectangle2D textBounds = font.getStringBounds(text, 
fontRenderContext);
+            double textX = x + (diameter - textBounds.getWidth()) / 2.0;
+            double textY = y + (diameter - textBounds.getHeight()) / 2.0 + 
textAscent;
+
+            graphics.drawString(text, (int) textX, (int) textY);
+        }
+    }
+
+    // TODO: possible other styles to implement:
+    // show radial marks
+
+    public Font getFont() {
+        return font;
+    }
+
+    public void setFont(Font font) {
+        Utils.checkNull(font, "font");
+
+        this.font = font;
+        invalidateComponent();
+    }
+
+    public final void setFont(String font) {
+        setFont(decodeFont(font));
+    }
+
+    public final void setFont(Dictionary<String, ?> font) {
+        setFont(Theme.deriveFont(font));
+    }
+
+    public Color getBackgroundColor() {
+        return backgroundColor;
+    }
+
+    public final void setBackgroundColor(Color backgroundColor) {
+        // We allow a null background color here
+        this.backgroundColor = backgroundColor;
+        repaintComponent();
+    }
+
+    public final void setBackgroundColor(String backgroundColor) {
+        setBackgroundColor(GraphicsUtilities.decodeColor(backgroundColor, 
"backgroundColor"));
+    }
+
+    public final void setBackgroundColor(int backgroundColor) {
+        Theme theme = currentTheme();
+        setBackgroundColor(theme.getColor(backgroundColor));
+    }
+
+    public Color getColor() {
+        return color;
+    }
+
+    public final void setColor(Color color) {
+        Utils.checkNull(color, "color");
+        this.color = color;
+        repaintComponent();
+    }
+
+    public final void setColor(String color) {
+        setColor(GraphicsUtilities.decodeColor(color, "color"));
+    }
+
+    public final void setColor(int color) {
+        Theme theme = currentTheme();
+        setColor(theme.getColor(color));
+    }
+
+    public Color getGaugeColor() {
+        return gaugeColor;
+    }
+
+    public final void setGaugeColor(Color gaugeColor) {
+        Utils.checkNull(gaugeColor, "gaugeColor");
+        this.gaugeColor = gaugeColor;
+        repaintComponent();
+    }
+
+    public final void setGaugeColor(String gaugeColor) {
+        setGaugeColor(GraphicsUtilities.decodeColor(gaugeColor, "gaugeColor"));
+    }
+
+    public final void setGaugeColor(int gaugeColor) {
+        Theme theme = currentTheme();
+        setGaugeColor(theme.getColor(gaugeColor));
+    }
+
+    public Color getTextColor() {
+        return textColor;
+    }
+
+    public final void setTextColor(Color textColor) {
+        Utils.checkNull(textColor, "textColor");
+        this.textColor = textColor;
+        repaintComponent();
+    }
+
+    public final void setTextColor(String textColor) {
+        setTextColor(GraphicsUtilities.decodeColor(textColor, "textColor"));
+    }
+
+    public final void setTextColor(int textColor) {
+        Theme theme = currentTheme();
+        setTextColor(theme.getColor(textColor));
+    }
+
+    public Color getWarningColor() {
+        return this.warningColor;
+    }
+
+    public final void setWarningColor(Color warningColor) {
+        // Note: null is okay here to effectively disable using the warning 
color logic
+        this.warningColor = warningColor;
+        repaintComponent();
+    }
+
+    public final void setWarningColor(String warningColor) {
+        setWarningColor(GraphicsUtilities.decodeColor(warningColor, 
"warningColor"));
+    }
+
+    public final void setWarningColor(int warningColor) {
+        Theme theme = currentTheme();
+        setWarningColor(theme.getColor(warningColor));
+    }
+
+    public Color getCriticalColor() {
+        return this.criticalColor;
+    }
+
+    public final void setCriticalColor(Color criticalColor) {
+        // Note: null is okay here to disable using the critical color logic
+        this.criticalColor = criticalColor;
+    }
+
+    public final void setCriticalColor(String criticalColor) {
+        setCriticalColor(GraphicsUtilities.decodeColor(criticalColor, 
"criticalColor"));
+    }
+
+    public final void setCriticalColor(int criticalColor) {
+        Theme theme = currentTheme();
+        setCriticalColor(theme.getColor(criticalColor));
+    }
+
+    public Insets getPadding() {
+        return padding;
+    }
+
+    public void setPadding(Insets padding) {
+        Utils.checkNull(padding, "padding");
+
+        this.padding = padding;
+        invalidateComponent();
+    }
+
+    public final void setPadding(Dictionary<String, ?> padding) {
+        setPadding(new Insets(padding));
+    }
+
+    public final void setPadding(Sequence<?> padding) {
+        setPadding(new Insets(padding));
+    }
+
+    public final void setPadding(int padding) {
+        setPadding(new Insets(padding));
+    }
+
+    public void setPadding(Number padding) {
+        setPadding(new Insets(padding));
+    }
+
+    public float getThickness() {
+        return thickness;
+    }
+
+    public final void setThickness(float thickness) {
+        Utils.checkPositive(thickness, "thickness");
+
+        this.thickness = thickness;
+        repaintComponent();
+    }
+
+    public final void setThickness(Number thickness) {
+        Utils.checkNull(thickness, "thickness");
+        setThickness(thickness.floatValue());
+    }
+
+    public final void setThickness(String thickness) {
+        Utils.checkNullOrEmpty(thickness, "thickness");
+        setThickness(StringUtils.toNumber(thickness, Float.class));
+    }
+
+    /**
+     * Sets the amount of space to leave between the edge of the Border and its
+     * content.
+     *
+     * @param padding A string containing an integer or a JSON dictionary with
+     * keys left, top, bottom, and/or right.
+     */
+    public final void setPadding(String padding) {
+        setPadding(Insets.decode(padding));
+    }
+
+    @Override
+    public void originChanged(Gauge<T> gauge, Origin previousOrigin) {
+        invalidateComponent();
+    }
+
+    @Override
+    public void valueChanged(Gauge<T> gauge, T previousValue) {
+        repaintComponent();
+    }
+
+    @Override
+    public void textChanged(Gauge<T> gauge, String previousText) {
+        String text = gauge.getText();
+        if (!Utils.isNullOrEmpty(text)) {
+            FontRenderContext fontRenderContext = 
Platform.getFontRenderContext();
+            LineMetrics lm = font.getLineMetrics(text, fontRenderContext);
+            textAscent = lm.getAscent();
+        }
+        repaintComponent();
+    }
+
+    @Override
+    public void minMaxValueChanged(Gauge<T> gauge, T previousMinValue, T 
previousMaxValue) {
+        repaintComponent();
+    }
+
+    @Override
+    public void warningCriticalLevelChanged(Gauge<T> gauge, T 
previousWarningLevel, T previousCriticalLevel) {
+        repaintComponent();
+    }
+}

Modified: 
pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraTheme.java
URL: 
http://svn.apache.org/viewvc/pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraTheme.java?rev=1825172&r1=1825171&r2=1825172&view=diff
==============================================================================
--- pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraTheme.java 
(original)
+++ pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraTheme.java 
Fri Feb 23 21:35:57 2018
@@ -49,6 +49,7 @@ import org.apache.pivot.wtk.FileBrowserS
 import org.apache.pivot.wtk.FillPane;
 import org.apache.pivot.wtk.Form;
 import org.apache.pivot.wtk.Frame;
+import org.apache.pivot.wtk.Gauge;
 import org.apache.pivot.wtk.GraphicsUtilities;
 import org.apache.pivot.wtk.GridPane;
 import org.apache.pivot.wtk.HyperlinkButton;
@@ -157,6 +158,7 @@ public final class TerraTheme extends Th
         componentSkinMap.put(FileBrowserSheet.class, 
TerraFileBrowserSheetSkin.class);
         componentSkinMap.put(Form.class, TerraFormSkin.class);
         componentSkinMap.put(Frame.class, TerraFrameSkin.class);
+        componentSkinMap.put(Gauge.class, TerraGaugeSkin.class);
         componentSkinMap.put(GridPane.class, TerraGridPaneSkin.class);
         componentSkinMap.put(HyperlinkButton.class, TerraLinkButtonSkin.class);
         componentSkinMap.put(Label.class, TerraLabelSkin.class);

Added: pivot/trunk/wtk/src/org/apache/pivot/wtk/Gauge.java
URL: 
http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/Gauge.java?rev=1825172&view=auto
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/Gauge.java (added)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/Gauge.java Fri Feb 23 21:35:57 2018
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.wtk;
+
+import org.apache.pivot.util.ListenerList;
+import org.apache.pivot.util.StringUtils;
+import org.apache.pivot.util.Utils;
+
+/**
+ * A circular gauge component that can display a single value of an arbitrary
+ * numeric type.
+ */
+public class Gauge<T extends Number> extends Component {
+    private Origin origin;
+    private String text;
+    private T value;
+    private T minValue;
+    private T maxValue;
+    private T warningLevel;
+    private T criticalLevel;
+    private GaugeListener.Listeners<T> gaugeListeners = new 
GaugeListener.Listeners<T>();
+    /** Runtime class (used to check values at runtime). */
+    private Class<? extends Number> clazz;
+
+    public Gauge() {
+       this(Origin.NORTH);
+    }
+
+    public Gauge(Origin origin) {
+        this.origin = origin;
+        installSkin(Gauge.class);
+    }
+
+    public Origin getOrigin() {
+        return this.origin;
+    }
+
+    public void setOrigin(Origin origin) {
+        Utils.checkNull(origin, "origin");
+
+        Origin previousOrigin = this.origin;
+
+        if (previousOrigin != origin) {
+            this.origin = origin;
+            gaugeListeners.originChanged(this, previousOrigin);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public void setType(String type) {
+        try {
+            this.clazz = (Class<? extends Number>)((type.indexOf('.') < 0) ? 
Class.forName("java.lang." + type) :
+                Class.forName(type));
+        } catch (ClassNotFoundException cnfe) {
+            throw new RuntimeException(cnfe);
+        }
+    }
+
+    /**
+     * If the {@link #clazz} was set by a prior call to {@link #setType} then
+     * check the runtime class of the value against it, otherwise call {@link 
#setType}
+     * to establish it for the future.
+     *
+     * @param value A value presumably compatible with the declared type of 
this gauge.
+     * @throws ClassCastException if the value is not compatible with the 
previously
+     * established type.
+     */
+    private void setOrCheckClass(T value) {
+        if (this.clazz != null) {
+            if (!clazz.isInstance(value)) {
+                throw new ClassCastException("Value is not an instance of " + 
clazz.getName());
+            }
+        } else {
+            setType(value.getClass().getName());
+        }
+    }
+
+    public T getValue() {
+        return value;
+    }
+
+    public void setValue(T value) {
+        Utils.checkNull(value, "value");
+        setOrCheckClass(value);
+
+        T previousValue = this.value;
+
+        if (value != previousValue) {
+            this.value = value;
+            gaugeListeners.valueChanged(this, previousValue);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public void setValue(String value) {
+        setValue((T)StringUtils.toNumber(value, clazz));
+    }
+
+    public T getMinValue() {
+        return minValue;
+    }
+
+    public void setMinValue(T minValue) {
+        if (minValue != null) {
+            setOrCheckClass(minValue);
+        }
+
+        T previousMinValue = this.minValue;
+
+        if (minValue != previousMinValue) {
+            this.minValue = minValue;
+            gaugeListeners.minMaxValueChanged(this, previousMinValue, 
this.maxValue);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public void setMinValue(String minValue) {
+        setMinValue((T)StringUtils.toNumber(minValue, clazz));
+    }
+
+    public T getMaxValue() {
+        return maxValue;
+    }
+
+    public void setMaxValue(T maxValue) {
+        if (maxValue != null) {
+            setOrCheckClass(maxValue);
+        }
+
+        T previousMaxValue = this.maxValue;
+
+        if (previousMaxValue != maxValue) {
+            this.maxValue = maxValue;
+            gaugeListeners.minMaxValueChanged(this, this.minValue, 
previousMaxValue);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public void setMaxValue(String maxValue) {
+        setMaxValue((T)StringUtils.toNumber(maxValue, clazz));
+    }
+
+    public T getWarningLevel() {
+        return this.warningLevel;
+    }
+
+    public final void setWarningLevel(T warningLevel) {
+        if (warningLevel != null) {
+            setOrCheckClass(warningLevel);
+        }
+
+        this.warningLevel = warningLevel;
+    }
+
+    @SuppressWarnings("unchecked")
+    public void setWarningLevel(String warningLevel) {
+        setWarningLevel((T)StringUtils.toNumber(warningLevel, clazz));
+    }
+
+    public T getCriticalLevel() {
+        return this.criticalLevel;
+    }
+
+    public final void setCriticalLevel(T criticalLevel) {
+        if (criticalLevel != null) {
+            setOrCheckClass(criticalLevel);
+        }
+
+        this.criticalLevel = criticalLevel;
+    }
+
+    @SuppressWarnings("unchecked")
+    public void setCriticalLevel(String criticalLevel) {
+        setCriticalLevel((T)StringUtils.toNumber(criticalLevel, clazz));
+    }
+
+    public String getText() {
+        return this.text;
+    }
+
+    public void setText(String text) {
+        // Null text is allowed
+        String previousText = this.text;
+
+        if ((previousText == null && text != null) ||
+            (previousText != null && text == null) ||
+            (!previousText.equals(text))) {
+            this.text = text;
+            gaugeListeners.textChanged(this, previousText);
+        }
+    }
+
+    public ListenerList<GaugeListener<T>> getGaugeListeners() {
+        return gaugeListeners;
+    }
+}

Added: pivot/trunk/wtk/src/org/apache/pivot/wtk/GaugeListener.java
URL: 
http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/GaugeListener.java?rev=1825172&view=auto
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/GaugeListener.java (added)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/GaugeListener.java Fri Feb 23 
21:35:57 2018
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.wtk;
+
+import org.apache.pivot.util.ListenerList;
+
+/**
+ * Gauge listener interface.
+ */
+public interface GaugeListener<T extends Number> {
+    /**
+     * Gauge listeners.
+     */
+    public static class Listeners<T extends Number> extends 
ListenerList<GaugeListener<T>> implements GaugeListener<T> {
+        @Override
+        public void originChanged(Gauge<T> gauge, Origin previousOrigin) {
+            forEach(listener -> listener.originChanged(gauge, previousOrigin));
+        }
+
+        @Override
+        public void valueChanged(Gauge<T> gauge, T previousValue) {
+            forEach(listener -> listener.valueChanged(gauge, previousValue));
+        }
+
+        @Override
+        public void textChanged(Gauge<T> gauge, String previousText) {
+            forEach(listener -> listener.textChanged(gauge, previousText));
+        }
+
+        @Override
+        public void minMaxValueChanged(Gauge<T> gauge, T previousMinValue, T 
previousMaxValue) {
+            forEach(listener -> listener.minMaxValueChanged(gauge, 
previousMinValue, previousMaxValue));
+        }
+
+        @Override
+        public void warningCriticalLevelChanged(Gauge<T> gauge, T 
previousWarningLevel, T previousCriticalLevel) {
+            forEach(listener -> listener.warningCriticalLevelChanged(gauge, 
previousWarningLevel, previousCriticalLevel));
+        }
+    }
+
+    /**
+     * Called when the origin (starting point of the gauge value) changes.
+     *
+     * @param gauge The gauge that has changed.
+     * @param previousOrigin The previous origin value.
+     */
+    default public void originChanged(Gauge<T> gauge, Origin previousOrigin) {
+    }
+
+    /**
+     * Called when the gauge value changes.
+     *
+     * @param gauge The gauge that is changing.
+     * @param previousValue The old value.
+     */
+    default public void valueChanged(Gauge<T> gauge, T previousValue) {
+    }
+
+    /**
+     * Called when the gauge's text changes.
+     *
+     * @param gauge The gauge whose text changed.
+     * @param previousText The previous text.
+     */
+    default public void textChanged(Gauge<T> gauge, String previousText) {
+    }
+
+    /**
+     * Called when min or max values change.
+     *
+     * @param gauge The gauge that is changing.
+     * @param previousMinValue The previous minimum.
+     * @param previousMaxValue The previous maximum.
+     */
+    default public void minMaxValueChanged(Gauge<T> gauge, T previousMinValue, 
T previousMaxValue) {
+    }
+
+    /**
+     * Called when the warning or critical levels for the gauge have changed.
+     *
+     * @param gauge The gauge we're talking about.
+     * @param previousWarningLevel The previous value for the warning level.
+     * @param previousCriticalLevel The previous value for the critical level.
+     */
+    default public void warningCriticalLevelChanged(Gauge<T> gauge, T 
previousWarningLevel, T previousCriticalLevel) {
+    }
+}
+

Added: pivot/trunk/wtk/src/org/apache/pivot/wtk/Origin.java
URL: 
http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/Origin.java?rev=1825172&view=auto
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/Origin.java (added)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/Origin.java Fri Feb 23 21:35:57 
2018
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.wtk;
+
+/**
+ * An enumeration of the possible origin (that is, starting point) values
+ * for a {@link Gauge} component, including the starting angle offset for each.
+ */
+public enum Origin {
+    /** Origin is at the top. */
+    NORTH   (90.0f),
+    /** Origin is on the right side of the gauge. */
+    EAST    (360.0f),
+    /** Origin is at the bottom. */
+    SOUTH   (270.0f),
+    /** Origin is to the left side of the gauge. */
+    WEST    (180.0f);
+
+    private float originAngle;
+
+    private Origin(float angle) {
+         this.originAngle = angle;
+    }
+
+    public float getOriginAngle() {
+        return this.originAngle;
+    }
+
+}

Added: pivot/trunk/wtk/test/org/apache/pivot/wtk/test/GaugeTest.java
URL: 
http://svn.apache.org/viewvc/pivot/trunk/wtk/test/org/apache/pivot/wtk/test/GaugeTest.java?rev=1825172&view=auto
==============================================================================
--- pivot/trunk/wtk/test/org/apache/pivot/wtk/test/GaugeTest.java (added)
+++ pivot/trunk/wtk/test/org/apache/pivot/wtk/test/GaugeTest.java Fri Feb 23 
21:35:57 2018
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.wtk.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import org.apache.pivot.wtk.Gauge;
+import org.apache.pivot.wtk.GaugeListener;
+import org.apache.pivot.wtk.Origin;
+
+
+public class GaugeTest implements GaugeListener<Integer> {
+    private int originChangeCount = 0;
+    private int valueChangeCount = 0;
+    private int textChangeCount = 0;
+    private int minMaxChangeCount = 0;
+
+    @Override
+    public void originChanged(Gauge<Integer> gauge, Origin previousOrigin) {
+        System.out.println("Origin changed to " + gauge.getOrigin());
+        originChangeCount++;
+    }
+
+    @Override
+    public void valueChanged(Gauge<Integer> gauge, Integer previousValue) {
+        System.out.println("Value changed to " + gauge.getValue());
+        valueChangeCount++;
+    }
+
+    @Override
+    public void textChanged(Gauge<Integer> gauge, String previousText) {
+        System.out.println("Text changed to " + gauge.getText());
+        textChangeCount++;
+    }
+
+    @Override
+    public void minMaxValueChanged(Gauge<Integer> gauge, Integer 
previousMinValue, Integer previousMaxValue) {
+        System.out.println("Min or max changed: min=" + gauge.getMinValue() + 
", max=" + gauge.getMaxValue());
+        minMaxChangeCount++;
+    }
+
+    @Test
+    public void testListeners() {
+        Gauge<Integer> gauge = new Gauge<>();
+        gauge.getGaugeListeners().add(this);
+
+        // Test all the listeners getting fired as they should
+        gauge.setOrigin(Origin.NORTH);  // no change here
+        gauge.setOrigin(Origin.SOUTH);
+        gauge.setOrigin(Origin.SOUTH);  // again, no change
+        gauge.setOrigin(Origin.EAST);
+        gauge.setOrigin(Origin.WEST);
+        gauge.setOrigin(Origin.NORTH);
+
+        gauge.setMinValue(0);
+        gauge.setMaxValue(100);
+        gauge.setMinValue(0);
+        gauge.setMaxValue(100);
+
+        gauge.setValue(0);
+        gauge.setValue(2);
+        gauge.setValue(10);
+        gauge.setValue(0);
+
+        // Now check for proper listener event counts
+        assertEquals(originChangeCount, 4);
+        assertEquals(minMaxChangeCount, 2);
+        assertEquals(valueChangeCount, 4);
+    }
+}


Reply via email to