blob: 77a009181f07cf75da5effd152ce62d57b8c0c69 [file] [log] [blame]
// 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 com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.ChangeApi;
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.diff.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.RestApi;
import com.google.gerrit.client.ui.NavigationTable;
import com.google.gerrit.reviewdb.client.Change;
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.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
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 com.google.gwtorm.client.KeyUtil;
import java.sql.Timestamp;
class FileTable extends FlowPanel {
static final FileTableResources R = GWT
.create(FileTableResources.class);
interface FileTableResources extends ClientBundle {
@Source("file_table.css")
FileTableCss css();
}
interface FileTableCss extends CssResource {
String pointer();
String reviewed();
String status();
String pathColumn();
String draftColumn();
String newColumn();
String commentColumn();
String deltaColumn1();
String deltaColumn2();
String commonPrefix();
String inserted();
String deleted();
}
private static final String REVIEWED;
private static final String OPEN;
private static final HyperlinkImpl link = GWT.create(HyperlinkImpl.class);
static {
REVIEWED = DOM.createUniqueId().replace('-', '_');
OPEN = DOM.createUniqueId().replace('-', '_');
init(REVIEWED, OPEN);
}
private static final native void init(String r, String o) /*-{
$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 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();
return false;
}
}
return true;
}
private static MyTable getMyTable(NativeEvent event) {
com.google.gwt.user.client.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 PatchSet.Id base;
private PatchSet.Id curr;
private MyTable table;
private boolean register;
private JsArrayString reviewed;
private String scrollToPath;
@Override
protected void onLoad() {
super.onLoad();
R.css().ensureInjected();
}
void setRevisions(PatchSet.Id base, PatchSet.Id curr) {
this.base = base;
this.curr = curr;
}
void setValue(NativeMap<FileInfo> fileMap,
Timestamp myLastReply,
NativeMap<JsArray<CommentInfo>> comments,
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 markReviewed(JsArrayString reviewed) {
if (table != null) {
table.markReviewed(reviewed);
} else {
this.reviewed = reviewed;
}
}
void registerKeys() {
register = true;
if (table != null) {
table.setRegisterKeys(true);
}
}
void scrollToPath(String path) {
if (table != null) {
table.scrollToPath(path);
} else {
scrollToPath = 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) {
// TODO(sop): Switch to Dispatcher.toPatchSideBySide.
Change.Id c = curr.getParentKey();
StringBuilder p = new StringBuilder();
p.append("/c/").append(c).append('/');
if (base != null) {
p.append(base.get()).append("..");
}
p.append(curr.get()).append('/').append(KeyUtil.encode(info.path()));
p.append(info.binary() ? ",unified" : ",cm");
return p.toString();
}
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()));
keysNavigation.add(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()));
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 != null ? base.toString() + ".." : "")
+ curr.toString());
}
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()) {
Gerrit.display(url(list.get(row - 1)));
}
}
}
private final class DisplayCommand implements RepeatingCommand {
private final SafeHtmlBuilder sb = new SafeHtmlBuilder();
private final MyTable table;
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 boolean attached;
private int row;
private double start;
private ProgressBar meter;
private String lastPath = "";
private int inserted;
private int deleted;
private DisplayCommand(NativeMap<FileInfo> map,
JsArray<FileInfo> list,
Timestamp myLastReply,
NativeMap<JsArray<CommentInfo>> comments,
NativeMap<JsArray<CommentInfo>> drafts) {
this.table = new MyTable(map, list);
this.list = list;
this.myLastReply = myLastReply;
this.comments = comments;
this.drafts = drafts;
this.hasUser = Gerrit.isSignedIn();
}
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);
table.resetHtml(sb);
table.finishDisplay();
setTable(table);
return false;
}
private void computeInsertedDeleted() {
inserted = 0;
deleted = 0;
for (int i = 0; i < list.length(); i++) {
FileInfo info = list.get(i);
if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
inserted += info.lines_inserted();
deleted += info.lines_deleted();
}
}
}
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();
sb.openTh().setStyleName(R.css().pointer()).closeTh();
sb.openTh().setStyleName(R.css().reviewed()).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();
columnReviewed(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 columnStatus(SafeHtmlBuilder sb, FileInfo info) {
sb.openTd().setStyleName(R.css().status());
if (!Patch.COMMIT_MSG.equals(info.path())
&& info.status() != null
&& !ChangeType.MODIFIED.matches(info.status())) {
sb.append(info.status());
}
sb.closeTd();
}
private void columnPath(SafeHtmlBuilder sb, FileInfo info) {
sb.openTd()
.setStyleName(R.css().pathColumn())
.openAnchor()
.setAttribute("href", "#" + url(info))
.setAttribute("onclick", OPEN + "(event," + info._row() + ")");
String path = info.path();
if (Patch.COMMIT_MSG.equals(path)) {
sb.append(Util.C.commitMessage());
} else {
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;
}
sb.closeAnchor()
.closeTd();
}
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 = get(info.path(), comments);
JsArray<CommentInfo> dList = 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> get(String p, NativeMap<JsArray<CommentInfo>> m) {
JsArray<CommentInfo> 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.COMMIT_MSG.equals(info.path()) && !info.binary()) {
sb.append(info.lines_inserted() + info.lines_deleted());
}
sb.closeTd();
}
private void columnDelta2(SafeHtmlBuilder sb, FileInfo info) {
sb.openTd().setStyleName(R.css().deltaColumn2());
if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()
&& (info.lines_inserted() != 0 || info.lines_deleted() != 0)) {
int w = 80;
int t = inserted + deleted;
int i = Math.max(5, (int) (((double) w) * info.lines_inserted() / t));
int d = Math.max(5, (int) (((double) w) * info.lines_deleted() / t));
sb.setAttribute(
"title",
Util.M.patchTableSize_LongModify(info.lines_inserted(),
info.lines_deleted()));
if (0 < info.lines_inserted()) {
sb.openDiv()
.setStyleName(R.css().inserted())
.setAttribute("style", "width:" + i + "px")
.closeDiv();
}
if (0 < info.lines_deleted()) {
sb.openDiv()
.setStyleName(R.css().deleted())
.setAttribute("style", "width:" + d + "px")
.closeDiv();
}
}
sb.closeTd();
}
private void footer(SafeHtmlBuilder sb) {
sb.openTr();
sb.openTh().setStyleName(R.css().pointer()).closeTh();
sb.openTh().setStyleName(R.css().reviewed()).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())
.append(Util.M.patchTableSize_Modify(inserted, deleted))
.closeTh();
// delta2
sb.openTh().setStyleName(R.css().deltaColumn2());
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();
}
}
}