// Copyright (C) 2008 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.changes; import com.google.gerrit.client.Dispatcher; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.patches.PatchScreen; import com.google.gerrit.client.ui.InlineHyperlink; import com.google.gerrit.client.ui.ListenableAccountDiffPreference; import com.google.gerrit.client.ui.NavigationTable; import com.google.gerrit.client.ui.PatchLink; import com.google.gerrit.common.data.PatchSetDetail; import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DiffView; import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.Patch.ChangeType; import com.google.gerrit.reviewdb.client.Patch.Key; import com.google.gerrit.reviewdb.client.Patch.PatchType; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.RepeatingCommand; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTMLTable.Cell; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.Widget; import com.google.gwtexpui.globalkey.client.KeyCommand; import com.google.gwtexpui.progress.client.ProgressBar; import com.google.gwtexpui.safehtml.client.SafeHtml; import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class PatchTable extends Composite { public interface PatchValidator { /** * @param patch * @return true if patch is valid. */ boolean isValid(Patch patch); } public final PatchValidator PREFERENCE_VALIDATOR = new PatchValidator() { @Override public boolean isValid(Patch patch) { return !((listenablePrefs.get().isSkipDeleted() && patch.getChangeType().equals(ChangeType.DELETED)) || (listenablePrefs.get().isSkipUncommented() && patch.getCommentCount() == 0)); } }; private final FlowPanel myBody; private PatchSetDetail detail; private Command onLoadCommand; private MyTable myTable; private String savePointerId; private PatchSet.Id base; private List<Patch> patchList; private Map<Patch.Key, Integer> patchMap; private ListenableAccountDiffPreference listenablePrefs; private List<ClickHandler> clickHandlers; private boolean active; private boolean registerKeys; public PatchTable(ListenableAccountDiffPreference prefs) { listenablePrefs = prefs; myBody = new FlowPanel(); initWidget(myBody); } public PatchTable() { this(new ListenableAccountDiffPreference()); } public int indexOf(Patch.Key patch) { Integer i = patchMap().get(patch); return i != null ? i : -1; } private Map<Key, Integer> patchMap() { if (patchMap == null) { patchMap = new HashMap<>(); for (int i = 0; i < patchList.size(); i++) { patchMap.put(patchList.get(i).getKey(), i); } } return patchMap; } public void display(PatchSet.Id base, PatchSetDetail detail) { this.base = base; this.detail = detail; this.patchList = detail.getPatches(); this.patchMap = null; myTable = null; final DisplayCommand cmd = new DisplayCommand(patchList, base); if (cmd.execute()) { cmd.initMeter(); Scheduler.get().scheduleIncremental(cmd); } else { cmd.showTable(); } } public PatchSet.Id getBase() { return base; } public void setSavePointerId(final String id) { savePointerId = id; } public boolean isLoaded() { return myTable != null; } public void onTableLoaded(final Command cmd) { if (myTable != null) { cmd.execute(); } else { onLoadCommand = cmd; } } public void addClickHandler(final ClickHandler clickHandler) { if (myTable != null) { myTable.addClickHandler(clickHandler); } else { if (clickHandlers == null) { clickHandlers = new ArrayList<>(2); } clickHandlers.add(clickHandler); } } public void setRegisterKeys(final boolean on) { registerKeys = on; if (myTable != null) { myTable.setRegisterKeys(on); } } public void movePointerTo(final Patch.Key k) { if (myTable != null) { myTable.movePointerTo(k); } } public void setActive(boolean active) { this.active = active; if (myTable != null) { myTable.setActive(active); } } public void notifyDraftDelta(final Patch.Key k, final int delta) { if (myTable != null) { myTable.notifyDraftDelta(k, delta); } } private void setMyTable(MyTable table) { myBody.clear(); myBody.add(table); myTable = table; if (clickHandlers != null) { for (ClickHandler ch : clickHandlers) { myTable.addClickHandler(ch); } clickHandlers = null; } if (active) { myTable.setActive(true); active = false; } if (registerKeys) { myTable.setRegisterKeys(registerKeys); registerKeys = false; } myTable.finishDisplay(); } /** * @return a link to the previous file in this patch set, or null. */ public InlineHyperlink getPreviousPatchLink(int index, PatchScreen.Type patchType) { int previousPatchIndex = getPreviousPatch(index, PREFERENCE_VALIDATOR); if (previousPatchIndex < 0) { return null; } return createLink(previousPatchIndex, patchType, SafeHtml.asis(Util.C.prevPatchLinkIcon()), null); } /** * @return a link to the next file in this patch set, or null. */ public InlineHyperlink getNextPatchLink(int index, PatchScreen.Type patchType) { int nextPatchIndex = getNextPatch(index, false, PREFERENCE_VALIDATOR); if (nextPatchIndex < 0) { return null; } return createLink(nextPatchIndex, patchType, null, SafeHtml.asis(Util.C.nextPatchLinkIcon())); } /** * @return a link to the the given patch. * @param index The patch to link to * @param screenType The screen type of patch display * @param before A string to display at the beginning of the href text * @param after A string to display at the end of the href text */ public PatchLink createLink(int index, PatchScreen.Type screenType, SafeHtml before, SafeHtml after) { Patch patch = patchList.get(index); Key thisKey = patch.getKey(); PatchLink link; if (isUnifiedPatchLink(patch, screenType)) { link = new PatchLink.Unified("", base, thisKey, index, detail, this); } else { link = new PatchLink.SideBySide("", base, thisKey, index, detail, this); } SafeHtmlBuilder text = new SafeHtmlBuilder(); text.append(before); text.append(getFileNameOnly(patch)); text.append(after); SafeHtml.set(link, text); return link; } private static boolean isUnifiedPatchLink(final Patch patch, final PatchScreen.Type screenType) { if (Dispatcher.isChangeScreen2()) { return (patch.getPatchType().equals(PatchType.BINARY) || Gerrit.getUserAccount().getGeneralPreferences().getDiffView() .equals(DiffView.UNIFIED_DIFF)); } return screenType == PatchScreen.Type.UNIFIED; } private static String getFileNameOnly(Patch patch) { // Note: use '/' here and not File.pathSeparator since git paths // are always separated by / // String fileName = getDisplayFileName(patch); int s = fileName.lastIndexOf('/'); if (s >= 0) { fileName = fileName.substring(s + 1); } return fileName; } public static String getDisplayFileName(Patch patch) { return getDisplayFileName(patch.getKey()); } public static String getDisplayFileName(Patch.Key patchKey) { if (Patch.COMMIT_MSG.equals(patchKey.get())) { return Util.C.commitMessage(); } return patchKey.get(); } /** * Update the reviewed status for the given patch. */ public void updateReviewedStatus(Patch.Key patchKey, boolean reviewed) { if (myTable != null) { myTable.updateReviewedStatus(patchKey, reviewed); } } public ListenableAccountDiffPreference getPreferences() { return listenablePrefs; } private class MyTable extends NavigationTable<Patch> { private static final int C_PATH = 2; private static final int C_DRAFT = 3; private static final int C_SIZE = 4; private static final int C_SIDEBYSIDE = 5; private int activeRow = -1; MyTable() { 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())); keysNavigation.add(new OpenUnifiedDiffKeyCommand(0, 'O', Util.C .patchTableOpenUnifiedDiff())); table.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { final Cell cell = table.getCellForEvent(event); if (cell != null && cell.getRowIndex() > 0) { movePointerTo(cell.getRowIndex()); } } }); setSavePointerId(PatchTable.this.savePointerId); } public void addClickHandler(final ClickHandler clickHandler) { table.addClickHandler(clickHandler); } void updateReviewedStatus(final Patch.Key patchKey, boolean reviewed) { int idx = patchMap().get(patchKey); if (0 <= idx) { Patch patch = patchList.get(idx); if (patch.isReviewedByCurrentUser() != reviewed) { int row = idx + 1; int col = C_SIDEBYSIDE + 2; if (patch.getPatchType() == Patch.PatchType.BINARY) { col = C_SIDEBYSIDE + 3; } if (reviewed) { table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck())); } else { table.clearCell(row, col); } patch.setReviewedByCurrentUser(reviewed); } } } void notifyDraftDelta(final Patch.Key key, final int delta) { int idx = patchMap().get(key); if (0 <= idx) { Patch p = patchList.get(idx); p.setDraftCount(p.getDraftCount() + delta); SafeHtmlBuilder m = new SafeHtmlBuilder(); appendCommentCount(m, p); SafeHtml.set(table, idx + 1, C_DRAFT, m); } } @Override public void resetHtml(final SafeHtml html) { super.resetHtml(html); } @Override public void movePointerTo(Object oldId) { super.movePointerTo(oldId); } /** Activates / Deactivates the key navigation and the highlighting of the current row for this table */ public void setActive(boolean active) { if (active) { if(activeRow > 0 && getCurrentRow() != activeRow) { super.movePointerTo(activeRow); activeRow = -1; } } else { if(getCurrentRow() > 0) { activeRow = getCurrentRow(); super.movePointerTo(-1); } } setRegisterKeys(active); } void initializeRow(int row) { Patch patch = PatchTable.this.patchList.get(row - 1); setRowItem(row, patch); Widget nameCol; nameCol = new PatchLink.SideBySide(getDisplayFileName(patch), base, patch.getKey(), row - 1, detail, PatchTable.this); if (patch.getSourceFileName() != null) { final String text; if (patch.getChangeType() == Patch.ChangeType.RENAMED) { text = Util.M.renamedFrom(patch.getSourceFileName()); } else if (patch.getChangeType() == Patch.ChangeType.COPIED) { text = Util.M.copiedFrom(patch.getSourceFileName()); } else { text = Util.M.otherFrom(patch.getSourceFileName()); } final Label line = new Label(text); line.setStyleName(Gerrit.RESOURCES.css().sourceFilePath()); final FlowPanel cell = new FlowPanel(); cell.add(nameCol); cell.add(line); nameCol = cell; } table.setWidget(row, C_PATH, nameCol); int C_UNIFIED = C_SIDEBYSIDE + 1; PatchLink sideBySide = new PatchLink.SideBySide(Util.C.patchTableDiffSideBySide(), base, patch.getKey(), row - 1, detail, PatchTable.this); sideBySide.setStyleName("gwt-Anchor"); PatchLink unified = new PatchLink.Unified(Util.C.patchTableDiffUnified(), base, patch.getKey(), row - 1, detail, PatchTable.this); unified.setStyleName("gwt-Anchor"); table.setWidget(row, C_SIDEBYSIDE, sideBySide); table.setWidget(row, C_UNIFIED, unified); } void initializeLastRow(int row) { Anchor sideBySide = new Anchor(Util.C.diffAllSideBySide()); sideBySide.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { for (Patch p : detail.getPatches()) { openWindow(Dispatcher.toPatchSideBySide(base, p.getKey())); } } }); table.setWidget(row, C_SIDEBYSIDE - 2, sideBySide); int C_UNIFIED = C_SIDEBYSIDE - 2 + 1; Anchor unified = new Anchor(Util.C.diffAllUnified()); unified.addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { for (Patch p : detail.getPatches()) { openWindow(Dispatcher.toPatchUnified(base, p.getKey())); } } }); table.setWidget(row, C_UNIFIED, unified); } private void openWindow(String token) { String url = Window.Location.getPath() + "#" + token; Window.open(url, "_blank", null); } void appendHeader(final SafeHtmlBuilder m) { m.openTr(); // Cursor m.openTd(); m.addStyleName(Gerrit.RESOURCES.css().iconHeader()); m.addStyleName(Gerrit.RESOURCES.css().leftMostCell()); m.nbsp(); m.closeTd(); // Mode m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().iconHeader()); m.nbsp(); m.closeTd(); // "File path" m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().dataHeader()); m.append(Util.C.patchTableColumnName()); m.closeTd(); // "Comments" m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().dataHeader()); m.append(Util.C.patchTableColumnComments()); m.closeTd(); // "Size" m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().dataHeader()); m.append(Util.C.patchTableColumnSize()); m.closeTd(); // "Diff" m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().dataHeader()); m.setAttribute("colspan", 3); m.append(Util.C.patchTableColumnDiff()); m.closeTd(); // "Reviewed" if (Gerrit.isSignedIn()) { m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().iconHeader()); m.addStyleName(Gerrit.RESOURCES.css().dataHeader()); m.append(Util.C.reviewed()); m.closeTd(); } m.closeTr(); } void appendRow(final SafeHtmlBuilder m, final Patch p, final boolean isReverseDiff) { m.openTr(); m.openTd(); m.addStyleName(Gerrit.RESOURCES.css().iconCell()); m.addStyleName(Gerrit.RESOURCES.css().leftMostCell()); m.nbsp(); m.closeTd(); m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().changeTypeCell()); if (isReverseDiff) { m.addStyleName(Gerrit.RESOURCES.css().patchCellReverseDiff()); } if (Patch.COMMIT_MSG.equals(p.getFileName())) { m.nbsp(); } else { m.append(p.getChangeType().getCode()); } m.closeTd(); m.openTd(); m.addStyleName(Gerrit.RESOURCES.css().dataCell()); m.addStyleName(Gerrit.RESOURCES.css().filePathCell()); m.closeTd(); m.openTd(); m.addStyleName(Gerrit.RESOURCES.css().dataCell()); m.addStyleName(Gerrit.RESOURCES.css().commentCell()); appendCommentCount(m, p); m.closeTd(); m.openTd(); m.addStyleName(Gerrit.RESOURCES.css().dataCell()); m.addStyleName(Gerrit.RESOURCES.css().patchSizeCell()); if (isReverseDiff) { m.addStyleName(Gerrit.RESOURCES.css().patchCellReverseDiff()); } appendSize(m, p); m.closeTd(); // Diff openlink(m, 2); m.closeTd(); openlink(m, 1); m.closeTd(); // Green check mark if the user is logged in and they reviewed that file if (Gerrit.isSignedIn()) { m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().dataCell()); if (p.isReviewedByCurrentUser()) { m.openDiv(); m.setStyleName(Gerrit.RESOURCES.css().greenCheckClass()); m.closeSelf(); } m.closeTd(); } m.closeTr(); } void appendLastRow(final SafeHtmlBuilder m, int ins, int dels, final boolean isReverseDiff) { m.openTr(); m.openTd(); m.addStyleName(Gerrit.RESOURCES.css().iconCell()); m.addStyleName(Gerrit.RESOURCES.css().noborder()); m.nbsp(); m.closeTd(); m.openTd(); m.setAttribute("colspan", C_SIZE - 1); m.closeTd(); m.openTd(); m.addStyleName(Gerrit.RESOURCES.css().dataCell()); m.addStyleName(Gerrit.RESOURCES.css().patchSizeCell()); m.addStyleName(Gerrit.RESOURCES.css().leftMostCell()); if (isReverseDiff) { m.addStyleName(Gerrit.RESOURCES.css().patchCellReverseDiff()); } m.append(Util.M.patchTableSize_Modify(ins, dels)); m.closeTd(); openlink(m, 2); m.closeTd(); openlink(m, 1); m.closeTd(); m.closeTr(); } void appendCommentCount(final SafeHtmlBuilder m, final Patch p) { if (p.getCommentCount() > 0) { m.append(Util.M.patchTableComments(p.getCommentCount())); } if (p.getDraftCount() > 0) { if (p.getCommentCount() > 0) { m.append(", "); } m.openSpan(); m.setStyleName(Gerrit.RESOURCES.css().drafts()); m.append(Util.M.patchTableDrafts(p.getDraftCount())); m.closeSpan(); } } void appendSize(final SafeHtmlBuilder m, final Patch p) { if (Patch.COMMIT_MSG.equals(p.getFileName())) { m.nbsp(); return; } if (p.getPatchType() == PatchType.UNIFIED) { int ins = p.getInsertions(); int dels = p.getDeletions(); switch (p.getChangeType()) { case ADDED: m.append(Util.M.patchTableSize_Lines(ins)); break; case DELETED: m.nbsp(); break; case MODIFIED: case COPIED: case RENAMED: m.append(Util.M.patchTableSize_Modify(ins, dels)); break; case REWRITE: break; } } else { m.nbsp(); } } private void openlink(final SafeHtmlBuilder m, final int colspan) { m.openTd(); m.addStyleName(Gerrit.RESOURCES.css().dataCell()); m.addStyleName(Gerrit.RESOURCES.css().diffLinkCell()); m.setAttribute("colspan", colspan); } @Override protected Object getRowItemKey(final Patch item) { return item.getKey(); } @Override protected void onOpenRow(final int row) { Widget link = table.getWidget(row, C_PATH); if (link instanceof FlowPanel) { link = ((FlowPanel) link).getWidget(0); } if (link instanceof InlineHyperlink) { ((InlineHyperlink) link).go(); } } private final class OpenUnifiedDiffKeyCommand extends KeyCommand { public OpenUnifiedDiffKeyCommand(int mask, char key, String help) { super(mask, key, help); } @Override public void onKeyPress(KeyPressEvent event) { Widget link = table.getWidget(getCurrentRow(), C_PATH); if (link instanceof FlowPanel) { link = ((FlowPanel) link).getWidget(0); } if (link instanceof PatchLink.Unified) { ((InlineHyperlink) link).go(); } else { link = table.getWidget(getCurrentRow(), C_SIDEBYSIDE + 1); if (link instanceof PatchLink.Unified) { ((InlineHyperlink) link).go(); } } } } } private final class DisplayCommand implements RepeatingCommand { private final MyTable table; private final List<Patch> list; private boolean attached; private SafeHtmlBuilder nc = new SafeHtmlBuilder(); private int stage = 0; private int row; private double start; private ProgressBar meter; private int insertions; private int deletions; private final PatchSet.Id psIdToCompareWith; private DisplayCommand(final List<Patch> list, final PatchSet.Id psIdToCompareWith) { this.table = new MyTable(); this.list = list; this.psIdToCompareWith = psIdToCompareWith; } /** * Add the files contained in the list of patches to the table, one per row. */ @SuppressWarnings("fallthrough") public boolean execute() { final 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; } boolean isReverseDiff = false; if (psIdToCompareWith != null && list.get(0).getKey().getParentKey().get() < psIdToCompareWith.get()) { isReverseDiff = true; } start = System.currentTimeMillis(); switch (stage) { case 0: if (row == 0) { table.appendHeader(nc); table.appendRow(nc, list.get(row++), isReverseDiff); } while (row < list.size()) { Patch p = list.get(row); insertions += p.getInsertions(); deletions += p.getDeletions(); table.appendRow(nc, p, isReverseDiff); if ((++row % 10) == 0 && longRunning()) { updateMeter(); return true; } } table.appendLastRow(nc, insertions, deletions, isReverseDiff); table.resetHtml(nc); table.initializeLastRow(row + 1); nc = null; stage = 1; row = 0; case 1: while (row < list.size()) { table.initializeRow(row + 1); if ((++row % 50) == 0 && longRunning()) { updateMeter(); return true; } } updateMeter(); showTable(); } return false; } void showTable() { setMyTable(table); if (PatchTable.this.onLoadCommand != null) { PatchTable.this.onLoadCommand.execute(); PatchTable.this.onLoadCommand = null; } } void initMeter() { if (meter == null) { meter = new ProgressBar(Util.M.loadingPatchSet(detail.getPatchSet().getId().get())); PatchTable.this.myBody.clear(); PatchTable.this.myBody.add(meter); } updateMeter(); } void updateMeter() { if (meter != null) { final int n = list.size(); meter.setValue(((100 * (stage * n + row)) / (2 * n))); } } private boolean longRunning() { return System.currentTimeMillis() - start > 200; } } /** * Gets the next patch * * @param currentIndex * @param validators * @param loopAround loops back around to the front and traverses if this is * true * @return index of next valid patch, or -1 if no valid patches */ public int getNextPatch(int currentIndex, boolean loopAround, PatchValidator... validators) { return getNextPatchHelper(currentIndex, loopAround, detail.getPatches() .size(), validators); } /** * Helper function for getNextPatch * * @param currentIndex * @param validators * @param loopAround * @param maxIndex will only traverse up to this index * @return index of next valid patch, or -1 if no valid patches */ private int getNextPatchHelper(int currentIndex, boolean loopAround, int maxIndex, PatchValidator... validators) { for (int i = currentIndex + 1; i < maxIndex; i++) { Patch patch = detail.getPatches().get(i); if (patch != null && patchIsValid(patch, validators)) { return i; } } if (loopAround) { return getNextPatchHelper(-1, false, currentIndex, validators); } return -1; } /** * @return the index to the previous patch */ public int getPreviousPatch(int currentIndex, PatchValidator... validators) { for (int i = currentIndex - 1; i >= 0; i--) { Patch patch = detail.getPatches().get(i); if (patch != null && patchIsValid(patch, validators)) { return i; } } return -1; } /** * Helper function that returns whether a patch is valid or not * * @param patch * @param validators * @return whether the patch is valid based on the validators */ private boolean patchIsValid(Patch patch, PatchValidator... validators) { for (PatchValidator v : validators) { if (!v.isValid(patch)) { return false; } } return true; } }