sclass BackgroundProcessesUI { Set processes = syncLinkedHashSet(); SimpleLiveValue lvCount = new SimpleLiveValue(Int, 0); srecord Entry(S name) { settable JMenuItem menuItem; toString { ret name; } } JLabel shortLabel() { var lbl = bindToolTipToTransformedLiveValue( n -> makeToolTip(), lvCount, simpleTransformedLiveValueLabel(n -> n2(n), lvCount)); onMouseDown_anyButton(lbl, e -> { var l = cloneList(processes); // make sure title is displayed fully JPopupMenu menu = new { public Dimension getMinimumSize() { TitledBorder border = optCast TitledBorder(getBorder()); ret maxDimension(super.getMinimumSize(), border?.getMinimumSize(this)); } }; int n = componentCount(menu); for (Entry a : l) addMenuItem(menu, processToMenuItem(a)); if (componentCount(menu) != n) { setBorder(menu, jRaisedSectionBorder("Background Processes")); showPopupMenu(menu, e); } }); ret lbl; } void add(Entry process) { if (processes.add(process)) lvCount.set(l(processes)); } void remove(Entry process) { if (processes.remove(process)) lvCount.set(l(processes)); } void addOrRemove(bool add, Entry process) { if (add) add(process); else remove(process); } S makeToolTip() { var l = cloneList(processes); ret empty(l) ? "No background processes" : n2(l, "background process", "background processes") + ": " + joinWithComma(processes); } swappable JMenuItem processToMenuItem(Entry process) { ret process.getMenuItem(); } }