/*****************************************************************************
* Copyright (C) The Apache Software Foundation. All rights reserved. *
* ------------------------------------------------------------------------- *
* This software is published under the terms of the Apache Software License *
* version 1.1, a copy of which has been included with this distribution in *
* the LICENSE file. *
*****************************************************************************/
package org.apache.batik.apps.svgbrowser;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.AttributedCharacterIterator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
import javax.swing.event.DocumentListener;
import javax.swing.event.DocumentEvent;
import org.apache.batik.gvt.CompositeGraphicsNode;
import org.apache.batik.gvt.GVTTreeWalker;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.gvt.TextNode;
import org.apache.batik.gvt.text.Mark;
import org.apache.batik.swing.JSVGCanvas;
import org.apache.batik.swing.gvt.TextSelectionManager;
import org.apache.batik.util.gui.ExtendedGridBagConstraints;
import org.apache.batik.util.gui.resource.ActionMap;
import org.apache.batik.util.gui.resource.ButtonFactory;
import org.apache.batik.util.gui.resource.MissingListenerException;
import org.apache.batik.util.gui.resource.ResourceManager;
/**
* This class represents a Dialog that lets the user searching for text inside
* an SVG document.
*
* @author <a href="mailto:tkormann@apache.org">Thierry Kormann</a>
* @version $Id: FindDialog.java,v 1.6 2001/10/05 15:34:26 tkormann Exp $
*/
public class FindDialog extends JDialog implements ActionMap {
/**
* The resource file name
*/
protected final static String RESOURCES =
"org.apache.batik.apps.svgbrowser.resources.FindDialog";
// action names
public final static String FIND_ACTION = "FindButtonAction";
public final static String CLEAR_ACTION = "ClearButtonAction";
public final static String CLOSE_ACTION = "CloseButtonAction";
/**
* The resource bundle
*/
protected static ResourceBundle bundle;
/**
* The resource manager
*/
protected static ResourceManager resources;
static {
bundle = ResourceBundle.getBundle(RESOURCES, Locale.getDefault());
resources = new ResourceManager(bundle);
}
/** The button factory */
protected ButtonFactory buttonFactory;
/** The GVT root into which text is searched. */
protected GraphicsNode gvtRoot;
/** The GVTTreeWalker used to scan the GVT Tree. */
protected GVTTreeWalker walker;
/** The current index in the TextNode's string. */
protected int currentIndex;
/** The TextField that owns the text to search. */
protected JTextField search;
/** The next button. */
protected JButton findButton;
/** The next button. */
protected JButton clearButton;
/** The cancel button. */
protected JButton closeButton;
/** The case sensitive button. */
protected JCheckBox caseSensitive;
/** The canvas. */
protected JSVGCanvas svgCanvas;
/** The highlight button. */
protected JRadioButton highlightButton;
/** The highlight and center button. */
protected JRadioButton highlightCenterButton;
/** The highlight center and zoom button. */
protected JRadioButton highlightCenterZoomButton;
/**
* Constructs a new <tt>FindDialog</tt>.
*/
public FindDialog(JSVGCanvas svgCanvas) {
this(null, svgCanvas);
}
/**
* Constructs a new <tt>FindDialog</tt>.
*/
public FindDialog(Frame owner, JSVGCanvas svgCanvas) {
super(owner, resources.getString("Dialog.title"));
this.svgCanvas = svgCanvas;
buttonFactory = new ButtonFactory(bundle, this);
listeners.put(FIND_ACTION,
new FindButtonAction());
listeners.put(CLEAR_ACTION,
new ClearButtonAction());
listeners.put(CLOSE_ACTION,
new CloseButtonAction());
JPanel p = new JPanel(new BorderLayout());
p.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
p.add(createFindPanel(), BorderLayout.CENTER);
p.add(createShowResultPanel(), BorderLayout.SOUTH);
getContentPane().add(p, BorderLayout.CENTER);
getContentPane().add(createButtonsPanel(), BorderLayout.SOUTH);
}
/**
* Creates the Find panel.
*/
protected JPanel createFindPanel() {
JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createTitledBorder
(BorderFactory.createEtchedBorder(),
resources.getString("Panel.title")));
ExtendedGridBagConstraints gbc = new ExtendedGridBagConstraints();
gbc.insets = new Insets(2, 2, 2, 2);
gbc.anchor = ExtendedGridBagConstraints.EAST;
gbc.fill = ExtendedGridBagConstraints.NONE;
gbc.setWeight(0, 0);
gbc.setGridBounds(0, 0, 1, 1);
panel.add(new JLabel(resources.getString("FindLabel.text")), gbc);
gbc.fill = ExtendedGridBagConstraints.HORIZONTAL;
gbc.setWeight(1.0, 0);
gbc.setGridBounds(1, 0, 2, 1);
panel.add(search = new JTextField(20), gbc);
gbc.fill = ExtendedGridBagConstraints.NONE;
gbc.anchor = ExtendedGridBagConstraints.WEST;
gbc.setWeight(0, 0);
gbc.setGridBounds(1, 1, 1, 1);
caseSensitive = buttonFactory.createJCheckBox("CaseSensitiveCheckBox");
panel.add(caseSensitive, gbc);
return panel;
}
protected JPanel createShowResultPanel() {
JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createTitledBorder
(BorderFactory.createEtchedBorder(),
resources.getString("ShowResultPanel.title")));
ExtendedGridBagConstraints gbc = new ExtendedGridBagConstraints();
gbc.insets = new Insets(2, 2, 2, 2);
gbc.anchor = ExtendedGridBagConstraints.WEST;
gbc.fill = ExtendedGridBagConstraints.NONE;
gbc.setWeight(0, 0);
ButtonGroup grp = new ButtonGroup();
highlightButton = buttonFactory.createJRadioButton("Highlight");
highlightButton.setSelected(true);
grp.add(highlightButton);
gbc.setGridBounds(0, 0, 1, 1);
panel.add(highlightButton, gbc);
highlightCenterButton =
buttonFactory.createJRadioButton("HighlightAndCenter");
grp.add(highlightCenterButton);
gbc.setGridBounds(0, 1, 1, 1);
panel.add(highlightCenterButton, gbc);
highlightCenterZoomButton =
buttonFactory.createJRadioButton("HighlightCenterAndZoom");
grp.add(highlightCenterZoomButton);
gbc.setGridBounds(0, 2, 1, 1);
panel.add(highlightCenterZoomButton, gbc);
return panel;
}
/**
* Creates the buttons panel
*/
protected JPanel createButtonsPanel() {
JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
panel.add(findButton = buttonFactory.createJButton("FindButton"));
panel.add(clearButton = buttonFactory.createJButton("ClearButton"));
panel.add(closeButton = buttonFactory.createJButton("CloseButton"));
return panel;
}
/**
* Sets the graphics node into which text is searched.
* @param gvtRoot the GVT root node
*/
public void setGraphicsNode(GraphicsNode gvtRoot) {
this.gvtRoot = gvtRoot;
if (gvtRoot != null) {
this.walker = new GVTTreeWalker(gvtRoot);
} else {
this.walker = null;
}
}
/**
* Returns the next GraphicsNode that matches the specified string or null
* if any.
*
* @param text the text to match
*/
protected GraphicsNode getNext(String text) {
if (walker == null && gvtRoot != null) {
walker = new GVTTreeWalker(gvtRoot);
}
GraphicsNode gn = walker.getCurrentGraphicsNode();
int index = match(gn, text, currentIndex+text.length());
if (index >= 0) {
currentIndex = index;
} else {
currentIndex = 0;
gn = walker.nextGraphicsNode();
while (gn != null &&
((currentIndex = match(gn, text, currentIndex)) < 0)) {
gn = walker.nextGraphicsNode();
}
}
return gn;
}
/**
* Returns the index inside the specified TextNode of the specified text, or
* -1 if not found.
*
* @param node the graphics node to check
* @param text the text use to match
* @param index the index from which to start
*/
protected int match(GraphicsNode node, String text, int index) {
if (!(node instanceof TextNode)
|| !node.isVisible()
|| text == null || text.length() == 0) {
return -1;
}
String s = ((TextNode)node).getText();
if (!caseSensitive.isSelected()) {
s = s.toLowerCase();
text = text.toLowerCase();
}
return s.indexOf(text, index);
}
/**
* Shows the current selected <tt>TextNode</tt>.
*/
protected void showSelectedGraphicsNode() {
GraphicsNode gn = walker.getCurrentGraphicsNode();
if (!(gn instanceof TextNode)) {
return;
}
TextNode textNode = (TextNode)gn;
// mark the selection of the substring found
String text = textNode.getText();
String pattern = search.getText();
AttributedCharacterIterator aci =
textNode.getAttributedCharacterIterator();
aci.first();
for (int i=0; i < text.indexOf(pattern, currentIndex); ++i) {
aci.next();
}
Mark startMark = textNode.getMarkerForChar(aci.getIndex(), true);
for (int i = 0; i < pattern.length()-1; ++i) {
aci.next();
}
Mark endMark = textNode.getMarkerForChar(aci.getIndex(), false);
svgCanvas.select(startMark, endMark);
// zoom on the TextNode if needed
if (highlightButton.isSelected()) {
return;
}
// get the highlight shape in GVT root (global) coordinate sytem
Shape s = textNode.getHighlightShape();
AffineTransform at;
if (highlightCenterZoomButton.isSelected()) {
at = svgCanvas.getInitialTransform();
} else {
at = svgCanvas.getRenderingTransform();
}
// get the bounds of the highlight shape in the canvas coordinate system
Rectangle2D gnb = at.createTransformedShape(s).getBounds();
Dimension canvasSize = svgCanvas.getSize();
// translate the highlight region to (0, 0) in the canvas coordinate
// system
AffineTransform Tx = AffineTransform.getTranslateInstance
(-gnb.getX()-gnb.getWidth()/2,
-gnb.getY()-gnb.getHeight()/2);
if (highlightCenterZoomButton.isSelected()) {
// zoom on the highlight shape such as the shape takes x% of the
// canvas size
double sx = canvasSize.width/gnb.getWidth();
double sy = canvasSize.height/gnb.getHeight();
double scale = Math.min(sx, sy) / 8;
if (scale > 1) {
Tx.preConcatenate
(AffineTransform.getScaleInstance(scale, scale));
}
}
Tx.preConcatenate(AffineTransform.getTranslateInstance
(canvasSize.width/2, canvasSize.height/2));
// take into account the initial transform
AffineTransform newRT = new AffineTransform(at);
newRT.preConcatenate(Tx);
// change the rendering transform
svgCanvas.setRenderingTransform(newRT);
}
// ActionMap implementation
/**
* The map that contains the listeners
*/
protected Map listeners = new HashMap(10);
/**
* Returns the action associated with the given string
* or null on error
* @param key the key mapped with the action to get
* @throws MissingListenerException if the action is not found
*/
public Action getAction(String key) throws MissingListenerException {
return (Action)listeners.get(key);
}
//////////////////////////////////////////////////////////////////////////
// Action implementation
//////////////////////////////////////////////////////////////////////////
/**
* The action associated to the 'find' button.
*/
protected class FindButtonAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
String text = search.getText();
if (text == null || text.length() == 0) {
return;
}
GraphicsNode gn = getNext(text);
if (gn != null) {
showSelectedGraphicsNode();
} else {
// end of document reached
walker = null;
JOptionPane.showMessageDialog(FindDialog.this,
resources.getString("End.text"),
resources.getString("End.title"),
JOptionPane.INFORMATION_MESSAGE);
}
}
}
/**
* The action associated to the 'clear' button.
*/
protected class ClearButtonAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
search.setText(null);
walker = null;
}
}
/**
* The action associated to the 'close' button.
*/
protected class CloseButtonAction extends AbstractAction {
public void actionPerformed(ActionEvent e) {
dispose();
}
}
}