com.google.gerrit.client.editor.EditScreen.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.client.editor.EditScreen.java

Source

// Copyright (C) 2014 The Android Open Source Project
//
// 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 com.google.gerrit.client.editor;

import static com.google.gwt.dom.client.Style.Visibility.HIDDEN;
import static com.google.gwt.dom.client.Style.Visibility.VISIBLE;

import com.google.gerrit.client.DiffWebLinkInfo;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.JumpKeys;
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.account.EditPreferences;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeEditApi;
import com.google.gerrit.client.diff.DiffApi;
import com.google.gerrit.client.diff.DiffInfo;
import com.google.gerrit.client.diff.Header;
import com.google.gerrit.client.info.ChangeInfo;
import com.google.gerrit.client.info.FileInfo;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.HttpCallback;
import com.google.gerrit.client.rpc.HttpResponse;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.extensions.client.KeyMapType;
import com.google.gerrit.extensions.client.Theme;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.ClosingEvent;
import com.google.gwt.user.client.Window.ClosingHandler;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.ImageResourceRenderer;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import java.util.List;
import net.codemirror.addon.AddonInjector;
import net.codemirror.addon.Addons;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.ChangesHandler;
import net.codemirror.lib.CodeMirror.CommandRunner;
import net.codemirror.lib.Configuration;
import net.codemirror.lib.KeyMap;
import net.codemirror.lib.MergeView;
import net.codemirror.lib.Pos;
import net.codemirror.mode.ModeInfo;
import net.codemirror.mode.ModeInjector;
import net.codemirror.theme.ThemeLoader;

public class EditScreen extends Screen {
    interface Binder extends UiBinder<HTMLPanel, EditScreen> {
    }

    private static final Binder uiBinder = GWT.create(Binder.class);

    interface Style extends CssResource {
        String fullWidth();

        String base();

        String hideBase();
    }

    private final PatchSet.Id revision;
    private final String path;
    private final int startLine;
    private EditPreferences prefs;
    private EditPreferencesAction editPrefsAction;
    private MergeView mv;
    private CodeMirror cmBase;
    private CodeMirror cmEdit;
    private HttpResponse<NativeString> content;
    private HttpResponse<NativeString> baseContent;
    private EditFileInfo editFileInfo;
    private JsArray<DiffWebLinkInfo> diffLinks;

    @UiField
    Element header;
    @UiField
    Element project;
    @UiField
    Element filePath;
    @UiField
    FlowPanel linkPanel;
    @UiField
    Element cursLine;
    @UiField
    Element cursCol;
    @UiField
    Element dirty;
    @UiField
    CheckBox showBase;
    @UiField
    Button close;
    @UiField
    Button save;
    @UiField
    Element editor;
    @UiField
    Style style;

    private HandlerRegistration resizeHandler;
    private HandlerRegistration closeHandler;
    private int generation;

    public EditScreen(Patch.Key patch, int startLine) {
        this.revision = patch.getParentKey();
        this.path = patch.get();
        this.startLine = startLine - 1;
        setRequiresSignIn(true);
        add(uiBinder.createAndBindUi(this));
        addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
    }

    @Override
    protected void onInitUI() {
        super.onInitUI();
        setHeaderVisible(false);
        setWindowTitle(FileInfo.getFileName(path));
    }

