1 | /*
|
---|
2 | * 02/21/2005
|
---|
3 | *
|
---|
4 | * CodeTemplateManager.java - manages code templates.
|
---|
5 | *
|
---|
6 | * This library is distributed under a modified BSD license. See the included
|
---|
7 | * RSyntaxTextArea.License.txt file for details.
|
---|
8 | */
|
---|
9 | package org.fife.ui.rsyntaxtextarea;
|
---|
10 |
|
---|
11 | import java.awt.event.InputEvent;
|
---|
12 | import java.awt.event.KeyEvent;
|
---|
13 | import java.beans.XMLDecoder;
|
---|
14 | import java.beans.XMLEncoder;
|
---|
15 | import java.io.BufferedInputStream;
|
---|
16 | import java.io.BufferedOutputStream;
|
---|
17 | import java.io.File;
|
---|
18 | import java.io.FileFilter;
|
---|
19 | import java.io.FileInputStream;
|
---|
20 | import java.io.FileOutputStream;
|
---|
21 | import java.io.IOException;
|
---|
22 | import java.io.Serializable;
|
---|
23 | import java.util.*;
|
---|
24 | import javax.swing.KeyStroke;
|
---|
25 | import javax.swing.text.BadLocationException;
|
---|
26 | import javax.swing.text.Document;
|
---|
27 | import javax.swing.text.Segment;
|
---|
28 |
|
---|
29 | import org.fife.ui.rsyntaxtextarea.templates.CodeTemplate;
|
---|
30 |
|
---|
31 |
|
---|
32 | /**
|
---|
33 | * Manages "code templates."<p>
|
---|
34 | *
|
---|
35 | * All methods in this class are synchronized for thread safety, but as a
|
---|
36 | * best practice, you should probably only modify the templates known to a
|
---|
37 | * <code>CodeTemplateManager</code> on the EDT. Modifying a
|
---|
38 | * <code>CodeTemplate</code> retrieved from a <code>CodeTemplateManager</code>
|
---|
39 | * while <em>not</em> on the EDT could cause problems.
|
---|
40 | *
|
---|
41 | * @author Robert Futrell
|
---|
42 | * @version 0.1
|
---|
43 | */
|
---|
44 | public class CodeTemplateManager {
|
---|
45 |
|
---|
46 | private int maxTemplateIDLength;
|
---|
47 | private List templates;
|
---|
48 |
|
---|
49 | private KeyStroke insertTrigger;
|
---|
50 | private String insertTriggerString;
|
---|
51 | private Segment s;
|
---|
52 | private TemplateComparator comparator;
|
---|
53 | private File directory;
|
---|
54 |
|
---|
55 | private static final int mask = InputEvent.CTRL_MASK|InputEvent.SHIFT_MASK;
|
---|
56 | static final KeyStroke TEMPLATE_KEYSTROKE = KeyStroke.
|
---|
57 | getKeyStroke(KeyEvent.VK_SPACE, mask);
|
---|
58 |
|
---|
59 |
|
---|
60 | /**
|
---|
61 | * Constructor.
|
---|
62 | */
|
---|
63 | public CodeTemplateManager() {
|
---|
64 |
|
---|
65 | // Default insert trigger is a space.
|
---|
66 | // FIXME: See notes in RSyntaxTextAreaDefaultInputMap.
|
---|
67 | setInsertTrigger(TEMPLATE_KEYSTROKE);
|
---|
68 |
|
---|
69 | s = new Segment();
|
---|
70 | comparator = new TemplateComparator();
|
---|
71 | templates = new ArrayList();
|
---|
72 |
|
---|
73 | }
|
---|
74 |
|
---|
75 |
|
---|
76 | /**
|
---|
77 | * Registers the specified template with this template manager.
|
---|
78 | *
|
---|
79 | * @param template The template to register.
|
---|
80 | * @throws IllegalArgumentException If <code>template</code> is
|
---|
81 | * <code>null</code>.
|
---|
82 | * @see #removeTemplate(CodeTemplate)
|
---|
83 | * @see #removeTemplate(String)
|
---|
84 | */
|
---|
85 | public synchronized void addTemplate(CodeTemplate template) {
|
---|
86 | if (template==null) {
|
---|
87 | throw new IllegalArgumentException("template cannot be null");
|
---|
88 | }
|
---|
89 | templates.add(template);
|
---|
90 | sortTemplates();
|
---|
91 | }
|
---|
92 |
|
---|
93 |
|
---|
94 | /**
|
---|
95 | * Returns the keystroke that is the "insert trigger" for templates;
|
---|
96 | * that is, the character that, when inserted into an instance of
|
---|
97 | * <code>RSyntaxTextArea</code>, triggers the search for
|
---|
98 | * a template matching the token ending at the caret position.
|
---|
99 | *
|
---|
100 | * @return The insert trigger.
|
---|
101 | * @see #getInsertTriggerString()
|
---|
102 | * @see #setInsertTrigger(KeyStroke)
|
---|
103 | */
|
---|
104 | /*
|
---|
105 | * FIXME: This text IS what's inserted if the trigger character is pressed
|
---|
106 | * in a text area but no template matches, but it is NOT the trigger
|
---|
107 | * character used in the text areas. This is because space (" ") is
|
---|
108 | * hard-coded into RSyntaxTextAreaDefaultInputMap.java. We need to make
|
---|
109 | * this dynamic somehow. See RSyntaxTextAreaDefaultInputMap.java.
|
---|
110 | */
|
---|
111 | public KeyStroke getInsertTrigger() {
|
---|
112 | return insertTrigger;
|
---|
113 | }
|
---|
114 |
|
---|
115 |
|
---|
116 | /**
|
---|
117 | * Returns the "insert trigger" for templates; that is, the character
|
---|
118 | * that, when inserted into an instance of <code>RSyntaxTextArea</code>,
|
---|
119 | * triggers the search for a template matching the token ending at the
|
---|
120 | * caret position.
|
---|
121 | *
|
---|
122 | * @return The insert trigger character.
|
---|
123 | * @see #getInsertTrigger()
|
---|
124 | * @see #setInsertTrigger(KeyStroke)
|
---|
125 | */
|
---|
126 | /*
|
---|
127 | * FIXME: This text IS what's inserted if the trigger character is pressed
|
---|
128 | * in a text area but no template matches, but it is NOT the trigger
|
---|
129 | * character used in the text areas. This is because space (" ") is
|
---|
130 | * hard-coded into RSyntaxTextAreaDefaultInputMap.java. We need to make
|
---|
131 | * this dynamic somehow. See RSyntaxTextAreaDefaultInputMap.java.
|
---|
132 | */
|
---|
133 | public String getInsertTriggerString() {
|
---|
134 | return insertTriggerString;
|
---|
135 | }
|
---|
136 |
|
---|
137 |
|
---|
138 | /**
|
---|
139 | * Returns the template that should be inserted at the current caret
|
---|
140 | * position, assuming the trigger character was pressed.
|
---|
141 | *
|
---|
142 | * @param textArea The text area that's getting text inserted into it.
|
---|
143 | * @return A template that should be inserted, if appropriate, or
|
---|
144 | * <code>null</code> if no template should be inserted.
|
---|
145 | */
|
---|
146 | public synchronized CodeTemplate getTemplate(RSyntaxTextArea textArea) {
|
---|
147 | int caretPos = textArea.getCaretPosition();
|
---|
148 | int charsToGet = Math.min(caretPos, maxTemplateIDLength);
|
---|
149 | try {
|
---|
150 | Document doc = textArea.getDocument();
|
---|
151 | doc.getText(caretPos-charsToGet, charsToGet, s);
|
---|
152 | int index = Collections.binarySearch(templates, s, comparator);
|
---|
153 | return index>=0 ? (CodeTemplate)templates.get(index) : null;
|
---|
154 | } catch (BadLocationException ble) {
|
---|
155 | ble.printStackTrace();
|
---|
156 | throw new InternalError("Error in CodeTemplateManager");
|
---|
157 | }
|
---|
158 | }
|
---|
159 |
|
---|
160 |
|
---|
161 | /**
|
---|
162 | * Returns the number of templates this manager knows about.
|
---|
163 | *
|
---|
164 | * @return The template count.
|
---|
165 | */
|
---|
166 | public synchronized int getTemplateCount() {
|
---|
167 | return templates.size();
|
---|
168 | }
|
---|
169 |
|
---|
170 |
|
---|
171 | /**
|
---|
172 | * Returns the templates currently available.
|
---|
173 | *
|
---|
174 | * @return The templates available.
|
---|
175 | */
|
---|
176 | public synchronized CodeTemplate[] getTemplates() {
|
---|
177 | CodeTemplate[] temp = new CodeTemplate[templates.size()];
|
---|
178 | return (CodeTemplate[])templates.toArray(temp);
|
---|
179 | }
|
---|
180 |
|
---|
181 |
|
---|
182 | /**
|
---|
183 | * Returns whether the specified character is a valid character for a
|
---|
184 | * <code>CodeTemplate</code> id.
|
---|
185 | *
|
---|
186 | * @param ch The character to check.
|
---|
187 | * @return Whether the character is a valid template character.
|
---|
188 | */
|
---|
189 | public static final boolean isValidChar(char ch) {
|
---|
190 | return RSyntaxUtilities.isLetterOrDigit(ch) || ch=='_';
|
---|
191 | }
|
---|
192 |
|
---|
193 |
|
---|
194 | /**
|
---|
195 | * Returns the specified code template.
|
---|
196 | *
|
---|
197 | * @param template The template to remove.
|
---|
198 | * @return <code>true</code> if the template was removed, <code>false</code>
|
---|
199 | * if the template was not in this template manager.
|
---|
200 | * @throws IllegalArgumentException If <code>template</code> is
|
---|
201 | * <code>null</code>.
|
---|
202 | * @see #removeTemplate(String)
|
---|
203 | * @see #addTemplate(CodeTemplate)
|
---|
204 | */
|
---|
205 | public synchronized boolean removeTemplate(CodeTemplate template) {
|
---|
206 |
|
---|
207 | if (template==null) {
|
---|
208 | throw new IllegalArgumentException("template cannot be null");
|
---|
209 | }
|
---|
210 |
|
---|
211 | // TODO: Do a binary search
|
---|
212 | return templates.remove(template);
|
---|
213 |
|
---|
214 | }
|
---|
215 |
|
---|
216 |
|
---|
217 | /**
|
---|
218 | * Returns the code template with the specified id.
|
---|
219 | *
|
---|
220 | * @param id The id to check for.
|
---|
221 | * @return The code template that was removed, or <code>null</code> if
|
---|
222 | * there was no template with the specified ID.
|
---|
223 | * @throws IllegalArgumentException If <code>id</code> is <code>null</code>.
|
---|
224 | * @see #removeTemplate(CodeTemplate)
|
---|
225 | * @see #addTemplate(CodeTemplate)
|
---|
226 | */
|
---|
227 | public synchronized CodeTemplate removeTemplate(String id) {
|
---|
228 |
|
---|
229 | if (id==null) {
|
---|
230 | throw new IllegalArgumentException("id cannot be null");
|
---|
231 | }
|
---|
232 |
|
---|
233 | // TODO: Do a binary search
|
---|
234 | for (Iterator i=templates.iterator(); i.hasNext(); ) {
|
---|
235 | CodeTemplate template = (CodeTemplate)i.next();
|
---|
236 | if (id.equals(template.getID())) {
|
---|
237 | i.remove();
|
---|
238 | return template;
|
---|
239 | }
|
---|
240 | }
|
---|
241 |
|
---|
242 | return null;
|
---|
243 |
|
---|
244 | }
|
---|
245 |
|
---|
246 |
|
---|
247 | /**
|
---|
248 | * Replaces the current set of available templates with the ones
|
---|
249 | * specified.
|
---|
250 | *
|
---|
251 | * @param newTemplates The new set of templates. Note that we will
|
---|
252 | * be taking a shallow copy of these and sorting them.
|
---|
253 | */
|
---|
254 | public synchronized void replaceTemplates(CodeTemplate[] newTemplates) {
|
---|
255 | templates.clear();
|
---|
256 | if (newTemplates!=null) {
|
---|
257 | for (int i=0; i<newTemplates.length; i++) {
|
---|
258 | templates.add(newTemplates[i]);
|
---|
259 | }
|
---|
260 | }
|
---|
261 | sortTemplates(); // Also recomputes maxTemplateIDLength.
|
---|
262 | }
|
---|
263 |
|
---|
264 |
|
---|
265 | /**
|
---|
266 | * Saves all templates as XML files in the current template directory.
|
---|
267 | *
|
---|
268 | * @return Whether or not the save was successful.
|
---|
269 | */
|
---|
270 | public synchronized boolean saveTemplates() {
|
---|
271 |
|
---|
272 | if (templates==null)
|
---|
273 | return true;
|
---|
274 | if (directory==null || !directory.isDirectory())
|
---|
275 | return false;
|
---|
276 |
|
---|
277 | // Blow away all old XML files to start anew, as some might be from
|
---|
278 | // templates we're removed from the template manager.
|
---|
279 | File[] oldXMLFiles = directory.listFiles(new XMLFileFilter());
|
---|
280 | if (oldXMLFiles==null)
|
---|
281 | return false; // Either an IOException or it isn't a directory.
|
---|
282 | int count = oldXMLFiles.length;
|
---|
283 | for (int i=0; i<count; i++) {
|
---|
284 | /*boolean deleted = */oldXMLFiles[i].delete();
|
---|
285 | }
|
---|
286 |
|
---|
287 | // Save all current templates as XML.
|
---|
288 | boolean wasSuccessful = true;
|
---|
289 | for (Iterator i=templates.iterator(); i.hasNext(); ) {
|
---|
290 | CodeTemplate template = (CodeTemplate)i.next();
|
---|
291 | File xmlFile = new File(directory, template.getID() + ".xml");
|
---|
292 | try {
|
---|
293 | XMLEncoder e = new XMLEncoder(new BufferedOutputStream(
|
---|
294 | new FileOutputStream(xmlFile)));
|
---|
295 | e.writeObject(template);
|
---|
296 | e.close();
|
---|
297 | } catch (IOException ioe) {
|
---|
298 | ioe.printStackTrace();
|
---|
299 | wasSuccessful = false;
|
---|
300 | }
|
---|
301 | }
|
---|
302 |
|
---|
303 | return wasSuccessful;
|
---|
304 |
|
---|
305 | }
|
---|
306 |
|
---|
307 |
|
---|
308 | /**
|
---|
309 | * Sets the "trigger" character for templates.
|
---|
310 | *
|
---|
311 | * @param trigger The trigger character to set for templates. This means
|
---|
312 | * that when this character is pressed in an
|
---|
313 | * <code>RSyntaxTextArea</code>, the last-typed token is found,
|
---|
314 | * and is checked against all template ID's to see if a template
|
---|
315 | * should be inserted. If a template ID matches, that template is
|
---|
316 | * inserted; if not, the trigger character is inserted. If this
|
---|
317 | * parameter is <code>null</code>, no change is made to the trigger
|
---|
318 | * character.
|
---|
319 | * @see #getInsertTrigger()
|
---|
320 | * @see #getInsertTriggerString()
|
---|
321 | */
|
---|
322 | /*
|
---|
323 | * FIXME: The trigger set here IS inserted when no matching template
|
---|
324 | * is found, but a space character (" ") is always used as the "trigger"
|
---|
325 | * to look for templates. This is because it is hard-coded in
|
---|
326 | * RSyntaxTextArea's input map this way. We need to change this.
|
---|
327 | * See RSyntaxTextAreaDefaultInputMap.java.
|
---|
328 | */
|
---|
329 | public void setInsertTrigger(KeyStroke trigger) {
|
---|
330 | if (trigger!=null) {
|
---|
331 | insertTrigger = trigger;
|
---|
332 | insertTriggerString = Character.toString(trigger.getKeyChar());
|
---|
333 | }
|
---|
334 | }
|
---|
335 |
|
---|
336 |
|
---|
337 | /**
|
---|
338 | * Sets the directory in which to look for templates. Calling this
|
---|
339 | * method adds any new templates found in the specified directory to
|
---|
340 | * the templates already registered.
|
---|
341 | *
|
---|
342 | * @param dir The new directory in which to look for templates.
|
---|
343 | * @return The new number of templates in this template manager, or
|
---|
344 | * <code>-1</code> if the specified directory does not exist.
|
---|
345 | */
|
---|
346 | public synchronized int setTemplateDirectory(File dir) {
|
---|
347 |
|
---|
348 | if (dir!=null && dir.isDirectory()) {
|
---|
349 |
|
---|
350 | this.directory = dir;
|
---|
351 |
|
---|
352 | File[] files = dir.listFiles(new XMLFileFilter());
|
---|
353 | int newCount = files==null ? 0 : files.length;
|
---|
354 | int oldCount = templates.size();
|
---|
355 |
|
---|
356 | List temp = new ArrayList(oldCount+newCount);
|
---|
357 | temp.addAll(templates);
|
---|
358 |
|
---|
359 | for (int i=0; i<newCount; i++) {
|
---|
360 | try {
|
---|
361 | XMLDecoder d = new XMLDecoder(new BufferedInputStream(
|
---|
362 | new FileInputStream(files[i])));
|
---|
363 | Object obj = d.readObject();
|
---|
364 | if (!(obj instanceof CodeTemplate)) {
|
---|
365 | throw new IOException("Not a CodeTemplate: " +
|
---|
366 | files[i].getAbsolutePath());
|
---|
367 | }
|
---|
368 | temp.add(obj);
|
---|
369 | d.close();
|
---|
370 | } catch (/*IO, NoSuchElement*/Exception e) {
|
---|
371 | // NoSuchElementException can be thrown when reading
|
---|
372 | // an XML file not in the format expected by XMLDecoder.
|
---|
373 | // (e.g. CodeTemplates in an old format).
|
---|
374 | e.printStackTrace();
|
---|
375 | }
|
---|
376 | }
|
---|
377 | templates = temp;
|
---|
378 | sortTemplates();
|
---|
379 |
|
---|
380 | return getTemplateCount();
|
---|
381 |
|
---|
382 | }
|
---|
383 |
|
---|
384 | return -1;
|
---|
385 |
|
---|
386 | }
|
---|
387 |
|
---|
388 |
|
---|
389 | /**
|
---|
390 | * Removes any null entries in the current set of templates (if
|
---|
391 | * any), sorts the remaining templates, and computes the new
|
---|
392 | * maximum template ID length.
|
---|
393 | */
|
---|
394 | private synchronized void sortTemplates() {
|
---|
395 |
|
---|
396 | // Get the maximum length of a template ID.
|
---|
397 | maxTemplateIDLength = 0;
|
---|
398 |
|
---|
399 | // Remove any null entries (should only happen because of
|
---|
400 | // IOExceptions, etc. when loading from files), and sort
|
---|
401 | // the remaining list.
|
---|
402 | for (Iterator i=templates.iterator(); i.hasNext(); ) {
|
---|
403 | CodeTemplate temp = (CodeTemplate)i.next();
|
---|
404 | if (temp==null || temp.getID()==null) {
|
---|
405 | i.remove();
|
---|
406 | }
|
---|
407 | else {
|
---|
408 | maxTemplateIDLength = Math.max(maxTemplateIDLength,
|
---|
409 | temp.getID().length());
|
---|
410 | }
|
---|
411 | }
|
---|
412 |
|
---|
413 | Collections.sort(templates);
|
---|
414 |
|
---|
415 | }
|
---|
416 |
|
---|
417 |
|
---|
418 | /**
|
---|
419 | * A comparator that takes a <code>CodeTemplate</code> as its first
|
---|
420 | * parameter and a <code>Segment</code> as its second, and knows
|
---|
421 | * to compare the template's ID to the segment's text.
|
---|
422 | */
|
---|
423 | private static class TemplateComparator implements Comparator, Serializable{
|
---|
424 |
|
---|
425 | public int compare(Object template, Object segment) {
|
---|
426 |
|
---|
427 | // Get template start index (0) and length.
|
---|
428 | CodeTemplate t = (CodeTemplate)template;
|
---|
429 | final char[] templateArray = t.getID().toCharArray();
|
---|
430 | int i = 0;
|
---|
431 | int len1 = templateArray.length;
|
---|
432 |
|
---|
433 | // Find "token" part of segment and get its offset and length.
|
---|
434 | Segment s = (Segment)segment;
|
---|
435 | char[] segArray = s.array;
|
---|
436 | int len2 = s.count;
|
---|
437 | int j = s.offset + len2 - 1;
|
---|
438 | while (j>=s.offset && isValidChar(segArray[j])) {
|
---|
439 | j--;
|
---|
440 | }
|
---|
441 | j++;
|
---|
442 | int segShift = j - s.offset;
|
---|
443 | len2 -= segShift;
|
---|
444 |
|
---|
445 | int n = Math.min(len1, len2);
|
---|
446 | while (n-- != 0) {
|
---|
447 | char c1 = templateArray[i++];
|
---|
448 | char c2 = segArray[j++];
|
---|
449 | if (c1 != c2)
|
---|
450 | return c1 - c2;
|
---|
451 | }
|
---|
452 | return len1 - len2;
|
---|
453 |
|
---|
454 | }
|
---|
455 |
|
---|
456 | }
|
---|
457 |
|
---|
458 |
|
---|
459 | /**
|
---|
460 | * A file filter for File.listFiles() (NOT for JFileChoosers!) that
|
---|
461 | * accepts only XML files.
|
---|
462 | */
|
---|
463 | private static class XMLFileFilter implements FileFilter {
|
---|
464 | public boolean accept(File f) {
|
---|
465 | return f.getName().toLowerCase().endsWith(".xml");
|
---|
466 | }
|
---|
467 | }
|
---|
468 |
|
---|
469 |
|
---|
470 | } |
---|