View Javadoc

1   /*
2    *  soapUI, copyright (C) 2004-2007 eviware.com 
3    *
4    *  soapUI is free software; you can redistribute it and/or modify it under the 
5    *  terms of version 2.1 of the GNU Lesser General Public License as published by 
6    *  the Free Software Foundation.
7    *
8    *  soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
9    *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
10   *  See the GNU Lesser General Public License for more details at gnu.org.
11   */
12  
13  package com.eviware.soapui.support.xml;
14  
15  import java.awt.Color;
16  import java.awt.Dimension;
17  import java.awt.Font;
18  import java.awt.Toolkit;
19  import java.awt.event.ActionEvent;
20  import java.awt.event.ActionListener;
21  import java.awt.event.FocusEvent;
22  import java.awt.event.FocusListener;
23  import java.lang.ref.Reference;
24  import java.lang.ref.ReferenceQueue;
25  import java.lang.ref.WeakReference;
26  import java.util.HashMap;
27  import java.util.Map;
28  
29  import javax.swing.AbstractAction;
30  import javax.swing.Action;
31  import javax.swing.BorderFactory;
32  import javax.swing.event.UndoableEditEvent;
33  import javax.swing.event.UndoableEditListener;
34  import javax.swing.text.BadLocationException;
35  import javax.swing.undo.CannotRedoException;
36  import javax.swing.undo.CannotUndoException;
37  import javax.swing.undo.UndoManager;
38  
39  import org.syntax.jedit.InputHandler;
40  import org.syntax.jedit.JEditTextArea;
41  import org.syntax.jedit.SyntaxStyle;
42  import org.syntax.jedit.tokenmarker.GroovyTokenMarker;
43  import org.syntax.jedit.tokenmarker.Token;
44  import org.syntax.jedit.tokenmarker.TokenMarker;
45  import org.syntax.jedit.tokenmarker.XMLTokenMarker;
46  
47  import com.eviware.soapui.SoapUI;
48  import com.eviware.soapui.model.settings.SettingsListener;
49  import com.eviware.soapui.settings.UISettings;
50  import com.eviware.soapui.support.UISupport;
51  import com.eviware.soapui.support.actions.FindAndReplaceDialog;
52  import com.eviware.soapui.support.actions.FindAndReplaceable;
53  import com.eviware.soapui.support.components.JEditorStatusBar.JEditorStatusBarTarget;
54  
55  /***
56   * JEditTextArea extension targeted specifically at XML-editing.
57   *
58   * //@todo move font handling to subclass
59   * 
60   * @author Ole.Matzura
61   */
62  
63  public class JXEditTextArea extends JEditTextArea implements
64  		UndoableEditListener, FocusListener, FindAndReplaceable, JEditorStatusBarTarget
65  {
66  	public static final int UNDO_LIMIT = 1500;
67  	private UndoManager undoManager;
68  	private UndoAction undoAction;
69  	private RedoAction redoAction;
70  	private FindAndReplaceDialog findAndReplaceAction;
71  	private boolean discardEditsOnSet = true;
72  	private GoToLineAction goToLineAction;
73  	
74  	public static JXEditTextArea createXmlEditor()
75  	{
76  		return new JXEditTextArea( new XMLTokenMarker() );
77  	}
78  	
79  	public static JXEditTextArea createGroovyEditor()
80  	{
81  		return new JXEditTextArea( new GroovyTokenMarker() );
82  	}
83  	
84  	public JXEditTextArea( TokenMarker tokenMarker )
85  	{
86  		String editorFont = SoapUI.getSettings().getString( UISettings.EDITOR_FONT, UISettings.DEFAULT_EDITOR_FONT );
87  		if( editorFont != null && editorFont.length() > 0 )
88  			getPainter().setFont(Font.decode(editorFont));
89  		else
90  			getPainter().setFont(Font.decode(UISettings.DEFAULT_EDITOR_FONT));
91  		
92  		getPainter().setLineHighlightColor( new Color( 240, 240, 180 ) );
93  		getPainter().setStyles(createXmlStyles());
94  		setTokenMarker(tokenMarker);
95  		setBorder(BorderFactory.createEtchedBorder());
96  		addFocusListener(this);
97  
98  		undoAction = new UndoAction();
99  		getInputHandler().addKeyBinding("C+Z", undoAction);
100 		redoAction = new RedoAction();
101 		getInputHandler().addKeyBinding("C+Y", redoAction);
102 		findAndReplaceAction = new FindAndReplaceDialog( this );
103 		getInputHandler().addKeyBinding( "C+F", findAndReplaceAction );
104 		getInputHandler().addKeyBinding( "F3", findAndReplaceAction );
105 		getInputHandler().addKeyBinding("A+RIGHT", new NextElementValueAction() );
106 		getInputHandler().addKeyBinding("A+LEFT", new PreviousElementValueAction() );
107 		getInputHandler().addKeyBinding("C+D", new DeleteLineAction() );
108 		getInputHandler().addKeyBinding( "S+INSERT", createPasteAction()  );
109 		getInputHandler().addKeyBinding( "S+DELETE", createCutAction() );
110 		
111 		goToLineAction = new GoToLineAction();
112 		getInputHandler().addKeyBinding( "CA+L", goToLineAction );
113 		
114 		setMinimumSize( new Dimension( 50, 50 ));
115 	   new InternalSettingsListener( this );
116 	}
117 	
118 	@Override
119 	public void addNotify()
120 	{
121 		super.addNotify();
122 		getDocument().addUndoableEditListener(this);
123 	}
124 	
125 	@Override
126 	public void removeNotify()
127 	{
128 		super.removeNotify();
129 		getDocument().removeUndoableEditListener(this);
130 	}
131 
132 	public Action getFindAndReplaceAction()
133 	{
134 		return findAndReplaceAction;
135 	}
136 	
137 	public Action getGoToLineAction()
138 	{
139 		return goToLineAction;
140 	}
141 
142 	public Action getRedoAction()
143 	{
144 		return redoAction;
145 	}
146 
147 	public Action getUndoAction()
148 	{
149 		return undoAction;
150 	}
151 
152 	public void setText(String text)
153 	{
154 		if( text != null && text.equals( getText() ))
155 			return;
156 			
157 		super.setText( text == null ? "" : text);
158 		
159 		if( discardEditsOnSet && undoManager != null )
160 			undoManager.discardAllEdits();
161 	}
162 	
163 	public boolean isDiscardEditsOnSet()
164 	{
165 		return discardEditsOnSet;
166 	}
167 
168 	public void setDiscardEditsOnSet(boolean discardEditsOnSet)
169 	{
170 		this.discardEditsOnSet = discardEditsOnSet;
171 	}
172 
173 	public UndoManager getUndoManager()
174 	{
175 		return undoManager;
176 	}
177 
178 	public SyntaxStyle[] createXmlStyles()
179 	{
180 		SyntaxStyle[] styles = new SyntaxStyle[Token.ID_COUNT];
181 
182 		styles[Token.COMMENT1] = new SyntaxStyle(Color.black, true, false);
183 		styles[Token.COMMENT2] = new SyntaxStyle(new Color(0x990033), true, false);
184 		styles[Token.KEYWORD1] = new SyntaxStyle(Color.blue, false, false);
185 		styles[Token.KEYWORD2] = new SyntaxStyle(Color.magenta, false, false);
186 		styles[Token.KEYWORD3] = new SyntaxStyle(new Color(0x009600), false,
187 				false);
188 		styles[Token.LITERAL1] = new SyntaxStyle(new Color(0x650099), false,
189 				false);
190 		styles[Token.LITERAL2] = new SyntaxStyle(new Color(0x650099), false, true);
191 		styles[Token.LABEL] = new SyntaxStyle(new Color(0x990033), false, true);
192 		styles[Token.OPERATOR] = new SyntaxStyle(Color.black, false, true);
193 		styles[Token.INVALID] = new SyntaxStyle(Color.red, false, true);
194 
195 		return styles;
196 	}
197 
198 	private void createUndoMananger()
199 	{
200 		undoManager = new UndoManager();
201 		undoManager.setLimit(UNDO_LIMIT);
202 	}
203 	
204 	/*
205 
206 	private void removeUndoMananger()
207 	{
208 		if (undoManager == null)
209 			return;
210 		undoManager.end();
211 		undoManager = null;
212 	}
213 	
214 	*/
215 
216 	public void focusGained(FocusEvent fe)
217 	{
218 		if (isEditable() && undoManager == null )
219 			createUndoMananger();
220 	}
221 	
222 	public void setEnabled( boolean flag )
223 	{
224 		super.setEnabled( flag );
225 		setEditable( flag );
226 	}
227 
228 	public void setEditable(boolean enabled)
229 	{
230 		super.setEditable(enabled);
231 		setCaretVisible( enabled );
232 		getPainter().setLineHighlightEnabled( enabled );
233 		
234 		//getPainter().setBackground( enabled ? Color.WHITE : new Color(238, 238, 238) );
235 		repaint();
236 	}
237 
238 	public void focusLost(FocusEvent fe)
239 	{
240 		//removeUndoMananger();
241 	}
242 
243 	public void undoableEditHappened(UndoableEditEvent e)
244 	{
245 		if (undoManager != null)
246 			undoManager.addEdit(e.getEdit());
247 	}
248 
249 	private static ReferenceQueue<JXEditTextArea> testQueue = new ReferenceQueue<JXEditTextArea>();
250 	private static Map<WeakReference<JXEditTextArea>, InternalSettingsListener> testMap = 
251 		new HashMap<WeakReference<JXEditTextArea>, InternalSettingsListener>();
252 	
253 	static
254 	{
255 		new Thread( new Runnable() {
256 
257 			public void run()
258 			{
259 				while( true )
260 				{
261 //					System.out.println( "Waiting for weak references to be released.." );
262 					
263 					try
264 					{
265 						Reference<? extends JXEditTextArea> ref = testQueue.remove();
266 //						System.out.println( "Got ref to clear" );
267 						InternalSettingsListener listener = testMap.remove( ref );
268 						if( listener != null )
269 						{
270 //							System.out.println( "Releasing listener" );
271 							listener.release();
272 						}
273 						else
274 						{
275 //							System.out.println( "Listener not found" );
276 						}
277 					}
278 					catch( InterruptedException e )
279 					{
280 						SoapUI.logError( e );
281 					}
282 				}
283 				
284 			}}, "ReferenceQueueMonitor" ).start();
285 	}
286 	
287 	private static final class InternalSettingsListener implements SettingsListener
288 	{
289 		private WeakReference<JXEditTextArea> textArea;
290 		
291 		public InternalSettingsListener( JXEditTextArea area )
292 		{
293 //			System.out.println( "Creating weakreference for textarea" );
294 			textArea = new WeakReference<JXEditTextArea>(area,testQueue);
295 			testMap.put( textArea, this );
296 			SoapUI.getSettings().addSettingsListener(this);
297 		}
298 
299 		public void release()
300 		{
301 			if( textArea.get() == null )
302 			{
303 				SoapUI.getSettings().removeSettingsListener(this);
304 			}
305 			else
306 			{
307 				System.err.println( "Error, cannot release listener" );
308 			}
309 		}
310 
311 		public void settingChanged(String name, String newValue, String oldValue)
312 		{
313 			if( name.equals( UISettings.EDITOR_FONT ) && textArea.get() != null)
314 			{
315 				textArea.get().getPainter().setFont( Font.decode( newValue ));
316 				textArea.get().invalidate();
317 			}
318 		}
319 		
320 		
321 	}
322 
323 	private class UndoAction extends AbstractAction
324 	{
325 		public UndoAction()
326 		{
327 			super( "Undo");
328 			putValue( Action.ACCELERATOR_KEY, UISupport.getKeyStroke( "menu Z" ));
329 		}
330 		
331 		public void actionPerformed(ActionEvent e)
332 		{
333 			undo();
334 		}
335 	}
336 
337 	private class RedoAction extends AbstractAction
338 	{
339 		public RedoAction()
340 		{
341 			super( "Redo");
342 			putValue( Action.ACCELERATOR_KEY, UISupport.getKeyStroke( "menu Y" ));
343 		}
344 
345 		public void actionPerformed(ActionEvent e)
346 		{
347 			redo();
348 		}
349 	}
350 	
351 	private final class GoToLineAction extends AbstractAction
352 	{
353 		public GoToLineAction()
354 		{
355 			super( "Go To Line" );
356 			putValue( Action.SHORT_DESCRIPTION, "Moves the caret to the specified line" );
357 			putValue( Action.ACCELERATOR_KEY, UISupport.getKeyStroke( "menu alt L" ));
358 		}
359 		
360 		public void actionPerformed( ActionEvent e )
361 		{
362 			String line = UISupport.prompt( "Enter line-number to (1.." + (getLineCount()) + ")", "Go To Line", 
363 						String.valueOf( getCaretLine()+1 ));
364 			
365 			if( line != null )
366 			{
367 				try
368 				{
369 					int ln = Integer.parseInt( line )-1;
370 					if( ln >= 0 && ln < getLineCount() )
371 					{
372 						scrollTo( ln, 0 );
373 						setCaretPosition( getLineStartOffset( ln ));
374 					}
375 				}
376 				catch( NumberFormatException e1 )
377 				{
378 				}
379 			}
380 		}
381 	}
382 	
383 	public class NextElementValueAction implements ActionListener
384 	{
385 		public void actionPerformed(ActionEvent e)
386 		{
387 			toNextElement();
388 		}
389 	}
390 	
391 	public class PreviousElementValueAction implements ActionListener
392 	{
393 		public void actionPerformed(ActionEvent e)
394 		{
395 			toPreviousElement();
396 		}
397 	}
398 
399 	
400 	public class DeleteLineAction implements ActionListener
401 	{
402 		public void actionPerformed(ActionEvent e)
403 		{
404 			if( !isEditable() )
405 			{
406 				getToolkit().beep();
407 				return;
408 			}
409 			
410 			int caretLine = getCaretLine();
411 			if( caretLine == -1 ) return;
412 			int lineStartOffset = getLineStartOffset( caretLine);
413 			int lineEndOffset = getLineEndOffset( caretLine);
414 			
415 			try
416 			{
417 				int len = lineEndOffset-lineStartOffset;
418 				if( lineStartOffset+len >= getDocumentLength() ) len = getDocumentLength()-lineStartOffset;
419 				
420 				getDocument().remove( lineStartOffset, len );
421 			}
422 			catch (BadLocationException e1)
423 			{
424 				SoapUI.logError( e1 );
425 			}
426 		}
427 	}
428 
429 	public Action createCopyAction()
430 	{
431 		return new InputHandler.clip_copy();
432 	}
433 	
434 	public void undo()
435 	{
436 		if( !isEditable()  )
437 		{
438 			getToolkit().beep();
439 			return;
440 		}
441 		
442 		try
443 		{
444 			if( undoManager != null )
445 				undoManager.undo();
446 		}
447 		catch (CannotUndoException cue)
448 		{
449 			Toolkit.getDefaultToolkit().beep();
450 		}
451 	}
452 
453 	public Action createCutAction()
454 	{
455 		return new InputHandler.clip_cut();
456 	}
457 	
458 	public Action createPasteAction()
459 	{
460 		return new InputHandler.clip_paste();
461 	}
462 
463 	public int getCaretColumn()
464 	{
465 		int pos = getCaretPosition();
466 		int line = getLineOfOffset( pos );
467 		
468 		return pos - getLineStartOffset( line );
469 	}
470 
471 	public void toNextElement()
472 	{
473 		int pos = getCaretPosition();
474 		String text = getText();
475 		
476 		while( pos < text.length() )
477 		{
478 			// find ending >
479 			if( text.charAt( pos ) == '>' && pos < text.length()-1 && 
480 					(pos > 2 && !text.substring(pos-2,pos).equals( "--" )) &&
481 					(pos > 1 && text.charAt(pos-1) != '/' ) &&
482 					text.indexOf( '/', pos ) == text.indexOf( '<', pos) +1 &&
483 					text.lastIndexOf( '/', pos ) != text.lastIndexOf( '<', pos) +1	)
484 			{
485 				setCaretPosition( pos+1 );
486 				return;
487 			}		
488 			
489 			pos++;
490 		}			
491 		
492 		getToolkit().beep();
493 	}
494 
495 	public void toPreviousElement()
496 	{
497 		int pos = getCaretPosition()-2;
498 		String text = getText();
499 		
500 		while( pos > 0 )
501 		{
502 			// find ending >
503 			if( text.charAt( pos ) == '>' && pos < text.length()-1 && 
504 					(pos > 2 && !text.substring(pos-2,pos).equals( "--" )) &&
505 					(pos > 1 && text.charAt(pos-1) != '/' ) &&
506 					text.indexOf( '/', pos ) == text.indexOf( '<', pos) +1 &&
507 					text.lastIndexOf( '/', pos ) != text.lastIndexOf( '<', pos) +1	)
508 			{
509 				setCaretPosition( pos+1 );
510 				return;
511 			}		
512 			
513 			pos--;
514 		}				
515 
516 		getToolkit().beep();
517 	}
518 
519 	public boolean canUndo()
520 	{
521 		return undoManager != null && undoManager.canUndo();
522 	}
523 	
524 	public boolean canRedo()
525 	{
526 		return undoManager != null && undoManager.canRedo();
527 	}
528 
529 	public void redo()
530 	{
531 		if( !isEditable()  )
532 		{
533 			getToolkit().beep();
534 			return;
535 		}
536 		
537 		try
538 		{
539 			if( canRedo() )
540 				undoManager.redo();
541 		}
542 		catch (CannotRedoException cue)
543 		{
544 			Toolkit.getDefaultToolkit().beep();
545 		}	}
546 }