/*
* 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.notification;
import com.google.common.util.concurrent.Atomics;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ex.ActionUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupAdapter;
import com.intellij.openapi.ui.popup.LightweightWindowEvent;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.reference.SoftReference;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
/**
* Notification bean class contains <b>title:</b>subtitle, content (plain text or HTML) and actions.
* <br><br>
* The notifications, generally, are shown in the balloons that appear on the screen when the corresponding events take place.<br>
* Notification balloon is of two types: two or three lines.<br>
* Two lines: title and content line; title and actions; content line and actions; contents on two lines.<br>
* Three lines: title and content line and actions; contents on two lines and actions; contents on three lines or more; etc.
* <br><br>
* Warning: be careful not to use the links in HTML content, use {@link #addAction(AnAction)}
*
* @author spleaner
* @author Alexander Lobas
*/
public class Notification {
private static final Logger LOG = Logger.getInstance("#com.intellij.notification.Notification");
private static final DataKey<Notification> KEY = DataKey.create("Notification");
public final String id;
private final String myGroupId;
private Icon myIcon;
private final NotificationType myType;
private String myTitle;
private String mySubtitle;
private String myContent;
private NotificationListener myListener;
private String myDropDownText;
private List<AnAction> myActions;
private AtomicReference<Boolean> myExpired = Atomics.newReference(false);
private Runnable myWhenExpired;
private Boolean myImportant;
private WeakReference<Balloon> myBalloonRef;
private final long myTimestamp;
public Notification(@NotNull String groupDisplayId, @NotNull Icon icon, @NotNull NotificationType type) {
this(groupDisplayId, icon, null, null, null, type, null);
}
/**
* @param groupDisplayId this should be a human-readable, capitalized string like "Facet Detector".
* It will appear in "Notifications" configurable.
* @param icon notification icon, if <b>null</b> used icon from type
* @param title notification title
* @param subtitle notification subtitle
* @param content notification content
* @param type notification type
* @param listener notification lifecycle listener
*/
public Notification(@NotNull String groupDisplayId,
@NotNull Icon icon,
@Nullable String title,
@Nullable String subtitle,
@Nullable String content,
@NotNull NotificationType type,
@Nullable NotificationListener listener) {
myGroupId = groupDisplayId;
myTitle = StringUtil.notNullize(title);
myContent = StringUtil.notNullize(content);
myType = type;
myListener = listener;
myTimestamp = System.currentTimeMillis();
myIcon = icon;
mySubtitle = subtitle;
LOG.assertTrue(hasTitle() || hasContent(),
"Notification should have title: " + title + " and/or subtitle and/or content groupId: " + myGroupId);
id = calculateId(this);
}
public Notification(@NotNull String groupDisplayId, @NotNull String title, @NotNull String content, @NotNull NotificationType type) {
this(groupDisplayId, title, content, type, null);
}
/**
* @param groupDisplayId this should be a human-readable, capitalized string like "Facet Detector".
* It will appear in "Notifications" configurable.
* @param title notification title
* @param content notification content
* @param type notification type
* @param listener notification lifecycle listener
*/
public Notification(@NotNull String groupDisplayId,
@NotNull String title,
@NotNull String content,
@NotNull NotificationType type,
@Nullable NotificationListener listener) {
myGroupId = groupDisplayId;
myTitle = title;
myContent = content;
myType = type;
myListener = listener;
myTimestamp = System.currentTimeMillis();
LOG.assertTrue(hasContent(), "Notification should have content, title: " + title + ", groupId: " + myGroupId);
id = calculateId(this);
}
/**
* Returns the time (in milliseconds since Jan 1, 1970) when the notification was created.
*/
public long getTimestamp() {
return myTimestamp;
}
@Nullable
public Icon getIcon() {
return myIcon;
}
@NotNull
public Notification setIcon(@Nullable Icon icon) {
myIcon = icon;
return this;
}
@NotNull
public String getGroupId() {
return myGroupId;
}
public boolean hasTitle() {
return !StringUtil.isEmptyOrSpaces(myTitle) || !StringUtil.isEmptyOrSpaces(mySubtitle);
}
@NotNull
public String getTitle() {
return myTitle;
}
@NotNull
public Notification setTitle(@Nullable String title) {
myTitle = StringUtil.notNullize(title);
return this;
}
@NotNull
public Notification setTitle(@Nullable String title, @Nullable String subtitle) {
return setTitle(title).setSubtitle(subtitle);
}
@Nullable
public String getSubtitle() {
return mySubtitle;
}
@NotNull
public Notification setSubtitle(@Nullable String subtitle) {
mySubtitle = subtitle;
return this;
}
public boolean hasContent() {
return !StringUtil.isEmptyOrSpaces(myContent);
}
@NotNull
public String getContent() {
return myContent;
}
@NotNull
public Notification setContent(@Nullable String content) {
myContent = StringUtil.notNullize(content);
return this;
}
@Nullable
public NotificationListener getListener() {
return myListener;
}
@NotNull
public Notification setListener(@NotNull NotificationListener listener) {
myListener = listener;
return this;
}
@NotNull
public List<AnAction> getActions() {
return ContainerUtil.notNullize(myActions);
}
@NotNull
public static Notification get(@NotNull AnActionEvent e) {
//noinspection ConstantConditions
return e.getData(KEY);
}
public static void fire(@NotNull final Notification notification, @NotNull AnAction action) {
AnActionEvent event = AnActionEvent.createFromAnAction(action, null, ActionPlaces.UNKNOWN, new DataContext() {
@Nullable
@Override
public Object getData(@NonNls String dataId) {
return KEY.getName().equals(dataId) ? notification : null;
}
});
if (ActionUtil.lastUpdateAndCheckDumb(action, event, false)) {
ActionUtil.performActionDumbAware(action, event);
}
}
public static void setDataProvider(@NotNull Notification notification, @NotNull JComponent component) {
DataManager.registerDataProvider(component, new DataProvider() {
@Nullable
@Override
public Object getData(@NonNls String dataId) {
return KEY.getName().equals(dataId) ? notification : null;
}
});
}
@NotNull
public String getDropDownText() {
if (myDropDownText == null) {
myDropDownText = "Actions";
}
return myDropDownText;
}
/**
* @param dropDownText text for popup when all actions collapsed (when all actions width more notification width)
*/
@NotNull
public Notification setDropDownText(@NotNull String dropDownText) {
myDropDownText = dropDownText;
return this;
}
@NotNull
public Notification addAction(@NotNull AnAction action) {
if (myActions == null) {
myActions = new ArrayList<>();
}
myActions.add(action);
return this;
}
@NotNull
public NotificationType getType() {
return myType;
}
public boolean isExpired() {
return myExpired.get();
}
public void expire() {
if (!myExpired.compareAndSet(false, true)) return;
UIUtil.invokeLaterIfNeeded(() -> hideBalloon());
NotificationsManager.getNotificationsManager().expire(this);
Runnable whenExpired = myWhenExpired;
if (whenExpired != null) whenExpired.run();
}
public Notification whenExpired(@Nullable Runnable whenExpired) {
myWhenExpired = whenExpired;
return this;
}
public void hideBalloon() {
if (myBalloonRef != null) {
final Balloon balloon = myBalloonRef.get();
if (balloon != null) {
balloon.hide();
}
myBalloonRef = null;
}
}
public void setBalloon(@NotNull final Balloon balloon) {
hideBalloon();
myBalloonRef = new WeakReference<>(balloon);
balloon.addListener(new JBPopupAdapter() {
@Override
public void onClosed(LightweightWindowEvent event) {
if (SoftReference.dereference(myBalloonRef) == balloon) {
myBalloonRef = null;
}
}
});
}
@Nullable
public Balloon getBalloon() {
return SoftReference.dereference(myBalloonRef);
}
public void notify(@Nullable Project project) {
Notifications.Bus.notify(this, project);
}
public Notification setImportant(boolean important) {
myImportant = important;
return this;
}
public boolean isImportant() {
if (myImportant != null) {
return myImportant;
}
return getListener() != null || !ContainerUtil.isEmpty(myActions);
}
@NotNull
private static String calculateId(@NotNull Object notification) {
return String.valueOf(System.currentTimeMillis()) + "." + String.valueOf(System.identityHashCode(notification));
}
}