/** * $RCSfile: ,v $ * $Revision: $ * $Date: $ * * Copyright (C) 2004-2011 Jive Software. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.spark.ui; import java.awt.Color; import java.awt.Cursor; import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.StringTokenizer; import javax.swing.AbstractAction; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.JSeparator; import javax.swing.JTextPane; import javax.swing.KeyStroke; import javax.swing.UIManager; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.Element; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; import org.jivesoftware.resource.Res; import org.jivesoftware.spark.SparkManager; import org.jivesoftware.spark.plugin.ContextMenuListener; import org.jivesoftware.spark.util.BrowserLauncher; import org.jivesoftware.spark.util.ModelUtil; import org.jivesoftware.spark.util.log.Log; import org.jivesoftware.sparkimpl.plugin.emoticons.EmoticonManager; import org.jivesoftware.sparkimpl.settings.local.LocalPreferences; import org.jivesoftware.sparkimpl.settings.local.SettingsManager; /** * The ChatArea class handles proper chat text formatting such as url handling. Use ChatArea for proper * formatting of bold, italics, underlined and urls. */ public class ChatArea extends JTextPane implements MouseListener, MouseMotionListener, ActionListener { private static final long serialVersionUID = -2155445968040220072L; /** * The SimpleAttributeSet used within this instance of JTextPane. */ public final SimpleAttributeSet styles = new SimpleAttributeSet(); /** * The default Hand cursor. */ public static final Cursor HAND_CURSOR = new Cursor(Cursor.HAND_CURSOR); /** * The default Text Cursor. */ public static final Cursor DEFAULT_CURSOR = new Cursor(Cursor.DEFAULT_CURSOR); /** * The currently selected Font Family to use. */ private String fontFamily; /** * The currently selected Font Size to use. */ private int fontSize; private List<ContextMenuListener> contextMenuListener = new ArrayList<ContextMenuListener>(); private JPopupMenu popup; private JMenuItem cutMenu; private JMenuItem copyMenu; private JMenuItem pasteMenu; private JMenuItem selectAll; private List<LinkInterceptor> interceptors = new ArrayList<LinkInterceptor>(); protected EmoticonManager emoticonManager; protected Boolean forceEmoticons = false; protected Boolean emoticonsAvailable = true; /** * ChatArea Constructor. */ public ChatArea() { emoticonManager = EmoticonManager.getInstance(); Collection<String> emoticonPacks = null; emoticonPacks = emoticonManager.getEmoticonPacks(); if(emoticonPacks == null) { emoticonsAvailable = false; } // Set Default Font final LocalPreferences pref = SettingsManager.getLocalPreferences(); int fs = pref.getChatRoomFontSize(); fontSize = fs; setFontSize(fs); cutMenu = new JMenuItem(Res.getString("action.cut")); cutMenu.addActionListener(this); copyMenu = new JMenuItem(Res.getString("action.copy")); copyMenu.addActionListener(this); pasteMenu = new JMenuItem(Res.getString("action.paste")); pasteMenu.addActionListener(this); selectAll = new JMenuItem(Res.getString("action.select.all")); selectAll.addActionListener(this); // Set Default Font setFont(new Font("Dialog", Font.PLAIN, 12)); getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("Ctrl x"), "cut"); getActionMap().put("cut", new AbstractAction("cut") { private static final long serialVersionUID = 9117190151545566922L; public void actionPerformed(ActionEvent evt) { cutAction(); } }); getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("Ctrl c"), "copy"); getActionMap().put("copy", new AbstractAction("copy") { private static final long serialVersionUID = 4949716854440264528L; public void actionPerformed(ActionEvent evt) { SparkManager.setClipboard(getSelectedText()); } }); getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke("Ctrl v"), "paste"); getActionMap().put("paste", new AbstractAction("paste") { private static final long serialVersionUID = -8767763580660683678L; public void actionPerformed(ActionEvent evt) { pasteAction(); } }); } /** * Set the current text of the ChatArea. * * @param message inserts the text directly into the ChatArea */ public void setText(String message) { // By default, use the hand cursor for link selection // and scrolling. // setCursor(HAND_CURSOR); // Make sure the message is not null. // message = message.trim(); // Why? // message = message.replaceAll("/\"", ""); if (ModelUtil.hasLength(message)) { try { insert(message); } catch (BadLocationException e) { Log.error(e); } } } /** * setText is a core JTextPane method that can beused to inject a different Document type * for instance HTMLDocument (setText("<HTML></HTML>") * We should keep the functionality - it is useful when we want to inject a different Document type * instead of StyleDocument * @param content */ public void setInitialContent(String content) { super.setText(content); } /** * Removes the last appearance of word from the TextArea * @param word */ public void removeLastWord(String word) { select(getText().lastIndexOf(word),getText().length()); replaceSelection(""); } /** * Removes everything in between <b>begin</b> and <b>end</b> * @param begin * @param end */ public void removeWordInBetween(int begin, int end){ select(begin, end); replaceSelection(""); } /** * Clear the current document. This will remove all text and element * attributes such as bold, italics, and underlining. Note that the font family and * font size will be persisted. */ public void clear() { super.setText(""); if (fontFamily != null) { setFont(fontFamily); } if (fontSize != 0) { setFontSize(fontSize); } StyleConstants.setUnderline(styles, false); StyleConstants.setBold(styles, false); StyleConstants.setItalic(styles, false); setCharacterAttributes(styles, false); } /** * Does the actual insertion of text, adhering to the styles * specified during message creation in either the thin or thick client. * * @param text - the text to insert. * @throws BadLocationException if location is not available to insert into. */ public void insert(String text) throws BadLocationException { boolean bold = false; boolean italic = false; boolean underlined = false; final StringTokenizer tokenizer = new StringTokenizer(text, " \n \t", true); while (tokenizer.hasMoreTokens()) { String textFound = tokenizer.nextToken(); if ((textFound.startsWith("http://") || textFound.startsWith("ftp://") || textFound.startsWith("https://") || textFound.startsWith("www.")) && textFound.indexOf(".") > 1) { insertLink(textFound); } else if ( textFound.startsWith("\\\\") || (textFound.indexOf("://") > 0 && textFound.indexOf(".") < 1) ) { insertAddress(textFound); } else if (!insertImage(textFound)) { insertText(textFound); } } // By default, always have decorations off. StyleConstants.setBold(styles, bold); StyleConstants.setItalic(styles, italic); StyleConstants.setUnderline(styles, underlined); } /** * Inserts text into the current document. * * @param text the text to insert * @throws BadLocationException if the location is not available for insertion. */ public void insertText(String text) throws BadLocationException { final Document doc = getDocument(); styles.removeAttribute("link"); doc.insertString(doc.getLength(), text, styles); setCaretPosition(doc.getLength()); } /** * Inserts text into the current document. * * @param text the text to insert * @param color the color of the text * @throws BadLocationException if the location is not available for insertion. */ public void insertText(String text, Color color) throws BadLocationException { final Document doc = getDocument(); StyleConstants.setForeground(styles, color); doc.insertString(doc.getLength(), text, styles); setCaretPosition(doc.getLength()); } /** * Inserts a link into the current document. * * @param link - the link to insert( ex. http://www.javasoft.com ) * @throws BadLocationException if the location is not available for insertion. */ public void insertLink(String link) throws BadLocationException { final Document doc = getDocument(); styles.addAttribute("link", link); StyleConstants.setForeground(styles, (Color)UIManager.get("Link.foreground")); StyleConstants.setUnderline(styles, true); doc.insertString(doc.getLength(), link, styles); StyleConstants.setUnderline(styles, false); StyleConstants.setForeground(styles, (Color)UIManager.get("TextPane.foreground")); styles.removeAttribute("link"); setCharacterAttributes(styles, false); setCaretPosition(doc.getLength()); } /** * Inserts a network address into the current document. * * @param address - the address to insert( ex. \superpc\etc\file\ OR http://localhost/ ) * @throws BadLocationException if the location is not available for insertion. */ public void insertAddress(String address) throws BadLocationException { final Document doc = getDocument(); styles.addAttribute("link", address); StyleConstants.setForeground(styles, (Color)UIManager.get("Address.foreground")); StyleConstants.setUnderline(styles, true); doc.insertString(doc.getLength(), address, styles); StyleConstants.setUnderline(styles, false); StyleConstants.setForeground(styles, (Color)UIManager.get("TextPane.foreground")); styles.removeAttribute("link"); setCharacterAttributes(styles, false); setCaretPosition(doc.getLength()); } /** * Inserts an emotion icon into the current document. * * @param imageKey - the smiley representation of the image.( ex. :) ) * @return true if the image was found, otherwise false. */ public boolean insertImage(String imageKey) { if(!forceEmoticons && !SettingsManager.getLocalPreferences().areEmoticonsEnabled() || !emoticonsAvailable){ return false; } final Document doc = getDocument(); Icon emotion = emoticonManager.getEmoticonImage(imageKey.toLowerCase()); if (emotion == null) { return false; } select(doc.getLength(), doc.getLength()); insertIcon(emotion); setCaretPosition(doc.getLength()); return true; } /** * Inserts horizontal line */ public void insertHorizontalLine() { try { insertComponent( new JSeparator() ); insertText("\n"); } catch (BadLocationException e) { Log.error("Error message.", e); } } /** * Sets the current element to be either bold or not depending * on the current state. If the element is currently set as bold, * it will be set to false, and vice-versa. */ public void setBold() { final Element element = getStyledDocument().getCharacterElement(getCaretPosition() - 1); if (element != null) { AttributeSet as = element.getAttributes(); boolean isBold = StyleConstants.isBold(as); StyleConstants.setBold(styles, !isBold); try { setCharacterAttributes(styles, true); } catch (Exception ex) { Log.error("Error settings bold:", ex); } } } /** * Sets the current element to be either italicized or not depending * on the current state. If the element is currently set as italic, * it will be set to false, and vice-versa. */ public void setItalics() { final Element element = getStyledDocument().getCharacterElement(getCaretPosition() - 1); if (element != null) { AttributeSet as = element.getAttributes(); boolean isItalic = StyleConstants.isItalic(as); StyleConstants.setItalic(styles, !isItalic); try { setCharacterAttributes(styles, true); } catch (Exception fontException) { Log.error("Error settings italics:", fontException); } } } /** * Sets the current document to be either underlined or not depending * on the current state. If the element is currently set as underlined, * it will be set to false, and vice-versa. */ public void setUnderlined() { final Element element = getStyledDocument().getCharacterElement(getCaretPosition() - 1); if (element != null) { AttributeSet as = element.getAttributes(); boolean isUnderlined = StyleConstants.isUnderline(as); StyleConstants.setUnderline(styles, !isUnderlined); try { setCharacterAttributes(styles, true); } catch (Exception underlineException) { Log.error("Error settings underline:", underlineException); } } } /** * Set the font on the current element. * * @param font the font to use with the current element */ public void setFont(String font) { StyleConstants.setFontFamily(styles, font); try { setCharacterAttributes(styles, false); } catch (Exception fontException) { Log.error("Error settings font:", fontException); } fontFamily = font; } /** * Set the current font size. * * @param size the current font size. */ public void setFontSize(int size) { StyleConstants.setFontSize(styles, size); try { setCharacterAttributes(styles, false); } catch (Exception fontException) { Log.error("Error settings font:", fontException); } fontSize = size; } public void mouseClicked(MouseEvent e) { try { final int pos = viewToModel(e.getPoint()); final Element element = getStyledDocument().getCharacterElement(pos); if (element != null) { final AttributeSet as = element.getAttributes(); final Object o = as.getAttribute("link"); if (o != null) { try { final String url = (String)o; boolean handled = fireLinkInterceptors(e, url); if (!handled) { if(e.getButton() == MouseEvent.BUTTON1) BrowserLauncher.openURL(url); else if (e.getButton() == MouseEvent.BUTTON3) { JPopupMenu popupmenu = new JPopupMenu(); JMenuItem linkcopy = new JMenuItem( Res.getString("action.copy")); linkcopy.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { SparkManager.setClipboard(url); } }); linkcopy.setEnabled(true); popupmenu.add(linkcopy); popupmenu.show(this, e.getX(), e.getY()); } } } catch (Exception ioe) { Log.error("Error launching browser:", ioe); } } } } catch (Exception ex) { Log.error("Visible Error", ex); } } public void mousePressed(MouseEvent e) { if (e.isPopupTrigger()) { handlePopup(e); } } /** * This launches the <code>BrowserLauncher</code> with the URL * located in <code>ChatArea</code>. Note that the url will * automatically be clickable when added to <code>ChatArea</code> * * @param e - the MouseReleased event */ public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) { handlePopup(e); } } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mouseDragged(MouseEvent e) { } /** * Checks to see if the mouse is located over a browseable * link. * * @param e - the current MouseEvent. */ public void mouseMoved(MouseEvent e) { checkForLink(e); } /** * Checks to see if the mouse is located over a browseable * link. * * @param e - the current MouseEvent. */ private void checkForLink(MouseEvent e) { try { final int pos = viewToModel(e.getPoint()); final Element element = getStyledDocument().getCharacterElement(pos); if (element != null) { final AttributeSet as = element.getAttributes(); final Object o = as.getAttribute("link"); if (o != null) { setCursor(HAND_CURSOR); } else { setCursor(DEFAULT_CURSOR); } } } catch (Exception ex) { Log.error("Error in CheckLink:", ex); } } /** * Examines the chatInput text pane, and returns a string containing the text with any markup * (jive markup in our case). This will strip any terminating new line from the input. * * @return a string of marked up text. */ public String getMarkup() { final StringBuffer buf = new StringBuffer(); final String text = getText(); final StyledDocument doc = getStyledDocument(); final Element rootElem = doc.getDefaultRootElement(); // MAY RETURN THIS BLOCK if (text.trim().length() <= 0) { return null; } boolean endsInNewline = text.charAt(text.length() - 1) == '\n'; for (int j = 0; j < rootElem.getElementCount(); j++) { final Element pElem = rootElem.getElement(j); for (int i = 0; i < pElem.getElementCount(); i++) { final Element e = pElem.getElement(i); final AttributeSet as = e.getAttributes(); final boolean bold = StyleConstants.isBold(as); final boolean italic = StyleConstants.isItalic(as); final boolean underline = StyleConstants.isUnderline(as); int end = e.getEndOffset(); if (end > text.length()) { end = text.length(); } if (endsInNewline && end >= text.length() - 1) { end--; } // swing text.. :-/ if (j == rootElem.getElementCount() - 1 && i == pElem.getElementCount() - 1) { end = text.length(); } final String current = text.substring(e.getStartOffset(), end); if (bold) { buf.append("[b]"); } if (italic) { buf.append("[i]"); } if (underline) { buf.append("[u]"); } //buf.append( "[font face=/\"" + fontFamily + "/\" size=/\"" + fontSize + "/\"/]" ); // Iterator over current string to find url tokens final StringTokenizer tkn = new StringTokenizer(current, " ", true); while (tkn.hasMoreTokens()) { final String token = tkn.nextToken(); if (token.startsWith("http://") || token.startsWith("ftp://") || token.startsWith("https://")) { buf.append("[url]").append(token).append("[/url]"); } else if (token.startsWith("www")) { buf.append("[url "); buf.append("http://").append(token); buf.append("]"); buf.append(token); buf.append("[/url]"); } else { buf.append(token); } } // Always add end tags for markup if (underline) { buf.append("[/u]"); } if (italic) { buf.append("[/i]"); } if (bold) { buf.append("[/b]"); } // buf.append( "[/font]" ); } } return buf.toString(); } private void handlePopup(MouseEvent e) { popup = new JPopupMenu(); popup.add(cutMenu); popup.add(copyMenu); popup.add(pasteMenu); fireContextMenuListeners(); popup.addSeparator(); popup.add(selectAll); // Handle enable boolean textSelected = ModelUtil.hasLength(getSelectedText()); String clipboard = SparkManager.getClipboard(); cutMenu.setEnabled(textSelected && isEditable()); copyMenu.setEnabled(textSelected); pasteMenu.setEnabled(ModelUtil.hasLength(clipboard) && isEditable()); popup.show(this, e.getX(), e.getY()); } /** * Adds a <code>ContextMenuListener</code> to ChatArea. * * @param listener the ContextMenuListener. */ public void addContextMenuListener(ContextMenuListener listener) { contextMenuListener.add(listener); } /** * Remove a <code>ContextMenuListener</code> to ChatArea. * * @param listener the ContextMenuListener. */ public void removeContextMenuListener(ContextMenuListener listener) { contextMenuListener.remove(listener); } private void fireContextMenuListeners() { for (ContextMenuListener listener : new ArrayList<ContextMenuListener>(contextMenuListener)) { listener.poppingUp(this, popup); } } public void addLinkInterceptor(LinkInterceptor interceptor) { interceptors.add(interceptor); } public void removeLinkInterceptor(LinkInterceptor interceptor) { interceptors.remove(interceptor); } public boolean fireLinkInterceptors(MouseEvent event, String link) { for (LinkInterceptor linkInterceptor : new ArrayList<LinkInterceptor>(interceptors)) { boolean handled = linkInterceptor.handleLink(event, link); if (handled) { return true; } } return false; } public void actionPerformed(ActionEvent e) { if (e.getSource() == cutMenu) { cutAction(); } else if (e.getSource() == copyMenu) { SparkManager.setClipboard(getSelectedText()); } else if (e.getSource() == pasteMenu) { pasteAction(); } else if (e.getSource() == selectAll) { requestFocus(); selectAll(); } } private void cutAction() { String selectedText = getSelectedText(); replaceSelection(""); SparkManager.setClipboard(selectedText); } private void pasteAction() { String text = SparkManager.getClipboard(); if (text != null) { replaceSelection(text); } } protected void releaseResources() { getActionMap().remove("copy"); getActionMap().remove("cut"); getActionMap().remove("paste"); } public Boolean getForceEmoticons() { return forceEmoticons; } public void setForceEmoticons(Boolean forceEmoticons) { this.forceEmoticons = forceEmoticons; } }