| //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 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.changes.PatchTable; |
| import com.google.gerrit.client.changes.Util; |
| 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.NavigationTable; |
| import com.google.gerrit.client.ui.NeedsSignInKeyCommand; |
| import com.google.gerrit.common.data.AccountInfoCache; |
| import com.google.gerrit.common.data.CommentDetail; |
| import com.google.gerrit.common.data.PatchScript; |
| import com.google.gerrit.common.data.PatchSetDetail; |
| import com.google.gerrit.prettify.client.ClientSideFormatter; |
| import com.google.gerrit.prettify.client.PrettyFormatter; |
| import com.google.gerrit.prettify.client.SparseHtmlFile; |
| import com.google.gerrit.prettify.common.SparseFileContent; |
| import com.google.gerrit.reviewdb.client.AccountDiffPreference; |
| import com.google.gerrit.reviewdb.client.Patch; |
| import com.google.gerrit.reviewdb.client.PatchLineComment; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gwt.core.client.GWT; |
| import com.google.gwt.event.dom.client.BlurEvent; |
| import com.google.gwt.event.dom.client.BlurHandler; |
| 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.FocusEvent; |
| import com.google.gwt.event.dom.client.FocusHandler; |
| import com.google.gwt.event.dom.client.KeyCodes; |
| import com.google.gwt.event.dom.client.KeyPressEvent; |
| import com.google.gwt.event.shared.HandlerRegistration; |
| import com.google.gwt.user.client.DOM; |
| import com.google.gwt.user.client.Element; |
| import com.google.gwt.user.client.History; |
| import com.google.gwt.user.client.ui.Button; |
| import com.google.gwt.user.client.ui.Focusable; |
| import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; |
| import com.google.gwt.user.client.ui.Image; |
| import com.google.gwt.user.client.ui.UIObject; |
| import com.google.gwt.user.client.ui.Widget; |
| 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.SafeHtmlBuilder; |
| import com.google.gwtorm.client.KeyUtil; |
| |
| import org.eclipse.jgit.diff.Edit; |
| |
| import java.sql.Timestamp; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| public abstract class AbstractPatchContentTable extends NavigationTable<Object> |
| implements CommentEditorContainer, FocusHandler, BlurHandler { |
| public static final int R_HEAD = 0; |
| static final short FILE_SIDE_A = (short) 0; |
| static final short FILE_SIDE_B = (short) 1; |
| protected PatchTable fileList; |
| protected AccountInfoCache accountCache = AccountInfoCache.empty(); |
| protected Patch.Key patchKey; |
| protected PatchSet.Id idSideA; |
| protected PatchSet.Id idSideB; |
| protected boolean onlyOneHunk; |
| protected PatchSetSelectBox headerSideA; |
| protected PatchSetSelectBox headerSideB; |
| protected Image iconA; |
| protected Image iconB; |
| |
| private final KeyCommandSet keysComment; |
| private HandlerRegistration regComment; |
| private final KeyCommandSet keysOpenByEnter; |
| private HandlerRegistration regOpenByEnter; |
| private CommentLinkProcessor commentLinkProcessor; |
| boolean isDisplayBinary; |
| |
| protected AbstractPatchContentTable() { |
| keysNavigation.add(new PrevKeyCommand(0, 'k', PatchUtil.C.linePrev())); |
| keysNavigation.add(new NextKeyCommand(0, 'j', PatchUtil.C.lineNext())); |
| keysNavigation.add(new PrevChunkKeyCmd(0, 'p', PatchUtil.C.chunkPrev())); |
| keysNavigation.add(new NextChunkKeyCmd(0, 'n', PatchUtil.C.chunkNext())); |
| keysNavigation.add(new PrevCommentCmd(0, 'P', PatchUtil.C.commentPrev())); |
| keysNavigation.add(new NextCommentCmd(0, 'N', PatchUtil.C.commentNext())); |
| |
| keysAction.add(new OpenKeyCommand(0, 'o', PatchUtil.C.expandComment())); |
| keysOpenByEnter = new KeyCommandSet(Gerrit.C.sectionNavigation()); |
| keysOpenByEnter.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, PatchUtil.C.expandComment())); |
| |
| if (Gerrit.isSignedIn()) { |
| keysAction.add(new InsertCommentCommand(0, 'c', PatchUtil.C |
| .commentInsert())); |
| keysAction.add(new PublishCommentsKeyCommand(0, 'r', Util.C |
| .keyPublishComments())); |
| |
| // See CommentEditorPanel |
| // |
| keysComment = new KeyCommandSet(PatchUtil.C.commentEditorSet()); |
| keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 's', PatchUtil.C |
| .commentSaveDraft())); |
| keysComment.add(new NoOpKeyCommand(0, KeyCodes.KEY_ESCAPE, PatchUtil.C |
| .commentCancelEdit())); |
| } else { |
| keysComment = null; |
| } |
| |
| table.setStyleName(Gerrit.RESOURCES.css().patchContentTable()); |
| } |
| |
| abstract void createFileCommentEditorOnSideA(); |
| |
| abstract void createFileCommentEditorOnSideB(); |
| |
| abstract PatchScreen.Type getPatchScreenType(); |
| |
| protected void initHeaders(PatchScript script, PatchSetDetail detail) { |
| PatchScreen.Type type = getPatchScreenType(); |
| headerSideA = new PatchSetSelectBox(PatchSetSelectBox.Side.A, type); |
| headerSideA.display(detail, script, patchKey, idSideA, idSideB); |
| headerSideA.addDoubleClickHandler(new DoubleClickHandler() { |
| @Override |
| public void onDoubleClick(DoubleClickEvent event) { |
| if (headerSideA.isFileOrCommitMessage()) { |
| createFileCommentEditorOnSideA(); |
| } |
| } |
| }); |
| headerSideB = new PatchSetSelectBox(PatchSetSelectBox.Side.B, type); |
| headerSideB.display(detail, script, patchKey, idSideA, idSideB); |
| headerSideB.addDoubleClickHandler(new DoubleClickHandler() { |
| @Override |
| public void onDoubleClick(DoubleClickEvent event) { |
| if (headerSideB.isFileOrCommitMessage()) { |
| createFileCommentEditorOnSideB(); |
| } |
| } |
| }); |
| |
| // Prepare icons. |
| iconA = new Image(Gerrit.RESOURCES.addFileComment()); |
| iconA.setTitle(PatchUtil.C.addFileCommentToolTip()); |
| iconA.addStyleName(Gerrit.RESOURCES.css().link()); |
| iconA.addClickHandler(new ClickHandler() { |
| @Override |
| public void onClick(ClickEvent event) { |
| createFileCommentEditorOnSideA(); |
| } |
| }); |
| iconB = new Image(Gerrit.RESOURCES.addFileComment()); |
| iconB.setTitle(PatchUtil.C.addFileCommentToolTip()); |
| iconB.addStyleName(Gerrit.RESOURCES.css().link()); |
| iconB.addClickHandler(new ClickHandler() { |
| @Override |
| public void onClick(ClickEvent event) { |
| createFileCommentEditorOnSideB(); |
| } |
| }); |
| } |
| |
| @Override |
| public void notifyDraftDelta(final int delta) { |
| if (fileList != null) { |
| fileList.notifyDraftDelta(patchKey, delta); |
| } |
| |
| Widget p = getParent(); |
| while (p != null) { |
| if (p instanceof CommentEditorContainer) { |
| ((CommentEditorContainer) p).notifyDraftDelta(delta); |
| break; |
| } |
| p = p.getParent(); |
| } |
| } |
| |
| @Override |
| public void remove(CommentEditorPanel panel) { |
| final int nRows = table.getRowCount(); |
| for (int row = 0; row < nRows; row++) { |
| final int nCells = table.getCellCount(row); |
| for (int cell = 0; cell < nCells; cell++) { |
| if (table.getWidget(row, cell) == panel) { |
| destroyEditor(row, cell); |
| Widget p = table; |
| while (p != null) { |
| if (p instanceof Focusable) { |
| ((Focusable) p).setFocus(true); |
| break; |
| } |
| p = p.getParent(); |
| } |
| |
| if (table.getCellFormatter().getStyleName(row - 1, cell) |
| .contains(Gerrit.RESOURCES.css().commentHolder())) { |
| table.getCellFormatter().addStyleName(row - 1, cell, |
| Gerrit.RESOURCES.css().commentPanelLast()); |
| } |
| return; |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void setRegisterKeys(final boolean on) { |
| super.setRegisterKeys(on); |
| if (on && keysComment != null && regComment == null) { |
| regComment = GlobalKey.add(this, keysComment); |
| } else if (!on && regComment != null) { |
| regComment.removeHandler(); |
| regComment = null; |
| } |
| |
| if (on && keysOpenByEnter != null && regOpenByEnter == null) { |
| regOpenByEnter = GlobalKey.add(this, keysOpenByEnter); |
| } else if (!on && regOpenByEnter != null) { |
| regOpenByEnter.removeHandler(); |
| regOpenByEnter = null; |
| } |
| } |
| |
| public void display(final Patch.Key k, final PatchSet.Id a, |
| final PatchSet.Id b, final PatchScript s, final PatchSetDetail d) { |
| patchKey = k; |
| idSideA = a; |
| idSideB = b; |
| |
| render(s, d); |
| } |
| |
| void setCommentLinkProcessor(CommentLinkProcessor commentLinkProcessor) { |
| this.commentLinkProcessor = commentLinkProcessor; |
| } |
| |
| protected boolean hasDifferences(PatchScript script) { |
| return hasEdits(script) || hasMeta(script) || hasComments(script); |
| } |
| |
| public boolean isPureMetaChange(PatchScript script) { |
| return !hasEdits(script) && hasMeta(script); |
| } |
| |
| // True if there are differences between the two patch sets |
| private boolean hasEdits(PatchScript script) { |
| for (Edit e : script.getEdits()) { |
| if (e.getType() != Edit.Type.EMPTY) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // True if one of the two patch sets has comments |
| private boolean hasComments(PatchScript script) { |
| return !script.getCommentDetail().getCommentsA().isEmpty() |
| || !script.getCommentDetail().getCommentsB().isEmpty(); |
| } |
| |
| // True if this change is a mode change or a pure rename/copy |
| private boolean hasMeta(PatchScript script) { |
| return !script.getPatchHeader().isEmpty(); |
| } |
| |
| protected void appendNoDifferences(SafeHtmlBuilder m) { |
| m.openTr(); |
| m.openTd(); |
| m.setAttribute("colspan", 5); |
| m.openDiv(); |
| m.addStyleName(Gerrit.RESOURCES.css().patchNoDifference()); |
| m.append(PatchUtil.C.noDifference()); |
| m.closeDiv(); |
| m.closeTd(); |
| m.closeTr(); |
| } |
| |
| protected SparseHtmlFile getSparseHtmlFileA(PatchScript s) { |
| AccountDiffPreference dp = new AccountDiffPreference(s.getDiffPrefs()); |
| dp.setShowWhitespaceErrors(false); |
| |
| PrettyFormatter f = ClientSideFormatter.FACTORY.get(); |
| f.setDiffPrefs(dp); |
| f.setFileName(s.getA().getPath()); |
| f.setEditFilter(PrettyFormatter.A); |
| f.setEditList(s.getEdits()); |
| f.format(s.getA()); |
| return f; |
| } |
| |
| protected SparseHtmlFile getSparseHtmlFileB(PatchScript s) { |
| AccountDiffPreference dp = new AccountDiffPreference(s.getDiffPrefs()); |
| |
| SparseFileContent b = s.getB(); |
| PrettyFormatter f = ClientSideFormatter.FACTORY.get(); |
| f.setDiffPrefs(dp); |
| f.setFileName(b.getPath()); |
| f.setEditFilter(PrettyFormatter.B); |
| f.setEditList(s.getEdits()); |
| |
| if (s.getA().isWholeFile() && !b.isWholeFile()) { |
| b = b.apply(s.getA(), s.getEdits()); |
| } |
| f.format(b); |
| return f; |
| } |
| |
| protected String getUrlA() { |
| final String rawBase = GWT.getHostPageBaseURL() + "cat/"; |
| final String url; |
| if (idSideA == null) { |
| url = rawBase + KeyUtil.encode(patchKey.toString()) + "^1"; |
| } else { |
| Patch.Key k = new Patch.Key(idSideA, patchKey.get()); |
| url = rawBase + KeyUtil.encode(k.toString()) + "^0"; |
| } |
| return url; |
| } |
| |
| protected String getUrlB() { |
| final String rawBase = GWT.getHostPageBaseURL() + "cat/"; |
| return rawBase + KeyUtil.encode(patchKey.toString()) + "^0"; |
| } |
| |
| protected abstract void render(PatchScript script, final PatchSetDetail detail); |
| |
| protected abstract void onInsertComment(PatchLine pl); |
| |
| public abstract void display(CommentDetail comments, boolean expandComments); |
| |
| @Override |
| protected Object getRowItemKey(final Object item) { |
| return null; |
| } |
| |
| protected void initScript(final PatchScript script) { |
| if (script.getEdits().size() == 1) { |
| final SparseFileContent a = script.getA(); |
| final SparseFileContent b = script.getB(); |
| onlyOneHunk = a.size() == 0 || b.size() == 0; |
| } else { |
| onlyOneHunk = false; |
| } |
| } |
| |
| private boolean isChunk(final int row) { |
| final Object o = getRowItem(row); |
| if (!onlyOneHunk && o instanceof PatchLine) { |
| final PatchLine pl = (PatchLine) o; |
| switch (pl.getType()) { |
| case DELETE: |
| case INSERT: |
| case REPLACE: |
| return true; |
| case CONTEXT: |
| break; |
| } |
| } else if (o instanceof CommentList) { |
| return true; |
| } |
| return false; |
| } |
| |
| private int findChunkStart(int row) { |
| while (0 <= row && isChunk(row)) { |
| row--; |
| } |
| return row + 1; |
| } |
| |
| private int findChunkEnd(int row) { |
| final int max = table.getRowCount(); |
| while (row < max && isChunk(row)) { |
| row++; |
| } |
| return row - 1; |
| } |
| |
| private static int oneBefore(final int begin) { |
| return 1 <= begin ? begin - 1 : begin; |
| } |
| |
| private int oneAfter(final int end) { |
| return end + 1 < table.getRowCount() ? end + 1 : end; |
| } |
| |
| private void moveToPrevChunk(int row) { |
| while (0 <= row && isChunk(row)) { |
| row--; |
| } |
| for (; 0 <= row; row--) { |
| if (isChunk(row)) { |
| final int start = findChunkStart(row); |
| movePointerTo(start, false); |
| scrollIntoView(oneBefore(start), oneAfter(row)); |
| return; |
| } |
| } |
| |
| // No prior hunk found? Try to hit the first line in the file. |
| // |
| for (row = 0; row < table.getRowCount(); row++) { |
| if (getRowItem(row) != null) { |
| movePointerTo(row); |
| break; |
| } |
| } |
| } |
| |
| private void moveToNextChunk(int row) { |
| final int max = table.getRowCount(); |
| while (row < max && isChunk(row)) { |
| row++; |
| } |
| for (; row < max; row++) { |
| if (isChunk(row)) { |
| movePointerTo(row, false); |
| scrollIntoView(oneBefore(row), oneAfter(findChunkEnd(row))); |
| return; |
| } |
| } |
| |
| // No next hunk found? Try to hit the last line in the file. |
| // |
| for (row = max - 1; row >= 0; row--) { |
| if (getRowItem(row) != null) { |
| movePointerTo(row); |
| break; |
| } |
| } |
| } |
| |
| private void moveToPrevComment(int row) { |
| while (0 <= row && isComment(row)) { |
| row--; |
| } |
| for (; 0 <= row; row--) { |
| if (isComment(row)) { |
| movePointerTo(row, false); |
| scrollIntoView(oneBefore(row), oneAfter(row)); |
| return; |
| } |
| } |
| |
| // No prior comment found? Try to hit the first line in the file. |
| // |
| for (row = 0; row < table.getRowCount(); row++) { |
| if (getRowItem(row) != null) { |
| movePointerTo(row); |
| break; |
| } |
| } |
| } |
| |
| private void moveToNextComment(int row) { |
| final int max = table.getRowCount(); |
| while (row < max && isComment(row)) { |
| row++; |
| } |
| for (; row < max; row++) { |
| if (isComment(row)) { |
| movePointerTo(row, false); |
| scrollIntoView(oneBefore(row), oneAfter(row)); |
| return; |
| } |
| } |
| |
| // No next comment found? Try to hit the last line in the file. |
| // |
| for (row = max - 1; row >= 0; row--) { |
| if (getRowItem(row) != null) { |
| movePointerTo(row); |
| break; |
| } |
| } |
| } |
| |
| private boolean isComment(int row) { |
| return getRowItem(row) instanceof CommentList; |
| } |
| |
| /** |
| * Invokes createCommentEditor() with an empty string as value for the comment |
| * parent UUID. This method is invoked by callers that want to create an |
| * editor for a comment that is not a reply. |
| */ |
| protected void createCommentEditor(final int suggestRow, final int column, |
| final int line, final short file) { |
| if (Gerrit.isSignedIn()) { |
| if (R_HEAD <= line) { |
| final Patch.Key parentKey; |
| final short side; |
| switch (file) { |
| case 0: |
| if (idSideA == null) { |
| parentKey = new Patch.Key(idSideB, patchKey.get()); |
| side = (short) 0; |
| } else { |
| parentKey = new Patch.Key(idSideA, patchKey.get()); |
| side = (short) 1; |
| } |
| break; |
| case 1: |
| parentKey = new Patch.Key(idSideB, patchKey.get()); |
| side = (short) 1; |
| break; |
| default: |
| throw new RuntimeException("unexpected file id " + file); |
| } |
| |
| final PatchLineComment newComment = new PatchLineComment( |
| new PatchLineComment.Key(parentKey, null), line, |
| Gerrit.getUserAccount().getId(), null, |
| new Timestamp(System.currentTimeMillis())); |
| newComment.setSide(side); |
| newComment.setMessage(""); |
| |
| findOrCreateCommentEditor(suggestRow, column, newComment, true) |
| .setFocus(true); |
| } |
| } else { |
| Gerrit.doSignIn(History.getToken()); |
| } |
| } |
| |
| protected void updateCursor(final PatchLineComment newComment) { |
| } |
| |
| abstract void insertFileCommentRow(final int row); |
| |
| private CommentEditorPanel findOrCreateCommentEditor(final int suggestRow, |
| final int column, final PatchLineComment newComment, final boolean create) { |
| int row = suggestRow; |
| int spans[] = new int[column + 1]; |
| FIND_ROW: while (row < table.getRowCount()) { |
| int col = 0; |
| for (int cell = 0; row < table.getRowCount() |
| && cell < table.getCellCount(row); cell++) { |
| while (col < column && 0 < spans[col]) { |
| spans[col++]--; |
| } |
| spans[col] = table.getFlexCellFormatter().getRowSpan(row, cell); |
| if (col == column) { |
| final Widget w = table.getWidget(row, cell); |
| if (w instanceof CommentEditorPanel |
| && ((CommentEditorPanel) w).getComment().getKey().getParentKey() |
| .equals(newComment.getKey().getParentKey())) { |
| // Don't insert two editors on the same position, it doesn't make |
| // any sense to the user. |
| // |
| return ((CommentEditorPanel) w); |
| |
| } else if (w instanceof CommentPanel) { |
| if (newComment != null && newComment.getParentUuid() != null) { |
| // If we are a reply, we were given the exact row to insert |
| // ourselves at. We should be before this panel so break. |
| // |
| break FIND_ROW; |
| } |
| row++; |
| cell--; |
| } else { |
| break FIND_ROW; |
| } |
| } |
| } |
| } |
| |
| if (newComment == null || !create) { |
| return null; |
| } |
| |
| final CommentEditorPanel ed = |
| new CommentEditorPanel(newComment, commentLinkProcessor); |
| ed.addFocusHandler(this); |
| ed.addBlurHandler(this); |
| boolean isCommentRow = false; |
| boolean needInsert = false; |
| if (row < table.getRowCount()) { |
| for (int cell = 0; cell < table.getCellCount(row); cell++) { |
| final Widget w = table.getWidget(row, cell); |
| if (w instanceof CommentEditorPanel || w instanceof CommentPanel) { |
| if (column == cell) { |
| needInsert = true; |
| } |
| isCommentRow = true; |
| } |
| } |
| } |
| if (needInsert || !isCommentRow) { |
| if (newComment.getLine() == R_HEAD) { |
| insertFileCommentRow(row); |
| } else { |
| insertRow(row); |
| } |
| styleCommentRow(row); |
| } |
| table.setWidget(row, column, ed); |
| styleLastCommentCell(row, column); |
| |
| int span = 1; |
| for (int r = row + 1; r < table.getRowCount(); r++) { |
| boolean hasComment = false; |
| for (int c = 0; c < table.getCellCount(r); c++) { |
| final Widget w = table.getWidget(r, c); |
| if (w instanceof CommentPanel || w instanceof CommentEditorPanel) { |
| if (c != column) { |
| hasComment = true; |
| break; |
| } |
| } |
| } |
| if (hasComment) { |
| table.removeCell(r, column); |
| span++; |
| } else { |
| break; |
| } |
| } |
| if (span > 1) { |
| table.getFlexCellFormatter().setRowSpan(row, column, span); |
| } |
| |
| for (int r = row - 1; r > 0; r--) { |
| if (getRowItem(r) instanceof CommentList) { |
| continue; |
| } else if (getRowItem(r) != null) { |
| movePointerTo(r); |
| break; |
| } |
| } |
| |
| updateCursor(newComment); |
| return ed; |
| } |
| |
| protected void insertRow(final int row) { |
| table.insertRow(row); |
| table.getCellFormatter().setStyleName(row, 0, |
| Gerrit.RESOURCES.css().iconCell()); |
| } |
| |
| @Override |
| protected void onOpenRow(final int row) { |
| final Object item = getRowItem(row); |
| if (item instanceof CommentList) { |
| for (final CommentPanel p : ((CommentList) item).panels) { |
| p.setOpen(!p.isOpen()); |
| } |
| } |
| } |
| |
| public void setAccountInfoCache(final AccountInfoCache aic) { |
| assert aic != null; |
| accountCache = aic; |
| } |
| |
| private void destroyEditor(final int row, final int col) { |
| table.clearCell(row, col); |
| final int span = table.getFlexCellFormatter().getRowSpan(row, col); |
| boolean removeRow = true; |
| final int nCells = table.getCellCount(row); |
| for (int cell = 0; cell < nCells; cell++) { |
| if (table.getWidget(row, cell) != null) { |
| removeRow = false; |
| break; |
| } |
| } |
| if (removeRow) { |
| destroyCommentRow(row); |
| } else { |
| destroyComment(row, col, span); |
| } |
| } |
| |
| protected void destroyCommentRow(int row) { |
| for (int r = row - 1; 0 <= r; r--) { |
| boolean data = false; |
| for (int c = 0; c < table.getCellCount(r); c++) { |
| data |= table.getWidget(r, c) != null; |
| final int s = table.getFlexCellFormatter().getRowSpan(r, c) - 1; |
| if (r + s == row) { |
| table.getFlexCellFormatter().setRowSpan(r, c, s); |
| } |
| } |
| if (!data) { |
| break; |
| } |
| } |
| table.removeRow(row); |
| } |
| |
| private void destroyComment(int row, int col, int span) { |
| table.getFlexCellFormatter().setStyleName(// |
| row, col, Gerrit.RESOURCES.css().diffText()); |
| |
| if (span != 1) { |
| table.getFlexCellFormatter().setRowSpan(row, col, 1); |
| for (int r = row + 1; r < row + span; r++) { |
| table.insertCell(r, col); |
| |
| table.getFlexCellFormatter().setStyleName(// |
| r, col, Gerrit.RESOURCES.css().diffText()); |
| } |
| } |
| } |
| |
| protected void bindComment(final int row, final int col, |
| final PatchLineComment line, final boolean isLast, boolean expandComment) { |
| if (line.getStatus() == PatchLineComment.Status.DRAFT) { |
| final CommentEditorPanel plc = |
| new CommentEditorPanel(line, commentLinkProcessor); |
| plc.addFocusHandler(this); |
| plc.addBlurHandler(this); |
| table.setWidget(row, col, plc); |
| styleLastCommentCell(row, col); |
| |
| } else { |
| final AccountInfo author = FormatUtil.asInfo(accountCache.get(line.getAuthor())); |
| final PublishedCommentPanel panel = |
| new PublishedCommentPanel(author, line); |
| panel.setOpen(expandComment); |
| panel.addFocusHandler(this); |
| panel.addBlurHandler(this); |
| table.setWidget(row, col, panel); |
| styleLastCommentCell(row, col); |
| |
| CommentList l = (CommentList) getRowItem(row); |
| if (l == null) { |
| l = new CommentList(); |
| setRowItem(row, l); |
| } |
| l.comments.add(line); |
| l.panels.add(panel); |
| } |
| |
| styleCommentRow(row); |
| } |
| |
| @Override |
| public void onFocus(FocusEvent event) { |
| // when the comment panel gets focused (actually when a button inside the |
| // comment panel gets focused) we have to unregister the key binding for |
| // ENTER that expands/collapses the comment panel, if we don't do this the |
| // focused button in the comment panel cannot be triggered by pressing ENTER |
| // since ENTER would then be already consumed by this key binding |
| if (regOpenByEnter != null) { |
| regOpenByEnter.removeHandler(); |
| regOpenByEnter = null; |
| } |
| } |
| |
| @Override |
| public void onBlur(BlurEvent event) { |
| // when the comment panel gets blurred (actually when a button inside the |
| // comment panel gets blurred) we have to re-register the key binding for |
| // ENTER that expands/collapses the comment panel |
| if (keysOpenByEnter != null && regOpenByEnter == null) { |
| regOpenByEnter = GlobalKey.add(this, keysOpenByEnter); |
| } |
| } |
| |
| private void styleCommentRow(final int row) { |
| final CellFormatter fmt = table.getCellFormatter(); |
| final Element iconCell = fmt.getElement(row, 0); |
| UIObject.setStyleName(DOM.getParent(iconCell), Gerrit.RESOURCES.css() |
| .commentHolder(), true); |
| } |
| |
| private void styleLastCommentCell(final int row, final int col) { |
| final CellFormatter fmt = table.getCellFormatter(); |
| fmt.removeStyleName(row - 1, col, // |
| Gerrit.RESOURCES.css().commentPanelLast()); |
| fmt.setStyleName(row, col, Gerrit.RESOURCES.css().commentHolder()); |
| fmt.addStyleName(row, col, Gerrit.RESOURCES.css().commentPanelLast()); |
| if (!fmt.getStyleName(row, col - 1).contains(Gerrit.RESOURCES.css().commentHolder())) { |
| fmt.addStyleName(row, col, Gerrit.RESOURCES.css().commentHolderLeftmost()); |
| } |
| } |
| |
| protected static class CommentList { |
| final List<PatchLineComment> comments = new ArrayList<PatchLineComment>(); |
| final List<PublishedCommentPanel> panels = |
| new ArrayList<PublishedCommentPanel>(); |
| } |
| |
| public static class NoOpKeyCommand extends NeedsSignInKeyCommand { |
| public NoOpKeyCommand(int mask, int key, String help) { |
| super(mask, key, help); |
| } |
| |
| @Override |
| public void onKeyPress(final KeyPressEvent event) { |
| } |
| } |
| |
| public class InsertCommentCommand extends NeedsSignInKeyCommand { |
| public InsertCommentCommand(int mask, int key, String help) { |
| super(mask, key, help); |
| } |
| |
| @Override |
| public void onKeyPress(final KeyPressEvent event) { |
| ensurePointerVisible(); |
| for (int row = getCurrentRow(); 0 <= row; row--) { |
| final Object item = getRowItem(row); |
| if (item instanceof PatchLine) { |
| onInsertComment((PatchLine) item); |
| return; |
| } else if (item instanceof CommentList) { |
| continue; |
| } else { |
| return; |
| } |
| } |
| } |
| } |
| |
| public class PublishCommentsKeyCommand extends NeedsSignInKeyCommand { |
| public PublishCommentsKeyCommand(int mask, char key, String help) { |
| super(mask, key, help); |
| } |
| |
| @Override |
| public void onKeyPress(final KeyPressEvent event) { |
| final PatchSet.Id id = patchKey.getParentKey(); |
| Gerrit.display(Dispatcher.toPublish(id)); |
| } |
| } |
| |
| public class PrevChunkKeyCmd extends KeyCommand { |
| public PrevChunkKeyCmd(int mask, int key, String help) { |
| super(mask, key, help); |
| } |
| |
| @Override |
| public void onKeyPress(final KeyPressEvent event) { |
| ensurePointerVisible(); |
| moveToPrevChunk(getCurrentRow()); |
| } |
| } |
| |
| public class NextChunkKeyCmd extends KeyCommand { |
| public NextChunkKeyCmd(int mask, int key, String help) { |
| super(mask, key, help); |
| } |
| |
| @Override |
| public void onKeyPress(final KeyPressEvent event) { |
| ensurePointerVisible(); |
| moveToNextChunk(getCurrentRow()); |
| } |
| } |
| |
| public class PrevCommentCmd extends KeyCommand { |
| public PrevCommentCmd(int mask, int key, String help) { |
| super(mask, key, help); |
| } |
| |
| @Override |
| public void onKeyPress(final KeyPressEvent event) { |
| ensurePointerVisible(); |
| moveToPrevComment(getCurrentRow()); |
| } |
| } |
| |
| public class NextCommentCmd extends KeyCommand { |
| public NextCommentCmd(int mask, int key, String help) { |
| super(mask, key, help); |
| } |
| |
| @Override |
| public void onKeyPress(final KeyPressEvent event) { |
| ensurePointerVisible(); |
| moveToNextComment(getCurrentRow()); |
| } |
| } |
| |
| private class PublishedCommentPanel extends CommentPanel implements |
| ClickHandler { |
| final PatchLineComment comment; |
| final Button reply; |
| final Button replyDone; |
| |
| PublishedCommentPanel(final AccountInfo author, final PatchLineComment c) { |
| super(author, c.getWrittenOn(), c.getMessage(), commentLinkProcessor); |
| this.comment = c; |
| |
| reply = new Button(PatchUtil.C.buttonReply()); |
| reply.addClickHandler(this); |
| addButton(reply); |
| |
| replyDone = new Button(PatchUtil.C.buttonReplyDone()); |
| replyDone.addClickHandler(this); |
| addButton(replyDone); |
| } |
| |
| @Override |
| public void onClick(final ClickEvent event) { |
| if (Gerrit.isSignedIn()) { |
| if (reply == event.getSource()) { |
| createReplyEditor(); |
| } else if (replyDone == event.getSource()) { |
| cannedReply(PatchUtil.C.cannedReplyDone()); |
| } |
| |
| } else { |
| Gerrit.doSignIn(History.getToken()); |
| } |
| } |
| |
| private void createReplyEditor() { |
| final PatchLineComment newComment = newComment(); |
| newComment.setMessage(""); |
| findOrCreateEditor(newComment, true).setFocus(true); |
| } |
| |
| private void cannedReply(String message) { |
| final PatchLineComment newComment = newComment(); |
| newComment.setMessage(message); |
| CommentEditorPanel p = findOrCreateEditor(newComment, false); |
| if (p == null) { |
| enableButtons(false); |
| PatchUtil.DETAIL_SVC.saveDraft(newComment, |
| new GerritCallback<PatchLineComment>() { |
| @Override |
| public void onSuccess(final PatchLineComment result) { |
| enableButtons(true); |
| notifyDraftDelta(1); |
| findOrCreateEditor(result, true).setOpen(false); |
| } |
| |
| @Override |
| public void onFailure(Throwable caught) { |
| enableButtons(true); |
| super.onFailure(caught); |
| } |
| }); |
| } else { |
| if (!p.isOpen()) { |
| p.setOpen(true); |
| } |
| p.setFocus(true); |
| } |
| } |
| |
| private CommentEditorPanel findOrCreateEditor( |
| PatchLineComment newComment, boolean create) { |
| int row = rowOf(getElement()); |
| int column = columnOf(getElement()); |
| return findOrCreateCommentEditor(row + 1, column, newComment, create); |
| } |
| |
| private PatchLineComment newComment() { |
| PatchLineComment newComment = |
| new PatchLineComment(new PatchLineComment.Key(comment.getKey() |
| .getParentKey(), null), comment.getLine(), Gerrit |
| .getUserAccount().getId(), comment.getKey().get(), |
| new Timestamp(System.currentTimeMillis())); |
| newComment.setSide(comment.getSide()); |
| return newComment; |
| } |
| } |
| } |