SideBySide2: Add clickable gutters to the right of CodeMirror B

Implementing a panel to the right of the scrollbar that holds fixed
clickable gutters indicating positions of comments and diff chunks.
This is a crude proof-of-concept that requires more tweaking.

Change-Id: Ib705581de407d91c36bfe92cf02dd063736158e1
This commit is contained in:
Michael Zhou
2013-07-17 17:37:37 -07:00
committed by Shawn Pearce
parent 4b6d72d541
commit 2c702aaf5a
9 changed files with 340 additions and 25 deletions

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.client.diff;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.diff.PaddingManager.PaddingWidgetWrapper;
import com.google.gerrit.client.diff.SideBySide2.DiffChunkInfo;
import com.google.gerrit.client.diff.SidePanel.GutterWrapper;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.user.client.ui.Composite;
@@ -31,6 +32,7 @@ abstract class CommentBox extends Composite {
private PaddingWidgetWrapper selfWidgetWrapper;
private SideBySide2 parent;
private DiffChunkInfo diffChunkInfo;
private GutterWrapper gutterWrapper;
@Override
protected void onLoad() {
@@ -87,4 +89,12 @@ abstract class CommentBox extends Composite {
void setParent(SideBySide2 parent) {
this.parent = parent;
}
void setGutterWrapper(GutterWrapper wrapper) {
gutterWrapper = wrapper;
}
GutterWrapper getGutterWrapper() {
return gutterWrapper;
}
}

View File

@@ -48,6 +48,9 @@ class DiffTable extends Composite {
@UiField
Element cmB;
@UiField
SidePanel sidePanel;
@UiField
Element patchsetNavRow;

View File

@@ -33,6 +33,9 @@ limitations under the License.
.difftable .CodeMirror pre span {
padding-bottom: 0.1em;
}
.sidePanelCell {
width: 10px;
}
.table {
width: 100%;
table-layout: fixed;
@@ -98,26 +101,33 @@ limitations under the License.
}
</ui:style>
<g:HTMLPanel styleName='{style.difftable}'>
<table class='{style.table}'>
<tr ui:field='patchsetNavRow'>
<td ui:field='patchsetNavCellA' class='{style.padding}'>
<d:PatchSelectBox2 ui:field='patchSelectBoxA' />
</td>
<td ui:field='patchsetNavCellB' class='{style.padding}'>
<d:PatchSelectBox2 ui:field='patchSelectBoxB' />
</td>
</tr>
<tr ui:field='fileCommentRow'>
<td ui:field='fileCommentCellA' class='{style.padding}'>
<d:FileCommentPanel ui:field='fileCommentPanelA' />
</td>
<td ui:field='fileCommentCellB' class='{style.padding}'>
<d:FileCommentPanel ui:field='fileCommentPanelB' />
</td>
</tr>
<table>
<tr>
<td ui:field='cmA' class='{style.a}'></td>
<td ui:field='cmB' class='{style.b}'></td>
<td>
<table class='{style.table}'>
<tr ui:field='patchsetNavRow'>
<td ui:field='patchsetNavCellA' class='{style.padding}'>
<d:PatchSelectBox2 ui:field='patchSelectBoxA' />
</td>
<td ui:field='patchsetNavCellB' class='{style.padding}'>
<d:PatchSelectBox2 ui:field='patchSelectBoxB' />
</td>
</tr>
<tr ui:field='fileCommentRow'>
<td ui:field='fileCommentCellA' class='{style.padding}'>
<d:FileCommentPanel ui:field='fileCommentPanelA' />
</td>
<td ui:field='fileCommentCellB' class='{style.padding}'>
<d:FileCommentPanel ui:field='fileCommentPanelB' />
</td>
</tr>
<tr>
<td ui:field='cmA' class='{style.a}'></td>
<td ui:field='cmB' class='{style.b}'></td>
</tr>
</table>
</td>
<td class='{style.sidePanelCell}'><d:SidePanel ui:field='sidePanel'/></td>
</tr>
</table>
</g:HTMLPanel>

View File

@@ -224,6 +224,7 @@ class DraftBox extends CommentBox {
parent.removeDraft(this, side, comment.line() - 1);
cm.focus();
getSelfWidgetWrapper().getWidget().clear();
getGutterWrapper().remove();
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {

View File

@@ -260,6 +260,7 @@ public class SideBySide2 extends Screen {
}
};
cm.on("renderLine", resizeLinePadding(getSideFromCm(cm)));
cm.on("viewportChange", adjustGutters(cm));
// TODO: Prevent right click from updating the cursor.
cm.addKeyMap(KeyMap.create()
.on("'j'", moveCursorDown(cm, 1))
@@ -430,6 +431,13 @@ public class SideBySide2 extends Screen {
}
markEdit(cmA, currentA, current.edit_a(), origLineA);
markEdit(cmB, currentB, current.edit_b(), origLineB);
if (aLength == 0 || bLength == 0) {
diffTable.sidePanel.addGutter(cmB, origLineB, aLength == 0
? SidePanel.GutterType.INSERT
: SidePanel.GutterType.DELETE);
} else {
diffTable.sidePanel.addGutter(cmB, origLineB, SidePanel.GutterType.EDIT);
}
}
}
}
@@ -454,10 +462,15 @@ public class SideBySide2 extends Screen {
DraftBox addDraftBox(CommentInfo info) {
CodeMirror cm = getCmFromSide(info.side());
DraftBox box = new DraftBox(this, cm, commentLinkProcessor, revision, info);
final DraftBox box = new DraftBox(this, cm, commentLinkProcessor, revision, info);
if (info.id() == null) {
box.setOpen(true);
box.setEdit(true);
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
box.setOpen(true);
box.setEdit(true);
}
});
}
if (!info.has_line()) {
return box;
@@ -511,6 +524,10 @@ public class SideBySide2 extends Screen {
if (otherChunk == null) {
box.setDiffChunkInfo(myChunk);
}
box.setGutterWrapper(diffTable.sidePanel.addGutter(cm, info.line() - 1,
box instanceof DraftBox ?
SidePanel.GutterType.DRAFT
: SidePanel.GutterType.COMMENT));
return box;
}
@@ -803,6 +820,21 @@ public class SideBySide2 extends Screen {
}
}
private Runnable adjustGutters(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
Viewport fromTo = cm.getViewport();
int size = fromTo.getTo() - fromTo.getFrom() + 1;
if (cm.getOldViewportSize() == size) {
return;
}
cm.setOldViewportSize(size);
diffTable.sidePanel.adjustGutters(cmB);
}
};
}
private Runnable updateActiveLine(final CodeMirror cm) {
final CodeMirror other = otherCm(cm);
return new Runnable() {
@@ -830,9 +862,8 @@ public class SideBySide2 extends Screen {
cm.addLineClass(handle, LineClassWhere.BACKGROUND, DiffTable.style.activeLineBg());
LineOnOtherInfo info =
mapper.lineOnOther(getSideFromCm(cm), cm.getLineNumber(handle));
int oLine = info.getLine();
LineHandle oLineHandle = other.getLineHandle(oLine);
if (info.isAligned()) {
LineHandle oLineHandle = other.getLineHandle(info.getLine());
other.setActiveLine(oLineHandle);
other.addLineClass(oLineHandle, LineClassWhere.WRAP,
DiffTable.style.activeLine());
@@ -1003,6 +1034,7 @@ public class SideBySide2 extends Screen {
cmA.refresh();
cmB.setHeight(Window.getClientHeight() - h);
cmB.refresh();
diffTable.sidePanel.adjustGutters(cmB);
}
static void setHeightInPx(Element ele, double height) {
@@ -1015,6 +1047,14 @@ public class SideBySide2 extends Screen {
: null;
}
CodeMirror getCmA() {
return cmA;
}
CodeMirror getCmB() {
return cmB;
}
static class EditIterator {
private final JsArrayString lines;
private final int startLine;

View File

@@ -0,0 +1,168 @@
//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.gwt.core.client.GWT;
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.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.shared.HandlerRegistration;
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.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Label;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.LineCharacter;
import java.util.ArrayList;
import java.util.List;
/** The Widget that handles the scrollbar gutters */
class SidePanel extends Composite {
interface Binder extends UiBinder<HTMLPanel, SidePanel> {}
private static Binder uiBinder = GWT.create(Binder.class);
interface SidePanelStyle extends CssResource {
String gutter();
String halfGutter();
String comment();
String draft();
String insert();
String delete();
}
enum GutterType {
COMMENT, DRAFT, INSERT, DELETE, EDIT;
}
@UiField
SidePanelStyle style;
private List<GutterWrapper> gutters;
private CodeMirror cmB;
SidePanel() {
initWidget(uiBinder.createAndBindUi(this));
this.gutters = new ArrayList<GutterWrapper>();
}
GutterWrapper addGutter(CodeMirror cm, int line, GutterType type) {
Label gutter = new Label();
GutterWrapper info = new GutterWrapper(this, gutter, cm, line, type);
adjustGutter(info);
Element ele = gutter.getElement();
gutter.addStyleName(style.gutter());
switch (type) {
case COMMENT:
gutter.addStyleName(style.comment());
break;
case DRAFT:
gutter.addStyleName(style.draft());
gutter.setText("*");
break;
case INSERT:
gutter.addStyleName(style.insert());
break;
case DELETE:
gutter.addStyleName(style.delete());
break;
case EDIT:
gutter.addStyleName(style.insert());
Label labelLeft = new Label();
labelLeft.addStyleName(style.halfGutter());
gutter.getElement().appendChild(labelLeft.getElement());
}
getElement().appendChild(ele);
((HTMLPanel) getWidget()).add(gutter);
gutters.add(info);
return info;
}
void adjustGutters(CodeMirror cmB) {
this.cmB = cmB;
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
for (GutterWrapper info : gutters) {
adjustGutter(info);
}
}
});
}
private void adjustGutter(GutterWrapper wrapper) {
if (cmB == null) {
return;
}
final CodeMirror cm = wrapper.cm;
final int line = wrapper.line;
Label gutter = wrapper.gutter;
final double height = cm.heightAtLine(line, "local");
final double scrollbarHeight = cmB.getScrollbarV().getClientHeight();
double top = height / (double) cmB.getSizer().getClientHeight() *
scrollbarHeight +
cmB.getScrollbarV().getAbsoluteTop();
if (top == 0) {
top = -10;
}
gutter.getElement().getStyle().setTop(top, Unit.PX);
wrapper.replaceClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
cm.setCursor(LineCharacter.create(line));
cm.scrollToY(Math.max(0, height - scrollbarHeight / 2));
cm.focus();
}
});
}
void removeGutter(GutterWrapper wrapper) {
gutters.remove(wrapper);
}
static class GutterWrapper {
private SidePanel host;
private Label gutter;
private CodeMirror cm;
private int line;
private HandlerRegistration regClick;
GutterWrapper(SidePanel host, Label anchor, CodeMirror cm, int line,
GutterType type) {
this.host = host;
this.gutter = anchor;
this.cm = cm;
this.line = line;
}
private void replaceClickHandler(ClickHandler newHandler) {
if (regClick != null) {
regClick.removeHandler();
}
regClick = gutter.addClickHandler(newHandler);
}
void remove() {
gutter.removeFromParent();
host.removeGutter(this);
}
}
}

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:g='urn:import:com.google.gwt.user.client.ui'>
<ui:style type='com.google.gerrit.client.diff.SidePanel.SidePanelStyle'>
.gutter {
cursor: pointer;
position: fixed;
height: 3px;
width: 10px;
border: 1px solid black;
}
.halfGutter {
cursor: pointer;
position: fixed;
height: 3px;
width: 5px;
background-color: #faa;
}
.comment, .draft {
background-color: #e5ecf9;
}
.draft {
text-align: center;
font-size: small;
line-height: 0.5;
color: inherit !important;
text-decoration: none !important;
}
.delete {
background-color: #faa;
}
.insert {
background-color: #9f9;
}
</ui:style>
<g:HTMLPanel/>
</ui:UiBinder>

