/*
* Copyright (C) 2010-2021 JPEXS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package com.jpexs.decompiler.flash.gui;
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.SWFInputStream;
import com.jpexs.decompiler.flash.action.Action;
import com.jpexs.decompiler.flash.action.LocalDataArea;
import com.jpexs.decompiler.flash.action.Stage;
import com.jpexs.decompiler.flash.configuration.Configuration;
import com.jpexs.decompiler.flash.ecma.Undefined;
import com.jpexs.decompiler.flash.exporters.commonshape.Matrix;
import com.jpexs.decompiler.flash.gui.player.MediaDisplay;
import com.jpexs.decompiler.flash.gui.player.MediaDisplayListener;
import com.jpexs.decompiler.flash.gui.player.Zoom;
import com.jpexs.decompiler.flash.tags.DefineButtonSoundTag;
import com.jpexs.decompiler.flash.tags.DoActionTag;
import com.jpexs.decompiler.flash.tags.base.BoundedTag;
import com.jpexs.decompiler.flash.tags.base.ButtonTag;
import com.jpexs.decompiler.flash.tags.base.CharacterTag;
import com.jpexs.decompiler.flash.tags.base.DrawableTag;
import com.jpexs.decompiler.flash.tags.base.PlaceObjectTypeTag;
import com.jpexs.decompiler.flash.tags.base.RenderContext;
import com.jpexs.decompiler.flash.tags.base.SoundTag;
import com.jpexs.decompiler.flash.tags.base.TextTag;
import com.jpexs.decompiler.flash.timeline.DepthState;
import com.jpexs.decompiler.flash.timeline.Frame;
import com.jpexs.decompiler.flash.timeline.Timeline;
import com.jpexs.decompiler.flash.timeline.Timelined;
import com.jpexs.decompiler.flash.types.ConstantColorColorTransform;
import com.jpexs.decompiler.flash.types.RECT;
import com.jpexs.decompiler.flash.types.SOUNDINFO;
import com.jpexs.helpers.ByteArrayRange;
import com.jpexs.helpers.Cache;
import com.jpexs.helpers.SerializableImage;
import com.jpexs.helpers.Stopwatch;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Transparency;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.JLabel;
import javax.swing.JPanel;
/**
*
* @author JPEXS
*/
public final class ImagePanel extends JPanel implements MediaDisplay {
private static final Logger logger = Logger.getLogger(ImagePanel.class.getName());
private final List listeners = new ArrayList<>();
private Timelined timelined;
private boolean stillFrame = false;
private volatile Timer timer;
private int frame = -1;
private boolean loop;
private LocalDataArea lda;
private boolean zoomAvailable = false;
private SWF swf;
private boolean loaded;
private int mouseButton;
private final JLabel debugLabel = new JLabel("-");
private Point cursorPosition = null;
private MouseEvent lastMouseEvent = null;
private final List soundPlayers = new ArrayList<>();
private final Cache displayObjectCache = Cache.getInstance(false, false, "displayObject");
private final IconPanel iconPanel;
private int time = 0;
private int selectedDepth = -1;
private Zoom zoom = new Zoom();
private final Object delayObject = new Object();
private boolean drawReady;
private final int drawWaitLimit = 50; // ms
private TextTag textTag;
private TextTag newTextTag;
private int msPerFrame;
private final boolean lowQuality = false;
private final double LQ_FACTOR = 2;
public synchronized void selectDepth(int depth) {
if (depth != selectedDepth) {
this.selectedDepth = depth;
}
hideMouseSelection();
}
public void fireMediaDisplayStateChanged() {
for (MediaDisplayListener l : listeners) {
l.mediaDisplayStateChanged(this);
}
}
@Override
public void addEventListener(MediaDisplayListener listener) {
listeners.add(listener);
}
@Override
public void removeEventListener(MediaDisplayListener listener) {
listeners.remove(listener);
}
private class IconPanel extends JPanel {
private SerializableImage _img;
private Rectangle _rect = null;
private ButtonTag mouseOverButton = null;
private boolean autoFit = false;
private boolean allowMove = true;
private Point dragStart = null;
private Point offsetPoint = new Point(0, 0);
private synchronized SerializableImage getImg() {
return _img;
}
public synchronized Rectangle getRect() {
return _rect;
}
public boolean hasAllowMove() {
return allowMove;
}
VolatileImage renderImage;
public void render() {
SerializableImage img = getImg();
Rectangle rect = getRect();
/*if (img == null) {
return;
}*/
Graphics2D g2 = null;
VolatileImage ri;
do {
ri = this.renderImage;
if (ri == null) {
return;
}
int valid = ri.validate(View.getDefaultConfiguration());
if (valid == VolatileImage.IMAGE_INCOMPATIBLE) {
ri = View.createRenderImage(getWidth(), getHeight(), Transparency.TRANSLUCENT);
}
try {
g2 = ri.createGraphics();
g2.setPaint(View.transparentPaint);
g2.fill(new Rectangle(0, 0, getWidth(), getHeight()));
g2.setComposite(AlphaComposite.SrcOver);
g2.setPaint(View.getSwfBackgroundColor());
g2.fill(new Rectangle(0, 0, getWidth(), getHeight()));
g2.setComposite(AlphaComposite.SrcOver);
if (rect != null && img != null) {
g2.drawImage(img.getBufferedImage(), rect.x, rect.y, rect.x + rect.width, rect.y + rect.height, 0, 0, img.getWidth(), img.getHeight(), null);
}
} finally {
if (g2 != null) {
g2.dispose();
}
}
} while (ri.contentsLost());
}
public IconPanel() {
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
int width = getWidth();
int height = getHeight();
if (width > 0 && height > 0) {
renderImage = View.createRenderImage(width, height, Transparency.TRANSLUCENT);
} else {
renderImage = null;
}
calcRect();
render();
repaint();
}
});
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
dragStart = e.getPoint();
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
dragStart = null;
}
}
});
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
if (dragStart != null && allowMove) {
Point dragEnd = e.getPoint();
Point delta = new Point(dragEnd.x - dragStart.x, dragEnd.y - dragStart.y);
offsetPoint.x += delta.x;
offsetPoint.y += delta.y;
dragStart = dragEnd;
repaint();
}
}
});
}
public void setAutoFit(boolean autoFit) {
this.autoFit = autoFit;
repaint();
}
public synchronized BufferedImage getLastImage() {
if (_img == null) {
return null;
}
return _img.getBufferedImage();
}
public synchronized void setImg(SerializableImage img) {
this._img = img;
calcRect();
render();
repaint();
}
public synchronized Point toImagePoint(Point p) {
if (_img == null) {
return null;
}
return new Point((p.x - _rect.x) * _img.getWidth() / _rect.width, (p.y - _rect.y) * _img.getHeight() / _rect.height);
}
private void setAllowMove(boolean allowMove) {
this.allowMove = allowMove;
if (!allowMove) {
offsetPoint = new Point();
}
}
private synchronized void calcRect() {
if (_img != null) {
int w1 = (int) (_img.getWidth() * (lowQuality ? LQ_FACTOR : 1));
int h1 = (int) (_img.getHeight() * (lowQuality ? LQ_FACTOR : 1));
int w2 = getWidth();
int h2 = getHeight();
int w;
int h;
if (autoFit) {
if (w1 <= w2 && h1 <= h2) {
w = w1;
h = h1;
} else {
h = h1 * w2 / w1;
if (h > h2) {
w = w1 * h2 / h1;
h = h2;
} else {
w = w2;
}
}
} else {
w = w1;
h = h1;
}
setAllowMove(h > h2 || w > w2);
_rect = new Rectangle(getWidth() / 2 - w / 2 + offsetPoint.x, getHeight() / 2 - h / 2 + offsetPoint.y, w, h);
} else {
_rect = null;
}
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
VolatileImage ri = this.renderImage;
if (ri != null) {
calcRect();
if (ri.validate(View.getDefaultConfiguration()) != VolatileImage.IMAGE_OK) {
ri = View.createRenderImage(getWidth(), getHeight(), Transparency.TRANSLUCENT);
render();
}
if (ri != null) {
g2d.drawImage(ri, 0, 0, null);
}
}
g2d.setColor(Color.red);
DecimalFormat df = new DecimalFormat();
df.setMaximumFractionDigits(2);
df.setMinimumFractionDigits(0);
df.setGroupingUsed(false);
float frameLoss = 100 - (getFpsIs() / fpsShouldBe * 100);
if (Configuration._debugMode.get()) {
g2d.drawString("frameLoss:" + df.format(frameLoss) + "%", 20, 20);
}
}
}
@Override
public void setBackground(Color bg) {
if (iconPanel != null) {
iconPanel.setBackground(bg);
}
super.setBackground(bg);
}
@Override
public synchronized void addMouseListener(MouseListener l) {
iconPanel.addMouseListener(l);
}
@Override
public synchronized void removeMouseListener(MouseListener l) {
iconPanel.removeMouseListener(l);
}
@Override
public synchronized void addMouseMotionListener(MouseMotionListener l) {
iconPanel.addMouseMotionListener(l);
}
@Override
public synchronized void removeMouseMotionListener(MouseMotionListener l) {
iconPanel.removeMouseMotionListener(l);
}
private void updatePos(Timelined timelined, MouseEvent lastMouseEvent, Timer thisTimer) {
if (timelined != null) {
BoundedTag bounded = (BoundedTag) timelined;
RECT rect = bounded.getRect();
int width = rect.getWidth();
double scale = 1.0;
/*if (width > swf.displayRect.getWidth()) {
scale = (double) swf.displayRect.getWidth() / (double) width;
}*/
Matrix m = Matrix.getTranslateInstance(-rect.Xmin, -rect.Ymin);
m.scale(scale);
Point p = lastMouseEvent == null ? null : lastMouseEvent.getPoint();
synchronized (ImagePanel.class) {
if (timer == thisTimer) {
cursorPosition = p;
}
}
}
}
private void showSelectedName() {
if (selectedDepth > -1 && frame > -1) {
DepthState ds = timelined.getTimeline().getFrame(frame).layers.get(selectedDepth);
if (ds != null) {
CharacterTag cht = timelined.getTimeline().swf.getCharacter(ds.characterId);
if (cht != null) {
debugLabel.setText(cht.getName());
}
}
}
}
public void hideMouseSelection() {
if (selectedDepth > -1) {
showSelectedName();
} else {
debugLabel.setText(" - ");
}
}
public ImagePanel() {
super(new BorderLayout());
//iconPanel.setHorizontalAlignment(JLabel.CENTER);
setOpaque(true);
setBackground(View.getDefaultBackgroundColor());
loop = true;
iconPanel = new IconPanel();
//labelPan.add(label, new GridBagConstraints());
add(iconPanel, BorderLayout.CENTER);
add(debugLabel, BorderLayout.NORTH);
iconPanel.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
synchronized (ImagePanel.class) {
lastMouseEvent = e;
redraw();
}
}
@Override
public void mouseExited(MouseEvent e) {
synchronized (ImagePanel.class) {
lastMouseEvent = null;
hideMouseSelection();
redraw();
}
}
@Override
public void mousePressed(MouseEvent e) {
synchronized (ImagePanel.class) {
mouseButton = e.getButton();
lastMouseEvent = e;
redraw();
ButtonTag button = iconPanel.mouseOverButton;
if (button != null) {
DefineButtonSoundTag sounds = button.getSounds();
if (sounds != null && sounds.buttonSoundChar2 != 0) { // OverUpToOverDown
playSound((SoundTag) swf.getCharacter(sounds.buttonSoundChar2), sounds.buttonSoundInfo2, timer);
}
}
}
}
@Override
public void mouseReleased(MouseEvent e) {
synchronized (ImagePanel.class) {
mouseButton = 0;
lastMouseEvent = e;
redraw();
ButtonTag button = iconPanel.mouseOverButton;
if (button != null) {
DefineButtonSoundTag sounds = button.getSounds();
if (sounds != null && sounds.buttonSoundChar3 != 0) { // OverDownToOverUp
playSound((SoundTag) swf.getCharacter(sounds.buttonSoundChar3), sounds.buttonSoundInfo3, timer);
}
}
}
}
});
iconPanel.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
synchronized (ImagePanel.class) {
lastMouseEvent = e;
redraw();
}
}
@Override
public void mouseDragged(MouseEvent e) {
synchronized (ImagePanel.class) {
lastMouseEvent = e;
redraw();
}
}
});
}
private synchronized void redraw() {
if (timer == null && timelined != null) {
startTimer(timelined.getTimeline(), false);
}
}
@Override
public synchronized void zoom(Zoom zoom) {
boolean modified = this.zoom.value != zoom.value || this.zoom.fit != zoom.fit;
if (modified) {
this.zoom = zoom;
displayObjectCache.clear();
redraw();
if (textTag != null) {
setText(textTag, newTextTag);
}
fireMediaDisplayStateChanged();
}
}
@Override
public synchronized BufferedImage printScreen() {
return iconPanel.getLastImage();
}
@Override
public synchronized double getZoomToFit() {
if (timelined != null) {
RECT bounds = timelined.getRect();
double w1 = bounds.getWidth() / SWF.unitDivisor;
double h1 = bounds.getHeight() / SWF.unitDivisor;
double w2 = getWidth();
double h2 = getHeight();
double w;
double h;
h = h1 * w2 / w1;
if (h > h2) {
w = w1 * h2 / h1;
} else {
w = w2;
}
if (w1 <= Double.MIN_NORMAL) {
return 1.0;
}
return (double) w / (double) w1;
}
return 1;
}
@Override
public synchronized boolean zoomAvailable() {
return zoomAvailable;
}
public void setTimelined(final Timelined drawable, final SWF swf, int frame) {
Stage stage = new Stage(drawable) {
@Override
public void callFrame(int frame) {
executeFrame(frame);
}
@Override
public Object callFunction(long functionAddress, long functionLength, List