| // 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.patches; |
| |
| import static com.google.gerrit.client.reviewdb.AccountGeneralPreferences.DEFAULT_CONTEXT; |
| import static com.google.gerrit.client.reviewdb.AccountGeneralPreferences.WHOLE_FILE_CONTEXT; |
| |
| import com.google.gerrit.client.Gerrit; |
| import com.google.gerrit.client.Link; |
| import com.google.gerrit.client.changes.ChangeScreen; |
| import com.google.gerrit.client.changes.PatchTable; |
| import com.google.gerrit.client.changes.Util; |
| import com.google.gerrit.client.data.PatchScript; |
| import com.google.gerrit.client.data.PatchScriptSettings; |
| import com.google.gerrit.client.data.PatchSetDetail; |
| import com.google.gerrit.client.data.PatchScriptSettings.Whitespace; |
| import com.google.gerrit.client.reviewdb.AccountGeneralPreferences; |
| import com.google.gerrit.client.reviewdb.Change; |
| import com.google.gerrit.client.reviewdb.Patch; |
| import com.google.gerrit.client.reviewdb.PatchSet; |
| import com.google.gerrit.client.rpc.GerritCallback; |
| import com.google.gerrit.client.ui.ChangeLink; |
| import com.google.gerrit.client.ui.DirectScreenLink; |
| import com.google.gerrit.client.ui.Screen; |
| import com.google.gwt.event.dom.client.ChangeEvent; |
| import com.google.gwt.event.dom.client.ChangeHandler; |
| import com.google.gwt.event.dom.client.KeyPressEvent; |
| import com.google.gwt.event.logical.shared.CloseEvent; |
| import com.google.gwt.event.logical.shared.CloseHandler; |
| import com.google.gwt.event.logical.shared.OpenEvent; |
| import com.google.gwt.event.logical.shared.OpenHandler; |
| import com.google.gwt.event.logical.shared.ValueChangeEvent; |
| import com.google.gwt.event.logical.shared.ValueChangeHandler; |
| import com.google.gwt.event.shared.HandlerRegistration; |
| import com.google.gwt.user.client.History; |
| import com.google.gwt.user.client.rpc.AsyncCallback; |
| import com.google.gwt.user.client.ui.CheckBox; |
| import com.google.gwt.user.client.ui.DisclosurePanel; |
| import com.google.gwt.user.client.ui.FlowPanel; |
| import com.google.gwt.user.client.ui.Grid; |
| import com.google.gwt.user.client.ui.HasHorizontalAlignment; |
| import com.google.gwt.user.client.ui.Label; |
| import com.google.gwt.user.client.ui.ListBox; |
| import com.google.gwt.user.client.ui.Widget; |
| import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; |
| import com.google.gwtexpui.globalkey.client.GlobalKey; |
| import com.google.gwtexpui.globalkey.client.KeyCommand; |
| import com.google.gwtexpui.globalkey.client.KeyCommandSet; |
| import com.google.gwtexpui.safehtml.client.SafeHtml; |
| import com.google.gwtjsonrpc.client.VoidResult; |
| |
| public abstract class PatchScreen extends Screen { |
| public static class SideBySide extends PatchScreen { |
| public SideBySide(final Patch.Key id, final int patchIndex, |
| final PatchTable patchTable) { |
| super(id, patchIndex, patchTable); |
| } |
| |
| @Override |
| protected SideBySideTable createContentTable() { |
| return new SideBySideTable(); |
| } |
| |
| @Override |
| protected PatchScreen.Type getPatchScreenType() { |
| return PatchScreen.Type.SIDE_BY_SIDE; |
| } |
| } |
| |
| public static class Unified extends PatchScreen { |
| public Unified(final Patch.Key id, final int patchIndex, |
| final PatchTable patchTable) { |
| super(id, patchIndex, patchTable); |
| } |
| |
| @Override |
| protected UnifiedDiffTable createContentTable() { |
| return new UnifiedDiffTable(); |
| } |
| |
| @Override |
| protected PatchScreen.Type getPatchScreenType() { |
| return PatchScreen.Type.UNIFIED; |
| } |
| } |
| |
| // Which patch set id's are being diff'ed |
| private static PatchSet.Id diffSideA = null; |
| private static PatchSet.Id diffSideB = null; |
| private static Boolean historyOpen = null; |
| private static final OpenHandler<DisclosurePanel> cacheOpenState = |
| new OpenHandler<DisclosurePanel>() { |
| @Override |
| public void onOpen(OpenEvent<DisclosurePanel> event) { |
| historyOpen = true; |
| } |
| }; |
| private static final CloseHandler<DisclosurePanel> cacheCloseState = |
| new CloseHandler<DisclosurePanel>() { |
| @Override |
| public void onClose(CloseEvent<DisclosurePanel> event) { |
| historyOpen = false; |
| } |
| }; |
| |
| // The change id for which the above patch set id's are valid |
| private static Change.Id currentChangeId = null; |
| |
| protected final Patch.Key patchKey; |
| protected PatchTable fileList; |
| protected PatchSet.Id idSideA; |
| protected PatchSet.Id idSideB; |
| protected final PatchScriptSettings scriptSettings; |
| |
| private DisclosurePanel historyPanel; |
| private HistoryTable historyTable; |
| private FlowPanel contentPanel; |
| private Label noDifference; |
| private AbstractPatchContentTable contentTable; |
| |
| private int rpcSequence; |
| private PatchScript script; |
| private CommentDetail comments; |
| |
| /** The index of the file we are currently looking at among the fileList */ |
| private int patchIndex; |
| |
| /** Keys that cause an action on this screen */ |
| private KeyCommandSet keysNavigation; |
| private HandlerRegistration regNavigation; |
| |
| /** Link to the screen for the previous file, null if not applicable */ |
| private DirectScreenLink previousFileLink; |
| |
| /** Link to the screen for the next file, null if not applicable */ |
| private DirectScreenLink nextFileLink; |
| |
| private static final char SHORTCUT_PREVIOUS_FILE = '['; |
| private static final char SHORTCUT_NEXT_FILE = ']'; |
| |
| /** |
| * How this patch should be displayed in the patch screen. |
| */ |
| public static enum Type { |
| UNIFIED, SIDE_BY_SIDE |
| } |
| |
| protected PatchScreen(final Patch.Key id, final int patchIndex, |
| final PatchTable patchTable) { |
| patchKey = id; |
| fileList = patchTable; |
| |
| // If we have any diff side stored, make sure they are applicable to the |
| // current change, discard them otherwise. |
| // |
| Change.Id thisChangeId = id.getParentKey().getParentKey(); |
| if (currentChangeId != null && !currentChangeId.equals(thisChangeId)) { |
| diffSideA = null; |
| diffSideB = null; |
| historyOpen = null; |
| } |
| currentChangeId = thisChangeId; |
| idSideA = diffSideA; // null here means we're diff'ing from the Base |
| idSideB = diffSideB != null ? diffSideB : id.getParentKey(); |
| this.patchIndex = patchIndex; |
| scriptSettings = new PatchScriptSettings(); |
| |
| initContextLines(); |
| } |
| |
| /** |
| * Initialize the context lines to the user's preference, or to the default |
| * number if the user is not logged in. |
| */ |
| private void initContextLines() { |
| if (Gerrit.isSignedIn()) { |
| final AccountGeneralPreferences p = |
| Gerrit.getUserAccount().getGeneralPreferences(); |
| scriptSettings.setContext(p.getDefaultContext()); |
| } else { |
| scriptSettings.setContext(DEFAULT_CONTEXT); |
| } |
| } |
| |
| @Override |
| protected void onInitUI() { |
| super.onInitUI(); |
| |
| keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation()); |
| keysNavigation.add(new UpToChangeCommand(0, 'u', PatchUtil.C.upToChange())); |
| keysNavigation.add(new FileListCmd(0, 'f', PatchUtil.C.fileList())); |
| |
| historyTable = new HistoryTable(this); |
| historyPanel = new DisclosurePanel(PatchUtil.C.patchHistoryTitle()); |
| historyPanel.setContent(historyTable); |
| historyPanel.setVisible(false); |
| // If the user selected a different patch set than the default for either |
| // side, expand the history panel |
| historyPanel.setOpen(diffSideA != null || diffSideB != null |
| || (historyOpen != null && historyOpen)); |
| historyPanel.addOpenHandler(cacheOpenState); |
| historyPanel.addCloseHandler(cacheCloseState); |
| add(historyPanel); |
| initDisplayControls(); |
| |
| noDifference = new Label(PatchUtil.C.noDifference()); |
| noDifference.setStyleName("gerrit-PatchNoDifference"); |
| noDifference.setVisible(false); |
| |
| contentTable = createContentTable(); |
| contentTable.fileList = fileList; |
| |
| add(createNextPrevLinks()); |
| contentPanel = new FlowPanel(); |
| contentPanel.setStyleName("gerrit-SideBySideScreen-SideBySideTable"); |
| contentPanel.add(noDifference); |
| contentPanel.add(contentTable); |
| add(contentPanel); |
| add(createNextPrevLinks()); |
| |
| // This must be done after calling createNextPrevLinks(), which initializes |
| // these fields |
| if (previousFileLink != null) { |
| installLinkShortCut(previousFileLink, SHORTCUT_PREVIOUS_FILE, PatchUtil.C |
| .previousFileHelp()); |
| } |
| if (nextFileLink != null) { |
| installLinkShortCut(nextFileLink, SHORTCUT_NEXT_FILE, PatchUtil.C |
| .nextFileHelp()); |
| } |
| } |
| |
| private void installLinkShortCut(final DirectScreenLink link, char shortcut, |
| String help) { |
| keysNavigation.add(new KeyCommand(0, shortcut, help) { |
| @Override |
| public void onKeyPress(KeyPressEvent event) { |
| link.go(); |
| } |
| }); |
| } |
| |
| private void initDisplayControls() { |
| final Grid displayControls = new Grid(0, 5); |
| displayControls.setStyleName("gerrit-PatchScreen-DisplayControls"); |
| add(displayControls); |
| |
| createIgnoreWhitespace(displayControls, 0, 0); |
| createContext(displayControls, 0, 2); |
| } |
| |
| /** |
| * Add the contextual widgets for this patch: "Show full files" and |
| * "Keep unreviewed" |
| */ |
| private void createContext(final Grid parent, final int row, final int col) { |
| parent.resizeRows(row + 1); |
| |
| // Show full files |
| final CheckBox cb = new CheckBox(PatchUtil.C.showFullFiles()); |
| cb.addValueChangeHandler(new ValueChangeHandler<Boolean>() { |
| @Override |
| public void onValueChange(ValueChangeEvent<Boolean> event) { |
| if (event.getValue()) { |
| // Show a diff of the full files |
| scriptSettings.setContext(WHOLE_FILE_CONTEXT); |
| } else { |
| // Restore the context lines to the user's preference |
| initContextLines(); |
| } |
| refresh(false /* not the first time */); |
| } |
| }); |
| parent.setWidget(row, col + 1, cb); |
| |
| // "Reviewed" check box |
| if (Gerrit.isSignedIn()) { |
| final CheckBox ku = new CheckBox(PatchUtil.C.reviewed()); |
| ku.addValueChangeHandler(new ValueChangeHandler<Boolean>() { |
| @Override |
| public void onValueChange(ValueChangeEvent<Boolean> event) { |
| setReviewedByCurrentUser(event.getValue()); |
| } |
| }); |
| // Checked by default |
| ku.setValue(true); |
| parent.setWidget(row, col + 2, ku); |
| } |
| |
| } |
| |
| private void setReviewedByCurrentUser(boolean reviewed) { |
| if (fileList != null) { |
| fileList.updateReviewedStatus(patchKey, reviewed); |
| } |
| |
| PatchUtil.DETAIL_SVC.setReviewedByCurrentUser(patchKey, reviewed, |
| new AsyncCallback<VoidResult>() { |
| |
| @Override |
| public void onFailure(Throwable arg0) { |
| // nop |
| } |
| |
| @Override |
| public void onSuccess(VoidResult result) { |
| // nop |
| } |
| |
| }); |
| } |
| |
| private void createIgnoreWhitespace(final Grid parent, final int row, |
| final int col) { |
| parent.resizeRows(row + 1); |
| final ListBox ws = new ListBox(); |
| ws.addItem(PatchUtil.C.whitespaceIGNORE_NONE(), Whitespace.IGNORE_NONE |
| .name()); |
| ws.addItem(PatchUtil.C.whitespaceIGNORE_SPACE_AT_EOL(), |
| Whitespace.IGNORE_SPACE_AT_EOL.name()); |
| ws.addItem(PatchUtil.C.whitespaceIGNORE_SPACE_CHANGE(), |
| Whitespace.IGNORE_SPACE_CHANGE.name()); |
| ws.addItem(PatchUtil.C.whitespaceIGNORE_ALL_SPACE(), |
| Whitespace.IGNORE_ALL_SPACE.name()); |
| ws.addChangeHandler(new ChangeHandler() { |
| @Override |
| public void onChange(ChangeEvent event) { |
| final int sel = ws.getSelectedIndex(); |
| if (0 <= sel) { |
| scriptSettings.setWhitespace(Whitespace.valueOf(ws.getValue(sel))); |
| refresh(false /* not the first time */); |
| } |
| } |
| }); |
| parent.setText(row, col, PatchUtil.C.whitespaceIgnoreLabel()); |
| parent.setWidget(row, col + 1, ws); |
| } |
| |
| private Widget createNextPrevLinks() { |
| final Grid table = new Grid(1, 3); |
| final CellFormatter fmt = table.getCellFormatter(); |
| table.setStyleName("gerrit-SideBySideScreen-LinkTable"); |
| fmt.setHorizontalAlignment(0, 0, HasHorizontalAlignment.ALIGN_LEFT); |
| fmt.setHorizontalAlignment(0, 1, HasHorizontalAlignment.ALIGN_CENTER); |
| fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT); |
| |
| if (fileList != null) { |
| previousFileLink = |
| fileList.getPreviousPatchLink(patchIndex, getPatchScreenType()); |
| table.setWidget(0, 0, previousFileLink); |
| |
| nextFileLink = |
| fileList.getNextPatchLink(patchIndex, getPatchScreenType()); |
| table.setWidget(0, 2, nextFileLink); |
| } |
| |
| final ChangeLink up = |
| new ChangeLink("", patchKey.getParentKey().getParentKey()); |
| SafeHtml.set(up, SafeHtml.asis(Util.C.upToChangeIconLink())); |
| table.setWidget(0, 1, up); |
| |
| return table; |
| } |
| |
| @Override |
| protected void onLoad() { |
| super.onLoad(); |
| refresh(true); |
| } |
| |
| @Override |
| protected void onUnload() { |
| if (regNavigation != null) { |
| regNavigation.removeHandler(); |
| regNavigation = null; |
| } |
| super.onUnload(); |
| } |
| |
| @Override |
| public void registerKeys() { |
| super.registerKeys(); |
| contentTable.setRegisterKeys(contentTable.isVisible()); |
| regNavigation = GlobalKey.add(this, keysNavigation); |
| } |
| |
| protected abstract AbstractPatchContentTable createContentTable(); |
| |
| protected abstract PatchScreen.Type getPatchScreenType(); |
| |
| protected void refresh(final boolean isFirst) { |
| final int rpcseq = ++rpcSequence; |
| script = null; |
| comments = null; |
| |
| // Mark this file reviewed as soon we display the diff screen |
| if (Gerrit.isSignedIn() && isFirst) { |
| setReviewedByCurrentUser(true /* reviewed */); |
| } |
| |
| PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB, |
| scriptSettings, new GerritCallback<PatchScript>() { |
| public void onSuccess(final PatchScript result) { |
| if (rpcSequence == rpcseq) { |
| script = result; |
| onResult(); |
| } |
| } |
| |
| @Override |
| public void onFailure(final Throwable caught) { |
| if (rpcSequence == rpcseq) { |
| super.onFailure(caught); |
| } |
| } |
| }); |
| |
| PatchUtil.DETAIL_SVC.patchComments(patchKey, idSideA, idSideB, |
| new GerritCallback<CommentDetail>() { |
| public void onSuccess(final CommentDetail result) { |
| if (rpcSequence == rpcseq) { |
| comments = result; |
| onResult(); |
| } |
| } |
| |
| @Override |
| public void onFailure(Throwable caught) { |
| // Ignore no such entity, the patch script RPC above would |
| // also notice the problem and report it. |
| // |
| if (!isNoSuchEntity(caught) && rpcSequence == rpcseq) { |
| super.onFailure(caught); |
| } |
| } |
| }); |
| } |
| |
| private void onResult() { |
| if (script != null && comments != null) { |
| final Change.Key cid = script.getChangeId(); |
| final String path = patchKey.get(); |
| String fileName = path; |
| final int last = fileName.lastIndexOf('/'); |
| if (last >= 0) { |
| fileName = fileName.substring(last + 1); |
| } |
| |
| setWindowTitle(PatchUtil.M.patchWindowTitle(cid.abbreviate(), fileName)); |
| setPageTitle(PatchUtil.M.patchPageTitle(cid.abbreviate(), path)); |
| |
| historyTable.display(comments.getHistory()); |
| historyPanel.setVisible(true); |
| |
| // True if there are differences between the two patch sets |
| boolean hasEdits = !script.getEdits().isEmpty(); |
| // True if this change is a mode change or a pure rename/copy |
| boolean hasMeta = !script.getPatchHeader().isEmpty(); |
| |
| boolean hasDifferences = hasEdits || hasMeta; |
| boolean pureMetaChange = !hasEdits && hasMeta; |
| |
| if (contentTable instanceof SideBySideTable && pureMetaChange) { |
| // User asked for SideBySide (or a link guessed, wrong) and we can't |
| // show a binary or pure-rename change there accurately. Switch to |
| // the unified view instead. |
| // |
| contentTable.removeFromParent(); |
| contentTable = new UnifiedDiffTable(); |
| contentTable.fileList = fileList; |
| contentPanel.add(contentTable); |
| History.newItem(Link.toPatchUnified(patchKey), false); |
| } |
| |
| if (hasDifferences) { |
| contentTable.display(patchKey, idSideA, idSideB, script); |
| contentTable.display(comments); |
| contentTable.finishDisplay(); |
| } |
| showPatch(hasDifferences); |
| |
| script = null; |
| comments = null; |
| |
| if (!isCurrentView()) { |
| display(); |
| } |
| } |
| } |
| |
| private void showPatch(final boolean showPatch) { |
| noDifference.setVisible(!showPatch); |
| contentTable.setVisible(showPatch); |
| contentTable.setRegisterKeys(isCurrentView() && showPatch); |
| } |
| |
| public void setSideA(PatchSet.Id patchSetId) { |
| idSideA = patchSetId; |
| diffSideA = patchSetId; |
| } |
| |
| public void setSideB(PatchSet.Id patchSetId) { |
| idSideB = patchSetId; |
| diffSideB = patchSetId; |
| } |
| |
| public class UpToChangeCommand extends KeyCommand { |
| public UpToChangeCommand(int mask, int key, String help) { |
| super(mask, key, help); |
| } |
| |
| @Override |
| public void onKeyPress(final KeyPressEvent event) { |
| final Change.Id ck = patchKey.getParentKey().getParentKey(); |
| Gerrit.display(Link.toChange(ck), new ChangeScreen(ck)); |
| } |
| } |
| |
| public class FileListCmd extends KeyCommand { |
| public FileListCmd(int mask, int key, String help) { |
| super(mask, key, help); |
| } |
| |
| @Override |
| public void onKeyPress(final KeyPressEvent event) { |
| if (fileList == null || fileList.isAttached()) { |
| final PatchSet.Id psid = patchKey.getParentKey(); |
| fileList = new PatchTable(); |
| fileList.setSavePointerId("PatchTable " + psid); |
| Util.DETAIL_SVC.patchSetDetail(psid, |
| new GerritCallback<PatchSetDetail>() { |
| public void onSuccess(final PatchSetDetail result) { |
| fileList.display(psid, result.getPatches()); |
| } |
| }); |
| } |
| |
| final PatchBrowserPopup p = new PatchBrowserPopup(patchKey, fileList); |
| p.open(); |
| } |
| } |
| } |