View Javadoc

1   /*
2    *  soapUI, copyright (C) 2004-2010 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 			public void run()
286 			{
287 				while( true )
288 				{
289 					// System.out.println(
290 					// "Waiting for weak references to be released.." );
291 
292 					try
293 					{
294 						if( testQueue != null )
295 						{
296 						Reference<? extends JXEditTextArea> ref = testQueue.remove();
297 						// System.out.println( "Got ref to clear" );
298 						InternalSettingsListener listener = testMap.remove( ref );
299 						if( listener != null )
300 						{
301 							// System.out.println( "Releasing listener" );
302 							listener.release();
303 						}
304 						else
305 						{
306 							// System.out.println( "Listener not found" );
307 						}
308 					}
309 					}
310 					catch( InterruptedException e )
311 					{
312 						SoapUI.logError( e );
313 					}
314 				}
315 
316 			}
317 		}, "ReferenceQueueMonitor" ).start();
318 	}
319 
320 	private static final class InternalSettingsListener implements SettingsListener
321 	{
322 		private WeakReference<JXEditTextArea> textArea;
323 
324 		public InternalSettingsListener( JXEditTextArea area )
325 		{
326 			// System.out.println( "Creating weakreference for textarea" );
327 			textArea = new WeakReference<JXEditTextArea>( area, testQueue );
328 			testMap.put( textArea, this );
329 			SoapUI.getSettings().addSettingsListener( this );
330 		}
331 
332 		public void release()
333 		{
334 			if( textArea.get() == null )
335 			{
336 				SoapUI.getSettings().removeSettingsListener( this );
337 			}
338 			else
339 			{
340 				System.err.println( "Error, cannot release listener" );
341 			}
342 		}
343 
344 		public void settingChanged( String name, String newValue, String oldValue )
345 		{
346 			if( name.equals( UISettings.EDITOR_FONT ) && textArea.get() != null )
347 			{
348 				textArea.get().getPainter().setFont( Font.decode( newValue ) );
349 				textArea.get().invalidate();
350 			}
351 		}
352 
353 	}
354 
355 	private class UndoAction extends AbstractAction
356 	{
357 		public UndoAction()
358 		{
359 			super( "Undo" );
360 			putValue( Action.ACCELERATOR_KEY, UISupport.getKeyStroke( "menu Z" ) );
361 		}
362 
363 		public void actionPerformed( ActionEvent e )
364 		{
365 			undo();
366 		}
367 	}
368 
369 	private class RedoAction extends AbstractAction
370 	{
371 		public RedoAction()
372 		{
373 			super( "Redo" );
374 			putValue( Action.ACCELERATOR_KEY, UISupport.getKeyStroke( "menu Y" ) );
375 		}
376 
377 		public void actionPerformed( ActionEvent e )
378 		{
379 			redo();
380 		}
381 	}
382 
383 	private final class GoToLineAction extends AbstractAction
384 	{
385 		public GoToLineAction()
386 		{
387 			super( "Go To Line" );
388 			putValue( Action.SHORT_DESCRIPTION, "Moves the caret to the specified line" );
389 			putValue( Action.ACCELERATOR_KEY, UISupport.getKeyStroke( "menu alt L" ) );
390 		}
391 
392 		public void actionPerformed( ActionEvent e )
393 		{
394 			String line = UISupport.prompt( "Enter line-number to (1.." + ( getLineCount() ) + ")", "Go To Line", String
395 					.valueOf( getCaretLine() + 1 ) );
396 
397 			if( line != null )
398 			{
399 				try
400 				{
401 					int ln = Integer.parseInt( line ) - 1;
402 					if( ln >= 0 && ln < getLineCount() )
403 					{
404 						scrollTo( ln, 0 );
405 						setCaretPosition( getLineStartOffset( ln ) );
406 					}
407 				}
408 				catch( NumberFormatException e1 )
409 				{
410 				}
411 			}
412 		}
413 	}
414 
415 	public class NextElementValueAction implements ActionListener
416 	{
417 		public void actionPerformed( ActionEvent e )
418 		{
419 			toNextElement();
420 		}
421 	}
422 
423 	public class PreviousElementValueAction implements ActionListener
424 	{
425 		public void actionPerformed( ActionEvent e )
426 		{
427 			toPreviousElement();
428 		}
429 	}
430 
431 	public class DeleteLineAction implements ActionListener
432 	{
433 		public void actionPerformed( ActionEvent e )
434 		{
435 			if( !isEditable() )
436 			{
437 				getToolkit().beep();
438 				return;
439 			}
440 
441 			int caretLine = getCaretLine();
442 			if( caretLine == -1 )
443 				return;
444 			int lineStartOffset = getLineStartOffset( caretLine );
445 			int lineEndOffset = getLineEndOffset( caretLine );
446 
447 			try
448 			{
449 				int len = lineEndOffset - lineStartOffset;
450 				if( lineStartOffset + len >= getDocumentLength() )
451 					len = getDocumentLength() - lineStartOffset;
452 
453 				getDocument().remove( lineStartOffset, len );
454 			}
455 			catch( BadLocationException e1 )
456 			{
457 				SoapUI.logError( e1 );
458 			}
459 		}
460 	}
461 
462 	public Action createCopyAction()
463 	{
464 		return new InputHandler.clip_copy();
465 	}
466 
467 	public void undo()
468 	{
469 		if( !isEditable() )
470 		{
471 			getToolkit().beep();
472 			return;
473 		}
474 
475 		try
476 		{
477 			if( undoManager != null )
478 				undoManager.undo();
479 		}
480 		catch( CannotUndoException cue )
481 		{
482 			Toolkit.getDefaultToolkit().beep();
483 		}
484 	}
485 
486 	public Action createCutAction()
487 	{
488 		return new InputHandler.clip_cut();
489 	}
490 
491 	public Action createPasteAction()
492 	{
493 		return new InputHandler.clip_paste();
494 	}
495 
496 	public int getCaretColumn()
497 	{
498 		int pos = getCaretPosition();
499 		int line = getLineOfOffset( pos );
500 
501 		return pos - getLineStartOffset( line );
502 	}
503 
504 	public void toNextElement()
505 	{
506 		int pos = getCaretPosition();
507 		String text = getText();
508 
509 		while( pos < text.length() )
510 		{
511 			// find ending >
512 			if( text.charAt( pos ) == '>' && pos < text.length() - 1
513 					&& ( pos > 2 && !text.substring( pos - 2, pos ).equals( "--" ) )
514 					&& ( pos > 1 && text.charAt( pos - 1 ) != '/' )
515 					&& text.indexOf( '/', pos ) == text.indexOf( '<', pos ) + 1
516 					&& text.lastIndexOf( '/', pos ) != text.lastIndexOf( '<', pos ) + 1 )
517 			{
518 				setCaretPosition( pos + 1 );
519 				return;
520 			}
521 
522 			pos++ ;
523 		}
524 
525 		getToolkit().beep();
526 	}
527 
528 	public void toPreviousElement()
529 	{
530 		int pos = getCaretPosition() - 2;
531 		String text = getText();
532 
533 		while( pos > 0 )
534 		{
535 			// find ending >
536 			if( text.charAt( pos ) == '>' && pos < text.length() - 1
537 					&& ( pos > 2 && !text.substring( pos - 2, pos ).equals( "--" ) )
538 					&& ( pos > 1 && text.charAt( pos - 1 ) != '/' )
539 					&& text.indexOf( '/', pos ) == text.indexOf( '<', pos ) + 1
540 					&& text.lastIndexOf( '/', pos ) != text.lastIndexOf( '<', pos ) + 1 )
541 			{
542 				setCaretPosition( pos + 1 );
543 				return;
544 			}
545 
546 			pos-- ;
547 		}
548 
549 		getToolkit().beep();
550 	}
551 
552 	public boolean canUndo()
553 	{
554 		return undoManager != null && undoManager.canUndo();
555 	}
556 
557 	public boolean canRedo()
558 	{
559 		return undoManager != null && undoManager.canRedo();
560 	}
561 
562 	public void redo()
563 	{
564 		if( !isEditable() )
565 		{
566 			getToolkit().beep();
567 			return;
568 		}
569 
570 		try
571 		{
572 			if( canRedo() )
573 				undoManager.redo();
574 		}
575 		catch( CannotRedoException cue )
576 		{
577 			Toolkit.getDefaultToolkit().beep();
578 		}
579 	}
580 }