View Javadoc
1   package org.vaadin.aceeditor;
2   
3   import java.lang.reflect.Method;
4   import java.util.Collections;
5   import java.util.Set;
6   import java.util.logging.Level;
7   import java.util.logging.Logger;
8   
9   import org.vaadin.aceeditor.client.AceAnnotation;
10  import org.vaadin.aceeditor.client.AceAnnotation.MarkerAnnotation;
11  import org.vaadin.aceeditor.client.AceAnnotation.RowAnnotation;
12  import org.vaadin.aceeditor.client.AceDoc;
13  import org.vaadin.aceeditor.client.AceEditorClientRpc;
14  import org.vaadin.aceeditor.client.AceEditorServerRpc;
15  import org.vaadin.aceeditor.client.AceEditorState;
16  import org.vaadin.aceeditor.client.AceMarker;
17  import org.vaadin.aceeditor.client.AceMarker.OnTextChange;
18  import org.vaadin.aceeditor.client.AceMarker.Type;
19  import org.vaadin.aceeditor.client.AceRange;
20  import org.vaadin.aceeditor.client.TransportDiff;
21  import org.vaadin.aceeditor.client.TransportDoc.TransportRange;
22  import org.vaadin.aceeditor.client.Util;
23  
24  import com.vaadin.annotations.JavaScript;
25  import com.vaadin.annotations.StyleSheet;
26  import com.vaadin.event.FieldEvents.BlurEvent;
27  import com.vaadin.event.FieldEvents.BlurListener;
28  import com.vaadin.v7.event.FieldEvents.BlurNotifier;
29  import com.vaadin.event.FieldEvents.FocusEvent;
30  import com.vaadin.event.FieldEvents.FocusListener;
31  import com.vaadin.v7.event.FieldEvents.FocusNotifier;
32  import com.vaadin.v7.event.FieldEvents.TextChangeEvent;
33  import com.vaadin.v7.event.FieldEvents.TextChangeListener;
34  import com.vaadin.v7.event.FieldEvents.TextChangeNotifier;
35  import com.vaadin.v7.ui.AbstractField;
36  import com.vaadin.v7.ui.AbstractTextField;
37  import com.vaadin.v7.ui.AbstractTextField.TextChangeEventMode;
38  import com.vaadin.util.ReflectTools;
39  
40  /**
41   * 
42   * AceEditor wraps an Ace code editor inside a TextField-like Vaadin component.
43   * 
44   */
45  @SuppressWarnings("serial")
46  @JavaScript({
47  	"client/js/ace/ace.js",
48  	"client/js/ace/ext-searchbox.js",
49  	"client/js/diff_match_patch.js" })
50  @StyleSheet("client/css/ace-gwt.css")
51  public class AceEditor extends AbstractField<String> implements BlurNotifier,
52  		FocusNotifier, TextChangeNotifier {
53  
54  	public static class DiffEvent extends Event {
55  		public static String EVENT_ID = "aceeditor-diff";
56  		private final ServerSideDocDiff diff;
57  
58  		public DiffEvent(AceEditor ed, ServerSideDocDiff diff) {
59  			super(ed);
60  			this.diff = diff;
61  		}
62  
63  		public ServerSideDocDiff getDiff() {
64  			return diff;
65  		}
66  	}
67  
68  	public interface DiffListener {
69  		public static final Method diffMethod = ReflectTools.findMethod(
70  				DiffListener.class, "diff", DiffEvent.class);
71  
72  		public void diff(DiffEvent e);
73  	}
74  
75  	public static class SelectionChangeEvent extends Event {
76  		public static String EVENT_ID = "aceeditor-selection";
77  		private final TextRange selection;
78  
79  		public SelectionChangeEvent(AceEditor ed) {
80  			super(ed);
81  			this.selection = ed.getSelection();
82  		}
83  
84  		public TextRange getSelection() {
85  			return selection;
86  		}
87  	}
88  
89  	public interface SelectionChangeListener {
90  		public static final Method selectionChangedMethod = ReflectTools
91  				.findMethod(SelectionChangeListener.class, "selectionChanged",
92  						SelectionChangeEvent.class);
93  
94  		public void selectionChanged(SelectionChangeEvent e);
95  	}
96  
97  	public static class TextChangeEventImpl extends TextChangeEvent {
98  		private final TextRange selection;
99  		private final String text;
100 
101 		private TextChangeEventImpl(final AceEditor ace, String text,
102 				AceRange selection) {
103 			super(ace);
104 			this.text = text;
105 			this.selection = ace.getSelection();
106 		}
107 
108 		@Override
109 		public AbstractTextField getComponent() {
110 			return (AbstractTextField) super.getComponent();
111 		}
112 
113 		@Override
114 		public int getCursorPosition() {
115 			return selection.getEnd();
116 		}
117 
118 		@Override
119 		public String getText() {
120 			return text;
121 		}
122 	}
123 
124 	private static final String DEFAULT_ACE_PATH = "http://d1n0x3qji82z53.cloudfront.net/src-min-noconflict";
125 
126 	private AceDoc doc = new AceDoc();
127 
128 	private boolean isFiringTextChangeEvent;
129 
130 	private boolean latestFocus = false;
131 	private long latestMarkerId = 0L;
132 
133 	private static final Logger logger = Logger.getLogger(AceEditor.class.getName());
134 
135 	private boolean onRoundtrip = false;
136 
137 	private AceEditorServerRpc rpc = new AceEditorServerRpc() {
138 		@Override
139 		public void changed(TransportDiff diff, TransportRange selection,
140 				boolean focused) {
141 			clientChanged(diff, selection, focused);
142 		}
143 
144 		@Override
145 		public void changedDelayed(TransportDiff diff,
146 				TransportRange selection, boolean focused) {
147 			clientChanged(diff, selection, focused);
148 		}
149 	};
150 
151 	private TextRange selection = new TextRange("", 0, 0, 0, 0);
152 	// {startPos,endPos} or {startRow,startCol,endRow,endCol}
153 	private Integer[] selectionToClient = null;
154 	private AceDoc shadow = new AceDoc();
155 
156 	{
157 		logger.setLevel(Level.WARNING);
158 	}
159 
160 	public AceEditor() {
161 		super();
162 		setWidth("300px");
163 		setHeight("200px");
164 
165 		setModePath(DEFAULT_ACE_PATH);
166 		setThemePath(DEFAULT_ACE_PATH);
167 		setWorkerPath(DEFAULT_ACE_PATH);
168 
169 		registerRpc(rpc);
170 	}
171 
172 	public void addDiffListener(DiffListener listener) {
173 		addListener(DiffEvent.EVENT_ID, DiffEvent.class, listener,
174 				DiffListener.diffMethod);
175 	}
176 
177 	@Override
178 	public void addFocusListener(FocusListener listener) {
179 		addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
180 				FocusListener.focusMethod);
181 		getState().listenToFocusChanges = true;
182 	}
183 
184 	@Override
185 	public void addBlurListener(BlurListener listener) {
186 		addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
187 				BlurListener.blurMethod);
188 		getState().listenToFocusChanges = true;
189 	}
190 
191 	@Deprecated
192 	public void addListener(BlurListener listener) {
193 		addBlurListener(listener);
194 	}
195 
196 	@Deprecated
197 	public void addListener(FocusListener listener) {
198 		addFocusListener(listener);
199 	}
200 
201 	@Deprecated
202 	public void addListener(TextChangeListener listener) {
203 		addTextChangeListener(listener);
204 	}
205 
206 	/**
207 	 * Adds an ace marker. The id of the marker must be unique within this
208 	 * editor.
209 	 * 
210 	 * @param marker
211 	 * @return marker id
212 	 */
213 	public String addMarker(AceMarker marker) {
214 		doc = doc.withAdditionalMarker(marker);
215 		markAsDirty();
216 		return marker.getMarkerId();
217 	}
218 
219 	/**
220 	 * Adds an ace marker with a generated id. The id is unique within this
221 	 * editor.
222 	 * 
223 	 * @param range
224 	 * @param cssClass
225 	 * @param type
226 	 * @param inFront
227 	 * @param onChange
228 	 * @return marker id
229 	 */
230 	public String addMarker(AceRange range, String cssClass, Type type,
231 			boolean inFront, OnTextChange onChange) {
232 		return addMarker(new AceMarker(newMarkerId(), range, cssClass, type,
233 				inFront, onChange));
234 	}
235 
236 	public void addMarkerAnnotation(AceAnnotation ann, AceMarker marker) {
237 		addMarkerAnnotation(ann, marker.getMarkerId());
238 	}
239 
240 	public void addMarkerAnnotation(AceAnnotation ann, String markerId) {
241 		doc = doc.withAdditionalMarkerAnnotation(new MarkerAnnotation(markerId,
242 				ann));
243 		markAsDirty();
244 	}
245 
246 	public void addRowAnnotation(AceAnnotation ann, int row) {
247 		doc = doc.withAdditionalRowAnnotation(new RowAnnotation(row, ann));
248 		markAsDirty();
249 	}
250 
251 	public void addSelectionChangeListener(SelectionChangeListener listener) {
252 		addListener(SelectionChangeEvent.EVENT_ID, SelectionChangeEvent.class,
253 				listener, SelectionChangeListener.selectionChangedMethod);
254 		getState().listenToSelectionChanges = true;
255 	}
256 
257 	@Override
258 	public void addTextChangeListener(TextChangeListener listener) {
259 		addListener(TextChangeListener.EVENT_ID, TextChangeEvent.class,
260 				listener, TextChangeListener.EVENT_METHOD);
261 	}
262 
263 	@Override
264 	public void beforeClientResponse(boolean initial) {
265 		super.beforeClientResponse(initial);
266 		if (initial) {
267 			getState().initialValue = doc.asTransport();
268 			shadow = doc;
269 		} else if (onRoundtrip) {
270 			ServerSideDocDiff diff = ServerSideDocDiff.diff(shadow, doc);
271 			shadow = doc;
272 			TransportDiff td = diff.asTransport();
273 			getRpcProxy(AceEditorClientRpc.class).diff(td);
274 
275 			onRoundtrip = false;
276 		} else if (true /* TODO !shadow.equals(doc) */) {
277 			getRpcProxy(AceEditorClientRpc.class).changedOnServer();
278 		}
279 
280 		if (selectionToClient != null) {
281 			// {startPos,endPos}
282 			if (selectionToClient.length == 2) {
283 				AceRange r = AceRange.fromPositions(selectionToClient[0],
284 						selectionToClient[1], doc.getText());
285 				getState().selection = r.asTransport();
286 			}
287 			// {startRow,startCol,endRow,endCol}
288 			else if (selectionToClient.length == 4) {
289 				TransportRange tr = new TransportRange(selectionToClient[0],
290 						selectionToClient[1], selectionToClient[2],
291 						selectionToClient[3]);
292 				getState().selection = tr;
293 			}
294 			selectionToClient = null;
295 		}
296 	}
297 
298 	public void clearMarkerAnnotations() {
299 		Set<MarkerAnnotation> manns = Collections.emptySet();
300 		doc = doc.withMarkerAnnotations(manns);
301 		markAsDirty();
302 	}
303 
304 	public void clearMarkers() {
305 		doc = doc.withoutMarkers();
306 		markAsDirty();
307 	}
308 
309 	public void clearRowAnnotations() {
310 		Set<RowAnnotation> ranns = Collections.emptySet();
311 		doc = doc.withRowAnnotations(ranns);
312 		markAsDirty();
313 	}
314 
315 	public int getCursorPosition() {
316 		return selection.getEnd();
317 	}
318 
319 	public AceDoc getDoc() {
320 		return doc;
321 	}
322 
323 	public TextRange getSelection() {
324 		return selection;
325 	}
326 
327 	@Override
328 	public Class<? extends String> getType() {
329 		return String.class;
330 	}
331 
332 	public void removeDiffListener(DiffListener listener) {
333 		removeListener(DiffEvent.EVENT_ID, DiffEvent.class, listener);
334 	}
335 
336 	@Override
337 	public void removeFocusListener(FocusListener listener) {
338 		removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
339 		getState().listenToFocusChanges =
340 				!getListeners(FocusEvent.class).isEmpty() ||
341 				!getListeners(BlurEvent.class).isEmpty();
342 	}
343 
344 	@Override
345 	public void removeBlurListener(BlurListener listener) {
346 		removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
347 		getState().listenToFocusChanges =
348 				!getListeners(FocusEvent.class).isEmpty() ||
349 				!getListeners(BlurEvent.class).isEmpty();
350 	}
351 
352 	@Deprecated
353 	public void removeListener(BlurListener listener) {
354 		removeBlurListener(listener);
355 	}
356 
357 	@Deprecated
358 	public void removeListener(FocusListener listener) {
359 		removeFocusListener(listener);
360 	}
361 
362 	@Deprecated
363 	public void removeListener(TextChangeListener listener) {
364 		removeTextChangeListener(listener);
365 	}
366 
367 	public void removeMarker(AceMarker marker) {
368 		removeMarker(marker.getMarkerId());
369 	}
370 
371 	public void removeMarker(String markerId) {
372 		doc = doc.withoutMarker(markerId);
373 		markAsDirty();
374 	}
375 
376 	public void removeSelectionChangeListener(SelectionChangeListener listener) {
377 		removeListener(SelectionChangeEvent.EVENT_ID,
378 				SelectionChangeEvent.class, listener);
379 		getState().listenToSelectionChanges = !getListeners(
380 				SelectionChangeEvent.class).isEmpty();
381 	}
382 
383 	@Override
384 	public void removeTextChangeListener(TextChangeListener listener) {
385 		removeListener(TextChangeListener.EVENT_ID, TextChangeEvent.class,
386 				listener);
387 	}
388 
389         public void setBasePath(String path) {
390             setAceConfig("basePath", path);
391         }
392         
393 	/**
394 	 * Sets the cursor position to be pos characters from the beginning of the
395 	 * text.
396 	 * 
397 	 * @param pos
398 	 */
399 	public void setCursorPosition(int pos) {
400 		setSelection(pos, pos);
401 	}
402 
403 	/**
404 	 * Sets the cursor on the given row and column.
405 	 * 
406 	 * @param row
407 	 *            starting from 0
408 	 * @param col
409 	 *            starting from 0
410 	 */
411 	public void setCursorRowCol(int row, int col) {
412 		setSelectionRowCol(row, col, row, col);
413 	}
414 
415 	public void setDoc(AceDoc doc) {
416 		if (this.doc.equals(doc)) {
417 			return;
418 		}
419 		this.doc = doc;
420 		boolean wasReadOnly = isReadOnly();
421 		setReadOnly(false);
422 		setValue(doc.getText());
423 		setReadOnly(wasReadOnly);
424 		markAsDirty();
425 	}
426 
427 	public void setMode(AceMode mode) {
428 		getState().mode = mode.toString();
429 	}
430 
431 	public void setMode(String mode) {
432 		getState().mode = mode;
433 	}
434 
435 	public void setModePath(String path) {
436 		setAceConfig("modePath", path);
437 	}
438 
439 	/**
440 	 * Sets the selection to be between characters [start,end).
441 	 * 
442 	 * The cursor will be at the end.
443 	 * 
444 	 * @param start
445 	 * @param end
446 	 */
447 	// TODO
448 	public void setSelection(int start, int end) {
449 		setSelectionToClient(new Integer[] { start, end });
450 		setInternalSelection(new TextRange(getInternalValue(), start, end));
451 	}
452 
453 	/**
454 	 * Sets the selection to be between the given (startRow,startCol) and
455 	 * (endRow, endCol).
456 	 * 
457 	 * The cursor will be at the end.
458 	 * 
459 	 * @param startRow
460 	 *            starting from 0
461 	 * @param startCol
462 	 *            starting from 0
463 	 * @param endRow
464 	 *            starting from 0
465 	 * @param endCol
466 	 *            starting from 0
467 	 */
468 	public void setSelectionRowCol(int startRow, int startCol, int endRow,
469 			int endCol) {
470 		setSelectionToClient(new Integer[] { startRow, startCol, endRow, endCol });
471 		setInternalSelection(new TextRange(doc.getText(), startRow, startCol,
472 				endRow, endCol));
473 	}
474 
475 	/**
476 	 * Sets the mode how the TextField triggers {@link TextChangeEvent}s.
477 	 * 
478 	 * @param inputEventMode
479 	 *            the new mode
480 	 * 
481 	 * @see TextChangeEventMode
482 	 */
483 	public void setTextChangeEventMode(TextChangeEventMode inputEventMode) {
484 		getState().changeMode = inputEventMode.toString();
485 	}
486 
487 	/**
488 	 * The text change timeout modifies how often text change events are
489 	 * communicated to the application when {@link #setTextChangeEventMode} is
490 	 * {@link TextChangeEventMode#LAZY} or {@link TextChangeEventMode#TIMEOUT}.
491 	 * 
492 	 * 
493 	 * @param timeoutMs
494 	 *            the timeout in milliseconds
495 	 */
496 	public void setTextChangeTimeout(int timeoutMs) {
497 		getState().changeTimeout = timeoutMs;
498 
499 	}
500 	
501 	/**
502 	 * Scrolls to the given row. First row is 0.
503 	 * 
504 	 */
505 	public void scrollToRow(int row) {
506 		getState().scrollToRow = row;
507 	}
508 	
509 	/**
510 	 * Scrolls the to the given position (characters from the start of the file).
511 	 * 
512 	 */
513 	public void scrollToPosition(int pos) {
514 		int[] rowcol = Util.lineColFromCursorPos(getInternalValue(), pos, 0);
515 		scrollToRow(rowcol[0]);
516 	}
517 	
518 	public void setTheme(AceTheme theme) {
519 		getState().theme = theme.toString();
520 	}
521 
522 	public void setTheme(String theme) {
523 		getState().theme = theme;
524 	}
525 
526 	public void setThemePath(String path) {
527 		setAceConfig("themePath", path);
528 	}
529 
530 	public void setUseWorker(boolean useWorker) {
531 		getState().useWorker = useWorker;
532 	}
533 
534 	public void setWordWrap(boolean ww) {
535 		getState().wordwrap = ww;
536 	}
537 
538     public void setShowGutter(boolean showGutter) {
539         getState().showGutter = showGutter;
540     }
541 
542     public boolean isShowGutter() {
543         return getState(false).showGutter;
544     }
545 
546     public void setShowPrintMargin(boolean showPrintMargin) {
547         getState().showPrintMargin = showPrintMargin;
548     }
549 
550     public boolean isShowPrintMargin() {
551         return getState(false).showPrintMargin;
552     }
553 
554     public void setHighlightActiveLine(boolean highlightActiveLine) {
555         getState().highlightActiveLine = highlightActiveLine;
556     }
557 
558     public boolean isHighlightActiveLine() {
559         return getState(false).highlightActiveLine;
560     }
561 
562 	public void setWorkerPath(String path) {
563 		setAceConfig("workerPath", path);
564 	}
565 
566 	protected void clientChanged(TransportDiff diff, TransportRange selection,
567 			boolean focused) {
568 		diffFromClient(diff);
569 		selectionFromClient(selection);
570 		if (latestFocus != focused) {
571 			latestFocus = focused;
572 			if (focused) {
573 				fireFocus();
574 			} else {
575 				fireBlur();
576 			}
577 		}
578 		
579 		clearStateFromServerToClient();
580 	}
581 	
582 	// Here we clear the selection etc. we sent earlier.
583 	// The client has already received the values,
584 	// and we must clear them at some point to not keep
585 	// setting the same selection etc. over and over.
586 	// TODO: this is a bit messy...
587 	private void clearStateFromServerToClient() {
588 		getState().selection = null;
589 		getState().scrollToRow = -1;
590 	}
591 
592 	@Override
593 	protected AceEditorState getState() {
594 		return (AceEditorState) super.getState();
595 	}
596 
597     @Override
598     protected AceEditorState getState(boolean markAsDirty) {
599         return (AceEditorState) super.getState(markAsDirty);
600     }
601 
602     @Override
603 	protected void setInternalValue(String newValue) {
604 		super.setInternalValue(newValue);
605 		doc = doc.withText(newValue);
606 	}
607 
608 	private void diffFromClient(TransportDiff d) {
609 		String previousText = doc.getText();
610 		ServerSideDocDiff diff = ServerSideDocDiff.fromTransportDiff(d);
611 		shadow = diff.applyTo(shadow);
612 		doc = diff.applyTo(doc);
613 		if (!TextUtils.equals(doc.getText(), previousText)) {
614             setValue(doc.getText(), true);
615 			fireTextChangeEvent();
616 		}
617 		if (!diff.isIdentity()) {
618 			fireDiff(diff);
619 		}
620 		onRoundtrip = true;
621 		markAsDirty();
622 	}
623 
624 	private void fireBlur() {
625 		fireEvent(new BlurEvent(this));
626 	}
627 
628 	private void fireDiff(ServerSideDocDiff diff) {
629 		fireEvent(new DiffEvent(this, diff));
630 	}
631 
632 	private void fireFocus() {
633 		fireEvent(new FocusEvent(this));
634 	}
635 
636 	private void fireSelectionChanged() {
637 		fireEvent(new SelectionChangeEvent(this));
638 	}
639 
640 	private void fireTextChangeEvent() {
641 		if (!isFiringTextChangeEvent) {
642 			isFiringTextChangeEvent = true;
643 			try {
644 				fireEvent(new TextChangeEventImpl(this, getInternalValue(),
645 						selection));
646 			} finally {
647 				isFiringTextChangeEvent = false;
648 			}
649 		}
650 	}
651 
652 	private String newMarkerId() {
653 		return "m" + (++latestMarkerId);
654 	}
655 
656 	private void selectionFromClient(TransportRange sel) {
657 		setInternalSelection(new TextRange(doc.getText(),
658 				AceRange.fromTransport(sel)));
659 		fireSelectionChanged();
660 	}
661 
662 	private void setAceConfig(String key, String value) {
663 		getState().config.put(key, value);
664 	}
665 
666 	private void setInternalSelection(TextRange selection) {
667 		this.selection = selection;
668 		getState().selection = selection.asTransport();
669 	}
670 
671 	private void setSelectionToClient(Integer[] stc) {
672 		selectionToClient = stc;
673 		markAsDirty();
674 	}
675 
676 }