/*
* 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.icons.AllIcons;
import com.intellij.icons.AllIcons.General;
import com.intellij.ide.BrowserUtil;
import com.intellij.ide.CopyPasteManagerEx;
import com.intellij.ide.OccurenceNavigator;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.actionSystem.ToggleAction;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.ui.InputValidatorEx;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.SimpleToolWindowPanel;
import com.intellij.pom.NavigatableAdapter;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.JBPanel;
import com.intellij.usageView.UsageViewBundle;
import com.intellij.util.ExceptionUtil;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import jetbrains.mps.RuntimeFlags;
import jetbrains.mps.ide.actions.MPSActionPlaces;
import jetbrains.mps.ide.actions.MPSCommonDataKeys;
import jetbrains.mps.ide.search.SearchHistoryStorage;
import jetbrains.mps.messages.IMessage;
import jetbrains.mps.messages.IMessageList;
import jetbrains.mps.messages.MessageKind;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SNodeReference;
import sun.font.FontDesignMetrics;
import javax.swing.AbstractAction;
import javax.swing.AbstractListModel;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Distinction between MessageList and its sole subclass in MessagesViewTool is subtle and perhaps not worth
* effort. Latter knows about IDEA's MessageView and Content, former is more about Swing UI and actions, both though depend
* from IDEA platform. I doubt anyone would reuse this class any time soon.
*/
public abstract class MessageList implements IMessageList, SearchHistoryStorage, Disposable {
private final MyToggleAction myWarningsAction = new MyToggleAction("Show Warning Messages", Icons.WARNING_ICON) {
@Override
protected boolean isEnabled() {
return hasWarnings();
}
};
private final MyToggleAction myInfoAction = new MyToggleAction("Show Informational Messages", Icons.INFORMATION_ICON) {
@Override
protected boolean isEnabled() {
return hasInfo();
}
};
private final MyToggleAction myAutoscrollToSourceAction = new MyToggleAction("Autoscroll to Source", Icons.AUTOSCROLLS_ICON) {
@Override
protected boolean isEnabled() {
return hasHintObjects();
}
};
private final Queue<IMessage> myMessages = new LinkedList<>();
private MessagesListCellRenderer myCellRenderer = new MessagesListCellRenderer();
protected int myInfos;
protected int myWarnings;
protected int myErrors;
protected int myHintObjects;
private final List<String> mySearches = new ArrayList<>();
private int myMaxListSize = 10000;
protected final FastListModel<IMessage> myModel = new FastListModel<>(this.myMaxListSize);
private final RootPanel myComponent = new RootPanel();
protected final JList myList = new JBList(myModel);
private ActionToolbar myToolbar;
private final AtomicInteger myMessagesInProgress = new AtomicInteger();
private MessageToolSearchPanel mySearchPanel = null;
private final MergingUpdateQueue myUpdateQueue = new MergingUpdateQueue("MessageList", 500, false, myComponent, null, null, true);
private final Object myUpdateIdentity = new Object();
private final ConcurrentLinkedQueue<IMessage> myMessagesQueue = new ConcurrentLinkedQueue<>();
private volatile boolean myIsDisposed = false;
private boolean myActivateOnMessage = false;
protected MessageList() {
myUpdateQueue.setRestartTimerOnAdd(true);
// Recreate render to update colors after scheme change
EditorColorsManager.getInstance().addEditorColorsListener(scheme -> {
myCellRenderer = new MessagesListCellRenderer();
myList.setCellRenderer(myCellRenderer);
}, this);
}
/**
* Tells whether the list shall show up once message is added.
* XXX Note, there's difference between "just show up" and "show up and get active", but I don't know what is it
* (i.e. whether window.show() brings the window to front, or it's windows.activate() that does, see #show(boolean), below.
* Perhaps, it's just about focus gained?)
* <p>
* By default, we don't show the list when message is added.
*
* @see #bringToFront()
*/
public void setActivateOnMessage(boolean activateOnMessage) {
myActivateOnMessage = activateOnMessage;
}
@Override
public void dispose() {
myUpdateQueue.dispose();
myIsDisposed = true;
}
@Override
public void clear() {
if (RuntimeFlags.isTestMode()) {
return;
}
// 1. clear of the list shall happen synchronously with addition of messages, hence use of the same queue/update mechanism and same priority
// as update on #add()
// 2. updates are not dispatched until view has been initialized (prevents exceptions like https://youtrack.jetbrains.com/issue/MPS-24408)
// 3. though only one clear() in the queue makes sense, and we can ignore all but the last one, I didn't find a way to accomplish this with
// MergingUpdateQueue API. It seems possible to merge subsequent clear with the one already in the queue, but that would yield incorrect
// results (e.g. update requests: m1, m2, clear1, m3, clear2. If clear2 is ignored and merged with clear1, m3 would be visible)
// That's why identity is a new object each time (I don't expect thousands of successive clear() calls)
myUpdateQueue.queue(new Update(new Object()) {
@Override
public void run() {
if (myIsDisposed) {
return;
}
myModel.clear();
myMessages.clear();
myErrors = 0;
myWarnings = 0;
myInfos = 0;
myHintObjects = 0;
myList.setFixedCellWidth(myList.getWidth());
updateHeader();
updateActions();
}
});
}
@Override
public void add(IMessage message) {
if (RuntimeFlags.isTestMode()) {
return;
}
myMessagesInProgress.incrementAndGet();
myMessagesQueue.add(message);
myUpdateQueue.queue(new Update(myUpdateIdentity) {
@Override
public void run() {
if (myIsDisposed) {
return;
}
List<IMessage> messagesToAdd = new ArrayList<>();
while (!myMessagesQueue.isEmpty()) {
IMessage message = myMessagesQueue.remove();
myMessagesInProgress.decrementAndGet();
if (isVisible(message)) {
messagesToAdd.add(message);
}
myMessages.add(message);
updateMessageCounters(message, 1);
}
int messagesToRemove = 0;
if (myMessages.size() > MessageList.this.myMaxListSize) {
for (int i = Math.min(myMessages.size() - MessageList.this.myMaxListSize, myMessages.size()); i > 0; i--) {
IMessage toRemove = myMessages.remove();
updateMessageCounters(toRemove, -1);
if (isVisible(toRemove)) {
messagesToRemove++;
}
}
if (messagesToRemove > myModel.getSize()) {
messagesToAdd = messagesToAdd.subList(messagesToRemove - myModel.getSize(), messagesToAdd.size());
messagesToRemove = myModel.getSize();
}
}
if (messagesToRemove > 0) {
myModel.removeFirst(messagesToRemove);
}
myModel.addAll(messagesToAdd);
int maxWidth = -1;
for (IMessage message : messagesToAdd) {
maxWidth = Math.max(maxWidth, getMessageWidth(message));
}
int index = myModel.getSize() - 1;
if (myList.getAutoscrolls()) {
myList.getSelectionModel().setSelectionInterval(index, index);
}
if (myMessagesInProgress.get() == 0) {
myList.ensureIndexIsVisible(index);
}
if (maxWidth > myList.getFixedCellWidth()) {
myList.setFixedCellWidth(maxWidth);
}
updateHeader();
updateActions();
if (myActivateOnMessage && messagesToAdd.size() > 0) {
bringToFront();
}
}
private void updateMessageCounters(IMessage m, int delta) {
if (m.getKind() == MessageKind.ERROR) {
myErrors += delta;
}
if (m.getKind() == MessageKind.WARNING) {
myWarnings += delta;
}
if (m.getKind() == MessageKind.INFORMATION) {
myInfos += delta;
}
if (m.getHintObject() != null) {
myHintObjects += delta;
}
}
});
}
@Override
public List<String> getSearches() {
return new ArrayList<>(mySearches);
}
@Override
public void setSearches(List<String> searches) {
mySearches.clear();
mySearches.addAll(searches);
}
public JComponent getComponent() {
return myComponent;
}
protected void initUI() {
myList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
myList.setCellRenderer(myCellRenderer);
DefaultActionGroup group = new DefaultActionGroup();
group.add(myWarningsAction);
group.add(myInfoAction);
group.add(myAutoscrollToSourceAction);
group.add(new MessagesLimitAction());
group.add(new ClearAction());
myToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, group, false);
myComponent.setToolbar(myToolbar.getComponent());
final JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myList, true);
// Add MouseWheelListener to scrollPane instead of myList itself, because otherwise scrollPane default behaviour will be blocked
scrollPane.addMouseWheelListener(e -> {
// Need to convert event to get right mouse point relative to list
final MouseEvent mouseEvent = SwingUtilities.convertMouseEvent(e.getComponent(), e, myList);
myCellRenderer.setIndexUnderMouse(myList.locationToIndex(mouseEvent.getPoint()));
});
myComponent.setContent(scrollPane);
myComponent.registerKeyboardAction(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (mySearchPanel == null) {
mySearchPanel = new MessageToolSearchPanel(myList, MessageList.this);
myComponent.add(mySearchPanel, BorderLayout.NORTH);
}
mySearchPanel.activate();
}
}, KeyStroke.getKeyStroke('F', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myList.setFixedCellHeight(FontDesignMetrics.getMetrics(myList.getFont()).getHeight() + 5);
final AbstractAction openCurrentMessage = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
openCurrentMessageIfPossible();
}
};
myList.registerKeyboardAction(openCurrentMessage, KeyStroke.getKeyStroke("F4"), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myList.registerKeyboardAction(openCurrentMessage, KeyStroke.getKeyStroke("ENTER"), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myList.registerKeyboardAction(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
showHelpForCurrentMessage();
}
}, KeyStroke.getKeyStroke("F1"), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myList.registerKeyboardAction(e -> selectAll(),
KeyStroke.getKeyStroke('A', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
myList.addMouseListener(new MouseAdapter() {
// Holds index of item, that was under cursor on mouse press action
private int mousePressedIndex = -1;
@Override
public void mouseClicked(MouseEvent e) {
boolean oneClickOpen = e.getClickCount() == 1 && e.getButton() == MouseEvent.BUTTON1 && myAutoscrollToSourceAction.isSelected(null);
boolean twoClickOpen = e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1;
if (oneClickOpen || twoClickOpen) {
openCurrentMessageIfPossible();
}
}
@Override
public void mousePressed(MouseEvent e) {
if (e.isPopupTrigger()) {
showPopupMenu(e);
} else {
mousePressedIndex = myList.locationToIndex(e.getPoint());
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.isPopupTrigger()) {
showPopupMenu(e);
return;
}
int index = myList.locationToIndex(e.getPoint());
if (mousePressedIndex != -1 && index != -1 && mousePressedIndex != index) {
myList.addSelectionInterval(mousePressedIndex, index);
}
// reset saved value to be set on next mouse press
mousePressedIndex = -1;
}
});
// This does not select message after scroll, but adding mouse scroll listener will block scrolling
myList.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent event) {
int index = myList.locationToIndex(event.getPoint());
final IMessage message = index != -1 && index >= myList.getFirstVisibleIndex() && index <= myList.getLastVisibleIndex()
? myModel.getElementAt(index) : null;
if (message == null || !myAutoscrollToSourceAction.isSelected(null)) {
myList.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
myCellRenderer.setIndexUnderMouse(-1);
myList.repaint();
return;
}
boolean canNavigate = canNavigate(message);
myList.setCursor(Cursor.getPredefinedCursor(canNavigate ? Cursor.HAND_CURSOR : Cursor.DEFAULT_CURSOR));
myCellRenderer.setIndexUnderMouse(index);
myList.repaint();
}
});
}
protected void openCurrentMessageIfPossible() {
final IMessage selectedMessage = (IMessage) myList.getSelectedValue();
if (selectedMessage != null && canNavigate(selectedMessage)) {
navigate(selectedMessage, true);
}
}
private void selectAll() {
myList.setSelectionInterval(0, myList.getModel().getSize() - 1);
}
/**
* Need to be invoked in EDT.
* Check if {@link IMessage} holds element, which can be navigated too
*
* @param message to be check for holding navigatable element
* @return <code>true<code/> if message can provide navigatable element
*/
protected abstract boolean canNavigate(@NotNull IMessage message);
/**
* Need to be invoked in EDT.
* Check if {@link IMessage} holds element, which can be navigated too
*
* @param message holds navigatable element
* @param focus sets obligation to focus element
*/
protected abstract void navigate(@NotNull IMessage message, boolean focus);
/**
* Invoke when visible messages are added, and list requires exposure to user
*/
protected abstract void bringToFront();
/**
* Override and implement registration of UI component into the MessageView.
* Don't forget to activate message dispatch once content is ready.
*
* @see #activateUpdate()
*/
public abstract void createContent(final boolean canClose, final boolean isMultiple);
/**
* MessageList doesn't update once instantiated, activate update dispatch once
* content is created ({@link #createContent(boolean, boolean)})
*/
protected final void activateUpdate() {
myUpdateQueue.activate();
}
public boolean isPinned() {
return false;
}
private void showPopupMenu(MouseEvent event) {
final int index = myList.locationToIndex(event.getPoint());
final IMessage message = index != -1 ? myModel.getElementAt(index) : null;
if (message != null && !myList.getSelectedValuesList().contains(message)) {
myList.setSelectedIndex(index);
}
DefaultActionGroup group = createActionGroup();
JPopupMenu menu = ActionManager.getInstance().createActionPopupMenu(MPSActionPlaces.MPS_MESSAGES_POPUP, group).getComponent();
menu.show(myList, event.getX(), event.getY());
}
@SuppressWarnings({"ThrowableInstanceNeverThrown", "unchecked"})
private DefaultActionGroup createActionGroup() {
DefaultActionGroup group = new DefaultActionGroup();
final List<IMessage> selectedValues = myList.getSelectedValuesList();
if (!selectedValues.isEmpty()) {
final StringBuilder sb = new StringBuilder();
for (IMessage message : selectedValues) {
sb.append(message.getText());
sb.append("\n");
if (message.getException() != null) {
sb.append(ExceptionUtil.getThrowableText(message.getException()));
}
}
group.add(new CopyToClipboardAction("Copy Text").setTextToCopy(sb.toString()));
Object hintObj;
if (selectedValues.size() == 1 && (hintObj = (selectedValues.get(0)).getHintObject()) != null) {
SNodeId nodeId = hintObj instanceof SNodeReference ? ((SNodeReference) hintObj).getNodeId() : null;
if (nodeId != null) {
group.add(new CopyToClipboardAction("Copy Node Id").setTextToCopy(nodeId.toString()));
}
}
}
group.addSeparator();
group.add(new AnAction("Show Help for This Message", null, null) {
@Override
public void update(AnActionEvent e) {
boolean enabled = getHelpUrlForCurrentMessage() != null;
Presentation presentation = e.getPresentation();
presentation.setEnabled(enabled);
presentation.setVisible(enabled);
}
@Override
public void actionPerformed(AnActionEvent e) {
showHelpForCurrentMessage();
}
});
group.addSeparator();
populateActions(myList, group);
group.addSeparator();
group.add(new ClearAction());
group.add(new SelectAllAction());
return group;
}
protected abstract void populateActions(JList list, DefaultActionGroup group);
private void showHelpForCurrentMessage() {
String helpURL = getHelpUrlForCurrentMessage();
if (helpURL == null) {
return;
}
BrowserUtil.browse(helpURL);
}
private String getHelpUrlForCurrentMessage() {
if (myList.getSelectedValuesList().size() != 1) {
return null;
}
IMessage message = (IMessage) (myList.getSelectedValue());
return message.getHelpUrl();
}
private void rebuildModel() {
myModel.clear();
myList.setFixedCellWidth(myList.getWidth());
List<IMessage> messagesToAdd = new ArrayList<>();
int width = 0;
for (IMessage m : myMessages) {
if (isVisible(m)) {
width = Math.max(width, getMessageWidth(m));
messagesToAdd.add(m);
}
}
myList.setFixedCellWidth(width);
myModel.addAll(messagesToAdd);
}
private int getMessageWidth(IMessage message) {
Component renderer = myCellRenderer.getListCellRendererComponent(myList, message, 0, false, false);
return renderer.getPreferredSize().width;
}
public boolean isVisible(IMessage m) {
switch (m.getKind()) {
case ERROR:
return true;
case WARNING:
return myWarningsAction.isSelected(null);
case INFORMATION:
return myInfoAction.isSelected(null);
}
return true;
}
protected void updateHeader() {
}
private void updateActions() {
if (myToolbar == null) {
return;
}
myToolbar.updateActionsImmediately();
}
protected boolean hasErrors() {
return myErrors > 0;
}
protected boolean hasWarnings() {
return myWarnings > 0;
}
protected boolean hasInfo() {
return myInfos > 0;
}
protected boolean hasHintObjects() {
return myHintObjects > 0;
}
private static class CopyToClipboardAction extends AnAction {
private String myTextToCopy;
CopyToClipboardAction(String actionTitle) {
super(actionTitle);
}
public CopyToClipboardAction setTextToCopy(String textToCopy) {
myTextToCopy = textToCopy;
return this;
}
@Override
public void update(AnActionEvent e) {
super.update(e);
e.getPresentation().setEnabled(myTextToCopy != null);
}
@Override
public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
CopyPasteManagerEx.getInstance().setContents(new StringSelection(myTextToCopy));
}
}
private class MyToggleAction extends ToggleAction {
private boolean mySelected;
private final Icon myIcon;
public MyToggleAction(String tooltip, Icon icon) {
super("", tooltip, icon);
myIcon = icon;
mySelected = true;
}
@Override
public boolean isSelected(AnActionEvent e) {
return mySelected;
}
@Override
public void setSelected(AnActionEvent e, boolean state) {
mySelected = state;
rebuildModel();
}
@Override
public void update(@NotNull AnActionEvent e) {
super.update(e);
boolean enabled = isEnabled();
Icon icon = enabled ? myIcon : UIManager.getLookAndFeel().getDisabledIcon(null, myIcon);
if (icon == null) {
icon = myIcon;
}
e.getPresentation().setIcon(icon);
}
protected boolean isEnabled() {
return true;
}
}
private class MessagesLimitAction extends AnAction {
private MessagesLimitAction() {
super(General.Settings);
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
String result = Messages.showInputDialog(MessageList.this.myComponent, "Set max number of showing messages", "Messages Limit", null,
String.valueOf(MessageList.this.myMaxListSize),
new InputValidatorEx() {
@Nullable
@Override
public String getErrorText(String inputString) {
return checkInput(inputString) ? null : "Enter correct number";
}
@Override
public boolean checkInput(String inputString) {
try {
final Integer i = Integer.valueOf(inputString);
if (i < 1) {
return false;
}
} catch (NumberFormatException nfe) {
return false;
}
return true;
}
@Override
public boolean canClose(String inputString) {
return checkInput(inputString);
}
});
if (result != null) {
MessageList.this.myMaxListSize = Integer.valueOf(result);
}
}
}
/*package*/ MessageListState getState() {
return new MessageListState(myWarningsAction.isSelected(null), myInfoAction.isSelected(null), myAutoscrollToSourceAction.isSelected(null), mySearches,
myMaxListSize);
}
/*package*/ void loadState(MessageListState state) {
myWarningsAction.setSelected(null, state.isWarnings());
myInfoAction.setSelected(null, state.isInfo());
myAutoscrollToSourceAction.setSelected(null, state.isAutoscrollToSource());
setSearches(state.getSearches());
myMaxListSize = state.getMaxListSize();
}
public void setWarningsEnabled(boolean enabled) {
myWarningsAction.setSelected(null, enabled);
}
public void setInfoEnabled(boolean enabled) {
myInfoAction.setSelected(null, enabled);
}
public /*for tests*/ static class FastListModel<T> extends AbstractListModel<T> {
private int myStart;
private int myEnd;
private int mySize;
private final T[] myItems;
@SuppressWarnings("unchecked")
FastListModel(int size) {
myItems = (T[]) new Object[size];
myStart = 0;
myEnd = 0;
}
@Override
public int getSize() {
return mySize;
}
@Override
public T getElementAt(int index) {
return myItems[(myStart + index) % myItems.length];
}
public void add(T item) {
if (mySize == myItems.length) {
throw new RuntimeException("Buffer overflow");
}
myItems[myEnd] = item;
myEnd = (myEnd + 1) % myItems.length;
mySize++;
fireIntervalAdded(this, mySize - 1, mySize - 1);
}
public void addAll(Collection<T> items) {
if (items.isEmpty()) {
return;
}
if (mySize + items.size() > myItems.length) {
throw new RuntimeException("Buffer overflow");
}
int intervalStart = mySize;
for (T item : items) {
myItems[myEnd] = item;
myEnd = (myEnd + 1) % myItems.length;
mySize++;
}
fireIntervalAdded(this, intervalStart, mySize - 1);
}
public void removeFirst() {
if (mySize == 0) {
throw new RuntimeException("Buffer underflow");
}
myItems[myStart] = null;
myStart = (myStart + 1) % myItems.length;
mySize--;
fireIntervalRemoved(this, 0, 0);
}
public void removeFirst(int count) {
if (count <= 0) {
throw new IllegalArgumentException("Illegal count value " + count);
}
if (mySize - count < 0) {
throw new RuntimeException("Buffer underflow");
}
for (int i = 0; i < count; i++) {
myItems[myStart] = null;
myStart = (myStart + 1) % myItems.length;
mySize--;
}
fireIntervalRemoved(this, 0, count - 1);
}
public void clear() {
myStart = 0;
myEnd = 0;
int oldSize = mySize;
mySize = 0;
Arrays.fill(myItems, null);
if (oldSize > 0) {
fireIntervalRemoved(this, 0, oldSize - 1);
}
}
}
public static class MessageListState {
private boolean myWarnings;
private boolean myInfo;
private boolean myAutoscrollToSource;
private List<String> mySearches = new ArrayList<>();
private int myMaxListSize = 10000;
public MessageListState() {
}
public MessageListState(boolean warnings, boolean info, boolean autoscrollToSource, List<String> searches, int maxListSize) {
myWarnings = warnings;
myInfo = info;
myAutoscrollToSource = autoscrollToSource;
mySearches = new ArrayList<>(searches);
myMaxListSize = maxListSize;
}
public boolean isWarnings() {
return myWarnings;
}
public void setWarnings(boolean warnings) {
myWarnings = warnings;
}
public boolean isInfo() {
return myInfo;
}
public void setInfo(boolean info) {
myInfo = info;
}
public boolean isAutoscrollToSource() {
return myAutoscrollToSource;
}
public void setAutoscrollToSource(boolean autoscrollToSource) {
myAutoscrollToSource = autoscrollToSource;
}
public List<String> getSearches() {
return mySearches;
}
public void setSearches(List<String> searches) {
mySearches = searches;
}
public int getMaxListSize() {
return myMaxListSize;
}
public void setMaxListSize(int maxListSize) {
myMaxListSize = maxListSize;
}
}
private class RootPanel extends SimpleToolWindowPanel implements OccurenceNavigator {
public RootPanel() {
super(false, true);
}
@SuppressWarnings("unchecked")
@Override
public Object getData(@NonNls String id) {
if (MPSCommonDataKeys.EXCEPTION.getName().equals(id)) {
Throwable exc = null;
for (IMessage message : (List<IMessage>) myList.getSelectedValuesList()) {
exc = message.getException();
if (exc != null) {
break;
}
}
return exc;
}
if (MPSCommonDataKeys.MESSAGES.getName().equals(id)) {
List selectedValues = myList.getSelectedValuesList();
return selectedValues == null || selectedValues.isEmpty() ? null : selectedValues;
}
if (PlatformDataKeys.HELP_ID.is(id)) {
return "ideaInterface.messageList";
}
return super.getData(id);
}
@Override
public boolean hasNextOccurence() {
return next(1, false) != null;
}
@Override
public boolean hasPreviousOccurence() {
return next(-1, false) != null;
}
@Override
public OccurenceInfo goNextOccurence() {
return next(1, true);
}
@Override
public OccurenceInfo goPreviousOccurence() {
return next(-1, true);
}
@Nullable
private OccurenceInfo next(final int delta, boolean doMove) {
int current = myList.getSelectedIndex();
for (current += delta; current >= 0 && current < myModel.getSize(); current += delta) {
IMessage msg = myModel.getElementAt(current);
if (msg.getHintObject() == null) {
continue;
}
if (doMove) {
myList.setSelectedIndex(current);
myList.ensureIndexIsVisible(current);
}
return new OccurenceInfo(new NavigatableAdapter() {
@Override
public void navigate(boolean requestFocus) {
openCurrentMessageIfPossible();
}
}, current, myModel.getSize());
}
return null;
}
@Override
public String getNextOccurenceActionName() {
return UsageViewBundle.message("action.next.occurrence");
}
@Override
public String getPreviousOccurenceActionName() {
return UsageViewBundle.message("action.previous.occurrence");
}
}
private class ClearAction extends AnAction {
public ClearAction() {
super("Clear", "Clear all messages", Icons.CLEAR_ICON);
}
@Override
public void actionPerformed(AnActionEvent e) {
clear();
}
}
private class SelectAllAction extends AnAction {
public SelectAllAction() {
super("Select All", "", AllIcons.Actions.Selectall);
}
@Override
public void actionPerformed(AnActionEvent e) {
selectAll();
}
}
}