/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* 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 com.intellij.ui.components.labels;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.wm.StatusBar;
import com.intellij.ui.ScreenUtil;
import com.intellij.ui.UI;
import com.intellij.util.ui.JBRectangle;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.accessibility.ScreenReader;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.accessibility.AccessibleAction;
import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleRole;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Set;
/**
* @author kir
*/
public class LinkLabel<T> extends JLabel {
protected boolean myUnderline;
private LinkListener<T> myLinkListener;
private T myLinkData;
private static final Set<String> ourVisitedLinks = new THashSet<>();
private boolean myIsLinkActive;
private String myVisitedLinksKey;
private Icon myHoveringIcon;
private Icon myInactiveIcon;
private boolean myClickIsBeingProcessed;
protected boolean myPaintUnderline = true;
public LinkLabel() {
this("", AllIcons.Ide.Link);
}
public LinkLabel(String text, @Nullable Icon icon) {
this(text, icon, null, null, null);
}
public LinkLabel(String text, @Nullable Icon icon, @Nullable LinkListener<T> aListener) {
this(text, icon, aListener, null, null);
}
@NotNull
public static LinkLabel<?> create(@Nullable String text, @Nullable Runnable action) {
return new LinkLabel<>(text, null, action == null ? null : new LinkListener<Object>() {
@Override
public void linkSelected(LinkLabel source, Object linkData) {
action.run();
}
}, null, null);
}
public LinkLabel(String text, @Nullable Icon icon, @Nullable LinkListener<T> aListener, @Nullable T aLinkData) {
this(text, icon, aListener, aLinkData, null);
}
public LinkLabel(String text,
@Nullable Icon icon,
@Nullable LinkListener<T> aListener,
@Nullable T aLinkData,
@Nullable String aVisitedLinksKey) {
super(text, icon, SwingConstants.LEFT);
setOpaque(false);
// Note: Ideally, we should be focusable by default in all cases, however,
// to preserve backward compatibility with existing behavior, we make
// ourselves focusable only when a screen reader is active.
setFocusable(ScreenReader.isActive());
setListener(aListener, aLinkData);
myInactiveIcon = getIcon();
MyMouseHandler mouseHandler = new MyMouseHandler();
addMouseListener(mouseHandler);
addMouseMotionListener(mouseHandler);
addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
super.keyReleased(e);
if (e.getModifiers() == 0 && e.getKeyCode() == KeyEvent.VK_SPACE) {
e.consume();
doClick();
}
}
});
addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
myUnderline = true;
repaint();
}
@Override
public void focusLost(FocusEvent e) {
myUnderline = false;
repaint();
}
});
myVisitedLinksKey = aVisitedLinksKey;
}
@Override
public void setIcon(Icon icon) {
super.setIcon(icon);
myInactiveIcon = icon;
}
public void setHoveringIcon(Icon iconForHovering) {
myHoveringIcon = iconForHovering;
}
public void setListener(LinkListener<T> listener, @Nullable T linkData) {
myLinkListener = listener;
myLinkData = linkData;
}
public T getLinkData() {
return myLinkData;
}
public void doClick() {
if (myClickIsBeingProcessed) return;
try {
myClickIsBeingProcessed = true;
if (myLinkListener != null) {
myLinkListener.linkSelected(this, myLinkData);
}
if (myVisitedLinksKey != null) {
ourVisitedLinks.add(myVisitedLinksKey);
}
repaint();
}
finally {
myClickIsBeingProcessed = false;
}
}
public boolean isVisited() {
return myVisitedLinksKey != null && ourVisitedLinks.contains(myVisitedLinksKey);
}
protected void paintComponent(Graphics g) {
setForeground(getTextColor());
super.paintComponent(g);
if (getText() != null) {
g.setColor(getTextColor());
if (myUnderline && myPaintUnderline) {
Rectangle bounds = getTextBounds();
int lineY = getUI().getBaseline(this, getWidth(), getHeight()) + 1;
g.drawLine(bounds.x, lineY, bounds.x + bounds.width, lineY);
}
if (isFocusOwner()){
g.setColor(UIUtil.getTreeSelectionBorderColor());
Rectangle bounds = getTextBounds();
// JLabel draws the text relative to the baseline. So, we must ensure
// we draw the dotted rectangle relative to that same baseline.
FontMetrics fm = getFontMetrics(getFont());
int baseLine = getUI().getBaseline(this, getWidth(), getHeight());
int textY = baseLine - fm.getLeading() - fm.getAscent();
int textHeight = fm.getHeight();
UIUtil.drawDottedRectangle(g, bounds.x, textY, bounds.x + bounds.width - 1, textY + textHeight - 1);
}
}
}
@NotNull
protected Rectangle getTextBounds() {
final Dimension size = getPreferredSize();
Icon icon = getIcon();
final Point point = new Point(0, 0);
final Insets insets = getInsets();
if (icon != null) {
point.x += getIconTextGap();
point.x += icon.getIconWidth();
}
point.x += insets.left;
point.y += insets.top;
size.width -= point.x;
size.width -= insets.right;
size.height -= insets.bottom;
return new Rectangle(point, size);
}
protected Color getTextColor() {
return myIsLinkActive ? getActive() : isVisited() ? getVisited() : getNormal();
}
public void setPaintUnderline(boolean paintUnderline) {
myPaintUnderline = paintUnderline;
}
public void removeNotify() {
super.removeNotify();
if (ScreenUtil.isStandardAddRemoveNotify(this))
disableUnderline();
}
private void setActive(boolean isActive) {
myIsLinkActive = isActive;
onSetActive(myIsLinkActive);
repaint();
}
protected void onSetActive(boolean active) {
}
private final JBRectangle iconR = new JBRectangle();
private final JBRectangle textR = new JBRectangle();
private final JBRectangle viewR = new JBRectangle();
protected boolean isInClickableArea(Point pt) {
iconR.clear();
textR.clear();
final Insets insets = getInsets(null);
viewR.x = insets.left;
viewR.y = insets.top;
viewR.width = getWidth() - (insets.left + insets.right);
viewR.height = getHeight() - (insets.top + insets.bottom);
SwingUtilities.layoutCompoundLabel(this,
getFontMetrics(getFont()),
getText(),
isEnabled() ? getIcon() : getDisabledIcon(),
getVerticalAlignment(),
getHorizontalAlignment(),
getVerticalTextPosition(),
getHorizontalTextPosition(),
viewR,
iconR,
textR,
getIconTextGap());
if (getIcon() != null) {
iconR.width += getIconTextGap(); //todo[kb] icon at right?
if (iconR.contains(pt)) {
return true;
}
}
return textR.contains(pt);
}
private void enableUnderline() {
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
myUnderline = true;
if (myHoveringIcon != null) {
super.setIcon(myHoveringIcon);
}
setStatusBarText(getStatusBarText());
repaint();
}
protected String getStatusBarText() {
return getToolTipText();
}
private void disableUnderline() {
setCursor(Cursor.getDefaultCursor());
myUnderline = false;
super.setIcon(myInactiveIcon);
setStatusBarText(null);
setActive(false);
}
private static void setStatusBarText(String statusBarText) {
if (ApplicationManager.getApplication() == null) return; // makes this component work in UIDesigner preview.
final Project[] projects = ProjectManager.getInstance().getOpenProjects();
for (Project project : projects) {
StatusBar.Info.set(statusBarText, project);
}
}
public static void clearVisitedHistory() {
ourVisitedLinks.clear();
}
protected Color getVisited() {
return UI.getColor("link.visited.foreground");
}
protected Color getActive() {
return UI.getColor("link.pressed.foreground");
}
protected Color getNormal() {
return UI.getColor("link.foreground");
}
public void entered(MouseEvent e) {
enableUnderline();
}
public void exited(MouseEvent e) {
disableUnderline();
}
public void pressed(MouseEvent e) {
doClick(e);
}
private class MyMouseHandler extends MouseAdapter implements MouseMotionListener {
public void mousePressed(MouseEvent e) {
if (isInClickableArea(e.getPoint())) {
setActive(true);
}
}
public void mouseReleased(MouseEvent e) {
if (myIsLinkActive && isInClickableArea(e.getPoint())) {
doClick(e);
}
setActive(false);
}
public void mouseMoved(MouseEvent e) {
if (isInClickableArea(e.getPoint())) {
enableUnderline();
}
else {
disableUnderline();
}
}
public void mouseExited(MouseEvent e) {
disableUnderline();
}
public void mouseDragged(MouseEvent e) {
}
}
public void doClick(InputEvent e) {
doClick();
}
@Override
public AccessibleContext getAccessibleContext() {
if (accessibleContext == null) {
accessibleContext = new AccessibleLinkLabel();
}
return accessibleContext;
}
protected class AccessibleLinkLabel extends AccessibleJLabel implements AccessibleAction {
@Override
public AccessibleRole getAccessibleRole() {
return AccessibleRole.HYPERLINK;
}
@Override
public int getAccessibleActionCount() {
return 1;
}
@Override
public String getAccessibleActionDescription(int i) {
if (i == 0) {
return UIManager.getString("AbstractButton.clickText");
}
else {
return null;
}
}
@Override
public boolean doAccessibleAction(int i) {
if (i == 0) {
doClick();
return true;
} else {
return false;
}
}
}
}