blob: 21b2f503b899453e0dbe5819e456d6ac7b176a13 [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.diff;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.changes.CommentApi;
import com.google.gerrit.client.changes.CommentInfo;
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.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DoubleClickEvent;
import com.google.gwt.event.dom.client.DoubleClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
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.Timer;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.globalkey.client.NpTextArea;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import net.codemirror.lib.CodeMirror;
/** An HtmlPanel for displaying and editing a draft */
class DraftBox extends CommentBox {
interface Binder extends UiBinder<HTMLPanel, DraftBox> {}
private static final Binder uiBinder = GWT.create(Binder.class);
private static final int INITIAL_LINES = 5;
private static final int MAX_LINES = 30;
private final CommentLinkProcessor linkProcessor;
private final PatchSet.Id psId;
private final boolean expandAll;
private CommentInfo comment;
private PublishedBox replyToBox;
private Timer expandTimer;
private Timer resizeTimer;
private int editAreaHeight;
private boolean autoClosed;
private CallbackGroup pendingGroup;
@UiField Widget header;
@UiField Element summary;
@UiField Element date;
@UiField Element p_view;
@UiField HTML message;
@UiField Button edit;
@UiField Button discard1;
@UiField Element p_edit;
@UiField NpTextArea editArea;
@UiField Button save;
@UiField Button cancel;
@UiField Button discard2;
DraftBox(
CommentGroup group,
CommentLinkProcessor clp,
PatchSet.Id id,
CommentInfo info,
boolean expandAllComments) {
super(group, info.range());
linkProcessor = clp;
psId = id;
expandAll = expandAllComments;
initWidget(uiBinder.createAndBindUi(this));
expandTimer = new Timer() {
@Override
public void run() {
expandText();
}
};
set(info);
header.addDomHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
if (!isEdit()) {
if (autoClosed && !isOpen()) {
setOpen(true);
setEdit(true);
} else {
setOpen(!isOpen());
}
}
}
}, ClickEvent.getType());
addDomHandler(new DoubleClickHandler() {
@Override
public void onDoubleClick(DoubleClickEvent event) {
if (isEdit()) {
editArea.setFocus(true);
} else {
setOpen(true);
setEdit(true);
}
}
}, DoubleClickEvent.getType());
initResizeHandler();
}
private void set(CommentInfo info) {
autoClosed = !expandAll && info.message() != null && info.message().length() < 70;
date.setInnerText(FormatUtil.shortFormatDayTime(info.updated()));
if (info.message() != null) {
String msg = info.message().trim();
summary.setInnerText(msg);
message.setHTML(linkProcessor.apply(
new SafeHtmlBuilder().append(msg).wikify()));
}
comment = info;
}
@Override
CommentInfo getCommentInfo() {
return comment;
}
@Override
boolean isOpen() {
return UIObject.isVisible(p_view);
}
@Override
void setOpen(boolean open) {
UIObject.setVisible(summary, !open);
UIObject.setVisible(p_view, open);
super.setOpen(open);
}
private void expandText() {
double cols = editArea.getCharacterWidth();
int rows = 2;
for (String line : editArea.getValue().split("\n")) {
rows += Math.ceil((1.0 + line.length()) / cols);
}
rows = Math.max(INITIAL_LINES, Math.min(rows, MAX_LINES));
if (editArea.getVisibleLines() != rows) {
editArea.setVisibleLines(rows);
}
editAreaHeight = editArea.getOffsetHeight();
getCommentGroup().resize();
}
boolean isEdit() {
return UIObject.isVisible(p_edit);
}
void setEdit(boolean edit) {
UIObject.setVisible(summary, false);
UIObject.setVisible(p_view, !edit);
UIObject.setVisible(p_edit, edit);
setRangeHighlight(edit);
if (edit) {
String msg = comment.message() != null
? comment.message().trim()
: "";
editArea.setValue(msg);
cancel.setVisible(!isNew());
expandText();
editAreaHeight = editArea.getOffsetHeight();
final int len = msg.length();
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
editArea.setFocus(true);
if (len > 0) {
editArea.setCursorPos(len);
}
}
});
} else {
expandTimer.cancel();
resizeTimer.cancel();
}
getCommentManager().setUnsaved(this, edit);
getCommentGroup().resize();
}
PublishedBox getReplyToBox() {
return replyToBox;
}
void setReplyToBox(PublishedBox box) {
replyToBox = box;
}
@Override
protected void onUnload() {
expandTimer.cancel();
resizeTimer.cancel();
super.onUnload();
}
private void removeUI() {
if (replyToBox != null) {
replyToBox.unregisterReplyBox();
}
getCommentManager().setUnsaved(this, false);
setRangeHighlight(false);
clearRange();
getAnnotation().remove();
getCommentGroup().remove(this);
getCm().focus();
}
private void restoreSelection() {
if (getFromTo() != null && comment.inReplyTo() == null) {
getCm().setSelection(getFromTo().from(), getFromTo().to());
}
}
@UiHandler("message")
void onMessageClick(ClickEvent e) {
e.stopPropagation();
}
@UiHandler("message")
void onMessageDoubleClick(@SuppressWarnings("unused") DoubleClickEvent e) {
setEdit(true);
}
@UiHandler("edit")
void onEdit(ClickEvent e) {
e.stopPropagation();
setEdit(true);
}
@UiHandler("save")
void onSave(ClickEvent e) {
e.stopPropagation();
CallbackGroup group = new CallbackGroup();
save(group);
group.done();
}
void save(CallbackGroup group) {
if (pendingGroup != null) {
pendingGroup.addListener(group);
return;
}
String message = editArea.getValue().trim();
if (message.length() == 0) {
return;
}
CommentInfo input = CommentInfo.copy(comment);
input.message(message);
enableEdit(false);
pendingGroup = group;
GerritCallback<CommentInfo> cb = new GerritCallback<CommentInfo>() {
@Override
public void onSuccess(CommentInfo result) {
enableEdit(true);
pendingGroup = null;
set(result);
setEdit(false);
if (autoClosed) {
setOpen(false);
}
getCommentManager().setUnsaved(DraftBox.this, false);
}
@Override
public void onFailure(Throwable e) {
enableEdit(true);
pendingGroup = null;
super.onFailure(e);
}
};
if (input.id() == null) {
CommentApi.createDraft(psId, input, group.add(cb));
} else {
CommentApi.updateDraft(psId, input.id(), input, group.add(cb));
}
CodeMirror cm = getCm();
cm.vim().handleKey("<Esc>");
cm.focus();
}
private void enableEdit(boolean on) {
editArea.setEnabled(on);
save.setEnabled(on);
cancel.setEnabled(on);
discard2.setEnabled(on);
}
@UiHandler("cancel")
void onCancel(ClickEvent e) {
e.stopPropagation();
if (isNew() && !isDirty()) {
removeUI();
restoreSelection();
} else {
setEdit(false);
if (autoClosed) {
setOpen(false);
}
getCm().focus();
}
}
@UiHandler({"discard1", "discard2"})
void onDiscard(ClickEvent e) {
e.stopPropagation();
if (isNew()) {
removeUI();
restoreSelection();
} else {
setEdit(false);
pendingGroup = new CallbackGroup();
CommentApi.deleteDraft(psId, comment.id(),
pendingGroup.addFinal(new GerritCallback<JavaScriptObject>() {
@Override
public void onSuccess(JavaScriptObject result) {
pendingGroup = null;
removeUI();
}
}));
}
}
@UiHandler("editArea")
void onKeyDown(KeyDownEvent e) {
resizeTimer.cancel();
if ((e.isControlKeyDown() || e.isMetaKeyDown())
&& !e.isAltKeyDown() && !e.isShiftKeyDown()) {
switch (e.getNativeKeyCode()) {
case 's':
case 'S':
e.preventDefault();
CallbackGroup group = new CallbackGroup();
save(group);
group.done();
return;
}
} else if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE && !isDirty()) {
if (isNew()) {
removeUI();
restoreSelection();
return;
} else {
setEdit(false);
if (autoClosed) {
setOpen(false);
}
getCm().focus();
return;
}
}
expandTimer.schedule(250);
}
@UiHandler("editArea")
void onBlur(@SuppressWarnings("unused") BlurEvent e) {
resizeTimer.cancel();
}
private void initResizeHandler() {
resizeTimer = new Timer() {
@Override
public void run() {
getCommentGroup().resize();
}
};
addDomHandler(new MouseMoveHandler() {
@Override
public void onMouseMove(MouseMoveEvent event) {
int h = editArea.getOffsetHeight();
if (isEdit() && h != editAreaHeight) {
getCommentGroup().resize();
resizeTimer.scheduleRepeating(50);
editAreaHeight = h;
}
}
}, MouseMoveEvent.getType());
addDomHandler(new MouseUpHandler() {
@Override
public void onMouseUp(MouseUpEvent event) {
resizeTimer.cancel();
getCommentGroup().resize();
}
}, MouseUpEvent.getType());
}
private boolean isNew() {
return comment.id() == null;
}
private boolean isDirty() {
String msg = editArea.getValue().trim();
if (isNew()) {
return msg.length() > 0;
}
return !msg.equals(comment.message() != null
? comment.message().trim()
: "");
}
}