// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.resource.bcs;
import java.awt.BorderLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Set;
import java.util.SortedMap;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.infinity.gui.BrowserMenuBar;
import org.infinity.gui.ButtonPanel;
import org.infinity.gui.ButtonPopupMenu;
import org.infinity.gui.InfinityScrollPane;
import org.infinity.gui.InfinityTextArea;
import org.infinity.gui.ScriptTextArea;
import org.infinity.gui.ViewFrame;
import org.infinity.icon.Icons;
import org.infinity.resource.Closeable;
import org.infinity.resource.Profile;
import org.infinity.resource.ResourceFactory;
import org.infinity.resource.TextResource;
import org.infinity.resource.ViewableContainer;
import org.infinity.resource.Writeable;
import org.infinity.resource.key.BIFFResourceEntry;
import org.infinity.resource.key.ResourceEntry;
import org.infinity.search.TextResourceSearcher;
import org.infinity.util.Decryptor;
import org.infinity.util.Misc;
import org.infinity.util.io.FileManager;
import org.infinity.util.io.StreamUtils;
public class BafResource implements TextResource, Writeable, Closeable, ItemListener, ActionListener,
DocumentListener
{
// for source panel
private static final ButtonPanel.Control CtrlCompile = ButtonPanel.Control.CUSTOM_1;
private static final ButtonPanel.Control CtrlErrors = ButtonPanel.Control.CUSTOM_2;
private static final ButtonPanel.Control CtrlWarnings = ButtonPanel.Control.CUSTOM_3;
// for code panel
private static final ButtonPanel.Control CtrlDecompile = ButtonPanel.Control.CUSTOM_1;
// for button panel
private static final ButtonPanel.Control CtrlUses = ButtonPanel.Control.CUSTOM_1;
private static final ButtonPanel.Control CtrlSaveScript = ButtonPanel.Control.CUSTOM_2;
private static JFileChooser chooser;
private final ResourceEntry entry;
private final ButtonPanel buttonPanel = new ButtonPanel();
private final ButtonPanel bpSource = new ButtonPanel();
private final ButtonPanel bpCode = new ButtonPanel();
private JTabbedPane tabbedPane;
private JMenuItem ifindall, ifindthis;
private JPanel panel;
private InfinityTextArea codeText, sourceText;
private String text;
private boolean sourceChanged = false;
public BafResource(ResourceEntry entry) throws Exception
{
this.entry = entry;
ByteBuffer buffer = entry.getResourceBuffer();
if (buffer.limit() > 1 && buffer.getShort(0) == -1) {
buffer = Decryptor.decrypt(buffer, 2);
}
text = StreamUtils.readString(buffer, buffer.limit());
}
// --------------------- Begin Interface ActionListener ---------------------
@Override
public void actionPerformed(ActionEvent event)
{
if (bpSource.getControlByType(CtrlCompile) == event.getSource()) {
JButton bCompile = (JButton)event.getSource();
JButton bDecompile = (JButton)bpCode.getControlByType(CtrlDecompile);
JButton bSaveScript = (JButton)buttonPanel.getControlByType(CtrlSaveScript);
ButtonPopupMenu bpmErrors = (ButtonPopupMenu)bpSource.getControlByType(CtrlErrors);
ButtonPopupMenu bpmWarnings = (ButtonPopupMenu)bpSource.getControlByType(CtrlWarnings);
ButtonPopupMenu bpmUses = (ButtonPopupMenu)buttonPanel.getControlByType(CtrlUses);
Compiler compiler = new Compiler(sourceText.getText());
codeText.setText(compiler.getCode());
codeText.setCaretPosition(0);
bCompile.setEnabled(false);
bDecompile.setEnabled(false);
bSaveScript.setEnabled(compiler.getErrors().size() == 0);
SortedMap<Integer, String> errorMap = compiler.getErrors();
SortedMap<Integer, String> warningMap = compiler.getWarnings();
bpmErrors.setText("Errors (" + errorMap.size() + ")...");
bpmWarnings.setText("Warnings (" + warningMap.size() + ")...");
if (errorMap.size() == 0) {
bpmErrors.setEnabled(false);
} else {
JMenuItem errorItems[] = new JMenuItem[errorMap.size()];
int counter = 0;
for (final Integer lineNr : errorMap.keySet()) {
String error = errorMap.get(lineNr);
errorItems[counter++] = new JMenuItem(lineNr.toString() + ": " + error);
}
bpmErrors.setMenuItems(errorItems);
bpmErrors.setEnabled(true);
}
if (warningMap.size() == 0) {
bpmWarnings.setEnabled(false);
} else {
JMenuItem warningItems[] = new JMenuItem[warningMap.size()];
int counter = 0;
for (final Integer lineNr : warningMap.keySet()) {
String warning = warningMap.get(lineNr);
warningItems[counter++] = new JMenuItem(lineNr.toString() + ": " + warning);
}
bpmWarnings.setMenuItems(warningItems);
bpmWarnings.setEnabled(true);
}
Decompiler decompiler = new Decompiler(codeText.getText(), true);
decompiler.decompile();
Set<ResourceEntry> uses = decompiler.getResourcesUsed();
JMenuItem usesItems[] = new JMenuItem[uses.size()];
int usesIndex = 0;
for (final ResourceEntry usesEntry : uses) {
if (usesEntry.getSearchString() != null) {
usesItems[usesIndex++] =
new JMenuItem(usesEntry.toString() + " (" + usesEntry.getSearchString() + ')');
} else {
usesItems[usesIndex++] = new JMenuItem(usesEntry.toString());
}
}
bpmUses.setMenuItems(usesItems);
bpmUses.setEnabled(usesItems.length > 0);
} else if (bpCode.getControlByType(CtrlDecompile) == event.getSource()) {
JButton bDecompile = (JButton)event.getSource();
JButton bCompile = (JButton)bpSource.getControlByType(CtrlCompile);
ButtonPopupMenu bpmUses = (ButtonPopupMenu)buttonPanel.getControlByType(CtrlUses);
Decompiler decompiler = new Decompiler(codeText.getText(), true);
sourceText.setText(decompiler.getSource());
sourceText.setCaretPosition(0);
Set<ResourceEntry> uses = decompiler.getResourcesUsed();
JMenuItem usesItems[] = new JMenuItem[uses.size()];
int usesIndex = 0;
for (final ResourceEntry usesEntry : uses) {
if (usesEntry.getSearchString() != null)
usesItems[usesIndex++] =
new JMenuItem(usesEntry.toString() + " (" + usesEntry.getSearchString() + ')');
else
usesItems[usesIndex++] = new JMenuItem(usesEntry.toString());
}
bpmUses.setMenuItems(usesItems);
bpmUses.setEnabled(usesItems.length > 0);
bCompile.setEnabled(false);
bDecompile.setEnabled(false);
tabbedPane.setSelectedIndex(0);
} else if (buttonPanel.getControlByType(ButtonPanel.Control.SAVE) == event.getSource()) {
JButton bSave = (JButton)event.getSource();
ButtonPopupMenu bpmErrors = (ButtonPopupMenu)bpSource.getControlByType(CtrlErrors);
if (bpmErrors.isEnabled()) {
String options[] = {"Save", "Cancel"};
int result = JOptionPane.showOptionDialog(panel, "Script contains errors. Save anyway?", "Errors found",
JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE, null, options, options[0]);
if (result != 0)
return;
}
if (ResourceFactory.saveResource(this, panel.getTopLevelAncestor())) {
bSave.setEnabled(false);
sourceChanged = false;
}
} else if (buttonPanel.getControlByType(CtrlSaveScript) == event.getSource()) {
if (chooser == null) {
chooser = new JFileChooser(Profile.getGameRoot().toFile());
chooser.setDialogTitle("Save source code");
chooser.setFileFilter(new javax.swing.filechooser.FileFilter()
{
@Override
public boolean accept(File pathname)
{
return pathname.isDirectory() || pathname.getName().toLowerCase(Locale.ENGLISH).endsWith(".bcs");
}
@Override
public String getDescription()
{
return "Infinity script (.BCS)";
}
});
}
chooser.setSelectedFile(new File(StreamUtils.replaceFileExtension(entry.toString(), "BCS")));
int returnval = chooser.showSaveDialog(panel.getTopLevelAncestor());
if (returnval == JFileChooser.APPROVE_OPTION) {
try (BufferedWriter bw = Files.newBufferedWriter(chooser.getSelectedFile().toPath())) {
bw.write(codeText.getText().replaceAll("\r?\n", Misc.LINE_SEPARATOR));
bw.newLine();
JOptionPane.showMessageDialog(panel, "File saved to \"" + chooser.getSelectedFile().toString() +
'\"', "Save completed", JOptionPane.INFORMATION_MESSAGE);
} catch (IOException e) {
JOptionPane.showMessageDialog(panel, "Error saving " + chooser.getSelectedFile().toString(),
"Error", JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
}
}
} else if (buttonPanel.getControlByType(ButtonPanel.Control.EXPORT_BUTTON) == event.getSource()) {
ResourceFactory.exportResource(entry, panel.getTopLevelAncestor());
}
}
// --------------------- End Interface ActionListener ---------------------
// --------------------- Begin Interface Closeable ---------------------
@Override
public void close() throws Exception
{
if (sourceChanged) {
Path output;
if (entry instanceof BIFFResourceEntry) {
output = FileManager.query(Profile.getRootFolders(), Profile.getOverrideFolderName(), entry.toString());
} else {
output = entry.getActualPath();
}
String options[] = {"Save changes", "Discard changes", "Cancel"};
int result = JOptionPane.showOptionDialog(panel, "Save changes to " + output + '?', "Resource changed",
JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE, null, options, options[0]);
if (result == 0)
ResourceFactory.saveResource(this, panel.getTopLevelAncestor());
else if (result != 1)
throw new Exception("Save aborted");
}
}
// --------------------- End Interface Closeable ---------------------
// --------------------- Begin Interface DocumentListener ---------------------
@Override
public void insertUpdate(DocumentEvent event)
{
if (event.getDocument() == codeText.getDocument()) {
bpCode.getControlByType(CtrlDecompile).setEnabled(true);
}
else if (event.getDocument() == sourceText.getDocument()) {
buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(true);
bpSource.getControlByType(CtrlCompile).setEnabled(true);
sourceChanged = true;
}
}
@Override
public void removeUpdate(DocumentEvent event)
{
if (event.getDocument() == codeText.getDocument()) {
bpCode.getControlByType(CtrlDecompile).setEnabled(true);
}
else if (event.getDocument() == sourceText.getDocument()) {
buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(true);
bpSource.getControlByType(CtrlCompile).setEnabled(true);
sourceChanged = true;
}
}
@Override
public void changedUpdate(DocumentEvent event)
{
if (event.getDocument() == codeText.getDocument()) {
bpCode.getControlByType(CtrlDecompile).setEnabled(true);
}
else if (event.getDocument() == sourceText.getDocument()) {
buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(true);
bpSource.getControlByType(CtrlCompile).setEnabled(true);
sourceChanged = true;
}
}
// --------------------- End Interface DocumentListener ---------------------
// --------------------- Begin Interface ItemListener ---------------------
@Override
public void itemStateChanged(ItemEvent event)
{
if (buttonPanel.getControlByType(ButtonPanel.Control.FIND_MENU) == event.getSource()) {
ButtonPopupMenu bpmFind = (ButtonPopupMenu)event.getSource();
if (bpmFind.getSelectedItem() == ifindall) {
java.util.List<ResourceEntry> files = ResourceFactory.getResources("BAF");
new TextResourceSearcher(files, panel.getTopLevelAncestor());
} else if (bpmFind.getSelectedItem() == ifindthis) {
java.util.List<ResourceEntry> files = new ArrayList<ResourceEntry>(1);
files.add(entry);
new TextResourceSearcher(files, panel.getTopLevelAncestor());
}
} else if (buttonPanel.getControlByType(CtrlUses) == event.getSource()) {
ButtonPopupMenu bpmUses = (ButtonPopupMenu)event.getSource();
JMenuItem item = bpmUses.getSelectedItem();
String name = item.getText();
int index = name.indexOf(" (");
if (index != -1) {
name = name.substring(0, index);
}
ResourceEntry resEntry = ResourceFactory.getResourceEntry(name);
new ViewFrame(panel.getTopLevelAncestor(), ResourceFactory.getResource(resEntry));
} else if (bpSource.getControlByType(CtrlErrors) == event.getSource()) {
ButtonPopupMenu bpmErrors = (ButtonPopupMenu)event.getSource();
String selected = bpmErrors.getSelectedItem().getText();
int linenr = Integer.parseInt(selected.substring(0, selected.indexOf(": ")));
highlightText(linenr, null);
} else if (bpSource.getControlByType(CtrlWarnings) == event.getSource()) {
ButtonPopupMenu bpmWarnings = (ButtonPopupMenu)event.getSource();
String selected = bpmWarnings.getSelectedItem().getText();
int linenr = Integer.parseInt(selected.substring(0, selected.indexOf(": ")));
highlightText(linenr, null);
}
}
// --------------------- End Interface ItemListener ---------------------
// --------------------- Begin Interface Resource ---------------------
@Override
public ResourceEntry getResourceEntry()
{
return entry;
}
// --------------------- End Interface Resource ---------------------
// --------------------- Begin Interface TextResource ---------------------
@Override
public String getText()
{
return text;
}
@Override
public void highlightText(int linenr, String highlightText)
{
String s = sourceText.getText();
int startpos = 0;
int i = (s.charAt(0) == '\n') ? 2 : 1;
for (; i < linenr; i++) {
startpos = s.indexOf("\n", startpos + 1);
}
if (startpos == -1) return;
if (highlightText != null) {
// try to select specified text string
int wordpos = -1;
if (highlightText != null) {
wordpos = s.toUpperCase(Locale.ENGLISH).indexOf(highlightText.toUpperCase(Locale.ENGLISH), startpos);
}
if (wordpos != -1) {
sourceText.select(wordpos, wordpos + highlightText.length());
} else {
sourceText.select(startpos, s.indexOf("\n", startpos + 1));
}
} else {
// select whole line
int endpos = s.indexOf("\n", startpos + 1);
if (endpos < 0) {
endpos = s.length();
}
sourceText.select(startpos, endpos);
}
sourceText.getCaret().setSelectionVisible(true);
}
// --------------------- End Interface TextResource ---------------------
// --------------------- Begin Interface Viewable ---------------------
@Override
public JComponent makeViewer(ViewableContainer container)
{
sourceText = new ScriptTextArea();
sourceText.setText(text);
sourceText.setCaretPosition(0);
sourceText.setAutoIndentEnabled(BrowserMenuBar.getInstance().getBcsAutoIndentEnabled());
sourceText.addCaretListener(container.getStatusBar());
sourceText.setFont(BrowserMenuBar.getInstance().getScriptFont());
sourceText.setMargin(new Insets(3, 3, 3, 3));
sourceText.setLineWrap(false);
sourceText.getDocument().addDocumentListener(this);
InfinityScrollPane scrollSource = new InfinityScrollPane(sourceText, true);
scrollSource.setBorder(BorderFactory.createLineBorder(UIManager.getColor("controlDkShadow")));
JButton bCompile = new JButton("Compile", Icons.getIcon(Icons.ICON_REDO_16));
bCompile.setMnemonic('c');
bCompile.addActionListener(this);
ButtonPopupMenu bpmErrors = new ButtonPopupMenu("Errors (0)...", new JMenuItem[]{});
bpmErrors.setIcon(Icons.getIcon(Icons.ICON_UP_16));
bpmErrors.addItemListener(this);
ButtonPopupMenu bpmWarnings = new ButtonPopupMenu("Warnings (0)...", new JMenuItem[]{});
bpmWarnings.setIcon(Icons.getIcon(Icons.ICON_UP_16));
bpmWarnings.addItemListener(this);
bpSource.addControl(bCompile, CtrlCompile);
bpSource.addControl(bpmErrors, CtrlErrors);
bpSource.addControl(bpmWarnings, CtrlWarnings);
JPanel sourcePanel = new JPanel(new BorderLayout());
sourcePanel.add(scrollSource, BorderLayout.CENTER);
sourcePanel.add(bpSource, BorderLayout.SOUTH);
codeText = new InfinityTextArea(true);
codeText.setFont(BrowserMenuBar.getInstance().getScriptFont());
codeText.setMargin(new Insets(3, 3, 3, 3));
codeText.setCaretPosition(0);
codeText.setLineWrap(false);
codeText.getDocument().addDocumentListener(this);
InfinityScrollPane scrollCode = new InfinityScrollPane(codeText, true);
scrollCode.setBorder(BorderFactory.createLineBorder(UIManager.getColor("controlDkShadow")));
JButton bDecompile = new JButton("Decompile", Icons.getIcon(Icons.ICON_UNDO_16));
bDecompile.setMnemonic('d');
bDecompile.addActionListener(this);
bpCode.addControl(bDecompile, CtrlDecompile);
JPanel codePanel = new JPanel(new BorderLayout());
codePanel.add(scrollCode, BorderLayout.CENTER);
codePanel.add(bpCode, BorderLayout.SOUTH);
ifindall = new JMenuItem("in all scripts");
ifindthis = new JMenuItem("in this script only");
ButtonPopupMenu bpmFind = (ButtonPopupMenu)buttonPanel.addControl(ButtonPanel.Control.FIND_MENU);
bpmFind.setMenuItems(new JMenuItem[]{ifindall, ifindthis});
bpmFind.addItemListener(this);
ButtonPopupMenu bpmUses = new ButtonPopupMenu("Uses...", new JMenuItem[]{});
bpmUses.setIcon(Icons.getIcon(Icons.ICON_FIND_16));
bpmUses.addItemListener(this);
buttonPanel.addControl(bpmUses, CtrlUses);
((JButton)buttonPanel.addControl(ButtonPanel.Control.EXPORT_BUTTON)).addActionListener(this);
JButton bSave = (JButton)buttonPanel.addControl(ButtonPanel.Control.SAVE);
bSave.addActionListener(this);
JButton bSaveScript = new JButton("Save code", Icons.getIcon(Icons.ICON_SAVE_16));
bSaveScript.addActionListener(this);
buttonPanel.addControl(bSaveScript, CtrlSaveScript);
tabbedPane = new JTabbedPane();
tabbedPane.addTab("Script source", sourcePanel);
tabbedPane.addTab("Script code (compiled)", codePanel);
panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.add(tabbedPane, BorderLayout.CENTER);
panel.add(buttonPanel, BorderLayout.SOUTH);
bCompile.setEnabled(true);
bpmErrors.setEnabled(false);
bpmWarnings.setEnabled(false);
bDecompile.setEnabled(false);
bSave.setEnabled(false);
bpmUses.setEnabled(false);
bSaveScript.setEnabled(false);
return panel;
}
// --------------------- End Interface Viewable ---------------------
// --------------------- Begin Interface Writeable ---------------------
@Override
public void write(OutputStream os) throws IOException
{
if (sourceText == null) {
StreamUtils.writeString(os, text, text.length());
} else {
sourceText.write(new OutputStreamWriter(os));
}
}
// --------------------- End Interface Writeable ---------------------
}