// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.gui; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.Stroke; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Set; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import org.fife.ui.rsyntaxtextarea.Token; import org.fife.ui.rsyntaxtextarea.folding.Fold; import org.infinity.NearInfinity; import org.infinity.resource.Resource; import org.infinity.resource.ResourceFactory; import org.infinity.resource.bcs.Compiler; import org.infinity.resource.bcs.Decompiler; import org.infinity.resource.key.ResourceEntry; import org.infinity.resource.text.modes.BCSTokenMaker; import org.infinity.util.IdsMapCache; import org.infinity.util.IdsMapEntry; public class ScriptTextArea extends InfinityTextArea { private ScriptPopupMenu menu = new ScriptPopupMenu(); public ScriptTextArea() { super(true); Language lang; if (BrowserMenuBar.getInstance() != null && BrowserMenuBar.getInstance().getBcsSyntaxHighlightingEnabled()) { lang = Language.BCS; } else { lang = Language.NONE; } applyExtendedSettings(lang, null); addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent ev) { handlePopup(ev); } @Override public void mouseReleased(MouseEvent ev) { handlePopup(ev); } }); } @Override public void setText(String text) { // prevent undo to remove the text super.setText(text); discardAllEdits(); } // try to paint an indicator below "crosslinks" @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Rectangle rect = g.getClipBounds(); // try to get the "lines" which need to be painted int upperLine = 0; int lowerLine = 0; try { upperLine = getLineOfOffset(viewToModel(new Point(rect.x, rect.y))); lowerLine = getLineOfOffset(viewToModel(new Point(rect.x + rect.width, rect.y + rect.height))); } catch (BadLocationException e) { } //System.err.println("would consider drawing from lines " + upperLine + " to " + lowerLine); for (int line = upperLine; line <= lowerLine; line++) { try { int start = getLineStartOffset(line); int end = getLineEndOffset(line) - 1; // newline int[][] linkOffsets = findLinksInSection(start, end); if (linkOffsets.length == 0) { continue; } Graphics2D g2d = (Graphics2D) g; // clear that line before doing anything Color oldColor = g2d.getColor(); g2d.setColor(getBackground()); Rectangle rectStart = modelToView(start); Rectangle rectEnd = modelToView(end); g2d.drawLine(rectStart.x, rectStart.y + rectStart.height, rectEnd.x, rectEnd.y + rectEnd.height); g2d.setColor(oldColor); Stroke oldStroke = g2d.getStroke(); g2d.setStroke(new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1, new float[] { 1, 2 }, 0)); // now underline the crosslinks for (int[] pair : linkOffsets) { // convert into view coordinates rectStart = modelToView(pair[0]); rectEnd = modelToView(pair[1]); g2d.drawLine(rectStart.x, rectStart.y + rectStart.height - 1, rectEnd.x, rectEnd.y + rectEnd.height - 1); } g2d.setStroke(oldStroke); } catch (BadLocationException e) { } } } // looks for "crosslinks" in script lines private int[][] findLinksInSection(int start, int end) throws BadLocationException { ArrayList<int[]> links = new ArrayList<int[]>(); int startLine = getLineOfOffset(start); int endLine = getLineOfOffset(end); for (int i = startLine; i <= endLine; i++) { // skipping folded lines boolean folded = false; for (int j = 0; j < getFoldManager().getFoldCount(); j++) { Fold fold = getFoldManager().getFold(j); if (fold.isCollapsed() && i >= fold.getStartLine() && i <= fold.getEndLine()) { folded = true; break; } } if (folded) { continue; } // looking for crosslinks String lineText = getText().substring(getLineStartOffset(i), getLineEndOffset(i)); Token token = getTokenListForLine(i); while (token != null && token.getType() != Token.NULL) { if (token.getOffset() >= start && token.getOffset() + token.length() <= end) { if (token.getType() == BCSTokenMaker.TOKEN_STRING && token.length() > 2) { int ofsTokenFromLineStart = token.getOffset() - getLineStartOffset(i); String text = token.getLexeme().substring(1, token.length() - 1); if (findResEntry(lineText, ofsTokenFromLineStart + 1, text) != null) { // add it to our list of crosslinks links.add(new int[]{start + ofsTokenFromLineStart + 1, start + ofsTokenFromLineStart + token.length() - 1}); } } else if (token.getType() == BCSTokenMaker.TOKEN_SYMBOL_SPELL) { int ofsTokenFromLineStart = token.getOffset() - getLineStartOffset(i); String text = token.getLexeme(); if (findResEntry(lineText, ofsTokenFromLineStart + 1, text) != null) { // add it to our list of crosslinks links.add(new int[]{start + ofsTokenFromLineStart, start + ofsTokenFromLineStart + token.length()}); } } } token = token.getNextToken(); } } return links.toArray(new int[0][0]); } private void handlePopup(MouseEvent ev) { if (ev.isPopupTrigger()) { try { // get "word" under click Document doc = getDocument(); int offset = viewToModel(ev.getPoint()); final int lineNr = getLineOfOffset(offset); final int lineStart = getLineStartOffset(lineNr); String line = doc.getText(lineStart, getLineEndOffset(lineNr) - lineStart); offset = offset - lineStart; String token = getToken(line, offset, "\"", "(), "); // quoted if (token == null) { // fall back to ids token parsing token = getToken(line, offset, ",()", "[].\" "); // IDS if (token == null) { return; } } ResourceEntry resEntry = findResEntry(line, offset, token); menu.setResEntry(resEntry); if (resEntry != null) { menu.show(this, ev.getX(), ev.getY()); } } catch (BadLocationException ble) {} } } private String getToken(String line, int offset, String delims, String invalidChars) { int tokenStart, tokenEnd; for (tokenStart = offset; tokenStart > 0; tokenStart--) { char current = line.charAt(tokenStart); if (delims.indexOf(current) != -1) { tokenStart++; break; } else if (invalidChars.indexOf(current) != -1) { return null; } } for (tokenEnd = offset + 1; tokenEnd < line.length(); tokenEnd++) { char current = line.charAt(tokenEnd); if (delims.indexOf(current) != -1) { break; } else if (invalidChars.indexOf(current) != -1) { return null; } } return line.substring(tokenStart, tokenEnd); } private ResourceEntry findResEntry(String line, int offset, String token) { // determine function name and param position int parenLevel = 0; int paramPos = 0; int idx; for (idx = offset; idx > 0; idx--) { char current = line.charAt(idx); if (current == ')') { parenLevel++; } else if ((current == ',') && (parenLevel == 0)) { paramPos++; } else if (current == '(') { if (parenLevel == 0) { // found end of corresponding function name break; } parenLevel--; } } int endPos = idx; while ((idx > 0) && (Character.isLetter(line.charAt(idx - 1)))) { idx--; } String function = line.substring(idx, endPos); // lookup function name in trigger.ids / action.ids String[] idsFiles = new String[] { "trigger.ids", "action.ids" }; for (final String idsFile : idsFiles) { IdsMapEntry idsEntry = IdsMapCache.get(idsFile).lookup(function + "("); if (idsEntry != null) { String[] paramDefs = idsEntry.getParameters().split(","); String definition; if (paramPos >= 0 && paramPos < paramDefs.length) { definition = paramDefs[paramPos]; } else { definition = ""; } // check script names (death var) if (definition.equalsIgnoreCase("O:Object*") || definition.equalsIgnoreCase("O:Target*") || definition.equalsIgnoreCase("O:Actor*") || (definition.equalsIgnoreCase("S:Name*") && (function.equalsIgnoreCase("Dead") || function.equalsIgnoreCase("Name") || function.equalsIgnoreCase("NumDead") || function.equalsIgnoreCase("NumDeadGT") || function.equalsIgnoreCase("NumDeadLT")))) { Compiler bcscomp = new Compiler(); if (bcscomp.hasScriptName(token)) { Set<ResourceEntry> entries = bcscomp.getResForScriptName(token); for (ResourceEntry entry : entries) { // for now, just return the first entry return entry; } } else { return null; } } // spell.ids if (definition.equalsIgnoreCase("I:Spell*Spell")) { // retrieving spell resource specified by symbolic spell name String resName = org.infinity.resource.spl.Viewer.getResourceName(token, true); if (resName != null && !resName.isEmpty() && ResourceFactory.resourceExists(resName, true)) { return ResourceFactory.getResourceEntry(resName, true); } else { return null; } } // guessing String[] possibleExtensions = guessExtension(function, definition); for (final String ext : possibleExtensions) { if (ResourceFactory.resourceExists(token + ext, true)) { return ResourceFactory.getResourceEntry(token + ext, true); } } break; } } return null; } // most parts stolen from Compiler.java private String[] guessExtension(String function, String definition) { definition = definition.trim(); // first the unique values if (definition.equalsIgnoreCase("S:Area*") || definition.equalsIgnoreCase("S:Area1*") || definition.equalsIgnoreCase("S:Area2*") || definition.equalsIgnoreCase("S:ToArea*") || definition.equalsIgnoreCase("S:Areaname*") || definition.equalsIgnoreCase("S:FromArea*")) { return new String[] { ".ARE" }; } else if (definition.equalsIgnoreCase("S:BamResRef*")) { return new String[] { ".BAM" }; } else if (definition.equals("S:CutScene*") || definition.equalsIgnoreCase("S:ScriptFile*") || definition.equalsIgnoreCase("S:Script*")) { return new String[] { ".BCS" }; } else if (definition.equalsIgnoreCase("S:Palette*")) { return new String[] { ".BMP" }; } else if (definition.equalsIgnoreCase("S:Item*") || definition.equalsIgnoreCase("S:Take*") || definition.equalsIgnoreCase("S:Give*") || definition.equalsIgnoreCase("S:Item") || definition.equalsIgnoreCase("S:OldObject*")) { return new String[] { ".ITM" }; } else if (definition.equalsIgnoreCase("S:Parchment*")) { return new String[] { ".MOS" }; } else if (definition.equalsIgnoreCase("S:Spell*") || definition.equalsIgnoreCase("S:Res*")) { return new String[] { ".SPL" }; } else if (definition.equalsIgnoreCase("S:Pool*")) { return new String[] { ".SRC" }; } else if (definition.startsWith("S:Store*")) { return new String[] { ".STO" }; } else if (definition.equalsIgnoreCase("S:Sound*") || definition.equalsIgnoreCase("S:Voice*")) { return new String[] { ".WAV" }; } else if (definition.equalsIgnoreCase("S:TextList*")) { return new String[] { ".2DA" }; } // and now the ambiguous else if (definition.equalsIgnoreCase("S:Effect*")) { return new String[] { ".VEF", ".VVC", ".BAM" }; } else if (definition.equalsIgnoreCase("S:DialogFile*")) { return new String[] { ".DLG", ".VEF", ".VVC", ".BAM" }; } else if (definition.equalsIgnoreCase("S:Object*")) { return Decompiler.getResRefType(function); } else if (definition.equalsIgnoreCase("S:NewObject*")) { return Decompiler.getResRefType(function); } else if (definition.equalsIgnoreCase("S:ResRef*")) { return Decompiler.getResRefType(function); } return new String[] {}; } //-------------------------- INNER CLASSES -------------------------- private class ScriptPopupMenu extends JPopupMenu implements ActionListener { private ResourceEntry resourceEntry = null; private final JMenuItem mi_open = new JMenuItem("Open"); private final JMenuItem mi_opennew = new JMenuItem("Open in new window"); ScriptPopupMenu() { add(mi_open); add(mi_opennew); mi_open.addActionListener(this); mi_opennew.addActionListener(this); } public void setResEntry(ResourceEntry resEntry) { this.resourceEntry = resEntry; } @Override public void actionPerformed(ActionEvent ev) { if (ev.getSource() == mi_open) { NearInfinity.getInstance().showResourceEntry(resourceEntry); } else if (ev.getSource() == mi_opennew) { Resource res = ResourceFactory.getResource(resourceEntry); new ViewFrame(NearInfinity.getInstance(), res); } } } }