package net.bettyluke.tracinstant.plugins; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Toolkit; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.TreeMap; import javax.swing.DefaultListCellRenderer; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import net.bettyluke.util.swing.VerticallyScrollingPanel; import net.bettyluke.tracinstant.data.Ticket; import net.bettyluke.util.swing.ArrayListModel; public class HistogramPane { private static final Dimension PANEL_PREFERRED_SIZE = new Dimension(300, 50); private static final String[] DEFAULT_FIELDS = { "Milestone", "Reporter", "Owner", "Type", "Priority", "Component", "Version", "Severity" }; public static ToolPlugin createPlugin() { return new HistogramPane().new Plugin(); } /** Holder class fetches rendering hints the first time it is needed, and then caches it. */ private static class HintHolder { static final Map<?,?> HINTS; static { Toolkit tk = Toolkit.getDefaultToolkit(); Map<?,?> hints = (Map<?,?>) tk.getDesktopProperty("awt.font.desktophints"); HINTS = hints == null ? Collections.emptyMap() : hints; } } private static final class Bar { private final String label; public int active = 0; public int closed = 0; public int selectedActive = 0; public int selectedClosed = 0; public Bar(String name) { label = name; } private int total() { return active + closed; } @Override public String toString() { return closed + " / " + total(); } } private static final class RenderInfo { private int maxResultsInCategory; private int maxStrLen; public RenderInfo(Collection<Bar> bars) { for (Bar bar : bars) { int total = bar.total(); if (maxResultsInCategory < total) { maxResultsInCategory = total; } int strLen = bar.toString().length(); if (maxStrLen < strLen) { maxStrLen = strLen; } } } } private final JComboBox<String> fieldSelector; private final JPanel mainPanel; private final JPanel histogram; private Ticket[] ticketsInView = new Ticket[0]; private Ticket[] selectedTickets = new Ticket[0]; private RenderInfo renderInfo = null; private JList<Bar> labels; private JList<Bar> histi; private static final class LabelRenderer extends DefaultListCellRenderer { @Override public Component getListCellRendererComponent(JList<?> list, final Object value, int index, boolean isSelected, boolean cellHasFocus) { Component comp = super.getListCellRendererComponent( list, value, index, isSelected, cellHasFocus); Bar bar = (Bar) value; JLabel label = (JLabel) comp; label.setHorizontalAlignment(RIGHT); label.setText(sanitiseLabel(bar.label)); return comp; } private String sanitiseLabel(String text) { if (text.isEmpty()) { text = " "; } int at = text.indexOf("@"); if (at >= 0 && at < text.length() - 4) { text = text.subSequence(0, at + 4) + "\u2026"; } return text; } } private final class BarRenderer extends DefaultListCellRenderer { Bar bar = null; @Override public Component getListCellRendererComponent(JList<?> list, final Object value, int index, boolean isSelected, boolean cellHasFocus) { bar = (Bar) value; return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); } @Override protected void paintComponent(Graphics g) { int w = getWidth() - getPreferredSize().width; Graphics2D g2 = (Graphics2D) g.create(); g2.addRenderingHints(HintHolder.HINTS); float closedRatio = (float) bar.closed / renderInfo.maxResultsInCategory; float totalRatio = (float) bar.total() / renderInfo.maxResultsInCategory; float selClosedRatio = (float) bar.selectedClosed / renderInfo.maxResultsInCategory; float selActiveRatio = (float) bar.selectedActive / renderInfo.maxResultsInCategory; int xClosed = (int) (w * closedRatio); int xTotal = (int) (w * totalRatio); g2.setColor(Color.GREEN.darker()); g2.fillRect(0, 5, xClosed, 12); g2.drawRect(0, 5, xTotal, 12); g2.setColor(new Color(96, 96, 224)); g2.fillRect(0, 5, (int) (w * selClosedRatio), 13); g2.setColor(new Color(200, 200, 255)); g2.fillRect(xClosed, 6, (int) (w * selActiveRatio), 11); g2.setColor(Color.RED.darker()); g2.drawString(bar.toString(), xTotal + 6, 16); } } /** The interface through which the application interacts with us. */ private class Plugin extends ToolPlugin { @Override public JComponent initialise(TicketUpdater tu) { return mainPanel; } @Override public void ticketViewUpdated(Ticket[] inView, Ticket[] selected) { ticketsInView = Arrays.copyOf(inView, inView.length); selectedTickets = Arrays.copyOf(selected, selected.length); updateHistogram(); } @Override public String toString() { return "Histograms"; } @Override public void hidden() { // TODO: Save last-used view preferences } } public HistogramPane() { fieldSelector = createFieldSelection(); labels = new JList<>(); labels.setCellRenderer(new LabelRenderer()); histi = new JList<>(); histi.setCellRenderer(new BarRenderer()); histogram = VerticallyScrollingPanel.create(histi); histogram.add(labels, BorderLayout.WEST); JScrollPane scroll = new JScrollPane(histogram); scroll.getViewport().setBackground(Color.WHITE); mainPanel = createMainPanel(fieldSelector, scroll); } private JComboBox<String> createFieldSelection() { JComboBox<String> combo = new JComboBox<>(DEFAULT_FIELDS); combo.addItemListener(e -> updateHistogram()); return combo; } private JPanel createMainPanel(JComponent north, JComponent centre) { JPanel panel = new JPanel(new BorderLayout()); panel.add(north, BorderLayout.NORTH); panel.add(new JScrollPane(centre)); panel.setPreferredSize(PANEL_PREFERRED_SIZE); return panel; } private void updateHistogram() { String field = ((String) fieldSelector.getSelectedItem()); Collection<Bar> bars = getBars(field).values(); populateView(bars); } private Map<String, Bar> getBars(String field) { Map<String, Bar> results = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); for (Ticket ticket : ticketsInView) { String value = ticket.getValue(field); if (value != null) { Bar bar = getOrCreate(results, value); if ("closed".equals(ticket.getValue("status"))) { bar.closed++; } else { bar.active++; } } } for (Ticket ticket : selectedTickets) { String value = ticket.getValue(field); if (value != null) { Bar bar = getOrCreate(results, value); if ("closed".equals(ticket.getValue("status"))) { bar.selectedClosed++; } else { bar.selectedActive++; } } } return results; } private Bar getOrCreate(Map<String, Bar> results, String value) { Bar bar = results.get(value); if (bar == null) { bar = new Bar(value); results.put(value, bar); } return bar; } private void populateView(Collection<Bar> bars) { renderInfo = new RenderInfo(bars); ArrayListModel<Bar> model = ArrayListModel.of(bars); labels.setModel(model); histi.setModel(model); histogram.invalidate(); histogram.revalidate(); histogram.repaint(); } }