/* * Copyright (C) 2014 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.android.tools.idea.wizard; import com.android.sdklib.repository.FullRevision; import com.google.common.collect.Lists; import com.google.gson.*; import com.google.gson.reflect.TypeToken; import com.intellij.openapi.diagnostic.Logger; import com.intellij.ui.JBColor; import com.intellij.util.ResourceUtil; import com.intellij.util.ui.GraphicsUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.IOException; import java.lang.reflect.Type; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; /** * Chart of distributions */ public class DistributionChartComponent extends JPanel { private static final Logger LOG = Logger.getInstance(DistributionChartComponent.class); // Because this text overlays colored components, it must stay white/gray, and does not change for dark themes.  private static final Color TEXT_COLOR = new Color(0xFEFEFE); private static final Color API_LEVEL_COLOR = new Color(0, 0, 0, 77); private static final int INTER_SECTION_SPACING = 1; private static final double MIN_PERCENTAGE_HEIGHT = 0.06; private static final double EXPANSION_ON_SELECTION = 1.063882064; private static final double RIGHT_GUTTER_PERCENTAGE = 0.209708738; private static final int TOP_PADDING = 40; private static final int NAME_OFFSET = 50; private static final int MIN_API_FONT_SIZE = 18; private static final int MAX_API_FONT_SIZE = 45; private static final int API_OFFSET = 120; private static final int NUMBER_OFFSET = 10; private static Font MEDIUM_WEIGHT_FONT; private static Font REGULAR_WEIGHT_FONT; private static Font VERSION_NAME_FONT; private static Font VERSION_NUMBER_FONT; private static Font TITLE_FONT; // These colors do not change for dark vs light theme. // These colors come from our UX team and they are very adamant // about their exactness. Hardcoding them is a pain. private static final Color[] RECT_COLORS = new Color[] { new Color(0xcbdfcb), new Color(0x7dc691), new Color(0x92b2b7), new Color(0xdeba40), new Color(0xe55d5f), new Color(0x6ec0d2), new Color(0xd88d63), new Color(0xff9229), new Color(0xeabd2d) }; private static List<Distribution> ourDistributions; private int[] myCurrentBottoms; private Distribution mySelectedDistribution; private DistributionSelectionChangedListener myListener; public void init() { addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent mouseEvent) { int y = mouseEvent.getY(); int i = 0; while (i < myCurrentBottoms.length && y > myCurrentBottoms[i]) { ++i; } if (i < myCurrentBottoms.length) { mySelectedDistribution = ourDistributions.get(i); if (myListener != null) { myListener.onDistributionSelected(mySelectedDistribution); } repaint(); } } }); if (ourDistributions == null) { try { String jsonString = ResourceUtil.loadText(ResourceUtil.getResource(this.getClass(), "wizardData", "distributions.json")); ourDistributions = loadDistributionsFromJson(jsonString); } catch (IOException e) { throw new RuntimeException(e); } } loadFonts(); } private static void loadFonts() { if (MEDIUM_WEIGHT_FONT == null) { REGULAR_WEIGHT_FONT = new Font("Sans", Font.PLAIN, 12); MEDIUM_WEIGHT_FONT = new Font("Sans", Font.BOLD, 12); VERSION_NAME_FONT = REGULAR_WEIGHT_FONT.deriveFont((float)16.0); VERSION_NUMBER_FONT = REGULAR_WEIGHT_FONT.deriveFont((float)20.0); TITLE_FONT = MEDIUM_WEIGHT_FONT.deriveFont((float)16.0); } } @Nullable private static List<Distribution> loadDistributionsFromJson(String jsonString) { Type fullRevisionType = new TypeToken<FullRevision>(){}.getType(); GsonBuilder gsonBuilder = new GsonBuilder() .registerTypeAdapter(fullRevisionType, new JsonDeserializer<FullRevision>() { @Override public FullRevision deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { return FullRevision.parseRevision(json.getAsString()); } }); Gson gson = gsonBuilder.create(); Type listType = new TypeToken<ArrayList<Distribution>>() {}.getType(); try { return gson.fromJson(jsonString, listType); } catch (JsonParseException e) { LOG.error(e); } return null; } public void registerDistributionSelectionChangedListener(@NotNull DistributionSelectionChangedListener listener) { myListener = listener; } @Override public Dimension getMinimumSize() { return new Dimension(300, 300); } @Override public void paintComponent(Graphics g) { GraphicsUtil.setupAntialiasing(g); GraphicsUtil.setupAAPainting(g); super.paintComponent(g); if (myCurrentBottoms == null) { myCurrentBottoms = new int[ourDistributions.size()]; } // Draw the proportioned rectangles int startY = TOP_PADDING; int totalWidth = getBounds().width; int rightGutter = (int)Math.round(totalWidth * RIGHT_GUTTER_PERCENTAGE); int width = totalWidth - rightGutter; int normalBoxSize = (int)Math.round((float)width/EXPANSION_ON_SELECTION); int leftGutter = (width - normalBoxSize) / 2; // Measure our fonts FontMetrics titleMetrics = g.getFontMetrics(TITLE_FONT); int titleHeight = titleMetrics.getHeight(); FontMetrics versionNumberMetrics = g.getFontMetrics(VERSION_NUMBER_FONT); int halfVersionNumberHeight = (versionNumberMetrics.getHeight() - versionNumberMetrics.getDescent()) / 2; FontMetrics versionNameMetrics = g.getFontMetrics(VERSION_NAME_FONT); int halfVersionNameHeight = (versionNameMetrics.getHeight() - versionNameMetrics.getDescent()) / 2; // Draw the titles g.setFont(TITLE_FONT); g.drawString("API Level".toUpperCase(), leftGutter, titleHeight); String distributionTitle = "Distribution".toUpperCase(); String accumulativeTitle = "Cumulative".toUpperCase(); g.drawString(accumulativeTitle, totalWidth - titleMetrics.stringWidth(accumulativeTitle), titleHeight); g.drawString(distributionTitle, totalWidth - titleMetrics.stringWidth(distributionTitle), titleHeight * 2); // We want a padding in between every element int heightToDistribute = getBounds().height - INTER_SECTION_SPACING * (ourDistributions.size() - 1) - TOP_PADDING; // Keep track of how much of the distribution we've covered so far double percentageSum = 0; int smallItemCount = 0; for (Distribution d : ourDistributions) { if (d.distributionPercentage < MIN_PERCENTAGE_HEIGHT) { smallItemCount++; } } heightToDistribute -= (int)Math.round(smallItemCount * MIN_PERCENTAGE_HEIGHT * heightToDistribute); int i = 0; for (Distribution d : ourDistributions) { if (d.color == null) { d.color = RECT_COLORS[i % RECT_COLORS.length]; } // Draw the colored rectangle g.setColor(d.color); double effectivePercentage = Math.max(d.distributionPercentage, MIN_PERCENTAGE_HEIGHT); int calculatedHeight = (int)Math.round(effectivePercentage * heightToDistribute); int bottom = startY + calculatedHeight; if (d.equals(mySelectedDistribution)) { g.fillRect(0, bottom - calculatedHeight, width, calculatedHeight); } else { g.fillRect(leftGutter, bottom - calculatedHeight, normalBoxSize, calculatedHeight); } // Size our fonts according to the rectangle size Font apiLevelFont = REGULAR_WEIGHT_FONT.deriveFont(logistic(effectivePercentage, MIN_API_FONT_SIZE, MAX_API_FONT_SIZE)); // Measure our font heights so we can center text FontMetrics apiLevelMetrics = g.getFontMetrics(apiLevelFont); int halfApiFontHeight = (apiLevelMetrics.getHeight() - apiLevelMetrics.getDescent()) / 2; int currentMidY = startY + calculatedHeight/2; // Write the name g.setColor(TEXT_COLOR); g.setFont(VERSION_NAME_FONT); myCurrentBottoms[i] = bottom; g.drawString(d.name, leftGutter + NAME_OFFSET, currentMidY + halfVersionNameHeight); // Write the version number g.setColor(API_LEVEL_COLOR); g.setFont(VERSION_NUMBER_FONT); String versionString = d.version.toString().substring(0, 3); g.drawString(versionString, leftGutter + NUMBER_OFFSET, currentMidY + halfVersionNumberHeight); // Write the API level g.setFont(apiLevelFont); g.drawString(Integer.toString(d.apiLevel), width - API_OFFSET, currentMidY + halfApiFontHeight); // Write the supported distribution percentageSum += d.distributionPercentage; // Write the percentage sum if (i < ourDistributions.size() - 1) { g.setColor(JBColor.foreground()); g.setFont(VERSION_NUMBER_FONT); String percentageString = new DecimalFormat("0.0%").format(.999 - percentageSum); int percentStringWidth = versionNumberMetrics.stringWidth(percentageString); g.drawString(percentageString, totalWidth - percentStringWidth - 2, bottom - 2); g.setColor(JBColor.darkGray); g.drawLine(leftGutter + normalBoxSize, startY + calculatedHeight, totalWidth, startY + calculatedHeight); } startY += calculatedHeight + INTER_SECTION_SPACING; i++; } } /** * Get an S-Curve value between min and max * @param normalizedValue a value between 0 and 1 * @return an integer between the given min and max value */ private static float logistic(double normalizedValue, int min, int max) { double t = normalizedValue * 1; double result = (max * min * Math.exp(min * t)) / (max + min * Math.exp(min * t)); return (float)result; } protected static class Distribution implements Comparable<Distribution> { public static class TextBlock { public String title; public String body; } public int apiLevel; public FullRevision version; public double distributionPercentage; public String name; public Color color; public String description; public String url; public List<TextBlock> descriptionBlocks; private Distribution() { // Private default for json conversion } @Override public int compareTo(Distribution other) { return Integer.valueOf(apiLevel).compareTo(other.apiLevel); } } public interface DistributionSelectionChangedListener { void onDistributionSelected(Distribution d); } public double getSupportedDistributionForApiLevel(int apiLevel) { double unsupportedSum = 0; for (Distribution d : ourDistributions) { if (d.apiLevel >= apiLevel) { break; } unsupportedSum += d.distributionPercentage; } return 1 - unsupportedSum; } }