/* * Copyright 2003-2011 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.nodeEditor; import com.intellij.icons.AllIcons.General; import com.intellij.openapi.editor.colors.CodeInsightColors; import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.ui.ColorUtil; import com.intellij.util.IconUtil; import com.intellij.util.containers.SortedList; import com.intellij.util.ui.ButtonlessScrollBarUI; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.update.MergingUpdateQueue; import com.intellij.util.ui.update.Update; import jetbrains.mps.errors.MessageStatus; import jetbrains.mps.ide.tooltips.MPSToolTipManager; import jetbrains.mps.ide.tooltips.TooltipComponent; import jetbrains.mps.openapi.editor.cells.EditorCell; import jetbrains.mps.openapi.editor.cells.EditorCell_Collection; import jetbrains.mps.openapi.editor.message.EditorMessageOwner; import jetbrains.mps.openapi.editor.message.SimpleEditorMessage; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComponent; import java.awt.Adjustable; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.geom.AffineTransform; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; public class MessagesGutter extends ButtonlessScrollBarUI.Transparent implements TooltipComponent, MouseMotionListener, MouseListener { private static final Comparator<GutterMark> EDITOR_MESSAGES_COMPARATOR = (mark, otherMark) -> otherMark.getPriority() != mark.getPriority() ? otherMark.getPriority() - mark.getPriority() : otherMark.getStatus().ordinal() - mark.getStatus().ordinal(); private EditorComponent myEditorComponent; private MyErrorsButton myErrorsButton = new MyErrorsButton(); private List<SimpleEditorMessage> myMessages = new CopyOnWriteArrayList<>(); private List<GutterMark> myGutterMarks = Collections.emptyList(); private boolean myRightToLeft; private MergingUpdateQueue myUpdateQueue; private Object myUpdateIdentity = new Object(); public MessagesGutter(EditorComponent editorComponent, boolean rightToLeft) { myEditorComponent = editorComponent; myRightToLeft = rightToLeft; } @Override protected JButton createDecreaseButton(int orientation) { return myErrorsButton; } @Override protected void installListeners() { super.installListeners(); if (MPSToolTipManager.getInstance() != null) { MPSToolTipManager.getInstance().registerComponentRightAligned(scrollbar); } scrollbar.addMouseListener(this); scrollbar.addMouseMotionListener(this); } @Override public void uninstallListeners() { scrollbar.removeMouseMotionListener(this); scrollbar.removeMouseListener(this); if (MPSToolTipManager.getInstance() != null) { MPSToolTipManager.getInstance().unregisterComponentRightAligned(scrollbar); } super.uninstallListeners(); } //copied from com.intellij.openapi.editor.impl.EditorMarkupModelImpl @Override protected Color adjustColor(Color c) { if (isMacOverlayScrollbar()) { return super.adjustColor(c); } if (UIUtil.isUnderDarcula()) { return c; } return ColorUtil.withAlpha(ColorUtil.shift(super.adjustColor(c), 0.9), 0.85); } //copied from com.intellij.openapi.editor.impl.EditorMarkupModelImpl @Override protected void paintThumb(Graphics g, JComponent c, Rectangle thumbBounds) { if (isMacOverlayScrollbar()) { if (!myRightToLeft) { super.paintThumb(g, c, thumbBounds); } else { Graphics2D g2d = (Graphics2D) g; AffineTransform old = g2d.getTransform(); AffineTransform tx = AffineTransform.getScaleInstance(-1, 1); tx.translate(-c.getWidth(), 0); g2d.transform(tx); g.translate(1, 0); super.paintThumb(g, c, thumbBounds); g2d.setTransform(old); } } else { int shift = myRightToLeft ? -9 : 9; g.translate(shift, 0); super.paintThumb(g, c, thumbBounds); g.translate(-shift, 0); } } @Override protected void doPaintTrack(Graphics g, JComponent c, Rectangle bounds) { super.doPaintTrack(g, c, bounds); g.setColor(getEditorComponent().getBackground()); g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); drawMarks(g); } @Override protected int getThickness() { // TODO: remove magic number! return super.getThickness() + 7; } @Override public void mouseDragged(MouseEvent e) { } @Override public void mouseMoved(MouseEvent e) { List<GutterMark> gutterMarks = getGutterMarksAt(e.getY()); if (gutterMarks.size() > 0) { scrollbar.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } else { scrollbar.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } } @Override public void mouseClicked(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { int y = e.getY(); List<GutterMark> gutterMarks = getGutterMarksAt(y); if (gutterMarks.size() > 0) { GutterMark mark = gutterMarks.get(0); SimpleEditorMessage message = mark.getEditorMessage(); if (message instanceof EditorMessage) { ((EditorMessage) message).doNavigate(myEditorComponent); } else { // (markY - y) / markHeight = (realY - start) / height int realY = message.getStart(myEditorComponent) + (mark.getY() - y) * message.getHeight(myEditorComponent) / mark.getHeight(); EditorCell editorCell = myEditorComponent.findCellWeak(1, realY + 1); if (editorCell != null) { myEditorComponent.changeSelection(editorCell); } } } } @Override public void mouseReleased(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } public EditorComponent getEditorComponent() { return myEditorComponent; } private void updateGutterMarks() { if (scrollbar == null) { return; } getUpdateQueue().queue(new Update(myUpdateIdentity) { @Override public void run() { GutterStatus status = GutterStatus.OK; List<GutterMark> marks = new ArrayList<>(); for (SimpleEditorMessage message : myMessages) { GutterMark mark = new GutterMark(message); if (!mark.isValid()) { continue; } GutterStatus messageStatus = GutterStatus.getStatus(message.getStatus()); if (messageStatus.ordinal() > status.ordinal()) { status = messageStatus; } marks.add(mark); } marks.sort(new Comparator<GutterMark>() { @Override public int compare(GutterMark mark1, GutterMark mark2) { if (mark1 == mark2) { return 0; } SimpleEditorMessage message1 = mark1.getEditorMessage(); SimpleEditorMessage message2 = mark2.getEditorMessage(); if (message1 instanceof EditorMessage == message2 instanceof EditorMessage) { if (message1 instanceof EditorMessage) { return mark1.getStatus() != mark2.getStatus() ? mark1.getStatus().ordinal() - mark2.getStatus().ordinal() : mark1.getY() - mark2.getY(); } else { return mark1.getY() - mark2.getY(); } } else { return message1 instanceof EditorMessage ? 1 : -1; } } }); myGutterMarks = marks; setStatus(status); //otherwise some messages (removal of which does not affect model) could be not repainted if (scrollbar != null) { scrollbar.repaint(); } } }); } private MergingUpdateQueue getUpdateQueue() { if (myUpdateQueue == null) { myUpdateQueue = new MergingUpdateQueue("MessagesGutter", 500, true, myEditorComponent, null, null, true); myUpdateQueue.setRestartTimerOnAdd(true); // TODO add update queue to the disposables tree } return myUpdateQueue; } @Override public boolean alwaysShowTrack() { return (scrollbar.getOrientation() == Adjustable.VERTICAL) || super.alwaysShowTrack(); } private void setStatus(GutterStatus status) { switch (status) { case OK: myErrorsButton.setIcon(General.InspectionsOK); break; case WARNING: final Color warningStripeColor = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(CodeInsightColors.WARNINGS_ATTRIBUTES).getErrorStripeColor(); myErrorsButton.setIcon(IconUtil.colorize(General.InspectionsError, warningStripeColor)); break; case ERROR: final Color errorStripeColor = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(CodeInsightColors.ERRORS_ATTRIBUTES).getErrorStripeColor(); myErrorsButton.setIcon(IconUtil.colorize(General.InspectionsError, errorStripeColor)); break; case IN_PROGRESS: myErrorsButton.setIcon(General.InspectionsEye); break; } } public void add(SimpleEditorMessage message) { myMessages.add(message); updateGutterMarks(); } public void remove(SimpleEditorMessage message) { myMessages.remove(message); updateGutterMarks(); } public boolean removeMessages(EditorMessageOwner owner) { boolean removedAnything = false; for (SimpleEditorMessage m : new ArrayList<>(myMessages)) { if (m.getOwner() == owner) { myMessages.remove(m); removedAnything = true; } } updateGutterMarks(); return removedAnything; } public void dispose() { if (myUpdateQueue != null) { // TODO unsure if this is the right way to dispose the queue myUpdateQueue.dispose(); } } private void drawMarks(Graphics graphics) { for (GutterMark mark : myGutterMarks) { if (graphics.hitClip(mark.getX(), mark.getY(), mark.getWidth(), mark.getHeight())) { mark.paint(graphics); } } } private int getMessagesAreaShift() { return Math.max(0, getDecrementButtonHeight() - scrollbar.getBounds().y); } private int getMessagesAreaHeight() { return scrollbar.getHeight() - getIncrementButtonHeight() - Math.max(getDecrementButtonHeight(), scrollbar.getBounds().y); } private List<GutterMark> getGutterMarksAt(int y) { List<GutterMark> result = new SortedList<>(EDITOR_MESSAGES_COMPARATOR); for (GutterMark gutterMark : myGutterMarks) { int start = gutterMark.getY(); int end = start + gutterMark.getHeight(); if (start - 3 <= y && y <= end + 3) { result.add(gutterMark); } } return result; } @Override public String getMPSTooltipText(MouseEvent event) { int y = event.getY(); List<GutterMark> gutterMarks = getGutterMarksAt(y); if (gutterMarks.size() > 0) { StringBuilder text = new StringBuilder(); for (GutterMark mark : gutterMarks) { if (text.length() > 0) { text.append("\n"); } text.append(mark.getEditorMessage().getMessage()); } return text.toString(); } return null; } private class GutterMark { private int myX, myY, myWidth, myHeight; private Color myColor; private SimpleEditorMessage myMessage; private boolean myValid = false; GutterMark(SimpleEditorMessage message) { if ((myMessage = message) == null || (myColor = myMessage.getColor()) == null || myMessage instanceof EditorMessage && !((EditorMessage) myMessage).isValid(myEditorComponent)) { return; } myValid = true; myX = myRightToLeft ? 3 : 5; myWidth = General.InspectionsOK.getIconWidth() - 1; if (!(myMessage instanceof EditorMessage)) { // thin myWidth /= 2; myWidth += 1; myX = myRightToLeft ? myWidth + 2 : 0; } myY = calculateY(myMessage); myHeight = calculateHeight(myMessage); } private int calculateY(SimpleEditorMessage message) { return getMessagesAreaShift() + (int) (message.getStart(myEditorComponent) * (((double) getMessagesAreaHeight()) / ((double) myEditorComponent.getHeight()))); } private int calculateHeight(SimpleEditorMessage message) { int height = message.getHeight(myEditorComponent); if (message instanceof EditorMessage) { EditorCell cell = ((EditorMessage) message).getCell(myEditorComponent); if (cell != null) { while (cell instanceof EditorCell_Collection) { cell = ((EditorCell_Collection) cell).lastCell(); } if (cell != null) { height -= cell.getHeight(); } } } return (int) (height * (((double) getMessagesAreaHeight()) / ((double) myEditorComponent.getHeight()))) + 2; } public boolean isValid() { return myValid; } public void paint(Graphics g) { assert myValid; g.setColor(myColor); int x = getX(); int y = getY(); int height = Math.max(getHeight(), 3); int width = getWidth(); g.fillRect(x + 1, y, width - 2, height); Color brighter = myColor.brighter(); g.setColor(brighter); // left decoration UIUtil.drawLine(g, x, y, x, y + height); // top decoration UIUtil.drawLine(g, x + 1, y, x + width - 2, y); Color darker = ColorUtil.shift(myColor, 0.75); g.setColor(darker); // bottom decoration UIUtil.drawLine(g, x + 1, y + height, x + width - 2, y + height); // large bottom to let overwrite by hl below // right decoration UIUtil.drawLine(g, x + width - 2, y, x + width - 2, y + height - 1); } public int getX() { return myX; } public int getY() { return myY; } public int getWidth() { return myWidth; } public int getHeight() { return myHeight; } public int getPriority() { return myMessage.getPriority(); } public MessageStatus getStatus() { return myMessage.getStatus(); } public SimpleEditorMessage getEditorMessage() { return myMessage; } } private enum GutterStatus { OK, WARNING, ERROR, IN_PROGRESS; static GutterStatus getStatus(MessageStatus status) { switch (status) { case WARNING: return WARNING; case ERROR: return ERROR; } return OK; } } private class MyErrorsButton extends JButton { private MyErrorsButton() { super(General.InspectionsEye); setFocusable(false); } @Override public void paint(Graphics g) { final Rectangle bounds = getBounds(); g.setColor(getEditorComponent().getBackground()); g.fillRect(0, 0, bounds.width, bounds.height); Icon icon = getIcon(); if (icon != null) { icon.paintIcon(this, g, (getWidth() - icon.getIconWidth()) / 2, (getHeight() - icon.getIconHeight()) / 2); } } @Override public Dimension getPreferredSize() { return new Dimension(General.InspectionsOK.getIconWidth(), General.InspectionsOK.getIconHeight()); } } }