/*
* Copyright 2003-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 jetbrains.mps.ide.messages;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.components.StoragePathMacros;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentManager;
import com.intellij.ui.content.MessageView;
import com.intellij.ui.content.MessageView.SERVICE;
import jetbrains.mps.RuntimeFlags;
import jetbrains.mps.ide.ThreadUtils.RunInUIRunnable;
import jetbrains.mps.ide.messages.MessageList.MessageListState;
import jetbrains.mps.ide.messages.MessagesViewTool.MessageViewToolState;
import jetbrains.mps.ide.messages.navigation.NavigationManager;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.messages.IMessage;
import jetbrains.mps.messages.IMessageHandler;
import jetbrains.mps.messages.Message;
import jetbrains.mps.messages.MessageKind;
import org.jetbrains.annotations.NotNull;
import javax.swing.JList;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@State(
name = "MessagesViewTool",
storages = @Storage(StoragePathMacros.WORKSPACE_FILE)
)
public class MessagesViewTool implements ProjectComponent, PersistentStateComponent<MessageViewToolState>, Disposable {
private static final String DEFAULT_LIST = "DEFAULT_LIST";
private final Project myProject;
private final NavigationManager myNavigationManager;
private final ToolWindowManager myToolWindowManager;
private final MyMessageList myDefaultList;
private final Map<Object, List<MessageList>> myMessageLists = new HashMap<Object, List<MessageList>>();
private final MessageViewLoggingHandler myMessageViewLoggingHandler;
public MessagesViewTool(Project project, NavigationManager navigationManager, ToolWindowManager toolWindowManager) {
myProject = project;
myNavigationManager = navigationManager;
myToolWindowManager = toolWindowManager;
myDefaultList = new MyMessageList("Messages");
Disposer.register(this, myDefaultList);
// default list doesn't need too much attention, don't activate it on any message
myDefaultList.setActivateOnMessage(false);
myDefaultList.setTitleUpdateFormat(
"{1,choice,0#--|1#1 error|2#{1} errors}/{2,choice,0#--|1#1 warning|2#{2} warnings}/{3,choice,0#--|1#1 info|2#{3} infos}");
addList(DEFAULT_LIST, myDefaultList);
myMessageViewLoggingHandler = new MessageViewLoggingHandler(this, ProjectHelper.fromIdeaProject(project));
}
@Override
public void dispose() {
myMessageLists.clear();
}
public Project getProject() {
return myProject;
}
public void clear() {
getDefaultList().clear();
}
public void clear(String listName) {
MessageList list = getAvailableList(listName, false);
if (list != null) {
list.clear();
}
}
public void add(final IMessage message) {
getDefaultList().add(message);
}
public void add(final IMessage message, String listName) {
getAvailableList(listName, true).add(message);
}
@Override
@NotNull
public String getComponentName() {
return MessagesViewTool.class.getSimpleName();
}
@Override
public void initComponent() {
getDefaultList().createContent(false, false);
}
@Override
public void disposeComponent() {
Disposer.dispose(this);
}
@Override
public void projectOpened() {
myMessageViewLoggingHandler.register();
}
@Override
public void projectClosed() {
myMessageViewLoggingHandler.unregister();
}
public static class MessageViewToolState {
public MessageViewToolState(MessageListState defaultListState) {
this.defaultListState = defaultListState;
}
public MessageViewToolState() {
// default cons is essential for IDEA to construct the state.
this.defaultListState = new MessageListState();
}
// XXX documentation of PersistentStateComponent tells it saves annotated and public fields. This one is neither of those.
// I don't see MessagesViewTool entry in .mps/workspace.xml either. Does it work?
MessageListState defaultListState;
}
@Override
public MessageViewToolState getState() {
return new MessageViewToolState(getDefaultList().getState());
}
@Override
public void loadState(MessageViewToolState state) {
getDefaultList().loadState(state.defaultListState);
}
/*package*/ MessageView getMessagesService() {
return SERVICE.getInstance(myProject);
}
public IMessageHandler newHandler() {
return new MsgHandler(getDefaultList());
}
/**
* Shorthand for {@link #newHandler(String, boolean) newHandler(name, false)}.
*/
public IMessageHandler newHandler(@NotNull final String name) {
return newHandler(name, false /*newHandler used to keep the list intact in MPS releases to date*/);
}
/**
* @param name identifies named collection of messages, value is visible in UI as the name of the collection.
* @param clear true if caller doesn't need to keep messages already reported under same named handler (if any)
* @return handler that pipes messages to designated UI component, never {@code null}
*/
public IMessageHandler newHandler(@NotNull final String name, boolean clear) {
MessageList availableList = getAvailableList(name, true);
if (clear) {
availableList.clear();
}
return new MsgHandler(availableList);
}
private synchronized void addList(String name, MessageList list) {
List<MessageList> lists = myMessageLists.containsKey(name) ? myMessageLists.get(name) : new ArrayList<MessageList>();
if (!myMessageLists.containsKey(name)) {
myMessageLists.put(name, lists);
}
lists.add(list);
}
/**
* @deprecated I don't feel it's a nice idea to expose implementation detail, {@link #newHandler()} shall suffice, perhaps,
* augmented with options object to pass info/warn/clear settings
*/
@Deprecated
public synchronized MessageList getAvailableList(String name, boolean createIfNotFound) {
List<MessageList> lists;
if (myMessageLists.containsKey(name)) {
lists = myMessageLists.get(name);
} else {
myMessageLists.put(name, lists = new ArrayList<MessageList>());
}
for (int i = lists.size() - 1; i >= 0; --i) {
MessageList messageList = lists.get(i);
if (!messageList.isPinned()) {
return messageList;
}
}
if (createIfNotFound) {
MessageList list = createList(name);
lists.add(list);
return list;
}
return null;
}
private synchronized MessageList createList(String name) {
MyMessageList list = new MyMessageList(name);
Disposer.register(this, list);
list.loadState(getDefaultList().getState());
list.setActivateOnMessage(true);
list.createContent(true, true);
return list;
}
/*package*/
synchronized void removeList(MessageList list, String name) {
List<MessageList> lists = myMessageLists.get(name);
if (lists != null) {
lists.remove(list);
}
}
private MessageList getDefaultList() {
return myDefaultList;
}
private class MyMessageList extends MessageList {
private final String myTitle;
private String myTitleUpdateFormat =
"{0}: {1,choice,0#--|1#1 error|2#{1} errors}/{2,choice,0#--|1#1 warning|2#{2} warnings}/{3,choice,0#--|1#1 info|2#{3} infos}";
/*
* getMessagesService().getContentManager() may fail with NPE as respective tool window is initialized as post-startup activity
* while users are quite quick to run rebuild (or another messages client). One way to prevent NPE is to check
* {@code myToolWindowManager.getToolWindow(ToolWindowId.MESSAGES_WINDOW) != null}, another is to track whether our createContent() completed.
*/
private boolean myContentReady = false;
protected MyMessageList(@NotNull String title) {
myTitle = title;
}
@Override
public void dispose() {
myContentReady = false;
super.dispose();
removeList(this, myTitle);
}
@Override
protected void bringToFront() {
ToolWindow window = myToolWindowManager.getToolWindow(ToolWindowId.MESSAGES_WINDOW);
if (window == null) {
return; // just in case
}
if (!window.isAvailable()) {
window.setAvailable(true, null);
}
if (!window.isVisible()) {
window.show(null);
}
Content content = getMessagesService().getContentManager().getContent(getComponent());
getMessagesService().getContentManager().setSelectedContent(content);
}
public void setTitleUpdateFormat(String pattern) {
myTitleUpdateFormat = pattern;
}
public void createContent(final boolean canClose, final boolean isMultiple) {
if (RuntimeFlags.isTestMode()) {
return;
}
final Runnable initRunnable = new Runnable() {
@Override
public void run() {
initUI();
final MessageView service = getMessagesService();
Content content = service.getContentManager().getFactory().createContent(getComponent(), myTitle, true);
content.setCloseable(canClose);
content.setPinnable(isMultiple);
if (canClose) {
content.setShouldDisposeContent(true);
content.setDisposer(MyMessageList.this);
}
content.putUserData(ToolWindow.SHOW_CONTENT_ICON, Boolean.TRUE);
service.getContentManager().addContent(content);
activateUpdate();
myContentReady = true;
}
};
getMessagesService().runWhenInitialized(new RunInUIRunnable(initRunnable, false));
}
@Override
public boolean isPinned() {
if (myContentReady) {
ContentManager contentManager = getMessagesService().getContentManager();
Content content = contentManager != null ? contentManager.getContent(getComponent()) : null;
return content != null && content.isPinned();
}
return super.isPinned();
}
@Override
protected void updateHeader() {
if (myTitle.equals(myTitleUpdateFormat) || myTitleUpdateFormat == null) {
return;
}
Content content = getMessagesService().getContentManager().getContent(getComponent());
if (content != null) {
if (hasErrors() || hasWarnings() || hasInfo()) {
final String t = MessageFormat.format(myTitleUpdateFormat, myTitle, myErrors, myWarnings, myInfos);
content.setDisplayName(t);
} else {
content.setDisplayName(myTitle);
}
}
}
@Override
protected boolean canNavigate(@NotNull IMessage message) {
return message.getHintObject() != null && myNavigationManager.canNavigateTo(message.getHintObject());
}
@Override
protected void navigate(@NotNull IMessage message, boolean focus) {
// XXX could receive Navigatable from NM and in case navigation fails (e.g. due to deleted/missing node)
// could show a balloon with explanation (it's better to do it here rather than in Navigatable itself as here we've
// got tool window to anchor.
myNavigationManager.navigateTo(message.getHintObject(), focus);
}
@Override
protected void populateActions(JList list, DefaultActionGroup group) {
ActionGroup acts = (ActionGroup) ActionManager.getInstance().getAction("MPS.MessagesView");
group.addAll(acts);
}
}
public static void log(Project p, MessageKind kind, String message) {
p.getComponent(MessagesViewTool.class).add(new Message(kind, message));
}
private static class MsgHandler implements IMessageHandler {
private final MessageList myList;
MsgHandler(@NotNull MessageList list) {
myList = list;
}
@Override
public void handle(@NotNull IMessage msg) {
myList.add(msg);
}
}
}