/** * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.waveprotocol.wave.client.editor.harness; import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.GWT.UncaughtExceptionHandler; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.logical.shared.SelectionEvent; import com.google.gwt.event.logical.shared.SelectionHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.InlineLabel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.MultiWordSuggestOracle; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.SuggestBox; import com.google.gwt.user.client.ui.SuggestOracle; import com.google.gwt.user.client.ui.TextArea; import com.google.gwt.user.client.ui.Widget; import org.waveprotocol.wave.client.common.util.KeySignalListener; import org.waveprotocol.wave.client.common.util.LogicalPanel; import org.waveprotocol.wave.client.common.util.SignalEvent; import org.waveprotocol.wave.client.debug.logger.DomLogger; import org.waveprotocol.wave.client.debug.logger.LogLevel; import org.waveprotocol.wave.client.debug.logger.LoggerListener; import org.waveprotocol.wave.client.doodad.diff.DiffAnnotationHandler; import org.waveprotocol.wave.client.doodad.link.LinkAnnotationHandler; import org.waveprotocol.wave.client.doodad.link.LinkAnnotationHandler.LinkAttributeAugmenter; import org.waveprotocol.wave.client.editor.Editor; import org.waveprotocol.wave.client.editor.EditorImpl; 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.ElementHandlerRegistry; import org.waveprotocol.wave.client.editor.content.ContentDocument; import org.waveprotocol.wave.client.editor.content.ContentDocument.Level; import org.waveprotocol.wave.client.editor.content.ContentNode; import org.waveprotocol.wave.client.editor.content.Registries; import org.waveprotocol.wave.client.editor.content.misc.StyleAnnotationHandler; import org.waveprotocol.wave.client.editor.content.paragraph.LineRendering; import org.waveprotocol.wave.client.editor.keys.KeyBindingRegistry; import org.waveprotocol.wave.client.editor.util.EditorDocFormatter; import org.waveprotocol.wave.client.editor.webdriver.EditorWebDriverUtil; import org.waveprotocol.wave.client.scheduler.ScheduleCommand; import org.waveprotocol.wave.client.scheduler.Scheduler; import org.waveprotocol.wave.client.scheduler.Scheduler.Task; 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.LoggerBundle; import org.waveprotocol.wave.model.document.operation.DocInitialization; import org.waveprotocol.wave.model.document.operation.DocOp; import org.waveprotocol.wave.model.document.operation.automaton.DocOpAutomaton.ViolationCollector; import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema; import org.waveprotocol.wave.model.document.operation.impl.DocOpValidator; import org.waveprotocol.wave.model.document.parser.XmlParseException; import org.waveprotocol.wave.model.document.util.DocIterate; import org.waveprotocol.wave.model.document.util.DocProviders; import org.waveprotocol.wave.model.document.util.LineContainers; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.operation.SilentOperationSink; import org.waveprotocol.wave.model.schema.conversation.ConversationSchemas; import java.util.ArrayList; import java.util.Map; /** * */ public class EditorHarness extends Composite implements KeySignalListener { private static final String TOPLEVEL_CONTAINER_TAGNAME = "body"; static { LineContainers.setTopLevelContainerTagname(TOPLEVEL_CONTAINER_TAGNAME); } /** * Debug logger for the test module */ LoggerBundle logger = new DomLogger("test"); ContentDocument doc1, doc2; /** * The editors we are testing */ Editor editor1; EditorBundle editorBundle1; /** * The editors we are testing */ Editor editor2; EditorBundle editorBundle2; LogicalPanel.Impl displayDoc1 = new LogicalPanel.Impl() { { setElement(Document.get().createDivElement()); getElement().getStyle().setProperty("border", "1px dashed silver"); } }; /** * Flag is ops coming out from editor1 should go to editor2 */ public boolean sendOps = true; /** * Flag if harness should operate quitly */ boolean quiet = false; /** * Output widgets for editor1's content */ private HTML prettyContent1; /** * Output widget for editor1's html content */ private HTML prettyHtml1; /** * Output widgets for editor2's content */ private HTML prettyContent2; /** * Output widget for editor2's html content */ private HTML prettyHtml2; /** * Oracle for contentBox */ MultiWordSuggestOracle contentOracle; /** * Input content box. Note: Only one of these regions is used. */ private TextArea contentBox; private SuggestBox contentSuggestBox; /** * Input content box */ private final Button setContentButton = new Button("Set content:", new ClickHandler() { public void onClick(ClickEvent e) { setFromContentBox(); } }); /** * Output widget for last operation */ private final HTML operationOutput = new HTML(); /** * Report errors here * * Error is triggered by error log messages. * Fatal is triggered by fatal log messages and uncaught exceptions. */ private HTML error = null; private HTML fatal = null; /** * Our log div */ private HTML log = null; private final Registries testEditorRegistries = Editor.ROOT_REGISTRIES; /** * Checkbox that toggles editing/display */ private CheckBox toggleEditCheck1; private CheckBox toggleEditCheck2; private CheckBox createEditToggleCheckBox(final Editor editor) { CheckBox check = new CheckBox("Toggle edit"); check.addValueChangeHandler(new ValueChangeHandler<Boolean>() { @Override public void onValueChange(ValueChangeEvent<Boolean> event) { setEditing(editor, event.getValue()); } }); return check; } private void setEditing(Editor editor, boolean isEditing) { editor.setEditing(isEditing); if (editor.isEditing()) { editor.focus(true); } } /** * Clear log button */ private final Button clearLogButton = new Button("Clear log", new ClickHandler() { public void onClick(ClickEvent e) { if (LogLevel.showDebug() && log != null) { log.setHTML(""); error.setText(""); fatal.setText(""); editor1.focus(true); } } }); /** * Quiet button */ private final Button quietButton = new Button("Quiet (app runs faster)", new ClickHandler() { public void onClick(ClickEvent e) { littleLogging(); quiet = true; editor1.focus(true); } }); /** * Loud button */ private final Button loudButton = new Button("Loud (more info)", new ClickHandler() { public void onClick(ClickEvent e) { lottaLogging(); quiet = false; editor1.focus(true); } }); private final CheckBox createEditor2DocDetached = new CheckBox("Create detached"); /** * Sets content in both editors * @param content */ private void syncEditors(String content) { DocInitialization op; try { op = DocProviders.POJO.parse(content).asOperation(); } catch (IllegalArgumentException e) { if (e.getCause() instanceof XmlParseException) { logger.error().log("Ill-formed XML string ", e.getCause()); } else { logger.error().log("Error", e); } return; } DocumentSchema schema = getSchema(); ViolationCollector vc = new ViolationCollector(); if (!DocOpValidator.validate(vc, schema, op).isValid()) { logger.error().log("That content does not conform to the schema", vc); return; } boolean useHack = createEditor2DocDetached.getValue(); // EDITOR1: Pojo -> editor mode double start = Duration.currentTimeMillis(); doc1 = new ContentDocument(testEditorRegistries, op, schema); double middle = Duration.currentTimeMillis(); editor1.setContent(doc1); double end = Duration.currentTimeMillis(); logger.log(AbstractLogger.Level.TRACE, "Set content1 took: " + (end - start) + " (Pojo creation: " + (middle - start) + ", rendering: " + (end - middle)); // EDITOR2: Build document in edit mode start = Duration.currentTimeMillis(); doc2 = new ContentDocument(schema); doc2.setRegistries(testEditorRegistries); editor2.setContent(doc2); // HACK to ensure it's created non-attached to dom, to ensure fair speed comparisons double start2, end2; if (useHack) { Element docDiv = doc2.getFullContentView().getDocumentElement().getImplNodelet(); Element editorDiv = editor2.getWidget().getElement(); assert docDiv.getParentElement() == editorDiv; docDiv.removeFromParent(); start2 = Duration.currentTimeMillis(); doc2.consume(op); end2 = Duration.currentTimeMillis(); editorDiv.appendChild(docDiv); } else { start2 = Duration.currentTimeMillis(); doc2.consume(op); end2 = Duration.currentTimeMillis(); } end = Duration.currentTimeMillis(); logger.log(AbstractLogger.Level.TRACE, "Set content2 took: " + (end - start) + " Just the op: " + (end2 - start2) + " Op + appendChild: " + (end - start2)); documentModeSelect.setSelectedIndex(doc1.getLevel().ordinal()); outputBothEditorStates(); } /** * Shows a red ERROR! message above the log panel. */ private void showRedErrorIndicator() { error.setText("ERROR!"); } /** * Shows a red FATAL! message above the log panel. */ private void showRedFatalIndicator() { fatal.setText("FATAL!"); } /** * Logs an error exception and shouts error! * * @param t */ private void logUncaughtExceptions(Throwable t) { showRedFatalIndicator(); logger.fatal().log(t); logger.trace(); GWT.log("Uncaught Exception", t); t.printStackTrace(System.err); } // // /** // * @param testCase // */ // private void runTestCase(TestBase testCase) { // String content = editor1.getContent(); // boolean pass = true; // try { // sendOps = false; // littleLogging(); // testCase.runTests(); // } catch (Throwable t) { // GWT.getUncaughtExceptionHandler().onUncaughtException(t); // pass = false; // } finally { // sendOps = true; // lottaLogging(); // } // // if (pass) { // logger.trace().log("<span style='color:green'>Pass :-)</span>"); // syncEditors(content); // } else { // syncEditors(); // } // editor1.focus(); // } // // /** // * Operations test button // */ // private final Button operationTestButton = new Button("Ops tests", new ClickHandler() { // public void onClick(ClickEvent e) { // runTestCase(new OperationTest()); // } // }); // // /** // * Paragaph test button // */ // private final Button pTestButton = new Button("P tests", new ClickHandler() { // public void onClick(ClickEvent e) { // runTestCase(new ParagraphTest()); // } // }); // // /** // * Title test button // */ // private final Button titleTestButton = new Button("Title tests", new ClickHandler() { // public void onClick(ClickEvent e) { // runTestCase(new ParagraphTest()); // } // }); // // /** // * Image thumbnail test button // */ // private final Button thumbnailTestButton = new Button("Thumb tests", new ClickHandler() { // public void onClick(ClickEvent e) { // runTestCase(new ImageThumbnailTest()); // } // }); /** * A queue of operations */ ArrayList<DocOp> queue = new ArrayList<DocOp>(); /** * Checkbox if harness should queue up operations from editor1 rather * than sending them straight to editor2 */ private final CheckBox queuingCheck = new CheckBox("Queue"); private final ClickHandler queuingCheckHandler = new ClickHandler() { public void onClick(ClickEvent e) { if (!queuingCheck.getValue()) { // Play any remaining operations in the queue while (queue.size() > 0) { playOne(); } } editor1.focus(true); } }; /** * Button for playing queued operations */ private final Button playButton = new Button("Play", new ClickHandler() { public void onClick(ClickEvent e) { playOne(); } }); /** * Button for clearing diff annotations and problem markers */ private final Button clearAnnotationsButton = new Button("Clear Annotations", new ClickHandler() { public void onClick(ClickEvent e) { editorBundle1.clearDiffs(); // ((EditorImpl)editor1).clearProblemMarkers(); } } ); /** * Checkbox if editor2 should show incoming operations as diffs */ private final CheckBox diffCheck = new CheckBox("Diffs"); private final ClickHandler diffCheckHandler = new ClickHandler() { public void onClick(ClickEvent e) { editorBundle2.setShowDiffMode(diffCheck.getValue()); } }; private final CheckBox disabledCheck = new CheckBox("Disable"); private final ClickHandler disabledCheckHandler = new ClickHandler() { public void onClick(ClickEvent e) { ((EditorImpl) editor1).debugSetDisabled(disabledCheck.getValue()); } }; private final ListBox documentModeSelect = new ListBox(); { for (Level level : Level.values()) { documentModeSelect.addItem(level.name()); } documentModeSelect.addChangeHandler(new ChangeHandler() { @Override public void onChange(ChangeEvent event) { setEditorLevel(); } }); } private void setEditorLevel() { Level newLevel = Level.values()[documentModeSelect.getSelectedIndex()]; Level current = doc1.getLevel(); logger.log(AbstractLogger.Level.TRACE, "Switching to " + newLevel); if (current == newLevel) { return; } if (current == Level.EDITING) { if (newLevel == Level.SHELVED) { editor1.removeContentAndUnrender(); } else { editor1.removeContent(); } doc1.replaceOutgoingSink(editor1Sink); } if (newLevel == Level.SHELVED) { doc1.setShelved(); } else if (newLevel == Level.EDITING) { editor1.setContent(doc1); } else { assert newLevel == Level.RENDERED || newLevel == Level.INTERACTIVE; if (newLevel == Level.RENDERED) { doc1.setRendering(); } else { doc1.setInteractive(displayDoc1); } } assert doc1.getLevel() == newLevel; if (doc1.getLevel() == Level.RENDERED || doc1.getLevel() == Level.INTERACTIVE) { displayDoc1.getElement().appendChild( doc1.getFullContentView().getDocumentElement().getImplNodelet()); } else if (doc1.getLevel() == Level.SHELVED) { for (ContentNode n : DocIterate.deep( doc1.getFullContentView(), doc1.getFullContentView().getDocumentElement(), null)) { assert n.getImplNodelet() == null; } } } /** * Clears both editors */ private final Button clearEditorsButton = new Button("Clear", new ClickHandler() { public void onClick(ClickEvent e) { clearEditors(); editor1.focus(true); } }); private final FlowPanel widgetRow; public void clearEditors() { syncEditors(""); LineContainers.appendLine(((EditorImpl) editor2).mutable(), null); } private int randomTestCounter; Scheduler.IncrementalTask randomTestProcess = new Scheduler.IncrementalTask() { @Override public boolean execute() { randomTestCounter++; logger.trace().log("Random test #" + randomTestCounter); try { // TODO(nigeltao): This might trigger an (incorrect) exception where the // FakeAtttachmentsManager returns null. I should fix that. // new ContentAnnotationPainterRandomTest().testPainter(); } catch (RuntimeException e) { logger.error().log(e); logger.error().log("TEST FAILED"); return true; } return true; } }; /** * Plays a single operation from the queue */ private void playOne() { if (queue.size() > 0) { DocOp operation = queue.remove(0); try { outputOperation(operation); editorBundle2.execute(operation); outputEditorState(editor2, prettyContent2, prettyHtml2); } catch (Throwable t) { GWT.getUncaughtExceptionHandler().onUncaughtException(t); } } setPlayButtonState(); } /** * Sets enabled/disabled state of play button */ private void setPlayButtonState() { playButton.setEnabled(queue.size() > 0); playButton.setHTML("Play" + (queue.size() > 0 ? " (" + queue.size() + ")" : "")); } /** * Output content */ private void outputBothEditorStates() { outputEditorState(editor1, prettyContent1, prettyHtml1); outputEditorState(editor2, prettyContent2, prettyHtml2); } /** * Output content for and editor */ private void outputEditorState(final Editor editor, final HTML prettyContent, final HTML prettyHtml) { Runnable printer = new Runnable() { public void run() { if (!quiet) { String content = EditorDocFormatter.formatContentDomString(editor); String html = EditorDocFormatter.formatImplDomString(editor); if (content != null) { prettyContent.setText(content); } if (html != null) { prettyHtml.setText(html); } } } }; if (LogLevel.showDebug()) { // Flush and update once it's safe if (editor.getContent().flush(printer)) { printer.run(); // note that if true is returned, the command isn't run inside flush. } } } /** * Output an operation * * @param operation */ private void outputOperation(DocOp operation) { if (!quiet) { operationOutput.setText(operation != null ? operation.toString() : ""); } } /** * Sets content in both editors from current text in content box. * Also adds that content to the content box oracle. */ private void setFromContentBox() { String content = contentBox != null ? contentBox.getText() : contentSuggestBox.getText(); if (contentOracle != null) { contentOracle.add(content); } syncEditors(bodyWrap(content)); editor1.focus(true); } /** * This is used for unit testing to call this to attach the main panel to an * arbitrary root. The unit testing framework does not call onModuleLoad(). * */ public static EditorHarness createTestPage() { EditorHarness harness = new EditorHarness(); RootPanel.get().add(harness, 0, 0); return harness; } void initContentText() { contentBox = new TextArea(); contentBox.getElement().setId("content-box"); } void initContentOracle() { contentOracle = new MultiWordSuggestOracle(); contentSuggestBox = new SuggestBox(contentOracle); contentSuggestBox.getElement().setId("content-box"); // Some initial content xml strings contentOracle.add(""); contentOracle.add("abcd"); contentSuggestBox.addSelectionHandler(new SelectionHandler<SuggestOracle.Suggestion>() { @Override public void onSelection(SelectionEvent<SuggestOracle.Suggestion> event) { setFromContentBox(); } }); String[] extra = extendSampleContent(); if (extra != null) { for (String content : extra) { contentOracle.add(content); } } } /** * Override this to provide additional sample content for the suggest box */ public String[] extendSampleContent() { return null; } /** * Convenience method to wrap sample content in the required body and line tags. */ public String bodyWrap(String sampleContent) { // Put an extra space so that the content oracle indexing works nicely on // the actual sample content. return "<body><line/> " + sampleContent + "</body>"; } public EditorHarness() { try { // Start with queueing off queuingCheck.setValue(false); setPlayButtonState(); // Construct log panel if (LogLevel.showDebug()) { log = new HTML(); log.setStyleName("log"); log.getElement().setId("log1"); error = new HTML(); error.getElement().setId("error1"); error.setStyleName("redIndicator"); fatal = new HTML(); fatal.getElement().setId("fatal1"); fatal.setStyleName("redIndicator"); // Initialize listener that turns error indicator red when any error // messages are logged DomLogger.addLoggerListener(new LoggerListener() { @Override public void onError() { showRedErrorIndicator(); } @Override public void onFatal() { showRedFatalIndicator(); } @Override public void onNeedOutput() {} @Override public void onNewLogger(String loggerName) {} }); } // Setup content and html display if (LogLevel.showDebug()) { prettyContent1 = new HTML(); prettyContent1.addStyleName("content"); prettyContent2 = new HTML(); prettyContent2.addStyleName("content"); prettyHtml1 = new HTML(); prettyHtml1.addStyleName("html"); prettyHtml2 = new HTML(); prettyHtml2.addStyleName("html"); } EditorWebDriverUtil.setDocumentSchema(getSchema()); registerDoodads(testEditorRegistries); // The editors we are testing editor1 = createEditor("editor1"); editorBundle1 = new EditorBundle(editor1, true); editor2 = createEditor("editor2"); editorBundle2 = new EditorBundle(editor2, false); toggleEditCheck1 = createEditToggleCheckBox(editor1); toggleEditCheck2 = createEditToggleCheckBox(editor2); operationOutput.setStyleName("operation"); final FlowPanel editMain = new FlowPanel(); editMain.setStyleName("main"); HorizontalPanel editors = new HorizontalPanel(); FlowPanel editorStack1 = new FlowPanel(); FlowPanel editorStack2 = new FlowPanel(); editorStack1.add(editor1.getWidget()); editorStack1.add(displayDoc1); editorStack1.add(toggleEditCheck1); editorStack1.add(disabledCheck); editorStack1.add(documentModeSelect); // xx editorStack1.add(disabledCheck); if (LogLevel.showDebug()) { editorStack1.add(prettyContent1); } editorStack2.add(editor2.getWidget()); editorStack2.add(toggleEditCheck2); editorStack2.add(diffCheck); editorStack2.add(createEditor2DocDetached); if (LogLevel.showDebug()) { editorStack2.add(prettyContent2); } editors.add(editorStack1); if (LogLevel.showDebug()) { editors.add(prettyHtml1); } editors.add(editorStack2); if (LogLevel.showDebug()) { editors.add(prettyHtml2); } HorizontalPanel operations = new HorizontalPanel(); operations.setVerticalAlignment(HorizontalPanel.ALIGN_MIDDLE); queuingCheck.addClickHandler(queuingCheckHandler); disabledCheck.addClickHandler(disabledCheckHandler); diffCheck.addClickHandler(diffCheckHandler); setPlayButtonState(); operations.add(clearAnnotationsButton); operations.add(queuingCheck); operations.add(playButton); operations.add(new Label("Operation:")); operations.add(operationOutput); editMain.add(operations); editMain.add(editors); // Exactly one of these methods should be uncommented. // initContentText(); initContentOracle(); widgetRow = new FlowPanel(); widgetRow.add(clearEditorsButton); widgetRow.add(setContentButton); widgetRow.add(new InlineLabel("<body><line/>")); widgetRow.add(contentBox != null ? contentBox : contentSuggestBox); widgetRow.add(new InlineLabel("</body>")); Button clearContentBoxButton = new Button("Clear text", new ClickHandler() { public void onClick(ClickEvent e) { if (contentBox != null) { contentBox.setValue(""); } else { contentSuggestBox.setValue(""); } } }); clearContentBoxButton.getElement().setId("clear-content-box"); widgetRow.add(clearContentBoxButton); // Hide it, only really used for webdriver tests clearContentBoxButton.setVisible(false); if (LogLevel.showDebug()) { widgetRow.add(new FlowPanel()); widgetRow.add(clearLogButton); widgetRow.add(quietButton); widgetRow.add(loudButton); } editMain.add(widgetRow); if (LogLevel.showDebug()) { editMain.add(error); editMain.add(fatal); editMain.add(log); // We need our own uncaught exception hander to make sure the error shout happens GWT.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { public void onUncaughtException(Throwable e) { logUncaughtExceptions(e); } }); } else { HTML spacer = new HTML(); editMain.add(spacer); } initWidget(editMain); // Enable logging (important to do this after main is attached // such that exceptions occurring before this can be handled by // GWT's Default handler...) if (LogLevel.showDebug()) { DomLogger.enable(log.getElement()); lottaLogging(); } KeyBindingRegistry keysRegistry = new KeyBindingRegistry(); extendKeyBindings(keysRegistry); // Start editing editor1.init(testEditorRegistries, keysRegistry, EditorSettings.DEFAULT); editor2.init(testEditorRegistries, keysRegistry, EditorSettings.DEFAULT); editor1.addUpdateListener(new EditorUpdateListener() { @Override public void onUpdate(EditorUpdateEvent event) { outputBothEditorStates(); } }); editor1.setOutputSink(editor1Sink); editor2.setOutputSink(editor2Sink); clearEditors(); editor1.setEditing(true); editor2.setEditing(false); toggleEditCheck2.setValue(false); toggleEditCheck1.setValue(true); // Output initial state outputBothEditorStates(); outputOperation(null); } catch (RuntimeException r) { // Do we need this at all? UncaughtExceptionHandler handler = GWT.getUncaughtExceptionHandler(); if (handler != null) { handler.onUncaughtException(r); } else { logger.error().log(r); } throw r; } } public DocumentSchema getSchema() { return ConversationSchemas.BLIP_SCHEMA_CONSTRAINTS; } public void registerDoodads(Registries registries) { ElementHandlerRegistry testHandlerRegistry = testEditorRegistries.getElementHandlerRegistry(); LineRendering.registerContainer(TOPLEVEL_CONTAINER_TAGNAME, registries.getElementHandlerRegistry()); StyleAnnotationHandler.register(registries); DiffAnnotationHandler.register( registries.getAnnotationHandlerRegistry(), registries.getPaintRegistry()); LinkAnnotationHandler.register(registries, new LinkAttributeAugmenter() { @Override public Map<String, String> augment(Map<String, Object> annotations, boolean isEditing, Map<String, String> current) { return current; } }); // TODO(danilatos): Open source spelly stuff // SpellDocument testSpellDocument = SpellDebugHelper.createTestSpellDocument( // EditorStaticDeps.logger); // SpellAnnotationHandler.register(Editor.ROOT_REGISTRIES, // SpellySettings.DEFAULT, testSpellDocument); // SpellDebugHelper.setDebugSpellDoc(testSpellDocument); // SpellSuggestion.register(testHandlerRegistry, testSpellDocument); // SpellTesting.registerDebugCombo(keysRegistry); // SpellDebugHelper.setDebugSpellDoc(testSpellDocument); extend(registries); } /** Override this method to register additional doodads. */ public void extend(Registries registries) { } /** Override this method to add keyboard handling hooks. */ public void extendKeyBindings(KeyBindingRegistry registry) { } static { Editors.initRootRegistries(); } private class EditorBundle { private final HighlightingDiffState diffState; private final boolean is1; private boolean showDiffs; EditorBundle(Editor editor, boolean is1) { this.diffState = new HighlightingDiffState(editor); this.is1 = is1; } void setShowDiffMode(boolean showDiffs) { this.showDiffs = showDiffs; } void execute(DocOp op) throws OperationException { if (showDiffs) { diffState.consume(op); } else { // TODO(danilatos): Clean this up (is1 ? doc1 : doc2).consume(op); } } void clearDiffs() { diffState.clearDiffs(); } } private Editor createEditor(String id) { EditorStaticDeps.setPopupProvider(Popup.LIGHTWEIGHT_POPUP_PROVIDER); EditorStaticDeps.setPopupChromeProvider(new PopupChromeProvider() { public PopupChrome createPopupChrome() { return null; } }); Editor editor = Editors.create(); editor.getWidget().getElement().setId(id); editor.addKeySignalListener(this); return editor; } /** * Log from all relevant modules */ private void lottaLogging() { DomLogger.enableAllModules(); DomLogger.enableModule("test", true); DomLogger.enableModule("editor", true); DomLogger.enableModule("editor-node", true); DomLogger.enableModule("operator", true); DomLogger.enableModule("dragdrop", true); } /** * Only log from test module */ private void littleLogging() { DomLogger.setMaxLevel(AbstractLogger.Level.ERROR); DomLogger.enableModule("test", true); } private final SilentOperationSink<DocOp> editor1Sink = new SilentOperationSink<DocOp>() { public void consume(DocOp operation) { try { if (operation != null && sendOps) { if (queuingCheck.getValue()) { queue.add(operation); setPlayButtonState(); } else { editorBundle2.execute(operation); } } } catch (Throwable t) { GWT.getUncaughtExceptionHandler().onUncaughtException(t); } finally { outputOperation(operation); } } }; private final SilentOperationSink<DocOp> editor2Sink = new SilentOperationSink<DocOp>() { public void consume(DocOp operation) { try { if (operation != null && sendOps) { editorBundle1.execute(operation); } } catch (Throwable t) { GWT.getUncaughtExceptionHandler().onUncaughtException(t); } finally { outputOperation(operation); } } }; /** * {@inheritDoc} */ public boolean onKeySignal(final Widget sender, SignalEvent event) { // Deferred command so we have a look at the content after it's updated ScheduleCommand.addCommand(new Task() { public void execute() { if (sender == editor1) { outputEditorState(editor1, prettyContent1, prettyHtml1); } else { outputEditorState(editor2, prettyContent2, prettyHtml2); } } }); return false; } public Editor getEditor1() { return editor1; } public Editor getEditor2() { return editor2; } /** * Convenience method to run the harness when it's the main application. */ public void run() { RootPanel.get().add(this); getEditor1().focus(true); } }