/* * Copyright 2014-2015 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.frc; import java.awt.Color; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.util.Iterator; import java.util.concurrent.CopyOnWriteArrayList; import javax.swing.JPanel; import ccre.channel.EventOutput; import ccre.log.Logger; import ccre.timers.ExpirationTimer; import ccre.util.Utils; /** * A base display panel used in device tree panels. * * @author skeggsc */ public final class DeviceListPanel extends JPanel implements Iterable<Device> { private static final int COLUMN_SPLIT_THRESHOLD = 900; private static final long serialVersionUID = 3194911460808795658L; /** * The width of the embedded scrollbar. */ private static final int SCROLLBAR_WIDTH = 20; /** * The width of the embedded scrollbar's padding. */ private static final int SCROLLBAR_PADDING = 2; /** * The currently visible list of devices. */ private final CopyOnWriteArrayList<Device> devices = new CopyOnWriteArrayList<Device>(); /** * The most recent position of the mouse. */ private transient int mouseX, mouseY; /** * The current scrolling position. Larger means further down the list. */ private transient int scrollPos, scrollMax; /** * An expiration timer to repaint the pane when appropriate. */ private transient ExpirationTimer painter; /** * The relative position of the currently-dragged scrollbar, or null if not * dragging. */ private transient Float dragPosition; /** * The number of devices that appear in the first column. */ private transient int devicesInFirstColumn; /** * The lines of the currently-displayed error message. */ private transient String[] errorMessageLines = {}; /** * Set the error message currently being displayed. * * @param thr the Throwable to be displayed, or null to display nothing. */ public void setErrorDisplay(Throwable thr) { errorMessageLines = thr == null ? new String[0] : Utils.toStringThrowable(thr).split("\n"); } /** * Add the specified device to this panel. * * @param <E> the type of the added device. * @param comp The device to add. * @return the added device. */ public synchronized <E extends Device> E add(E comp) { comp.setParent(this); devices.add(comp); repaint(); return comp; } /** * Remove the specified device from this panel. * * @param comp The device to remove. */ public synchronized void remove(Device comp) { if (devices.remove(comp)) { comp.setParent(null); 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, new EventOutput() { @Override public void event() { repaint(); } }); painter.start(); } @Override public void paint(Graphics go) { try { boolean splitColumns = shouldColumnsSplit(); Graphics2D g = (Graphics2D) go; g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); int w = getWidth() - SCROLLBAR_WIDTH; int h = getHeight(); g.setFont(Rendering.labels); FontMetrics fontMetrics = g.getFontMetrics(); renderBackground(g, w, h, fontMetrics); int maxColumnDevices = calculateDevicesInFirstColumn(); scrollPos = Math.max(Math.min(scrollPos, scrollMax - h), 0); renderScrollbar(g, w, SCROLLBAR_WIDTH); int yPosition = -scrollPos, xPosition = 0; for (Device comp : devices) { if (splitColumns && maxColumnDevices-- == 0) { yPosition = -scrollPos; // new column, reset y position. xPosition = w / 2; } int deviceHeight = comp.getHeight(); int bottom = yPosition + deviceHeight; if (yPosition >= -deviceHeight && bottom <= h + deviceHeight) { g.setFont(Rendering.labels); g.translate(xPosition, yPosition); Shape clip = g.getClip(); g.setClip(new Rectangle(0, 0, splitColumns ? w / 2 : w, deviceHeight)); comp.render(g, splitColumns ? w / 2 : w, deviceHeight, fontMetrics, mouseX - xPosition, mouseY - yPosition); g.setClip(clip); g.translate(-xPosition, -yPosition); } yPosition = bottom; } if (painter != null && painter.isRunning()) { painter.feed(); } if (painter == null || errorMessageLines.length != 0) { g.setFont(Rendering.error); String[] lines = errorMessageLines.length != 0 ? errorMessageLines : new String[] { "Panel Not Started" }; g.setColor(Color.BLACK); int textHeight = g.getFontMetrics().getHeight() * lines.length; int textWidth = 0; for (String line : lines) { textWidth = Math.max(textWidth, g.getFontMetrics().stringWidth(line)); } int boxTop = h / 2 - textHeight / 2; int yline = boxTop + g.getFontMetrics().getAscent(); g.setColor(Color.BLACK); g.fillRect(w / 2 - textWidth / 2 - 8, boxTop - 8, textWidth + 16, textHeight + 16); g.setColor(Color.WHITE); g.fillRect(w / 2 - textWidth / 2 - 4, boxTop - 4, textWidth + 8, textHeight + 8); g.setColor(Color.BLACK); for (String line : lines) { g.drawString(line, w / 2 - textWidth / 2, yline); yline += g.getFontMetrics().getHeight(); } } } catch (Throwable thr) { Logger.severe("Exception while handling paint event", thr); } } private int calculateDevicesInFirstColumn() { int totalHeight = 0; for (Device comp : devices) { totalHeight += comp.getHeight(); } int calcDevicesInFirstColumn = 0; if (shouldColumnsSplit()) { // Closer together is better, is lower score. int columnA = 0, columnB = totalHeight, lastScore = Math.abs(columnB - columnA); for (Device comp : devices) { int deviceHeight = comp.getHeight(); columnB -= deviceHeight; columnA += deviceHeight; int newscore = Math.abs(columnB - columnA); if (newscore > lastScore) { // it's worse, so cancel. columnB += deviceHeight; columnA -= deviceHeight; break; } lastScore = newscore; calcDevicesInFirstColumn++; } this.scrollMax = Math.max(columnA, columnB); } else { this.scrollMax = totalHeight; } this.devicesInFirstColumn = calcDevicesInFirstColumn; return calcDevicesInFirstColumn; } private boolean shouldColumnsSplit() { return getWidth() > COLUMN_SPLIT_THRESHOLD; } private int scrollbarRange() { return getHeight() - SCROLLBAR_PADDING * 2; } private float positionToScrollbarPosition(float y) { return SCROLLBAR_PADDING + scrollbarRange() * y / scrollMax; } private float scrollbarPositionToPosition(float y) { return (y - SCROLLBAR_PADDING) * scrollMax / scrollbarRange(); } private void renderScrollbar(Graphics2D g, int x, int width) { int height = getHeight(); g.setColor(Color.LIGHT_GRAY); g.fillRect(x, 0, width, height); g.setColor(Color.WHITE); g.drawRect(x, 0, width - 1, height - 1); int scrollbarHeight = Math.min(Math.round(positionToScrollbarPosition(height) - SCROLLBAR_PADDING), getHeight() - SCROLLBAR_PADDING * 2); int position = Math.round(positionToScrollbarPosition(scrollPos)); g.setColor(Color.BLACK); g.fillRect(x + SCROLLBAR_PADDING, position, width - SCROLLBAR_PADDING * 2, scrollbarHeight); g.setColor(Color.GRAY); g.drawRect(x + SCROLLBAR_PADDING, position, width - SCROLLBAR_PADDING * 2 - 1, scrollbarHeight - 1); } private void renderBackground(Graphics2D g, int w, int h, FontMetrics fontMetrics) { g.setColor(Color.WHITE); g.fillRect(0, 0, w, h); } private class SuperCanvasMouseAdapter extends MouseAdapter { SuperCanvasMouseAdapter() { } @Override public void mousePressed(MouseEvent e) { try { if (e.getX() >= getWidth() - SCROLLBAR_WIDTH) { // It's on the scrollbar. dragPosition = scrollbarPositionToPosition(e.getY()) - scrollPos; repaint(); return; } int yPosition = scrollPos + e.getY(); int columnWidth = (getWidth() - SCROLLBAR_WIDTH) / 2; boolean inSecondColumn = shouldColumnsSplit() && e.getX() > columnWidth; int xPosition = inSecondColumn ? e.getX() - columnWidth : e.getX(); int devicesRemainingToSkip = inSecondColumn ? devicesInFirstColumn : 0; for (Device dev : devices) { if (devicesRemainingToSkip-- > 0) { continue; } int deviceHeight = dev.getHeight(); if (yPosition >= 0 && yPosition < deviceHeight) { dev.onPress(xPosition, yPosition); repaint(); } yPosition -= deviceHeight; } } catch (Throwable thr) { Logger.severe("Exception while handling mouse press", thr); } } private void updateDragLocation(int newY) { scrollPos = Math.round(scrollbarPositionToPosition(newY) - dragPosition); repaint(); } @Override public void mouseReleased(MouseEvent e) { try { if (dragPosition != null) { updateDragLocation(e.getY()); dragPosition = null; return; } int yPosition = scrollPos + e.getY(); int columnWidth = (getWidth() - SCROLLBAR_WIDTH) / 2; boolean inSecondColumn = shouldColumnsSplit() && e.getX() > columnWidth; int xPosition = inSecondColumn ? e.getX() - columnWidth : e.getX(); int devicesRemainingToSkip = inSecondColumn ? devicesInFirstColumn : 0; for (Device dev : devices) { if (devicesRemainingToSkip-- > 0) { continue; } int deviceHeight = dev.getHeight(); if (yPosition >= 0 && yPosition < deviceHeight) { dev.onRelease(xPosition, yPosition); repaint(); } yPosition -= deviceHeight; } } catch (Throwable thr) { Logger.severe("Exception while handling mouse release", thr); } } @Override public void mouseWheelMoved(MouseWheelEvent e) { try { scrollPos += e.getWheelRotation(); repaint(); } catch (Throwable thr) { Logger.severe("Exception while handling mouse wheel", thr); } } @Override public void mouseDragged(MouseEvent e) { mouseMoved(e); } @Override public void mouseMoved(MouseEvent e) { try { int oldMouseX = mouseX; int oldMouseY = mouseY; mouseX = e.getX(); mouseY = e.getY(); int columnWidth = shouldColumnsSplit() ? (getWidth() - SCROLLBAR_WIDTH) / 2 : getWidth() - SCROLLBAR_WIDTH; boolean inSecondColumn = shouldColumnsSplit() && e.getX() > columnWidth; int oldXPosition = inSecondColumn ? oldMouseX - columnWidth : oldMouseX; int xPosition = inSecondColumn ? mouseX - columnWidth : mouseX; boolean wasInSelectionArea = oldXPosition >= 0 && oldXPosition < columnWidth; boolean isInSelectionArea = xPosition >= 0 && xPosition < columnWidth; if (dragPosition != null) { updateDragLocation(e.getY()); return; } int yPosition = scrollPos + mouseY; int oldYPosition = scrollPos + oldMouseY; int devicesRemainingToSkip = inSecondColumn ? devicesInFirstColumn : 0; for (Device dev : devices) { if (devicesRemainingToSkip-- > 0) { continue; } int deviceHeight = dev.getHeight(); boolean isIn = yPosition >= 0 && yPosition < deviceHeight && isInSelectionArea; boolean wasIn = oldYPosition >= 0 && oldYPosition < deviceHeight && wasInSelectionArea; if (isIn) { if (wasIn) { dev.onMouseMove(xPosition, yPosition); } else { dev.onMouseEnter(xPosition, yPosition); } } else if (wasIn) { dev.onMouseExit(xPosition, yPosition); } yPosition -= deviceHeight; oldYPosition -= deviceHeight; } } catch (Throwable thr) { Logger.severe("Exception while handling mouse move", thr); } } } @Override public Iterator<Device> iterator() { return devices.iterator(); } }