/* * Copyright 2011 Luke Usherwood. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 2.1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package net.bettyluke.tracinstant.ui; import java.awt.FlowLayout; import java.awt.Font; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import javax.swing.Action; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import net.bettyluke.swing.ScrollingMenu; public class MenuCascader { private static final int MAX_COMPACT_TOP_HITS = 20; interface Item extends Action { String getName(); int getHits(); } private final int maxSubMenuSize; private final int minSubMenuSize; private static String m_LastName, m_NextName; public MenuCascader() { this(35, 15); } public MenuCascader(int maxSubMenuSize, int minSubMenuSize) { if (maxSubMenuSize < 2) { throw new IllegalArgumentException( "MenuCascader requires a maximum target size of at least 2"); } if (minSubMenuSize < 1) { throw new IllegalArgumentException( "MenuCascader requires a minimum target size of at least 2"); } this.maxSubMenuSize = maxSubMenuSize; this.minSubMenuSize = minSubMenuSize; } public JPopupMenu create(List<Item> items) { if (items.size() <= maxSubMenuSize) { return createTinyMenu(items); } JPopupMenu menu = createCascadedMenu(items); insertTopHits(menu, items); return menu.getComponentCount() < 40 ? menu : new ScrollingMenu(menu, 40); } private void insertTopHits(JPopupMenu menu, List<Item> items) { int nSubMenus = menu.getComponentCount(); int compact = Math.min(MAX_COMPACT_TOP_HITS, maxSubMenuSize - nSubMenus - 2 - 5); if (compact > minSubMenuSize) { List<Item> topHits = getTopHits(items, compact); menu.insert(createHeadingPanel("All"), 0); menu.add(createHeadingPanel("Top")); addAllToMenu(menu, topHits); } else { List<Item> topHits = getTopHits(items, maxSubMenuSize - 5); JMenu subMenu = new JMenu("Top"); styleHeadingComponent(subMenu); addAllToMenu(subMenu, topHits); menu.insert(subMenu, 0); } } private JPanel createHeadingPanel(String text) { JPanel result = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 2)); JLabel label = new JLabel(" " + text + " "); styleHeadingComponent(label); result.add(label); return result; } private void styleHeadingComponent(JComponent comp) { Font font = comp.getFont(); comp.setOpaque(true); comp.setFont(font.deriveFont(Font.ITALIC | Font.BOLD)); } private List<Item> getTopHits(List<Item> items, int count) { Item[] sorted = items.toArray(new Item[0]); // Sort by number of hits. Note: this uses a stable sort. Arrays.sort(sorted, (o1, o2) -> -(o1.getHits() - o2.getHits())); // Trim to length. sorted = Arrays.copyOf(sorted, count); // Resort alphabetically. Arrays.sort(sorted, (o1, o2) -> o1.getName().compareToIgnoreCase(o2.getName())); return Arrays.asList(sorted); } private JPopupMenu createTinyMenu(List<Item> items) { final JPopupMenu menu = new JPopupMenu(); addAllToMenu(menu, items); return menu; } private JPopupMenu createCascadedMenu(List<Item> items) { final JPopupMenu menu = new JPopupMenu(); m_LastName = ""; // Destroy a copy of the list, not the passed argument. items = new LinkedList<>(items); List<Item> batch = new ArrayList<>(); int depth = 1; while (!items.isEmpty()) { String firstName = items.get(0).getName(); if (firstName.length() < depth) { transferFromHead(items, 1, batch); continue; } String start = firstName.substring(0, depth); int count = countItemsBeginingWith(items, start); if (count + batch.size() <= maxSubMenuSize) { transferFromHead(items, count, batch); } else if (batch.size() >= minSubMenuSize) { menu.add(createSubMenu(batch, items)); batch.clear(); depth = 1; } else { ++depth; } } menu.add(createSubMenu(batch, items)); return menu; } private JMenu createSubMenu(List<Item> batch, List<Item> subsequent) { m_NextName = subsequent.isEmpty() ? "" : subsequent.get(0).getName(); String from = batch.get(0).getName(); String to = batch.get(batch.size() - 1).getName(); final JMenu subMenu = new JMenu(createMenuName(from, to)); addAllToMenu(subMenu, batch); m_LastName = to; return subMenu; } private String createMenuName(String from, String to) { from = abbreviate(from, m_LastName); to = abbreviate(to, m_LastName, m_NextName); if (from.equalsIgnoreCase(to)) { return titleCaps(from); } return titleCaps(from) + " - " + titleCaps(to); } /** Return the shortest substring from 'name' that distinguishes it from 'against' */ private String abbreviate(String name, String... others) { next_char: for (int i = 0; i < name.length(); ++i) { for (String other : others) { if (i < other.length() && charsMatch(name, other, i)) { continue next_char; } } return name.substring(0, i + 1); } return name; } private boolean charsMatch(String s1, String s2, int index) { return Character.toUpperCase(s1.charAt(index)) == Character.toUpperCase(s2.charAt(index)); } private String titleCaps(String str) { if (str.isEmpty()) { return ""; } String result = str.substring(0, 1).toUpperCase(); return (str.length() == 1) ? result : result + str.substring(1).toLowerCase(); } private int countItemsBeginingWith(List<Item> items, String start) { int count = 0; for (Item item : items) { String text = item.getName(); if (text.startsWith(start) || text.toUpperCase().startsWith(start.toUpperCase())) { count++; } else { break; } } return count; } private void transferFromHead(List<Item> from, int count, List<Item> to) { for (int i = 0; i < count; ++i) { to.add(from.remove(0)); } } private void addAllToMenu(final JMenu menu, List<Item> items) { for (JMenuItem mi : createMenuItems(items)) { menu.add(mi); } } private void addAllToMenu(final JPopupMenu menu, List<Item> items) { for (JMenuItem mi : createMenuItems(items)) { menu.add(mi); } } private List<JMenuItem> createMenuItems(List<Item> items) { List<JMenuItem> menuItems = new ArrayList<>(items.size()); for (Item item : items) { JMenuItem mi = new JMenuItem(item); menuItems.add(mi); } return menuItems; } }