| // 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.gwt.event.dom.client.KeyCodes.KEY_ENTER; |
| import static com.google.gwt.event.dom.client.KeyCodes.KEY_MAC_ENTER; |
| import static java.util.stream.Collectors.collectingAndThen; |
| import static java.util.stream.Collectors.toList; |
| |
| 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.ReviewInput; |
| import com.google.gerrit.client.changes.ReviewInput.DraftHandling; |
| import com.google.gerrit.client.changes.Util; |
| import com.google.gerrit.client.info.ChangeInfo.ApprovalInfo; |
| import com.google.gerrit.client.info.ChangeInfo.LabelInfo; |
| import com.google.gerrit.client.info.ChangeInfo.MessageInfo; |
| import com.google.gerrit.client.rpc.GerritCallback; |
| 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.CommentLinkProcessor; |
| import com.google.gerrit.common.PageLinks; |
| import com.google.gerrit.common.data.LabelValue; |
| import com.google.gerrit.reviewdb.client.Patch; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gerrit.reviewdb.client.Project; |
| 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.core.client.Scheduler.ScheduledCommand; |
| import com.google.gwt.dom.client.Element; |
| import com.google.gwt.event.dom.client.ClickEvent; |
| import com.google.gwt.event.dom.client.ClickHandler; |
| import com.google.gwt.event.dom.client.KeyDownEvent; |
| import com.google.gwt.event.dom.client.KeyDownHandler; |
| import com.google.gwt.event.dom.client.KeyPressEvent; |
| import com.google.gwt.event.dom.client.KeyPressHandler; |
| import com.google.gwt.event.dom.client.MouseOutEvent; |
| import com.google.gwt.event.dom.client.MouseOutHandler; |
| import com.google.gwt.event.dom.client.MouseOverEvent; |
| import com.google.gwt.event.dom.client.MouseOverHandler; |
| import com.google.gwt.event.logical.shared.ValueChangeEvent; |
| import com.google.gwt.event.logical.shared.ValueChangeHandler; |
| import com.google.gwt.resources.client.CssResource; |
| import com.google.gwt.uibinder.client.UiBinder; |
| import com.google.gwt.uibinder.client.UiField; |
| import com.google.gwt.uibinder.client.UiHandler; |
| import com.google.gwt.user.client.rpc.AsyncCallback; |
| import com.google.gwt.user.client.ui.Button; |
| import com.google.gwt.user.client.ui.CheckBox; |
| import com.google.gwt.user.client.ui.Composite; |
| import com.google.gwt.user.client.ui.FlowPanel; |
| import com.google.gwt.user.client.ui.Grid; |
| import com.google.gwt.user.client.ui.HTMLPanel; |
| import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; |
| import com.google.gwt.user.client.ui.PopupPanel; |
| import com.google.gwt.user.client.ui.RadioButton; |
| import com.google.gwt.user.client.ui.ScrollPanel; |
| import com.google.gwt.user.client.ui.TextArea; |
| import com.google.gwt.user.client.ui.UIObject; |
| import com.google.gwt.user.client.ui.Widget; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.TreeSet; |
| |
| public class ReplyBox extends Composite { |
| interface Binder extends UiBinder<HTMLPanel, ReplyBox> {} |
| |
| private static final Binder uiBinder = GWT.create(Binder.class); |
| |
| interface Styles extends CssResource { |
| String label_name(); |
| |
| String label_value(); |
| |
| String label_help(); |
| } |
| |
| private final CommentLinkProcessor clp; |
| private final Project.NameKey project; |
| private final PatchSet.Id psId; |
| private final String revision; |
| private ReviewInput in = ReviewInput.create(); |
| private int labelHelpColumn; |
| private LocalComments lc; |
| |
| @UiField Styles style; |
| @UiField TextArea message; |
| @UiField Element labelsParent; |
| @UiField Grid labelsTable; |
| @UiField Button post; |
| @UiField Button cancel; |
| @UiField ScrollPanel commentsPanel; |
| @UiField FlowPanel comments; |
| |
| ReplyBox( |
| CommentLinkProcessor clp, |
| Project.NameKey project, |
| PatchSet.Id psId, |
| String revision, |
| NativeMap<LabelInfo> all, |
| NativeMap<JsArrayString> permitted) { |
| this.clp = clp; |
| this.project = project; |
| this.psId = psId; |
| this.revision = revision; |
| this.lc = new LocalComments(project, psId.getParentKey()); |
| initWidget(uiBinder.createAndBindUi(this)); |
| |
| List<String> names = |
| permitted.keySet().stream() |
| .sorted() |
| .collect(collectingAndThen(toList(), Collections::unmodifiableList)); |
| if (names.isEmpty()) { |
| UIObject.setVisible(labelsParent, false); |
| } else { |
| renderLabels(names, all, permitted); |
| } |
| |
| addDomHandler( |
| new KeyDownHandler() { |
| @Override |
| public void onKeyDown(KeyDownEvent e) { |
| e.stopPropagation(); |
| if ((e.getNativeKeyCode() == KEY_ENTER || e.getNativeKeyCode() == KEY_MAC_ENTER) |
| && (e.isControlKeyDown() || e.isMetaKeyDown())) { |
| e.preventDefault(); |
| if (post.isEnabled()) { |
| onPost(null); |
| } |
| } |
| } |
| }, |
| KeyDownEvent.getType()); |
| addDomHandler( |
| new KeyPressHandler() { |
| @Override |
| public void onKeyPress(KeyPressEvent e) { |
| e.stopPropagation(); |
| } |
| }, |
| KeyPressEvent.getType()); |
| } |
| |
| @Override |
| protected void onLoad() { |
| commentsPanel.setVisible(false); |
| post.setEnabled(false); |
| if (lc.hasReplyComment()) { |
| message.setText(lc.getReplyComment()); |
| lc.removeReplyComment(); |
| } |
| ChangeApi.drafts(project.get(), psId.getParentKey().get()) |
| .get( |
| new AsyncCallback<NativeMap<JsArray<CommentInfo>>>() { |
| @Override |
| public void onSuccess(NativeMap<JsArray<CommentInfo>> result) { |
| displayComments(result); |
| post.setEnabled(true); |
| } |
| |
| @Override |
| public void onFailure(Throwable caught) { |
| post.setEnabled(true); |
| } |
| }); |
| |
| Scheduler.get() |
| .scheduleDeferred( |
| new ScheduledCommand() { |
| @Override |
| public void execute() { |
| message.setFocus(true); |
| } |
| }); |
| Scheduler.get() |
| .scheduleFixedDelay( |
| new RepeatingCommand() { |
| @Override |
| public boolean execute() { |
| String t = message.getText(); |
| if (t != null) { |
| message.setCursorPos(t.length()); |
| } |
| return false; |
| } |
| }, |
| 0); |
| } |
| |
| @UiHandler("post") |
| void onPost(@SuppressWarnings("unused") ClickEvent e) { |
| postReview(); |
| } |
| |
| void quickApprove(ReviewInput quickApproveInput) { |
| in.mergeLabels(quickApproveInput); |
| postReview(); |
| } |
| |
| boolean hasMessage() { |
| return !message.getText().trim().isEmpty(); |
| } |
| |
| private void postReview() { |
| in.message(message.getText().trim()); |
| // Don't send any comments in the request; just publish everything, even if |
| // e.g. a draft was modified in another tab since we last looked it up. |
| in.drafts(DraftHandling.PUBLISH_ALL_REVISIONS); |
| in.prePost(); |
| ChangeApi.revision(project.get(), psId.getParentKey().get(), revision) |
| .view("review") |
| .post( |
| in, |
| new GerritCallback<ReviewInput>() { |
| @Override |
| public void onSuccess(ReviewInput result) { |
| Gerrit.display(PageLinks.toChange(project, psId)); |
| } |
| |
| @Override |
| public void onFailure(Throwable caught) { |
| if (RestApi.isNotSignedIn(caught)) { |
| lc.setReplyComment(message.getText()); |
| } |
| super.onFailure(caught); |
| } |
| }); |
| hide(); |
| } |
| |
| @UiHandler("cancel") |
| void onCancel(@SuppressWarnings("unused") ClickEvent e) { |
| message.setText(""); |
| hide(); |
| } |
| |
| void replyTo(MessageInfo msg) { |
| if (msg.message() != null) { |
| String t = message.getText(); |
| String m = quote(removePatchSetHeaderLine(msg.message())); |
| if (t == null || t.isEmpty()) { |
| t = m; |
| } else if (t.endsWith("\n\n")) { |
| t += m; |
| } else if (t.endsWith("\n")) { |
| t += "\n" + m; |
| } else { |
| t += "\n\n" + m; |
| } |
| message.setText(t); |
| } |
| } |
| |
| private static String removePatchSetHeaderLine(String msg) { |
| msg = msg.trim(); |
| if (msg.startsWith("Patch Set ")) { |
| int i = msg.indexOf('\n'); |
| if (i > 0) { |
| msg = msg.substring(i + 1).trim(); |
| } |
| } |
| return msg; |
| } |
| |
| public static String quote(String msg) { |
| msg = msg.trim(); |
| StringBuilder quotedMsg = new StringBuilder(); |
| for (String line : msg.split("\\n")) { |
| line = line.trim(); |
| while (line.length() > 67) { |
| int i = line.lastIndexOf(' ', 67); |
| if (i < 50) { |
| i = line.indexOf(' ', 67); |
| } |
| if (i > 0) { |
| quotedMsg.append(" > ").append(line.substring(0, i)).append("\n"); |
| line = line.substring(i + 1); |
| } else { |
| break; |
| } |
| } |
| quotedMsg.append(" > ").append(line).append("\n"); |
| } |
| quotedMsg.append("\n"); |
| return quotedMsg.toString(); |
| } |
| |
| private void hide() { |
| for (Widget w = getParent(); w != null; w = w.getParent()) { |
| if (w instanceof PopupPanel) { |
| ((PopupPanel) w).hide(); |
| break; |
| } |
| } |
| } |
| |
| private void renderLabels( |
| List<String> names, NativeMap<LabelInfo> all, NativeMap<JsArrayString> permitted) { |
| TreeSet<Short> values = new TreeSet<>(); |
| List<LabelAndValues> labels = new ArrayList<>(permitted.size()); |
| for (String id : names) { |
| JsArrayString p = permitted.get(id); |
| if (p != null) { |
| if (!all.containsKey(id)) { |
| continue; |
| } |
| Set<Short> a = new TreeSet<>(); |
| for (int i = 0; i < p.length(); i++) { |
| a.add(LabelInfo.parseValue(p.get(i))); |
| } |
| labels.add(new LabelAndValues(all.get(id), a)); |
| values.addAll(a); |
| } |
| } |
| List<Short> columns = new ArrayList<>(values); |
| |
| labelsTable.resize(1 + labels.size(), 2 + values.size()); |
| for (int c = 0; c < columns.size(); c++) { |
| labelsTable.setText(0, 1 + c, LabelValue.formatValue(columns.get(c))); |
| labelsTable.getCellFormatter().setStyleName(0, 1 + c, style.label_value()); |
| } |
| |
| List<LabelAndValues> checkboxes = new ArrayList<>(labels.size()); |
| int row = 1; |
| for (LabelAndValues lv : labels) { |
| if (isCheckBox(lv.info.valueSet())) { |
| checkboxes.add(lv); |
| } else { |
| renderRadio(row++, columns, lv); |
| } |
| } |
| for (LabelAndValues lv : checkboxes) { |
| renderCheckBox(row++, lv); |
| } |
| } |
| |
| private Short normalizeDefaultValue(Short defaultValue, Set<Short> permittedValues) { |
| Short pmin = Collections.min(permittedValues); |
| Short pmax = Collections.max(permittedValues); |
| Short dv = defaultValue; |
| if (dv > pmax) { |
| dv = pmax; |
| } else if (dv < pmin) { |
| dv = pmin; |
| } |
| return dv; |
| } |
| |
| private void renderRadio(int row, List<Short> columns, LabelAndValues lv) { |
| String id = lv.info.name(); |
| Short dv = normalizeDefaultValue(lv.info.defaultValue(), lv.permitted); |
| |
| labelHelpColumn = 1 + columns.size(); |
| labelsTable.setText(row, 0, id); |
| |
| CellFormatter fmt = labelsTable.getCellFormatter(); |
| fmt.setStyleName(row, 0, style.label_name()); |
| fmt.setStyleName(row, labelHelpColumn, style.label_help()); |
| |
| ApprovalInfo self = |
| Gerrit.isSignedIn() ? lv.info.forUser(Gerrit.getUserAccount()._accountId()) : null; |
| |
| final LabelRadioGroup group = new LabelRadioGroup(row, id, lv.permitted.size()); |
| for (int i = 0; i < columns.size(); i++) { |
| Short v = columns.get(i); |
| if (lv.permitted.contains(v)) { |
| String text = lv.info.valueText(LabelValue.formatValue(v)); |
| LabelRadioButton b = new LabelRadioButton(group, text, v); |
| if ((self != null && v == self.value()) || (self == null && v.equals(dv))) { |
| b.setValue(true); |
| group.select(b); |
| in.label(group.label, v); |
| labelsTable.setText(row, labelHelpColumn, b.text); |
| } |
| group.buttons.add(b); |
| labelsTable.setWidget(row, 1 + i, b); |
| } |
| } |
| } |
| |
| private void renderCheckBox(int row, LabelAndValues lv) { |
| ApprovalInfo self = |
| Gerrit.isSignedIn() ? lv.info.forUser(Gerrit.getUserAccount()._accountId()) : null; |
| |
| final String id = lv.info.name(); |
| final CheckBox b = new CheckBox(); |
| b.setText(id); |
| b.setEnabled(lv.permitted.contains((short) 1)); |
| if (self != null && self.value() == 1) { |
| b.setValue(true); |
| } |
| b.addValueChangeHandler( |
| new ValueChangeHandler<Boolean>() { |
| @Override |
| public void onValueChange(ValueChangeEvent<Boolean> event) { |
| in.label(id, event.getValue() ? (short) 1 : (short) 0); |
| } |
| }); |
| b.setStyleName(style.label_name()); |
| labelsTable.setWidget(row, 0, b); |
| |
| CellFormatter fmt = labelsTable.getCellFormatter(); |
| fmt.setStyleName(row, labelHelpColumn, style.label_help()); |
| labelsTable.setText(row, labelHelpColumn, lv.info.valueText("+1")); |
| } |
| |
| private static boolean isCheckBox(Set<Short> values) { |
| return values.size() == 2 && values.contains((short) 0) && values.contains((short) 1); |
| } |
| |
| private void displayComments(NativeMap<JsArray<CommentInfo>> m) { |
| comments.clear(); |
| |
| JsArray<CommentInfo> l = m.get(Patch.COMMIT_MSG); |
| if (l != null) { |
| comments.add( |
| new FileComments( |
| clp, project, psId, Util.C.commitMessage(), copyPath(Patch.COMMIT_MSG, l))); |
| } |
| l = m.get(Patch.MERGE_LIST); |
| if (l != null) { |
| comments.add( |
| new FileComments( |
| clp, project, psId, Util.C.commitMessage(), copyPath(Patch.MERGE_LIST, l))); |
| } |
| |
| List<String> paths = |
| m.keySet().stream() |
| .sorted() |
| .collect(collectingAndThen(toList(), Collections::unmodifiableList)); |
| |
| for (String path : paths) { |
| if (!Patch.isMagic(path)) { |
| comments.add(new FileComments(clp, project, psId, path, copyPath(path, m.get(path)))); |
| } |
| } |
| |
| commentsPanel.setVisible(comments.getWidgetCount() > 0); |
| } |
| |
| private static List<CommentInfo> copyPath(String path, JsArray<CommentInfo> l) { |
| for (int i = 0; i < l.length(); i++) { |
| l.get(i).path(path); |
| } |
| return Natives.asList(l); |
| } |
| |
| private static class LabelAndValues { |
| final LabelInfo info; |
| final Set<Short> permitted; |
| |
| LabelAndValues(LabelInfo info, Set<Short> permitted) { |
| this.info = info; |
| this.permitted = permitted; |
| } |
| } |
| |
| private class LabelRadioGroup { |
| final int row; |
| final String label; |
| final List<LabelRadioButton> buttons; |
| LabelRadioButton selected; |
| |
| LabelRadioGroup(int row, String label, int cnt) { |
| this.row = row; |
| this.label = label; |
| this.buttons = new ArrayList<>(cnt); |
| } |
| |
| void select(LabelRadioButton b) { |
| selected = b; |
| labelsTable.setText(row, labelHelpColumn, b.text); |
| } |
| } |
| |
| private class LabelRadioButton extends RadioButton |
| implements ValueChangeHandler<Boolean>, ClickHandler, MouseOverHandler, MouseOutHandler { |
| private final LabelRadioGroup group; |
| private final String text; |
| private final short value; |
| |
| LabelRadioButton(LabelRadioGroup group, String text, short value) { |
| super(group.label); |
| this.group = group; |
| this.text = text; |
| this.value = value; |
| addValueChangeHandler(this); |
| addClickHandler(this); |
| addMouseOverHandler(this); |
| addMouseOutHandler(this); |
| } |
| |
| @Override |
| public void onValueChange(ValueChangeEvent<Boolean> event) { |
| if (event.getValue()) { |
| select(); |
| } |
| } |
| |
| @Override |
| public void onClick(ClickEvent event) { |
| select(); |
| } |
| |
| void select() { |
| group.select(this); |
| in.label(group.label, value); |
| } |
| |
| @Override |
| public void onMouseOver(MouseOverEvent event) { |
| labelsTable.setText(group.row, labelHelpColumn, text); |
| } |
| |
| @Override |
| public void onMouseOut(MouseOutEvent event) { |
| LabelRadioButton b = group.selected; |
| String s = b != null ? b.text : ""; |
| labelsTable.setText(group.row, labelHelpColumn, s); |
| } |
| } |
| } |