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(); } } } } }