/*
* 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;
import com.jpexs.decompiler.flash.SWF;
import com.jpexs.decompiler.flash.amf.amf3.Amf3Value;
import com.jpexs.decompiler.flash.gui.generictageditors.Amf3ValueEditor;
import com.jpexs.decompiler.flash.gui.generictageditors.BinaryDataEditor;
import com.jpexs.decompiler.flash.gui.generictageditors.BooleanEditor;
import com.jpexs.decompiler.flash.gui.generictageditors.ChangeListener;
import com.jpexs.decompiler.flash.gui.generictageditors.ColorEditor;
import com.jpexs.decompiler.flash.gui.generictageditors.GenericTagEditor;
import com.jpexs.decompiler.flash.gui.generictageditors.NumberEditor;
import com.jpexs.decompiler.flash.gui.generictageditors.StringEditor;
import com.jpexs.decompiler.flash.gui.helpers.SpringUtilities;
import com.jpexs.decompiler.flash.tags.Tag;
import com.jpexs.decompiler.flash.types.ARGB;
import com.jpexs.decompiler.flash.types.RGB;
import com.jpexs.decompiler.flash.types.RGBA;
import com.jpexs.decompiler.flash.types.annotations.Calculated;
import com.jpexs.decompiler.flash.types.annotations.Conditional;
import com.jpexs.decompiler.flash.types.annotations.Internal;
import com.jpexs.decompiler.flash.types.annotations.Multiline;
import com.jpexs.decompiler.flash.types.annotations.Optional;
import com.jpexs.decompiler.flash.types.annotations.SWFType;
import com.jpexs.decompiler.flash.types.annotations.parser.AnnotationParseException;
import com.jpexs.decompiler.flash.types.annotations.parser.ConditionEvaluator;
import com.jpexs.helpers.ByteArrayRange;
import com.jpexs.helpers.Helper;
import com.jpexs.helpers.ReflectionTools;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SpringLayout;
/**
* Old Generic Tag editor
*
* @author JPEXS
*/
public class GenericTagPanel extends JPanel implements ChangeListener {
private static final Logger logger = Logger.getLogger(GenericTagPanel.class.getName());
protected final MainPanel mainPanel;
private final JEditorPane genericTagPropertiesEditorPane;
private final JPanel genericTagPropertiesEditPanel;
private final JScrollPane genericTagPropertiesEditorPaneScrollPanel;
private final JScrollPane genericTagPropertiesEditPanelScrollPanel;
private Tag tag;
private Tag editedTag;
private List<String> keys = new ArrayList<>();
private Map<String, GenericTagEditor> editors = new HashMap<>();
private Map<String, Component> labels = new HashMap<>();
private Map<String, Component> types = new HashMap<>();
private Map<String, List<Field>> fieldPaths = new HashMap<>();
private Map<String, List<Integer>> fieldIndices = new HashMap<>();
private HeaderLabel hdr;
private Set<String> addKeys = new HashSet<>();
private Map<String, Component> addButtons = new HashMap<>();
private Map<String, Component> removeButtons = new HashMap<>();
public GenericTagPanel(MainPanel mainPanel) {
super(new BorderLayout());
this.mainPanel = mainPanel;
hdr = new HeaderLabel("");
add(hdr, BorderLayout.NORTH);
genericTagPropertiesEditorPane = new JEditorPane() {
@Override
public boolean getScrollableTracksViewportWidth() {
return true;
}
};
genericTagPropertiesEditorPane.setEditable(false);
genericTagPropertiesEditorPaneScrollPanel = new JScrollPane(genericTagPropertiesEditorPane);
add(genericTagPropertiesEditorPaneScrollPanel, BorderLayout.CENTER);
genericTagPropertiesEditPanel = new JPanel();
genericTagPropertiesEditPanel.setLayout(new SpringLayout());
JPanel edPanel = new JPanel(new BorderLayout());
edPanel.add(genericTagPropertiesEditPanel, BorderLayout.NORTH);
genericTagPropertiesEditPanelScrollPanel = new JScrollPane(edPanel);
}
public void clear() {
editors.clear();
fieldPaths.clear();
fieldIndices.clear();
labels.clear();
types.clear();
keys.clear();
addKeys.clear();
addButtons.clear();
removeButtons.clear();
genericTagPropertiesEditPanel.removeAll();
genericTagPropertiesEditPanel.setSize(0, 0);
}
public void setEditMode(boolean edit, Tag tag) {
if (tag == null) {
tag = this.tag;
}
this.tag = tag;
this.editedTag = Helper.deepCopy(tag);
generateEditControls(editedTag, !edit);
if (edit) {
remove(genericTagPropertiesEditorPaneScrollPanel);
add(genericTagPropertiesEditPanelScrollPanel, BorderLayout.CENTER);
} else {
genericTagPropertiesEditPanel.removeAll();
genericTagPropertiesEditPanel.setSize(0, 0);
remove(genericTagPropertiesEditPanelScrollPanel);
add(genericTagPropertiesEditorPaneScrollPanel, BorderLayout.CENTER);
setTagText(this.tag);
}
revalidate();
repaint();
}
private void setTagText(Tag tag) {
clear();
generateEditControls(tag, true);
StringBuilder val = new StringBuilder();
for (String key : keys) {
GenericTagEditor ed = editors.get(key);
if (((Component) ed).isVisible()) {
val.append(key).append(" : ").append(ed.getReadOnlyValue()).append("<br>");
}
}
//HTML for colors:
val.insert(0, "<html>").append("</html>");
genericTagPropertiesEditorPane.setContentType("text/html");
genericTagPropertiesEditorPane.setText(val.toString());
genericTagPropertiesEditorPane.setCaretPosition(0);
hdr.setText(tag.toString());
}
private void generateEditControls(Tag tag, boolean readonly) {
clear();
generateEditControlsRecursive(tag, "", new ArrayList<>(), new ArrayList<>(), readonly);
change(null);
}
private void relayout(int propCount) {
//Lay out the panel.
SpringUtilities.makeCompactGrid(genericTagPropertiesEditPanel,
propCount, 3, //rows, cols
6, 6, //initX, initY
6, 6); //xPad, yPad
revalidate();
repaint();
}
private int generateEditControlsRecursive(final Object obj, String parent, List<Field> parentFields, List<Integer> parentIndices, boolean readonly) {
if (obj == null) {
return 0;
}
Field[] fields = obj.getClass().getDeclaredFields();
int propCount = 0;
for (final Field field : fields) {
try {
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
field.setAccessible(true);
String name = parent + field.getName();
final Object value = field.get(obj);
if (List.class.isAssignableFrom(field.getType())) {
if (value != null) {
int i = 0;
for (Object obj1 : (Iterable) value) {
final String subname = name + "[" + i + "]";
propCount += addEditor(subname, obj, field, i, obj1.getClass(), obj1, parentFields, parentIndices, readonly);
final int fi = i;
i++;
JButton removeButton = new JButton(View.getIcon("close16"));
removeButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
removeItem(obj, field, fi);
}
});
removeButtons.put(subname, removeButton);
}
}
} else if (field.getType().isArray()) {
if (value != null) {
for (int i = 0; i < Array.getLength(value); i++) {
Object item = Array.get(value, i);
String subname = name + "[" + i + "]";
propCount += addEditor(subname, obj, field, i, item.getClass(), item, parentFields, parentIndices, readonly);
final int fi = i;
JButton removeButton = new JButton(View.getIcon("close16"));
removeButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
removeItem(obj, field, fi);
}
});
removeButtons.put(subname, removeButton);
}
}
} else {
propCount += addEditor(name, obj, field, 0, field.getType(), value, parentFields, parentIndices, readonly);
}
if (ReflectionTools.needsIndex(field) && !readonly && !field.getName().equals("clipActionRecords")) { //No clip actions, sorry
JButton addButton = new JButton(View.getIcon("add16"));
addButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
addItem(obj, field);
}
});
name += "[]";
List<Field> parList = new ArrayList<>(parentFields);
parList.add(field);
fieldPaths.put(name, parList);
List<Integer> parIndices = new ArrayList<>(parentIndices);
parIndices.add(0);
fieldIndices.put(name, parIndices);
addRow(name, addButton, field);
addKeys.add(name);
addButtons.put(name, addButton);
}
} catch (IllegalArgumentException | IllegalAccessException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
return propCount;
}
private void removeItem(Object obj, Field field, int index) {
final JScrollBar sb = genericTagPropertiesEditPanelScrollPanel.getVerticalScrollBar();
final int val = sb.getValue(); //save scroll top
SWFType swfType = field.getAnnotation(SWFType.class);
if (swfType != null && !swfType.countField().isEmpty()) { //Fields with same countField must be removed from too
Field[] fields = obj.getClass().getDeclaredFields();
for (int f = 0; f < fields.length; f++) {
SWFType fieldSwfType = fields[f].getAnnotation(SWFType.class);
if (fieldSwfType != null && fieldSwfType.countField().equals(swfType.countField())) {
ReflectionTools.removeFromField(obj, fields[f], index);
}
}
try {
//If countField exists, decrement, otherwise do nothing
Field countField = obj.getClass().getDeclaredField(swfType.countField());
int cnt = countField.getInt(obj);
cnt--;
countField.setInt(obj, cnt);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
//ignored
}
} else {
ReflectionTools.removeFromField(obj, field, index);
}
generateEditControls(editedTag, false);
//Restore scroll top after some time. TODO: Handle this better. I don't know how :-(.
new Thread() {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
logger.log(Level.SEVERE, null, ex);
}
View.execInEventDispatch(() -> {
genericTagPropertiesEditPanelScrollPanel.getVerticalScrollBar().setValue(val);
});
}
}.start();
revalidate();
repaint();
}
private void addItem(Object obj, Field field) {
final JScrollBar sb = genericTagPropertiesEditPanelScrollPanel.getVerticalScrollBar();
final int val = sb.getValue(); //save scroll top
SWFType swfType = field.getAnnotation(SWFType.class);
if (swfType != null && !swfType.countField().isEmpty()) { //Fields with same countField must be enlarged too
Field[] fields = obj.getClass().getDeclaredFields();
for (int f = 0; f < fields.length; f++) {
SWFType fieldSwfType = fields[f].getAnnotation(SWFType.class);
if (fieldSwfType != null && fieldSwfType.countField().equals(swfType.countField())) {
ReflectionTools.addToField(obj, fields[f], ReflectionTools.getFieldSubSize(obj, fields[f]), true, null);
}
}
try {
//If countField exists, increment, otherwise do nothing
Field countField = obj.getClass().getDeclaredField(swfType.countField());
int cnt = countField.getInt(obj);
cnt++;
countField.setInt(obj, cnt);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
//ignored
}
} else {
ReflectionTools.addToField(obj, field, ReflectionTools.getFieldSubSize(obj, field), true, null);
}
generateEditControls(editedTag, false);
//Restore scroll top after some time. TODO: Handle this better. I don't know how :-(.
new Thread() {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
logger.log(Level.SEVERE, null, ex);
}
View.execInEventDispatch(() -> {
genericTagPropertiesEditPanelScrollPanel.getVerticalScrollBar().setValue(val);
});
}
}.start();
revalidate();
repaint();
}
private int addEditor(String name, Object obj, Field field, int index, Class<?> type, Object value, List<Field> parentList, List<Integer> parentIndices, boolean readonly) throws IllegalArgumentException, IllegalAccessException {
Calculated calculated = field.getAnnotation(Calculated.class);
if (calculated != null) {
return 0;
}
List<Field> parList = new ArrayList<>(parentList);
parList.add(field);
List<Integer> parIndices = new ArrayList<>(parentIndices);
parIndices.add(index);
Internal inter = field.getAnnotation(Internal.class);
if (inter != null) {
return 0;
}
SWFType swfType = field.getAnnotation(SWFType.class);
Multiline multiline = field.getAnnotation(Multiline.class);
Component editor;
if (type.equals(int.class) || type.equals(Integer.class)
|| type.equals(short.class) || type.equals(Short.class)
|| type.equals(long.class) || type.equals(Long.class)
|| type.equals(double.class) || type.equals(Double.class)
|| type.equals(float.class) || type.equals(Float.class)) {
editor = new NumberEditor(name, obj, field, index, type, swfType);
} else if (type.equals(boolean.class) || type.equals(Boolean.class)) {
editor = new BooleanEditor(name, obj, field, index, type);
} else if (type.equals(String.class)) {
editor = new StringEditor(name, obj, field, index, type, multiline != null);
} else if (type.equals(RGB.class) || type.equals(RGBA.class) || type.equals(ARGB.class)) {
editor = new ColorEditor(name, obj, field, index, type);
} else if (type.equals(ByteArrayRange.class)) {
editor = new BinaryDataEditor(mainPanel, name, obj, field, index, type);
} else if (type.equals(Amf3Value.class)) {
editor = new Amf3ValueEditor(name, obj, field, index, type);
} else {
if (value == null) {
if (readonly) {
return 0;
}
Optional opt = field.getAnnotation(Optional.class);
if (opt == null) {
try {
value = ReflectionTools.newInstanceOf(field.getType());
field.set(obj, value);
} catch (InstantiationException | IllegalAccessException ex) {
logger.log(Level.SEVERE, null, ex);
return 0;
}
} else {
return 0;
}
}
return generateEditControlsRecursive(value, name + ".", parList, parIndices, readonly);
}
if (editor instanceof GenericTagEditor) {
GenericTagEditor ce = (GenericTagEditor) editor;
ce.addChangeListener(this);
editors.put(name, ce);
fieldPaths.put(name, parList);
fieldIndices.put(name, parIndices);
addRow(name, editor, field);
ce.added();
}
return 1;
}
private void addRow(String name, Component editor, Field field) {
JLabel label = new JLabel(name + ":", JLabel.TRAILING);
label.setVerticalAlignment(JLabel.TOP);
genericTagPropertiesEditPanel.add(label);
label.setLabelFor(editor);
labels.put(name, label);
genericTagPropertiesEditPanel.add(editor);
JLabel typeLabel = new JLabel(swfTypeToString(field.getAnnotation(SWFType.class)), JLabel.TRAILING);
typeLabel.setVerticalAlignment(JLabel.TOP);
genericTagPropertiesEditPanel.add(typeLabel);
types.put(name, typeLabel);
keys.add(name);
}
public String swfTypeToString(SWFType swfType) {
if (swfType == null) {
return null;
}
String result = swfType.value().toString();
if (swfType.count() > 0) {
result += "[" + swfType.count();
if (swfType.countAdd() > 0) {
result += " + " + swfType.countAdd();
}
result += "]";
} else if (!swfType.countField().isEmpty()) {
result += "[" + swfType.countField();
if (swfType.countAdd() > 0) {
result += " + " + swfType.countAdd();
}
result += "]";
}
return result;
}
private void assignTag(Tag t, Tag assigned) {
if (t.getClass() != assigned.getClass()) {
return;
}
for (Field f : t.getClass().getDeclaredFields()) {
if ((f.getModifiers() & Modifier.FINAL) == Modifier.FINAL) {
continue;
}
if ((f.getModifiers() & Modifier.STATIC) == Modifier.STATIC) {
continue;
}
try {
f.set(t, f.get(assigned));
} catch (IllegalArgumentException | IllegalAccessException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
}
public boolean save() {
for (Object component : genericTagPropertiesEditPanel.getComponents()) {
if (component instanceof GenericTagEditor) {
try {
((GenericTagEditor) component).validateValue();
((GenericTagEditor) component).save();
} catch (IllegalArgumentException iex) {
return false;
}
}
}
SWF swf = tag.getSwf();
assignTag(tag, editedTag);
tag.setModified(true);
tag.setSwf(swf);
setTagText(tag);
return true;
}
public Tag getTag() {
return tag;
}
@Override
public void change(GenericTagEditor ed) {
for (String key : editors.keySet()) {
GenericTagEditor dependentEditor = editors.get(key);
Component dependentLabel = labels.get(key);
Component dependentTypeLabel = types.get(key);
List<Field> path = fieldPaths.get(key);
List<Integer> indices = fieldIndices.get(key);
String p = "";
boolean conditionMet = true;
for (int i = 0; i < path.size(); i++) {
Field f = path.get(i);
int index = indices.get(i);
String par = p;
if (!p.isEmpty()) {
p += ".";
}
p += f.getName();
if (ReflectionTools.needsIndex(f)) {
p += "[" + index + "]";
}
Conditional cond = f.getAnnotation(Conditional.class);
if (cond != null) {
ConditionEvaluator ev = new ConditionEvaluator(cond);
try {
Set<String> fieldNames = ev.getFields();
Map<String, Boolean> fields = new HashMap<>();
for (String fld : fieldNames) {
String ckey = "";
if (!par.isEmpty()) {
ckey = par + ".";
}
ckey += fld;
if (editors.containsKey(ckey)) {
GenericTagEditor editor = editors.get(ckey);
Object val = editor.getChangedValue();
fields.put(fld, true);
if (val instanceof Boolean) {
fields.put(fld, (Boolean) val);
}
}
}
boolean ok = ev.eval(fields);
if (conditionMet) {
conditionMet = ok;
}
((Component) dependentEditor).setVisible(conditionMet);
dependentLabel.setVisible(conditionMet);
dependentTypeLabel.setVisible(conditionMet);
} catch (AnnotationParseException ex) {
logger.log(Level.SEVERE, "Invalid condition", ex);
}
}
if (!conditionMet) {
break;
}
}
}
genericTagPropertiesEditPanel.removeAll();
genericTagPropertiesEditPanel.setSize(0, 0);
int propCount = 0;
for (String key : keys) {
Component dependentEditor;
if (addKeys.contains(key)) {
dependentEditor = addButtons.get(key);
} else if (removeButtons.containsKey(key)) { //It's array/list, add remove button
JPanel editRemPanel = new JPanel(new BorderLayout());
editRemPanel.add((Component) editors.get(key), BorderLayout.CENTER);
editRemPanel.add(removeButtons.get(key), BorderLayout.EAST);
dependentEditor = editRemPanel;
} else {
dependentEditor = (Component) editors.get(key);
}
Component dependentLabel = labels.get(key);
Component dependentTypeLabel = types.get(key);
if (dependentEditor.isVisible()) {
genericTagPropertiesEditPanel.add(dependentLabel);
genericTagPropertiesEditPanel.add(((Component) dependentEditor));
genericTagPropertiesEditPanel.add(dependentTypeLabel);
propCount++;
}
}
/*genericTagPropertiesEditPanel.add(new JPanel());
genericTagPropertiesEditPanel.add(new JPanel());
genericTagPropertiesEditPanel.add(new JPanel());*/
relayout(propCount /*+ 1*/);
}
}