/*
* 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;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.ListIterator;
import javax.swing.JPanel;
import ccre.cluck.Cluck;
import ccre.log.Logger;
import ccre.timers.ExpirationTimer;
/**
* A base display panel used in tree-and-canvas panels. "SuperCanvas" means that
* it's a superset of the canvas space.
*
* @author skeggsc
*/
public final class SuperCanvasPanel extends JPanel {
private static final long serialVersionUID = 7927046605855742517L;
/**
* The currently held component.
*/
private transient SuperCanvasComponent activeEntity = null;
/**
* The currently visible list of components.
*/
private final LinkedList<SuperCanvasComponent> components = new LinkedList<SuperCanvasComponent>();
/**
* The components currently being hovered over by the mouse.
*/
private transient final LinkedHashSet<SuperCanvasComponent> mouseOver = new LinkedHashSet<SuperCanvasComponent>(4);
/**
* The relative position between the cursor and the currently held
* component.
*/
private transient int relActiveX, relActiveY;
/**
* The most recent position of the mouse.
*/
private transient int mouseX, mouseY;
/**
* An expiration timer to repaint the pane when appropriate.
*/
private transient ExpirationTimer painter;
/**
* The current string being edited, if any.
*/
public StringBuilder editing = null;
/**
* Is this canvas currently in edit mode.
*/
public boolean editmode = true;
/**
* Add the specified component to this panel.
*
* @param comp The component to add.
*/
public synchronized void add(SuperCanvasComponent comp) {
comp.setPanel(this);
components.add(comp);
sortComponents();
repaint();
}
/**
* Raise the specified component to the top of the display stack.
*
* @param comp the component to raise.
*/
public synchronized void raise(SuperCanvasComponent comp) {
if (components.remove(comp)) {
components.add(comp);
sortComponents();
repaint();
}
}
private void sortComponents() {
Collections.sort(components, new Comparator<SuperCanvasComponent>() {
@Override
public int compare(SuperCanvasComponent o1, SuperCanvasComponent o2) {
return Integer.compare(o1.zIndex, o2.zIndex);
}
});
}
/**
* Remove the specified component from this panel.
*
* @param comp The component to remove.
*/
public synchronized void remove(SuperCanvasComponent comp) {
comp.unsetPanel(this);
if (components.remove(comp)) {
if (comp == activeEntity) {
activeEntity = null;
}
mouseOver.remove(comp);
repaint();
}
}
/**
* Start the IntelligenceMain instance so that it runs.
*/
public void start() {
MouseAdapter listener = new SuperCanvasMouseAdapter();
this.addMouseWheelListener(listener);
this.addMouseListener(listener);
this.addMouseMotionListener(listener);
painter = new ExpirationTimer();
painter.schedule(100, () -> repaint());
painter.start();
}
@Override
public void paint(Graphics go) {
try {
Graphics2D g = (Graphics2D) go;
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
int w = getWidth();
int h = getHeight();
g.setFont(Rendering.console);
FontMetrics fontMetrics = g.getFontMetrics();
renderBackground(g, w, h, fontMetrics);
for (SuperCanvasComponent comp : components) {
if (editmode || !comp.hideInOperateMode) {
g.setFont(Rendering.console);
comp.render(g, w, h, fontMetrics, mouseX, mouseY);
}
}
if (painter != null) {
painter.feed();
} else {
String navail = "Panel Not Started";
g.setColor(Color.BLACK);
g.drawString(navail, w / 2 - fontMetrics.stringWidth(navail) / 2, h / 2 - fontMetrics.getHeight() / 2);
}
} catch (Throwable thr) {
Logger.severe("Exception while handling paint event", thr);
}
}
/**
* Start a drag operation on the specified component. This involves asking
* it for its drag offsets and beginning the drag.
*
* @param component the component to drag.
* @param x the mouse X coordinate.
* @param y the mouse Y coordinate.
* @see SuperCanvasComponent#getDragRelX(int)
* @see SuperCanvasComponent#getDragRelY(int)
*/
public void startDrag(SuperCanvasComponent component, int x, int y) {
activeEntity = component;
relActiveX = component.getDragRelX(x);
relActiveY = component.getDragRelY(y);
}
/**
* Check if the given object is being dragged.
*
* @param component the component to check for dragging.
*
* @return if the specified object is being dragged.
*/
public boolean isBeingDragged(SuperCanvasComponent component) {
return activeEntity == component;
}
/**
* Called to notify components that enter has been pressed, for example to
* finish text input.
*/
public void pressedEnter() {
for (ListIterator<SuperCanvasComponent> it = components.listIterator(components.size()); it.hasPrevious();) {
SuperCanvasComponent comp = it.previous();
if (editmode || !comp.hideInOperateMode) {
comp.onPressedEnter();
}
}
}
private void renderBackground(Graphics2D g, int w, int h, FontMetrics fontMetrics) {
if (editmode) {
g.setColor(Color.BLACK);
} else {
g.setColor(Color.WHITE);
}
g.fillRect(0, 0, w, h);
}
/**
* Save the layout of this panel to the specified ObjectOutputStream.
*
* @param out the stream to write to.
* @throws IOException if the contents cannot be saved.
*/
public void save(ObjectOutputStream out) throws IOException {
out.writeObject(components);
}
/**
* Load the layout of this panel from the specified ObjectInputStream. This
* deletes all of the current contents of the panel!
*
* @param in the stream to read from.
* @throws IOException if the contents cannot be loaded.
* @throws java.lang.ClassNotFoundException if the contents cannot be
* loaded.
*/
@SuppressWarnings("unchecked")
public void load(ObjectInputStream in) throws IOException, ClassNotFoundException {
// Remove components
for (Iterator<SuperCanvasComponent> it = components.iterator(); it.hasNext();) {
SuperCanvasComponent comp = it.next();
comp.unsetPanel(this);
comp.onDelete(true);
it.remove();
}
activeEntity = null;
mouseOver.clear();
// Load new components
components.addAll((Collection<? extends SuperCanvasComponent>) in.readObject());
for (SuperCanvasComponent comp : components) {
comp.setPanel(this);
}
Cluck.getNode().notifyNetworkModified();
repaint();
}
/**
* Check if any components of the specified component type.
*
* @param componentType the type of component to find.
* @return if any components were found.
*/
public boolean containsAny(Class<? extends SuperCanvasComponent> componentType) {
for (SuperCanvasComponent comp : components) {
if (componentType.isAssignableFrom(comp.getClass())) {
return true;
}
}
return false;
}
/**
* Get the first of any components of the specified component type.
*
* @param <T> the type of the expected component.
* @param componentType the type of component to find.
* @return the component, if found, otherwise null.
*/
public <T extends SuperCanvasComponent> T getAny(Class<T> componentType) {
for (SuperCanvasComponent comp : components) {
if (componentType.isAssignableFrom(comp.getClass())) {
return componentType.cast(comp);
}
}
return null;
}
/**
* (Safely) remove all instances of the specified class (or any subclass)
* present in this panel.
*
* @param componentType the type of component to remove.
* @return if any components were removed by this operation.
*/
public boolean removeAll(Class<? extends SuperCanvasComponent> componentType) {
boolean any = false;
for (Iterator<SuperCanvasComponent> it = components.iterator(); it.hasNext();) {
SuperCanvasComponent comp = it.next();
if (componentType.isAssignableFrom(comp.getClass())) {
comp.unsetPanel(this);
comp.onDelete(true);
it.remove();
if (comp == activeEntity) {
activeEntity = null;
}
mouseOver.remove(comp);
any = true;
}
}
return any;
}
private class SuperCanvasMouseAdapter extends MouseAdapter {
SuperCanvasMouseAdapter() {
}
@Override
public void mousePressed(MouseEvent e) {
try {
if (!editmode ^ (e.getButton() == MouseEvent.BUTTON3)) {
for (ListIterator<SuperCanvasComponent> it = components.listIterator(components.size()); it.hasPrevious();) {
SuperCanvasComponent comp = it.previous();
if (editmode || !comp.hideInOperateMode) {
if (comp.contains(e.getX(), e.getY())) {
if (comp.onInteract(e.getX(), e.getY())) {
break;
}
}
}
}
} else {
for (ListIterator<SuperCanvasComponent> it = components.listIterator(components.size()); it.hasPrevious();) {
SuperCanvasComponent comp = it.previous();
if (editmode || !comp.hideInOperateMode) {
if (comp.contains(e.getX(), e.getY())) {
if (comp.onSelect(e.getX(), e.getY())) {
raise(comp);
break;
}
}
}
}
}
} catch (Throwable thr) {
Logger.severe("Exception while handling mouse press", thr);
}
}
@Override
public void mouseReleased(MouseEvent e) {
try {
if (activeEntity != null) {
for (ListIterator<SuperCanvasComponent> it = components.listIterator(components.size()); it.hasPrevious();) {
SuperCanvasComponent comp = it.previous();
if (editmode || !comp.hideInOperateMode) {
if (comp != activeEntity && comp.contains(e.getX(), e.getY())) {
if (activeEntity.canDrop() && comp.onReceiveDrop(e.getX(), e.getY(), activeEntity)) {
break;
}
}
}
}
activeEntity = null;
}
} catch (Throwable thr) {
Logger.severe("Exception while handling mouse release", thr);
}
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
try {
for (ListIterator<SuperCanvasComponent> it = components.listIterator(components.size()); it.hasPrevious();) {
SuperCanvasComponent comp = it.previous();
if (editmode || !comp.hideInOperateMode) {
if (comp.contains(e.getX(), e.getY())) {
if (comp.onScroll(e.getX(), e.getY(), e.getWheelRotation())) {
break;
}
}
}
}
repaint();
} catch (Throwable thr) {
Logger.severe("Exception while handling mouse wheel", thr);
}
}
@Override
public void mouseDragged(MouseEvent e) {
try {
mouseX = e.getX();
mouseY = e.getY();
if (activeEntity != null) {
dragToMove(e);
} else if (!editmode) {
dragToInteract();
} else {
dragToSelect(e);
}
repaint();
} catch (Throwable thr) {
Logger.severe("Exception while handling mouse drag", thr);
}
}
private void dragToSelect(MouseEvent e) {
for (ListIterator<SuperCanvasComponent> it = components.listIterator(components.size()); it.hasPrevious();) {
SuperCanvasComponent comp = it.previous();
if (editmode || !comp.hideInOperateMode) {
if (comp.wantsDragSelect() && comp.contains(e.getX(), e.getY())) {
if (comp.onSelect(e.getX(), e.getY())) {
break;
}
}
}
}
}
private void dragToMove(MouseEvent e) {
int gx = e.getX(), gy = e.getY();
if (gx < 5) {
gx = 5;
} else if (gx > getWidth() - 5) {
gx = getWidth() - 5;
}
if (gy < 5) {
gy = 5;
} else if (gy > getHeight() - 5) {
gy = getHeight() - 5;
}
activeEntity.moveForDrag(relActiveX + gx, relActiveY + gy);
}
private void dragToInteract() {
for (ListIterator<SuperCanvasComponent> it = components.listIterator(components.size()); it.hasPrevious();) {
SuperCanvasComponent comp = it.previous();
if (editmode || !comp.hideInOperateMode) {
if (comp.contains(mouseX, mouseY)) {
if (comp.canDragInteract() && comp.onInteract(mouseX, mouseY)) {
break;
}
}
}
}
}
@Override
public void mouseMoved(MouseEvent e) {
try {
boolean mod = false;
mouseX = e.getX();
mouseY = e.getY();
for (SuperCanvasComponent comp : components) {
if (editmode || !comp.hideInOperateMode) {
if (comp.contains(e.getX(), e.getY())) {
if (mouseOver.add(comp)) {
mod |= comp.onMouseEnter(e.getX(), e.getY());
} else {
mod |= comp.onMouseMove(e.getX(), e.getY());
}
} else {
if (mouseOver.remove(comp)) {
mod |= comp.onMouseExit(e.getX(), e.getY());
}
}
}
}
if (mod) {
repaint();
}
} catch (Throwable thr) {
Logger.severe("Exception while handling mouse move", thr);
}
}
}
}