com.google.gerrit.client.change.FileTable.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.client.change.FileTable.java

Source

// Copyright (C) 2013 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.change;

import static com.google.gerrit.client.FormatUtil.formatAbsBytes;
import static com.google.gerrit.client.FormatUtil.formatAbsPercentage;
import static com.google.gerrit.client.FormatUtil.formatBytes;
import static com.google.gerrit.client.FormatUtil.formatPercentage;

import com.google.gerrit.client.DiffObject;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeEditApi;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.changes.ReviewInfo;
import com.google.gerrit.client.changes.Util;
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.NativeMap;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.ui.NavigationTable;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.Patch.ChangeType;
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.JsArrayString;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwt.user.client.ui.ImageResourceRenderer;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.impl.HyperlinkImpl;
import com.google.gwtexpui.globalkey.client.KeyCommand;
import com.google.gwtexpui.progress.client.ProgressBar;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import java.sql.Timestamp;

public class FileTable extends FlowPanel {
    private static final FileTableResources R = GWT.create(FileTableResources.class);

    interface FileTableResources extends ClientBundle {
        @Source("file_table.css")
        FileTableCss css();
    }

    interface FileTableCss extends CssResource {
        String table();

        String nohover();

        String pointer();

        String reviewed();

        String status();

        String pathColumn();

        String commonPrefix();

        String renameCopySource();

        String draftColumn();

        String newColumn();

        String commentColumn();

        String deltaColumn1();

        String deltaColumn2();

        String inserted();

        String deleted();

        String restoreDelete();

        String error();
    }

    public enum Mode {
        REVIEW, EDIT
    }