    @Override
    protected void onLoad() {
        super.onLoad();

        prefs = EditPreferences.create(Gerrit.getEditPreferences());

        CallbackGroup group1 = new CallbackGroup();
        final CallbackGroup group2 = new CallbackGroup();
        final CallbackGroup group3 = new CallbackGroup();

        CodeMirror.initLibrary(group1.add(new AsyncCallback<Void>() {
            final AsyncCallback<Void> themeCallback = group3.addEmpty();

            @Override
            public void onSuccess(Void result) {
                // Load theme after CM library to ensure theme can override CSS.
                ThemeLoader.loadTheme(prefs.theme(), themeCallback);
                group2.done();

                new AddonInjector().add(Addons.I.merge_bundled().getName()).inject(new AsyncCallback<Void>() {
                    @Override
                    public void onFailure(Throwable caught) {
                    }

                    @Override
                    public void onSuccess(Void result) {
                        if (!prefs.showBase() || revision.get() > 0) {
                            group3.done();
                        }
                    }
                });
            }

            @Override
            public void onFailure(Throwable caught) {
            }
        }));

        ChangeApi.detail(revision.getParentKey().get(), group1.add(new AsyncCallback<ChangeInfo>() {
            @Override
            public void onSuccess(ChangeInfo c) {
                project.setInnerText(c.project());
                SafeHtml.setInnerHTML(filePath, Header.formatPath(path));
            }

            @Override
            public void onFailure(Throwable caught) {
            }
        }));

        if (revision.get() == 0) {
            ChangeEditApi.getMeta(revision, path, group1.add(new AsyncCallback<EditFileInfo>() {
                @Override
                public void onSuccess(EditFileInfo editInfo) {
                    editFileInfo = editInfo;
                }

                @Override
                public void onFailure(Throwable e) {
                }
            }));

            if (prefs.showBase()) {
                ChangeEditApi.get(revision, path, true /* base */,
                        group1.addFinal(new HttpCallback<NativeString>() {
                            @Override
                            public void onSuccess(HttpResponse<NativeString> fc) {
                                baseContent = fc;
                                group3.done();
                            }

                            @Override
                            public void onFailure(Throwable e) {
                            }
                        }));
            } else {
                group1.done();
            }
        } else {
            // TODO(davido): We probably want to create dedicated GET EditScreenMeta
            // REST endpoint. Abuse GET diff for now, as it retrieves links we need.
            DiffApi.diff(revision, path).webLinksOnly().get(group1.addFinal(new AsyncCallback<DiffInfo>() {
                @Override
                public void onSuccess(DiffInfo diffInfo) {
                    diffLinks = diffInfo.webLinks();
                }

                @Override
                public void onFailure(Throwable e) {
                }
            }));
        }

        ChangeEditApi.get(revision, path, group2.add(new HttpCallback<NativeString>() {
            final AsyncCallback<Void> modeCallback = group3.addEmpty();

            @Override
            public void onSuccess(HttpResponse<NativeString> fc) {
                content = fc;
                if (revision.get() > 0) {
                    baseContent = fc;
                }

                if (prefs.syntaxHighlighting()) {
                    injectMode(fc.getContentType(), modeCallback);
                } else {
                    modeCallback.onSuccess(null);
                }
            }

            @Override
            public void onFailure(Throwable e) {
                // "Not Found" means it's a new file.
                if (RestApi.isNotFound(e)) {
                    content = null;
                    modeCallback.onSuccess(null);
                } else {
                    GerritCallback.showFailure(e);
                }
            }
        }));

        group3.addListener(new ScreenLoadCallback<Void>(this) {
            @Override
            protected void preDisplay(Void result) {
                initEditor();

                renderLinks(editFileInfo, diffLinks);
                editFileInfo = null;
                diffLinks = null;

                showBase.setValue(prefs.showBase(), true);
                cmBase.refresh();
            }
        });
    }

    @Override
    public void registerKeys() {
        super.registerKeys();
        KeyMap localKeyMap = KeyMap.create();
        localKeyMap.on("Ctrl-L", gotoLine()).on("Cmd-L", gotoLine()).on("Cmd-S", save());

        // TODO(davido): Find a better way to prevent key maps collisions
        if (prefs.keyMapType() != KeyMapType.EMACS) {
            localKeyMap.on("Ctrl-S", save());
        }

        cmBase.addKeyMap(localKeyMap);
        cmEdit.addKeyMap(localKeyMap);
    }

    private Runnable gotoLine() {
        return () -> cmEdit.execCommand("jumpToLine");
    }

    @Override
    public void onShowView() {
        super.onShowView();
        Window.enableScrolling(false);
        JumpKeys.enable(false);
        if (prefs.hideTopMenu()) {
            Gerrit.setHeaderVisible(false);
        }
        resizeHandler = Window.addResizeHandler(new ResizeHandler() {
            @Override
            public void onResize(ResizeEvent event) {
                adjustHeight();
            }
        });
        closeHandler = Window.addWindowClosingHandler(new ClosingHandler() {
            @Override
            public void onWindowClosing(ClosingEvent event) {
                if (!cmEdit.isClean(generation)) {
                    event.setMessage(EditConstants.I.closeUnsavedChanges());
                }
            }
        });

        generation = cmEdit.changeGeneration(true);
        setClean(true);
        cmEdit.on(new ChangesHandler() {
            @Override
            public void handle(CodeMirror cm) {
                setClean(cm.isClean(generation));
            }
        });

        adjustHeight();
        cmEdit.on("cursorActivity", updateCursorPosition());
        setShowTabs(prefs.showTabs());
        setLineLength(prefs.lineLength());
        cmEdit.refresh();
        cmEdit.focus();

        if (startLine > 0) {
            cmEdit.scrollToLine(startLine);
        }
        updateActiveLine();
        editPrefsAction = new EditPreferencesAction(this, prefs);
    }

    @Override
    protected void onUnload() {
        super.onUnload();
        if (cmBase != null) {
            cmBase.getWrapperElement().removeFromParent();
        }
        if (cmEdit != null) {
            cmEdit.getWrapperElement().removeFromParent();
        }
        if (resizeHandler != null) {
            resizeHandler.removeHandler();
        }
        if (closeHandler != null) {
            closeHandler.removeHandler();
        }
        Window.enableScrolling(true);
        Gerrit.setHeaderVisible(true);
        JumpKeys.enable(true);
    }

