/* * 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; } } } }