    private static final String DELETE;
    private static final String RESTORE;
    private static final String REVIEWED;
    private static final String OPEN;
    private static final int C_PATH = 3;
    private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);

    static {
        DELETE = DOM.createUniqueId().replace('-', '_');
        RESTORE = DOM.createUniqueId().replace('-', '_');
        REVIEWED = DOM.createUniqueId().replace('-', '_');
        OPEN = DOM.createUniqueId().replace('-', '_');
        init(DELETE, RESTORE, REVIEWED, OPEN);
    }

    private static native void init(String d, String t, String r, String o) /*-{
                                                                            $wnd[d] = $entry(function(e,i) {
                                                                            @com.google.gerrit.client.change.FileTable::onDelete(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
                                                                            });
                                                                            $wnd[t] = $entry(function(e,i) {
                                                                            @com.google.gerrit.client.change.FileTable::onRestore(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
                                                                            });
                                                                            $wnd[r] = $entry(function(e,i) {
                                                                            @com.google.gerrit.client.change.FileTable::onReviewed(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i)
                                                                            });
                                                                            $wnd[o] = $entry(function(e,i) {
                                                                            return @com.google.gerrit.client.change.FileTable::onOpen(Lcom/google/gwt/dom/client/NativeEvent;I)(e,i);
                                                                            });
                                                                            }-*/;

    private static void onDelete(NativeEvent e, int idx) {
        MyTable t = getMyTable(e);
        if (t != null) {
            t.onDelete(idx);
        }
    }

    private static boolean onRestore(NativeEvent e, int idx) {
        MyTable t = getMyTable(e);
        if (t != null) {
            t.onRestore(idx);
            e.preventDefault();
            e.stopPropagation();
            return false;
        }
        return true;
    }

    private static void onReviewed(NativeEvent e, int idx) {
        MyTable t = getMyTable(e);
        if (t != null) {
            t.onReviewed(InputElement.as(Element.as(e.getEventTarget())), idx);
        }
    }

    private static boolean onOpen(NativeEvent e, int idx) {
        if (link.handleAsClick(e.<Event>cast())) {
            MyTable t = getMyTable(e);
            if (t != null) {
                t.onOpenRow(1 + idx);
                e.preventDefault();
                e.stopPropagation();
                return false;
            }
        }
        return true;
    }

    private static MyTable getMyTable(NativeEvent event) {
        Element e = event.getEventTarget().cast();
        for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
            EventListener l = DOM.getEventListener(e);
            if (l instanceof MyTable) {
                return (MyTable) l;
            }
        }
        return null;
    }

    private DiffObject base;
    private PatchSet.Id curr;
    private MyTable table;
    private boolean register;
    private JsArrayString reviewed;
    private String scrollToPath;
    private ChangeScreen.Style style;
    private Widget replyButton;
    private boolean editExists;
    private Mode mode;

    @Override
    protected void onLoad() {
        super.onLoad();
        R.css().ensureInjected();
    }

    public void set(DiffObject base, PatchSet.Id curr, ChangeScreen.Style style, Widget replyButton, Mode mode,
            boolean editExists) {
        this.base = base;
        this.curr = curr;
        this.style = style;
        this.replyButton = replyButton;
        this.mode = mode;
        this.editExists = editExists;
    }

    void setValue(NativeMap<FileInfo> fileMap, Timestamp myLastReply,
            @Nullable NativeMap<JsArray<CommentInfo>> comments, @Nullable NativeMap<JsArray<CommentInfo>> drafts) {
        JsArray<FileInfo> list = fileMap.values();
        FileInfo.sortFileInfoByPath(list);

        DisplayCommand cmd = new DisplayCommand(fileMap, list, myLastReply, comments, drafts);
        if (cmd.execute()) {
            cmd.showProgressBar();
            Scheduler.get().scheduleIncremental(cmd);
        }
    }

    void showError(Throwable t) {
        clear();
        Label l = new Label(Resources.M.failedToLoadFileList(t.getMessage()));
        add(l);
        l.setStyleName(R.css().error());
    }

    void markReviewed(JsArrayString reviewed) {
        if (table != null) {
            table.markReviewed(reviewed);
        } else {
            this.reviewed = reviewed;
        }
    }

    void unregisterKeys() {
        register = false;

        if (table != null) {
            table.setRegisterKeys(false);
        }
    }

    void registerKeys() {
        register = true;

        if (table != null) {
            table.setRegisterKeys(true);
        }
    }

    void scrollToPath(String path) {
        if (table != null) {
            table.scrollToPath(path);
        } else {
            scrollToPath = path;
        }
    }

    void openAll() {
        if (table != null) {
            String self = Gerrit.selfRedirect(null);
            for (FileInfo info : Natives.asList(table.list)) {
                if (canOpen(info.path())) {
                    Window.open(self + "#" + url(info), "_blank", null);
                }
            }
        }
    }

    private boolean canOpen(String path) {
        return mode != Mode.EDIT || !Patch.isMagic(path) || Patch.COMMIT_MSG.equals(path);
    }

    private void setTable(MyTable table) {
        clear();
        add(table);
        this.table = table;

        if (register) {
            table.setRegisterKeys(true);
        }
        if (reviewed != null) {
            table.markReviewed(reviewed);
            reviewed = null;
        }
        if (scrollToPath != null) {
            table.scrollToPath(scrollToPath);
            scrollToPath = null;
        }
    }

    private String url(FileInfo info) {
        return info.binary() ? Dispatcher.toUnified(base, curr, info.path())
                : mode == Mode.REVIEW ? Dispatcher.toPatch(base, curr, info.path())
                        : Dispatcher.toEditScreen(curr, info.path());
    }

    private final class MyTable extends NavigationTable<FileInfo> {
        private final NativeMap<FileInfo> map;
        private final JsArray<FileInfo> list;

        MyTable(NativeMap<FileInfo> map, JsArray<FileInfo> list) {
            this.map = map;
            this.list = list;
            table.setWidth("");

            keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.patchTablePrev()),
                    new NextKeyCommand(0, 'j', Util.C.patchTableNext()));
            keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.patchTableOpenDiff()));
            keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C.patchTableOpenDiff()));

            keysNavigation.add(new OpenFileCommand(list.length() - 1, 0, '[', Resources.C.openLastFile()),
                    new OpenFileCommand(0, 0, ']', Resources.C.openCommitMessage()));

            keysAction.add(new KeyCommand(0, 'r', PatchUtil.C.toggleReviewed()) {
                @Override
                public void onKeyPress(KeyPressEvent event) {
                    int row = getCurrentRow();
                    if (1 <= row && row <= MyTable.this.list.length()) {
                        FileInfo info = MyTable.this.list.get(row - 1);
                        InputElement b = getReviewed(info);
                        boolean c = !b.isChecked();
                        setReviewed(info, c);
                        b.setChecked(c);
                    }
                }
            });

            setSavePointerId((!base.isBase() ? base.asString() + ".." : "") + curr.toString());
        }

        void onDelete(int idx) {
            String path = list.get(idx).path();
            ChangeEditApi.delete(curr.getParentKey().get(), path, new AsyncCallback<VoidResult>() {
                @Override
                public void onSuccess(VoidResult result) {
                    Gerrit.display(PageLinks.toChangeInEditMode(curr.getParentKey()));
                }

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

        void onRestore(int idx) {
            String path = list.get(idx).path();
            ChangeEditApi.restore(curr.getParentKey().get(), path, new AsyncCallback<VoidResult>() {
                @Override
                public void onSuccess(VoidResult result) {
                    Gerrit.display(PageLinks.toChangeInEditMode(curr.getParentKey()));
                }

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

        void onReviewed(InputElement checkbox, int idx) {
            setReviewed(list.get(idx), checkbox.isChecked());
        }

        private void setReviewed(FileInfo info, boolean r) {
            RestApi api = ChangeApi.revision(curr).view("files").id(info.path()).view("reviewed");
            if (r) {
                api.put(CallbackGroup.<ReviewInfo>emptyCallback());
            } else {
                api.delete(CallbackGroup.<ReviewInfo>emptyCallback());
            }
        }

        void markReviewed(JsArrayString reviewed) {
            for (int i = 0; i < reviewed.length(); i++) {
                FileInfo info = map.get(reviewed.get(i));
                if (info != null) {
                    getReviewed(info).setChecked(true);
                }
            }
        }

        private InputElement getReviewed(FileInfo info) {
            CellFormatter fmt = table.getCellFormatter();
            Element e = fmt.getElement(1 + info._row(), 1);
            return InputElement.as(e.getFirstChildElement());
        }

        void scrollToPath(String path) {
            FileInfo info = map.get(path);
            if (info != null) {
                movePointerTo(1 + info._row(), true);
            }
        }

        @Override
        protected Object getRowItemKey(FileInfo item) {
            return item.path();
        }

        @Override
        protected int findRow(Object id) {
            FileInfo info = map.get((String) id);
            return info != null ? 1 + info._row() : -1;
        }

        @Override
        protected FileInfo getRowItem(int row) {
            if (1 <= row && row <= list.length()) {
                return list.get(row - 1);
            }
            return null;
        }

        @Override
        protected void onOpenRow(int row) {
            if (1 <= row && row <= list.length()) {
                FileInfo info = list.get(row - 1);
                if (canOpen(info.path())) {
                    Gerrit.display(url(info));
                }
            }
        }

        @Override
        protected void onCellSingleClick(Event event, int row, int column) {
            if (column == C_PATH && link.handleAsClick(event)) {
                onOpenRow(row);
            } else {
                super.onCellSingleClick(event, row, column);
            }
        }

        private class OpenFileCommand extends KeyCommand {
            private final int index;

            OpenFileCommand(int index, int modifiers, char c, String helpText) {
                super(modifiers, c, helpText);
                this.index = index;
            }

            @Override
            public void onKeyPress(KeyPressEvent event) {
                FileInfo info = list.get(index);
                if (canOpen(info.path())) {
                    Gerrit.display(url(info));
                }
            }
        }
    }

    private final class DisplayCommand implements RepeatingCommand {
        private final SafeHtmlBuilder sb = new SafeHtmlBuilder();
        private final MyTable myTable;
        private final JsArray<FileInfo> list;
        private final Timestamp myLastReply;
        private final NativeMap<JsArray<CommentInfo>> comments;
        private final NativeMap<JsArray<CommentInfo>> drafts;
        private final boolean hasUser;
        private final boolean showChangeSizeBars;
        private boolean attached;
        private int row;
        private double start;
        private ProgressBar meter;
        private String lastPath = "";

        private boolean hasBinaryFile;
        private boolean hasNonBinaryFile;
        private int inserted;
        private int deleted;
        private long binOldSize;
        private long bytesInserted;
        private long bytesDeleted;

        private DisplayCommand(NativeMap<FileInfo> map, JsArray<FileInfo> list, Timestamp myLastReply,
                @Nullable NativeMap<JsArray<CommentInfo>> comments,
                @Nullable NativeMap<JsArray<CommentInfo>> drafts) {
            this.myTable = new MyTable(map, list);
            this.list = list;
            this.myLastReply = myLastReply;
            this.comments = comments;
            this.drafts = drafts;
            this.hasUser = Gerrit.isSignedIn();
            this.showChangeSizeBars = Gerrit.getUserPreferences().sizeBarInChangeTable();
            myTable.addStyleName(R.css().table());
        }

        @Override
        public boolean execute() {
            boolean attachedNow = isAttached();
            if (!attached && attachedNow) {
                // Remember that we have been attached at least once. If
                // later we find we aren't attached we should stop running.
                attached = true;
            } else if (attached && !attachedNow) {
                // If the user navigated away, we aren't in the DOM anymore.
                // Don't continue to render.
                return false;
            }

            start = System.currentTimeMillis();
            if (row == 0) {
                header(sb);
                computeInsertedDeleted();
            }
            while (row < list.length()) {
                FileInfo info = list.get(row);
                info._row(row);
                render(sb, info);
                if ((++row % 10) == 0 && longRunning()) {
                    updateMeter();
                    return true;
                }
            }
            footer(sb);
            myTable.resetHtml(sb);
            myTable.finishDisplay();
            setTable(myTable);
            return false;
        }

        private void computeInsertedDeleted() {
            inserted = 0;
            deleted = 0;
            binOldSize = 0;
            bytesInserted = 0;
            bytesDeleted = 0;
            for (int i = 0; i < list.length(); i++) {
                FileInfo info = list.get(i);
                if (!Patch.isMagic(info.path())) {
                    if (!info.binary()) {
                        hasNonBinaryFile = true;
                        inserted += info.linesInserted();
                        deleted += info.linesDeleted();
                    } else {
                        hasBinaryFile = true;
                        binOldSize += info.size() - info.sizeDelta();
                        if (info.sizeDelta() >= 0) {
                            bytesInserted += info.sizeDelta();
                        } else {
                            bytesDeleted += info.sizeDelta();
                        }
                    }
                }
            }
        }

        void showProgressBar() {
            if (meter == null) {
                meter = new ProgressBar(Util.M.loadingPatchSet(curr.get()));
                FileTable.this.clear();
                FileTable.this.add(meter);
            }
            updateMeter();
        }

        void updateMeter() {
            if (meter != null) {
                int n = list.length();
                meter.setValue((100 * row) / n);
            }
        }

        private boolean longRunning() {
            return System.currentTimeMillis() - start > 200;
        }

        private void header(SafeHtmlBuilder sb) {
            sb.openTr().setStyleName(R.css().nohover());
            sb.openTh().setStyleName(R.css().pointer()).closeTh();
            if (mode == Mode.REVIEW) {
                sb.openTh().setStyleName(R.css().reviewed()).closeTh();
            } else {
                sb.openTh().setStyleName(R.css().restoreDelete()).closeTh();
            }
            sb.openTh().setStyleName(R.css().status()).closeTh();
            sb.openTh().append(Util.C.patchTableColumnName()).closeTh();
            sb.openTh().setAttribute("colspan", 3).append(Util.C.patchTableColumnComments()).closeTh();
            sb.openTh().setAttribute("colspan", 2).append(Util.C.patchTableColumnSize()).closeTh();
            sb.closeTr();
        }

        private void render(SafeHtmlBuilder sb, FileInfo info) {
            sb.openTr();
            sb.openTd().setStyleName(R.css().pointer()).closeTd();
            if (mode == Mode.REVIEW) {
                columnReviewed(sb, info);
            } else {
                columnDeleteRestore(sb, info);
            }
            columnStatus(sb, info);
            columnPath(sb, info);
            columnComments(sb, info);
            columnDelta1(sb, info);
            columnDelta2(sb, info);
            sb.closeTr();
        }

        private void columnReviewed(SafeHtmlBuilder sb, FileInfo info) {
            sb.openTd().setStyleName(R.css().reviewed());
            if (hasUser) {
                sb.openElement("input").setAttribute("title", Resources.C.reviewedFileTitle())
                        .setAttribute("type", "checkbox")
                        .setAttribute("onclick", REVIEWED + "(event," + info._row() + ")").closeSelf();
            }
            sb.closeTd();
        }

        private void columnDeleteRestore(SafeHtmlBuilder sb, FileInfo info) {
            sb.openTd().setStyleName(R.css().restoreDelete());
            if (hasUser) {
                if (!Patch.isMagic(info.path())) {
                    boolean editable = isEditable(info);
                    sb.openDiv().openElement("button").setAttribute("title", Resources.C.restoreFileInline())
                            .setAttribute("onclick", RESTORE + "(event," + info._row() + ")")
                            .append(new ImageResourceRenderer().render(Gerrit.RESOURCES.editUndo()))
                            .closeElement("button");
                    if (editable) {
                        sb.openElement("button").setAttribute("title", Resources.C.removeFileInline())
                                .setAttribute("onclick", DELETE + "(event," + info._row() + ")")
                                .append(new ImageResourceRenderer().render(Gerrit.RESOURCES.redNot()))
                                .closeElement("button");
                    }
                    sb.closeDiv();
                }
            }
            sb.closeTd();
        }

        private boolean isEditable(FileInfo info) {
            String status = info.status();
            return status == null || !ChangeType.DELETED.matches(status);
        }

        private void columnStatus(SafeHtmlBuilder sb, FileInfo info) {
            sb.openTd().setStyleName(R.css().status());
            if (!Patch.isMagic(info.path()) && info.status() != null
                    && !ChangeType.MODIFIED.matches(info.status())) {
                sb.append(info.status());
            }
            sb.closeTd();
        }

        private void columnPath(SafeHtmlBuilder sb, FileInfo info) {
            String path = info.path();

            sb.openTd().setStyleName(R.css().pathColumn());

            if (!canOpen(path)) {
                sb.openDiv();
                appendPath(path);
                sb.closeDiv();
                sb.closeTd();
                return;
            }

            sb.openAnchor();

            if (mode == Mode.EDIT && !isEditable(info)) {
                sb.setAttribute("onclick", RESTORE + "(event," + info._row() + ")");
            } else {
                sb.setAttribute("href", "#" + url(info)).setAttribute("onclick",
                        OPEN + "(event," + info._row() + ")");
            }
            appendPath(path);
            sb.closeAnchor();
            if (info.oldPath() != null) {
                sb.br();
                sb.openSpan().setStyleName(R.css().renameCopySource()).append(info.oldPath()).closeSpan();
            }
            sb.closeTd();
        }

        private void appendPath(String path) {
            if (Patch.COMMIT_MSG.equals(path)) {
                sb.append(Util.C.commitMessage());
            } else if (Patch.MERGE_LIST.equals(path)) {
                sb.append(Util.C.mergeList());
            } else if (Gerrit.getUserPreferences().muteCommonPathPrefixes()) {
                int commonPrefixLen = commonPrefix(path);
                if (commonPrefixLen > 0) {
                    sb.openSpan().setStyleName(R.css().commonPrefix()).append(path.substring(0, commonPrefixLen))
                            .closeSpan();
                }
                sb.append(path.substring(commonPrefixLen));
                lastPath = path;
            } else {
                sb.append(path);
            }
        }

        private int commonPrefix(String path) {
            for (int n = path.length(); n > 0;) {
                int s = path.lastIndexOf('/', n);
                if (s < 0) {
                    return 0;
                }

                String p = path.substring(0, s + 1);
                if (lastPath.startsWith(p)) {
                    return s + 1;
                }
                n = s - 1;
            }
            return 0;
        }

        private void columnComments(SafeHtmlBuilder sb, FileInfo info) {
            JsArray<CommentInfo> cList = filterForParent(get(info.path(), comments));
            JsArray<CommentInfo> dList = filterForParent(get(info.path(), drafts));

            sb.openTd().setStyleName(R.css().draftColumn());
            if (dList.length() > 0) {
                sb.append("drafts: ").append(dList.length());
            }
            sb.closeTd();

            int cntAll = cList.length();
            int cntNew = 0;
            if (myLastReply != null) {
                for (int i = cntAll - 1; i >= 0; i--) {
                    CommentInfo m = cList.get(i);
                    if (m.updated().compareTo(myLastReply) > 0) {
                        cntNew++;
                    } else {
                        break;
                    }
                }
            }

            sb.openTd().setStyleName(R.css().newColumn());
            if (cntNew > 0) {
                sb.append("new: ").append(cntNew);
            }
            sb.closeTd();

            sb.openTd().setStyleName(R.css().commentColumn());
            if (cntAll - cntNew > 0) {
                sb.append("comments: ").append(cntAll - cntNew);
            }
            sb.closeTd();
        }

        private JsArray<CommentInfo> filterForParent(JsArray<CommentInfo> list) {
            JsArray<CommentInfo> result = JsArray.createArray().cast();
            for (CommentInfo c : Natives.asList(list)) {
                if (c.side() == Side.REVISION) {
                    result.push(c);
                } else if (base.isBaseOrAutoMerge() && !c.hasParent()) {
                    result.push(c);
                } else if (base.isParent() && c.parent() == base.getParentNum()) {
                    result.push(c);
                }
            }
            return result;
        }

        private JsArray<CommentInfo> get(String p, NativeMap<JsArray<CommentInfo>> m) {
            JsArray<CommentInfo> r = null;
            if (m != null) {
                r = m.get(p);
            }
            if (r == null) {
                r = JsArray.createArray().cast();
            }
            return r;
        }

        private void columnDelta1(SafeHtmlBuilder sb, FileInfo info) {
            sb.openTd().setStyleName(R.css().deltaColumn1());
            if (!Patch.isMagic(info.path()) && !info.binary()) {
                if (showChangeSizeBars) {
                    sb.append(info.linesInserted() + info.linesDeleted());
                } else if (!ChangeType.DELETED.matches(info.status())) {
                    if (ChangeType.ADDED.matches(info.status())) {
                        sb.append(info.linesInserted()).append(" lines");
                    } else {
                        sb.append("+").append(info.linesInserted()).append(", -").append(info.linesDeleted());
                    }
                }
            } else if (info.binary()) {
                sb.append(formatBytes(info.sizeDelta()));
                long oldSize = info.size() - info.sizeDelta();
                if (oldSize != 0) {
                    sb.append(" (").append(formatPercentage(oldSize, info.sizeDelta())).append(")");
                }
            }
            sb.closeTd();
        }

        private void columnDelta2(SafeHtmlBuilder sb, FileInfo info) {
            sb.openTd().setStyleName(R.css().deltaColumn2());
            if (showChangeSizeBars && !Patch.isMagic(info.path()) && !info.binary()
                    && (info.linesInserted() != 0 || info.linesDeleted() != 0)) {
                int w = 80;
                int t = inserted + deleted;
                int i = Math.max(5, (int) (((double) w) * info.linesInserted() / t));
                int d = Math.max(5, (int) (((double) w) * info.linesDeleted() / t));

                sb.setAttribute("title",
                        Util.M.patchTableSize_LongModify(info.linesInserted(), info.linesDeleted()));

                if (0 < info.linesInserted()) {
                    sb.openDiv().setStyleName(R.css().inserted()).setAttribute("style", "width:" + i + "px")
                            .closeDiv();
                }
                if (0 < info.linesDeleted()) {
                    sb.openDiv().setStyleName(R.css().deleted()).setAttribute("style", "width:" + d + "px")
                            .closeDiv();
                }
            }
            sb.closeTd();
        }

        private void footer(SafeHtmlBuilder sb) {
            sb.openTr().setStyleName(R.css().nohover());
            sb.openTh().setStyleName(R.css().pointer()).closeTh();
            if (mode == Mode.REVIEW) {
                sb.openTh().setStyleName(R.css().reviewed()).closeTh();
            } else {
                sb.openTh().setStyleName(R.css().restoreDelete()).closeTh();
            }
            sb.openTh().setStyleName(R.css().status()).closeTh();
            sb.openTd().closeTd(); // path
            sb.openTd().setAttribute("colspan", 3).closeTd(); // comments

            // delta1
            sb.openTh().setStyleName(R.css().deltaColumn1());
            if (hasNonBinaryFile) {
                sb.append(Util.M.patchTableSize_Modify(inserted, deleted));
            }
            if (hasBinaryFile) {
                if (hasNonBinaryFile) {
                    sb.br();
                }
                if (binOldSize != 0) {
                    sb.append(Util.M.patchTableSize_ModifyBinaryFilesWithPercentages(formatAbsBytes(bytesInserted),
                            formatAbsPercentage(binOldSize, bytesInserted), formatAbsBytes(bytesDeleted),
                            formatAbsPercentage(binOldSize, bytesDeleted)));
                } else {
                    sb.append(Util.M.patchTableSize_ModifyBinaryFiles(formatAbsBytes(bytesInserted),
                            formatAbsBytes(bytesDeleted)));
                }
            }
            sb.closeTh();

            // delta2
            sb.openTh().setStyleName(R.css().deltaColumn2());
            if (showChangeSizeBars) {
                int w = 80;
                int t = inserted + deleted;
                int i = Math.max(1, (int) (((double) w) * inserted / t));
                int d = Math.max(1, (int) (((double) w) * deleted / t));
                if (i + d > w && i > d) {
                    i = w - d;
                } else if (i + d > w && d > i) {
                    d = w - i;
                }
                if (0 < inserted) {
                    sb.openDiv().setStyleName(R.css().inserted()).setAttribute("style", "width:" + i + "px")
                            .closeDiv();
                }
                if (0 < deleted) {
                    sb.openDiv().setStyleName(R.css().deleted()).setAttribute("style", "width:" + d + "px")
                            .closeDiv();
                }
            }
            sb.closeTh();

            sb.closeTr();
        }
    }
}