    CodeMirror getEditor() {
        return cmEdit;
    }

    @UiHandler("editSettings")
    void onEditSetting(@SuppressWarnings("unused") ClickEvent e) {
        editPrefsAction.show();
    }

    @UiHandler("save")
    void onSave(@SuppressWarnings("unused") ClickEvent e) {
        save().run();
    }

    @UiHandler("close")
    void onClose(@SuppressWarnings("unused") ClickEvent e) {
        if (cmEdit.isClean(generation) || Window.confirm(EditConstants.I.cancelUnsavedChanges())) {
            upToChange();
        }
    }

    private void displayBase() {
        cmBase.getWrapperElement().getParentElement().removeClassName(style.hideBase());
        cmEdit.getWrapperElement().getParentElement().removeClassName(style.fullWidth());
        mv.getGapElement().removeClassName(style.hideBase());
        setCmBaseValue();
        setLineLength(prefs.lineLength());
        cmBase.refresh();
    }

    @UiHandler("showBase")
    void onShowBase(ValueChangeEvent<Boolean> e) {
        boolean shouldShow = e.getValue();
        if (shouldShow) {
            if (baseContent == null) {
                ChangeEditApi.get(revision, path, true /* base */, new HttpCallback<NativeString>() {
                    @Override
                    public void onSuccess(HttpResponse<NativeString> fc) {
                        baseContent = fc;
                        displayBase();
                    }

                    @Override
                    public void onFailure(Throwable e) {
                    }
                });
            } else {
                displayBase();
            }
        } else {
            cmBase.getWrapperElement().getParentElement().addClassName(style.hideBase());
            cmEdit.getWrapperElement().getParentElement().addClassName(style.fullWidth());
            mv.getGapElement().addClassName(style.hideBase());
        }
        mv.setShowDifferences(shouldShow);
    }

    void setOption(String option, String value) {
        cmBase.setOption(option, value);
        cmEdit.setOption(option, value);
    }

    void setOption(String option, boolean value) {
        cmBase.setOption(option, value);
        cmEdit.setOption(option, value);
    }

    void setOption(String option, double value) {
        cmBase.setOption(option, value);
        cmEdit.setOption(option, value);
    }

    void setTheme(Theme newTheme) {
        cmBase.operation(() -> cmBase.setOption("theme", newTheme.name().toLowerCase()));
        cmEdit.operation(() -> cmEdit.setOption("theme", newTheme.name().toLowerCase()));
    }

    void setLineLength(int length) {
        int adjustedLength = Patch.COMMIT_MSG.equals(path) ? 72 : length;
        cmBase.extras().lineLength(adjustedLength);
        cmEdit.extras().lineLength(adjustedLength);
    }

    void setIndentUnit(int indent) {
        cmEdit.setOption("indentUnit", Patch.COMMIT_MSG.equals(path) ? 2 : indent);
    }

    void setShowLineNumbers(boolean show) {
        cmBase.setOption("lineNumbers", show);
        cmEdit.setOption("lineNumbers", show);
    }

    void setShowWhitespaceErrors(boolean show) {
        cmBase.operation(() -> cmBase.setOption("showTrailingSpace", show));
        cmEdit.operation(() -> cmEdit.setOption("showTrailingSpace", show));
    }

    void setShowTabs(boolean show) {
        cmBase.extras().showTabs(show);
        cmEdit.extras().showTabs(show);
    }

    void adjustHeight() {
        int height = header.getOffsetHeight();
        int rest = Gerrit.getHeaderFooterHeight() + height + 5; // Estimate
        mv.getGapElement().getStyle().setHeight(Window.getClientHeight() - rest, Unit.PX);
        cmBase.adjustHeight(height);
        cmEdit.adjustHeight(height);
    }

    void setSyntaxHighlighting(boolean b) {
        ModeInfo modeInfo = ModeInfo.findMode(content.getContentType(), path);
        final String mode = modeInfo != null ? modeInfo.mime() : null;
        if (b && mode != null && !mode.isEmpty()) {
            injectMode(mode, new AsyncCallback<Void>() {
                @Override
                public void onSuccess(Void result) {
                    cmBase.setOption("mode", mode);
                    cmEdit.setOption("mode", mode);
                }

                @Override
                public void onFailure(Throwable caught) {
                    prefs.syntaxHighlighting(false);
                }
            });
        } else {
            cmBase.setOption("mode", (String) null);
            cmEdit.setOption("mode", (String) null);
        }
    }

    private void upToChange() {
        Gerrit.display(PageLinks.toChangeInEditMode(revision.getParentKey()));
    }

