View Javadoc

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