// 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.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; 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 javax.swing.filechooser.FileFilter; 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.ScriptReferenceSearcher; 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 final class BcsResource implements TextResource, Writeable, Closeable, ActionListener, ItemListener, DocumentListener { // for decompile 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 compiled 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 JFileChooser chooser; private final ResourceEntry entry; private final ButtonPanel buttonPanel = new ButtonPanel(); private final ButtonPanel bpDecompile = new ButtonPanel(); private final ButtonPanel bpCompiled = new ButtonPanel(); private JMenuItem ifindall, ifindthis, ifindusage, iexportsource, iexportscript; private JPanel panel; private JTabbedPane tabbedPane; private InfinityTextArea codeText; private ScriptTextArea sourceText; private String text; private boolean sourceChanged = false, codeChanged = false; public BcsResource(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 (bpDecompile.getControlByType(CtrlCompile) == event.getSource()) { JButton bCompile = (JButton)event.getSource(); JButton bDecompile = (JButton)bpCompiled.getControlByType(CtrlDecompile); ButtonPopupMenu bpmErrors = (ButtonPopupMenu)bpDecompile.getControlByType(CtrlErrors); ButtonPopupMenu bpmWarnings = (ButtonPopupMenu)bpDecompile.getControlByType(CtrlWarnings); Compiler compiler = new Compiler(sourceText.getText()); codeText.setText(compiler.getCode()); codeText.setCaretPosition(0); bCompile.setEnabled(false); bDecompile.setEnabled(false); sourceChanged = false; codeChanged = true; iexportscript.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); } } else if (bpCompiled.getControlByType(CtrlDecompile) == event.getSource()) { JButton bDecompile = (JButton)event.getSource(); JButton bCompile = (JButton)bpDecompile.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); sourceChanged = false; tabbedPane.setSelectedIndex(0); } else if (buttonPanel.getControlByType(ButtonPanel.Control.SAVE) == event.getSource()) { JButton bSave = (JButton)event.getSource(); ButtonPopupMenu bpmErrors = (ButtonPopupMenu)bpDecompile.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; codeChanged = false; } } } // --------------------- End Interface ActionListener --------------------- // --------------------- Begin Interface Closeable --------------------- @Override public void close() throws Exception { if (sourceChanged) { String options[] = {"Compile & save", "Discard changes", "Cancel"}; int result = JOptionPane.showOptionDialog(panel, "Script contains uncompiled changes", "Uncompiled changes", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); if (result == 0) { ((JButton)bpDecompile.getControlByType(CtrlCompile)).doClick(); if (bpDecompile.getControlByType(CtrlErrors).isEnabled()) { throw new Exception("Save aborted"); } ResourceFactory.saveResource(this, panel.getTopLevelAncestor()); } else if (result == 2 || result == JOptionPane.CLOSED_OPTION) throw new Exception("Save aborted"); } else if (codeChanged) { 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()) { buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(true); bpCompiled.getControlByType(CtrlDecompile).setEnabled(true); sourceChanged = false; codeChanged = true; } else if (event.getDocument() == sourceText.getDocument()) { bpDecompile.getControlByType(CtrlCompile).setEnabled(true); sourceChanged = true; } } @Override public void removeUpdate(DocumentEvent event) { if (event.getDocument() == codeText.getDocument()) { buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(true); bpCompiled.getControlByType(CtrlDecompile).setEnabled(true); sourceChanged = false; codeChanged = true; } else if (event.getDocument() == sourceText.getDocument()) { bpDecompile.getControlByType(CtrlCompile).setEnabled(true); sourceChanged = true; } } @Override public void changedUpdate(DocumentEvent event) { if (event.getDocument() == codeText.getDocument()) { buttonPanel.getControlByType(ButtonPanel.Control.SAVE).setEnabled(true); bpCompiled.getControlByType(CtrlDecompile).setEnabled(true); sourceChanged = false; codeChanged = true; } else if (event.getDocument() == sourceText.getDocument()) { bpDecompile.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) { List<ResourceEntry> files = ResourceFactory.getResources("BCS"); files.addAll(ResourceFactory.getResources("BS")); new TextResourceSearcher(files, panel.getTopLevelAncestor()); } else if (bpmFind.getSelectedItem() == ifindthis) { List<ResourceEntry> files = new ArrayList<ResourceEntry>(1); files.add(entry); new TextResourceSearcher(files, panel.getTopLevelAncestor()); } else if (bpmFind.getSelectedItem() == ifindusage) new ScriptReferenceSearcher(entry, 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 (buttonPanel.getControlByType(ButtonPanel.Control.EXPORT_MENU) == event.getSource()) { ButtonPopupMenu bpmExport = (ButtonPopupMenu)event.getSource(); if (bpmExport.getSelectedItem() == iexportsource) { if (chooser == null) { chooser = new JFileChooser(Profile.getGameRoot().toFile()); chooser.setDialogTitle("Export source"); chooser.setFileFilter(new FileFilter() { @Override public boolean accept(File pathname) { return pathname.isDirectory() || pathname.getName().toLowerCase(Locale.ENGLISH).endsWith(".baf"); } @Override public String getDescription() { return "Infinity script (.BAF)"; } }); } chooser.setSelectedFile(new File(StreamUtils.replaceFileExtension(entry.toString(), "BAF"))); int returnval = chooser.showSaveDialog(panel.getTopLevelAncestor()); if (returnval == JFileChooser.APPROVE_OPTION) { try (BufferedWriter bw = Files.newBufferedWriter(chooser.getSelectedFile().toPath())) { bw.write(sourceText.getText().replaceAll("\r?\n", Misc.LINE_SEPARATOR)); bw.newLine(); JOptionPane.showMessageDialog(panel, "File saved to \"" + chooser.getSelectedFile().toString() + '\"', "Export complete", JOptionPane.INFORMATION_MESSAGE); } catch (IOException e) { JOptionPane.showMessageDialog(panel, "Error exporting " + chooser.getSelectedFile().toString(), "Error", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); } } } else if (bpmExport.getSelectedItem() == iexportscript) { ResourceFactory.exportResource(entry, panel.getTopLevelAncestor()); } } else if (bpDecompile.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 (bpDecompile.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() { if (sourceText != null) { return sourceText.getText(); } Decompiler decompiler = new Decompiler(text, false); return decompiler.getSource(); } @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.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 scrollDecompiled = new InfinityScrollPane(sourceText, true); scrollDecompiled.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[0]); bpmErrors.setIcon(Icons.getIcon(Icons.ICON_UP_16)); bpmErrors.addItemListener(this); ButtonPopupMenu bpmWarnings = new ButtonPopupMenu("Warnings (0)...", new JMenuItem[0]); bpmWarnings.setIcon(Icons.getIcon(Icons.ICON_UP_16)); bpmWarnings.addItemListener(this); bpDecompile.addControl(bCompile, CtrlCompile); bpDecompile.addControl(bpmErrors, CtrlErrors); bpDecompile.addControl(bpmWarnings, CtrlWarnings); JPanel decompiledPanel = new JPanel(new BorderLayout()); decompiledPanel.add(scrollDecompiled, BorderLayout.CENTER); decompiledPanel.add(bpDecompile, BorderLayout.SOUTH); codeText = new InfinityTextArea(text, 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 scrollCompiled = new InfinityScrollPane(codeText, true); scrollCompiled.setBorder(BorderFactory.createLineBorder(UIManager.getColor("controlDkShadow"))); JButton bDecompile = new JButton("Decompile", Icons.getIcon(Icons.ICON_UNDO_16)); bDecompile.setMnemonic('d'); bDecompile.addActionListener(this); bpCompiled.addControl(bDecompile, CtrlDecompile); JPanel compiledPanel = new JPanel(new BorderLayout()); compiledPanel.add(scrollCompiled, BorderLayout.CENTER); compiledPanel.add(bpCompiled, BorderLayout.SOUTH); ifindall = new JMenuItem("in all scripts"); ifindthis = new JMenuItem("in this script only"); ifindusage = new JMenuItem("references to this script"); ButtonPopupMenu bpmFind = (ButtonPopupMenu)buttonPanel.addControl(ButtonPanel.Control.FIND_MENU); bpmFind.setMenuItems(new JMenuItem[]{ifindall, ifindthis, ifindusage}); 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); iexportscript = new JMenuItem("script code"); iexportsource = new JMenuItem("script source"); iexportscript.setToolTipText("NB! Will export last *saved* version"); ButtonPopupMenu bpmExport = (ButtonPopupMenu)buttonPanel.addControl(ButtonPanel.Control.EXPORT_MENU); bpmExport.setMenuItems(new JMenuItem[]{iexportscript, iexportsource}); bpmExport.addItemListener(this); JButton bSave = (JButton)buttonPanel.addControl(ButtonPanel.Control.SAVE); bSave.addActionListener(this); tabbedPane = new JTabbedPane(); tabbedPane.addTab("Script source (decompiled)", decompiledPanel); tabbedPane.addTab("Script code", compiledPanel); panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.add(tabbedPane, BorderLayout.CENTER); panel.add(buttonPanel, BorderLayout.SOUTH); bDecompile.doClick(); bCompile.setEnabled(true); if (BrowserMenuBar.getInstance().autocheckBCS()) { bCompile.doClick(); codeChanged = false; } else { bpmErrors.setEnabled(false); bpmWarnings.setEnabled(false); } bDecompile.setEnabled(false); bSave.setEnabled(false); return panel; } // --------------------- End Interface Viewable --------------------- // --------------------- Begin Interface Writeable --------------------- @Override public void write(OutputStream os) throws IOException { if (codeText == null) { StreamUtils.writeString(os, text, text.length()); } else { StreamUtils.writeString(os, codeText.getText(), codeText.getText().length()); } } // --------------------- End Interface Writeable --------------------- public String getCode() { return text; } public void insertString(String s) { int pos = sourceText.getCaret().getDot(); sourceText.insert(s, pos); } }