    private void initEditor() {
        ModeInfo mode = null;
        String editContent = "";
        if (content != null && content.getResult() != null) {
            editContent = content.getResult().asString();
            if (prefs.syntaxHighlighting()) {
                mode = ModeInfo.findMode(content.getContentType(), path);
            }
        }
        mv = MergeView.create(editor, Configuration.create().set("autoCloseBrackets", prefs.autoCloseBrackets())
                .set("cursorBlinkRate", prefs.cursorBlinkRate()).set("cursorHeight", 0.85)
                .set("indentUnit", prefs.indentUnit()).set("keyMap", prefs.keyMapType().name().toLowerCase())
                .set("lineNumbers", prefs.hideLineNumbers()).set("lineWrapping", false)
                .set("matchBrackets", prefs.matchBrackets()).set("mode", mode != null ? mode.mime() : null)
                .set("origLeft", editContent).set("scrollbarStyle", "overlay")
                .set("showTrailingSpace", prefs.showWhitespaceErrors()).set("styleSelectedText", true)
                .set("tabSize", prefs.tabSize()).set("theme", prefs.theme().name().toLowerCase()).set("value", ""));

        cmBase = mv.leftOriginal();
        cmBase.getWrapperElement().addClassName(style.base());
        cmEdit = mv.editor();
        setCmBaseValue();
        cmEdit.setValue(editContent);

        CodeMirror.addCommand("save", new CommandRunner() {
            @Override
            public void run(CodeMirror instance) {
                save().run();
            }
        });
    }

    private void renderLinks(EditFileInfo editInfo, JsArray<DiffWebLinkInfo> diffLinks) {
        renderLinksToDiff();

        if (editInfo != null) {
            renderLinks(Natives.asList(editInfo.webLinks()));
        } else if (diffLinks != null) {
            renderLinks(Natives.asList(diffLinks));
        }
    }

    private void renderLinks(List<DiffWebLinkInfo> links) {
        if (links != null) {
            for (DiffWebLinkInfo webLink : links) {
                linkPanel.add(webLink.toAnchor());
            }
        }
    }

    private void renderLinksToDiff() {
        InlineHyperlink sbs = new InlineHyperlink();
        sbs.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.sideBySideDiff()));
        sbs.setTargetHistoryToken(Dispatcher.toPatch("sidebyside", null, new Patch.Key(revision, path)));
        sbs.setTitle(PatchUtil.C.sideBySideDiff());
        linkPanel.add(sbs);

        InlineHyperlink unified = new InlineHyperlink();
        unified.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.unifiedDiff()));
        unified.setTargetHistoryToken(Dispatcher.toPatch("unified", null, new Patch.Key(revision, path)));
        unified.setTitle(PatchUtil.C.unifiedDiff());
        linkPanel.add(unified);
    }

    private Runnable updateCursorPosition() {
        return () -> {
            // The rendering of active lines has to be deferred. Reflow
            // caused by adding and removing styles chokes Firefox when arrow
            // key (or j/k) is held down. Performance on Chrome is fine
            // without the deferral.
            //
            Scheduler.get().scheduleDeferred(() -> cmEdit.operation(this::updateActiveLine));
        };
    }

    private void updateActiveLine() {
        Pos p = cmEdit.getCursor("end");
        cursLine.setInnerText(Integer.toString(p.line() + 1));
        cursCol.setInnerText(Integer.toString(p.ch() + 1));
        cmEdit.extras().activeLine(cmEdit.getLineHandleVisualStart(p.line()));
    }

    private void setClean(boolean clean) {
        save.setEnabled(!clean);
        close.setEnabled(true);
        dirty.getStyle().setVisibility(!clean ? VISIBLE : HIDDEN);
    }

    private Runnable save() {
        return () -> {
            if (!cmEdit.isClean(generation)) {
                close.setEnabled(false);
                String text = cmEdit.getValue();
                if (Patch.COMMIT_MSG.equals(path)) {
                    String trimmed = text.trim() + "\r";
                    if (!trimmed.equals(text)) {
                        text = trimmed;
                        cmEdit.setValue(text);
                    }
                }
                final int g = cmEdit.changeGeneration(false);
                ChangeEditApi.put(revision.getParentKey().get(), path, text, new GerritCallback<VoidResult>() {
                    @Override
                    public void onSuccess(VoidResult result) {
                        generation = g;
                        setClean(cmEdit.isClean(g));
                    }

                    @Override
                    public void onFailure(final Throwable caught) {
                        close.setEnabled(true);
                    }
                });
            }
        };
    }

    private void injectMode(String type, AsyncCallback<Void> cb) {
        new ModeInjector().add(type).inject(cb);
    }

    private void setCmBaseValue() {
        cmBase.setValue(
                baseContent != null && baseContent.getResult() != null ? baseContent.getResult().asString() : "");
    }
}