import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.border.*; import javax.swing.plaf.TextUI; import javax.swing.event.*; import javax.swing.text.*; /** * A ruler that shows the tabs for the current paragraph, as well as allowing * the user to manipulate the tabs. * * @author Scott Violet */ public class Ruler extends JPanel implements CaretListener { // Some defines used in painting the ruler. protected static final int DPI = 72; protected static final int H_DPI = DPI / 2; protected static final int Q_DPI = DPI / 4; protected static final int E_DPI = DPI / 8; // Sizes for drawing tabs. protected static final int TabSize = 2; protected static final int TabWidth = 6; protected static final int TabHeight = 4; /** Shared instance of default border. */ protected static final Border DefaultBorder = new RulerBorder(); /** TextPane showing tabs for. */ private JTextPane textPane; /** Current TabSet showing. */ private TabSet tabs; /** Current paragraph element at character position. */ private Element paragraph; /** Offset to start drawing tabs from. */ private int xOffset; /** If false, the value of xOffset is not valid. */ private boolean validOffset; /** Font using for the units. */ private Font unitsFont; /** Total font height. */ private int fontHeight; /** Font ascent. */ private int fontAscent; public Ruler() { setBackground(Color.white); MouseInputListener ml = createMouseInputListener(); if (ml != null) { addMouseListener(ml); addMouseMotionListener(ml); } Border border = createBorder(); if (border != null) { setBorder(border); } } public Ruler(JTextPane text) { this(); setTextPane(text); } /** * Sets the text pane tabs are rendered for. */ public void setTextPane(JTextPane text) { if (textPane != null) { textPane.removeCaretListener(this); } textPane = text; if (text != null) { text.addCaretListener(this); updateTabSet(text.getSelectionStart()); } else { updateTabSet(0); } } /** * Gets the text pane tabs are being rendered for. */ public JTextPane getTextPane() { return textPane; } /** * Called when the caret position is updated. * * @param e the caret event */ public void caretUpdate(CaretEvent e) { updateTabSet(Math.min(e.getDot(), e.getMark())); } /** * Resets the TabSet, which determines what to display, to be * the TabSet in the Paragraph Element at charPosition. */ protected void updateTabSet(int charPosition) { JTextPane text = getTextPane(); TabSet newTabs; if (text != null) { Element pe = text.getStyledDocument(). getParagraphElement(charPosition); if (pe != paragraph) { Integer newOffset = determineOffset(pe); paragraph = pe; newTabs = StyleConstants.getTabSet(pe.getAttributes()); if (newOffset == null) { validOffset = false; } else if (newOffset.intValue() != xOffset) { validOffset = true; xOffset = newOffset.intValue(); if (tabs == newTabs) { repaint(); } } } else { newTabs = tabs; } } else { newTabs = null; } if (tabs != newTabs) { tabs = newTabs; repaint(); } } /** * Sets the tabs the receiver represents, forces a repaint. */ protected void setTabSet(TabSet tabs) { this.tabs = tabs; repaint(); } /** * Returns the current TabSet, which may be null. */ protected TabSet getTabSet() { return tabs; } /** * Returns the offset, along the x axis, tabs are to start from. */ protected int getXOffset() { if (!validOffset && getParagraphElement() != null) { Integer offset = determineOffset(getParagraphElement()); if (offset != null) { xOffset = offset.intValue(); validOffset = true; // Force a complete repaint. repaint(); } else { // Not valid offset, return 0. return 0; } } return xOffset; } /** * Returns the current paragraph element. If the selection extends * across multiple paragraphs this will return the first paragraph. */ protected Element getParagraphElement() { return paragraph; } // // Painting methods // /** * Messaged to paint the Component, will fill the background and * message paintUnits and paintTabs. */ protected void paintComponent(Graphics g) { Rectangle clip = g.getClipBounds(); Insets insets = getInsets(); updateFontIfNecessary(); g.setColor(getBackground()); g.fillRect(clip.x, clip.y, clip.width, clip.height); paintUnits(g, clip, insets); paintTabs(g, clip, insets); } /** * Paints the unit indicators. */ protected void paintUnits(Graphics g, Rectangle clip, Insets insets) { int xOffset = getXOffset(); int fontY = getUnitsFontAscent(); int midY = getUnitsFontHeight() / 2; int dpiOffset = xOffset % DPI; if (insets != null) { fontY += insets.top; midY += insets.top; } g.setColor(getUnitsColor()); g.setFont(getUnitsFont()); FontMetrics fm = g.getFontMetrics(); for (int x = Math.max(xOffset, clip.x / DPI * DPI + dpiOffset), maxX = clip.x + clip.width; x <= maxX; x += E_DPI) { int tempX = x - dpiOffset; if (tempX % DPI == 0) { String numString = Integer.toString((x - xOffset) / DPI); g.drawString(numString, x - fm.stringWidth(numString) / 2, fontY); } else if (tempX % H_DPI == 0) { g.drawLine(x, midY - 3, x, midY + 3); } else if (tempX % Q_DPI == 0) { g.drawLine(x, midY - 2, x, midY + 2); } else { g.drawLine(x, midY - 1, x, midY + 1); } } } /** * Paints the tabs. */ protected void paintTabs(Graphics g, Rectangle clip, Insets insets) { int xOffset = getXOffset(); TabSet tabs = getTabSet(); int lastX = clip.x - 10; int maxX = clip.x + clip.width + 10; int maxY = getUnitsFontHeight() + TabHeight; if (insets != null) { maxY += insets.top; } if (tabs == null) { g.setColor(getSynthesizedTabColor()); // Paragraph treats a null tabset as a tab at every 72 pixels. // Different implementations of View used to represent a // Paragraph may not due this. lastX = Math.max(xOffset, lastX / DPI * DPI + xOffset % DPI); while (lastX <= maxX) { paintTab(g, clip, null, (float)lastX, maxY, TabStop.ALIGN_LEFT, TabStop.LEAD_NONE); lastX += DPI; } } else { TabStop tab; g.setColor(getTabColor()); do { tab = tabs.getTabAfter((float)lastX + .01f); if (tab != null) { lastX = (int)tab.getPosition() + xOffset; if (lastX <= maxX) { paintTab(g, clip, tab, (float)lastX, maxY, tab.getAlignment(), tab.getLeader()); } else { tab = null; } } } while (tab != null); } } /** * Paints a particular tab. tab may be null, indicating * a synthesized tab is being painted. */ protected void paintTab(Graphics g, Rectangle clip, TabStop tab, float position, int maxY, int alignment, int leader) { int iPos = (int)position; switch (alignment) { case TabStop.ALIGN_LEFT: g.fillRect(iPos, maxY - TabHeight, TabSize, TabHeight); g.fillRect(iPos, maxY - TabSize, TabWidth + TabSize, TabSize); break; case TabStop.ALIGN_RIGHT: g.fillRect(iPos, maxY - TabHeight, TabSize, TabHeight); g.fillRect(iPos - TabWidth, maxY - TabSize, TabWidth, TabSize); break; case TabStop.ALIGN_DECIMAL: g.fillRect(iPos, maxY - TabHeight - TabSize - 2, TabSize, TabSize); case TabStop.ALIGN_CENTER: g.fillRect(iPos, maxY - TabHeight, TabSize, TabHeight); g.fillRect(iPos - TabWidth, maxY - TabSize, TabWidth * 2 + TabSize, TabSize); break; default: break; } } /** * Returns the color to use for the units and ticks. */ protected Color getUnitsColor() { return Color.black; } /** * Returns the Font to use for the units. Override this to specify a * different font. */ protected Font getUnitsFont() { return getFont(); } /** * Returns the color to draw the actual tabs in. */ protected Color getTabColor() { return Color.black; } /** * Returns the color to draw generated tabs in (tabs are generated if * there is no TabSet set on a particular Element). */ protected Color getSynthesizedTabColor() { return Color.lightGray; } // // Component methods // public Dimension getPreferredSize() { updateFontIfNecessary(); Insets insets = getInsets(); if (insets != null) { return new Dimension(insets.left + insets.right + 10, insets.top + insets.bottom + getUnitsFontHeight() + TabHeight); } return new Dimension(10, getUnitsFontHeight()); } public Dimension getMinimumSize() { return getPreferredSize(); } public Dimension getMaximumSize() { return getPreferredSize(); } /** * The ascent of the units font. */ protected int getUnitsFontAscent() { return fontAscent; } /** * Returns the height of the tray. */ protected int getUnitsFontHeight() { return fontHeight; } /** * Updates font height information. */ private void updateFontIfNecessary() { Font font = getUnitsFont(); if (unitsFont != font) { fontHeight = fontAscent = 0; if (font != null) { Toolkit tk = getToolkit(); if (tk != null) { FontMetrics fm = tk.getFontMetrics(font); if (fm != null) { fontHeight = fm.getHeight(); fontAscent = fm.getAscent(); unitsFont = font; } } } } } /** * Determines the offset (along the x axis) from which tabs are to begin. * This is obtained from the bounds of the View that represents * paragraph. A return value of null indicates the offset * could not be obtained. */ protected Integer determineOffset(Element paragraph) { JTextPane text = getTextPane(); if (text != null) { // This is a workaround to avoid a NullPointerException that // is fixed in post swing 1.1 (JDK1.2). try { if (text.modelToView(paragraph.getStartOffset()) == null) { return null; } } catch (BadLocationException ble) { return null; } // This assumes the views are layed out sequentially. Insets insets = text.getInsets(); Rectangle alloc = new Rectangle(text.getSize()); TextUI ui = text.getUI(); View view = ui.getRootView(text); int offset = paragraph.getStartOffset(); alloc.x += insets.left; alloc.y += insets.top; alloc.width -= insets.left + insets.right; alloc.height -= insets.top + insets.bottom; Shape bounds = alloc; while (view != null && view.getElement() != paragraph) { int nchildren = view.getViewCount(); int index; int lower = 0; int upper = nchildren - 1; int mid = 0; int p0 = view.getStartOffset(); int p1; if (nchildren == 0 || offset >= view.getEndOffset() || offset < view.getStartOffset()) { view = null; } else { boolean found = false; while (lower <= upper) { mid = lower + ((upper - lower) / 2); View v = view.getView(mid); p0 = v.getStartOffset(); p1 = v.getEndOffset(); if ((offset >= p0) && (offset < p1)) { // found the location found = true; bounds = view.getChildAllocation(mid, bounds); view = v; lower = upper + 1; } else if (offset < p0) { upper = mid - 1; } else { lower = mid + 1; } } if (!found) { view = null; } } } if (view != null && bounds != null) { return new Integer(bounds.getBounds().x); } } return null; } /** * Returns the TabStop closest to the passed in location. This * may return null, or this may return a synthesized tab if there are * currently no tabs and the location is close to a synthesized tab. */ protected TabStop getTabClosestTo(int xLocation, int yLocation) { TabSet tabs = getTabSet(); xLocation -= getXOffset(); float xFloat = (float)xLocation; if (tabs == null) { if (xLocation % DPI <= 5) { return new TabStop(xLocation / DPI * DPI); } } else { for (int counter = tabs.getTabCount() - 1; counter >= 0; counter--) { TabStop tab = tabs.getTab(counter); switch (tab.getAlignment()) { case TabStop.ALIGN_LEFT: if (xFloat >= tab.getPosition() && xFloat <= (tab.getPosition() + TabWidth + 2)) { return tab; } break; case TabStop.ALIGN_RIGHT: if (xFloat <= tab.getPosition() && xFloat >= (tab.getPosition() - TabWidth)) { return tab; } break; case TabStop.ALIGN_CENTER: case TabStop.ALIGN_DECIMAL: if (xFloat >= (tab.getPosition() - TabWidth) && xFloat <= (tab.getPosition() + TabWidth)) { return tab; } break; default: break; } } } return null; } /** * Creates and returns the listener to use for moving around tabs. */ protected MouseInputListener createMouseInputListener() { return new MouseInputHandler(); } /** * Returns the default border to use. */ protected Border createBorder() { return DefaultBorder; } /** * Draws a little border around the Ruler. */ protected static class RulerBorder implements Border { protected static final Insets DefaultInsets = new Insets(2, 0, 4, 0); /** * Paints the border for the specified component with the specified * position and size. * @param c the component for which this border is being painted * @param g the paint graphics * @param x the x position of the painted border * @param y the y position of the painted border * @param width the width of the painted border * @param height the height of the painted border */ public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { g.setColor(Color.darkGray); g.drawLine(x, y + 1, x + width, y + 1); g.setColor(Color.lightGray); g.drawLine(x, y, x + width, y); g.fillRect(x, y + height - 3, width, 2); } /** * Returns the insets of the border. * @param c the component for which this border insets value applies */ public Insets getBorderInsets(Component c) { return (Insets)DefaultInsets.clone(); } /** * Returns whether or not the border is opaque. If the border * is opaque, it is responsible for filling in it's own * background when painting. */ public boolean isBorderOpaque() { return false; } } /** * MouseInputHandler is responsible for receiving mouse events and * translating that into adjusting the TabSet. A mouse down on an * existing tab allows the user to move that tab around, if the * shift key is held down on the initial click the type of tab will * change to the next type of alignment (cycling through left, right, * centered, and decimal). A mouse down not near an exising tab causes * a new tab to be created. A tab can be removed by dragging it outside * the bounds of the Ruler. */ protected class MouseInputHandler extends MouseInputAdapter { /** The tab the user is dragging, non null indicates * a valid tab has been selected. */ protected TabStop tab; /** Original TabSet. */ protected TabSet originalTabs; /** Tab user clicked on. Null indicates the user is creating a * new tab. */ protected TabStop originalTab; /** While the mouse is down this will be true. */ protected boolean dragging; /** Specifies the alignment passed into createTabStop. */ protected int newAlignment; /** * Invoked when a mouse button has been pressed on a component. */ public void mousePressed(MouseEvent e) { dragging = true; originalTabs = getTabSet(); originalTab = getTabClosestTo(e.getX(), e.getY()); newAlignment = TabStop.ALIGN_LEFT; if (originalTab == null) { tab = createTabStop(e.getX(), e.getY(), newAlignment); resetTabs(); } else { tab = originalTab; if ((e.getModifiers() & InputEvent.SHIFT_MASK) != 0) { // Shift mask changes the alignment of the tab. switch(tab.getAlignment()) { case TabStop.ALIGN_LEFT: newAlignment = TabStop.ALIGN_RIGHT; break; case TabStop.ALIGN_RIGHT: newAlignment = TabStop.ALIGN_CENTER; break; case TabStop.ALIGN_CENTER: newAlignment = TabStop.ALIGN_DECIMAL; break; default: newAlignment = TabStop.ALIGN_LEFT; } tab = new TabStop(tab.getPosition(), newAlignment, tab.getLeader()); resetTabs(); } else { newAlignment = tab.getAlignment(); } } } /** * Invoked when a mouse button has been released on a component. */ public void mouseReleased(MouseEvent e) { dragging = false; // Push the tabs to the text pane (we may not need to do this // if the tabs haven't changed). TabSet tabs = getTabSet(); if (tabs == null) { SimpleAttributeSet sas = new SimpleAttributeSet (getParagraphElement().getAttributes()); sas.removeAttribute(StyleConstants.TabSet); getTextPane().setParagraphAttributes(sas, true); } else { SimpleAttributeSet sas = new SimpleAttributeSet(); StyleConstants.setTabSet(sas, tabs); getTextPane().setParagraphAttributes(sas, false); } } /** * Invoked when a mouse button is pressed on a component and then * dragged. Mouse drag events will continue to be delivered to * the component where the first originated until the mouse button is * released (regardless of whether the mouse position is within the * bounds of the component). */ public void mouseDragged(MouseEvent e) { if (dragging) { TabStop newTab = createTabStop(e.getX(), e.getY(), newAlignment); // Workaround for TabStop.equals not handling null being // passed in. if (newTab != tab && ((newTab != null && tab != null && !newTab.equals(tab)) || (newTab == null || tab == null))) { tab = newTab; resetTabs(); } } } /** * Creates a new TabSet and messages setTabSet. */ protected void resetTabs() { TabStop[] stops; if (tab == null) { // The tab has been moved off screen, indicating we should // remove it. if (originalTab != null && originalTabs != null) { int tabCount = originalTabs.getTabCount(); if (tabCount > 1) { stops = new TabStop[tabCount - 1]; for (int counter = 0, index = 0; counter < tabCount; counter++) { TabStop tab = originalTabs.getTab(counter); if (tab != originalTab) { stops[index++] = tab; } } setTabSet(new TabSet(stops)); } else { setTabSet(null); } } else { setTabSet(originalTabs); } return; } if (originalTabs == null) { // No starting TabSet, create a new one. stops = new TabStop[1]; stops[0] = tab; } else if (originalTab == null) { // Adding a new tab. int numTabs = originalTabs.getTabCount(); int nextIndex = originalTabs.getTabIndexAfter (tab.getPosition()); stops = new TabStop[numTabs + 1]; if (nextIndex == -1) { for (int counter = 0; counter < numTabs; counter++) { stops[counter] = originalTabs.getTab(counter); } stops[numTabs] = tab; } else { for (int counter = 0; counter < nextIndex; counter++) { stops[counter] = originalTabs.getTab(counter); } stops[nextIndex] = tab; for (int counter = nextIndex; counter < numTabs; counter++) { stops[counter + 1] = originalTabs.getTab(counter); } } } else { // Moving an existing tab. int numTabs = originalTabs.getTabCount(); int nextIndex = originalTabs.getTabIndexAfter (tab.getPosition()); int index = 0; stops = new TabStop[numTabs]; if (nextIndex == -1) { for (int counter = 0; counter < numTabs; counter++) { stops[index] = originalTabs.getTab(counter); if (stops[index] != originalTab) { index++; } } stops[index] = tab; } else { for (int counter = 0; counter < nextIndex; counter++) { stops[index] = originalTabs.getTab(counter); if (stops[index] != originalTab) { index++; } } stops[index++] = tab; for (int counter = nextIndex; counter < numTabs && index < numTabs; counter++) { stops[index] = originalTabs.getTab(counter); if (stops[index] != originalTab) { index++; } } } } if (stops != null) { setTabSet(new TabSet(stops)); } else { setTabSet(null); } } /** * Creates a tab stop at the passed in visual location. This should * be offset from any margins. */ protected TabStop createTabStop(int x, int y, int alignment) { if (x < 0 || x > getBounds().width || y < 0 || y > getBounds().height) { return null; } // Constrain the x to 1/8 of an inch. x = (x - getXOffset()) / E_DPI * E_DPI; return new TabStop((float)x, alignment, TabStop.LEAD_NONE); } } }