/*
* Copyright 2000-2013 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.util.ui;
import com.intellij.ui.ClickListener;
import com.intellij.ui.SimpleColoredComponent;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.ui.UIBundle;
import com.intellij.ui.components.JBViewport;
import com.intellij.util.ObjectUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
public abstract class StatusText {
public static final SimpleTextAttributes DEFAULT_ATTRIBUTES = SimpleTextAttributes.GRAYED_ATTRIBUTES;
public static final String DEFAULT_EMPTY_TEXT = UIBundle.message("message.nothingToShow");
@Nullable
private Component myOwner;
private Component myMouseTarget;
private final MouseMotionListener myMouseMotionListener;
private final ClickListener myClickListener;
private boolean myIsDefaultText;
private String myText = "";
protected final SimpleColoredComponent myComponent = new SimpleColoredComponent();
private final List<ActionListener> myClickListeners = new ArrayList<>();
private boolean myHasActiveClickListeners; // calculated field for performance optimization
private boolean myShowAboveCenter = true;
protected StatusText(JComponent owner) {
this();
attachTo(owner);
}
public StatusText() {
myClickListener = new ClickListener() {
@Override
public boolean onClick(@NotNull MouseEvent e, int clickCount) {
if (e.getButton() == MouseEvent.BUTTON1 && clickCount == 1) {
ActionListener actionListener = findActionListenerAt(e.getPoint());
if (actionListener != null) {
actionListener.actionPerformed(new ActionEvent(this, 0, ""));
return true;
}
}
return false;
}
};
myMouseMotionListener = new MouseAdapter() {
private Cursor myOriginalCursor;
@Override
public void mouseMoved(final MouseEvent e) {
if (isStatusVisible()) {
if (findActionListenerAt(e.getPoint()) != null) {
if (myOriginalCursor == null) {
myOriginalCursor = myMouseTarget.getCursor();
myMouseTarget.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
}
}
else if (myOriginalCursor != null) {
myMouseTarget.setCursor(myOriginalCursor);
myOriginalCursor = null;
}
}
}
};
myComponent.setOpaque(false);
myComponent.setFont(UIUtil.getLabelFont());
setText(DEFAULT_EMPTY_TEXT, DEFAULT_ATTRIBUTES);
myIsDefaultText = true;
}
public void attachTo(@Nullable Component owner) {
attachTo(owner, owner);
}
public void attachTo(@Nullable Component owner, @Nullable Component mouseTarget) {
if (myMouseTarget != null) {
myClickListener.uninstall(myMouseTarget);
myMouseTarget.removeMouseMotionListener(myMouseMotionListener);
}
myOwner = owner;
myMouseTarget = mouseTarget;
if (myMouseTarget != null) {
myClickListener.installOn(myMouseTarget);
myMouseTarget.addMouseMotionListener(myMouseMotionListener);
}
}
protected abstract boolean isStatusVisible();
@Nullable
private ActionListener findActionListenerAt(Point point) {
if (!myHasActiveClickListeners || !isStatusVisible()) return null;
point = SwingUtilities.convertPoint(myMouseTarget, point, myOwner);
Rectangle b = getTextComponentBound();
if (b.contains(point)) {
int index = myComponent.findFragmentAt(point.x - b.x);
if (index >= 0 && index < myClickListeners.size()) {
return myClickListeners.get(index);
}
}
return null;
}
protected Rectangle getTextComponentBound() {
Rectangle ownerRec = myOwner == null ? new Rectangle(0, 0, 0, 0) : myOwner.getBounds();
Dimension size = myComponent.getPreferredSize();
int x = (ownerRec.width - size.width) / 2;
int y = (ownerRec.height - size.height) / (isShowAboveCenter() ? 3 : 2);
return new Rectangle(x, y, size.width, size.height);
}
public final boolean isShowAboveCenter() {
return myShowAboveCenter;
}
public final StatusText setShowAboveCenter(boolean showAboveCenter) {
myShowAboveCenter = showAboveCenter;
return this;
}
@NotNull
public String getText() {
return myText;
}
public StatusText setText(String text) {
return setText(text, DEFAULT_ATTRIBUTES);
}
public StatusText setText(String text, SimpleTextAttributes attrs) {
return clear().appendText(text, attrs);
}
public StatusText clear() {
myText = "";
myComponent.clear();
myClickListeners.clear();
myHasActiveClickListeners = false;
if (myOwner != null && isStatusVisible()) myOwner.repaint();
return this;
}
public StatusText appendText(String text) {
return appendText(text, DEFAULT_ATTRIBUTES);
}
public StatusText appendText(String text, SimpleTextAttributes attrs) {
return appendText(text, attrs, null);
}
public StatusText appendText(String text, SimpleTextAttributes attrs, ActionListener listener) {
if (myIsDefaultText) {
clear();
myIsDefaultText = false;
}
myText += text;
myComponent.append(text, attrs);
myClickListeners.add(listener);
if (listener != null) {
myHasActiveClickListeners = true;
}
if (myOwner != null && isStatusVisible()) myOwner.repaint();
return this;
}
public void paint(Component owner, Graphics g) {
if (!isStatusVisible()) return;
if (owner == myOwner) {
doPaintStatusText(g, getTextComponentBound());
}
else {
paintOnComponentUnderViewport(owner, g);
}
}
private void paintOnComponentUnderViewport(Component component, Graphics g) {
JBViewport viewport = ObjectUtils.tryCast(myOwner, JBViewport.class);
if (viewport == null || viewport.getView() != component || viewport.isPaintingNow()) return;
// We're painting a component which has a viewport as it's ancestor.
// As the viewport paints status text, we'll erase it, so we need to schedule a repaint for the viewport with status text's bounds.
// But it causes flicker, so we paint status text over the component first and then schedule the viewport repaint.
Rectangle textBoundsInViewport = getTextComponentBound();
int xInOwner = textBoundsInViewport.x - component.getX();
int yInOwner = textBoundsInViewport.y - component.getY();
Rectangle textBoundsInOwner = new Rectangle(xInOwner, yInOwner, textBoundsInViewport.width, textBoundsInViewport.height);
doPaintStatusText(g, textBoundsInOwner);
viewport.repaint(textBoundsInViewport);
}
private void doPaintStatusText(Graphics g, Rectangle textComponentBounds) {
myComponent.setBounds(0, 0, textComponentBounds.width, textComponentBounds.height);
Graphics2D g2 = (Graphics2D)g.create(textComponentBounds.x, textComponentBounds.y, textComponentBounds.width, textComponentBounds.height);
myComponent.paint(g2);
g2.dispose();
}
@NotNull
public SimpleColoredComponent getComponent() {
return myComponent;
}
public Dimension getPreferredSize() {
return myComponent.getPreferredSize();
}
}