Ticket #11153: 11153.2.patch

File 11153.2.patch, 16.6 KB (added by taylor.smock, 3 years ago)

Fix UI not showing selection

  • src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java

    Subject: [PATCH] #11153: improve readability of validator warnings
    ---
    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java
    a b  
    1111import java.util.HashMap;
    1212import java.util.HashSet;
    1313import java.util.List;
     14import java.util.Locale;
    1415import java.util.Map;
    1516import java.util.Objects;
    1617import java.util.Optional;
     
    4445import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
    4546import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
    4647import org.openstreetmap.josm.io.IllegalDataException;
     48import org.openstreetmap.josm.spi.preferences.Config;
    4749import org.openstreetmap.josm.tools.CheckParameterUtil;
    4850import org.openstreetmap.josm.tools.Logging;
    4951import org.openstreetmap.josm.tools.Utils;
     
    277279                    Integer.parseInt(m.group(1)), m.group(2), p);
    278280            try {
    279281                // Perform replacement with null-safe + regex-safe handling
    280                 m.appendReplacement(sb, String.valueOf(argument).replace("^(", "").replace(")$", ""));
     282                final String replacement = String.valueOf(argument).replace("^(", "").replace(")$", "");
     283                final String type = m.group(2);
     284                final String url;
     285                if ("key".equals(type) || "tag".equals(type)) {
     286                    url = Config.getUrls().getOSMWiki() + "/wiki/"
     287                            + m.group(2).substring(0, 1).toUpperCase(Locale.ROOT) + m.group(2).substring(1)
     288                            + ":" + replacement;
     289                } else {
     290                    url = null;
     291                }
     292                m.appendReplacement(sb, (url != null ? "<a href=\"" + url + "\">" : "")
     293                        + replacement
     294                        + (url != null ? "</a>" : ""));
    281295            } catch (IndexOutOfBoundsException | IllegalArgumentException e) {
    282296                Logging.log(Logging.LEVEL_ERROR, tr("Unable to replace argument {0} in {1}: {2}", argument, sb, e.getMessage()), e);
    283297            }
  • src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreePanel.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreePanel.java b/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreePanel.java
    a b  
    267267                    }
    268268                    final String msg = addSize(searchMsg, errorsWithDescription);
    269269
    270                     final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
     270                    final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode("<html>" + msg + "</html>");
    271271                    DefaultMutableTreeNode currNode = groupNode != null ? groupNode : severityNode;
    272272                    currNode.add(messageNode);
    273273                    if (oldExpandedRows.contains(searchMsg)) {
  • src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreeRenderer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreeRenderer.java b/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreeRenderer.java
    a b  
    33
    44import java.awt.Component;
    55
     6import javax.swing.JLabel;
    67import javax.swing.JTree;
    78import javax.swing.tree.DefaultMutableTreeNode;
    8 import javax.swing.tree.DefaultTreeCellRenderer;
    99
    1010import org.openstreetmap.josm.data.validation.Severity;
    1111import org.openstreetmap.josm.data.validation.TestError;
    1212import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
     13import org.openstreetmap.josm.gui.widgets.HtmlTreeCellRenderer;
    1314import org.openstreetmap.josm.tools.ImageProvider;
    1415
    1516/**
    1617 * Tree renderer for displaying errors
    1718 * @author frsantos
    1819 */
    19 public class ValidatorTreeRenderer extends DefaultTreeCellRenderer {
     20public class ValidatorTreeRenderer extends HtmlTreeCellRenderer {
     21
     22    private static final long serialVersionUID = 4750085115702320153L;
    2023
    2124    @Override
    2225    public Component getTreeCellRendererComponent(JTree tree, Object value,
    2326            boolean selected, boolean expanded, boolean leaf, int row,
    2427            boolean hasFocus) {
    25         super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
     28        final Component component = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
    2629
    27         DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
    28         Object nodeInfo = node.getUserObject();
     30        final DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
     31        final Object nodeInfo = node.getUserObject();
    2932
    30         if (nodeInfo instanceof Severity) {
    31             Severity s = (Severity) nodeInfo;
    32             setIcon(ImageProvider.get("data", s.getIcon()));
    33         } else if (nodeInfo instanceof TestError) {
    34             TestError error = (TestError) nodeInfo;
    35             MultipleNameVisitor v = error.getNameVisitor();
    36             setText(v.getText());
    37             setIcon(v.getIcon());
     33        if (component instanceof JLabel) {
     34            final JLabel label = (JLabel) component;
     35            if (nodeInfo instanceof Severity) {
     36                final Severity s = (Severity) nodeInfo;
     37                label.setIcon(ImageProvider.get("data", s.getIcon()));
     38            } else if (nodeInfo instanceof TestError) {
     39                final TestError error = (TestError) nodeInfo;
     40                MultipleNameVisitor v = error.getNameVisitor();
     41                label.setText(v.getText());
     42                label.setIcon(v.getIcon());
     43            }
    3844        }
    39         return this;
     45        return component;
    4046    }
    4147}
  • new file src/org/openstreetmap/josm/gui/widgets/HtmlTreeCellRenderer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/widgets/HtmlTreeCellRenderer.java b/src/org/openstreetmap/josm/gui/widgets/HtmlTreeCellRenderer.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.widgets;
     3
     4import java.awt.Color;
     5import java.awt.Component;
     6import java.awt.Point;
     7import java.awt.Rectangle;
     8import java.awt.event.MouseEvent;
     9import java.text.MessageFormat;
     10import java.util.Arrays;
     11
     12import javax.annotation.Nullable;
     13import javax.swing.JTree;
     14import javax.swing.event.HyperlinkEvent;
     15import javax.swing.event.MouseInputAdapter;
     16import javax.swing.text.html.HTMLDocument;
     17import javax.swing.text.html.StyleSheet;
     18import javax.swing.tree.DefaultMutableTreeNode;
     19import javax.swing.tree.DefaultTreeCellRenderer;
     20import javax.swing.tree.TreePath;
     21
     22import org.openstreetmap.josm.tools.ColorHelper;
     23import org.openstreetmap.josm.tools.OpenBrowser;
     24
     25/**
     26 * A {@link javax.swing.tree.TreeCellRenderer} for cells that may contain hyperlinks.
     27 * @since xxx
     28 */
     29public class HtmlTreeCellRenderer extends DefaultTreeCellRenderer {
     30    /**
     31     * This only exists since the JTree cannot pass events to subcomponents.
     32     */
     33    private static class JTreeMouseInputListener extends MouseInputAdapter {
     34        @Override
     35        public void mouseClicked(MouseEvent mouseEvent) {
     36            if (mouseEvent.getComponent() instanceof JTree) {
     37                final Point p = mouseEvent.getPoint();
     38                final JTree tree = (JTree) mouseEvent.getComponent();
     39                final Component component = getComponentAt(tree, p); // p is translated here
     40                if (component != null) {
     41                    component.dispatchEvent(new MouseEvent(component, // Use the component we found to fire the event
     42                            mouseEvent.getID(), mouseEvent.getWhen(), mouseEvent.getModifiers(),
     43                            p.x, p.y, mouseEvent.getClickCount(), mouseEvent.isPopupTrigger(),
     44                            mouseEvent.getButton()));
     45                }
     46            }
     47        }
     48
     49        /**
     50         * Get a component at
     51         * @param tree The tree to get the component from
     52         * @param p The point to get the component at (will be modified)
     53         * @return The component
     54         */
     55        @Nullable
     56        private static Component getComponentAt(JTree tree, Point p) {
     57            final TreePath path = tree.getPathForLocation(p.x, p.y);
     58            if (path != null) {
     59                final int row = tree.getRowForPath(path);
     60                final Object last = path.getLastPathComponent();
     61                // We need to get the component as "shown" so that we can get the link clicked.
     62                final Component component = tree.getCellRenderer().getTreeCellRendererComponent(tree, last,
     63                        tree.isRowSelected(row), tree.isExpanded(row),
     64                        tree.getModel().isLeaf(last), row, true);
     65                final Rectangle bounds = tree.getPathBounds(path);
     66                if (bounds != null) {
     67                    // If we don't translate the point, we are attempting to get the link in the wrong frame of reference
     68                    // The reference x/y are 0. This is set in BasicTextUI#getVisibleEditorRect, so we must translate the point here.
     69                    p.translate(-bounds.x, -bounds.y);
     70                    // Just make certain that we are consistent for other objects.
     71                    bounds.x = 0;
     72                    bounds.y = 0;
     73                    // Set the bounds from the JTree component (needed for proper layout)
     74                    component.setBounds(bounds);
     75                    return component;
     76                }
     77            }
     78            return null;
     79        }
     80    }
     81
     82    private static final long serialVersionUID = -4842755204541968238L;
     83    /**
     84     * A reusable panel to avoid new objects where possible.
     85     * It isn't worth it to make a new object for each row, since it doesn't receive mouse events (tested on Java 8).
     86     */
     87    private final JosmEditorPane panel = new JosmEditorPane();
     88    /** JTree does not send mouse events to subcomponents */
     89    private final JTreeMouseInputListener mouseTreeListener = new JTreeMouseInputListener();
     90
     91    /**
     92     * Create a new renderer
     93     */
     94    public HtmlTreeCellRenderer() {
     95        panel.addHyperlinkListener(e -> {
     96            if (e.getURL() != null && HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) {
     97                OpenBrowser.displayUrl(e.getURL().toString());
     98            }
     99        });
     100        JosmEditorPane.makeJLabelLike(panel, false);
     101        final Color defaultLink = ColorHelper.html2color(JosmEditorPane.getLinkColor());
     102        final Color lighterLink = defaultLink.brighter().brighter();
     103        final Color darkerLink = defaultLink.darker().darker();
     104        final String divRule = "{0} '{'background-color:{1}; color:{2};'}'";
     105        final String aRule = "{0} a '{'text-decoration: underline; color:{1}'}'";
     106        StyleSheet ss = ((HTMLDocument) panel.getDocument()).getStyleSheet();
     107        ss.addRule(MessageFormat.format(divRule, ".selected", ColorHelper.color2html(getBackgroundSelectionColor()),
     108                ColorHelper.color2html(getTextSelectionColor())));
     109        ss.addRule(MessageFormat.format(aRule, ".selected",
     110                ColorHelper.color2html(getAppropriateColor(getBackgroundSelectionColor(), defaultLink, lighterLink, darkerLink))));
     111        ss.addRule(MessageFormat.format(divRule, ".not-selected", ColorHelper.color2html(getBackgroundNonSelectionColor()),
     112                ColorHelper.color2html(getTextNonSelectionColor())));
     113        ss.addRule(MessageFormat.format(aRule, ".not-selected",
     114                ColorHelper.color2html(getAppropriateColor(getBackgroundNonSelectionColor(), defaultLink, lighterLink, darkerLink))));
     115    }
     116
     117    @Override
     118    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded,
     119                                                  boolean leaf, int row, boolean hasFocus) {
     120        if (value instanceof DefaultMutableTreeNode) {
     121            if (Arrays.stream(tree.getMouseListeners()).noneMatch(this.mouseTreeListener::equals)) {
     122                tree.addMouseListener(this.mouseTreeListener);
     123                tree.addMouseMotionListener(this.mouseTreeListener);
     124            }
     125            final DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
     126            final Object object = node.getUserObject();
     127            if (object instanceof String && ((String) object).startsWith("<html>") && ((String) object).endsWith("</html>")) {
     128                final String text = (String) object;
     129                final String modified = "<html><div class=\"" + (sel ? "selected" : "not-selected") + "\">"
     130                        + text.substring("<html>".length(), text.length() - "</html>".length()) + "</div></html>";
     131                this.panel.setText(modified);
     132                return this.panel;
     133            }
     134        }
     135        return super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
     136    }
     137
     138    /**
     139     * Convert the rgb value for use in a relative luminance calculation
     140     * @param rgb The value to convert
     141     * @return The appropriate value to use in the calculation
     142     */
     143    private static double convertRGB(int rgb) {
     144        final double normalized = rgb / 255d;
     145        if (normalized <= 0.03928) {
     146            return normalized / 12.92;
     147        }
     148        return Math.pow(((normalized + 0.055)/1.055), 2.4);
     149    }
     150
     151    /**
     152     * Calculate the contrast ratio between two luminance values
     153     * @param luminanceOne The first luminance value
     154     * @param luminanceTwo The second luminance value
     155     * @return The contrast ratio. Higher is usually better.
     156     */
     157    private static double contrastRatio(double luminanceOne, double luminanceTwo) {
     158        final double min = Math.min(luminanceOne, luminanceTwo);
     159        final double max = Math.max(luminanceOne, luminanceTwo);
     160        return (max + 0.05) / (min + 0.05);
     161    }
     162
     163    /**
     164     * Calculate the luminance of a color
     165     * @param red The red part
     166     * @param green The green part
     167     * @param blue The blue part
     168     * @return The relative luminance (0-1, 0 black, 1 white)
     169     */
     170    private static double luminance(int red, int green, int blue) {
     171        return 0.2126 * convertRGB(red)
     172                + 0.7152 * convertRGB(green)
     173                + 0.0722 * convertRGB(blue);
     174    }
     175
     176    /**
     177     * Get the appropriate color given a background
     178     * @param background The background color
     179     * @param options The options to choose from
     180     * @return The appropriate color
     181     */
     182    private static Color getAppropriateColor(Color background, Color... options) {
     183        if (options.length < 2) {
     184            throw new IllegalArgumentException("There must be at least two color options");
     185        }
     186        // sRGB calculation
     187        final double backgroundLuminance = luminance(background.getRed(), background.getGreen(), background.getBlue());
     188        double lastContrastRatio = 0; // W3 recommends a contrast ratio of at least 4.5:1. We should be shooting for 7:1.
     189        Color lastColor = null;
     190        for (Color option : options) {
     191            final double contrastRatio = contrastRatio(backgroundLuminance, luminance(option.getRed(), option.getGreen(), option.getBlue()));
     192            if (contrastRatio > lastContrastRatio) {
     193                lastContrastRatio = contrastRatio;
     194                lastColor = option;
     195            }
     196        }
     197        return lastColor;
     198    }
     199}