Source code: org/gjt/sp/jedit/Registers.java
1 /*
2 * Registers.java - Register manager
3 * :tabSize=8:indentSize=8:noTabs=false:
4 * :folding=explicit:collapseFolds=1:
5 *
6 * Copyright (C) 1999, 2003 Slava Pestov
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
21 */
22
23 package org.gjt.sp.jedit;
24
25 //{{{ Imports
26 import com.microstar.xml.*;
27 import java.awt.datatransfer.*;
28 import java.awt.Toolkit;
29 import java.io.*;
30 import org.gjt.sp.jedit.gui.*;
31 import org.gjt.sp.jedit.textarea.*;
32 import org.gjt.sp.util.Log;
33 //}}}
34
35 /**
36 * jEdit's registers are an extension of the clipboard metaphor.<p>
37 *
38 * A {@link Registers.Register} is string of text indexed by a
39 * single character. Typically the text is taken from selected buffer text
40 * and the index character is a keyboard character selected by the user.<p>
41 *
42 * This class defines a number of static methods
43 * that give each register the properties of a virtual clipboard.<p>
44 *
45 * Two classes implement the {@link Registers.Register} interface. A
46 * {@link Registers.ClipboardRegister} is tied to the contents of the
47 * system clipboard. jEdit assigns a
48 * {@link Registers.ClipboardRegister} to the register indexed under
49 * the character <code>$</code>. A
50 * {@link Registers.StringRegister} is created for registers assigned
51 * by the user. In addition, jEdit assigns <code>%</code> to
52 * the last text segment selected in the text area. On Windows this is a
53 * {@link Registers.StringRegister}, on Unix under Java 2 version 1.4, a
54 * {@link Registers.ClipboardRegister}.
55 *
56 * @author Slava Pestov
57 * @author John Gellene (API documentation)
58 * @version $Id: Registers.java,v 1.19 2003/07/23 06:08:54 spestov Exp $
59 */
60 public class Registers
61 {
62 //{{{ copy() method
63 /**
64 * Copies the text selected in the text area into the specified register.
65 * This will replace the existing contents of the designated register.
66 *
67 * @param textArea The text area
68 * @param register The register
69 * @since jEdit 2.7pre2
70 */
71 public static void copy(JEditTextArea textArea, char register)
72 {
73 String selection = textArea.getSelectedText();
74 if(selection == null)
75 return;
76
77 setRegister(register,selection);
78 HistoryModel.getModel("clipboard").addItem(selection);
79 } //}}}
80
81 //{{{ cut() method
82 /**
83 * Copies the text selected in the text area into the specified
84 * register, and then removes it from the buffer.
85 *
86 * @param textArea The text area
87 * @param register The register
88 * @since jEdit 2.7pre2
89 */
90 public static void cut(JEditTextArea textArea, char register)
91 {
92 if(textArea.isEditable())
93 {
94 String selection = textArea.getSelectedText();
95 if(selection == null)
96 return;
97
98 setRegister(register,selection);
99 HistoryModel.getModel("clipboard").addItem(selection);
100
101 textArea.setSelectedText("");
102 }
103 else
104 textArea.getToolkit().beep();
105 } //}}}
106
107 //{{{ append() method
108 /**
109 * Appends the text selected in the text area to the specified register,
110 * with a newline between the old and new text.
111 * @param textArea The text area
112 * @param register The register
113 */
114 public static void append(JEditTextArea textArea, char register)
115 {
116 append(textArea,register,"\n",false);
117 } //}}}
118
119 //{{{ append() method
120 /**
121 * Appends the text selected in the text area to the specified register.
122 * @param textArea The text area
123 * @param register The register
124 * @param separator The separator to insert between the old and new text
125 */
126 public static void append(JEditTextArea textArea, char register,
127 String separator)
128 {
129 append(textArea,register,separator,false);
130 } //}}}
131
132 //{{{ append() method
133 /**
134 * Appends the text selected in the text area to the specified register.
135 * @param textArea The text area
136 * @param register The register
137 * @param separator The text to insert between the old and new text
138 * @param cut Should the current selection be removed?
139 * @since jEdit 3.2pre1
140 */
141 public static void append(JEditTextArea textArea, char register,
142 String separator, boolean cut)
143 {
144 if(cut && !textArea.isEditable())
145 {
146 textArea.getToolkit().beep();
147 return;
148 }
149
150 String selection = textArea.getSelectedText();
151 if(selection == null)
152 return;
153
154 Register reg = getRegister(register);
155
156 if(reg != null)
157 {
158 String registerContents = reg.toString();
159 if(registerContents != null)
160 {
161 if(registerContents.endsWith(separator))
162 selection = registerContents + selection;
163 else
164 selection = registerContents + separator + selection;
165 }
166 }
167
168 setRegister(register,selection);
169 HistoryModel.getModel("clipboard").addItem(selection);
170
171 if(cut)
172 textArea.setSelectedText("");
173 } //}}}
174
175 //{{{ paste() method
176 /**
177 * Insets the contents of the specified register into the text area.
178 * @param textArea The text area
179 * @param register The register
180 * @since jEdit 2.7pre2
181 */
182 public static void paste(JEditTextArea textArea, char register)
183 {
184 paste(textArea,register,false);
185 } //}}}
186
187 //{{{ paste() method
188 /**
189 * Inserts the contents of the specified register into the text area.
190 * @param textArea The text area
191 * @param register The register
192 * @param vertical Vertical (columnar) paste
193 * @since jEdit 4.1pre1
194 */
195 public static void paste(JEditTextArea textArea, char register,
196 boolean vertical)
197 {
198 if(!textArea.isEditable())
199 {
200 textArea.getToolkit().beep();
201 return;
202 }
203
204 Register reg = getRegister(register);
205
206 if(reg == null)
207 {
208 textArea.getToolkit().beep();
209 return;
210 }
211 else
212 {
213 String selection = reg.toString();
214 if(selection == null)
215 {
216 textArea.getToolkit().beep();
217 return;
218 }
219
220 if(vertical && textArea.getSelectionCount() == 0)
221 {
222 Buffer buffer = textArea.getBuffer();
223
224 try
225 {
226 buffer.beginCompoundEdit();
227
228 int caret = textArea.getCaretPosition();
229 int caretLine = textArea.getCaretLine();
230 Selection.Rect rect = new Selection.Rect(
231 caretLine,caret,caretLine,caret);
232 textArea.setSelectedText(rect,selection);
233 caretLine = textArea.getCaretLine();
234
235 if(caretLine != textArea.getLineCount() - 1)
236 {
237 int startColumn = rect.getStartColumn(
238 buffer);
239 int offset = buffer
240 .getOffsetOfVirtualColumn(
241 caretLine + 1,startColumn,null);
242 if(offset == -1)
243 {
244 buffer.insertAtColumn(caretLine + 1,startColumn,"");
245 textArea.setCaretPosition(
246 buffer.getLineEndOffset(
247 caretLine + 1) - 1);
248 }
249 else
250 {
251 textArea.setCaretPosition(
252 buffer.getLineStartOffset(
253 caretLine + 1) + offset);
254 }
255 }
256 }
257 finally
258 {
259 buffer.endCompoundEdit();
260 }
261 }
262 else
263 textArea.setSelectedText(selection);
264
265 HistoryModel.getModel("clipboard").addItem(selection);
266 }
267 } //}}}
268
269 //{{{ getRegister() method
270 /**
271 * Returns the specified register.
272 * @param name The name
273 */
274 public static Register getRegister(char name)
275 {
276 if(name != '$' && name != '%')
277 {
278 if(!loaded)
279 loadRegisters();
280 }
281
282 if(registers == null || name >= registers.length)
283 return null;
284 else
285 return registers[name];
286 } //}}}
287
288 //{{{ setRegister() method
289 /**
290 * Sets the specified register.
291 * @param name The name
292 * @param newRegister The new value
293 */
294 public static void setRegister(char name, Register newRegister)
295 {
296 if(name != '%' && name != '$')
297 {
298 if(!loaded)
299 loadRegisters();
300
301 if(!loading)
302 modified = true;
303 }
304
305 if(name >= registers.length)
306 {
307 Register[] newRegisters = new Register[
308 Math.min(1<<16,name * 2)];
309 System.arraycopy(registers,0,newRegisters,0,
310 registers.length);
311 registers = newRegisters;
312 }
313
314 registers[name] = newRegister;
315 } //}}}
316
317 //{{{ setRegister() method
318 /**
319 * Sets the specified register.
320 * @param name The name
321 * @param value The new value
322 */
323 public static void setRegister(char name, String value)
324 {
325 Register register = getRegister(name);
326 if(register != null)
327 register.setValue(value);
328 else
329 setRegister(name,new StringRegister(value));
330 } //}}}
331
332 //{{{ clearRegister() method
333 /**
334 * Sets the value of the specified register to <code>null</code>.
335 * @param name The register name
336 */
337 public static void clearRegister(char name)
338 {
339 if(name >= registers.length)
340 return;
341
342 Register register = registers[name];
343 if(name == '$' || name == '%')
344 register.setValue("");
345 else
346 registers[name] = null;
347 } //}}}
348
349 //{{{ getRegisters() method
350 /**
351 * Returns an array of all available registers. Some of the elements
352 * of this array might be <code>null</code>.
353 */
354 public static Register[] getRegisters()
355 {
356 if(!loaded)
357 loadRegisters();
358 return registers;
359 } //}}}
360
361 //{{{ getRegisterStatusPrompt() method
362 /**
363 * Returns the status prompt for the given register action. Only
364 * intended to be called from <code>actions.xml</code>.
365 * @since jEdit 4.2pre2
366 */
367 public static String getRegisterStatusPrompt(String action)
368 {
369 return jEdit.getProperty("view.status." + action,
370 new String[] { getRegisterNameString() });
371 } //}}}
372
373 //{{{ getRegisterNameString() method
374 /**
375 * Returns a string of all defined registers, used by the status bar
376 * (eg, "a b $ % ^").
377 * @since jEdit 4.2pre2
378 */
379 public static String getRegisterNameString()
380 {
381 if(!loaded)
382 loadRegisters();
383
384 StringBuffer buf = new StringBuffer();
385 for(int i = 0; i < registers.length; i++)
386 {
387 if(registers[i] != null)
388 {
389 if(buf.length() != 0)
390 buf.append(' ');
391 buf.append((char)i);
392 }
393 }
394
395 if(buf.length() == 0)
396 return jEdit.getProperty("view.status.no-registers");
397 else
398 return buf.toString();
399 } //}}}
400
401 //{{{ saveRegisters() method
402 public static void saveRegisters()
403 {
404 if(!loaded || !modified)
405 return;
406
407 Log.log(Log.MESSAGE,Registers.class,"Saving registers.xml");
408 File file1 = new File(MiscUtilities.constructPath(
409 jEdit.getSettingsDirectory(), "#registers.xml#save#"));
410 File file2 = new File(MiscUtilities.constructPath(
411 jEdit.getSettingsDirectory(), "registers.xml"));
412 if(file2.exists() && file2.lastModified() != registersModTime)
413 {
414 Log.log(Log.WARNING,Registers.class,file2 + " changed"
415 + " on disk; will not save registers");
416 return;
417 }
418
419 jEdit.backupSettingsFile(file2);
420
421 String lineSep = System.getProperty("line.separator");
422
423 try
424 {
425 BufferedWriter out = new BufferedWriter(
426 new FileWriter(file1));
427
428 out.write("<?xml version=\"1.0\"?>");
429 out.write(lineSep);
430 out.write("<!DOCTYPE REGISTERS SYSTEM \"registers.dtd\">");
431 out.write(lineSep);
432 out.write("<REGISTERS>");
433 out.write(lineSep);
434
435 Register[] registers = getRegisters();
436 for(int i = 0; i < registers.length; i++)
437 {
438 Register register = registers[i];
439 if(register == null || i == '$' || i == '%')
440 continue;
441
442 out.write("<REGISTER NAME=\"");
443 if(i == '"')
444 out.write(""");
445 else
446 out.write((char)i);
447 out.write("\">");
448
449 out.write(MiscUtilities.charsToEntities(
450 register.toString()));
451
452 out.write("</REGISTER>");
453 out.write(lineSep);
454 }
455
456 out.write("</REGISTERS>");
457 out.write(lineSep);
458
459 out.close();
460
461 /* to avoid data loss, only do this if the above
462 * completed successfully */
463 file2.delete();
464 file1.renameTo(file2);
465 }
466 catch(Exception e)
467 {
468 Log.log(Log.ERROR,Registers.class,e);
469 }
470
471 registersModTime = file2.lastModified();
472 modified = false;
473 } //}}}
474
475 //{{{ Private members
476 private static Register[] registers;
477 private static long registersModTime;
478 private static boolean loaded, loading, modified;
479
480 private Registers() {}
481
482 static
483 {
484 registers = new Register[256];
485 registers['$'] = new ClipboardRegister(Toolkit
486 .getDefaultToolkit().getSystemClipboard());
487 }
488
489 //{{{ loadRegisters() method
490 private static void loadRegisters()
491 {
492 String settingsDirectory = jEdit.getSettingsDirectory();
493 if(settingsDirectory == null)
494 return;
495
496 File registerFile = new File(MiscUtilities.constructPath(
497 jEdit.getSettingsDirectory(),"registers.xml"));
498 if(!registerFile.exists())
499 return;
500
501 registersModTime = registerFile.lastModified();
502 loaded = true;
503
504 Log.log(Log.MESSAGE,jEdit.class,"Loading registers.xml");
505
506 RegistersHandler handler = new RegistersHandler();
507 XmlParser parser = new XmlParser();
508 parser.setHandler(handler);
509 Reader in = null;
510 try
511 {
512 loading = true;
513 in = new BufferedReader(new FileReader(registerFile));
514 parser.parse(null, null, in);
515 }
516 catch(XmlException xe)
517 {
518 int line = xe.getLine();
519 String message = xe.getMessage();
520 Log.log(Log.ERROR,Registers.class,registerFile + ":"
521 + line + ": " + message);
522 }
523 catch(FileNotFoundException fnf)
524 {
525 //Log.log(Log.DEBUG,Registers.class,fnf);
526 }
527 catch(Exception e)
528 {
529 Log.log(Log.ERROR,Registers.class,e);
530 }
531 finally
532 {
533 loading = false;
534 try
535 {
536 if(in != null)
537 in.close();
538 }
539 catch(IOException io)
540 {
541 Log.log(Log.ERROR,Registers.class,io);
542 }
543 }
544 } //}}}
545
546 //}}}
547
548 //{{{ Inner classes
549
550 //{{{ Register interface
551 /**
552 * A register.
553 */
554 public interface Register
555 {
556 /**
557 * Converts to a string.
558 */
559 String toString();
560
561 /**
562 * Sets the register contents.
563 */
564 void setValue(String value);
565 } //}}}
566
567 //{{{ ClipboardRegister class
568 /**
569 * A clipboard register. Register "$" should always be an
570 * instance of this.
571 */
572 public static class ClipboardRegister implements Register
573 {
574 Clipboard clipboard;
575
576 public ClipboardRegister(Clipboard clipboard)
577 {
578 this.clipboard = clipboard;
579 }
580
581 /**
582 * Sets the clipboard contents.
583 */
584 public void setValue(String value)
585 {
586 StringSelection selection = new StringSelection(value);
587 clipboard.setContents(selection,null);
588 }
589
590 /**
591 * Returns the clipboard contents.
592 */
593 public String toString()
594 {
595 try
596 {
597 String selection = (String)(clipboard
598 .getContents(this).getTransferData(
599 DataFlavor.stringFlavor));
600
601 boolean trailingEOL = (selection.endsWith("\n")
602 || selection.endsWith(System.getProperty(
603 "line.separator")));
604
605 // Some Java versions return the clipboard
606 // contents using the native line separator,
607 // so have to convert it here
608 BufferedReader in = new BufferedReader(
609 new StringReader(selection));
610 StringBuffer buf = new StringBuffer();
611 String line;
612 while((line = in.readLine()) != null)
613 {
614 buf.append(line);
615 buf.append('\n');
616 }
617 // remove trailing \n
618 if(!trailingEOL && buf.length() != 0)
619 buf.setLength(buf.length() - 1);
620 return buf.toString();
621 }
622 catch(Exception e)
623 {
624 Log.log(Log.NOTICE,this,e);
625 return null;
626 }
627 }
628 } //}}}
629
630 //{{{ StringRegister class
631 /**
632 * Register that stores a string.
633 */
634 public static class StringRegister implements Register
635 {
636 private String value;
637
638 /**
639 * Creates a new string register.
640 * @param value The contents
641 */
642 public StringRegister(String value)
643 {
644 this.value = value;
645 }
646
647 /**
648 * Sets the register contents.
649 */
650 public void setValue(String value)
651 {
652 this.value = value;
653 }
654
655 /**
656 * Converts to a string.
657 */
658 public String toString()
659 {
660 return value;
661 }
662
663 /**
664 * Called when this register is no longer available. This
665 * implementation does nothing.
666 */
667 public void dispose() {}
668 } //}}}
669
670 //{{{ RegistersHandler class
671 static class RegistersHandler extends HandlerBase
672 {
673 //{{{ resolveEntity() method
674 public Object resolveEntity(String publicId, String systemId)
675 {
676 if("registers.dtd".equals(systemId))
677 {
678 // this will result in a slight speed up, since we
679 // don't need to read the DTD anyway, as AElfred is
680 // non-validating
681 return new StringReader("<!-- -->");
682
683 /* try
684 {
685 return new BufferedReader(new InputStreamReader(
686 getClass().getResourceAsStream("registers.dtd")));
687 }
688 catch(Exception e)
689 {
690 Log.log(Log.ERROR,this,"Error while opening"
691 + " recent.dtd:");
692 Log.log(Log.ERROR,this,e);
693 } */
694 }
695
696 return null;
697 } //}}}
698
699 //{{{ attribute() method
700 public void attribute(String aname, String value, boolean isSpecified)
701 {
702 if(aname.equals("NAME"))
703 registerName = value;
704 } //}}}
705
706 //{{{ doctypeDecl() method
707 public void doctypeDecl(String name, String publicId,
708 String systemId) throws Exception
709 {
710 if("REGISTERS".equals(name))
711 return;
712
713 Log.log(Log.ERROR,this,"registers.xml: DOCTYPE must be REGISTERS");
714 } //}}}
715
716 //{{{ endElement() method
717 public void endElement(String name)
718 {
719 if(name.equals("REGISTER"))
720 {
721 if(registerName == null || registerName.length() != 1)
722 Log.log(Log.ERROR,this,"Malformed NAME: " + registerName);
723 else
724 setRegister(registerName.charAt(0),charData);
725 }
726 } //}}}
727
728 //{{{ charData() method
729 public void charData(char[] ch, int start, int length)
730 {
731 charData = new String(ch,start,length);
732 } //}}}
733
734 //{{{ Private members
735 private String registerName;
736 private String charData;
737 //}}}
738 } //}}}
739
740 //}}}
741 }