/*
* Copyright 2014-2016 Cel Skeggs.
*
* This file is part of the CCRE, the Common Chicken Runtime Engine.
*
* The CCRE is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the CCRE. If not, see <http://www.gnu.org/licenses/>.
*/
package ccre.supercanvas.components.channels;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.io.Serializable;
import ccre.channel.CancelOutput;
import ccre.channel.FloatInput;
import ccre.channel.FloatOutput;
import ccre.log.Logger;
import ccre.rconf.RConf;
import ccre.rconf.RConf.Entry;
import ccre.supercanvas.BaseChannelComponent;
import ccre.supercanvas.Rendering;
import ccre.supercanvas.SuperCanvasPanel;
/**
* A component allowing interaction with floats.
*
* @author skeggsc
*/
public class FloatControlComponent extends BaseChannelComponent<FloatControlComponent.View> {
private static final long serialVersionUID = -5862659067200938010L;
static enum View {
HORIZONTAL_POINTER, TICKER, TEXTUAL
}
private float lastSentValue;
private final FloatInput alternateSource;
private final FloatOutput rawOut;
private float minimum = -1.0f, maximum = 1.0f;
private boolean hasSentInitial = false;
private StringBuilder activeBuffer;
private transient CancelOutput unsubscribe;
/**
* Create a new FloatControlComponent with a FloatOutput to control.
*
* @param cx the X coordinate.
* @param cy the Y coordinate.
* @param name the name of the output.
* @param out the FloatOutput to control.
*/
public FloatControlComponent(int cx, int cy, String name, FloatOutput out) {
this(cx, cy, name, null, out);
}
/**
* Create a new FloatControlComponent, with an input channel to represent
* the actual value as returned by the remote.
*
* @param cx the X coordinate.
* @param cy the Y coordinate.
* @param name the name of the output.
* @param inp the FloatInput to monitor.
* @param out the FloatOutput to control.
*/
public FloatControlComponent(int cx, int cy, String name, FloatInput inp, FloatOutput out) {
super(cx, cy, name);
rawOut = out;
alternateSource = inp;
}
/**
* Create a new FloatControlComponent.
*
* @param cx the X coordinate.
* @param cy the Y coordinate.
* @param name the name of the output.
*/
public FloatControlComponent(int cx, int cy, String name) {
this(cx, cy, name, FloatOutput.ignored);
}
@Override
protected boolean containsForInteract(int x, int y) {
switch (activeView) {
case HORIZONTAL_POINTER:
return x >= centerX - halfWidth + 10 && x <= centerX + halfWidth - 10 && y >= centerY - halfHeight / 2 && y <= centerY + halfHeight / 2;
case TEXTUAL:
return y >= centerY - 5 && y <= centerY + 10;
case TICKER:
for (int i = 0; i < 6; i++) {
if (mouseInBox(i, x, y)) {
return true;
}
}
return false;
default:
return false;
}
}
@Override
public void channelRender(Graphics2D g, int screenWidth, int screenHeight, FontMetrics fontMetrics, int mouseX, int mouseY) {
if (activeView != View.TEXTUAL) {
if (getPanel().editing == activeBuffer) {
getPanel().editing = null;
}
activeBuffer = null;
}
boolean hasValue = alternateSource != null || this.hasSentInitial;
switch (activeView) {
case HORIZONTAL_POINTER:
g.setColor(Color.WHITE);
g.fillRect(centerX - halfWidth + 10, centerY - halfHeight / 2, 2 * halfWidth - 19, halfHeight);
g.setColor(Color.BLACK);
g.drawRect(centerX - halfWidth + 10, centerY - halfHeight / 2, 2 * halfWidth - 20, halfHeight - 1);
g.drawLine(centerX, centerY + halfHeight / 2 - 1, centerX, centerY + 5);
g.drawLine(centerX + halfWidth * 2 / 3, centerY + halfHeight / 2 - 1, centerX + halfWidth * 2 / 3, centerY + 5);
g.drawLine(centerX - halfWidth * 2 / 3, centerY + halfHeight / 2 - 1, centerX - halfWidth * 2 / 3, centerY + 5);
g.drawLine(centerX - halfWidth / 6, centerY + halfHeight / 2 - 1, centerX - halfWidth / 6, centerY + 15);
g.drawLine(centerX + halfWidth / 6, centerY + halfHeight / 2 - 1, centerX + halfWidth / 6, centerY + 15);
g.drawLine(centerX - halfWidth / 3, centerY + halfHeight / 2 - 1, centerX - halfWidth / 3, centerY + 10);
g.drawLine(centerX + halfWidth / 3, centerY + halfHeight / 2 - 1, centerX + halfWidth / 3, centerY + 10);
g.drawLine(centerX - 3 * halfWidth / 6, centerY + halfHeight / 2 - 1, centerX - 3 * halfWidth / 6, centerY + 15);
g.drawLine(centerX + 3 * halfWidth / 6, centerY + halfHeight / 2 - 1, centerX + 3 * halfWidth / 6, centerY + 15);
if (hasValue) {
float value = getDele();
int ptrCtr = (int) (centerX + halfWidth * ((2 * (value - minimum) / (maximum - minimum)) - 1) * 2 / 3);
if (value < 0) {
g.setColor(value == -1 ? Color.RED : Color.RED.darker().darker());
} else if (value > 0) {
g.setColor(value == 1 ? Color.GREEN : Color.GREEN.darker().darker());
} else {
g.setColor(Color.ORANGE);
}
Shape c = g.getClip();
g.setClip(new Rectangle(centerX - halfWidth + 10, centerY - halfHeight / 2, halfWidth * 2 - 20, halfHeight));
g.drawPolygon(new int[] { ptrCtr - 12, ptrCtr - 8, ptrCtr - 12 }, new int[] { centerY - 8, centerY - 4, centerY }, 3);
g.drawPolygon(new int[] { ptrCtr + 12, ptrCtr + 8, ptrCtr + 12 }, new int[] { centerY - 8, centerY - 4, centerY }, 3);
g.fillRect(ptrCtr - 5, centerY - halfHeight / 2 + 1, 11, halfHeight / 2 - 4);
g.fillPolygon(new int[] { ptrCtr - 5, ptrCtr, ptrCtr + 6 }, new int[] { centerY - 3, centerY + 3, centerY - 3 }, 3);
g.setClip(c);
}
break;
case TICKER:
g.setColor(Color.BLACK);
g.setFont(Rendering.labels);
fontMetrics = g.getFontMetrics();
String text = hasValue ? String.format("%.2f", getDele()) : "????";
g.drawString(text, centerX - fontMetrics.stringWidth(text) / 2, centerY + fontMetrics.getDescent());
paintBox(g, fontMetrics, mouseX, mouseY, true, 0);
paintBox(g, fontMetrics, mouseX, mouseY, true, 1);
paintBox(g, fontMetrics, mouseX, mouseY, true, 2);
paintBox(g, fontMetrics, mouseX, mouseY, false, 0);
paintBox(g, fontMetrics, mouseX, mouseY, false, 1);
paintBox(g, fontMetrics, mouseX, mouseY, false, 2);
break;
case TEXTUAL:
g.setFont(Rendering.labels);
String default_ = hasValue ? Float.toString(getDele()) : "?";
if (activeBuffer == null) {
activeBuffer = new StringBuilder(default_);
}
if (getPanel().editing != activeBuffer && !activeBuffer.toString().equals(default_)) {
activeBuffer.setLength(0);
activeBuffer.append(default_);
}
g.setColor(getPanel().editing == activeBuffer ? Color.RED : Color.BLACK);
if (g.getFontMetrics().stringWidth(activeBuffer.toString()) > 2 * halfWidth) {
g.setFont(Rendering.console);
}
g.drawString(activeBuffer.toString(), centerX - g.getFontMetrics().stringWidth(activeBuffer.toString()) / 2, centerY + 5);
break;
}
}
private int[] boundingBoxes;
private boolean mouseInBox(int boxId, int mouseX, int mouseY) {
return boundingBoxes[boxId * 4 + 0] <= mouseX && mouseX < boundingBoxes[boxId * 4 + 1] && boundingBoxes[boxId * 4 + 2] <= mouseY && mouseY < boundingBoxes[boxId * 4 + 3];
}
private void paintBox(Graphics g, FontMetrics fontMetrics, int mouseX, int mouseY, boolean isRight, int rowId) {
int left = isRight ? centerX + halfWidth - fontMetrics.stringWidth("+") : centerX - halfWidth;
int right = left + fontMetrics.stringWidth(isRight ? "+" : "-");
int bottom = centerY + rowId * (fontMetrics.getHeight()) / 2 + (getPanel().editmode ? 0 : -10);
int top = bottom - fontMetrics.getHeight() / 2;
if (boundingBoxes == null) {
boundingBoxes = new int[6 * 4];
}
int boxIndex = rowId + (isRight ? 3 : 0);
boundingBoxes[4 * boxIndex + 0] = left;
boundingBoxes[4 * boxIndex + 1] = right;
boundingBoxes[4 * boxIndex + 2] = top;
boundingBoxes[4 * boxIndex + 3] = bottom;
g.setColor(mouseInBox(boxIndex, mouseX, mouseY) ? Color.BLACK : Color.GRAY);
g.drawString(isRight ? "+" : "-", left, bottom);
}
@Override
public boolean wantsDragSelect() {
return true;
}
@Override
public void onPressedEnter() {
if (activeView == View.TEXTUAL && getPanel().editing == activeBuffer) {
try {
setDele(false, Float.parseFloat(activeBuffer.toString()));
getPanel().editing = null;
} catch (NumberFormatException ex) {
Logger.warning("Could not parse number '" + activeBuffer + "'.");
}
}
}
private float getDele() {
// Checks null in case unserialized from old version
return alternateSource == null ? lastSentValue : alternateSource.get();
}
private void setDele(boolean requireDifferent, float value) {
if (!(requireDifferent && value == getDele() && hasSentInitial)) {
lastSentValue = value;
if (rawOut != null) {
rawOut.safeSet(value);
hasSentInitial = true;
}
}
}
@Override
public boolean onInteract(int x, int y) {
if (isBeingDragged()) {
return false;
}
float value;
switch (activeView) {
case HORIZONTAL_POINTER:
value = (x - centerX - 1) / (halfWidth * 2 / 3f);
// min to max, inclusive
value = minimum + ((value + 1) / 2) * (maximum - minimum);
value = Math.min(maximum, Math.max(minimum, value));
if (-0.1 < value && value < 0.1) {
value = 0;
}
break;
case TEXTUAL:
getPanel().editing = (getPanel().editing == activeBuffer) ? null : activeBuffer;
return true;
case TICKER:
value = getDele();
for (int i = 0; i < 6; i++) {
if (mouseInBox(i, x, y)) {
if (i < 3) {
value -= 0.1 * Math.pow(10, -1 + i);
} else {
value += 0.1 * Math.pow(10, -4 + i);
}
break;
}
}
value = Math.round(value * 100) / 100f;
break;
default:
return false;
}
setDele(true, value);
return true;
}
@Override
public boolean canDragInteract() {
return true;
}
@Override
protected void setDefaultView() {
activeView = View.TEXTUAL;
}
private final FloatOutput fakeOut = new FakeFloatOutput();
private boolean isFakeSubscribed = false;
@Override
protected void onChangePanel(SuperCanvasPanel panel) {
boolean hasPanel = panel != null;
if (alternateSource != null && hasPanel != isFakeSubscribed) {
if (unsubscribe != null) {
unsubscribe.cancel();
unsubscribe = null;
}
if (hasPanel) {
unsubscribe = alternateSource.send(fakeOut);
}
isFakeSubscribed = hasPanel;
}
}
private static final class FakeFloatOutput implements FloatOutput, Serializable {
private static final long serialVersionUID = 8588017785288111886L;
@Override
public void set(float f) {
// Do nothing. This is just so that we can make the remote end send
// us data by subscribing.
}
}
@Override
public Entry[] queryRConf() throws InterruptedException {
return rconfBase(RConf.string("minimum"), RConf.fieldFloat(minimum), RConf.string("maximum"), RConf.fieldFloat(maximum));
}
@Override
public boolean signalRConf(int field, byte[] data) throws InterruptedException {
switch (rconfBase(field, data)) {
case 1:
minimum = RConf.bytesToFloat(data);
return true;
case 3:
maximum = RConf.bytesToFloat(data);
return true;
case BASE_VALID:
return true;
default:
return false;
}
}
}