/*
* JSwiff is an open source Java API for Macromedia Flash file generation
* and manipulation
*
* Copyright (C) 2004-2005 Ralf Terdic (contact@jswiff.com)
*
* 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 2 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, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.jswiff.investigator;
import com.jswiff.SWFDocument;
import com.jswiff.SWFReader;
import com.jswiff.SWFWriter;
import com.jswiff.listeners.SWFDocumentReader;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import javax.swing.AbstractAction;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.BevelBorder;
import javax.swing.border.SoftBevelBorder;
import javax.swing.filechooser.FileFilter;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
/**
* Implements a simple Flash file structure viewer. Either pass an SWF file as
* argument, or choose one in the open dialog.
*/
public final class Investigator extends JFrame {
static int threadCounter = 0;
File file;
JPanel treePanel;
JTree tree;
private JToolBar statusBar;
JTextField searchTextField;
private JCheckBox caseSenseCheckBox;
private JCheckBox backwardsCheckBox;
JButton expandButton;
private JButton findButton;
JButton refreshButton;
JButton openButton;
JButton infoButton;
private JButton copyButton;
DefaultMutableTreeNode rootNode;
int nodeNumber;
private Clipboard clipboard = getToolkit().getSystemClipboard();
/**
* Main method.
*
* @param args arguments (an optional SWF file path)
*/
public static void main(final String[] args) {
SwingUtilities.invokeLater(
new Runnable() {
public void run() {
Investigator investigator = new Investigator();
URL logoUrl = getClass().getResource(
"/com/jswiff/investigator/resources/logo16x16.png");
investigator.setIconImage(new ImageIcon(logoUrl).getImage());
investigator.run(args);
}
});
threadCounter++;
}
void copy() {
TreePath[] selectionPaths = tree.getSelectionPaths();
if (selectionPaths == null) {
return;
}
StringBuffer nodeStringBuffer = new StringBuffer();
// get minimum depth
int minDepth = Integer.MAX_VALUE;
for (int i = 0; i < selectionPaths.length; i++) {
TreePath selectionPath = selectionPaths[i];
minDepth = Math.min(minDepth, selectionPath.getPath().length);
}
for (int i = 0; i < selectionPaths.length; i++) {
TreePath selectionPath = selectionPaths[i];
if (selectionPath != null) {
TreeNode selectedNode = (TreeNode) selectionPath.getLastPathComponent();
if (i > 0) {
nodeStringBuffer.append("\n");
}
int depth = selectionPath.getPath().length - minDepth;
// add 'depth' spaces
for (int j = 0; j < depth; j++) {
nodeStringBuffer.append(' ');
}
nodeStringBuffer.append(filterHTML(selectedNode.toString()));
}
}
clipboard.setContents(
new NodeStringTransferable(nodeStringBuffer.toString()), null);
}
void displayInfo() {
infoButton.setEnabled(false);
SplashWindow splashWindow = new SplashWindow(this);
splashWindow.enableCloseOnClick();
}
void displayNodeNumber() {
displayStatus(nodeNumber + " nodes.");
}
void displayStatus(String status) {
statusBar.removeAll();
statusBar.add(new JLabel(status));
statusBar.revalidate();
statusBar.repaint();
}
void displayTree() {
treePanel.removeAll();
rootNode = new DefaultMutableTreeNode(file.getAbsolutePath());
tree = new JTree(rootNode);
// if Arial Unicode MS is installed, let's use it
tree.setFont(new Font("Arial Unicode MS", Font.PLAIN, 11));
setAccelerators(tree);
read();
tree.setScrollsOnExpand(true);
tree.setRootVisible(true);
tree.setShowsRootHandles(true);
treePanel.add(new JScrollPane(tree));
treePanel.revalidate();
treePanel.repaint();
displayStatus(nodeNumber + " nodes read.");
}
/*
* This method provides testing functionality hidden from the GUI - invoke it
* by pressing ctrl+d. Chosen files are parsed to SWFDocument instances
* which are finally written to /copies subdirectory. The author encourages
* you to use this on complex files and then compare the behavior of copies
* and original files, which, of course, should be identical. Also watch
* the console output.
*/
void duplicateFiles() {
JFileChooser chooser = new JFileChooser(file);
chooser.setFileFilter(new SWFFileFilter());
chooser.setMultiSelectionEnabled(true);
chooser.setDialogTitle("Choose files to duplicate");
File[] sourceFiles = null;
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
sourceFiles = chooser.getSelectedFiles();
}
if ((sourceFiles != null) && (sourceFiles.length > 0)) {
try {
for (int i = 0; i < sourceFiles.length; i++) {
File sourceFile = sourceFiles[i];
System.out.println("Duplicating " + sourceFile);
String destPathString = sourceFile.getParentFile().getAbsolutePath() +
File.separatorChar + "copies";
new File(destPathString).mkdir();
File destFile = new File(
destPathString + File.separatorChar + sourceFile.getName());
SWFReader reader = new SWFReader(
new FileInputStream(sourceFile));
SWFDocumentReader docReader = new SWFDocumentReader();
reader.addListener(docReader);
reader.read();
SWFDocument doc = docReader.getDocument();
SWFWriter writer = new SWFWriter(doc, new FileOutputStream(destFile));
writer.write();
}
JOptionPane.showMessageDialog(
this,
sourceFiles.length + " files processed - check console for details.",
"Completed", JOptionPane.INFORMATION_MESSAGE);
} catch (IOException e) {
e.printStackTrace();
}
}
}
void expand() {
tree.setScrollsOnExpand(false);
Enumeration en = rootNode.postorderEnumeration();
ProgressDialog progressDialog = new ProgressDialog(
this, "Expanding tree...", "Expanded nodes:", "0", 0, nodeNumber, true);
int nodeCounter = 0;
progressDialog.setProgressValue(nodeCounter);
while (en.hasMoreElements()) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) en.nextElement();
if (node.isLeaf()) {
tree.makeVisible(getPath(node));
}
nodeCounter++;
if ((nodeCounter & 15) == 0) {
if (progressDialog.isCanceled()) {
break;
}
progressDialog.setProgressValue(nodeCounter);
progressDialog.setNote(Integer.toString(nodeCounter));
}
}
progressDialog.close();
expandButton.setEnabled(true);
openButton.setEnabled(true);
refreshButton.setEnabled(true);
tree.setScrollsOnExpand(true);
}
void expandRoot() {
tree.expandPath(new TreePath(rootNode));
}
void expandTree() {
Thread expandThread = new Thread() {
public void run() {
expand();
}
};
expandButton.setEnabled(false);
openButton.setEnabled(false);
refreshButton.setEnabled(false);
expandThread.start();
}
void find(String searchText) {
if (searchText.length() == 0) {
return;
}
boolean backwards = this.backwardsCheckBox.isSelected();
boolean caseSense = this.caseSenseCheckBox.isSelected();
TreeNode selectedNode;
TreePath selectionPath = tree.getSelectionPath();
if (selectionPath == null) {
// startPath = new TreePath(tree.getModel().getRoot());
selectedNode = rootNode;
} else {
selectedNode = (TreeNode) selectionPath.getLastPathComponent();
}
findButton.setEnabled(false);
TreeNode foundNode = findNode(
selectedNode, searchText, caseSense, backwards);
if (foundNode == null) {
displayStatus("\"" + searchText + "\" not found!");
} else {
TreePath foundPath = getPath(foundNode);
tree.setSelectionPath(foundPath);
tree.scrollPathToVisible(foundPath);
}
findButton.setEnabled(true);
}
void openFile() {
if (chooseFile()) {
displayTree();
}
}
void run(String[] args) {
setLAF();
setExtendedState(MAXIMIZED_BOTH);
setVisible(true);
if (threadCounter == 1) {
(new SplashWindow(this, 5000)).enableCloseOnClick();
}
if (args.length != 0) {
file = new File(args[0]);
if (!file.exists() || !file.canRead()) {
JOptionPane.showMessageDialog(
this,
"Cannot read file " + args[0] + ", please choose another SWF file!",
"Error", JOptionPane.ERROR_MESSAGE);
file = null;
setTitle("Choose SWF file...");
chooseFile();
}
} else {
setTitle("Choose SWF file...");
chooseFile();
}
if (file == null) {
System.exit(0);
}
setTitle();
display();
displayTree();
setVisible(false);
pack();
setExtendedState(MAXIMIZED_BOTH);
setVisible(true);
}
private void setAccelerators(JComponent component) {
// F5 = Refresh
component.getActionMap().put(
"Refresh",
new AbstractAction("Refresh") {
public void actionPerformed(ActionEvent evt) {
displayTree();
}
});
component.getInputMap().put(
KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "Refresh");
// Ctrl+C = Copy
component.getActionMap().put(
"Copy",
new AbstractAction("Copy") {
public void actionPerformed(ActionEvent evt) {
copy();
}
});
component.getInputMap().put(KeyStroke.getKeyStroke("control C"), "Copy");
// Ctrl+E = Expand
component.getActionMap().put(
"Expand",
new AbstractAction("Expand") {
public void actionPerformed(ActionEvent evt) {
expand();
}
});
component.getInputMap().put(KeyStroke.getKeyStroke("control E"), "Expand");
// F3 = Expand
component.getActionMap().put(
"Find",
new AbstractAction("Find") {
public void actionPerformed(ActionEvent evt) {
find(searchTextField.getText());
}
});
component.getInputMap().put(
KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0), "Find");
// Ctrl+O = Open
component.getActionMap().put(
"Open",
new AbstractAction("Open") {
public void actionPerformed(ActionEvent evt) {
openFile();
}
});
component.getInputMap().put(KeyStroke.getKeyStroke("control O"), "Open");
// Ctrl+N = New
component.getActionMap().put(
"New",
new AbstractAction("New") {
public void actionPerformed(ActionEvent evt) {
String[] args = new String[1];
args[0] = file.getAbsolutePath();
main(args);
}
});
component.getInputMap().put(KeyStroke.getKeyStroke("control N"), "New");
// Ctrl+D = Duplicate
component.getActionMap().put(
"Duplicate",
new AbstractAction("Duplicate") {
public void actionPerformed(ActionEvent evt) {
duplicateFiles();
}
});
component.getInputMap().put(
KeyStroke.getKeyStroke("control D"), "Duplicate");
}
private void setLAF() {
try {
UIManager.setLookAndFeel(
"com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
} catch (Exception e) {
// continue with Java l&f
}
}
private TreePath getPath(TreeNode node) {
java.util.List list = new ArrayList();
// Add all nodes to list
while (node != null) {
list.add(node);
node = node.getParent();
}
Collections.reverse(list);
// Convert array of nodes to TreePath
return new TreePath(list.toArray());
}
private void setTitle() {
setTitle("JSwiff Investigator - " + file);
}
private boolean chooseFile() {
JFileChooser chooser;
if (file != null) {
chooser = new JFileChooser(file);
} else {
chooser = new JFileChooser();
}
chooser.setFileFilter(new SWFFileFilter());
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
file = chooser.getSelectedFile();
} else {
return false;
}
setTitle();
return true;
}
private void display() {
// setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e) {
if (threadCounter > 1) {
threadCounter--;
// Thread.currentThread().interrupt();
setVisible(false);
dispose();
} else {
System.exit(0);
}
}
});
JToolBar toolBar = new JToolBar();
setAccelerators(toolBar);
displayToolBar(toolBar);
initTreePanel();
getContentPane().add(toolBar, "North");
getContentPane().add(treePanel);
statusBar = new JToolBar();
setAccelerators(statusBar);
statusBar.setFloatable(false);
getContentPane().add(statusBar, "South");
}
private void displayToolBar(JToolBar toolBar) {
toolBar.setFloatable(false);
openButton = new JButton("Open");
openButton.setToolTipText("Open new file (Ctrl+O)");
openButton.addActionListener(
new ActionListener() {
public void actionPerformed(ActionEvent e) {
openFile();
}
});
openButton.addMouseListener(
new MouseAdapter() {
public void mouseEntered(MouseEvent e) {
displayStatus("Opens a new file.");
}
public void mouseExited(MouseEvent e) {
displayNodeNumber();
}
});
refreshButton = new JButton("Refresh");
refreshButton.setToolTipText("Refresh tree (F5)");
refreshButton.addActionListener(
new ActionListener() {
public void actionPerformed(ActionEvent e) {
displayTree();
}
});
refreshButton.addMouseListener(
new MouseAdapter() {
public void mouseEntered(MouseEvent e) {
displayStatus("Reads the file again and refreshes the tree view.");
}
public void mouseExited(MouseEvent e) {
displayNodeNumber();
}
});
expandButton = new JButton("Expand");
expandButton.setToolTipText("Expand all nodes (Ctrl+E)");
expandButton.addActionListener(
new ActionListener() {
public void actionPerformed(ActionEvent e) {
expandTree();
}
});
expandButton.addMouseListener(
new MouseAdapter() {
public void mouseEntered(MouseEvent e) {
displayStatus(
"Expands all nodes of the tree. This may take long for big files.");
}
public void mouseExited(MouseEvent e) {
displayNodeNumber();
}
});
findButton = new JButton("Find: ");
findButton.setToolTipText("Find text (F3)");
findButton.addActionListener(
new ActionListener() {
public void actionPerformed(ActionEvent e) {
find(searchTextField.getText());
}
});
findButton.addMouseListener(
new MouseAdapter() {
public void mouseEntered(MouseEvent e) {
displayStatus(
"Searches for text in the tree, starting at the selected node.");
}
public void mouseExited(MouseEvent e) {
displayNodeNumber();
}
});
copyButton = new JButton("Copy");
copyButton.setToolTipText("Copies selected nodes (Ctrl+C)");
copyButton.addActionListener(
new ActionListener() {
public void actionPerformed(ActionEvent e) {
copy();
}
});
copyButton.addMouseListener(
new MouseAdapter() {
public void mouseEntered(MouseEvent e) {
displayStatus(
"Copies selected nodes to clipboard. Use Ctrl/Shift for" +
" multiple selections. Selection order and indentation" +
" levels are preserved.");
}
public void mouseExited(MouseEvent e) {
displayNodeNumber();
}
});
caseSenseCheckBox = new JCheckBox("Case sensitive");
// caseSenseCheckBox.setSelected(false);
caseSenseCheckBox.setToolTipText("Perform case sensitive searches");
setAccelerators(caseSenseCheckBox);
backwardsCheckBox = new JCheckBox("Backwards");
// backwardsCheckBox.setSelected(false);
backwardsCheckBox.setToolTipText("Search backwards");
setAccelerators(backwardsCheckBox);
searchTextField = new JTextField();
searchTextField.setToolTipText("Enter search text");
searchTextField.addMouseListener(
new MouseAdapter() {
public void mouseEntered(MouseEvent e) {
displayStatus("Enter here the text you want to search for.");
}
public void mouseExited(MouseEvent e) {
displayNodeNumber();
}
});
searchTextField.addKeyListener(
new KeyListener() {
public void keyPressed(KeyEvent e) {
if (e.getKeyChar() == '\n') {
find(searchTextField.getText());
}
}
public void keyReleased(KeyEvent e) {
// do nothing
}
public void keyTyped(KeyEvent e) {
// do nothing
}
});
infoButton = new JButton("Info");
infoButton.setToolTipText("Displays program info");
infoButton.addActionListener(
new ActionListener() {
public void actionPerformed(ActionEvent e) {
displayInfo();
}
});
infoButton.addMouseListener(
new MouseAdapter() {
public void mouseEntered(MouseEvent e) {
displayStatus("Displays information about the program.");
}
public void mouseExited(MouseEvent e) {
displayNodeNumber();
}
});
toolBar.add(openButton);
toolBar.addSeparator();
toolBar.add(refreshButton);
toolBar.addSeparator();
toolBar.add(expandButton);
toolBar.addSeparator();
toolBar.add(copyButton);
toolBar.addSeparator();
toolBar.add(findButton);
toolBar.add(searchTextField);
toolBar.add(caseSenseCheckBox);
toolBar.add(backwardsCheckBox);
toolBar.addSeparator();
toolBar.add(infoButton);
}
private String filterHTML(String string) {
StringBuffer result = new StringBuffer();
boolean tag = false;
for (int i = 0; i < string.length(); i++) {
char c = string.charAt(i);
if (c == '<') {
tag = true;
} else if (c == '>') {
tag = false;
} else if (tag == false) {
result.append(c);
}
}
return result.toString();
}
private TreeNode findNode(
TreeNode selectedNode, String searchText, boolean caseSense,
boolean backwards) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode) selectedNode;
if (!caseSense) {
searchText = searchText.toUpperCase();
}
while (true) {
node = backwards ? node.getPreviousNode() : node.getNextNode();
if (node == null) {
break;
}
if (
(caseSense && (node.toString().lastIndexOf(searchText) != -1)) ||
(!caseSense &&
(node.toString().toUpperCase().lastIndexOf(searchText) != -1))) {
return node;
}
}
return null;
}
private void initTreePanel() {
treePanel = new JPanel();
treePanel.setLayout(new GridLayout(1, 1));
setAccelerators(treePanel);
}
private void read() {
Thread readThread = new Thread() {
public void run() {
expandButton.setEnabled(false);
openButton.setEnabled(false);
refreshButton.setEnabled(false);
SWFReader reader;
try {
reader = new SWFReader(new FileInputStream(file));
} catch (FileNotFoundException e) {
// do nothing, just print stack trace
e.printStackTrace();
return;
}
SWFTreeListener listener = new SWFTreeListener(
rootNode, Investigator.this);
reader.addListener(listener);
reader.read();
nodeNumber = listener.getNodeNumber() + 1; // +1 because of root node
displayNodeNumber();
tree.setScrollsOnExpand(true);
tree.setRootVisible(true);
tree.setShowsRootHandles(true);
expandButton.setEnabled(true);
openButton.setEnabled(true);
refreshButton.setEnabled(true);
treePanel.revalidate();
treePanel.repaint();
System.gc(); // let's release some precious RAM
if (listener.isProtected()) {
JOptionPane.showMessageDialog(
Investigator.this,
"This SWF document contains a Protect tag. Make sure you don't violate any copyrights!",
"Protected SWF", JOptionPane.WARNING_MESSAGE);
}
}
};
readThread.start();
}
private final class SWFFileFilter extends FileFilter {
public String getDescription() {
return "Flash files (*.swf)";
}
public boolean accept(File f) {
if (f.isDirectory()) {
return true;
}
String name = f.getName().toLowerCase();
if (name.endsWith(".swf")) {
return true;
}
return false;
}
}
private final class SplashWindow extends JWindow {
private boolean showLicense;
public SplashWindow(Frame f) {
super(f);
showLicense = true;
openSplash(f);
}
public SplashWindow(final Frame f, final long millisecs) {
super(f);
Thread splashThread = new Thread() {
public void run() {
openSplash(f);
try {
Thread.sleep(millisecs);
} catch (InterruptedException e) {
// do nothing
} finally {
closeSplash();
}
}
};
splashThread.start();
}
public void enableCloseOnClick() {
addMouseListener(
new MouseAdapter() {
public void mousePressed(MouseEvent e) {
closeSplash();
}
});
}
void closeSplash() {
setVisible(false);
dispose();
if (infoButton != null) {
infoButton.setEnabled(true);
}
}
void openSplash(Frame f) {
JPanel infoPane = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 0));
URL logoUrl = ClassLoader.getSystemResource(
"com/jswiff/investigator/resources/logo.png");
JLabel logo = new JLabel(new ImageIcon(logoUrl));
logo.setBorder(new BevelBorder(BevelBorder.LOWERED));
infoPane.add(logo);
JLabel copy = new JLabel(
"<html><h3>JSwiff Investigator (v " + SWFDocument.JSWIFF_VERSION +
")</h3>" +
"This software is free, you are welcome to redistribute it<br>" +
"under the terms of the GNU General Public License." +
"<br><p>Part of <b>JSwiff</b>, an open source Java framework<br>" +
"for Macromedia Flash file generation and manipulation.<br>" +
"More information at http://www.jswiff.com<br><br>" +
"© 2004-2005 Ralf Terdic.<br></<html>");
infoPane.add(copy);
JPanel mainPane = new JPanel();
mainPane.setLayout(new BoxLayout(mainPane, BoxLayout.Y_AXIS));
mainPane.setBorder(new SoftBevelBorder(BevelBorder.RAISED));
mainPane.add(infoPane);
if (showLicense) {
JScrollPane licenseScrollPane = getLicenseScrollPane();
mainPane.add(licenseScrollPane);
}
getContentPane().add(mainPane);
pack();
setLocationRelativeTo(f);
setVisible(true);
}
private JScrollPane getLicenseScrollPane() {
URL licenseUrl = getClass().getResource(
"/com/jswiff/investigator/resources/license.html");
JEditorPane licensePane = new JEditorPane();
licensePane.setEditable(false);
licensePane.setContentType("text/html");
try {
licensePane.setPage(licenseUrl);
} catch (IOException e) {
e.printStackTrace();
}
JScrollPane licenseScrollPane = new JScrollPane(licensePane);
licenseScrollPane.setPreferredSize(new Dimension(440, 150));
licenseScrollPane.setMaximumSize(new Dimension(440, 150));
licenseScrollPane.setBorder(new SoftBevelBorder(BevelBorder.LOWERED));
return licenseScrollPane;
}
}
private class NodeStringTransferable implements Transferable {
private String nodeString;
private DataFlavor stringFlavor = new DataFlavor(String.class, "String");
public NodeStringTransferable(String nodeString) {
this.nodeString = nodeString;
}
public boolean isDataFlavorSupported(DataFlavor flavor) {
if (flavor == stringFlavor) {
return true;
}
return false;
}
public Object getTransferData(DataFlavor flavor) {
return nodeString;
}
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[] { stringFlavor };
}
}
}