package chatty.gui.components.textpane;
import chatty.User;
import chatty.gui.LinkListener;
import chatty.gui.MouseClickedListener;
import chatty.gui.UserListener;
import chatty.gui.components.menus.ChannelContextMenu;
import chatty.gui.components.menus.ContextMenu;
import chatty.gui.components.menus.ContextMenuListener;
import chatty.gui.components.menus.EmoteContextMenu;
import chatty.gui.components.menus.UrlContextMenu;
import chatty.gui.components.menus.UserContextMenu;
import chatty.gui.components.menus.UsericonContextMenu;
import chatty.util.api.Emoticon.EmoticonImage;
import chatty.util.api.usericons.Usericon;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JPopupMenu;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.StyledDocument;
import javax.swing.text.html.HTML;
/**
* Detects any clickable text in the document and reacts accordingly. It shows
* the appropriate cursor when moving over it with the mouse and reacts to
* clicks on clickable text.
*
* It knows to look for links and User objects at the moment.
*
* @author tduva
*/
public class LinkController extends MouseAdapter implements MouseMotionListener {
private static final Cursor HAND_CURSOR = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
private static final Cursor NORMAL_CURSOR = Cursor.getDefaultCursor();
/**
* When a User is clicked, the User object is send here
*/
private final Set<UserListener> userListener = new HashSet<>();
/**
* When a link is clicked, the String with the url is send here
*/
private LinkListener linkListener;
private MouseClickedListener mouseClickedListener;
private ContextMenuListener contextMenuListener;
private ContextMenu defaultContextMenu;
/**
* Set the object that should receive the User object once a User is clicked
*
* @param listener
*/
public void addUserListener(UserListener listener) {
if (listener != null) {
userListener.add(listener);
}
}
/**
* Set the object that should receive the url String once a link is clicked
*
* @param listener
*/
public void setLinkListener(LinkListener listener) {
linkListener = listener;
}
public void setMouseClickedListener(MouseClickedListener listener) {
mouseClickedListener = listener;
}
/**
* Set the listener for all context menus.
*
* @param listener
*/
public void setContextMenuListener(ContextMenuListener listener) {
contextMenuListener = listener;
if (defaultContextMenu != null) {
defaultContextMenu.addContextMenuListener(listener);
}
}
/**
* Set the context menu for when no special context menus (user, link) are
* appropriate.
*
* @param contextMenu
*/
public void setDefaultContextMenu(ContextMenu contextMenu) {
defaultContextMenu = contextMenu;
contextMenu.addContextMenuListener(contextMenuListener);
}
/**
* Handles mouse presses. This is favourable to mouseClicked because it
* might work better in a fast moving chat and you won't select text
* instead of opening userinfo etc.
*
* @param e
*/
@Override
public void mousePressed(MouseEvent e) {
if (e.getClickCount() == 1 && SwingUtilities.isLeftMouseButton(e)) {
String url = getUrl(e);
if (url != null && !isUrlDeleted(e)) {
if (linkListener != null) {
linkListener.linkClicked(url);
}
return;
}
User user = getUser(e);
if (user != null) {
String messageId = (String)getAttributes(e).getAttribute(ChannelTextPane.Attribute.ID);
for (UserListener listener : userListener) {
listener.userClicked(user, messageId, e);
}
return;
}
EmoticonImage emote = getEmoticon(e);
if (emote != null) {
for (UserListener listener : userListener) {
listener.emoteClicked(emote.getEmoticon(), e);
}
return;
}
Usericon usericon = getUsericon(e);
if (usericon != null) {
for (UserListener listener : userListener) {
listener.usericonClicked(usericon, e);
}
}
}
else if (e.isPopupTrigger()) {
openContextMenu(e);
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
openContextMenu(e);
}
}
/**
* Handle clicks (pressed and released) on the text pane.
*
* @param e
*/
@Override
public void mouseClicked(MouseEvent e) {
if (mouseClickedListener != null && e.getClickCount() == 1
&& !e.isAltDown() && !e.isAltGraphDown()) {
// Doing this on mousePressed will prevent selection of text,
// because this is used to change the focus to the input
mouseClickedListener.mouseClicked();
}
}
@Override
public void mouseMoved(MouseEvent e) {
JTextPane text = (JTextPane)e.getSource();
String url = getUrl(e);
if ((url != null && !isUrlDeleted(e)) || getUser(e) != null ||
getEmoticon(e) != null || getUsericon(e) != null) {
text.setCursor(HAND_CURSOR);
} else {
text.setCursor(NORMAL_CURSOR);
}
}
/**
* Gets the URL from the MouseEvent (if there is any).
*
* @param e
* @return The URL or null if none was found.
*/
private String getUrl(MouseEvent e) {
AttributeSet attributes = getAttributes(e);
if (attributes != null) {
return (String)(attributes.getAttribute(HTML.Attribute.HREF));
}
return null;
}
private boolean isUrlDeleted(MouseEvent e) {
AttributeSet attributes = getAttributes(e);
if (attributes != null) {
Boolean deleted = (Boolean)attributes.getAttribute(ChannelTextPane.Attribute.URL_DELETED);
if (deleted == null) {
return false;
}
return deleted;
}
return false;
}
/**
* Gets the User object from the MouseEvent (if there is any).
*
* @param e
* @return The User object or null if none was found.
*/
private User getUser(MouseEvent e) {
AttributeSet attributes = getAttributes(e);
if (attributes != null) {
return (User)(attributes.getAttribute(ChannelTextPane.Attribute.USER));
}
return null;
}
private EmoticonImage getEmoticon(MouseEvent e) {
AttributeSet attributes = getAttributes(e);
if (attributes != null) {
return (EmoticonImage)(attributes.getAttribute(ChannelTextPane.Attribute.EMOTICON));
}
return null;
}
private Usericon getUsericon(MouseEvent e) {
AttributeSet attributes = getAttributes(e);
if (attributes != null) {
return (Usericon)(attributes.getAttribute(ChannelTextPane.Attribute.USERICON));
}
return null;
}
public static Element getElement(MouseEvent e) {
JTextPane text = (JTextPane) e.getSource();
Point mouseLocation = new Point(e.getX(), e.getY());
int pos = text.viewToModel(mouseLocation);
if (pos >= 0) {
/**
* Check if the found element is actually located where the mouse is
* pointing, and if not try the previous element.
*
* This is a fix to make detection of emotes more reliable. The
* viewToModel() method apparently searches for the closest element
* to the given position, disregarding the size of the elements,
* which means on the right side of an emote the next (non-emote)
* element is nearer.
*
* See also:
* http://stackoverflow.com/questions/24036650/detecting-image-on-current-mouse-position-only-works-on-part-of-image
*/
try {
Rectangle rect = text.modelToView(pos);
if (e.getX() < rect.x && e.getY() < rect.y + rect.height && pos > 0) {
pos--;
}
} catch (BadLocationException ex) {
}
StyledDocument doc = text.getStyledDocument();
Element element = doc.getCharacterElement(pos);
return element;
}
return null;
}
/**
* Gets the attributes from the element in the document the mouse is
* pointing at.
*
* @param e
* @return The attributes of this element or null if the mouse wasn't
* pointing at an element
*/
public static AttributeSet getAttributes(MouseEvent e) {
Element element = getElement(e);
if (element != null) {
return element.getAttributes();
}
return null;
}
private void openContextMenu(MouseEvent e) {
// Component to show the context menu on has to be showing to determine
// it's location (it might not be showing if the channel changed after
// the click)
if (!e.getComponent().isShowing()) {
return;
}
User user = getUser(e);
String url = getUrl(e);
EmoticonImage emote = getEmoticon(e);
Usericon usericon = getUsericon(e);
JPopupMenu m;
if (user != null) {
m = new UserContextMenu(user, contextMenuListener);
}
else if (url != null) {
m = new UrlContextMenu(url, isUrlDeleted(e), contextMenuListener);
}
else if (emote != null) {
m = new EmoteContextMenu(emote, contextMenuListener);
}
else if (usericon != null) {
m = new UsericonContextMenu(usericon, contextMenuListener);
}
else {
if (defaultContextMenu == null) {
m = new ChannelContextMenu(contextMenuListener);
} else {
m = defaultContextMenu;
}
}
m.show(e.getComponent(), e.getX(), e.getY());
}
}