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  	
73  	public static JXEditTextArea createXmlEditor()
74  	{
75  		return new JXEditTextArea( new XMLTokenMarker() );
76  	}
77  	
78  	public static JXEditTextArea createGroovyEditor()
79  	{
80  		return new JXEditTextArea( new GroovyTokenMarker() );
81  	}
82  	
83  	public JXEditTextArea( TokenMarker tokenMarker )
84  	{
85  		String editorFont = SoapUI.getSettings().getString( UISettings.EDITOR_FONT, UISettings.DEFAULT_EDITOR_FONT );
86  		if( editorFont != null && editorFont.length() > 0 )
87  			getPainter().setFont(Font.decode(editorFont));
88  		else
89  			getPainter().setFont(Font.decode(UISettings.DEFAULT_EDITOR_FONT));
90  		
91  		getPainter().setLineHighlightColor( new Color( 240, 240, 180 ) );
92  		getPainter().setStyles(createXmlStyles());
93  		setTokenMarker(tokenMarker);
94  		setBorder(BorderFactory.createEtchedBorder());
95  		addFocusListener(this);
96  
97  		undoAction = new UndoAction();
98  		getInputHandler().addKeyBinding("C+Z", undoAction);
99  		redoAction = new RedoAction();
100 		getInputHandler().addKeyBinding("C+Y", redoAction);
101 		findAndReplaceAction = new FindAndReplaceDialog( this );
102 		getInputHandler().addKeyBinding( "C+F", findAndReplaceAction );
103 		getInputHandler().addKeyBinding( "F3", findAndReplaceAction );
104 		getInputHandler().addKeyBinding("A+RIGHT", new NextElementValueAction() );
105 		getInputHandler().addKeyBinding("A+LEFT", new PreviousElementValueAction() );
106 		getInputHandler().addKeyBinding("C+D", new DeleteLineAction() );
107 		getInputHandler().addKeyBinding( "S+INSERT", createPasteAction()  );
108 		getInputHandler().addKeyBinding( "S+DELETE", createCutAction() );
109 		
110 		setMinimumSize( new Dimension( 50, 50 ));
111 	   new InternalSettingsListener( this );
112 	}
113 	
114 	@Override
115 	public void addNotify()
116 	{
117 		super.addNotify();
118 		getDocument().addUndoableEditListener(this);
119 	}
120 	
121 	@Override
122 	public void removeNotify()
123 	{
124 		super.removeNotify();
125 		getDocument().removeUndoableEditListener(this);
126 	}
127 
128 	public Action getFindAndReplaceAction()
129 	{
130 		return findAndReplaceAction;
131 	}
132 
133 	public Action getRedoAction()
134 	{
135 		return redoAction;
136 	}
137 
138 	public Action getUndoAction()
139 	{
140 		return undoAction;
141 	}
142 
143 	public void setText(String text)
144 	{
145 		if( text != null && text.equals( getText() ))
146 			return;
147 			
148 		super.setText( text == null ? "" : text);
149 		
150 		if( discardEditsOnSet && undoManager != null )
151 			undoManager.discardAllEdits();
152 	}
153 	
154 	public boolean isDiscardEditsOnSet()
155 	{
156 		return discardEditsOnSet;
157 	}
158 
159 	public void setDiscardEditsOnSet(boolean discardEditsOnSet)
160 	{
161 		this.discardEditsOnSet = discardEditsOnSet;
162 	}
163 
164 	public UndoManager getUndoManager()
165 	{
166 		return undoManager;
167 	}
168 
169 	public SyntaxStyle[] createXmlStyles()
170 	{
171 		SyntaxStyle[] styles = new SyntaxStyle[Token.ID_COUNT];
172 
173 		styles[Token.COMMENT1] = new SyntaxStyle(Color.black, true, false);
174 		styles[Token.COMMENT2] = new SyntaxStyle(new Color(0x990033), true, false);
175 		styles[Token.KEYWORD1] = new SyntaxStyle(Color.blue, false, false);
176 		styles[Token.KEYWORD2] = new SyntaxStyle(Color.magenta, false, false);
177 		styles[Token.KEYWORD3] = new SyntaxStyle(new Color(0x009600), false,
178 				false);
179 		styles[Token.LITERAL1] = new SyntaxStyle(new Color(0x650099), false,
180 				false);
181 		styles[Token.LITERAL2] = new SyntaxStyle(new Color(0x650099), false, true);
182 		styles[Token.LABEL] = new SyntaxStyle(new Color(0x990033), false, true);
183 		styles[Token.OPERATOR] = new SyntaxStyle(Color.black, false, true);
184 		styles[Token.INVALID] = new SyntaxStyle(Color.red, false, true);
185 
186 		return styles;
187 	}
188 
189 	private void createUndoMananger()
190 	{
191 		undoManager = new UndoManager();
192 		undoManager.setLimit(UNDO_LIMIT);
193 	}
194 	
195 	/*
196 
197 	private void removeUndoMananger()
198 	{
199 		if (undoManager == null)
200 			return;
201 		undoManager.end();
202 		undoManager = null;
203 	}
204 	
205 	*/
206 
207 	public void focusGained(FocusEvent fe)
208 	{
209 		if (isEditable() && undoManager == null )
210 			createUndoMananger();
211 	}
212 	
213 	public void setEnabled( boolean flag )
214 	{
215 		super.setEnabled( flag );
216 		setEditable( flag );
217 	}
218 
219 	public void setEditable(boolean enabled)
220 	{
221 		super.setEditable(enabled);
222 		setCaretVisible( enabled );
223 		getPainter().setLineHighlightEnabled( enabled );
224 		
225 		//getPainter().setBackground( enabled ? Color.WHITE : new Color(238, 238, 238) );
226 		repaint();
227 	}
228 
229 	public void focusLost(FocusEvent fe)
230 	{
231 		//removeUndoMananger();
232 	}
233 
234 	public void undoableEditHappened(UndoableEditEvent e)
235 	{
236 		if (undoManager != null)
237 			undoManager.addEdit(e.getEdit());
238 	}
239 
240 	private static ReferenceQueue<JXEditTextArea> testQueue = new ReferenceQueue<JXEditTextArea>();
241 	private static Map<WeakReference<JXEditTextArea>, InternalSettingsListener> testMap = 
242 		new HashMap<WeakReference<JXEditTextArea>, InternalSettingsListener>();
243 	
244 	static
245 	{
246 		new Thread( new Runnable() {
247 
248 			public void run()
249 			{
250 				while( true )
251 				{
252 //					System.out.println( "Waiting for weak references to be released.." );
253 					
254 					try
255 					{
256 						Reference<? extends JXEditTextArea> ref = testQueue.remove();
257 //						System.out.println( "Got ref to clear" );
258 						InternalSettingsListener listener = testMap.remove( ref );
259 						if( listener != null )
260 						{
261 //							System.out.println( "Releasing listener" );
262 							listener.release();
263 						}
264 						else
265 						{
266 //							System.out.println( "Listener not found" );
267 						}
268 					}
269 					catch( InterruptedException e )
270 					{
271 						SoapUI.logError( e );
272 					}
273 				}
274 				
275 			}}, "ReferenceQueueMonitor" ).start();
276 	}
277 	
278 	private static final class InternalSettingsListener implements SettingsListener
279 	{
280 		private WeakReference<JXEditTextArea> textArea;
281 		
282 		public InternalSettingsListener( JXEditTextArea area )
283 		{
284 //			System.out.println( "Creating weakreference for textarea" );
285 			textArea = new WeakReference<JXEditTextArea>(area,testQueue);
286 			testMap.put( textArea, this );
287 			SoapUI.getSettings().addSettingsListener(this);
288 		}
289 
290 		public void release()
291 		{
292 			if( textArea.get() == null )
293 			{
294 				SoapUI.getSettings().removeSettingsListener(this);
295 			}
296 			else
297 			{
298 				System.err.println( "Error, cannot release listener" );
299 			}
300 		}
301 
302 		public void settingChanged(String name, String newValue, String oldValue)
303 		{
304 			if( name.equals( UISettings.EDITOR_FONT ) && textArea.get() != null)
305 			{
306 				textArea.get().getPainter().setFont( Font.decode( newValue ));
307 				textArea.get().invalidate();
308 			}
309 		}
310 		
311 		
312 	}
313 
314 	private class UndoAction extends AbstractAction
315 	{
316 		public UndoAction()
317 		{
318 			super( "Undo");
319 			putValue( Action.ACCELERATOR_KEY, UISupport.getKeyStroke( "menu Z" ));
320 		}
321 		
322 		public void actionPerformed(ActionEvent e)
323 		{
324 			if( !isEditable()  )
325 			{
326 				getToolkit().beep();
327 				return;
328 			}
329 			
330 			try
331 			{
332 				if( undoManager != null )
333 					undoManager.undo();
334 			}
335 			catch (CannotUndoException cue)
336 			{
337 				Toolkit.getDefaultToolkit().beep();
338 			}
339 		}
340 	}
341 
342 	private class RedoAction extends AbstractAction
343 	{
344 		public RedoAction()
345 		{
346 			super( "Redo");
347 			putValue( Action.ACCELERATOR_KEY, UISupport.getKeyStroke( "menu Y" ));
348 		}
349 
350 		public void actionPerformed(ActionEvent e)
351 		{
352 			if( !isEditable()  )
353 			{
354 				getToolkit().beep();
355 				return;
356 			}
357 			
358 			try
359 			{
360 				if( undoManager != null )
361 					undoManager.redo();
362 			}
363 			catch (CannotRedoException cue)
364 			{
365 				Toolkit.getDefaultToolkit().beep();
366 			}
367 		}
368 	}
369 	
370 	public class NextElementValueAction implements ActionListener
371 	{
372 		public void actionPerformed(ActionEvent e)
373 		{
374 			toNextElement();
375 		}
376 	}
377 	
378 	public class PreviousElementValueAction implements ActionListener
379 	{
380 		public void actionPerformed(ActionEvent e)
381 		{
382 			toPreviousElement();
383 		}
384 	}
385 
386 	
387 	public class DeleteLineAction implements ActionListener
388 	{
389 		public void actionPerformed(ActionEvent e)
390 		{
391 			if( !isEditable() )
392 			{
393 				getToolkit().beep();
394 				return;
395 			}
396 			
397 			int caretLine = getCaretLine();
398 			if( caretLine == -1 ) return;
399 			int lineStartOffset = getLineStartOffset( caretLine);
400 			int lineEndOffset = getLineEndOffset( caretLine);
401 			
402 			try
403 			{
404 				int len = lineEndOffset-lineStartOffset;
405 				if( lineStartOffset+len >= getDocumentLength() ) len = getDocumentLength()-lineStartOffset;
406 				
407 				getDocument().remove( lineStartOffset, len );
408 			}
409 			catch (BadLocationException e1)
410 			{
411 				SoapUI.logError( e1 );
412 			}
413 		}
414 	}
415 
416 	public Action createCopyAction()
417 	{
418 		return new InputHandler.clip_copy();
419 	}
420 	
421 	public Action createCutAction()
422 	{
423 		return new InputHandler.clip_cut();
424 	}
425 	
426 	public Action createPasteAction()
427 	{
428 		return new InputHandler.clip_paste();
429 	}
430 
431 	public int getCaretColumn()
432 	{
433 		int pos = getCaretPosition();
434 		int line = getLineOfOffset( pos );
435 		
436 		return pos - getLineStartOffset( line );
437 	}
438 
439 	public void toNextElement()
440 	{
441 		int pos = getCaretPosition();
442 		String text = getText();
443 		
444 		while( pos < text.length() )
445 		{
446 			// find ending >
447 			if( text.charAt( pos ) == '>' && pos < text.length()-1 && 
448 					(pos > 2 && !text.substring(pos-2,pos).equals( "--" )) &&
449 					(pos > 1 && text.charAt(pos-1) != '/' ) &&
450 					text.indexOf( '/', pos ) == text.indexOf( '<', pos) +1 &&
451 					text.lastIndexOf( '/', pos ) != text.lastIndexOf( '<', pos) +1	)
452 			{
453 				setCaretPosition( pos+1 );
454 				return;
455 			}		
456 			
457 			pos++;
458 		}			
459 		
460 		getToolkit().beep();
461 	}
462 
463 	public void toPreviousElement()
464 	{
465 		int pos = getCaretPosition()-2;
466 		String text = getText();
467 		
468 		while( pos > 0 )
469 		{
470 			// find ending >
471 			if( text.charAt( pos ) == '>' && pos < text.length()-1 && 
472 					(pos > 2 && !text.substring(pos-2,pos).equals( "--" )) &&
473 					(pos > 1 && text.charAt(pos-1) != '/' ) &&
474 					text.indexOf( '/', pos ) == text.indexOf( '<', pos) +1 &&
475 					text.lastIndexOf( '/', pos ) != text.lastIndexOf( '<', pos) +1	)
476 			{
477 				setCaretPosition( pos+1 );
478 				return;
479 			}		
480 			
481 			pos--;
482 		}				
483 
484 		getToolkit().beep();
485 	}
486 }