/*
* Copyright (C) 2010-2016 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 <http://www.gnu.org/licenses/>.
*/
package com.jpexs.decompiler.flash.gui.player;
import com.jpexs.decompiler.flash.configuration.Configuration;
import com.jpexs.decompiler.flash.gui.AppStrings;
import com.jpexs.decompiler.flash.gui.MainPanel;
import com.jpexs.decompiler.flash.gui.View;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.font.TextAttribute;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JColorChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
/**
*
* @author JPEXS
*/
public class PlayerControls extends JPanel implements MediaDisplayListener {
private final JButton pauseButton;
private final JButton loopButton;
private MediaDisplay display;
private JProgressBar progress;
private final JLabel timeLabel;
private final JLabel frameLabel;
private final JLabel totalTimeLabel;
private final JLabel totalFrameLabel;
private static final Icon pauseIcon = View.getIcon("pause16");
private static final Icon playIcon = View.getIcon("play16");
private static final Icon loopIcon = View.getIcon("loopon16");
private static final Icon noLoopIcon = View.getIcon("loopoff16");
private final JLabel percentLabel = new JLabel("100%");
private final JPanel zoomPanel;
private final JPanel graphicControls;
private final JPanel playbackControls;
private final JPanel frameControls;
private boolean zoomToFit = false;
private double realZoom = 1.0;
private final JButton zoomFitButton;
private final JButton snapshotButton;
public static final int ZOOM_DECADE_STEPS = 10;
public static final double ZOOM_MULTIPLIER = Math.pow(10, 1.0 / ZOOM_DECADE_STEPS);
private static String underline(String s) {
return "<html><font color=\"#000099\"><u>" + s + "</u></font></html>";
}
private static Font underlinedFont = null;
private static Font notUnderlinedFont = null;
private final int zeroCharacterWidth;
static {
Font font = new JLabel().getFont();
notUnderlinedFont = font;
Map<TextAttribute, Integer> fontAttributes = new HashMap<>();
fontAttributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
underlinedFont = font.deriveFont(fontAttributes);
}
public PlayerControls(final MainPanel mainPanel, MediaDisplay display) {
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
graphicControls = new JPanel(new BorderLayout());
JPanel graphicButtonsPanel = new JPanel(new FlowLayout());
JButton selectColorButton = new JButton(View.getIcon("color16"));
selectColorButton.addActionListener(this::selectBkColorButtonActionPerformed);
selectColorButton.setToolTipText(AppStrings.translate("button.selectbkcolor.hint"));
JButton zoomInButton = new JButton(View.getIcon("zoomin16"));
zoomInButton.addActionListener(this::zoomInButtonActionPerformed);
zoomInButton.setToolTipText(AppStrings.translate("button.zoomin.hint"));
JButton zoomOutButton = new JButton(View.getIcon("zoomout16"));
zoomOutButton.addActionListener(this::zoomOutButtonActionPerformed);
zoomOutButton.setToolTipText(AppStrings.translate("button.zoomout.hint"));
zoomFitButton = new JButton(View.getIcon("zoomfit16"));
zoomFitButton.addActionListener(this::zoomFitButtonActionPerformed);
zoomFitButton.setToolTipText(AppStrings.translate("button.zoomfit.hint"));
JButton zoomNoneButton = new JButton(View.getIcon("zoomnone16"));
zoomNoneButton.addActionListener(this::zoomNoneButtonActionPerformed);
zoomNoneButton.setToolTipText(AppStrings.translate("button.zoomnone.hint"));
snapshotButton = new JButton(View.getIcon("snapshot16"));
snapshotButton.addActionListener(this::snapShotButtonActionPerformed);
snapshotButton.setToolTipText(AppStrings.translate("button.snapshot.hint"));
snapshotButton.setVisible(false);
zoomPanel = new JPanel(new FlowLayout());
zoomPanel.add(percentLabel);
zoomPanel.add(zoomInButton);
zoomPanel.add(zoomOutButton);
zoomPanel.add(zoomNoneButton);
zoomPanel.add(zoomFitButton);
zoomPanel.add(selectColorButton);
zoomPanel.setVisible(false);
graphicButtonsPanel.add(zoomPanel);
graphicButtonsPanel.add(snapshotButton);
graphicControls.add(graphicButtonsPanel, BorderLayout.EAST);
graphicControls.setVisible(false);
add(graphicControls);
graphicControls.setVisible(display.screenAvailable());
playbackControls = new JPanel();
this.display = display;
JPanel controlPanel = new JPanel(new BorderLayout());
frameLabel = new JLabel("0", SwingConstants.CENTER);
frameLabel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
frameLabel.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
}
});
frameLabel.setVisible(false);
zeroCharacterWidth = frameLabel.getFontMetrics(notUnderlinedFont).stringWidth("0");
frameLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
frameLabel.setFont(underlinedFont);
}
@Override
public void mouseExited(MouseEvent e) {
frameLabel.setFont(notUnderlinedFont);
}
@Override
public void mouseClicked(MouseEvent e) {
int gotoFrame = PlayerControls.this.display.getCurrentFrame();
if (gotoFrame > 0) {
mainPanel.gotoFrame(gotoFrame);
}
}
});
timeLabel = new JLabel("(00:00.00)");
totalTimeLabel = new JLabel("(00:00.00)");
totalFrameLabel = new JLabel("0");
totalFrameLabel.setVisible(false);
frameControls = new JPanel(new FlowLayout());
JButton prevFrameButton = new JButton(View.getIcon("prevframe16"));
prevFrameButton.setToolTipText(AppStrings.translate("preview.prevframe"));
prevFrameButton.setMargin(new Insets(4, 2, 2, 2));
prevFrameButton.addActionListener(this::prevFrameButtonActionPerformed);
frameControls.add(prevFrameButton);
frameControls.setVisible(display.screenAvailable());
frameControls.add(frameLabel);
JButton nextFrameButton = new JButton(View.getIcon("nextframe16"));
nextFrameButton.setToolTipText(AppStrings.translate("preview.nextframe"));
nextFrameButton.setMargin(new Insets(4, 2, 2, 2));
nextFrameButton.addActionListener(this::nextFrameButtonActionPerformed);
frameControls.add(nextFrameButton);
JButton gotoFrameButton = new JButton(View.getIcon("gotoframe16"));
gotoFrameButton.setToolTipText(AppStrings.translate("preview.gotoframe"));
gotoFrameButton.setMargin(new Insets(4, 2, 2, 2));
gotoFrameButton.addActionListener(this::gotoFrameButtonActionPerformed);
frameControls.add(gotoFrameButton);
frameControls.setVisible(false);
JPanel currentPanel = new JPanel(new FlowLayout());
currentPanel.add(frameControls);
currentPanel.add(timeLabel);
JPanel totalPanel = new JPanel(new FlowLayout());
totalPanel.add(totalFrameLabel);
totalPanel.add(totalTimeLabel);
controlPanel.add(currentPanel, BorderLayout.WEST);
controlPanel.add(totalPanel, BorderLayout.EAST);
playbackControls.setLayout(new BoxLayout(playbackControls, BoxLayout.Y_AXIS));
JPanel buttonsPanel = new JPanel(new FlowLayout());
pauseButton = new JButton(pauseIcon);
pauseButton.setToolTipText(AppStrings.translate("preview.pause"));
pauseButton.setMargin(new Insets(4, 2, 2, 2));
pauseButton.addActionListener(this::pauseButtonActionPerformed);
JButton stopButton = new JButton(View.getIcon("stop16"));
stopButton.setToolTipText(AppStrings.translate("preview.stop"));
stopButton.setMargin(new Insets(4, 2, 2, 2));
stopButton.addActionListener(this::stopButtonActionPerformed);
loopButton = new JButton(pauseIcon);
loopButton.setToolTipText(AppStrings.translate("preview.loop"));
loopButton.setMargin(new Insets(4, 2, 2, 2));
loopButton.addActionListener(this::loopButtonActionPerformed);
boolean loop = Configuration.loopMedia.get();
loopButton.setIcon(loop ? loopIcon : noLoopIcon);
buttonsPanel.add(pauseButton);
buttonsPanel.add(stopButton);
buttonsPanel.add(loopButton);
controlPanel.add(buttonsPanel, BorderLayout.CENTER);
progress = new JProgressBar();
Dimension pref = progress.getPreferredSize();
pref.height = 20;
progress.setPreferredSize(pref);
final PlayerControls t = this;
progress.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
int frame = (int) Math.floor(e.getX() * t.display.getTotalFrames() / (double) progress.getWidth());
boolean p = t.display.isPlaying();
t.display.gotoFrame(frame);
if (p) {
t.display.play();
}
}
});
playbackControls.add(progress);
playbackControls.add(controlPanel);
add(playbackControls);
this.display.addEventListener(this);
}
private String formatMs(long ms) {
long s = ms / 1000;
ms %= 1000;
long m = s / 60;
s %= 60;
long h = m / 60;
m %= 60;
return (h > 0 ? h + ":" : "") + pad(m) + ":" + pad(s) + "." + pad(ms / 10);
}
private String pad(long t) {
String ret = Long.toString(t);
while (ret.length() < 2) {
ret = "0" + ret;
}
return ret;
}
public void setMedia(MediaDisplay media) {
if (this.display != null) {
this.display.removeEventListener(this);
}
this.display = media;
this.display.addEventListener(this);
update();
}
private void update() {
if (!display.isLoaded()) {
return;
}
View.execInEventDispatchLater(() -> {
updateZoom();
int totalFrames = display.getTotalFrames();
int currentFrame = display.getCurrentFrame();
if (currentFrame >= totalFrames) {
currentFrame = totalFrames - 1;
}
float frameRate = display.getFrameRate();
Zoom zoom = display.getZoom();
zoomFitButton.setVisible(zoom != null);
percentLabel.setVisible(zoom != null);
zoomPanel.setVisible(display.zoomAvailable());
boolean screenAvailable = display.screenAvailable();
snapshotButton.setVisible(screenAvailable);
graphicControls.setVisible(screenAvailable);
totalFrameLabel.setVisible(screenAvailable);
frameLabel.setVisible(screenAvailable);
if (screenAvailable) {
int charCount = Math.max(Integer.toString(totalFrames).length(), 3);
Dimension min = new Dimension(zeroCharacterWidth * charCount, frameLabel.getPreferredSize().height);
frameLabel.setMinimumSize(min);
frameLabel.setPreferredSize(min);
}
frameControls.setVisible(screenAvailable);
if (totalFrames == 0) {
progress.setIndeterminate(true);
} else {
progress.setMaximum(totalFrames - 1);
progress.setMinimum(0);
progress.setValue(currentFrame);
progress.setIndeterminate(false);
}
frameLabel.setText(Integer.toString(currentFrame + 1));
totalFrameLabel.setText(Integer.toString(totalFrames));
if (frameRate != 0) {
timeLabel.setText("(" + formatMs((int) (currentFrame * 1000.0 / frameRate)) + ")");
totalTimeLabel.setText("(" + formatMs((int) (totalFrames * 1000.0 / frameRate)) + ")");
}
if (totalFrames <= 1 && playbackControls.isVisible()) {
playbackControls.setVisible(false);
}
if (totalFrames > 1 && !playbackControls.isVisible()) {
playbackControls.setVisible(true);
}
boolean paused1 = !display.isPlaying();
if (paused1) {
pauseButton.setToolTipText(AppStrings.translate("preview.play"));
pauseButton.setIcon(playIcon);
} else {
pauseButton.setToolTipText(AppStrings.translate("preview.pause"));
pauseButton.setIcon(pauseIcon);
}
});
}
private static double roundZoom(double realZoom, int mantisa) {
double l10 = Math.log10(realZoom);
int lg = (int) (-Math.floor(l10) + mantisa - 1);
if (lg < 0) {
lg = 0;
}
BigDecimal bd = new BigDecimal(String.valueOf(realZoom)).setScale(lg, BigDecimal.ROUND_HALF_UP);
return bd.doubleValue();
}
private void updateZoom() {
double pctzoom = roundZoom(getRealZoom() * 100, 3);
String r = Double.toString(pctzoom);
double zoom = pctzoom / 100.0;
if (r.endsWith(".0")) {
r = r.substring(0, r.length() - 2);
}
r += "%";
if (zoomToFit) {
percentLabel.setText(AppStrings.translate("fit") + " (" + r + ")");
} else {
percentLabel.setText(r);
}
Zoom zoomObj = new Zoom();
zoomObj.value = zoom;
zoomObj.fit = zoomToFit;
display.zoom(zoomObj);
}
private void pauseButtonActionPerformed(ActionEvent evt) {
if (display.isPlaying()) {
display.pause();
} else {
display.play();
}
}
private void loopButtonActionPerformed(ActionEvent evt) {
boolean loop = !Configuration.loopMedia.get();
Configuration.loopMedia.set(loop);
loopButton.setIcon(loop ? loopIcon : noLoopIcon);
display.setLoop(loop);
}
private void gotoFrameButtonActionPerformed(ActionEvent evt) {
final JPanel gotoPanel = new JPanel(new BorderLayout());
final JTextField frameField = new JTextField("" + display.getCurrentFrame());
gotoPanel.add(new JLabel(AppStrings.translate("preview.gotoframe.dialog.message").replace("%min%", "1").replace("%max%", "" + display.getTotalFrames())), BorderLayout.NORTH);
gotoPanel.add(frameField, BorderLayout.CENTER);
gotoPanel.addAncestorListener(new AncestorListener() {
@Override
public void ancestorAdded(AncestorEvent event) {
final AncestorListener al = this;
View.execInEventDispatchLater(() -> {
frameField.selectAll();
frameField.requestFocusInWindow();
gotoPanel.removeAncestorListener(al);
});
}
@Override
public void ancestorRemoved(AncestorEvent event) {
}
@Override
public void ancestorMoved(AncestorEvent event) {
}
});
if (View.showConfirmDialog(this, gotoPanel, AppStrings.translate("preview.gotoframe.dialog.title"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE) == JOptionPane.OK_OPTION) {
int frame = -1;
try {
frame = Integer.parseInt(frameField.getText());
} catch (NumberFormatException nfe) {
//handled as -1
}
if (frame <= 0 || frame > display.getTotalFrames()) {
View.showMessageDialog(this, AppStrings.translate("preview.gotoframe.dialog.frame.error").replace("%min%", "1").replace("%max%", "" + display.getTotalFrames()), AppStrings.translate("error"), JOptionPane.ERROR_MESSAGE);
return;
}
display.gotoFrame(frame - 1);
}
}
private void nextFrameButtonActionPerformed(ActionEvent evt) {
display.gotoFrame(display.getCurrentFrame() + 1);
}
private void prevFrameButtonActionPerformed(ActionEvent evt) {
display.gotoFrame(display.getCurrentFrame() - 1);
}
private void stopButtonActionPerformed(ActionEvent evt) {
display.stop();
}
private void selectBkColorButtonActionPerformed(ActionEvent evt) {
Color newColor = JColorChooser.showDialog(null, AppStrings.translate("dialog.selectbkcolor.title"), View.getSwfBackgroundColor());
if (newColor != null) {
View.setSwfBackgroundColor(newColor);
display.setBackground(newColor);
}
}
private void zoomInButtonActionPerformed(ActionEvent evt) {
realZoom = getRealZoom() * ZOOM_MULTIPLIER;
zoomToFit = false;
updateZoom();
}
private void zoomOutButtonActionPerformed(ActionEvent evt) {
realZoom = getRealZoom() / ZOOM_MULTIPLIER;
zoomToFit = false;
updateZoom();
}
private void zoomNoneButtonActionPerformed(ActionEvent evt) {
realZoom = 1.0;
zoomToFit = false;
updateZoom();
}
private void zoomFitButtonActionPerformed(ActionEvent evt) {
realZoom = 1.0;
zoomToFit = true;
updateZoom();
}
private void snapShotButtonActionPerformed(ActionEvent evt) {
putImageToClipBoard(display.printScreen());
}
private double getRealZoom() {
if (zoomToFit) {
return display.getZoomToFit();
}
return realZoom;
}
@Override
public void mediaDisplayStateChanged(MediaDisplay source) {
if (display != source) {
return;
}
update();
}
@Override
public void playingFinished(MediaDisplay source) {
}
private class TransferableImage implements Transferable {
Image img;
public TransferableImage(Image img) {
this.img = img;
}
@Override
public Object getTransferData(DataFlavor flavor)
throws UnsupportedFlavorException, IOException {
if (flavor.equals(DataFlavor.imageFlavor) && img != null) {
return img;
} else {
throw new UnsupportedFlavorException(flavor);
}
}
@Override
public DataFlavor[] getTransferDataFlavors() {
DataFlavor[] flavors = new DataFlavor[1];
flavors[0] = DataFlavor.imageFlavor;
return flavors;
}
@Override
public boolean isDataFlavorSupported(DataFlavor flavor) {
DataFlavor[] flavors = getTransferDataFlavors();
for (int i = 0; i < flavors.length; i++) {
if (flavor.equals(flavors[i])) {
return true;
}
}
return false;
}
}
private void putImageToClipBoard(BufferedImage img) {
if (img == null) {
return;
}
TransferableImage trans = new TransferableImage(img);
Clipboard c = Toolkit.getDefaultToolkit().getSystemClipboard();
c.setContents(trans, new ClipboardOwner() {
@Override
public void lostOwnership(Clipboard clipboard, Transferable contents) {
}
});
}
}