| // 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.change.LocalComments; |
| 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.rpc.RestApi; |
| import com.google.gerrit.client.ui.CommentLinkProcessor; |
| import com.google.gerrit.common.Nullable; |
| 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.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; |
| @Nullable private final Project.NameKey project; |
| 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, |
| @Nullable Project.NameKey pj, |
| PatchSet.Id id, |
| CommentInfo info, |
| boolean expandAllComments) { |
| super(group, info.range()); |
| |
| linkProcessor = clp; |
| psId = id; |
| project = pj; |
| 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() : ""; |
| 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; |
| final LocalComments lc = new LocalComments(project, psId); |
| 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; |
| if (RestApi.isNotSignedIn(e)) { |
| CommentInfo saved = CommentInfo.copy(comment); |
| saved.message(editArea.getValue().trim()); |
| lc.setInlineComment(saved); |
| } |
| super.onFailure(e); |
| } |
| }; |
| if (input.id() == null) { |
| CommentApi.createDraft(Project.NameKey.asStringOrNull(project), psId, input, group.add(cb)); |
| } else { |
| CommentApi.updateDraft( |
| Project.NameKey.asStringOrNull(project), 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( |
| Project.NameKey.asStringOrNull(project), |
| 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; |
| } |
| 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() : ""); |
| } |
| } |