package org.swellrt.client.editor; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.swellrt.api.BrowserSession; import org.swellrt.client.editor.TextEditorDefinitions.ParagraphAnnotation; import org.swellrt.model.generic.TextType; import org.swellrt.model.shared.ModelUtils; import org.waveprotocol.wave.client.account.ProfileManager; import org.waveprotocol.wave.client.account.impl.ProfileManagerImpl; import org.waveprotocol.wave.client.common.util.JsoStringMap; import org.waveprotocol.wave.client.common.util.JsoStringSet; import org.waveprotocol.wave.client.common.util.LogicalPanel; import org.waveprotocol.wave.client.doodad.annotation.AnnotationHandler; import org.waveprotocol.wave.client.doodad.annotation.jso.JsoAnnotation; import org.waveprotocol.wave.client.doodad.annotation.jso.JsoAnnotationController; import org.waveprotocol.wave.client.doodad.annotation.jso.JsoRange; import org.waveprotocol.wave.client.doodad.annotation.jso.JsoParagraphAnnotation; import org.waveprotocol.wave.client.doodad.diff.DiffAnnotationHandler; import org.waveprotocol.wave.client.doodad.diff.DiffDeleteRenderer; import org.waveprotocol.wave.client.doodad.link.LinkAnnotationHandler; import org.waveprotocol.wave.client.doodad.link.LinkAnnotationHandler.LinkAttributeAugmenter; import org.waveprotocol.wave.client.doodad.selection.SelectionAnnotationHandler; import org.waveprotocol.wave.client.doodad.selection.SelectionAnnotationHandler.CaretListener; import org.waveprotocol.wave.client.doodad.selection.SelectionExtractor; import org.waveprotocol.wave.client.doodad.widget.WidgetDoodad; import org.waveprotocol.wave.client.doodad.widget.jso.JsoWidget; import org.waveprotocol.wave.client.doodad.widget.jso.JsoWidgetController; import org.waveprotocol.wave.client.editor.Editor; import org.waveprotocol.wave.client.editor.EditorSettings; import org.waveprotocol.wave.client.editor.EditorStaticDeps; import org.waveprotocol.wave.client.editor.EditorUpdateEvent; import org.waveprotocol.wave.client.editor.EditorUpdateEvent.EditorUpdateListener; import org.waveprotocol.wave.client.editor.Editors; import org.waveprotocol.wave.client.editor.content.CMutableDocument; import org.waveprotocol.wave.client.editor.content.ContentDocument; import org.waveprotocol.wave.client.editor.content.ContentElement; import org.waveprotocol.wave.client.editor.content.ContentNode; import org.waveprotocol.wave.client.editor.content.Registries; import org.waveprotocol.wave.client.editor.content.misc.AnnotationPaint; import org.waveprotocol.wave.client.editor.content.misc.AnnotationPaint.EventHandler; import org.waveprotocol.wave.client.editor.content.misc.AnnotationPaint.MutationHandler; import org.waveprotocol.wave.client.editor.content.misc.StyleAnnotationHandler; import org.waveprotocol.wave.client.editor.content.paragraph.LineRendering; import org.waveprotocol.wave.client.editor.content.paragraph.Paragraph; import org.waveprotocol.wave.client.editor.content.paragraph.Paragraph.LineStyle; import org.waveprotocol.wave.client.editor.content.paragraph.ParagraphBehaviour; import org.waveprotocol.wave.client.editor.keys.KeyBindingRegistry; import org.waveprotocol.wave.client.editor.util.EditorAnnotationUtil; import org.waveprotocol.wave.client.scheduler.SchedulerInstance; import org.waveprotocol.wave.client.scheduler.TimerService; import org.waveprotocol.wave.client.wave.InteractiveDocument; import org.waveprotocol.wave.client.wave.RegistriesHolder; import org.waveprotocol.wave.client.wave.WaveDocuments; import org.waveprotocol.wave.client.widget.popup.PopupChrome; import org.waveprotocol.wave.client.widget.popup.PopupChromeProvider; import org.waveprotocol.wave.client.widget.popup.simple.Popup; import org.waveprotocol.wave.common.logging.AbstractLogger; import org.waveprotocol.wave.common.logging.AbstractLogger.Level; import org.waveprotocol.wave.common.logging.LogSink; import org.waveprotocol.wave.model.conversation.AnnotationConstants; import org.waveprotocol.wave.model.document.AnnotationInterval; import org.waveprotocol.wave.model.document.util.DocHelper; import org.waveprotocol.wave.model.document.util.LineContainers; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.document.util.Range; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.util.CollectionUtils; import org.waveprotocol.wave.model.util.Preconditions; import org.waveprotocol.wave.model.util.ReadableStringMap.ProcV; import org.waveprotocol.wave.model.util.ReadableStringSet.Proc; import org.waveprotocol.wave.model.util.StringMap; import org.waveprotocol.wave.model.wave.ParticipantId; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.user.client.Event; /** * A wrapper of the original Wave {@link Editor} to be integrated in the SwellRT * Web API. Users can edit TextType instances in this editor. * * @author pablojan * */ public class TextEditor implements EditorUpdateListener { public interface Configurator { /** * The gateway to get UI-versions of Blips. Registry is GWT related, so must * be injected in the Editor by the JS API. Model's classes have to ignore it. */ public WaveDocuments<? extends InteractiveDocument> getDocumentRegistry(); /** * @return a profile manager instance */ public ProfileManager getProfileManager(); /** * @return a full session id including tab/window info. */ public String getSessionId(); /** * * @return current logged in user id */ public ParticipantId getParticipantId(); } public static class JSLogSink extends LogSink { private native void console(String msg) /*-{ console.log(msg); }-*/; @Override public void log(Level level, String message) { console("[" + level.name() + "] " + message); } @Override public void lazyLog(Level level, Object... messages) { for (Object o : messages) { log(level, o.toString()); } } } public static class CustomLogger extends AbstractLogger { public CustomLogger(LogSink sink) { super(sink); } @Override public boolean isModuleEnabled() { return enableEditorLog(); } @Override protected boolean shouldLog(Level level) { return enableEditorLog(); } } private static native boolean enableEditorLog() /*-{ return $wnd.__debugEditor && ($wnd.__debugEditor == true); }-*/; private static final String TOPLEVEL_CONTAINER_TAGNAME = "body"; static { Editors.initRootRegistries(); LineContainers.setTopLevelContainerTagname(TOPLEVEL_CONTAINER_TAGNAME); if (enableEditorLog()) EditorStaticDeps.logger = new CustomLogger(new JSLogSink()); } private static final EditorSettings EDITOR_SETTINGS = new EditorSettings() .setHasDebugDialog(true).setUndoEnabled(true).setUseFancyCursorBias(true) .setUseSemanticCopyPaste(false).setUseWhitelistInEditor(false) .setUseWebkitCompositionEvents(true); // Wave Editor specific private final Registries registries = RegistriesHolder.get(); private final KeyBindingRegistry KEY_REGISTRY = new KeyBindingRegistry(); private final LogicalPanel.Impl editorPanel; private LogicalPanel.Impl docPanel; private ContentDocument doc; private SelectionExtractor selectionExtractor; /** The actual implementation of the editor */ private Editor editor; /** Listener for editor events */ private TextEditorListener listener; private WaveId containerWaveId; private boolean shouldFireEvents = false; private TimerService clock; /** * Registry of JavaScript controllers for widgets */ private final StringMap<JsoWidgetController> widgetRegistry; /** * Registry of JavaScript controllers for annotations */ private final StringMap<JsoAnnotationController> annotationRegistry; { EditorStaticDeps.setPopupProvider(Popup.LIGHTWEIGHT_POPUP_PROVIDER); EditorStaticDeps.setPopupChromeProvider(new PopupChromeProvider() { public PopupChrome createPopupChrome() { return null; } }); } public static TextEditor create(Element parent, StringMap<JsoWidgetController> widgetControllers, StringMap<JsoAnnotationController> annotationControllers) { Preconditions.checkNotNull(parent, "Element to hook editor canvas doesn't exist"); TextEditor editor = new TextEditor(parent, widgetControllers, annotationControllers); return editor; } protected TextEditor(final Element containerElement, StringMap<JsoWidgetController> widgetControllers, StringMap<JsoAnnotationController> annotationControllers) { this.editorPanel = new LogicalPanel.Impl() { { setElement(containerElement); } }; this.widgetRegistry = widgetControllers; this.annotationRegistry = annotationControllers; this.clock = SchedulerInstance.getLowPriorityTimer(); } /** * This is a nasty method to pass a reference of editor's js facade * to widget and annotation controllers. * * So there is a circular reference between TextEditor and TextEditorJS. * * @param editorJsFacade editor's pure JavaScript facade */ public void initialize(JavaScriptObject editorJsFacade) { registerDoodads(editorJsFacade); registerAnnotations(editorJsFacade); } protected void registerDoodads(final JavaScriptObject editorJsFacade) { // TOPLEVEL_CONTAINER_TAGNAME LineRendering.registerContainer(TOPLEVEL_CONTAINER_TAGNAME, registries.getElementHandlerRegistry()); StyleAnnotationHandler.register(registries); // Listen for Diff annotations to paint new content or to insert a // delete-content tag // to be rendered by the DiffDeleteRendere DiffAnnotationHandler.register(registries.getAnnotationHandlerRegistry(), registries.getPaintRegistry()); DiffDeleteRenderer.register(registries.getElementHandlerRegistry()); // // Reuse existing link annotation handler, but also support external // controller to get notified on mutation or input events // LinkAnnotationHandler.register(registries, new LinkAttributeAugmenter() { @Override public Map<String, String> augment(Map<String, Object> annotations, boolean isEditing, Map<String, String> current) { return current; } }); // Set reference to js editor's facade to controllers. widgetRegistry.each(new ProcV<JsoWidgetController>() { @Override public void apply(String key, JsoWidgetController value) { value.setEditorJsFacade(editorJsFacade); } }); WidgetDoodad.register(registries.getElementHandlerRegistry(), widgetRegistry); } protected void registerAnnotations(final JavaScriptObject editorJsFacade) { // Configure annotations. // For shake of encapsulation, we don't expose native JS type // JsoAnntationController to inner editor classes. annotationRegistry.each(new ProcV<JsoAnnotationController>() { @Override public void apply(String key, JsoAnnotationController controller) { // sets the circular reference to the editor's js facade controller.setEditorJsFacade(editorJsFacade); if (key.contains(AnnotationConstants.LINK_PREFIX)) { // Link annotations are actually registered in registerDoodads() method // here only handlers are registered AnnotationPaint.registerEventHandler(AnnotationConstants.LINK_PREFIX, new EventHandler() { @Override public void onEvent(ContentElement node, Event event) { if (controller != null && shouldFireEvents) controller.onEvent(JsoRange.Builder.create(node.getMutableDoc()).range(node) .annotation(AnnotationConstants.LINK_PREFIX, null).build(), event); } }); AnnotationPaint.setMutationHandler(AnnotationConstants.LINK_PREFIX, new MutationHandler() { @Override public void onMutation(ContentElement node) { if (controller != null && shouldFireEvents) controller.onChange(JsoRange.Builder.create(node.getMutableDoc()) .range(node).annotation(AnnotationConstants.LINK_PREFIX, null).build()); } @Override public void onAdded(ContentElement node) { if (controller != null && shouldFireEvents) controller.onAdd(JsoRange.Builder.create(node.getMutableDoc()) .range(node).annotation(AnnotationConstants.LINK_PREFIX, null).build()); } @Override public void onRemoved(ContentElement node) { if (controller != null && shouldFireEvents) controller.onRemove(JsoRange.Builder.create(node.getMutableDoc()) .range(node).annotation(AnnotationConstants.LINK_PREFIX, null).build()); } }); } else if (TextEditorDefinitions.isParagraphAnnotation(key)) { if (key.equals(ParagraphAnnotation.HEADER.toString())) { Paragraph.registerEventHandler(ParagraphBehaviour.HEADING, new Paragraph.EventHandler() { @Override public void onEvent(ContentElement node, Event event) { if (controller != null && shouldFireEvents) controller.onEvent( JsoRange.Builder.create(node.getMutableDoc()).range(node) .annotation(key, node.getAttribute(Paragraph.SUBTYPE_ATTR)).build(), event); } }); Paragraph.registerMutationHandler(ParagraphBehaviour.HEADING, new Paragraph.MutationHandler() { @Override public void onMutation(ContentElement node) { if (controller != null && shouldFireEvents) controller .onChange(JsoRange.Builder.create(node.getMutableDoc()).range(node) .annotation(key, node.getAttribute(Paragraph.SUBTYPE_ATTR)).build()); } @Override public void onAdded(ContentElement node) { if (controller != null && shouldFireEvents) controller .onAdd(JsoRange.Builder.create(node.getMutableDoc()).range(node) .annotation(key, node.getAttribute(Paragraph.SUBTYPE_ATTR)).build()); } @Override public void onRemoved(ContentElement node) { if (controller != null && shouldFireEvents) controller .onRemove(JsoRange.Builder.create(node.getMutableDoc()).range(node) .annotation(key, node.getAttribute(Paragraph.SUBTYPE_ATTR)).build()); } }); } } else if (TextEditorDefinitions.isStyleAnnotation(key)) { // Nothing to do with style annotations } else { // Register custom annotations AnnotationHandler.register(registries, key, controller, new AnnotationHandler.Activator() { @Override public boolean shouldFireEvent() { return shouldFireEvents; } }); } } }); } public void setListener(TextEditorListener listener) { this.listener = listener; } public WaveId getWaveId() { return containerWaveId; } /** * Start an editing session. * * @param text the TextType instance to edit * @param a handler to listen on caret events * @param configurator editor dependencies not related with text * */ public void edit(TextType text, CaretListener caretListener, Configurator configurator) { Preconditions.checkNotNull(text, "Text object can't be null"); Preconditions.checkNotNull(configurator, "Editor configurator can't be null"); shouldFireEvents = false; if (!isClean()) cleanUp(); // Place here selection extractor to ensure session id and user id are refreshed //SelectionAnnotationHandler.register(registries, configurator.getSessionId(), configurator.getProfileManager(), caretListener); selectionExtractor = null; // new SelectionExtractor(clock, configurator.getParticipantId().getAddress(), configurator.getSessionId()); doc = getContentDocument(text, configurator.getDocumentRegistry()); Preconditions.checkArgument(doc != null, "Can't edit an unattached TextType"); doc.setRegistries(registries); this.docPanel = new LogicalPanel.Impl() { { setElement(Document.get().createDivElement()); } }; doc.setInteractive(docPanel); // Append the doc panel to the provided container panel editorPanel.getElement().appendChild( doc.getFullContentView().getDocumentElement().getImplNodelet()); if (editor == null) { editor = Editors.attachTo(doc); editor.init(null, KEY_REGISTRY, EditorSettings.DEFAULT); } else { // Reuse existing editor. if (editor.hasDocument()) { editor.removeContentAndUnrender(); editor.reset(); } editor.setContent(doc); editor.init(null, KEY_REGISTRY, EDITOR_SETTINGS); } editor.addUpdateListener(this); // editor.addKeySignalListener(parentPanel); editor.setEditing(true); editor.focus(true); selectionExtractor.start(editor); containerWaveId = text.getModel().getWaveId(); shouldFireEvents = true; } private ContentDocument getContentDocument(TextType text, WaveDocuments<? extends InteractiveDocument> documentRegistry) { Preconditions.checkArgument(text != null, "Unable to get ContentDocument from null TextType"); Preconditions.checkArgument(documentRegistry != null, "Unable to get ContentDocument from null DocumentRegistry"); return documentRegistry.getBlipDocument(ModelUtils.serialize(text.getModel().getWaveletId()), text.getDocumentId()).getDocument(); } public void setEditing(boolean isEditing) { if (editor != null && editor.hasDocument()) { if (editor.isEditing() != isEditing) editor.setEditing(isEditing); } } public void cleanUp() { if (editor != null && doc != null) { if (selectionExtractor != null) { selectionExtractor.stop(editor); selectionExtractor = null; } editor.removeUpdateListener(this); editor.removeContentAndUnrender(); editor.reset(); doc = null; containerWaveId = null; } } public boolean isClean() { return doc == null; } public void toggleDebug() { editor.debugToggleDebugDialog(); } /** * Insert or append a Widget at the current cursor position or at the end iff the type * is registered. * * @param type * @param state * * @return The widget as DOM element */ public JsoWidget addWidget(String type, String state) { Point<ContentNode> currentPoint = null; if (editor.getSelectionHelper().getOrderedSelectionPoints() != null) currentPoint = editor.getSelectionHelper().getOrderedSelectionPoints().getFirst(); ContentElement w = WidgetDoodad.addWidget(editor.getDocument(), currentPoint, type, state); return JsoWidget.create(w.getImplNodelet(), w); } /** * Get the widget associated with the DOM element * * @param domElement the widget element or a descendant */ public JsoWidget getWidget(Element domElement) { return WidgetDoodad.getWidget(editor.getDocument(), domElement); } protected boolean isValidAnnotationKey(String key) { return (TextEditorDefinitions.isParagraphAnnotation(key) || TextEditorDefinitions.isStyleAnnotation(key) || annotationRegistry.containsKey(key) || key.equals("link")); } /** * Calculate the value of all annotations in the provided range. * If annotation is not set in the range, the result it will be a null value. * * @return a native JS object having a property for each annotation. */ protected JsoStringMap<String> getAllAnnotationsInRange(final Range range) { // Map to contain the current state of each annotation final JsoStringMap<String> annotationMap = JsoStringMap.<String>create(); if (range != null) { // get state of caret annotations TextEditorDefinitions.CARET_ANNOTATIONS.each(new Proc() { @Override public void apply(String annotationName) { String annotationValue = EditorAnnotationUtil.getAnnotationOverRangeIfFull(editor.getDocument(), editor.getCaretAnnotations(), annotationName, range.getStart(), range.getEnd()); annotationMap.put(annotationName, annotationValue); } }); // get all custom annotations annotationRegistry.keySet().each(new Proc() { @Override public void apply(String annotationName) { String annotationValue = EditorAnnotationUtil.getAnnotationOverRangeIfFull(editor.getDocument(), editor.getCaretAnnotations(), annotationName, range.getStart(), range.getEnd()); annotationMap.put(annotationName, annotationValue); } }); // get paragraph annotations TextEditorDefinitions.PARAGRAPH_ANNOTATIONS.each(new Proc() { @Override public void apply(String annotationName) { Collection<Entry<String, LineStyle>> styles = TextEditorDefinitions.ParagraphAnnotation.fromString(annotationName).values .entrySet(); String annotationValue = null; for (Entry<String, LineStyle> s : styles) { if (Paragraph.appliesEntirely(editor.getDocument(), range.getStart(), range.getEnd(), s.getValue())) { annotationValue = s.getKey(); break; } } annotationMap.put(annotationName, annotationValue); } }); } return annotationMap; } /** * Generate a map of all accepted annotations with the default value as null * * @return */ protected JsoStringMap<String> getAnnotationsByDefault() { // Map to contain the current state of each annotation final JsoStringMap<String> annotationMap = JsoStringMap.<String>create(); // caret annotations TextEditorDefinitions.CARET_ANNOTATIONS.each(new Proc() { @Override public void apply(String annotationName) { annotationMap.put(annotationName, null); } }); // get all custom annotations annotationRegistry.keySet().each(new Proc() { @Override public void apply(String annotationName) { annotationMap.put(annotationName, null); } }); // get paragraph annotations TextEditorDefinitions.PARAGRAPH_ANNOTATIONS.each(new Proc() { @Override public void apply(String annotationName) { annotationMap.put(annotationName, TextEditorDefinitions.ParagraphAnnotation.fromString(annotationName).defaultValue); } }); return annotationMap; } /** * Return the list of all ranges in the document having the annotation. * * @param key the annotation name to list. */ public JsArray<JsoAnnotation> getAnnotationSet(String key) { Preconditions.checkArgument(isValidAnnotationKey(key), "Invalid annotation key"); @SuppressWarnings("unchecked") JsArray<JsoAnnotation> ranges = (JsArray<JsoAnnotation>) JsArray.createArray(); if (TextEditorDefinitions.isParagraphAnnotation(key)) { ParagraphAnnotation pa = ParagraphAnnotation.fromString(key); Paragraph.traverseDoc(editor.getDocument(), new ContentElement.RangedAction() { @Override public void execute(ContentElement e, Range r) { for (Entry<String, LineStyle> ls : pa.getLineStyles().entrySet()) { if (ls.getValue().isApplied(e)) { // TODO(pablojan) this might not work in all situations. I need look into line/paragraph rendering Element implNodelet = (Element) e.getImplNodeletRightwards(); ranges.push(JsoParagraphAnnotation.create(editor.getDocument(), r.getStart(), r.getEnd(), key, ls.getKey(), implNodelet)); } } } }); } else { JsoStringSet annotationNames = JsoStringSet.create(); annotationNames.add(key); for (AnnotationInterval<String> interval : editor.getDocument().annotationIntervals(0, editor.getDocument().size(), annotationNames)) { // TODO(pablojan) checking for intervals with actual value for // the annotation, if not, void intervals are returned if (interval.annotations().get(key) != null) ranges.push(JsoAnnotation.create(editor, new Range(interval.start(), interval.end()), key)); } } return ranges; } public JsoAnnotation getAnnotationInRange(JsoRange editorRange, String key) { Preconditions.checkNotNull(editorRange, "Range can't be null"); Preconditions.checkArgument(isValidAnnotationKey(key), "Invalid annotation key"); if (TextEditorDefinitions.isParagraphAnnotation(key)) { for(Entry<String, LineStyle> ls: ParagraphAnnotation.fromString(key).getLineStyles().entrySet()) { if (Paragraph.appliesEntirely(editor.getDocument(), editorRange.start(), editorRange.end(), ls.getValue())) { Point<ContentNode> point = editor.getDocument().locate(editorRange.start()); ContentNode lineNode = LineContainers.getRelatedLineElement(editor.getDocument(), point); return JsoParagraphAnnotation.create(editor.getDocument(), editorRange.start(), editorRange.end(), key, ls.getKey(), lineNode.asElement().getImplNodelet()); } } } else { Range range = EditorAnnotationUtil.getEncompassingAnnotationRange(editor.getDocument(), key, editorRange.start()); if (range == null) return null; return JsoAnnotation.create(editor, range, key); } return null; } /** * Toggle a paragraph annotation in the provided range. To unset the annotation use null value. * * @param annotationName * @param annotationValue * @param start * @param end */ protected void setParagraphAnnotation(String annotationName, String annotationValue, int start, int end) { Preconditions.checkArgument(TextEditorDefinitions.isParagraphAnnotation(annotationName), "Invalid paragraph annotation"); ParagraphAnnotation annotation = TextEditorDefinitions.ParagraphAnnotation.fromString(annotationName); final LineStyle style = annotation.getLineStyleForValue(annotationValue); if (annotationName.equals(ParagraphAnnotation.HEADER.toString())) { //Date now = new Date(); //style.setId(String.valueOf(now.getTime()) + String.valueOf(start) + String.valueOf(end)); } final boolean isOn = annotationValue != null && !annotationValue.isEmpty(); editor.undoableSequence(new Runnable() { @Override public void run() { Paragraph.apply(editor.getDocument(), start, end, style, isOn); } }); } /** * Set any type of annotation in the current selected text or caret. * * @param key * @param value */ public JsoAnnotation setAnnotation(String key, String value) { Preconditions.checkArgument(isValidAnnotationKey(key), "Unknown annotation key"); final Range range = editor.getSelectionHelper().getOrderedSelectionRange(); if (range == null) return null; if (TextEditorDefinitions.isParagraphAnnotation(key)) { setParagraphAnnotation(key, value, range.getStart(), range.getEnd()); } else { EditorAnnotationUtil.setAnnotationOverSelection(editor, key, value); } return JsoAnnotation.create(editor, range, key); } /** * Set any type of annotation in a range of text. * * @param range * @param key * @param value */ public JsoAnnotation setAnnotationInRange(JsoRange range, String key, String value) { Preconditions.checkArgument(isValidAnnotationKey(key), "Unknown annotation key"); Preconditions.checkArgument(range != null && range.start() <= range.end(), "Invalid range object"); if (TextEditorDefinitions.isParagraphAnnotation(key)) { setParagraphAnnotation(key, value, range.start(), range.end()); } else { EditorAnnotationUtil.setAnnotationOverRange(editor.getDocument(), editor.getCaretAnnotations(), key, value, range.start(), range.end()); } return JsoAnnotation.create(editor, new Range(range.start(), range.end()), key); } /** * Clear the annotation in the current selection or caret position. * * @param keyPrefix */ public void clearAnnotation(String keyPrefix) { Range r = editor.getSelectionHelper().getOrderedSelectionRange(); if (r == null) { return; } clearAnnotationInRange(JsoRange.Builder.create(editor.getDocument()).range(r).build(), keyPrefix); } /** * Clear all annotations in the range starting with the prefix * * @param range the range in the doc */ public void clearAnnotationInRange(JsoRange range, String keyPrefix) { Preconditions.checkNotNull(keyPrefix, "Annotation key or prefix can't be null"); Preconditions.checkNotNull(range, "Range can't be null"); StringMap<String> annotations = range.getAnnotations(); if (annotations.isEmpty()) { annotations = getAllAnnotationsInRange(new Range(range.start(), range.end())); } List<String> textAnnotations = new ArrayList<String>(); annotations.each(new ProcV<String>() { @Override public void apply(String key, String value) { if (key.startsWith(keyPrefix)) { if (TextEditorDefinitions.isParagraphAnnotation(key)) setParagraphAnnotation(key, null, range.start(), range.end()); else textAnnotations.add(key); } } }); if (!textAnnotations.isEmpty()) { String[] anotArray = (String[]) textAnnotations.toArray(new String[]{}); EditorAnnotationUtil.clearAnnotationsOverRange(editor.getDocument(), editor.getCaretAnnotations(), anotArray, range.start(), range.end()); } } /** * Insert o replace text over range in the document * * @param range * @param text */ public JsoRange setText(JsoRange range, String text) { Preconditions.checkNotNull(range, "Range can't be null"); CMutableDocument doc = editor.getDocument(); if (range.start() == range.end()) { doc.insertText(range.start(), text); } else if (range.start() > 0 && range.start() < range.end() && range.end() < editor.getDocument().size()) { doc.beginMutationGroup(); doc.deleteRange(range.start(), range.end()); doc.insertText(range.start(), text); doc.endMutationGroup(); } else { Preconditions.checkArgument(false, "Range is not correct"); } return JsoRange.Builder.create(doc).range(range.start(), range.start()+text.length(), text.length()).build(); } /** * Delete text in a range * * @param range * @return */ public void deleteText(JsoRange range) { Preconditions.checkNotNull(range, "Range can't be null"); CMutableDocument doc = editor.getDocument(); doc.deleteRange(range.start(), range.end()); } public String getText(JsoRange range) { Preconditions.checkNotNull(range, "Range can't be null"); CMutableDocument doc = editor.getDocument(); Preconditions.checkArgument(range.start() >= 0 && range.start() <= range.end() && range.end() < doc.size(), "Range is not correct"); return DocHelper.getText(doc, range.start(), range.end()); } /** * Gets the current selection. See {@link JsoRange} for methods to * update the document's selection. * * Includes annotations. * * @return */ public JsoRange getSelection() { Range r = editor.getSelectionHelper().getOrderedSelectionRange(); if (r == null) { return null; } return JsoRange.Builder.create(editor.getDocument()).range(r) .annotations(getAllAnnotationsInRange(r)).build(); } @Override public void onUpdate(final EditorUpdateEvent event) { if (event.selectionLocationChanged()) { Range range = editor.getSelectionHelper().getOrderedSelectionRange(); JsoRange.Builder editorRangeBuilder = JsoRange.Builder.create(editor.getDocument()); if (range != null) { editorRangeBuilder.range(range) .annotations(getAllAnnotationsInRange(range)).build(); } else { editorRangeBuilder.annotations(getAnnotationsByDefault()); } if (listener != null) listener.onSelectionChange(editorRangeBuilder.build()); } } }