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:
committed by
Shawn Pearce
parent
4b6d72d541
commit
2c702aaf5a
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ class DiffTable extends Composite {
|
||||
@UiField
|
||||
Element cmB;
|
||||
|
||||
@UiField
|
||||
SidePanel sidePanel;
|
||||
|
||||
@UiField
|
||||
Element patchsetNavRow;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user