/*
* 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.Graphics2D;
import java.util.Objects;
import javax.swing.JOptionPane;
import ccre.concurrency.CollapsingWorkerThread;
import ccre.log.Logger;
import ccre.rconf.RConf;
import ccre.rconf.RConfable;
import ccre.supercanvas.DraggableBoxComponent;
import ccre.supercanvas.Rendering;
import ccre.supercanvas.SuperCanvasComponent;
import ccre.supercanvas.SuperCanvasPanel;
import ccre.supercanvas.components.palette.NetworkPaletteComponent;
import ccre.timers.Ticker;
import ccre.util.Utils;
/**
* A SuperCanvas-based component to allow interaction with RConf data.
*
* @author skeggsc
*/
public class RConfComponent extends DraggableBoxComponent {
@SuppressWarnings("serial")
private final class UpdatingWorker extends CollapsingWorkerThread {
private UpdatingWorker() {
super("RConf-Updater");
}
@Override
protected void doWork() throws Throwable {
RConf.Entry[] out = device.queryRConf();
if (out == null) {
lastSignalSucceeded = false;
consecutiveUpdateFailures++;
// refresh it because the additional failures may have made it
// slower.
setAutoRefreshDelay(autoRefreshDelay);
} else {
lastSignalSucceeded = true;
entries = out;
consecutiveUpdateFailures = 0;
}
showSignalSuccessUntil = System.currentTimeMillis() + SIGNAL_SUCCESS_FLASH_TIME;
}
}
@SuppressWarnings("serial")
private final class SignalingWorker extends CollapsingWorkerThread {
private SignalingWorker() {
super("RConf-Signaler");
}
private int signalField = -1;
private byte[] signalPayload;
private long lastSent = 0;
@Override
protected void doWork() throws Throwable {
int field;
byte[] payload;
synchronized (this) {
field = signalField;
payload = signalPayload;
signalPayload = null;
}
if (payload == null) {
return;
}
lastSignalSucceeded = device.signalRConf(field, payload);
showSignalSuccessUntil = System.currentTimeMillis() + SIGNAL_SUCCESS_FLASH_TIME;
getUpdater().trigger();
}
public synchronized void signal(int field, byte[] payload) {
lastSent = System.currentTimeMillis();
signalField = field;
signalPayload = payload;
trigger();
}
public synchronized boolean recentlySent(int field, int within) {
return signalField == field && System.currentTimeMillis() - lastSent < within;
}
}
private static final long serialVersionUID = 6222627208004874042L;
private static final Color bodyColor = Color.ORANGE;
private static final Color successColor = Color.GREEN;
private static final Color failureColor = Color.RED;
private static final int SIGNAL_SUCCESS_FLASH_TIME = 500;
private RConf.Entry[] entries = new RConf.Entry[0];
/**
* The RConfable accessed by this RConfComponent.
*/
protected final RConfable device;
private boolean lastSignalSucceeded = false;
private long showSignalSuccessUntil = 0;
private transient int consecutiveUpdateFailures = 0;
private transient SignalingWorker signaler;
private transient UpdatingWorker updater;
private String path;
private transient Integer autoRefreshDelay = null;
private transient Ticker autoRefreshTicker = null;
@Override
protected synchronized void onChangePanel(SuperCanvasPanel newPanel) {
if (newPanel == null) {
if (signaler != null) {
signaler.terminate();
signaler = null;
}
if (updater != null) {
updater.terminate();
updater = null;
}
setAutoRefreshDelay(null);
}
}
/**
* Create a new RConfComponent at the given location with a specified
* device.
*
* @param cx the X position.
* @param cy the Y position.
* @param path the path to this component
* @param device the device to interact with.
*/
public RConfComponent(int cx, int cy, String path, RConfable device) {
super(cx, cy);
this.path = path;
this.device = device;
getUpdater().trigger();
halfWidth = 100;
halfHeight = 20;
}
private synchronized void setAutoRefreshDelay(Integer delay) {
if (delay != null && delay < 10000) {
delay = (int) Math.min(Math.round(delay * Math.pow(2, consecutiveUpdateFailures / 5f)), 10000);
}
if (delay != null && delay < 10) {
delay = 10;
}
if (Objects.equals(delay, autoRefreshDelay)) {
return;
}
autoRefreshDelay = delay;
if (autoRefreshTicker != null) {
autoRefreshTicker.terminate();
autoRefreshTicker = null;
}
if (delay != null) {
autoRefreshTicker = new Ticker(delay);
autoRefreshTicker.send(() -> {
if (getPanel() == null) {
setAutoRefreshDelay(null);
} else {
getUpdater().trigger();
}
});
}
}
@Override
public void render(Graphics2D g, int screenWidth, int screenHeight, FontMetrics fontMetrics, int mouseX, int mouseY) {
Rendering.drawBody(Rendering.blend(bodyColor, lastSignalSucceeded ? successColor : failureColor, (showSignalSuccessUntil - System.currentTimeMillis()) / (float) SIGNAL_SUCCESS_FLASH_TIME), g, this);
halfHeight = 10 * (entries.length + 1) + 5;
g.setColor(Color.BLACK);
int curY = centerY - halfHeight + 5;
{
boolean asTitle = true;
for (RConf.Entry e : entries) {
if (e.type == RConf.F_TITLE) {
asTitle = false;
break;
}
}
g.setFont(asTitle ? Rendering.midlabels : Rendering.console);
String str = getUpdater().isDoingWork() ? "loading..." : this.path;
int hw = g.getFontMetrics().stringWidth(str) / 2;
if (hw + 10 > halfWidth) {
halfWidth = hw + 10;
}
g.drawString(str, centerX - hw, curY + (asTitle ? 20 : 15));
curY += 20;
}
int field = 0;
Integer newAutoRefreshDelay = null; // don't refresh by default
for (RConf.Entry e : entries) {
String str;
int textShift = 15;
g.setFont(Rendering.console);
if (e.type == RConf.F_TITLE) {
g.setFont(Rendering.midlabels);
textShift = 15;
}
switch (e.type) {
case RConf.F_TITLE:
String title = e.parseTextual();
str = title == null ? "<invalid:bad-title>" : title;
break;
case RConf.F_BOOLEAN:
Boolean b = e.parseBoolean();
str = b == null ? "<invalid:bad-bool>" : "FALSE <- [" + b.toString() + "] -> TRUE ";
break;
case RConf.F_BUTTON:
String label = e.parseTextual();
g.setColor(signaler != null && signaler.recentlySent(field, 500) ? Color.GREEN : Color.RED);
int wlabel = g.getFontMetrics().stringWidth(label) + 20;
g.fillRect(centerX - wlabel / 2, curY + 1, wlabel, 18);
g.setColor(Color.BLACK);
g.drawRect(centerX - wlabel / 2, curY + 1, wlabel, 18);
str = label == null ? "<invalid:bad-label>" : label;
break;
case RConf.F_AUTO_REFRESH:
field++;
newAutoRefreshDelay = e.parseInteger();
continue;
default:
str = e.toString();
break;
}
int hw = g.getFontMetrics().stringWidth(str) / 2;
if (hw + 20 > halfWidth) {
halfWidth = hw + 20;
}
g.drawString(str, centerX - hw, curY + textShift);
curY += 20;
field++;
}
setAutoRefreshDelay(newAutoRefreshDelay);
if (getPanel().editmode) {
g.setColor(new Color(255, 0, 0, 128));
g.fillOval(centerX + halfWidth - 10, centerY + halfHeight - 10, 8, 8);
}
}
@Override
public boolean onSelect(int x, int y) {
return checkDelete(x, y) || super.onSelect(x, y);
}
private boolean checkDelete(int x, int y) {
if (!getPanel().editmode || centerY + halfHeight - 10 > y || y > centerY + halfHeight - 2 || centerX + halfWidth - 10 > x || x > centerX + halfWidth - 2) {
return false;
}
if (this.onDelete(false)) {
getPanel().remove(this);
} else {
Logger.warning("Component deletion disallowed: " + this);
}
return true;
}
@Override
protected boolean containsForInteract(int x, int y) {
int relY = y - centerY + halfHeight - 5;
if (relY >= 20) {
for (RConf.Entry e : entries) {
if (e.type == RConf.F_AUTO_REFRESH) {
continue;
}
relY -= 20;
if (relY < 20) {
return e.type == RConf.F_BUTTON || e.type == RConf.F_CLUCK_REF;
}
}
}
return false;
}
@Override
public boolean onInteract(int x, int y) {
if (checkDelete(x, y)) {
return true;
}
int relY = y - centerY + halfHeight - 5;
if (relY < 20) {
getUpdater().trigger();
} else {
int field = 0;
for (RConf.Entry e : entries) {
if (e.type == RConf.F_AUTO_REFRESH) {
field++;
continue;
}
relY -= 20;
if (relY < 20) {
byte[] payload = null;
switch (e.type) {
case RConf.F_BOOLEAN:
payload = new byte[] { (byte) (x < centerX ? 0 : 1) };
break;
case RConf.F_INTEGER:
Integer oldInt = e.parseInteger();
String asked = JOptionPane.showInputDialog("Enter integer", oldInt == null ? "0" : Integer.toString(oldInt));
int newInt;
if (asked == null) {
break;
}
try {
newInt = Integer.parseInt(asked);
} catch (NumberFormatException ex) {
break;
}
payload = new byte[] { (byte) (newInt >> 24), (byte) (newInt >> 16), (byte) (newInt >> 8), (byte) newInt };
break;
case RConf.F_FLOAT:
Float oldFloat = e.parseFloat();
asked = JOptionPane.showInputDialog("Enter float", oldFloat == null ? "0" : Float.toString(oldFloat));
float newFloat;
if (asked == null) {
break;
}
try {
newFloat = Float.parseFloat(asked);
} catch (NumberFormatException ex) {
break;
}
int intBits = Float.floatToIntBits(newFloat);
payload = new byte[] { (byte) (intBits >> 24), (byte) (intBits >> 16), (byte) (intBits >> 8), (byte) intBits };
break;
case RConf.F_STRING:
String oldString = e.parseTextual();
asked = JOptionPane.showInputDialog("Enter string", oldString == null ? "" : oldString);
if (asked != null) {
payload = Utils.getBytes(asked);
}
break;
case RConf.F_CLUCK_REF:
String rawRef = e.parseTextual();
if (rawRef != null) {
NetworkPaletteComponent comp = getPanel().getAny(NetworkPaletteComponent.class);
if (comp == null) {
Logger.warning("A network palette must be available in order to drag out anything from RConf components!");
} else {
String ref = path.contains("/") ? path.substring(0, path.lastIndexOf('/') + 1) + rawRef : rawRef;
SuperCanvasComponent nent = comp.getComponentFor(x, y, ref);
if (nent == null) {
Logger.warning("No network entry could be found for reference: " + ref);
} else {
getPanel().add(nent);
getPanel().startDrag(nent, x, y);
}
}
}
break;
case RConf.F_AUTO_REFRESH:
break;
default:
payload = new byte[0];
}
if (payload != null) {
if (signaler == null) {
signaler = new SignalingWorker();
}
signaler.signal(field, payload);
}
break;
}
field++;
}
}
return true;
}
@Override
public String toString() {
return "RConf Access";
}
private synchronized UpdatingWorker getUpdater() {
if (updater == null) {
updater = new UpdatingWorker();
}
return updater;
}
}