/*
* Copyright 2000-2015 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.openapi.wm.impl;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.AbstractPainter;
import com.intellij.openapi.ui.GraphicsConfig;
import com.intellij.openapi.ui.Painter;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFrame;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ImageLoader;
import com.intellij.util.containers.ContainerUtil;
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 java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.VolatileImage;
import java.io.File;
import java.net.URL;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import static com.intellij.openapi.wm.impl.IdeBackgroundUtil.getBackgroundSpec;
final class PaintersHelper implements Painter.Listener {
private static final Logger LOG = Logger.getInstance(PaintersHelper.class);
private final Set<Painter> myPainters = ContainerUtil.newLinkedHashSet();
private final Map<Painter, Component> myPainter2Component = ContainerUtil.newLinkedHashMap();
private final JComponent myRootComponent;
public PaintersHelper(@NotNull JComponent component) {
myRootComponent = component;
}
public boolean hasPainters() {
return !myPainters.isEmpty();
}
public boolean needsRepaint() {
for (Painter painter : myPainters) {
if (painter.needsRepaint()) return true;
}
return false;
}
public void addPainter(@NotNull Painter painter, @Nullable Component component) {
myPainters.add(painter);
myPainter2Component.put(painter, component == null ? myRootComponent : component);
painter.addListener(this);
}
public void removePainter(@NotNull Painter painter) {
painter.removeListener(this);
myPainters.remove(painter);
myPainter2Component.remove(painter);
}
public void clear() {
for (Painter painter : myPainters) {
painter.removeListener(this);
}
myPainters.clear();
myPainter2Component.clear();
}
public void paint(Graphics g) {
runAllPainters(g, computeOffsets(g, myRootComponent));
}
void runAllPainters(Graphics gg, int[] offsets) {
if (myPainters.isEmpty()) return;
Graphics2D g = (Graphics2D)gg;
AffineTransform orig = g.getTransform();
int i = 0;
// restore transform at the time of computeOffset()
AffineTransform t = new AffineTransform();
t.translate(offsets[i++], offsets[i++]);
for (Painter painter : myPainters) {
if (!painter.needsRepaint()) continue;
Component cur = myPainter2Component.get(painter);
g.setTransform(t);
g.translate(offsets[i++], offsets[i++]);
// paint in the orig graphics scale (note, the offsets are pre-scaled)
g.scale(orig.getScaleX(), orig.getScaleY());
painter.paint(cur, g);
}
g.setTransform(orig);
}
@NotNull
int[] computeOffsets(Graphics gg, @NotNull JComponent component) {
if (myPainters.isEmpty()) return ArrayUtil.EMPTY_INT_ARRAY;
int i = 0;
int[] offsets = new int[2 + myPainters.size() * 2];
// store current graphics transform
Graphics2D g = (Graphics2D)gg;
AffineTransform tx = g.getTransform();
// graphics tx offsets include graphics scale
offsets[i++] = (int)tx.getTranslateX();
offsets[i++] = (int)tx.getTranslateY();
// calculate relative offsets for painters
Rectangle r = null;
Component prev = null;
for (Painter painter : myPainters) {
if (!painter.needsRepaint()) continue;
Component cur = myPainter2Component.get(painter);
if (cur != prev || r == null) {
Container curParent = cur.getParent();
if (curParent == null) continue;
r = SwingUtilities.convertRectangle(curParent, cur.getBounds(), component);
prev = cur;
}
// component offsets don't include graphics scale, so compensate
offsets[i++] = (int)(r.x * tx.getScaleX());
offsets[i++] = (int)(r.y * tx.getScaleY());
}
return offsets;
}
@Override
public void onNeedsRepaint(Painter painter, JComponent dirtyComponent) {
if (dirtyComponent != null && dirtyComponent.isShowing()) {
Rectangle rec = SwingUtilities.convertRectangle(dirtyComponent, dirtyComponent.getBounds(), myRootComponent);
myRootComponent.repaint(rec);
}
else {
myRootComponent.repaint();
}
}
public enum Fill {
PLAIN, SCALE, TILE
}
public enum Place {
CENTER, TOP_CENTER, BOTTOM_CENTER,
TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT
}
public static void initWallpaperPainter(@NotNull String propertyName, @NotNull PaintersHelper painters) {
ImagePainter painter = (ImagePainter)newWallpaperPainter(propertyName, painters.myRootComponent);
painters.addPainter(painter, null);
}
private static AbstractPainter newWallpaperPainter(@NotNull final String propertyName,
@NotNull final JComponent rootComponent) {
return new ImagePainter() {
Image image;
float alpha;
Insets insets;
Fill fillType;
Place place;
String current;
@Override
public boolean needsRepaint() {
return ensureImageLoaded();
}
@Override
public void executePaint(Component component, Graphics2D g) {
if (image == null) return; // covered by needsRepaint()
executePaint(g, component, image, fillType, place, alpha, insets);
}
boolean ensureImageLoaded() {
IdeFrame frame = UIUtil.getParentOfType(IdeFrame.class, rootComponent);
Project project = frame == null ? null : frame.getProject();
String value = getBackgroundSpec(project, propertyName);
if (!Comparing.equal(value, current)) {
current = value;
loadImageAsync(value);
// keep the current image for a while
}
return image != null;
}
private void resetImage(String value, Image newImage, float newAlpha, Fill newFill, Place newPlace) {
if (!Comparing.equal(current, value)) return;
boolean prevOk = image != null;
clearImages(-1);
image = newImage;
insets = JBUI.emptyInsets();
alpha = newAlpha;
fillType = newFill;
place = newPlace;
boolean newOk = newImage != null;
if (prevOk || newOk) {
ModalityState modalityState = ModalityState.stateForComponent(rootComponent);
if (modalityState.dominates(ModalityState.NON_MODAL)) {
UIUtil.getActiveWindow().repaint();
}
else {
IdeBackgroundUtil.repaintAllWindows();
}
}
}
private void loadImageAsync(final String propertyValue) {
String[] parts = (propertyValue != null ? propertyValue : propertyName + ".png").split(",");
final float newAlpha = Math.abs(Math.min(StringUtil.parseInt(parts.length > 1 ? parts[1] : "", 10) / 100f, 1f));
final Fill newFillType = StringUtil.parseEnum(parts.length > 2 ? parts[2].toUpperCase(Locale.ENGLISH) : "", Fill.SCALE, Fill.class);
final Place newPlace = StringUtil.parseEnum(parts.length > 3 ? parts[3].toUpperCase(Locale.ENGLISH) : "", Place.CENTER, Place.class);
String filePath = parts[0];
if (StringUtil.isEmpty(filePath)) {
resetImage(propertyValue, null, newAlpha, newFillType, newPlace);
return;
}
try {
URL url = filePath.contains("://") ? new URL(filePath) :
(FileUtil.isAbsolutePlatformIndependent(filePath)
? new File(filePath)
: new File(PathManager.getConfigPath(), filePath)).toURI().toURL();
ApplicationManager.getApplication().executeOnPooledThread(() -> {
final Image m = ImageLoader.loadFromUrl(url);
ModalityState modalityState = ModalityState.stateForComponent(rootComponent);
ApplicationManager.getApplication().invokeLater(() -> resetImage(propertyValue, m, newAlpha, newFillType, newPlace), modalityState);
});
}
catch (Exception e) {
resetImage(propertyValue, null, newAlpha, newFillType, newPlace);
}
}
};
}
public static AbstractPainter newImagePainter(@NotNull final Image image, final Fill fillType, final Place place, final float alpha, final Insets insets) {
return new ImagePainter() {
@Override
public boolean needsRepaint() {
return true;
}
@Override
public void executePaint(Component component, Graphics2D g) {
executePaint(g, component, image, fillType, place, alpha, insets);
}
};
}
private static class Cached {
final VolatileImage image;
final Dimension used;
long touched;
Cached(VolatileImage image, Dimension dim) {
this.image = image;
used = dim;
}
}
private abstract static class ImagePainter extends AbstractPainter {
final Map<GraphicsConfiguration, Cached> cachedMap = ContainerUtil.newHashMap();
public void executePaint(Graphics2D g, Component component, Image image, Fill fillType, Place place, float alpha, Insets insets) {
int cw0 = component.getWidth();
int ch0 = component.getHeight();
Insets i = JBUI.insets(insets.top * ch0 / 100, insets.left * cw0 / 100, insets.bottom * ch0 / 100, insets.right * cw0 / 100);
int cw = cw0 - i.left - i.right;
int ch = ch0 - i.top - i.bottom;
int w = image.getWidth(null);
int h = image.getHeight(null);
if (w <= 0 || h <= 0) return;
// performance: pre-compute scaled image or tiles
@Nullable
GraphicsConfiguration cfg = g.getDeviceConfiguration();
Cached cached = cachedMap.get(cfg);
VolatileImage scaled = cached == null ? null : cached.image;
if (fillType == Fill.SCALE || fillType == Fill.TILE) {
int sw, sh;
if (fillType == Fill.SCALE) {
boolean useWidth = cw * h > ch * w;
sw = useWidth ? cw : w * ch / h;
sh = useWidth ? h * cw / w : ch;
}
else {
sw = cw < w ? w : (cw + w) / w * w;
sh = ch < h ? h : (ch + h) / h * h;
}
int sw0 = scaled == null ? -1 : scaled.getWidth(null);
int sh0 = scaled == null ? -1 : scaled.getHeight(null);
boolean rescale = cached == null || cached.used.width != sw || cached.used.height != sh;
while ((scaled = validateImage(cfg, scaled)) == null || rescale) {
if (scaled == null || sw0 < sw || sh0 < sh) {
scaled = createImage(cfg, sw + sw / 10, sh + sh / 10); // + 10 percent
cachedMap.put(cfg, cached = new Cached(scaled, new Dimension(sw, sh)));
}
else {
cached.used.setSize(sw, sh);
}
Graphics2D gg = scaled.createGraphics();
gg.setComposite(AlphaComposite.Src);
if (fillType == Fill.SCALE) {
gg.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
gg.drawImage(image, 0, 0, sw, sh, null);
}
else {
for (int x = 0; x < sw; x += w) {
for (int y = 0; y < sh; y += h) {
UIUtil.drawImage(gg, image, x, y, null);
}
}
}
gg.dispose();
rescale = false;
}
w = sw;
h = sh;
}
else {
while ((scaled = validateImage(cfg, scaled)) == null) {
scaled = createImage(cfg, w, h);
cachedMap.put(cfg, cached = new Cached(scaled, new Dimension(w, h)));
Graphics2D gg = scaled.createGraphics();
gg.setComposite(AlphaComposite.Src);
gg.drawImage(image, 0, 0, null);
gg.dispose();
}
}
long currentTime = System.currentTimeMillis();
cached.touched = currentTime;
if (cachedMap.size() > 2) {
clearImages(currentTime);
}
int x, y;
if (place == Place.CENTER ||
place == Place.TOP_CENTER || place == Place.BOTTOM_CENTER) {
x = i.left + (cw - w) / 2;
y = place == Place.TOP_CENTER ? i.top :
place == Place.BOTTOM_CENTER ? ch0 - i.bottom - h :
i.top + (ch - h) / 2;
}
else if (place == Place.TOP_LEFT || place == Place.TOP_RIGHT ||
place == Place.BOTTOM_LEFT || place == Place.BOTTOM_RIGHT) {
x = place == Place.TOP_LEFT || place == Place.BOTTOM_LEFT ? i.left : cw0 - i.right - w;
y = place == Place.TOP_LEFT || place == Place.TOP_RIGHT ? i.top : ch0 - i.bottom - h;
}
else {
return;
}
float adjustedAlpha = Boolean.TRUE.equals(g.getRenderingHint(IdeBackgroundUtil.ADJUST_ALPHA)) ? alpha / 2 : alpha;
GraphicsConfig gc = new GraphicsConfig(g).setAlpha(adjustedAlpha);
UIUtil.drawImage(g, scaled, x, y, w, h, null);
gc.restore();
}
void clearImages(long currentTime) {
boolean all = currentTime <= 0;
for (Iterator<GraphicsConfiguration> it = cachedMap.keySet().iterator(); it.hasNext(); ) {
GraphicsConfiguration cfg = it.next();
Cached c = cachedMap.get(cfg);
if (all || currentTime - c.touched > 2 * 60 * 1000L) {
it.remove();
LOG.info(logPrefix(cfg, c.image) + "image flushed" +
(all ? "" : "; untouched for " + StringUtil.formatDuration(currentTime - c.touched)));
c.image.flush();
}
}
}
@Nullable
private static VolatileImage validateImage(@Nullable GraphicsConfiguration cfg, @Nullable VolatileImage image) {
if (image == null) return null;
boolean lost1 = image.contentsLost();
int validated = image.validate(cfg);
boolean lost2 = image.contentsLost();
if (lost1 || lost2 || validated != VolatileImage.IMAGE_OK) {
LOG.info(logPrefix(cfg, image) + "image flushed" +
": contentsLost=" + lost1 + "||" + lost2 + "; validate=" + validated);
image.flush();
return null;
}
return image;
}
@NotNull
private static VolatileImage createImage(@Nullable GraphicsConfiguration cfg, int w, int h) {
GraphicsConfiguration safe;
safe = cfg != null ? cfg : GraphicsEnvironment.getLocalGraphicsEnvironment()
.getDefaultScreenDevice().getDefaultConfiguration();
VolatileImage image;
try {
image = safe.createCompatibleVolatileImage(w, h, new ImageCapabilities(true), Transparency.TRANSLUCENT);
}
catch (Exception e) {
image = safe.createCompatibleVolatileImage(w, h, Transparency.TRANSLUCENT);
}
// validate first time (it's always RESTORED & cleared)
image.validate(cfg);
image.setAccelerationPriority(1f);
ImageCapabilities caps = image.getCapabilities();
LOG.info(logPrefix(cfg, image) +
(caps.isAccelerated() ? "" : "non-") + "accelerated " +
(caps.isTrueVolatile() ? "" : "non-") + "volatile " +
"image created");
return image;
}
@NotNull
private static String logPrefix(@Nullable GraphicsConfiguration cfg, @NotNull VolatileImage image) {
return "(" + (cfg == null ? "null" : cfg.getClass().getSimpleName()) + ") "
+ image.getWidth() + "x" + image.getHeight() + " ";
}
}
}