/* ******************************************************************************
*
* Copyright 2008-2010 Hans Dijkema
*
* JRichTextEditor 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.
*
* JRichTextEditor 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 JRichTextEditor. If not, see <http://www.gnu.org/licenses/>.
*
* ******************************************************************************/
package nl.dykema.jxmlnote.widgets;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.border.Border;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.MouseInputAdapter;
import javax.swing.event.MouseInputListener;
import javax.swing.plaf.TextUI;
import javax.swing.text.Element;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.TabSet;
import javax.swing.text.TabStop;
import javax.swing.text.View;
import nl.dykema.jxmlnote.document.XMLNoteDocument;
import nl.dykema.jxmlnote.interfaces.UpdateViewListener;
import nl.dykema.jxmlnote.utils.DPIAdjuster;
/**
* 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 JXMLNoteRuler extends JPanel implements CaretListener, UpdateViewListener {
/**
* Version
*/
private static final long serialVersionUID = 1L;
// Some defines used in painting the ruler.
protected int DPI = DPIAdjuster.getScreenDPI();
protected int H_DPI = DPI / 2;
protected int Q_DPI = DPI / 4;
protected 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 JXMLNoteRuler() {
{
JTextField f=new JTextField();
setBackground(f.getBackground());
}
//MouseInputListener ml = createMouseInputListener();
//if (ml != null) {
// addMouseListener(ml);
// addMouseMotionListener(ml);
//}
Border border = createBorder();
if (border != null) {
setBorder(border);
}
}
public JXMLNoteRuler(JTextPane text) {
this();
setTextPane(text);
}
/**
* Sets the text pane tabs are rendered for.
*/
public void setTextPane(JTextPane text) {
if (textPane != null) {
textPane.removeCaretListener(this);
if (textPane instanceof JXMLNotePane) {
((JXMLNotePane) textPane).removeUpdateViewListener(this);
}
}
textPane = text;
if (text != null) {
text.addCaretListener(this);
if (textPane instanceof JXMLNotePane) {
((JXMLNotePane) textPane).addUpdateViewListener(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 <code>charPosition</code>.
*/
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;
double zoom=1.0f;
if (getTextPane() instanceof JXMLNotePane) {
zoom=((JXMLNotePane) getTextPane()).getZoomFactor();
}
if (insets != null) {
fontY += insets.top;
midY += insets.top;
}
g.setColor(getUnitsColor());
g.setFont(getUnitsFont());
FontMetrics fm = g.getFontMetrics();
int nDPI=(int) Math.round(zoom*DPI);
if ((nDPI%2)==1) { nDPI+=1; }
int nE_DPI=nDPI/8;
int nH_DPI=nDPI/2;
int nQ_DPI=nDPI/4;
for (int x = Math.max(xOffset, clip.x / nDPI * nDPI + dpiOffset),
maxX = clip.x + clip.width; x <= maxX; x += nE_DPI) {
int tempX = x - dpiOffset;
if (tempX % nDPI == 0) {
String numString = Integer.toString((x - xOffset) / nDPI);
g.drawString(numString, x -
fm.stringWidth(numString) / 2, fontY);
}
else if (tempX % nH_DPI == 0) {
g.drawLine(x, midY - 3, x, midY + 3);
}
else if (tempX % nQ_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;
double zoom=1.0f;
if (getTextPane() instanceof JXMLNotePane) {
zoom=((JXMLNotePane) getTextPane()).getZoomFactor();
}
if (insets != null) {
maxY += insets.top;
}
int nDPI=(int) Math.round(zoom*DPI);
if ((nDPI%2)==1) { nDPI+=1; }
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 / nDPI * nDPI + xOffset % nDPI);
while (lastX <= maxX) {
paintTab(g, clip, null, (float)lastX, maxY,
TabStop.ALIGN_LEFT, TabStop.LEAD_NONE);
lastX += DPI;
}
}
else {
TabStop tab;
TabStop []_tabs=new TabStop[tabs.getTabCount()];
int i,N;
for(i=0,N=tabs.getTabCount();i<N;i++) {
TabStop st=tabs.getTab(i);
_tabs[i]=new TabStop((int) Math.round(st.getPosition()*zoom),st.getAlignment(),st.getLeader());
}
TabSet ttabs=new TabSet(_tabs);
g.setColor(getTabColor());
do {
tab = ttabs.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. <code>tab</code> 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
* <code>paragraph</code>. 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
// *** THIS WORKAROUND MADE THIS CODE THROW A NULLPOINTEREXCEPTION!
// 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();
int parIndent=(int) StyleConstants.getLeftIndent(paragraph.getAttributes());
alloc.x += insets.left;
alloc.x += parIndent;
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);
}
}
public void updateView(DocumentEvent e) {
if (e instanceof RulerRepaintEvent) {
this.repaint();
} else {
JTextPane pane=getTextPane();
if (pane instanceof JXMLNotePane) {
XMLNoteDocument doc=((JXMLNotePane) pane).getXMLNoteDoc();
Element par=doc.getParagraphElement(e.getOffset());
Element cpar=doc.getParagraphElement(pane.getCaretPosition());
if (cpar==par) {
paragraph=null;
updateTabSet(pane.getCaretPosition());
}
}
}
}
}