//
// @(#)TextViewer.java 1.00 4/1/2002
//
// Copyright 2002 Zachary DelProposto. All rights reserved.
// Use is subject to license terms.
//
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
// Or from http://www.gnu.org/
//
package dip.gui.dialog;
import dip.gui.ClientMenu;
import dip.gui.swing.XJTextPane;
import dip.gui.swing.XJEditorPane;
import dip.gui.swing.XJScrollPane;
import dip.gui.dialog.ErrorDialog;
import dip.gui.dialog.prefs.GeneralPreferencePanel;
import dip.misc.Utils;
import dip.misc.SimpleFileFilter;
import dip.gui.swing.XJFileChooser;
import dip.misc.Log;
import java.awt.Font;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.util.HashMap;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTextPane;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.border.CompoundBorder;
import javax.swing.border.LineBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.EtchedBorder;
import javax.swing.text.Document;
import javax.swing.text.html.*;
import java.util.regex.*;
import java.io.*;
import javax.swing.*;
import java.awt.datatransfer.*;
import javax.swing.text.*;
import javax.swing.event.*;
/**
* Display and (optionally) edit Text inside a HeaderDialog.
* <p>
* May display plain or HTML-formatted text. Has a menu allowing
* (as appropriate) cut/copy/paste/select all/clear of contents.
* <p>
* Note: When constructing a TextViewer, don't forget to set if modality
* with setModal() and the title, with setTitle().
* <p>
* Nonmodal textboxes have some convenince methods available to allow
* lazy-loading of text. This improves perceived responsiveness.
*
*/
public class TextViewer extends HeaderDialog
{
/** "Loading" HTML message */
private static final String WAIT_MESSAGE = "TextViewer.message.wait";
// Menu i18n constants
private static final String MENU_EDIT = "TV_EDIT";
private static final String MENU_FILE = "TV_FILE";
private static final String MENU_ITEM_COPY = "TV_EDIT_COPY";
private static final String MENU_ITEM_CUT = "TV_EDIT_CUT";
private static final String MENU_ITEM_PASTE = "TV_EDIT_PASTE";
private static final String MENU_ITEM_SELECTALL = "TV_EDIT_SELECTALL";
private static final String MENU_ITEM_SAVEAS = "TV_FILE_SAVEAS";
private static final String SAVEAS_ACTION_CMD = "edit_saveas_action";
/** Content Type: text/html */
public static final String CONTENT_HTML = "text/html";
/** Content Type: text/plain */
public static final String CONTENT_PLAIN = "text/plain";
/** Default text area font */
protected static final Font tvFont = new Font("SansSerif", Font.PLAIN, 14);
/** Text margin */
private static final int TEXT_INSETS = 5;
private boolean _isAccepted = false;
private AcceptListener acceptListener = null;
private JEditorPane textPane;
private JScrollPane jsp;
/**
* Display the TextViewer, and return <code>true</code> if the
* text was acceptable, or false otherwise. If the dialog is
* closed / cancelled, false is returned.
*
*/
public boolean displayDialog()
{
pack();
setSize(Utils.getScreenSize(0.62f));
Utils.centerIn(this, getParent());
setVisible(true);
return _isAccepted;
}// displayDialog()
/** Create a non-modal TextViewer */
public TextViewer(JFrame parent)
{
this(parent, false);
}// TextViewer()
/** Create a TextViewer */
public TextViewer(final JFrame parent, boolean isModal)
{
super(parent, "", isModal);
// text pane
textPane = new XJEditorPane();
textPane.setEditable(true);
textPane.setBorder(new CompoundBorder(new EtchedBorder(), new EmptyBorder(5,5,5,5)));
textPane.setMargin(new Insets(TEXT_INSETS, TEXT_INSETS, TEXT_INSETS, TEXT_INSETS));
textPane.setFont(tvFont);
new java.awt.dnd.DropTarget(textPane, new FileDropTargetListener()
{
public void processDroppedFiles(File[] files)
{
final Document doc = textPane.getDocument();
for(int i=0; i<files.length; i++)
{
StringBuffer sb = new StringBuffer();
BufferedReader br = null;
try
{
br = new BufferedReader(new FileReader(files[i]));
String line = br.readLine();
while(line != null)
{
sb.append(line);
sb.append('\n');
line = br.readLine();
}
}
catch(IOException e)
{
ErrorDialog.displayFileIO(parent, e, files[i].getName());
}
finally
{
try
{
if(br != null)
{
br.close();
}
}
catch(IOException e)
{
ErrorDialog.displayFileIO(parent, e, files[i].getName());
}
}
try
{
doc.insertString(0, sb.toString(), null);
}
catch(BadLocationException ble)
{
Log.println("TextViewer error: ", ble);
}
}
}// processDroppedFiles()
});
// allow a modifiable transfer handler
textPane.setTransferHandler(new javax.swing.TransferHandler()
{
public void exportToClipboard(JComponent comp, Clipboard clip, int action)
{
if(comp instanceof JTextComponent)
{
try
{
JEditorPane textPane = (JEditorPane) comp;
final int selStart = textPane.getSelectionStart();
final int selEnd = textPane.getSelectionEnd();
Document doc = textPane.getDocument();
String text = null;
// don't export as HTML (if we are text/html). Export as filtered text.
// with newlines as appropriate.
//
if(doc instanceof HTMLDocument)
{
try
{
StringWriter sw = new StringWriter();
HTMLWriter hw = new HTMLWriter(sw, (HTMLDocument) doc, selStart, (selEnd-selStart));
hw.write();
text = filterHTML(filterExportedText(sw.toString()));
}
catch(Exception hwe)
{
text = null;
}
}
// if we are NOT an HTMLDocument, or, the above failed,
// this is the standard cop-out that always works.
if(text == null)
{
text = doc.getText(selStart, selEnd-selStart);
text = TextViewer.this.filterExportedText(text);
}
StringSelection contents = new StringSelection(text);
clip.setContents(contents, null);
// support for move
if(action == TransferHandler.MOVE)
{
doc.remove(selStart, selEnd-selStart);
}
}
catch(BadLocationException ble)
{
// do nothing
}
catch(IllegalStateException ise)
{
// could happen, say, if the clipboard is unavailable
Log.println("TextViewer::exportToClipboard(): "+ise);
}
}
}
public boolean importData(JComponent comp, Transferable t)
{
if(comp instanceof JTextComponent && textPane.isEditable())
{
// we don't want the BEST flavor, we want the Java String
// flavor. If that doesn't exist, we'll use the "best"
// text flavor.
DataFlavor stringDF = null;
DataFlavor[] dfs = t.getTransferDataFlavors();
for(int i=0; i<dfs.length; i++)
{
if(dfs[i].equals(DataFlavor.stringFlavor))
{
stringDF = dfs[i];
break;
}
}
// Use String flavor by default. It's easiest.
//
if(stringDF != null)
{
try
{
Object obj = t.getTransferData(stringDF) ;
if(obj instanceof String)
{
String importText = (String) obj;
textPane.replaceSelection(importText);
return true;
}
}
catch(Exception e)
{} // do nothing
}
else
{
// Plan "B". I'm not sure if this is
// really nescessary.
//
DataFlavor bestTextFlavor = DataFlavor.selectBestTextFlavor(t.getTransferDataFlavors());
if(bestTextFlavor != null)
{
// typical / fancy case
Reader reader = null;
try
{
reader = bestTextFlavor.getReaderForText(t);
char[] buffer = new char[128];
StringBuffer sb = new StringBuffer(2048);
int nRead = reader.read(buffer);
while(nRead != -1)
{
sb.append(buffer, 0, nRead);
nRead = reader.read(buffer);
}
textPane.replaceSelection(sb.toString());
return true;
}
catch(Exception e)
{} // do nothing
finally
{
if(reader != null)
{
try
{
reader.close();
}
catch(IOException e)
{}
}
}
}
}
}
return false;
}// importData()
public boolean canImport(JComponent comp, DataFlavor[] transferFlavors)
{
if(comp instanceof JTextComponent && textPane.isEditable())
{
// any text type is acceptable.
return (DataFlavor.selectBestTextFlavor(transferFlavors) != null);
}
return false;
}// canImport()
});
// menu setup
makeMenu();
// content pane setup
// BUG: top/bottom border of textPane disappears when scrolling. If we add a
// lineborder to the viewport on the scroller, it doesn't look right. So, for
// now, we will do nothing.
jsp = new XJScrollPane(textPane);
createDefaultContentBorder(jsp);
setContentPane(jsp);
}// TextViewer()
/**
* Allows modification of the exported document text prior to
* placing it in the system clipboard. Thus this method is called
* after a copy() occurs, but before the data is placed into the
* clipboard. This method should NOT return null.
* <p>
* By default, this method will search for unicode arrow \u2192
* and replace it with "->".
*
*/
protected String filterExportedText(String in)
{
return Utils.replaceAll(in, "\u2192", "->");
}// filterExportedText()
/**
* Simple HTML filter. This creates 'plain text' from
* a "text/html" MIME type. All this does is exclude
* content between angle brackets.
*/
protected String filterHTML(String in)
{
StringBuffer out = new StringBuffer(in.length());
boolean noCopy = false;
final int len = in.length();
for(int i=0; i<len; i++)
{
final char c = in.charAt(i);
if(c == '<')
{
noCopy = true;
}
else if(c == '>')
{
noCopy = false;
}
else
{
if(!noCopy)
{
out.append(c);
}
}
}
return out.toString();
}// filterHTML()
/**
* Sets the content type to "text/HTML", sets the
* loading message (WAIT_MESSAGE), then displays
* the dialog.
* <p>
* This only works for non-modal dialogs!
*/
public void lazyLoadDisplayDialog(TVRunnable r)
{
if(r == null)
{
throw new IllegalArgumentException();
}
if(isModal())
{
throw new IllegalStateException("lazyLoadDisplayDialog(): only for NON modal dialogs.");
}
setContentType(CONTENT_HTML);
setText(Utils.getLocalString(WAIT_MESSAGE));
displayDialog();
r.setTV(this);
Thread t = new Thread(r);
t.start();
}// lazyLoad()
/** Lazy Loading worker thread; must be subclassed */
public abstract static class TVRunnable implements Runnable
{
private TextViewer tv;
/** Create a TVRunnable */
public TVRunnable()
{
}// TVRunnable()
/** This method must be implemented by subclasses */
public abstract void run();
/** Used internally by lazyLoadDisplayDialog */
private void setTV(TextViewer tv)
{
this.tv = tv;
}// setTV()
/** Set the text */
protected final void setText(String text)
{
if(tv == null)
{
throw new IllegalStateException();
}
tv.setText(text);
}// setText()
}// nested class TVRunnable
/** Change how Horizontal scrolling is handled. */
public void setHorizontalScrollBarPolicy(int policy)
{
jsp.setHorizontalScrollBarPolicy(policy);
}// setHorizontalScrollBarPolicy()
/** Set the Content Type (e.g., "text/html", or "text/plain") of the TextViewer */
public void setContentType(String text)
{
textPane.setContentType(text);
Document doc = textPane.getDocument();
if(doc instanceof HTMLDocument)
{
((HTMLDocument) doc).setBase(Utils.getResourceBase());
}
}// setContentType()
/** Set Font. Use is not recommended if content type is "text/html". */
public void setFont(Font font)
{
textPane.setFont(font);
}// setFont()
/** Get the JEditorPane component */
public JEditorPane getEditorPane()
{
return textPane;
}// getEditorPane()
/**
* Set the AcceptListener. If no AcceptListener is desired,
* the AcceptListener may be set to null.
*/
public void setAcceptListener(AcceptListener value)
{
acceptListener = value;
}// setAcceptListener()
/** Set if this TextViewer is editable */
public void setEditable(boolean value)
{
textPane.setEditable(value);
}// setEditable()
/** Set if this TextViewer is highlightable */
public void setHighlightable(boolean value)
{
if(!value)
{
textPane.setHighlighter(null);
}
else
{
textPane.setHighlighter(new javax.swing.text.DefaultHighlighter());
}
}// setHighlightable()
/** Set the TextViewer text. Note: setContentType() should be called first. */
public void setText(String value)
{
textPane.setText(value);
textPane.setCaretPosition(0); // scroll to top
}// setText()
/** Get the TextViewer text. */
public String getText()
{
return textPane.getText();
}// getText()
/** AcceptListener: This class is called when the "Accept" button in clicked. */
public interface AcceptListener
{
/**
* Determines if the text is acceptable.
* <p>
* If it is acceptable (true) the dialog will close.
* If it is unacceptable (false), the dialog may close or stay
* open, depending upon the value returned by
* getCloseDialogAfterUnacceptable()
*
*/
public boolean isAcceptable(TextViewer t);
/**
* If true, the dialog closes after unacceptable input is given (but a warning
* message could be displayed). If false, the dialog is not closed.
*/
public boolean getCloseDialogAfterUnacceptable();
}// inner interface AcceptListener
/** Close() override. Calls AcceptListener (if any) on OK or Close actions. */
protected void close(String actionCommand)
{
if(isOKorAccept(actionCommand))
{
// if no accept() handler, assume accepted.
_isAccepted = true;
if(acceptListener != null)
{
_isAccepted = acceptListener.isAcceptable(this);
if(acceptListener.getCloseDialogAfterUnacceptable())
{
dispose();
}
}
if(_isAccepted)
{
dispose();
}
return;
}
else
{
_isAccepted = false;
dispose();
}
}// close()
/** Create the Dialog's Edit menu */
private void makeMenu()
{
final JMenuBar menuBar = new JMenuBar();
final JTextComponentMenuListener menuListener = new JTextComponentMenuListener(textPane);
JMenuItem menuItem = null;
// FILE menu
ClientMenu.Item cmItem = new ClientMenu.Item(MENU_FILE);
JMenu menu = new JMenu(cmItem.getName());
menu.setMnemonic(cmItem.getMnemonic());
menuItem = new ClientMenu.Item(MENU_ITEM_SAVEAS).makeMenuItem(false);
menuItem.setActionCommand(SAVEAS_ACTION_CMD);
menuItem.addActionListener(menuListener);
menu.add(menuItem);
menuBar.add(menu);
// EDIT menu
//
cmItem = new ClientMenu.Item(MENU_EDIT);
menu = new JMenu(cmItem.getName());
menu.setMnemonic(cmItem.getMnemonic());
final JMenuItem cutMenuItem = new ClientMenu.Item(MENU_ITEM_CUT).makeMenuItem(false);
cutMenuItem.setActionCommand(DefaultEditorKit.cutAction);
cutMenuItem.addActionListener(menuListener);
menu.add(cutMenuItem);
menuItem = new ClientMenu.Item(MENU_ITEM_COPY).makeMenuItem(false);
menuItem.setActionCommand(DefaultEditorKit.copyAction);
menuItem.addActionListener(menuListener);
menu.add(menuItem);
final JMenuItem pasteMenuItem = new ClientMenu.Item(MENU_ITEM_PASTE).makeMenuItem(false);
pasteMenuItem.setActionCommand(DefaultEditorKit.pasteAction);
pasteMenuItem.addActionListener(menuListener);
menu.add(pasteMenuItem);
menu.add(new JSeparator());
menuItem = new ClientMenu.Item(MENU_ITEM_SELECTALL).makeMenuItem(false);
menuItem.setActionCommand(DefaultEditorKit.selectAllAction);
menuItem.addActionListener(menuListener);
menu.add(menuItem);
// add a listener to enable/disable 'paste'
menu.addMenuListener(new MenuListener()
{
public void menuCanceled(MenuEvent e)
{}
public void menuDeselected(MenuEvent e)
{}
public void menuSelected(MenuEvent e)
{
cutMenuItem.setEnabled(textPane.isEditable());
pasteMenuItem.setEnabled(textPane.isEditable());
}
});
menuBar.add(menu);
setJMenuBar(menuBar);
}// makeMenu()
/** A specialized Listener for registered JTextComponent Actions */
private class JTextComponentMenuListener implements ActionListener
{
private final JTextComponent textComponent;
public JTextComponentMenuListener(JTextComponent component)
{
if(component == null) { throw new IllegalArgumentException(); }
textComponent = component;
}// JTextComponentMenuListener()
public void actionPerformed(ActionEvent e)
{
final String action = (String) e.getActionCommand();
Action a = textComponent.getActionMap().get(action);
if(a != null)
{
a.actionPerformed(new ActionEvent(textComponent,
ActionEvent.ACTION_PERFORMED,
null));
}
else
{
if(action.equals(SAVEAS_ACTION_CMD))
{
saveContents();
}
}
}// actionPerformed()
}// inner class JTextComponentMenuListener
/**
* Saves the contents of the Dialog. This saves as HTML if we are
* text/HTML, otherwise, it saves as a .txt file.
*/
protected void saveContents()
{
File file = null;
if(textPane.getContentType().equals("text/html"))
{
file = getFileName(SimpleFileFilter.HTML_FILTER);
}
else
{
file = getFileName(SimpleFileFilter.TXT_FILTER);
}
if(file != null)
{
FileWriter fw = null;
try
{
StringWriter sw = new StringWriter();
textPane.write(sw);
String output = inlineStyleSheet(sw.toString());
fw = new FileWriter(file);
fw.write(output);
}
catch(IOException e)
{
ErrorDialog.displayFileIO((JFrame) getParent(), e, file.toString());
}
finally
{
if(fw != null)
{
try { fw.close(); } catch(IOException ioe) {}
}
}
}
}// saveContents()
/** Insert (inline) the CSS style sheet (if any) */
private String inlineStyleSheet(String text)
{
if(!textPane.getContentType().equals("text/html"))
{
return text;
}
// setup a regex; our capture group is the css link HREF
//
Pattern link = Pattern.compile("(?i)<link\\s+rel=\"stylesheet\"\\s+href=\"([^\"]+)\">");
Matcher m = link.matcher(text);
if(m.find())
{
// load the link line.
String cssText = Utils.getText(Utils.getResourceBasePrefix()+m.group(1));
if(cssText != null)
{
StringBuffer sb = new StringBuffer(text.length() + 4096);
sb.append(text.substring(0, m.start()));
sb.append("<style type=\"text/css\" media=\"screen\">\n\t<!--\n");
sb.append(cssText);
sb.append("\n\t-->\n</style>");
sb.append(text.substring(m.end()));
return sb.toString();
}
}
return text;
}// inlineStyleSheet()
/**
* Popup a file requester; returns the file name, or null if
* the requester was cancelled.
*/
protected File getFileName(final SimpleFileFilter sff)
{
if(sff == null)
{
throw new IllegalArgumentException();
}
// JFileChooser setup
XJFileChooser chooser = XJFileChooser.getXJFileChooser();
chooser.addFileFilter(sff);
chooser.setFileFilter(sff);
// set default save-game path
chooser.setCurrentDirectory( GeneralPreferencePanel.getDefaultGameDir() );
// show dialog
File file = chooser.displaySaveAs(getParent());
XJFileChooser.dispose();
return file;
}// getFileName()
}// class TextViewer