/* * Copyright (C) 2008, 2012 IsmAvatar <IsmAvatar@gmail.com> * Copyright (C) 2007, 2008 Quadduc <quadduc@gmail.com> * Copyright (C) 2013-2014 Robert B. Colton * * This file is part of LateralGM. * LateralGM is free software and comes with ABSOLUTELY NO WARRANTY. * See LICENSE for details. */ package org.lateralgm.components; import java.awt.Color; import java.awt.Font; import java.awt.Frame; import java.awt.Graphics; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.print.PrinterException; import java.util.HashSet; import java.util.Map.Entry; import java.util.Set; import java.util.SortedSet; import java.util.Timer; import java.util.TimerTask; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.GroupLayout; import javax.swing.GroupLayout.Alignment; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JToolBar; import javax.swing.SwingUtilities; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import org.lateralgm.file.ProjectFile.ResourceHolder; import org.lateralgm.file.ResourceList; import org.lateralgm.joshedit.lexers.GMLKeywords; import org.lateralgm.joshedit.Code; import org.lateralgm.joshedit.CompletionMenu; import org.lateralgm.joshedit.DefaultKeywords; import org.lateralgm.joshedit.DefaultTokenMarker; import org.lateralgm.joshedit.CompletionMenu.Completion; import org.lateralgm.joshedit.DefaultTokenMarker.KeywordSet; import org.lateralgm.joshedit.DefaultKeywords.HasKeywords; import org.lateralgm.joshedit.JoshText; import org.lateralgm.joshedit.JoshText.CodeMetrics; import org.lateralgm.joshedit.JoshText.LineChangeListener; import org.lateralgm.joshedit.JoshText.Highlighter; import org.lateralgm.joshedit.Runner; import org.lateralgm.joshedit.Runner.EditorInterface; import org.lateralgm.joshedit.JoshTextPanel; import org.lateralgm.main.LGM; import org.lateralgm.main.Prefs; import org.lateralgm.main.UpdateSource.UpdateEvent; import org.lateralgm.main.UpdateSource.UpdateListener; import org.lateralgm.messages.Messages; import org.lateralgm.resources.Resource; import org.lateralgm.resources.Script; public class CodeTextArea extends JoshTextPanel implements UpdateListener,ActionListener { private static final long serialVersionUID = 1L; static { Runner.editorInterface = new EditorInterface() { public ImageIcon getIconForKey(String key) { return LGM.getIconForKey(key); } public String getString(String key) { return Messages.getString(key); } public String getString(String key, String def) { String str = getString(key); if (str.equals('!' + key + '!')) return def; return str; } }; } protected static Timer timer; protected Integer lastUpdateTaskID = 0; private Set<SortedSet<String>> resourceKeywords = new HashSet<SortedSet<String>>(); protected Completion[] completions; protected DefaultTokenMarker tokenMarker; private static final Color PURPLE = new Color(138,54,186); private static final Color BROWN = new Color(150,0,0); private static final Color FUNCTION = new Color(0,100,150); //new Color(255,0,128); static KeywordSet resNames, scrNames, constructs, functions, operators, constants, variables; public CodeTextArea() { this(null,MarkerCache.getMarker("gml")); } public CodeTextArea(String code) { this(code,MarkerCache.getMarker("gml")); } public CodeTextArea(String code, DefaultTokenMarker marker) { super(code); tokenMarker = marker; setTabSize(Prefs.tabSize); setTokenMarker(tokenMarker); setupKeywords(); updateKeywords(); updateResourceKeywords(); text.setFont(Prefs.codeFont); //painter.setStyles(PrefsStore.getSyntaxStyles()); text.getActionMap().put("COMPLETIONS",completionAction); LGM.currentFile.updateSource.addListener(this); // build popup menu final JPopupMenu popup = new JPopupMenu(); popup.add(makeContextButton(this.text.actCut)); popup.add(makeContextButton(this.text.actCopy)); popup.add(makeContextButton(this.text.actPaste)); popup.addSeparator(); final JMenuItem undoItem = makeContextButton(this.text.actUndo); popup.add(undoItem); final JMenuItem redoItem = makeContextButton(this.text.actRedo); popup.add(redoItem); popup.addSeparator(); popup.add(makeContextButton(this.text.actSelAll)); popup.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuCanceled(PopupMenuEvent arg0) { // TODO Auto-generated method stub } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent arg0) { // TODO Auto-generated method stub } @Override public void popupMenuWillBecomeVisible(PopupMenuEvent arg0) { undoItem.setEnabled(text.canUndo()); redoItem.setEnabled(text.canRedo()); } }); text.setComponentPopupMenu(popup); } private JButton makeToolbarButton(String name) { String key = "JoshText." + name; JButton b = new JButton(LGM.getIconForKey(key)); b.setToolTipText(Messages.getString(key)); b.setRequestFocusEnabled(false); b.setActionCommand(key); b.addActionListener(this); return b; } private static JMenuItem makeContextButton(Action a) { String key = "JoshText." + a.getValue(Action.NAME); JMenuItem b = new JMenuItem(); b.setIcon(LGM.getIconForKey(key)); b.setText(Messages.getString(key)); b.setRequestFocusEnabled(false); b.addActionListener(a); return b; } public void addEditorButtons(JToolBar tb) { tb.add(makeToolbarButton("LOAD")); tb.add(makeToolbarButton("SAVE")); tb.add(makeToolbarButton("PRINT")); tb.addSeparator(); tb.add(makeToolbarButton("CUT")); tb.add(makeToolbarButton("COPY")); tb.add(makeToolbarButton("PASTE")); tb.addSeparator(); final JButton undoButton = makeToolbarButton("UNDO"); tb.add(undoButton); final JButton redoButton = makeToolbarButton("REDO"); tb.add(redoButton); // need to set the default state unlike the component popup undoButton.setEnabled(text.canUndo()); redoButton.setEnabled(text.canRedo()); text.addLineChangeListener(new LineChangeListener() { @Override public void linesChanged(Code code, int start, int end) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { undoButton.setEnabled(text.canUndo()); redoButton.setEnabled(text.canRedo()); } }); } }); tb.addSeparator(); tb.add(makeToolbarButton("FIND")); tb.add(makeToolbarButton("GOTO")); } public void aGoto() { int line = showGotoDialog(getCaretLine()); line = Math.max(0,Math.min(getLineCount() - 1,line)); setCaretPosition(line,0); repaint(); } public static int showGotoDialog(int defVal) { final JDialog d = new JDialog((Frame) null,true); JPanel p = new JPanel(); GroupLayout layout = new GroupLayout(p); layout.setAutoCreateGaps(true); layout.setAutoCreateContainerGaps(true); p.setLayout(layout); JLabel l = new JLabel("Line: "); NumberField f = new NumberField(defVal); f.selectAll(); JButton b = new JButton("Goto"); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { d.setVisible(false); } }); layout.setHorizontalGroup(layout.createParallelGroup() /**/.addGroup(layout.createSequentialGroup() /* */.addComponent(l) /* */.addComponent(f)) /**/.addComponent(b,Alignment.CENTER)); layout.setVerticalGroup(layout.createSequentialGroup() /**/.addGroup(layout.createParallelGroup(Alignment.BASELINE) /* */.addComponent(l) /* */.addComponent(f)) /**/.addComponent(b)); d.setContentPane(p); d.pack(); d.setResizable(false); d.setLocationRelativeTo(null); d.setVisible(true); //blocks until user clicks OK return f.getIntValue(); } private void setupKeywords() { resNames = tokenMarker.addKeywordSet("Resource Names",PURPLE,Font.PLAIN); scrNames = tokenMarker.addKeywordSet("Script Names",FUNCTION,Font.PLAIN); functions = tokenMarker.addKeywordSet("Functions",FUNCTION,Font.PLAIN); constructs = tokenMarker.addKeywordSet("Constructs",Color.BLACK,Font.BOLD); operators = tokenMarker.addKeywordSet("Operators",Color.BLACK,Font.BOLD); constants = tokenMarker.addKeywordSet("Constants",BROWN,Font.PLAIN); variables = tokenMarker.addKeywordSet("Variables",Color.BLUE,Font.ITALIC); } //TODO: I believe this method can be removed. public static void updateKeywords() { constructs.words.clear(); operators.words.clear(); constants.words.clear(); variables.words.clear(); functions.words.clear(); for (DefaultKeywords.Construct keyword : GMLKeywords.CONSTRUCTS) constructs.words.add(keyword.getName()); for (DefaultKeywords.Operator keyword : GMLKeywords.OPERATORS) operators.words.add(keyword.getName()); for (DefaultKeywords.Constant keyword : GMLKeywords.CONSTANTS) constants.words.add(keyword.getName()); for (DefaultKeywords.Variable keyword : GMLKeywords.VARIABLES) variables.words.add(keyword.getName()); for (DefaultKeywords.Function keyword : GMLKeywords.FUNCTIONS) functions.words.add(keyword.getName()); } public static void updateResourceKeywords() { resNames.words.clear(); scrNames.words.clear(); for (Entry<Class<?>,ResourceHolder<?>> e : LGM.currentFile.resMap.entrySet()) { if (!(e.getValue() instanceof ResourceList<?>)) continue; ResourceList<?> rl = (ResourceList<?>) e.getValue(); KeywordSet ks = e.getKey() == Script.class ? scrNames : resNames; for (Resource<?,?> r : rl) ks.words.add(r.getName()); } } protected void updateCompletions(DefaultTokenMarker tokenMarker2) { int l = 0; for (Set<String> a : resourceKeywords) { l += a.size(); } DefaultKeywords.Keyword[][] keywords = null; if (tokenMarker2 instanceof HasKeywords) { HasKeywords hk = (HasKeywords) tokenMarker2; keywords = hk.getKeywords(); for (DefaultKeywords.Keyword[] a : keywords) l += a.length; } completions = new Completion[l]; int i = 0; for (Set<String> a : resourceKeywords) { for (String s : a) { completions[i] = new CompletionMenu.WordCompletion(s); i += 1; } } if (keywords == null) return; for (DefaultKeywords.Keyword[] a : keywords) for (DefaultKeywords.Keyword k : a) { if (k instanceof DefaultKeywords.Function) completions[i] = new FunctionCompletion((DefaultKeywords.Function) k); else if (k instanceof DefaultKeywords.Variable) completions[i] = new VariableCompletion((DefaultKeywords.Variable) k); else completions[i] = new CompletionMenu.WordCompletion(k.getName()); i++; } } public class VariableCompletion extends CompletionMenu.Completion { private final DefaultKeywords.Variable variable; public VariableCompletion(DefaultKeywords.Variable v) { variable = v; name = v.getName(); } public boolean apply(JoshText a, char input, int row, int start, int end) { String s = name; int p = s.length(); if (variable.arraySize > 0) { s += "[]"; boolean ci = true; switch (input) { case '\0': case '[': break; case ']': ci = false; break; default: s += String.valueOf(input); } if (ci) p = s.length() - 1; else p = s.length(); } if (!replace(a,row,start,end,s)) return false; setCaretPosition(row,start + p); return true; } public String toString() { String s = name; if (variable.arraySize > 0) s += "[0.." + String.valueOf(variable.arraySize - 1) + "]"; if (variable.readOnly) s += "*"; return s; } } public class FunctionCompletion extends CompletionMenu.Completion { private final DefaultKeywords.Function function; public FunctionCompletion(DefaultKeywords.Function f) { function = f; name = f.getName(); } public boolean apply(JoshText a, char input, int row, int start, int end) { String s = name + "(" + getArguments() + ")"; int p1, p2; boolean argSel = true; switch (input) { case '\0': case '(': break; case ')': argSel = false; break; default: s += String.valueOf(input); } if (argSel && function.arguments.length > 0) { p1 = name.length() + 1; p2 = p1 + getArgument(0).length(); } else { p1 = s.length(); p2 = p1; } if (!replace(a,row,start,end,s)) return false; setSelection(row,start + p1,row,start + p2); return true; } public String getArgument(int i) { if (i >= function.arguments.length) return null; return function.arguments[i] + (i == function.dynArgIndex ? "..." : ""); } public String getArguments() { String s = ""; for (int i = 0; i < function.arguments.length; i++) s += (i > 0 ? "," : "") + getArgument(i); return s; } public String toString() { return name + "(" + getArguments() + ")"; } } private static String find(String input, Pattern p) { Matcher m = p.matcher(input); if (m.find()) return m.group(); return new String(); } AbstractAction completionAction = new AbstractAction("COMPLETE") { private static final long serialVersionUID = 1L; final Pattern W_BEFORE = Pattern.compile("\\w+$"); final Pattern W_AFTER = Pattern.compile("^\\w+"); public void actionPerformed(ActionEvent e) { int pos = getCaretColumn(); int row = getCaretLine(); String lt = getLineText(row); int x1 = pos - find(lt.substring(0,pos),W_BEFORE).length(); int x2 = pos + find(lt.substring(pos),W_AFTER).length(); if (completions == null) updateCompletions(tokenMarker); new CompletionMenu(LGM.frame,text,row,x1,x2,pos,completions); } }; public void updated(UpdateEvent e) { if (timer == null) timer = new Timer(); timer.schedule(new UpdateTask(),500); } private class UpdateTask extends TimerTask { private int id; public UpdateTask() { synchronized (lastUpdateTaskID) { id = ++lastUpdateTaskID; } } public void run() { synchronized (lastUpdateTaskID) { if (id != lastUpdateTaskID) return; } SwingUtilities.invokeLater(new Runnable() { public void run() { updateResourceKeywords(); text.repaint(); //should be capable of figuring out its own visible lines //int fl = getFirstLine(); //painter.invalidateLineRange(fl,fl + getVisibleLines()); } }); } } public boolean requestFocusInWindow() { return text.requestFocusInWindow(); } public void markError(final int line, final int pos, int abs) { final Highlighter err = new ErrorHighlighter(line,pos); text.highlighters.add(err); text.addLineChangeListener(new LineChangeListener() { public void linesChanged(Code code, int start, int end) { text.highlighters.remove(err); text.removeLineChangeListener(this); } }); text.repaint(); } class ErrorHighlighter implements Highlighter { protected final Color COL_SQ = Color.RED; protected final Color COL_HL = new Color(255,240,230); protected int line, pos, x2; public ErrorHighlighter(int line, int pos) { this.line = line; this.pos = pos; String code = getLineText(line); int otype = JoshText.selGetKind(code,pos); x2 = pos; do x2++; while (JoshText.selOfKind(code,x2,otype)); } public void paint(Graphics g, Insets i, CodeMetrics cm, int line_start, int line_end) { int gh = cm.lineHeight(); g.setColor(COL_HL); g.fillRect(0,i.top + line * gh,g.getClipBounds().width,gh); g.setColor(COL_SQ); int y = i.top + line * gh + gh; int start = i.left + cm.lineWidth(line,pos); int end = i.left + cm.lineWidth(line,x2); for (int x = start; x < end; x += 2) { g.drawLine(x,y,x + 1,y - 1); g.drawLine(x + 1,y - 1,x + 2,y); } } } public void setTokenMarker(DefaultTokenMarker tokenMarker2) { tokenMarker = tokenMarker2; super.setTokenMarker(tokenMarker2); this.updateCompletions(tokenMarker2); } public void actionPerformed(ActionEvent ev) { String com = ev.getActionCommand(); if (com.equals("JoshText.LOAD")) { text.Load(); } else if (com.equals("JoshText.SAVE")) { text.Save(); } else if (com.equals("JoshText.PRINT")) { try { this.Print(); } catch (PrinterException e) { LGM.showDefaultExceptionHandler(e); } } else if (com.equals("JoshText.UNDO")) { text.Undo(); } else if (com.equals("JoshText.REDO")) { text.Redo(); } else if (com.equals("JoshText.CUT")) { text.Cut(); } else if (com.equals("JoshText.COPY")) { text.Copy(); } else if (com.equals("JoshText.PASTE")) { text.Paste(); } else if (com.equals("JoshText.FIND")) { text.ShowFind(); } else if (com.equals("JoshText.GOTO")) { this.aGoto(); } else if (com.equals("JoshText.SELALL")) { text.SelectAll(); } } }