package jmri.util; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import javax.annotation.Nonnull; import java.awt.Container; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.swing.*; import org.junit.jupiter.api.Assertions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Accessibility Tool for checking swing JFrames and JPanels. *

* Checks for JButton, JTextArea, JTextField components to * ensure they have accessible content for screen reading software. * * @author Steve Young Copyright (C) 2022 */ public class AccessibilityChecks { private static final boolean LOGSYSTEMOUT = Boolean.getBoolean("jmri.util.AccessibilityChecks.logToSystemOut"); // false unless set true private static final boolean WARNISSUES = Boolean.getBoolean("jmri.util.AccessibilityChecks.warnOnIssue"); // false unless set true private static final boolean ASSERTFAIL = Boolean.getBoolean("jmri.util.AccessibilityChecks.failOnIssue"); // false unless set true private static final boolean INCLUDELAF = Boolean.getBoolean("jmri.util.AccessibilityChecks.includeLaf"); // false unless set true /** * Check a JPanel or Container for Accessibility issues. *

* Typical usage would be to pass a JPanel. * * @param contentPane eg. JFrame.getContentPane() or a JPanel * @return Empty string if no errors, else String containing details. */ @Nonnull public static String check( @Nonnull final Container contentPane) { return check(contentPane, false); } /** * Check a JPanel or Container for Accessibility issues. *

* Typical usage would be to pass a JPanel. * * @param contentPane eg. JFrame.getContentPane() or a JPanel * @param failOnIssue set true to fail the unit test if any issue found. * @return Empty string if no errors, else String containing details. */ @Nonnull public static String check( @Nonnull final Container contentPane, final boolean failOnIssue) { return feedBack(getSingleContentPaneList(contentPane), failOnIssue); } /** * Check a Frame for Accessibility issues. *

* Typical usage would be to pass a JFrame. * Searches the frame via * getContentPane() , getLayeredPane(), getRootPane() * for issues. * * @param frame a JFrame for which to search through. * @return Empty string if no errors, else String containing details. */ @Nonnull public static String check(@Nonnull JFrame frame) { return check(frame, false); } /** * Check a Frame for Accessibility issues. *

* Typical usage would be to pass a JFrame. * Searches the frame via * getContentPane() , getLayeredPane(), getRootPane() * for issues. * * @param frame a JFrame for which to search through. * @param failOnIssue set true to fail the unit test if any issue found. * @return Empty string if no errors, else String containing details. */ @Nonnull public static String check(@Nonnull JFrame frame, final boolean failOnIssue) { HashSet set = new HashSet<>(); set.addAll(getSingleContentPaneList(frame.getContentPane())); set.addAll(getSingleContentPaneList(frame.getLayeredPane())); set.addAll(getSingleContentPaneList(frame.getRootPane())); return feedBack(set, failOnIssue); } private static Set getSingleContentPaneList(@Nonnull final Container contentPane){ HashSet set = new HashSet<>(); set.addAll(checkComponent(contentPane, JButton.class)); set.addAll(checkComponent(contentPane, JTextArea.class)); set.addAll(checkComponent(contentPane, JTextField.class)); return set; } @SuppressFBWarnings(value = "SLF4J_SIGN_ONLY_FORMAT",justification = "getMessageString(components) contains context information") private static String feedBack(Set components, boolean forceFailOnIssue) { if (components.isEmpty()){ return ""; } String msg = getMessageString(components); if ( LOGSYSTEMOUT ) { System.out.println(msg); } if ( WARNISSUES ) { log.warn("{}",msg); } if ( ASSERTFAIL || forceFailOnIssue ) { Assertions.fail(msg); } return msg; } private static boolean includeComponent(JComponent component){ if (!INCLUDELAF) { if ( component.getClass().getName().contains(".laf.") ) { return false; } if ( component.getClass().getName().contains(".plaf.") ) { return false; } } return true; } private static String getMessageString(Set components){ StringBuilder sb = new StringBuilder(); sb.append(components.size()).append(" Potential Issue(s) found. "); components.forEach(s -> { sb.append(System.getProperty("line.separator")); sb.append("No accessible Content for: ").append(s.getClass()); if ( s.getName() != null ) { sb.append(" Name:").append(s.getName()); } if ( s.getToolTipText() != null ) { sb.append(" ToolTip:").append(s.getToolTipText()); } }); sb.append(System.getProperty("line.separator")); return sb.toString(); } private static Set checkComponent( final Container container, final Class componentType ) { Set as = findComponents(container, componentType); HashSet list = new HashSet<>(); as.forEach(s -> { String accessibleContent = s.getAccessibleContext().getAccessibleName(); if (accessibleContent == null || accessibleContent.isEmpty()) { if (includeComponent(s)) { list.add(s); } } }); return list; } // recursive loop to find all components which match // is there a way of matching a set of JButton, JTextarea etc. into this ? private static Set findComponents( final Container container, final Class componentType ) { return Stream.concat( Arrays.stream(container.getComponents()) .filter(componentType::isInstance) .map(componentType::cast), Arrays.stream(container.getComponents()) .filter(Container.class::isInstance) .map(Container.class::cast) .flatMap(c -> findComponents(c, componentType).stream()) ).collect(Collectors.toSet()); } private static final Logger log = LoggerFactory.getLogger(AccessibilityChecks.class); }