// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.collide.client.diff;
import com.google.collide.client.ui.menu.PositionController.HorizontalAlign;
import com.google.collide.client.ui.menu.PositionController.VerticalAlign;
import com.google.collide.client.ui.tooltip.Tooltip;
import com.google.collide.client.util.Elements;
import com.google.collide.dto.DiffStatsDto;
import com.google.collide.mvp.CompositeView;
import com.google.collide.mvp.UiComponent;
import com.google.common.annotations.VisibleForTesting;
import com.google.gwt.resources.client.CssResource;
import elemental.html.DivElement;
import elemental.html.Element;
/**
* Presenter for the graphical delta statistics of a change. This presenter
* shows a set of bars that depict the change in percentage of
* added/deleted/changed/unmodified lines.
*
*/
public class DeltaInfoBar extends UiComponent<DeltaInfoBar.View> {
/**
* Static factory method for obtaining an instance of the DeltaInfoBar.
*/
public static DeltaInfoBar create(Resources resources, DiffStatsDto diffStats,
boolean includeTooltip) {
View view = new View(resources);
DeltaInfoBar deltaInfoBar = new DeltaInfoBar(resources, view, includeTooltip);
deltaInfoBar.setStats(diffStats);
return deltaInfoBar;
}
public interface Css extends CssResource {
String added();
String bar();
String count();
String inline();
String modified();
String deleted();
String unmodified();
}
public interface Resources extends Tooltip.Resources {
@Source("DeltaInfoBar.css")
Css deltaInfoBarCss();
}
public static class View extends CompositeView<Void> {
@VisibleForTesting
DivElement barsDiv;
private DivElement container;
private DivElement countDiv;
private final Resources res;
private final Css css;
private View(Resources res) {
this.res = res;
this.css = res.deltaInfoBarCss();
setElement(createDom());
}
/**
* Create several bars of the given style and append them to the element.
*/
private void createBars(Element element, int bars, String style) {
for (int i = 0; i < bars; i++) {
DivElement bar = Elements.createDivElement(css.bar());
bar.addClassName(style);
element.appendChild(bar);
}
}
private Element createDom() {
container = Elements.createDivElement(css.inline());
barsDiv = Elements.createDivElement(css.inline());
countDiv = Elements.createDivElement(css.inline(), css.count());
container.appendChild(barsDiv);
container.appendChild(countDiv);
return container;
}
}
@VisibleForTesting
static final int BAR_COUNT = 12;
/**
* The maximum number of affected lines to display. If the maximum number of
* affected lines exceeds this value, we display >##### instead of the full
* number. Used to improve formatting.
*/
private static final int MAX_AFFECTED = 10000;
/**
* For a given component, calculate the number of bars to display relative to
* the total. For a non-zero component, at least one bar will always be shown.
* Bar counts are always rounded down.
*/
@VisibleForTesting
static int calculateBars(int component, int total) {
if (total == 0) {
// Prevent divide-by-zero.
return 0;
}
double bars = (((double) component / (double) total) * BAR_COUNT);
// Force at least one bar if we have any of that type
if (component > 0 && bars < 1) {
bars = 1;
}
return (int) bars;
}
private final Css css;
private Tooltip tooltip;
private final boolean includeTooltip;
private DeltaInfoBar(Resources resources, View view, boolean includeTooltip) {
super(view);
css = resources.deltaInfoBarCss();
this.includeTooltip = includeTooltip;
}
/**
* Cleans up the tooltip used by this info bar.
*/
public void destroy() {
if (tooltip != null) {
tooltip.destroy();
tooltip = null;
}
}
/**
* Calculates and display the components for the given {@link DiffStatsDto}.
*/
public void setStats(DiffStatsDto diffStats) {
// Cleanup any old state.
destroy();
DivElement barsDiv = getView().barsDiv;
barsDiv.setInnerHTML("");
int total = getTotal(diffStats);
int addedBars = calculateBars(diffStats.getAdded(), total);
int deletedBars = calculateBars(diffStats.getDeleted(), total);
int modifiedBars = calculateBars(diffStats.getChanged(), total);
int unmodifiedBars = Math.max(0, BAR_COUNT - (addedBars + deletedBars + modifiedBars));
getView().createBars(barsDiv, addedBars, css.added());
getView().createBars(barsDiv, deletedBars, css.deleted());
getView().createBars(barsDiv, modifiedBars, css.modified());
getView().createBars(barsDiv, unmodifiedBars, css.unmodified());
int affected = getAffected(diffStats);
if (affected > MAX_AFFECTED) {
getView().countDiv.setTextContent(">" + String.valueOf(MAX_AFFECTED));
} else {
getView().countDiv.setTextContent(String.valueOf(affected));
}
// Create a tooltip for the bar.
if (includeTooltip) {
tooltip = Tooltip.create(getView().res, getView().container, VerticalAlign.MIDDLE,
HorizontalAlign.RIGHT, getDescription(diffStats));
}
}
/**
* Get the total number of lines affected by the change.
*/
private static int getAffected(DiffStatsDto diffStats) {
return diffStats.getAdded() + diffStats.getChanged() + diffStats.getDeleted();
}
/**
* Get the textual description of this line count suitable for the UI.
*/
private static String getDescription(DiffStatsDto diffStats) {
return "" + diffStats.getAdded() + " added, " + diffStats.getDeleted() + " deleted, "
+ diffStats.getChanged() + " changed (" + diffStats.getUnchanged() + " unchanged)";
}
/**
* Get the total count for each component of the change.
*/
private static int getTotal(DiffStatsDto diffStats) {
return getAffected(diffStats) + diffStats.getUnchanged();
}
}