/******************************************************************************* * Breakout Cave Survey Visualizer * * Copyright (C) 2014 James Edwards * * jedwards8 at fastmail dot fm * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program 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 General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *******************************************************************************/ package org.andork.swing.jump; import java.awt.Adjustable; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Map; import java.util.function.Function; import javax.swing.AbstractButton; import javax.swing.JComponent; import javax.swing.JScrollBar; import javax.swing.ListModel; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import org.andork.awt.layout.Axis; /** * A companion to a scroll bar that draws colored rectangles indicating the * position of specially marked elements in the scrolled content. When the user * clicks a rectangle it will scroll to the corresponding element (just like the * component to the right of the scroll bar in an Eclipse editor pane). If the * user clicks a blank area it will scroll to that relative location in the * content.<br /> * <br /> * {@code JumpBar} can be configured to work with any kind of content with * discrete elements via a corresponding {@link ListModel} implementation. * * @author andy.edwards */ @SuppressWarnings("serial") public class JumpBar extends JComponent { private class ChangeHandler implements ListDataListener { @Override public void contentsChanged(ListDataEvent e) { repaint(); } @Override public void intervalAdded(ListDataEvent e) { repaint(); } @Override public void intervalRemoved(ListDataEvent e) { repaint(); } } /** * Provides {@link JumpBar} the ability to scroll any element of the content * to visible. * * @author andy.edwards */ public static interface JumpSupport { /** * {@link JumpBar} will call this method to scroll a given element of * the content to visible. * * @param index * the index of the element to scroll to visible. */ public void scrollElementToVisible(int index); } private class MouseHandler extends MouseAdapter { @Override public void mouseMoved(MouseEvent e) { Rectangle insetRect = SwingUtilities.calculateInnerArea(JumpBar.this, null); Axis axis = getAxis(); Rectangle track = getScrollBarTrackBounds(); track = SwingUtilities.convertRectangle(scrollBar, track, JumpBar.this); int start = axis.get(e.getPoint()) - markSize / 2; int end = start + markSize; Cursor cursor = null; if (insetRect.contains(e.getPoint()) && model != null) { start = Math.max(axis.lower(track), start); end = Math.min(axis.upper(track), end); int startIndex = (start - axis.lower(track)) * model.getSize() / axis.size(track); int endIndex = (end - axis.lower(track)) * model.getSize() / axis.size(track); endIndex = Math.min(endIndex, model.getSize() - 1); for (int index = startIndex; index <= endIndex; index++) { if (getColor(index) != null) { cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); break; } } } setCursor(cursor); } @Override public void mousePressed(MouseEvent e) { Axis axis = getAxis(); Rectangle track = getScrollBarTrackBounds(); track = SwingUtilities.convertRectangle(scrollBar, track, JumpBar.this); if (jumpSupport != null && model != null) { Rectangle insetRect = SwingUtilities.calculateInnerArea(JumpBar.this, null); if (insetRect.contains(e.getPoint())) { int mid = axis.get(e.getPoint()); int start = mid - markSize / 2; int end = start + markSize; start = Math.max(axis.lower(track), start); end = Math.min(axis.upper(track), end); int startIndex = (start - axis.lower(track)) * model.getSize() / axis.size(track); int midIndex = (mid - axis.lower(track)) * model.getSize() / axis.size(track); int endIndex = (end - axis.lower(track)) * model.getSize() / axis.size(track); endIndex = Math.min(endIndex, model.getSize() - 1); for (int i = 0; i <= Math.max(endIndex - midIndex, midIndex - startIndex); i = i < 0 ? -i : -i - 1) { int index = midIndex + i; if (index >= 0 && index < model.getSize()) { if (getColor(index) != null) { jumpSupport.scrollElementToVisible(index); return; } } } } } scrollBar.setValue(Math.max(scrollBar.getMinimum(), Math.min(scrollBar.getMaximum(), (axis.get(e.getPoint()) - axis.lower(track)) * (scrollBar.getMaximum() - scrollBar.getMinimum()) / axis.size(track) - scrollBar.getVisibleAmount() / 2))); } } /** * */ private static final long serialVersionUID = 3211864660337552967L; JScrollBar scrollBar; ListModel model; JumpSupport jumpSupport; int markSize = 5; MouseHandler mouseHandler = new MouseHandler(); ChangeHandler changeHandler = new ChangeHandler(); Function<Object, Color> colorer; public JumpBar(JScrollBar scrollBar) { this(scrollBar, null, null); } public JumpBar(JScrollBar scrollBar, ListModel model) { this(scrollBar, model, null); } public JumpBar(JScrollBar scrollBar, ListModel model, JumpSupport jumpSupport) { super(); this.scrollBar = scrollBar; this.model = model; this.jumpSupport = jumpSupport; setBorder(new EmptyBorder(2, 2, 2, 2)); addMouseListener(mouseHandler); addMouseMotionListener(mouseHandler); } private Axis getAxis() { Axis axis = scrollBar.getOrientation() == Adjustable.VERTICAL ? Axis.Y : Axis.X; return axis; } protected Color getColor(int index) { Object o = model.getElementAt(index); if (o == null) { return null; } if (o instanceof Color) { return (Color) o; } if (colorer != null) { return colorer.apply(o); } return null; } private Color getDarkerColor(Color c, float darkness, int alpha) { float[] hsb = new float[4]; Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), hsb); hsb[2] = Math.max(0, hsb[2] - darkness); Color result = Color.getHSBColor(hsb[0], hsb[1], hsb[2]); return new Color(result.getRed(), result.getGreen(), result.getBlue(), alpha); } public JumpSupport getJumpSupport() { return jumpSupport; } @Override public Dimension getMinimumSize() { if (!isMinimumSizeSet()) { return scrollBar.getMinimumSize(); } return super.getMinimumSize(); } public ListModel getModel() { return model; } @Override public Dimension getPreferredSize() { if (!isPreferredSizeSet()) { return scrollBar.getPreferredSize(); } return super.getPreferredSize(); } public Rectangle getScrollBarTrackBounds() { Axis axis = getAxis(); AbstractButton decrButton = null, incrButton = null; for (Component comp : scrollBar.getComponents()) { if (comp instanceof AbstractButton) { if (decrButton == null) { decrButton = (AbstractButton) comp; } else if (incrButton == null) { incrButton = (AbstractButton) comp; break; } } } if (decrButton == null || incrButton == null) { return SwingUtilities.calculateInnerArea(scrollBar, null); } if (axis.lower(decrButton) > axis.lower(incrButton)) { AbstractButton swap = decrButton; decrButton = incrButton; incrButton = swap; } Point p; p = axis.upperSide().center(decrButton.getBounds()); p = SwingUtilities.convertPoint(scrollBar, p, this); int start = axis.get(p) + 1; p = axis.lowerSide().center(incrButton.getBounds()); p = SwingUtilities.convertPoint(scrollBar, p, this); int end = axis.get(p) - 1; Rectangle r = SwingUtilities.calculateInnerArea(scrollBar, null); axis.setLower(r, start); axis.setUpper(r, end); return r; } @Override protected void paintComponent(Graphics g) { if (model == null) { return; } Insets insets = getInsets(); Graphics2D g2 = (Graphics2D) g; Axis axis = getAxis(); Rectangle track = getScrollBarTrackBounds(); if (track == null) { return; } track = SwingUtilities.convertRectangle(scrollBar, track, this); int start = axis.lower(track); int span = axis.size(track); for (int index = 0; index < model.getSize(); index++) { Color markColor = getColor(index); if (markColor == null) { continue; } int markStart = start + index * span / model.getSize(); while (index + 1 < model.getSize() && markColor.equals(getColor(index + 1))) { index++; } int markEnd = start + (index + 1) * span / model.getSize(); int remaining = markSize + markStart - markEnd; if (remaining > 0) { markStart -= remaining / 2; markEnd = markStart + markSize; } g2.setColor(new Color(markColor.getRed(), markColor.getGreen(), markColor.getBlue(), 64)); int x = axis == Axis.Y ? insets.left : markStart; int y = axis == Axis.Y ? markStart : insets.top; int width = axis == Axis.Y ? getWidth() - insets.right - insets.left : markEnd - markStart; int height = axis == Axis.Y ? markEnd - markStart : getHeight() - insets.top - insets.bottom; g2.fillRect(x, y, width, height); g2.setColor(getDarkerColor(markColor, 0.3f, 128)); g2.drawRect(x, y, width, height); } } public void setColorer(Function<Object, Color> colorer) { this.colorer = colorer; } public void setColorMap(Map<?, Color> colorMap) { colorer = colorMap == null ? null : colorMap::get; } public void setJumpSupport(JumpSupport jumpSupport) { this.jumpSupport = jumpSupport; } public void setModel(ListModel model) { if (this.model != model) { if (this.model != null) { this.model.removeListDataListener(changeHandler); } this.model = model; if (this.model != null) { this.model.addListDataListener(changeHandler); } repaint(); } } }