/*
* LineTableView.java
*
* Copyright (C) 2009-12 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.studio.client.workbench.views.vcs.common.diff;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.cellview.client.CellTable;
import com.google.gwt.user.cellview.client.Column;
import com.google.gwt.user.cellview.client.RowStyles;
import com.google.gwt.user.cellview.client.TextColumn;
import com.google.gwt.view.client.MultiSelectionModel;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.gwt.view.client.SelectionChangeEvent.Handler;
import com.google.inject.Inject;
import org.rstudio.core.client.SafeHtmlUtil;
import org.rstudio.core.client.dom.DomUtils;
import org.rstudio.core.client.dom.DomUtils.NodePredicate;
import org.rstudio.core.client.theme.RStudioCellTableStyle;
import org.rstudio.core.client.widget.FontSizer;
import org.rstudio.core.client.widget.MultiSelectCellTable;
import org.rstudio.studio.client.common.vcs.GitServerOperations.PatchMode;
import org.rstudio.studio.client.workbench.views.vcs.common.diff.Line.Type;
import org.rstudio.studio.client.workbench.views.vcs.common.diff.LineTablePresenter.Display;
import org.rstudio.studio.client.workbench.views.vcs.common.events.DiffChunkActionEvent;
import org.rstudio.studio.client.workbench.views.vcs.common.events.DiffChunkActionEvent.Action;
import org.rstudio.studio.client.workbench.views.vcs.common.events.DiffChunkActionHandler;
import org.rstudio.studio.client.workbench.views.vcs.common.events.DiffLinesActionEvent;
import org.rstudio.studio.client.workbench.views.vcs.common.events.DiffLinesActionHandler;
import java.util.ArrayList;
import java.util.HashSet;
public class LineTableView extends MultiSelectCellTable<ChunkOrLine> implements Display
{
public interface LineTableViewCellTableResources extends CellTable.Resources
{
@Source({RStudioCellTableStyle.RSTUDIO_DEFAULT_CSS,
"LineTableViewCellTableStyle.css"})
LineTableViewCellTableStyle cellTableStyle();
}
public interface LineTableViewCellTableStyle extends CellTable.Style
{
String header();
String same();
String insertion();
String deletion();
String comment();
String info();
String lineNumber();
String lastLineNumber();
String actions();
String lineActions();
String chunkActions();
String start();
String end();
String stageMode();
String workingMode();
String noStageMode();
}
public class LineContentCell extends AbstractCell<ChunkOrLine>
{
public LineContentCell()
{
super("mousedown");
}
@Override
public void onBrowserEvent(Context context,
Element parent,
ChunkOrLine value,
NativeEvent event,
ValueUpdater<ChunkOrLine> chunkOrLineValueUpdater)
{
if ("mousedown".equals(event.getType())
&& event.getButton() == NativeEvent.BUTTON_LEFT
&& parent.isOrHasChild(event.getEventTarget().<Node>cast()))
{
Element el = (Element) DomUtils.findNodeUpwards(
event.getEventTarget().<Node>cast(),
parent,
new NodePredicate()
{
@Override
public boolean test(Node n)
{
return n.getNodeType() == Node.ELEMENT_NODE &&
((Element) n).hasAttribute("data-action");
}
});
if (el != null)
{
event.preventDefault();
event.stopPropagation();
Action action = Action.valueOf(el.getAttribute("data-action"));
if (value.getChunk() != null)
fireEvent(new DiffChunkActionEvent(action, value.getChunk()));
else
fireEvent(new DiffLinesActionEvent(action));
}
}
super.onBrowserEvent(context,
parent,
value,
event,
chunkOrLineValueUpdater);
}
@Override
public void render(Context context, ChunkOrLine value, SafeHtmlBuilder sb)
{
if (value.getLine() != null)
{
sb.appendEscaped(value.getLine().getText());
if (showActions_
&& value.getLine().getType() != Line.Type.Same
&& value.getLine().getType() != Line.Type.Info
&& value == firstSelectedLine_)
{
renderActionButtons(
sb,
RES.cellTableStyle().lineActions(),
selectionModel_.getSelectedSet().size() > 1
? " selection"
: " line");
}
}
else
{
sb.appendEscaped(UnifiedEmitter.createChunkString(value.getChunk()));
if (showActions_)
{
renderActionButtons(sb,
RES.cellTableStyle().chunkActions(),
" chunk");
}
}
}
private void renderActionButtons(SafeHtmlBuilder sb,
String className,
String labelSuffix)
{
sb.append(SafeHtmlUtil.createOpenTag(
"div",
"class", RES.cellTableStyle().actions() + " " + className));
renderActionButton(sb, Action.Unstage, labelSuffix);
renderActionButton(sb, Action.Stage, labelSuffix);
renderActionButton(sb, Action.Discard, labelSuffix);
sb.appendHtmlConstant("</div>");
}
private void renderActionButton(SafeHtmlBuilder sb,
Action action,
String labelSuffix)
{
if (action == Action.Stage)
{
blueButtonRenderer_.render(
sb, action.name() + labelSuffix, action.name());
}
else
{
grayButtonRenderer_.render(
sb, action.name() + labelSuffix, action.name());
}
}
}
private class SwitchableSelectionModel<T> extends MultiSelectionModel<T>
{
private SwitchableSelectionModel()
{
}
private SwitchableSelectionModel(ProvidesKey<T> keyProvider)
{
super(keyProvider);
}
@Override
public void setSelected(T object, boolean selected)
{
if (!enabled_)
return;
super.setSelected(object, selected);
}
@SuppressWarnings("unused")
public boolean isEnabled()
{
return enabled_;
}
public void setEnabled(boolean enabled)
{
this.enabled_ = enabled;
}
private boolean enabled_ = true;
}
public LineTableView(int filesCompared)
{
this(filesCompared,
GWT.<LineTableViewCellTableResources>create(LineTableViewCellTableResources.class));
}
@Inject
public LineTableView(final LineTableViewCellTableResources res)
{
this(2, res);
}
public LineTableView(int filesCompared,
final LineTableViewCellTableResources res)
{
super(1, res);
FontSizer.applyNormalFontSize(this);
for (int i = 0; i < filesCompared; i++)
{
final int index = i;
TextColumn<ChunkOrLine> col = new TextColumn<ChunkOrLine>()
{
@Override
public String getValue(ChunkOrLine object)
{
Line line = object.getLine();
if (line == null)
return "\u00A0";
if (!line.getAppliesTo()[index])
return "\u00A0";
return intToString(line.getLines()[index]);
}
};
col.setHorizontalAlignment(TextColumn.ALIGN_RIGHT);
addColumn(col);
setColumnWidth(col, 100, Unit.PX);
addColumnStyleName(i, res.cellTableStyle().lineNumber());
if (i == filesCompared - 1)
addColumnStyleName(i, res.cellTableStyle().lastLineNumber());
}
Column<ChunkOrLine, ChunkOrLine> textCol =
new Column<ChunkOrLine, ChunkOrLine>(new LineContentCell())
{
@Override
public ChunkOrLine getValue(ChunkOrLine object)
{
return object;
}
};
addColumn(textCol);
setColumnWidth(textCol, 100, Unit.PCT);
setRowStyles(new RowStyles<ChunkOrLine>()
{
@Override
public String getStyleNames(ChunkOrLine chunkOrLine, int rowIndex)
{
Line line = chunkOrLine.getLine();
if (line == null)
{
return res.cellTableStyle().header();
}
else
{
String prefix = "";
if (startRows_.contains(rowIndex))
prefix += res.cellTableStyle().start() + " ";
if (endRows_.contains(rowIndex))
prefix += res.cellTableStyle().end() + " ";
switch (line.getType())
{
case Same:
return prefix + res.cellTableStyle().same();
case Insertion:
return prefix + res.cellTableStyle().insertion();
case Deletion:
return prefix + res.cellTableStyle().deletion();
case Comment:
return prefix + res.cellTableStyle().comment();
case Info:
return prefix + res.cellTableStyle().info();
default:
return "";
}
}
}
});
selectionModel_ = new SwitchableSelectionModel<ChunkOrLine>(new ProvidesKey<ChunkOrLine>()
{
@Override
public Object getKey(ChunkOrLine item)
{
if (item.getChunk() != null)
return item.getChunk().getDiffIndex();
else
return item.getLine().getDiffIndex();
}
}) {
@Override
public void setSelected(ChunkOrLine object, boolean selected)
{
if (object.getLine() != null &&
object.getLine().getType() != Line.Type.Same &&
object.getLine().getType() != Line.Type.Info)
{
super.setSelected(object, selected);
}
}
};
selectionModel_.addSelectionChangeHandler(new Handler()
{
@Override
public void onSelectionChange(SelectionChangeEvent event)
{
ChunkOrLine newFirstSelectedLine = null;
for (ChunkOrLine value : selectionModel_.getSelectedSet())
{
if (value.getLine() != null &&
(newFirstSelectedLine == null || newFirstSelectedLine.getLine().compareTo(value.getLine()) > 0))
{
newFirstSelectedLine = value;
}
}
if (newFirstSelectedLine != null)
refreshValue(newFirstSelectedLine);
if (firstSelectedLine_ != newFirstSelectedLine)
{
if (firstSelectedLine_ != null)
refreshValue(firstSelectedLine_);
}
firstSelectedLine_ = newFirstSelectedLine;
}
});
setSelectionModel(selectionModel_);
setData(new ArrayList<ChunkOrLine>(), PatchMode.Working);
}
private void refreshValue(ChunkOrLine value)
{
int index = lines_.indexOf(value);
if (index >= 0)
{
ArrayList<ChunkOrLine> list = new ArrayList<ChunkOrLine>();
list.add(value);
setRowData(index, list);
}
}
private String intToString(Integer value)
{
if (value == null)
return "";
return value.toString();
}
public boolean isShowActions()
{
return showActions_;
}
public void setShowActions(boolean showActions)
{
showActions_ = showActions;
selectionModel_.setEnabled(showActions);
}
public void hideStageCommands()
{
addStyleName(RES.cellTableStyle().noStageMode());
}
public void setUseStartBorder(boolean useStartBorder)
{
useStartBorder_ = useStartBorder;
}
public void setUseEndBorder(boolean useEndBorder)
{
useEndBorder_ = useEndBorder;
}
@Override
public void setData(ArrayList<ChunkOrLine> diffData, PatchMode patchMode)
{
removeStyleName(RES.cellTableStyle().stageMode());
removeStyleName(RES.cellTableStyle().workingMode());
switch (patchMode)
{
case Stage:
addStyleName(RES.cellTableStyle().stageMode());
break;
case Working:
addStyleName(RES.cellTableStyle().workingMode());
break;
}
lines_ = diffData;
setPageSize(diffData.size());
selectionModel_.clear();
firstSelectedLine_ = null;
setRowData(diffData);
startRows_.clear();
endRows_.clear();
Line.Type state = Line.Type.Same;
boolean suppressNextStart = true; // Suppress at start to avoid 2px border
for (int i = 0; i < lines_.size(); i++)
{
ChunkOrLine chunkOrLine = lines_.get(i);
Line line = chunkOrLine.getLine();
boolean isChunk = line == null;
Line.Type newState = isChunk ? Line.Type.Same : line.getType();
if (useStartBorder_ && i == 0)
startRows_.add(i);
// Edge case: last line is a diff line
if (useEndBorder_ && i == lines_.size() - 1)
endRows_.add(i);
if (newState != state)
{
// Note: endRows_ doesn't include the borders between insertions and
// deletions, or vice versa. This is to avoid 2px borders between
// these regions when just about everything else is 1px.
if (state != Line.Type.Same && newState == Line.Type.Same && !isChunk)
endRows_.add(i-1);
if (!suppressNextStart && newState != Line.Type.Same)
startRows_.add(i);
state = newState;
}
suppressNextStart = isChunk;
}
}
@Override
protected boolean canSelectVisibleRow(int visibleRow)
{
if (visibleRow < 0 || visibleRow >= lines_.size())
return false;
Line line = lines_.get(visibleRow).getLine();
return line != null && (line.getType() == Type.Insertion
|| line.getType() == Type.Deletion);
}
@Override
public void clear()
{
setData(new ArrayList<ChunkOrLine>(), PatchMode.Working);
}
@Override
public ArrayList<Line> getSelectedLines()
{
ArrayList<Line> selected = new ArrayList<Line>();
for (ChunkOrLine line : lines_)
if (line.getLine() != null && selectionModel_.isSelected(line))
selected.add(line.getLine());
return selected;
}
@Override
public ArrayList<Line> getAllLines()
{
ArrayList<Line> selected = new ArrayList<Line>();
for (ChunkOrLine line : lines_)
if (line.getLine() != null)
selected.add(line.getLine());
return selected;
}
@Override
public HandlerRegistration addDiffChunkActionHandler(DiffChunkActionHandler handler)
{
return addHandler(handler, DiffChunkActionEvent.TYPE);
}
@Override
public HandlerRegistration addDiffLineActionHandler(DiffLinesActionHandler handler)
{
return addHandler(handler, DiffLinesActionEvent.TYPE);
}
@Override
public HandlerRegistration addSelectionChangeHandler(SelectionChangeEvent.Handler handler)
{
return selectionModel_.addSelectionChangeHandler(handler);
}
public static void ensureStylesInjected()
{
RES.cellTableStyle().ensureInjected();
}
private boolean showActions_ = true;
private ArrayList<ChunkOrLine> lines_;
private SwitchableSelectionModel<ChunkOrLine> selectionModel_;
private HashSet<Integer> startRows_ = new HashSet<Integer>();
private HashSet<Integer> endRows_ = new HashSet<Integer>();
private boolean useStartBorder_ = false;
private boolean useEndBorder_ = true;
// Keep explicit track of the first selected line so we can render it differently
private ChunkOrLine firstSelectedLine_;
private static final LineTableViewCellTableResources RES = GWT.create(LineTableViewCellTableResources.class);
private static final LineActionButtonRenderer blueButtonRenderer_ = LineActionButtonRenderer.createBlue();
private static final LineActionButtonRenderer grayButtonRenderer_ = LineActionButtonRenderer.createGray();
}