/*
* Copyright (C) 2010-2015 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.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.Timeline;
import com.jpexs.decompiler.flash.timeline.Timelined;
import com.jpexs.decompiler.flash.types.ColorTransform;
import com.jpexs.decompiler.flash.types.ConstantColorColorTransform;
import com.jpexs.decompiler.flash.types.RECT;
import com.jpexs.decompiler.flash.types.shaperecords.SHAPERECORD;
import com.jpexs.helpers.ByteArrayRange;
import com.jpexs.helpers.SerializableImage;
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.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.io.IOException;
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 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 DepthState stateUnderCursor = null;
private MouseEvent lastMouseEvent = null;
private final List soundPlayers = new ArrayList<>();
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;
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 List dss;
private List outlines;
public BufferedImage getLastImage() {
return img.getBufferedImage();
}
public synchronized void setOutlines(List dss, List outlines) {
this.outlines = outlines;
this.dss = dss;
}
public void setImg(SerializableImage img) {
this.img = img;
calcRect();
repaint();
}
public synchronized List getObjectsUnderPoint(Point p) {
List ret = new ArrayList<>();
for (int i = 0; i < outlines.size(); i++) {
if (outlines.get(i).contains(p)) {
ret.add(dss.get(i));
}
}
return ret;
}
public Rectangle getRect() {
return rect;
}
public 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 calcRect() {
if (img != null) {
int w1 = img.getWidth();
int h1 = img.getHeight();
int w2 = getWidth();
int h2 = getHeight();
int w;
int h;
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;
}
}
rect = new Rectangle(getWidth() / 2 - w / 2, getHeight() / 2 - h / 2, w, h);
} else {
rect = null;
}
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.setPaint(View.transparentPaint);
g2d.fill(new Rectangle(0, 0, getWidth(), getHeight()));
g2d.setComposite(AlphaComposite.SrcOver);
g2d.setPaint(View.getSwfBackgroundColor());
g2d.fill(new Rectangle(0, 0, getWidth(), getHeight()));
if (img != null) {
calcRect();
g2d.setComposite(AlphaComposite.SrcOver);
g2d.drawImage(img.getBufferedImage(), rect.x, rect.y, rect.x + rect.width, rect.y + rect.height, 0, 0, img.getWidth(), img.getHeight(), null);
}
}
}
@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) {
boolean handCursor = false;
DepthState newStateUnderCursor = null;
if (timelined != null) {
Timeline tim = ((Timelined) timelined).getTimeline();
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 = new Matrix();
m.translate(-rect.Xmin, -rect.Ymin);
m.scale(scale);
Point p = lastMouseEvent == null ? null : lastMouseEvent.getPoint();
List objs = new ArrayList<>();
StringBuilder ret = new StringBuilder();
synchronized (ImagePanel.class) {
if (timer == thisTimer) {
p = p == null ? null : iconPanel.toImagePoint(p);
if (p != null) {
int x = p.x;
int y = p.y;
objs = iconPanel.getObjectsUnderPoint(p);
ret.append(" [").append(x).append(",").append(y).append("] : ");
}
}
}
boolean first = true;
for (int i = 0; i < objs.size(); i++) {
DepthState ds = objs.get(i);
if (!first) {
ret.append(", ");
}
first = false;
CharacterTag c = tim.swf.getCharacter(ds.characterId);
if (c instanceof ButtonTag) {
newStateUnderCursor = ds;
handCursor = true;
}
ret.append(c.toString());
if (timelined instanceof ButtonTag) {
handCursor = true;
}
}
if (first) {
ret.append(" - ");
}
synchronized (ImagePanel.class) {
if (timer == thisTimer) {
debugLabel.setText(ret.toString());
if (handCursor) {
iconPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
} else {
iconPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
if (newStateUnderCursor != stateUnderCursor) {
stateUnderCursor = newStateUnderCursor;
}
}
}
}
}
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) {
stateUnderCursor = null;
lastMouseEvent = null;
hideMouseSelection();
redraw();
}
}
@Override
public void mousePressed(MouseEvent e) {
synchronized (ImagePanel.class) {
mouseButton = e.getButton();
lastMouseEvent = e;
redraw();
if (stateUnderCursor != null) {
ButtonTag b = (ButtonTag) swf.getCharacter(stateUnderCursor.characterId);
DefineButtonSoundTag sounds = b.getSounds();
if (sounds != null && sounds.buttonSoundChar2 != 0) { //OverUpToOverDown
playSound((SoundTag) swf.getCharacter(sounds.buttonSoundChar2), timer);
}
}
}
}
@Override
public void mouseReleased(MouseEvent e) {
synchronized (ImagePanel.class) {
mouseButton = 0;
lastMouseEvent = e;
redraw();
if (stateUnderCursor != null) {
ButtonTag b = (ButtonTag) swf.getCharacter(stateUnderCursor.characterId);
DefineButtonSoundTag sounds = b.getSounds();
if (sounds != null && sounds.buttonSoundChar3 != 0) { //OverDownToOverUp
playSound((SoundTag) swf.getCharacter(sounds.buttonSoundChar3), timer);
}
}
}
}
});
iconPanel.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
synchronized (ImagePanel.class) {
lastMouseEvent = e;
redraw();
DepthState lastUnderCur = stateUnderCursor;
if (stateUnderCursor != null) {
if (lastUnderCur == null || lastUnderCur.instanceId != stateUnderCursor.instanceId) {
// New mouse entered
ButtonTag b = (ButtonTag) swf.getCharacter(stateUnderCursor.characterId);
DefineButtonSoundTag sounds = b.getSounds();
if (sounds != null && sounds.buttonSoundChar1 != 0) { //IddleToOverUp
playSound((SoundTag) swf.getCharacter(sounds.buttonSoundChar1), timer);
}
}
}
if (lastUnderCur != null) {
if (stateUnderCursor == null || stateUnderCursor.instanceId != lastUnderCur.instanceId) {
// Old mouse leave
ButtonTag b = (ButtonTag) swf.getCharacter(lastUnderCur.characterId);
DefineButtonSoundTag sounds = b.getSounds();
if (sounds != null && sounds.buttonSoundChar0 != 0) { //OverUpToIddle
playSound((SoundTag) swf.getCharacter(sounds.buttonSoundChar0), timer);
}
}
}
}
}
@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;
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() {
@Override
public void callFrame(int frame) {
executeFrame(frame);
}
@Override
public Object callFunction(long functionAddress, long functionLength, List