/*
* 02/21/2005
*
* CodeTemplateManager.java - manages code templates.
* Copyright (C) 2005 Robert Futrell
* robert_futrell at users.sourceforge.net
* http://fifesoft.com/rsyntaxtextarea
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
*/
package org.fife.ui.rsyntaxtextarea;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import javax.swing.KeyStroke;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Segment;
import org.fife.ui.rsyntaxtextarea.templates.CodeTemplate;
/**
* Manages "code templates."
* <p>
*
* All methods in this class are synchronized for thread safety, but as a best practice, you should probably only modify
* the templates known to a <code>CodeTemplateManager</code> on the EDT. Modifying a <code>CodeTemplate</code> retrieved
* from a <code>CodeTemplateManager</code> while <em>not</em> on the EDT could cause problems.
*
* @author Robert Futrell
* @version 0.1
*/
public class CodeTemplateManager {
private int maxTemplateIDLength;
private List templates;
private KeyStroke insertTrigger;
private String insertTriggerString;
private Segment s;
private TemplateComparator comparator;
private File directory;
private static final int mask = InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK;
static final KeyStroke TEMPLATE_KEYSTROKE = KeyStroke.
getKeyStroke(KeyEvent.VK_SPACE, mask);
/**
* Constructor.
*/
public CodeTemplateManager() {
// Default insert trigger is a space.
// FIXME: See notes in RSyntaxTextAreaDefaultInputMap.
setInsertTrigger(TEMPLATE_KEYSTROKE);
s = new Segment();
comparator = new TemplateComparator();
templates = new ArrayList();
}
/**
* Registers the specified template with this template manager.
*
* @param template
* The template to register.
* @throws IllegalArgumentException
* If <code>template</code> is <code>null</code>.
* @see #removeTemplate(CodeTemplate)
* @see #removeTemplate(String)
*/
public synchronized void addTemplate(CodeTemplate template) {
if (template == null) {
throw new IllegalArgumentException("template cannot be null");
}
templates.add(template);
sortTemplates();
}
/**
* Returns the keystroke that is the "insert trigger" for templates; that is, the character that, when inserted into
* an instance of <code>RSyntaxTextArea</code>, triggers the search for a template matching the token ending at the
* caret position.
*
* @return The insert trigger.
* @see #getInsertTriggerString()
* @see #setInsertTrigger(KeyStroke)
*/
/*
* FIXME: This text IS what's inserted if the trigger character is pressed in a text area but no template matches,
* but it is NOT the trigger character used in the text areas. This is because space (" ") is hard-coded into
* RSyntaxTextAreaDefaultInputMap.java. We need to make this dynamic somehow. See
* RSyntaxTextAreaDefaultInputMap.java.
*/
public KeyStroke getInsertTrigger() {
return insertTrigger;
}
/**
* Returns the "insert trigger" for templates; that is, the character that, when inserted into an instance of
* <code>RSyntaxTextArea</code>, triggers the search for a template matching the token ending at the caret position.
*
* @return The insert trigger character.
* @see #getInsertTrigger()
* @see #setInsertTrigger(KeyStroke)
*/
/*
* FIXME: This text IS what's inserted if the trigger character is pressed in a text area but no template matches,
* but it is NOT the trigger character used in the text areas. This is because space (" ") is hard-coded into
* RSyntaxTextAreaDefaultInputMap.java. We need to make this dynamic somehow. See
* RSyntaxTextAreaDefaultInputMap.java.
*/
public String getInsertTriggerString() {
return insertTriggerString;
}
/**
* Returns the template that should be inserted at the current caret position, assuming the trigger character was
* pressed.
*
* @param textArea
* The text area that's getting text inserted into it.
* @return A template that should be inserted, if appropriate, or <code>null</code> if no template should be
* inserted.
*/
public synchronized CodeTemplate getTemplate(RSyntaxTextArea textArea) {
int caretPos = textArea.getCaretPosition();
int charsToGet = Math.min(caretPos, maxTemplateIDLength);
try {
Document doc = textArea.getDocument();
doc.getText(caretPos - charsToGet, charsToGet, s);
int index = Collections.binarySearch(templates, s, comparator);
return index >= 0 ? (CodeTemplate) templates.get(index) : null;
} catch (BadLocationException ble) {
ble.printStackTrace();
throw new InternalError("Error in CodeTemplateManager");
}
}
/**
* Returns the number of templates this manager knows about.
*
* @return The template count.
*/
public synchronized int getTemplateCount() {
return templates.size();
}
/**
* Returns the templates currently available.
*
* @return The templates available.
*/
public synchronized CodeTemplate[] getTemplates() {
CodeTemplate[] temp = new CodeTemplate[templates.size()];
return (CodeTemplate[]) templates.toArray(temp);
}
/**
* Returns whether the specified character is a valid character for a <code>CodeTemplate</code> id.
*
* @param ch
* The character to check.
* @return Whether the character is a valid template character.
*/
public static final boolean isValidChar(char ch) {
return RSyntaxUtilities.isLetterOrDigit(ch) || ch == '_';
}
/**
* Returns the specified code template.
*
* @param template
* The template to remove.
* @return <code>true</code> if the template was removed, <code>false</code> if the template was not in this
* template manager.
* @throws IllegalArgumentException
* If <code>template</code> is <code>null</code>.
* @see #removeTemplate(String)
* @see #addTemplate(CodeTemplate)
*/
public synchronized boolean removeTemplate(CodeTemplate template) {
if (template == null) {
throw new IllegalArgumentException("template cannot be null");
}
// TODO: Do a binary search
return templates.remove(template);
}
/**
* Returns the code template with the specified id.
*
* @param id
* The id to check for.
* @return The code template that was removed, or <code>null</code> if there was no template with the specified ID.
* @throws IllegalArgumentException
* If <code>id</code> is <code>null</code>.
* @see #removeTemplate(CodeTemplate)
* @see #addTemplate(CodeTemplate)
*/
public synchronized CodeTemplate removeTemplate(String id) {
if (id == null) {
throw new IllegalArgumentException("id cannot be null");
}
// TODO: Do a binary search
for (Iterator i = templates.iterator(); i.hasNext();) {
CodeTemplate template = (CodeTemplate) i.next();
if (id.equals(template.getID())) {
i.remove();
return template;
}
}
return null;
}
/**
* Replaces the current set of available templates with the ones specified.
*
* @param newTemplates
* The new set of templates. Note that we will be taking a shallow copy of these and sorting them.
*/
public synchronized void replaceTemplates(CodeTemplate[] newTemplates) {
templates.clear();
if (newTemplates != null) {
for (int i = 0; i < newTemplates.length; i++) {
templates.add(newTemplates[i]);
}
}
sortTemplates(); // Also recomputes maxTemplateIDLength.
}
/**
* Saves all templates as XML files in the current template directory.
*
* @return Whether or not the save was successful.
*/
public synchronized boolean saveTemplates() {
if (templates == null)
return true;
if (directory == null || !directory.isDirectory())
return false;
// Blow away all old XML files to start anew, as some might be from
// templates we're removed from the template manager.
File[] oldXMLFiles = directory.listFiles(new XMLFileFilter());
if (oldXMLFiles == null)
return false; // Either an IOException or it isn't a directory.
int count = oldXMLFiles.length;
for (int i = 0; i < count; i++) {
/* boolean deleted = */oldXMLFiles[i].delete();
}
// Save all current templates as XML.
boolean wasSuccessful = true;
for (Iterator i = templates.iterator(); i.hasNext();) {
CodeTemplate template = (CodeTemplate) i.next();
File xmlFile = new File(directory, template.getID() + ".xml");
try {
XMLEncoder e = new XMLEncoder(new BufferedOutputStream(
new FileOutputStream(xmlFile)));
e.writeObject(template);
e.close();
} catch (IOException ioe) {
ioe.printStackTrace();
wasSuccessful = false;
}
}
return wasSuccessful;
}
/**
* Sets the "trigger" character for templates.
*
* @param trigger
* The trigger character to set for templates. This means that when this character is pressed in an
* <code>RSyntaxTextArea</code>, the last-typed token is found, and is checked against all template ID's
* to see if a template should be inserted. If a template ID matches, that template is inserted; if not,
* the trigger character is inserted. If this parameter is <code>null</code>, no change is made to the
* trigger character.
* @see #getInsertTrigger()
* @see #getInsertTriggerString()
*/
/*
* FIXME: The trigger set here IS inserted when no matching template is found, but a space character (" ") is always
* used as the "trigger" to look for templates. This is because it is hard-coded in RSyntaxTextArea's input map this
* way. We need to change this. See RSyntaxTextAreaDefaultInputMap.java.
*/
public void setInsertTrigger(KeyStroke trigger) {
if (trigger != null) {
insertTrigger = trigger;
insertTriggerString = Character.toString(trigger.getKeyChar());
}
}
/**
* Sets the directory in which to look for templates. Calling this method adds any new templates found in the
* specified directory to the templates already registered.
*
* @param dir
* The new directory in which to look for templates.
* @return The new number of templates in this template manager, or <code>-1</code> if the specified directory does
* not exist.
*/
public synchronized int setTemplateDirectory(File dir) {
if (dir != null && dir.isDirectory()) {
this.directory = dir;
File[] files = dir.listFiles(new XMLFileFilter());
int newCount = files == null ? 0 : files.length;
int oldCount = templates.size();
List temp = new ArrayList(oldCount + newCount);
temp.addAll(templates);
for (int i = 0; i < newCount; i++) {
try {
XMLDecoder d = new XMLDecoder(new BufferedInputStream(
new FileInputStream(files[i])));
Object obj = d.readObject();
if (!(obj instanceof CodeTemplate)) {
throw new IOException("Not a CodeTemplate: " +
files[i].getAbsolutePath());
}
temp.add(obj);
d.close();
} catch (/* IO, NoSuchElement */Exception e) {
// NoSuchElementException can be thrown when reading
// an XML file not in the format expected by XMLDecoder.
// (e.g. CodeTemplates in an old format).
e.printStackTrace();
}
}
templates = temp;
sortTemplates();
return getTemplateCount();
}
return -1;
}
/**
* Removes any null entries in the current set of templates (if any), sorts the remaining templates, and computes
* the new maximum template ID length.
*/
private synchronized void sortTemplates() {
// Get the maximum length of a template ID.
maxTemplateIDLength = 0;
// Remove any null entries (should only happen because of
// IOExceptions, etc. when loading from files), and sort
// the remaining list.
for (Iterator i = templates.iterator(); i.hasNext();) {
CodeTemplate temp = (CodeTemplate) i.next();
if (temp == null || temp.getID() == null) {
i.remove();
}
else {
maxTemplateIDLength = Math.max(maxTemplateIDLength,
temp.getID().length());
}
}
Collections.sort(templates);
}
/**
* A comparator that takes a <code>CodeTemplate</code> as its first parameter and a <code>Segment</code> as its
* second, and knows to compare the template's ID to the segment's text.
*/
private static class TemplateComparator implements Comparator, Serializable {
public int compare(Object template, Object segment) {
// Get template start index (0) and length.
CodeTemplate t = (CodeTemplate) template;
final char[] templateArray = t.getID().toCharArray();
int i = 0;
int len1 = templateArray.length;
// Find "token" part of segment and get its offset and length.
Segment s = (Segment) segment;
char[] segArray = s.array;
int len2 = s.count;
int j = s.offset + len2 - 1;
while (j >= s.offset && isValidChar(segArray[j])) {
j--;
}
j++;
int segShift = j - s.offset;
len2 -= segShift;
int n = Math.min(len1, len2);
while (n-- != 0) {
char c1 = templateArray[i++];
char c2 = segArray[j++];
if (c1 != c2)
return c1 - c2;
}
return len1 - len2;
}
}
/**
* A file filter for File.listFiles() (NOT for JFileChoosers!) that accepts only XML files.
*/
private static class XMLFileFilter implements FileFilter {
public boolean accept(File f) {
return f.getName().toLowerCase().endsWith(".xml");
}
}
}