/* * 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. */ /* * @author max */ package com.intellij.ui; import com.intellij.ide.PowerSaveMode; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.util.ProgressIndicatorUtils; import com.intellij.openapi.project.IndexNotReadyException; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.ScalableIcon; import com.intellij.openapi.util.registry.Registry; import com.intellij.ui.tabs.impl.TabLabel; import com.intellij.util.Alarm; import com.intellij.util.Function; import com.intellij.util.concurrency.AppExecutorUtil; import com.intellij.util.containers.TransferToEDTQueue; import com.intellij.util.ui.EmptyIcon; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.plaf.TreeUI; import javax.swing.plaf.basic.BasicTreeUI; import java.awt.*; import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.Executor; public class DeferredIconImpl<T> extends JBUI.UpdatingScalableJBIcon<DeferredIconImpl<T>> implements DeferredIcon, RetrievableIcon { private static final Logger LOG = Logger.getInstance("#com.intellij.ui.DeferredIconImpl"); private static final int MIN_AUTO_UPDATE_MILLIS = 950; private static final RepaintScheduler ourRepaintScheduler = new RepaintScheduler(); @NotNull private final Icon myDelegateIcon; private volatile Icon myScaledDelegateIcon; private Function<T, Icon> myEvaluator; private volatile boolean myIsScheduled; private T myParam; private static final Icon EMPTY_ICON = JBUI.scale(EmptyIcon.create(16)); private final boolean myNeedReadAction; private boolean myDone; private final boolean myAutoUpdatable; private long myLastCalcTime; private long myLastTimeSpent; private static final Executor ourIconsCalculatingExecutor = AppExecutorUtil.createBoundedApplicationPoolExecutor("ourIconsCalculating pool",1); private final IconListener<T> myEvalListener; private static final TransferToEDTQueue<Runnable> ourLaterInvocator = TransferToEDTQueue.createRunnableMerger("Deferred icon later invocator", 200); private DeferredIconImpl(@NotNull DeferredIconImpl<T> icon) { super(icon); myDelegateIcon = icon.myDelegateIcon; myScaledDelegateIcon = icon.myDelegateIcon; myEvaluator = icon.myEvaluator; myIsScheduled = icon.myIsScheduled; myParam = icon.myParam; myNeedReadAction = icon.myNeedReadAction; myDone = icon.myDone; myAutoUpdatable = icon.myAutoUpdatable; myLastCalcTime = icon.myLastCalcTime; myLastTimeSpent = icon.myLastTimeSpent; myEvalListener = icon.myEvalListener; } @NotNull @Override protected DeferredIconImpl<T> copy() { return new DeferredIconImpl<>(this); } @Override public void setScale(float scale) { if (getScale() != scale && myDelegateIcon instanceof ScalableIcon) { myScaledDelegateIcon = ((ScalableIcon)myDelegateIcon).scale(scale); super.setScale(scale); } } private static class Holder { private static final boolean CHECK_CONSISTENCY = ApplicationManager.getApplication().isUnitTestMode(); } DeferredIconImpl(Icon baseIcon, T param, @NotNull Function<T, Icon> evaluator, @NotNull IconListener<T> listener, boolean autoUpdatable) { this(baseIcon, param, true, evaluator, listener, autoUpdatable); } public DeferredIconImpl(Icon baseIcon, T param, final boolean needReadAction, @NotNull Function<T, Icon> evaluator) { this(baseIcon, param, needReadAction, evaluator, null, false); } private DeferredIconImpl(Icon baseIcon, T param, boolean needReadAction, @NotNull Function<T, Icon> evaluator, @Nullable IconListener<T> listener, boolean autoUpdatable) { myParam = param; myDelegateIcon = nonNull(baseIcon); myScaledDelegateIcon = myDelegateIcon; myEvaluator = evaluator; myNeedReadAction = needReadAction; myEvalListener = listener; myAutoUpdatable = autoUpdatable; checkDelegationDepth(); } private void checkDelegationDepth() { int depth = 0; DeferredIconImpl each = this; while (each.myScaledDelegateIcon instanceof DeferredIconImpl && depth < 50) { depth++; each = (DeferredIconImpl)each.myScaledDelegateIcon; } if (depth >= 50) { LOG.error("Too deep deferred icon nesting"); } } @NotNull private static Icon nonNull(final Icon icon) { return icon == null ? EMPTY_ICON : icon; } @Override public void paintIcon(final Component c, @NotNull final Graphics g, final int x, final int y) { if (!(myScaledDelegateIcon instanceof DeferredIconImpl && ((DeferredIconImpl)myScaledDelegateIcon).myScaledDelegateIcon instanceof DeferredIconImpl)) { myScaledDelegateIcon.paintIcon(c, g, x, y); //SOE protection } if (isDone() || myIsScheduled || PowerSaveMode.isEnabled()) { return; } myIsScheduled = true; final Component target = getTarget(c); final Component paintingParent = SwingUtilities.getAncestorOfClass(PaintingParent.class, c); final Rectangle paintingParentRec = paintingParent == null ? null : ((PaintingParent)paintingParent).getChildRec(c); ourIconsCalculatingExecutor.execute(() -> { int oldWidth = myScaledDelegateIcon.getIconWidth(); final Icon[] evaluated = new Icon[1]; final long startTime = System.currentTimeMillis(); if (myNeedReadAction) { boolean result = ProgressIndicatorUtils.runInReadActionWithWriteActionPriority(() -> { IconDeferrerImpl.evaluateDeferred(() -> evaluated[0] = evaluate()); if (myAutoUpdatable) { myLastCalcTime = System.currentTimeMillis(); myLastTimeSpent = myLastCalcTime - startTime; } }); if (!result) { myIsScheduled = false; return; } } else { IconDeferrerImpl.evaluateDeferred(() -> evaluated[0] = evaluate()); if (myAutoUpdatable) { myLastCalcTime = System.currentTimeMillis(); myLastTimeSpent = myLastCalcTime - startTime; } } final Icon result = evaluated[0]; myScaledDelegateIcon = result; checkDelegationDepth(); final boolean shouldRevalidate = Registry.is("ide.tree.deferred.icon.invalidates.cache") && myScaledDelegateIcon.getIconWidth() != oldWidth; ourLaterInvocator.offer(() -> { setDone(result); Component actualTarget = target; if (actualTarget != null && SwingUtilities.getWindowAncestor(actualTarget) == null) { actualTarget = paintingParent; if (actualTarget == null || SwingUtilities.getWindowAncestor(actualTarget) == null) { actualTarget = null; } } if (actualTarget == null) return; if (shouldRevalidate) { // revalidate will not work: JTree caches size of nodes if (actualTarget instanceof JTree) { final TreeUI ui = ((JTree)actualTarget).getUI(); if (ui instanceof BasicTreeUI) { // this call is "fake" and only need to reset tree layout cache ((BasicTreeUI)ui).setLeftChildIndent(UIUtil.getTreeLeftChildIndent()); } } } if (c == actualTarget) { c.repaint(x, y, getIconWidth(), getIconHeight()); } else { ourRepaintScheduler.pushDirtyComponent(actualTarget, paintingParentRec); } }); }); } private static Component getTarget(Component c) { final Component target; final Container list = SwingUtilities.getAncestorOfClass(JList.class, c); if (list != null) { target = list; } else { final Container tree = SwingUtilities.getAncestorOfClass(JTree.class, c); if (tree != null) { target = tree; } else { final Container table = SwingUtilities.getAncestorOfClass(JTable.class, c); if (table != null) { target = table; } else { final Container box = SwingUtilities.getAncestorOfClass(JComboBox.class, c); if (box != null) { target = box; } else { final Container tabLabel = SwingUtilities.getAncestorOfClass(TabLabel.class, c); target = tabLabel == null ? c : tabLabel; } } } } return target; } void setDone(@NotNull Icon result) { if (myEvalListener != null) { myEvalListener.evalDone(this, myParam, result); } myDone = true; if (!myAutoUpdatable) { myEvaluator = null; myParam = null; } } @Nullable @Override public Icon retrieveIcon() { return isDone() ? myScaledDelegateIcon : evaluate(); } @NotNull @Override public Icon evaluate() { Icon result; try { result = nonNull(myEvaluator.fun(myParam)); } catch (IndexNotReadyException e) { result = EMPTY_ICON; } if (Holder.CHECK_CONSISTENCY) { checkDoesntReferenceThis(result); } if (getScale() != 1f && result instanceof ScalableIcon) { result = ((ScalableIcon)result).scale(getScale()); } return result; } private void checkDoesntReferenceThis(final Icon icon) { if (icon == this) { throw new IllegalStateException("Loop in icons delegation"); } if (icon instanceof DeferredIconImpl) { checkDoesntReferenceThis(((DeferredIconImpl)icon).myScaledDelegateIcon); } else if (icon instanceof LayeredIcon) { for (Icon layer : ((LayeredIcon)icon).getAllLayers()) { checkDoesntReferenceThis(layer); } } else if (icon instanceof RowIcon) { final RowIcon rowIcon = (RowIcon)icon; final int count = rowIcon.getIconCount(); for (int i = 0; i < count; i++) { checkDoesntReferenceThis(rowIcon.getIcon(i)); } } } @Override public int getIconWidth() { return myScaledDelegateIcon.getIconWidth(); } @Override public int getIconHeight() { return myScaledDelegateIcon.getIconHeight(); } public boolean isDone() { if (myAutoUpdatable && myDone && myLastCalcTime > 0 && System.currentTimeMillis() - myLastCalcTime > Math.max(MIN_AUTO_UPDATE_MILLIS, 10 * myLastTimeSpent)) { myDone = false; myIsScheduled = false; } return myDone; } private static class RepaintScheduler { private final Alarm myAlarm = new Alarm(); private final Set<RepaintRequest> myQueue = new LinkedHashSet<>(); private void pushDirtyComponent(@NotNull Component c, final Rectangle rec) { ApplicationManager.getApplication().assertIsDispatchThread(); // assert myQueue accessed from EDT only myAlarm.cancelAllRequests(); myAlarm.addRequest(() -> { for (RepaintRequest each : myQueue) { Rectangle r = each.getRectangle(); if (r == null) { each.getComponent().repaint(); } else { each.getComponent().repaint(r.x, r.y, r.width, r.height); } } myQueue.clear(); }, 50); myQueue.add(new RepaintRequest(c, rec)); } } private static class RepaintRequest { private final Component myComponent; private final Rectangle myRectangle; private RepaintRequest(@NotNull Component component, Rectangle rectangle) { myComponent = component; myRectangle = rectangle; } @NotNull public Component getComponent() { return myComponent; } public Rectangle getRectangle() { return myRectangle; } } @FunctionalInterface interface IconListener<T> { void evalDone(DeferredIconImpl<T> source, T key, @NotNull Icon result); } static boolean equalIcons(Icon icon1, Icon icon2) { if (icon1 instanceof DeferredIconImpl) { return ((DeferredIconImpl)icon1).isDeferredAndEqual(icon2); } if (icon2 instanceof DeferredIconImpl) { return ((DeferredIconImpl)icon2).isDeferredAndEqual(icon1); } return Comparing.equal(icon1, icon2); } private boolean isDeferredAndEqual(Icon icon) { return icon instanceof DeferredIconImpl && Comparing.equal(myParam, ((DeferredIconImpl)icon).myParam) && equalIcons(myScaledDelegateIcon, ((DeferredIconImpl)icon).myScaledDelegateIcon); } @Override public String toString() { return "Deferred. Base=" + myScaledDelegateIcon; } }