View File

@@ -124,10 +124,18 @@ public class CodeMirror extends JavaScriptObject {
return this.lineAtHeight(height);
}-*/;
public final native int lineAtHeight(double height, String mode) /*-{
return this.lineAtHeight(height, mode);
}-*/;
public final native double heightAtLine(int line) /*-{
return this.heightAtLine(line);
}-*/;
public final native double heightAtLine(int line, String mode) /*-{
return this.heightAtLine(line, mode);
}-*/;
public final native CodeMirrorDoc getDoc() /*-{
return this.getDoc();
}-*/;
@@ -148,6 +156,14 @@ public class CodeMirror extends JavaScriptObject {
return this.getViewport();
}-*/;
public final native int getOldViewportSize() /*-{
return this.state.oldViewportSize || 0;
}-*/;
public final native void setOldViewportSize(int lines) /*-{
this.state.oldViewportSize = lines;
}-*/;
public final native double getScrollSetAt() /*-{
return this.state.scrollSetAt || 0;
}-*/;
@@ -239,6 +255,10 @@ public class CodeMirror extends JavaScriptObject {
this.focus();
}-*/;
public final native int lineCount() /*-{
return this.lineCount();
}-*/;
/** Hack into CodeMirror to disable unwanted keys */
public static final native void disableUnwantedKey(String category,
String name) /*-{
@@ -253,6 +273,18 @@ public class CodeMirror extends JavaScriptObject {
return this.getGutterElement();
}-*/;
public final native Element getScrollerElement() /*-{
return this.getScrollerElement();
}-*/;
public final native Element getSizer() /*-{
return this.display.sizer;
}-*/;
public final native Element getScrollbarV() /*-{
return this.display.scrollbarV;
}-*/;
protected CodeMirror() {
}

View File

@@ -30,7 +30,6 @@ public class ScrollInfo extends JavaScriptObject {
public final native double getClientWidth() /*-{ return this.clientWidth; }-*/;
public final native double getClientHeight() /*-{ return this.clientHeight; }-*/;
protected ScrollInfo() {
}
}