package lcm.logging;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.event.*;
import java.util.*;
public class JScrubber extends JComponent
{
static final int BARHEIGHT = 5;
static final int KNOBSIZE = 10;
static final int MARGIN = 10;
static final int MIN_HEIGHT = KNOBSIZE + 2*MARGIN + BARHEIGHT*5;
static final int BOOKMARK_HEIGHT = 15, BOOKMARK_WIDTH = 6;
static final int CLICK_CLOSENESS = 5;
static final int REPEAT_DOT_SIZE = 4;
double position = 0.5;
double zoomfrac = 0.1;
double zoom0, zoom1; // positions (0,1) representing the left and right end of the zoom scrubber
int cy, cy2;
int lastDrawX = -1, lastDrawX2 = -1; // used to eliminate extra redraws
ArrayList<JScrubberListener> listeners = new ArrayList<JScrubberListener>();
boolean inhibitGeometryChanges = false;
JPopupMenu popupMenu = new JPopupMenu();
double popupPosition;
int mouseDownRow = 0;
public static final int BOOKMARK_PLAIN = 0, BOOKMARK_LREPEAT = 1, BOOKMARK_RREPEAT = 2;
class Bookmark
{
double position;
int type;
Bookmark(double p, int type)
{
this.position = p;
this.type = type;
}
}
ArrayList<Bookmark> bookmarks = new ArrayList<Bookmark>();
public JScrubber()
{
MyMouseAdapter ma = new MyMouseAdapter();
addMouseListener(ma);
addMouseMotionListener(ma);
popupMenu.add(new PopupAction("Bookmark", BOOKMARK_PLAIN));
popupMenu.add(new PopupAction("Right repeat :]", BOOKMARK_RREPEAT));
popupMenu.add(new PopupAction("Left repeat [:", BOOKMARK_LREPEAT));
popupMenu.addSeparator();
int zooms[] = new int[] {10, 20, 50, 100, 200, 500, 1000};
ButtonGroup group = new ButtonGroup();
for (int i = 0; i < zooms.length; i++) {
JRadioButtonMenuItem jmi = new JRadioButtonMenuItem("Zoom "+zooms[i]+"x");
jmi.addActionListener(new ZoomAction("foo", zooms[i]));
group.add(jmi);
popupMenu.add(jmi);
if (i==3) {
jmi.setSelected(true);
zoomfrac = 1.0 / zooms[i];
}
}
popupMenu.addSeparator();
popupMenu.add(new ExportAction());
}
public void clearBookmarks()
{
bookmarks = new ArrayList<Bookmark>();
repaint();
}
public void addBookmark(int type, double position)
{
bookmarks.add(new Bookmark(position, type));
repaint();
}
public ArrayList<Bookmark> getBookmarks()
{
return bookmarks;
}
class ExportAction extends AbstractAction
{
public ExportAction()
{
super("Export log snippet...");
}
public void actionPerformed(ActionEvent e)
{
Bookmark b0 = new Bookmark(0, BOOKMARK_PLAIN);
Bookmark b1 = new Bookmark(1, BOOKMARK_PLAIN);
if (bookmarks.size() == 0) {
System.out.println("didn't find bookmark region");
return;
} else if (bookmarks.size() == 1){
//if there is only one bookmark, use from beginning/end to it
Bookmark b = bookmarks.get(0);
if (position > b.position)
b0 = b;
else
b1 = b;
} else if (bookmarks.size() == 2) {
// if there are only two bookmarks, just use those.
b0 = bookmarks.get(0);
b1 = bookmarks.get(1);
if (b0.position>b1.position){ //b0 should be before b1
Bookmark swp = b0;
b0 = b1;
b1 = b0;
}
} else {
// find previous and next book marks.
for (Bookmark b : bookmarks) {
if (b.position < position && (b0 == null || b0.position < b.position))
b0 = b;
if (b.position > position && (b1 == null || b1.position > b.position))
b1 = b;
}
}
for (JScrubberListener jsl : listeners)
jsl.scrubberExportRegion(JScrubber.this, b0.position, b1.position);
}
}
class ZoomAction extends AbstractAction
{
int zoom;
public ZoomAction(String name, int zoom)
{
super(name);
this.zoom = zoom;
}
public void actionPerformed(ActionEvent e)
{
setZoomFraction(1.0/zoom);
}
}
class PopupAction extends AbstractAction
{
int op;
public PopupAction(String name, int op)
{
super(name);
this.op = op;
}
public void actionPerformed(ActionEvent e)
{
bookmarks.add(new Bookmark(popupPosition, op));
repaint();
}
}
public double getZoomFraction()
{
return zoomfrac;
}
public void setZoomFraction(double f)
{
zoomfrac = f;
updateGeometry();
repaint();
}
public void addScrubberListener(JScrubberListener l)
{
listeners.add(l);
}
public Dimension getMinimumSize()
{
return new Dimension(100, MIN_HEIGHT);
}
public Dimension getPreferredSize()
{
Dimension d = super.getPreferredSize();
return new Dimension((int) d.getWidth(), (int) Math.max(d.getHeight(), MIN_HEIGHT));
}
// convert a position to an X coordinate for the main scrubber
int getX(double v)
{
return (int) (MARGIN + v*(getWidth()-MARGIN*2));
}
// convert a position to an X coordinate for the zoom scrubber
int getX2(double v)
{
return getX( (v-zoom0)/(zoom1-zoom0) );
}
// convert an X coordinate to a position for the main scrubber
double getPosition(int x)
{
double pos = ((double) x - MARGIN)/(getWidth() - 2*MARGIN);
if (pos < 0)
pos = 0;
if (pos > 1)
pos = 1;
return pos;
}
// convert an X coordinate to a position for the zoomed-in scrubber
double getPosition2(int x)
{
double pos = ((double) x - MARGIN)/(getWidth() - 2*MARGIN);
if (pos < 0)
return zoom0;
if (pos > 1)
return zoom1;
double pos2 = pos * (zoom1-zoom0) + zoom0;
return pos2;
}
int getRow(int x, int y)
{
return y > ((cy + cy2)/2) ? 1 : 0;
}
double getPosition(int x, int y)
{
double position;
if (getRow(x, y) == 0)
position = getPosition(x);
else
position = getPosition2(x);
return position;
}
/**
* Get position for a mouse click on a particular row
* Set row = 1 for the zoomed in slider.
*/
double getPosition(int x, int y, int row) {
if (row == 1) {
return getPosition2(x);
} else {
return getPosition(x);
}
}
void updateGeometry()
{
if (inhibitGeometryChanges)
return;
if (position < zoomfrac/2) {
zoom0 = 0;
zoom1 = zoomfrac;
} else if (position > 1-zoomfrac/2) {
zoom0 = 1-zoomfrac;
zoom1 = 1;
} else {
zoom0 = position-zoomfrac/2;
zoom1 = position+zoomfrac/2;
}
cy = getHeight()/3;
cy2 = cy + BARHEIGHT*4;
}
void drawBookmark(Graphics g, Bookmark b, int x, int cy)
{
g.setColor(new Color(0, 200, 0));
g.fillRect(x-BOOKMARK_WIDTH/2, cy - BOOKMARK_HEIGHT/2, BOOKMARK_WIDTH, BOOKMARK_HEIGHT);
g.setColor(new Color(0, 100, 0));
g.drawRect(x-BOOKMARK_WIDTH/2, cy - BOOKMARK_HEIGHT/2, BOOKMARK_WIDTH, BOOKMARK_HEIGHT);
if (b.type == BOOKMARK_LREPEAT) {
g.setColor(new Color(0, 50, 0));
g.fillRect(x + BOOKMARK_WIDTH/2, cy - BOOKMARK_HEIGHT/2, REPEAT_DOT_SIZE, REPEAT_DOT_SIZE);
g.fillRect(x + BOOKMARK_WIDTH/2, cy + BOOKMARK_HEIGHT/2 - REPEAT_DOT_SIZE, REPEAT_DOT_SIZE, REPEAT_DOT_SIZE);
}
if (b.type == BOOKMARK_RREPEAT) {
g.setColor(new Color(0, 50, 0));
g.fillRect(x - BOOKMARK_WIDTH/2 - REPEAT_DOT_SIZE, cy - BOOKMARK_HEIGHT/2, REPEAT_DOT_SIZE, REPEAT_DOT_SIZE);
g.fillRect(x - BOOKMARK_WIDTH/2 - REPEAT_DOT_SIZE, cy + BOOKMARK_HEIGHT/2 - REPEAT_DOT_SIZE, REPEAT_DOT_SIZE, REPEAT_DOT_SIZE);
}
}
public void paint(Graphics g)
{
Graphics2D g2d = (Graphics2D) g;
int width = getWidth(), height = getHeight();
int margin = 4;
int barheight = 10;
g.setColor(getBackground());
g.fillRect(0,0, width, height);
double position2 = (position - zoom0)/zoomfrac;
// draw the trapezoid between the two bars
g.setColor(Color.lightGray);
GeneralPath gp = new GeneralPath();
gp.moveTo(getX(zoom0), cy + BARHEIGHT/2+1);
gp.lineTo(getX(zoom1), cy + BARHEIGHT/2+1);
gp.lineTo(getX(1.0), cy2 - BARHEIGHT/2);
gp.lineTo(getX(0.0), cy2 - BARHEIGHT/2);
gp.closePath();
// g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.fill(gp);
// draw the bar
g.setColor(new Color(150, 150, 255));
g.fillRect(MARGIN, cy - BARHEIGHT/2, width - 2*MARGIN, BARHEIGHT);
g.setColor(Color.blue);
g.drawRect(MARGIN, cy - BARHEIGHT/2, width - 2*MARGIN, BARHEIGHT);
g.setColor(new Color(150, 255, 255));
g.fillRect(MARGIN, cy2 - BARHEIGHT/2, width - 2*MARGIN, BARHEIGHT);
g.setColor(Color.blue);
g.drawRect(MARGIN, cy2 - BARHEIGHT/2, width - 2*MARGIN, BARHEIGHT);
g.setColor(Color.darkGray);
g.fillRect(getX(zoom0), cy - BARHEIGHT*1/3, getX(zoom1)-getX(zoom0), BARHEIGHT);
for (Bookmark b : bookmarks)
{
drawBookmark(g, b, getX(b.position), cy);
if (b.position >= zoom0 && b.position <= zoom1)
drawBookmark(g, b, getX2(b.position), cy2);
}
// draw the knob
g.setColor(Color.yellow);
g.fillOval(getX(position)-KNOBSIZE/2, cy - KNOBSIZE/2, KNOBSIZE, KNOBSIZE);
g.setColor(Color.black);
g.drawOval(getX(position)-KNOBSIZE/2, cy - KNOBSIZE/2, KNOBSIZE, KNOBSIZE);
g.setColor(Color.yellow);
g.fillOval(getX2(position)-KNOBSIZE/2, cy2 - KNOBSIZE/2, KNOBSIZE, KNOBSIZE);
g.setColor(Color.black);
g.drawOval(getX2(position)-KNOBSIZE/2, cy2 - KNOBSIZE/2, KNOBSIZE, KNOBSIZE);
lastDrawX = getX(position);
lastDrawX2 = getX2(position);
}
void userSet(double newpos)
{
position = newpos;
for (JScrubberListener l : listeners)
l.scrubberMovedByUser(this, position);
updateGeometry();
repaint();
}
synchronized public void set(double pos)
{
double oldpos = this.position;
this.position = pos;
updateGeometry();
// now we've updated geometry, maybe we don't need to draw.
if (Math.abs(getX(position) - lastDrawX) > 1 || Math.abs(getX2(position) - lastDrawX2) > 1)
repaint();
for (Bookmark b : bookmarks)
{
if (b.type == BOOKMARK_RREPEAT && b.position > oldpos && b.position <= pos)
{
Bookmark lrepeat = null;
// find most recent LREPEAT
for (Bookmark bl : bookmarks)
{
if (bl.position < b.position && bl.type == BOOKMARK_LREPEAT && (lrepeat == null || bl.position > lrepeat.position))
lrepeat = bl;
}
double lposition = (lrepeat == null) ? 0 : lrepeat.position;
for (JScrubberListener l : listeners)
{
l.scrubberPassedRepeat(this, b.position, lposition);
}
}
}
}
class MyMouseAdapter extends MouseInputAdapter
{
Bookmark trackbookmark;
Bookmark findBookmark(double position, double tolerance)
{
double minerr = tolerance;
Bookmark best = null;
for (Bookmark b : bookmarks)
{
double thiserr = Math.abs(position - b.position);
if (thiserr < minerr)
{
best = b;
minerr = thiserr;
}
}
return best;
}
public void mousePressed(MouseEvent e)
{
mouseDownRow = getRow(e.getX(), e.getY());
double position = getPosition(e.getX(), e.getY(), mouseDownRow);
double tolerance = getPosition(e.getX()+CLICK_CLOSENESS, e.getY(), mouseDownRow) - position;
if (e.getButton()==3)
{
trackbookmark = findBookmark(position, tolerance);
}
}
public void mouseReleased(MouseEvent e)
{
if (trackbookmark != null)
{
if (trackbookmark.position == 0 || trackbookmark.position == 1)
{
bookmarks.remove(trackbookmark);
repaint();
}
trackbookmark = null;
}
inhibitGeometryChanges = false;
}
public void mouseClicked(MouseEvent e)
{
int mods=e.getModifiersEx();
boolean shift = (mods&MouseEvent.SHIFT_DOWN_MASK)>0;
boolean ctrl = (mods&MouseEvent.CTRL_DOWN_MASK)>0;
boolean alt = shift & ctrl;
ctrl = ctrl & (!alt);
shift = shift & (!alt);
boolean nomods = !(shift | ctrl | alt);
double tolerance = getPosition(e.getX()+CLICK_CLOSENESS, e.getY()) - position;
double position = getPosition(e.getX(), e.getY());
if (e.getButton()==1)
{
Bookmark nearest = findBookmark(position, tolerance);
if (nearest == null)
userSet(position);
else
userSet(nearest.position);
}
if (e.getButton()==3)
{
popupPosition = position;
popupMenu.show(JScrubber.this, e.getX(), e.getY());
/*
int err = Math.abs(e.getX() - getX(position));
// if they clicked near the current cursor, create
// a bookmark exactly where the cursor is
int type = BOOKMARK_PLAIN;
if (shift)
type = BOOKMARK_LREPEAT;
if (ctrl)
type = BOOKMARK_RREPEAT;
if (err < CLICK_CLOSENESS)
bookmarks.add(new Bookmark(position, type));
else
bookmarks.add(new Bookmark(position, type));
repaint();
*/
}
}
public void mouseDragged(MouseEvent e)
{
double position = getPosition(e.getX(), e.getY(), mouseDownRow);
if (mouseDownRow == 1)
inhibitGeometryChanges = true;
else
inhibitGeometryChanges = false;
if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0)
{
userSet(position);
}
if ((e.getModifiers() & MouseEvent.BUTTON3_MASK) != 0)
{
if (trackbookmark != null)
{
trackbookmark.position = position;
repaint();
}
}
}
}
}