blob: c69f915ea3bf03f3c3747f9ac7ba92b8851ac262 [file] [log] [blame]
// 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.FormatUtil;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.AccountInfo;
import com.google.gerrit.client.projects.ConfigInfoCache;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.client.ui.CommentPanel;
import com.google.gerrit.client.ui.ComplexDisclosurePanel;
import com.google.gerrit.client.ui.ExpandAllCommand;
import com.google.gerrit.client.ui.LinkMenuBar;
import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.data.AccountInfoCache;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.data.ChangeInfo;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.CommentVisibilityStrategy;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
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.i18n.client.LocaleInfo;
import com.google.gwt.user.client.ui.Button;
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.HorizontalPanel;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.globalkey.client.KeyCommand;
import com.google.gwtexpui.globalkey.client.KeyCommandSet;
import java.sql.Timestamp;
import java.util.List;
public class ChangeScreen extends Screen
implements ValueChangeHandler<ChangeDetail> {
private final Change.Id changeId;
private final PatchSet.Id openPatchSetId;
private ChangeDetailCache detailCache;
private com.google.gerrit.client.changes.ChangeInfo changeInfo;
private ChangeDescriptionBlock descriptionBlock;
private ApprovalTable approvals;
private IncludedInTable includedInTable;
private DisclosurePanel includedInPanel;
private ComplexDisclosurePanel dependenciesPanel;
private ChangeTable dependencies;
private ChangeTable.Section dependsOn;
private ChangeTable.Section neededBy;
private PatchSetsBlock patchSetsBlock;
private Panel comments;
private CommentLinkProcessor commentLinkProcessor;
private KeyCommandSet keysNavigation;
private KeyCommandSet keysAction;
private HandlerRegistration regNavigation;
private HandlerRegistration regAction;
private HandlerRegistration regDetailCache;
private Grid patchesGrid;
private ListBox patchesList;
/**
* The change id for which the old version history is valid.
*/
private static Change.Id currentChangeId;
/**
* Which patch set id is the diff base.
*/
private static PatchSet.Id diffBaseId;
public ChangeScreen(final Change.Id toShow) {
changeId = toShow;
openPatchSetId = null;
}
public ChangeScreen(final PatchSet.Id toShow) {
changeId = toShow.getParentKey();
openPatchSetId = toShow;
}
public ChangeScreen(final ChangeInfo c) {
this(c.getId());
}
@Override
protected void onLoad() {
super.onLoad();
detailCache.refresh();
}
@Override
protected void onUnload() {
if (regNavigation != null) {
regNavigation.removeHandler();
regNavigation = null;
}
if (regAction != null) {
regAction.removeHandler();
regAction = null;
}
if (regDetailCache != null) {
regDetailCache.removeHandler();
regDetailCache = null;
}
super.onUnload();
}
@Override
public void registerKeys() {
super.registerKeys();
regNavigation = GlobalKey.add(this, keysNavigation);
regAction = GlobalKey.add(this, keysAction);
if (openPatchSetId != null) {
patchSetsBlock.activate(openPatchSetId);
}
}
@Override
protected void onInitUI() {
super.onInitUI();
ChangeCache cache = ChangeCache.get(changeId);
detailCache = cache.getChangeDetailCache();
regDetailCache = detailCache.addValueChangeHandler(this);
addStyleName(Gerrit.RESOURCES.css().changeScreen());
addStyleName(Gerrit.RESOURCES.css().screenNoHeader());
keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
keysNavigation.add(new UpToListKeyCommand(0, 'u', Util.C.upToChangeList()));
keysNavigation.add(new ExpandCollapseDependencySectionKeyCommand(0, 'd', Util.C.expandCollapseDependencies()));
if (Gerrit.isSignedIn()) {
keysAction.add(new PublishCommentsKeyCommand(0, 'r', Util.C
.keyPublishComments()));
}
descriptionBlock = new ChangeDescriptionBlock(keysAction);
add(descriptionBlock);
approvals = new ApprovalTable();
add(approvals);
includedInPanel = new DisclosurePanel(Util.C.changeScreenIncludedIn());
includedInTable = new IncludedInTable(changeId);
includedInPanel.setContent(includedInTable);
add(includedInPanel);
dependencies = new ChangeTable() {
{
table.setWidth("auto");
}
};
dependsOn = new ChangeTable.Section(Util.C.changeScreenDependsOn());
dependsOn.setChangeRowFormatter(new ChangeTable.ChangeRowFormatter() {
@Override
public String getRowStyle(ChangeInfo c) {
if (! c.isLatest() || Change.Status.ABANDONED.equals(c.getStatus())) {
return Gerrit.RESOURCES.css().outdated();
}
return null;
}
@Override
public String getDisplayText(final ChangeInfo c, final String displayText) {
if (! c.isLatest()) {
return displayText + " [OUTDATED]";
}
return displayText;
}
});
neededBy = new ChangeTable.Section(Util.C.changeScreenNeededBy());
dependencies.addSection(dependsOn);
dependencies.addSection(neededBy);
dependenciesPanel = new ComplexDisclosurePanel(
Util.C.changeScreenDependencies(), false);
dependenciesPanel.setContent(dependencies);
add(dependenciesPanel);
patchesList = new ListBox();
patchesList.addChangeHandler(new ChangeHandler() {
@Override
public void onChange(ChangeEvent event) {
final int index = patchesList.getSelectedIndex();
final String selectedPatchSet = patchesList.getValue(index);
if (index == 0) {
diffBaseId = null;
} else {
diffBaseId = PatchSet.Id.parse(selectedPatchSet);
}
if (patchSetsBlock != null) {
patchSetsBlock.refresh(diffBaseId);
}
}
});
patchesGrid = new Grid(1, 2);
patchesGrid.setStyleName(Gerrit.RESOURCES.css().selectPatchSetOldVersion());
patchesGrid.setText(0, 0, Util.C.referenceVersion());
patchesGrid.setWidget(0, 1, patchesList);
add(patchesGrid);
patchSetsBlock = new PatchSetsBlock();
add(patchSetsBlock);
comments = new FlowPanel();
comments.setStyleName(Gerrit.RESOURCES.css().changeComments());
add(comments);
}
private void displayTitle(final Change.Key changeId, final String subject) {
final StringBuilder titleBuf = new StringBuilder();
if (LocaleInfo.getCurrentLocale().isRTL()) {
if (subject != null) {
titleBuf.append(subject);
titleBuf.append(" :");
}
titleBuf.append(Util.M.changeScreenTitleId(changeId.abbreviate()));
} else {
titleBuf.append(Util.M.changeScreenTitleId(changeId.abbreviate()));
if (subject != null) {
titleBuf.append(": ");
titleBuf.append(subject);
}
}
setPageTitle(titleBuf.toString());
setHeaderVisible(false);
}
@Override
public void onValueChange(final ValueChangeEvent<ChangeDetail> event) {
if (isAttached() && isLastValueChangeHandler()) {
// Until this screen is fully migrated to the new API, these calls must
// happen sequentially after the ChangeDetail lookup, because we can't
// start an async get at the source of every call that might trigger a
// value change.
CallbackGroup cbs = new CallbackGroup();
ConfigInfoCache.get(
event.getValue().getChange().getProject(),
cbs.add(new GerritCallback<ConfigInfoCache.Entry>() {
@Override
public void onSuccess(ConfigInfoCache.Entry result) {
commentLinkProcessor = result.getCommentLinkProcessor();
setTheme(result.getTheme());
}
@Override
public void onFailure(Throwable caught) {
// Handled by last callback's onFailure.
}
}));
ChangeApi.detail(event.getValue().getChange().getId().get(), cbs.add(
new GerritCallback<com.google.gerrit.client.changes.ChangeInfo>() {
@Override
public void onSuccess(
com.google.gerrit.client.changes.ChangeInfo result) {
changeInfo = result;
display(event.getValue());
}
}));
}
}
// Find the last attached screen.
// When DialogBox is used (i. e. CommentedActionDialog) then the original
// ChangeScreen is still in attached state.
// Use here the fact, that the handlers (ChangeScreen) are sorted.
private boolean isLastValueChangeHandler() {
int count = detailCache.getHandlerCount();
return count > 0 && detailCache.getHandler(count - 1) == this;
}
private void display(final ChangeDetail detail) {
displayTitle(detail.getChange().getKey(), detail.getChange().getSubject());
discardDiffBaseIfNotApplicable(detail.getChange().getId());
if (Status.MERGED == detail.getChange().getStatus()) {
includedInPanel.setVisible(true);
includedInPanel.addOpenHandler(includedInTable);
} else {
includedInPanel.setVisible(false);
}
dependencies.setAccountInfoCache(detail.getAccounts());
descriptionBlock.display(detail,
detail.isStarred(),
detail.canEditCommitMessage(),
detail.getCurrentPatchSetDetail().getInfo(),
detail.getAccounts(), detail.getSubmitTypeRecord(),
commentLinkProcessor);
dependsOn.display(detail.getDependsOn());
neededBy.display(detail.getNeededBy());
approvals.display(changeInfo);
patchesList.clear();
if (detail.getCurrentPatchSetDetail().getInfo().getParents().size() > 1) {
patchesList.addItem(Util.C.autoMerge());
} else {
patchesList.addItem(Util.C.baseDiffItem());
}
for (PatchSet pId : detail.getPatchSets()) {
if (patchesList != null) {
patchesList.addItem(Util.M.patchSetHeader(pId.getPatchSetId()), pId
.getId().toString());
}
}
if (diffBaseId != null && patchesList != null) {
patchesList.setSelectedIndex(diffBaseId.get());
}
patchSetsBlock.display(detail, diffBaseId);
addComments(detail);
// If any dependency change is still open, or is outdated,
// or the change is needed by a change that is new or submitted,
// show our dependency list.
//
boolean depsOpen = false;
int outdated = 0;
if (!detail.getChange().getStatus().isClosed()) {
final List<ChangeInfo> dependsOn = detail.getDependsOn();
if (dependsOn != null) {
for (final ChangeInfo ci : dependsOn) {
if (!ci.isLatest()) {
depsOpen = true;
outdated++;
} else if (ci.getStatus() != Change.Status.MERGED) {
depsOpen = true;
}
}
}
}
final List<ChangeInfo> neededBy = detail.getNeededBy();
if (neededBy != null) {
for (final ChangeInfo ci : neededBy) {
if ((ci.getStatus() == Change.Status.NEW) ||
(ci.getStatus() == Change.Status.SUBMITTED) ||
(ci.getStatus() == Change.Status.DRAFT)) {
depsOpen = true;
}
}
}
dependenciesPanel.setOpen(depsOpen);
dependenciesPanel.getHeader().clear();
if (outdated > 0) {
dependenciesPanel.getHeader().add(new InlineLabel(
Util.M.outdatedHeader(outdated)));
}
if (!isCurrentView()) {
display();
}
patchSetsBlock.setRegisterKeys(true);
}
private static void discardDiffBaseIfNotApplicable(final Change.Id toShow) {
if (currentChangeId != null && !currentChangeId.equals(toShow)) {
diffBaseId = null;
}
currentChangeId = toShow;
}
private void addComments(final ChangeDetail detail) {
comments.clear();
final AccountInfoCache accts = detail.getAccounts();
final List<ChangeMessage> msgList = detail.getMessages();
HorizontalPanel title = new HorizontalPanel();
title.setWidth("100%");
title.add(new Label(Util.C.changeScreenComments()));
if (msgList.size() > 1) {
title.add(messagesMenuBar());
}
title.setStyleName(Gerrit.RESOURCES.css().blockHeader());
comments.add(title);
final long AGE = 7 * 24 * 60 * 60 * 1000L;
final Timestamp aged = new Timestamp(System.currentTimeMillis() - AGE);
CommentVisibilityStrategy commentVisibilityStrategy =
CommentVisibilityStrategy.EXPAND_MOST_RECENT;
if (Gerrit.isSignedIn()) {
commentVisibilityStrategy = Gerrit.getUserAccount()
.getGeneralPreferences().getCommentVisibilityStrategy();
}
for (int i = 0; i < msgList.size(); i++) {
final ChangeMessage msg = msgList.get(i);
AccountInfo author;
if (msg.getAuthor() != null) {
author = FormatUtil.asInfo(accts.get(msg.getAuthor()));
} else {
author = AccountInfo.create(0, Util.C.messageNoAuthor(), null);
}
boolean isRecent;
if (i == msgList.size() - 1) {
isRecent = true;
} else {
// TODO Instead of opening messages by strict age, do it by "unread"?
isRecent = msg.getWrittenOn().after(aged);
}
final CommentPanel cp = new CommentPanel(author, msg.getWrittenOn(),
msg.getMessage(), commentLinkProcessor);
cp.setRecent(isRecent);
cp.addStyleName(Gerrit.RESOURCES.css().commentPanelBorder());
if (i == msgList.size() - 1) {
cp.addStyleName(Gerrit.RESOURCES.css().commentPanelLast());
}
boolean isOpen = false;
switch (commentVisibilityStrategy) {
case COLLAPSE_ALL:
break;
case EXPAND_RECENT:
isOpen = isRecent;
break;
case EXPAND_ALL:
isOpen = true;
break;
case EXPAND_MOST_RECENT:
default:
isOpen = i == msgList.size() - 1;
break;
}
cp.setOpen(isOpen);
comments.add(cp);
}
final Button b = new Button(Util.C.changeScreenAddComment());
b.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
PatchSet.Id currentPatchSetId = patchSetsBlock.getCurrentPatchSet().getId();
Gerrit.display(Dispatcher.toPublish(currentPatchSetId));
}
});
comments.add(b);
comments.setVisible(msgList.size() > 0);
}
private LinkMenuBar messagesMenuBar() {
final Panel c = comments;
final LinkMenuBar menuBar = new LinkMenuBar();
menuBar.addItem(Util.C.messageExpandRecent(), new ExpandAllCommand(c, true) {
@Override
protected void expand(final CommentPanel w) {
w.setOpen(w.isRecent());
}
});
menuBar.addItem(Util.C.messageExpandAll(), new ExpandAllCommand(c, true));
menuBar.addItem(Util.C.messageCollapseAll(), new ExpandAllCommand(c, false));
menuBar.addStyleName(Gerrit.RESOURCES.css().commentPanelMenuBar());
return menuBar;
}
public class UpToListKeyCommand extends KeyCommand {
public UpToListKeyCommand(int mask, char key, String help) {
super(mask, key, help);
}
@Override
public void onKeyPress(final KeyPressEvent event) {
Gerrit.displayLastChangeList();
}
}
public class ExpandCollapseDependencySectionKeyCommand extends KeyCommand {
public ExpandCollapseDependencySectionKeyCommand(int mask, char key, String help) {
super(mask, key, help);
}
@Override
public void onKeyPress(KeyPressEvent event) {
dependenciesPanel.setOpen(!dependenciesPanel.isOpen());
}
}
public class PublishCommentsKeyCommand extends NeedsSignInKeyCommand {
public PublishCommentsKeyCommand(int mask, char key, String help) {
super(mask, key, help);
}
@Override
public void onKeyPress(final KeyPressEvent event) {
PatchSet.Id currentPatchSetId = patchSetsBlock.getCurrentPatchSet().getId();
Gerrit.display(Dispatcher.toPublish(currentPatchSetId));
}
}
}