/*
* Copyright 2014 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;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import ccre.log.LogLevel;
import ccre.log.Logger;
import ccre.log.LoggingTarget;
import ccre.supercanvas.DraggableBoxComponent;
import ccre.supercanvas.Rendering;
import ccre.util.LineCollectorOutputStream;
/**
* A component that displays a scrollable log of recentCCRE logging messages.
*
* @author skeggsc
*/
public class LoggingComponent extends DraggableBoxComponent {
private static final long serialVersionUID = -946247852428245215L;
private transient List<String> lines;
private transient PrintStream pstr;
private transient LoggingTarget tgt;
private transient ResizeState resizeState;
private transient int scroll = 0, maxScroll = 0, clearingThreshold = 0;
private transient boolean isClearing = false;
/**
* Create a new LoggingComponent at the specified position.
*
* @param cx the X-coordinate.
* @param cy the Y-coordinate.
*/
public LoggingComponent(int cx, int cy) {
super(cx, cy);
halfWidth = 300;
halfHeight = 95;
setupLines();
}
private void setupLines() {
resizeState = ResizeState.TRANSLATE;
lines = new ArrayList<String>(100);
this.pstr = new PrintStream(new LineCollectorOutputStream() {
@Override
protected void collect(String param) {
synchronized (LoggingComponent.this) {
lines.add(param);
}
}
});
this.tgt = new LoggingTarget() {
@Override
public synchronized void log(LogLevel level, String message, Throwable thr) {
if (thr != null) {
pstr.println("{" + level.message + "} " + message);
thr.printStackTrace(pstr);
} else {
pstr.println("[" + level.message + "] " + message);
}
}
@Override
public synchronized void log(LogLevel level, String message, String extended) {
pstr.println("[" + level.message + "] " + message);
if (extended != null && !extended.isEmpty()) {
pstr.println(extended);
}
}
};
Logger.addTarget(tgt);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
setupLines();
}
@Override
public void moveForDrag(int x, int y) {
switch (resizeState) {
case CORNER_BR:
halfWidth = x;
halfHeight = y;
break;
case CORNER_UR:
halfWidth = x;
halfHeight = -y;
break;
case CORNER_BL:
halfWidth = -x;
halfHeight = y;
break;
case CORNER_UL:
halfWidth = -x;
halfHeight = -y;
break;
case SCROLL:
scroll = y * -maxScroll / (2 * halfHeight - 24);
constrainScrolling();
return;
default:
super.moveForDrag(x, y);
return;
}
if (halfWidth < 50) {
halfWidth = 50;
}
if (halfHeight < 50) {
halfHeight = 50;
}
}
@Override
public boolean canDrop() {
return resizeState == ResizeState.TRANSLATE;
}
@Override
public int getDragRelX(int x) {
switch (resizeState) {
case CORNER_BR:
case CORNER_UR:
return halfWidth - x;
case CORNER_BL:
case CORNER_UL:
return -halfWidth - x;
case SCROLL:
return 0;
default:
return super.getDragRelX(x);
}
}
@Override
public int getDragRelY(int y) {
switch (resizeState) {
case CORNER_BR:
case CORNER_BL:
return halfHeight - y;
case CORNER_UR:
case CORNER_UL:
return -halfHeight - y;
case SCROLL:
return (int) ((2 * halfHeight - 24) * (scroll / (float) -maxScroll)) - y;
default:
return super.getDragRelY(y);
}
}
@Override
public void render(Graphics2D g, int screenWidth, int screenHeight, FontMetrics fontMetrics, int mouseX, int mouseY) {
drawBackground(g);
Shape originalClippingShape = g.getClip();
int lineCount;
{
g.setClip(new Rectangle(centerX - halfWidth + 16, centerY - halfHeight + 16, halfWidth * 2 - 32, halfHeight * 2 - 32));
lineCount = drawLoggedLines(g, fontMetrics, centerY + halfHeight - 16 - fontMetrics.getDescent() - scroll, centerX - halfWidth + 16, fontMetrics.getHeight());
}
g.setClip(originalClippingShape);
drawScrollbar(lineCount, fontMetrics, g);
if (isClearing) {
drawClearingOverlay(g, fontMetrics, mouseX);
}
}
private void drawBackground(Graphics2D g) {
Rendering.drawBody(Color.LIGHT_GRAY.brighter(), g, this);
g.setColor(Color.BLACK);
if (getPanel().editmode) {
g.drawLine(centerX - halfWidth + 6, centerY - halfHeight + 10, centerX - halfWidth + 10, centerY - halfHeight + 6);
g.drawLine(centerX + halfWidth - 6, centerY - halfHeight + 10, centerX + halfWidth - 10, centerY - halfHeight + 6);
g.drawLine(centerX - halfWidth + 6, centerY + halfHeight - 10, centerX - halfWidth + 10, centerY + halfHeight - 6);
g.drawLine(centerX + halfWidth - 6, centerY + halfHeight - 10, centerX + halfWidth - 10, centerY + halfHeight - 6);
}
}
private void drawScrollbar(int lineCount, FontMetrics fontMetrics, Graphics2D g) {
this.maxScroll = lineCount * fontMetrics.getHeight() - halfHeight;
float frac = scroll / (float) -maxScroll;
if (frac < 0) {
frac = 0;
} else if (frac > 1) {
frac = 1;
}
Rendering.drawScrollbar(g, scroll != 0, centerX - halfWidth + 8, centerY - halfHeight + 12 + (int) ((2 * halfHeight - 24) * frac));
}
private synchronized int drawLoggedLines(Graphics2D g, FontMetrics fontMetrics, int initialYPos, int xPos, int rowHeight) {
int yPos = initialYPos;
ArrayList<String> temp = new ArrayList<String>(lines.size() + lines.size() >> 1);
int lineCount = 0;
for (int i = lines.size() - 1; i >= 0; i--) {
g.setColor(Color.BLACK);
String line = lines.get(i);
if (fontMetrics.stringWidth(line) > halfWidth * 2 - 32) {
temp.clear();
int base = 0;
int linepos = line.length();
while (linepos > base) {
String substr = line.substring(base, linepos);
if (fontMetrics.stringWidth(substr) <= halfWidth * 2 - 32) {
temp.add(substr);
base = linepos;
linepos = line.length() - 1;
} else {
linepos -= 1;
}
}
for (int j = temp.size() - 1; j >= 0; j--) {
g.drawString(temp.get(j), xPos, yPos);
yPos -= rowHeight;
lineCount++;
}
} else {
g.drawString(line, xPos, yPos);
yPos -= rowHeight;
lineCount++;
}
}
return lineCount;
}
private void drawClearingOverlay(Graphics2D g, FontMetrics fontMetrics, int mouseX) {
Composite composite = g.getComposite();
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.7f));
Rendering.drawBody(Color.LIGHT_GRAY, g, this);
g.setComposite(composite);
String display = "Really clear?";
int width = fontMetrics.stringWidth(display);
g.setColor(mouseX < centerX - width / 2 ? Color.RED : Color.GRAY);
g.drawLine(centerX - halfWidth + 12, centerY - halfHeight + 12, centerX - width / 2 - 2, centerY - fontMetrics.getAscent());
g.drawLine(centerX - halfWidth + 12, centerY + halfHeight - 12, centerX - width / 2 - 2, centerY - fontMetrics.getAscent() + fontMetrics.getHeight());
g.setColor(mouseX > centerX + width / 2 ? Color.GREEN.darker() : Color.GRAY);
g.drawLine(centerX + halfWidth - 12, centerY - halfHeight + 12, centerX + width / 2 + 2, centerY - fontMetrics.getAscent());
g.drawLine(centerX + halfWidth - 12, centerY + halfHeight - 12, centerX + width / 2 + 2, centerY - fontMetrics.getAscent() + fontMetrics.getHeight());
g.setColor(Color.BLACK);
clearingThreshold = centerX - width / 2;
g.drawString(display, centerX - width / 2, centerY);
display = "Clear";
g.setColor(Color.RED);
g.drawString(display, centerX - halfWidth / 2 - fontMetrics.stringWidth(display) / 2, centerY);
display = "Keep";
g.setColor(Color.GREEN.darker());
g.drawString(display, centerX + halfWidth / 2 - fontMetrics.stringWidth(display) / 2, centerY);
}
@Override
public boolean onInteract(int x, int y) {
if (isClearing) {
if (x < clearingThreshold) {
lines.clear();
}
isClearing = false;
} else if (x < centerX) {
resizeState = ResizeState.SCROLL;
getPanel().startDrag(this, x, y);
} else {
isClearing = true;
}
return true;
}
@Override
public boolean onSelect(int x, int y) {
if (containsForInteract(x, y)) {
return onInteract(x, y);
}
resizeState = ResizeState.TRANSLATE;
if (x >= centerX + halfWidth - 10) {
if (y >= centerY + halfHeight - 10) {
resizeState = ResizeState.CORNER_BR;
} else if (y <= centerY - halfHeight + 10) {
resizeState = ResizeState.CORNER_UR;
}
} else if (x <= centerX - halfWidth + 10) {
if (y >= centerY + halfHeight - 10) {
resizeState = ResizeState.CORNER_BL;
} else if (y <= centerY - halfHeight + 10) {
resizeState = ResizeState.CORNER_UL;
} else {
resizeState = ResizeState.SCROLL;
}
}
getPanel().startDrag(this, x, y);
return true;
}
@Override
public boolean onScroll(int x, int y, int wheelRotation) {
scroll += wheelRotation;
constrainScrolling();
return true;
}
private void constrainScrolling() {
if (scroll > 0) {
scroll = 0;
} else if (scroll < -maxScroll) {
scroll = -maxScroll;
}
}
@Override
public String toString() {
return lines == null ? "deactivated logging window" : "logging window [" + lines.size() + "]";
}
@Override
public boolean onDelete(boolean forced) {
if (tgt != null) {
Logger.removeTarget(tgt);
tgt = null;
pstr = null;
lines = null;
}
return true;
}
private static enum ResizeState {
TRANSLATE, CORNER_BR, CORNER_UR, CORNER_BL, CORNER_UL, SCROLL
}
}