From de6d4e61e53bc79f3b08f3cea0aca215aeef8f6d Mon Sep 17 00:00:00 2001
From: Robert Scott <code@humanleg.org.uk>
Date: Sun, 8 Oct 2017 10:28:26 +0100
Subject: [PATCH 2/3] SlippyMapBBoxChooser: redesign SourceButton using a
 regular drop-down JPopupMenu

this behaves more similarly to other ui components in the application and allows
for more extensibility in that we're now able to allow the "show downloaded area"
feature to be enabled or disabled through a simple JCheckBoxMenuItem
---
 .../josm/gui/bbox/SlippyMapBBoxChooser.java        |  24 ++-
 .../openstreetmap/josm/gui/bbox/SourceButton.java  | 216 +++++++++------------
 2 files changed, 113 insertions(+), 127 deletions(-)

diff --git a/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java b/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
index 6635f4f..93d4358 100644
--- a/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
+++ b/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
@@ -21,8 +21,12 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
 
+import javax.swing.ButtonModel;
+import javax.swing.JToggleButton;
 import javax.swing.JOptionPane;
 import javax.swing.SpringLayout;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.ChangeEvent;
 
 import org.openstreetmap.gui.jmapviewer.Coordinate;
 import org.openstreetmap.gui.jmapviewer.JMapViewer;
@@ -55,7 +59,7 @@ import org.openstreetmap.josm.tools.Logging;
 /**
  * This panel displays a map and lets the user chose a {@link BBox}.
  */
-public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser, MainLayerManager.ActiveLayerChangeListener {
+public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser, ChangeListener, MainLayerManager.ActiveLayerChangeListener {
 
     /**
      * A list of tile sources that can be used for displaying the map.
@@ -130,6 +134,7 @@ public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser, Mai
     private final transient OsmTileLoader uncachedLoader;
 
     private final SizeButton iSizeButton;
+    private final ButtonModel showDownloadAreaButtonModel = new JToggleButton.ToggleButtonModel();
     private final SourceButton iSourceButton;
     private transient Bounds bbox;
 
@@ -172,10 +177,11 @@ public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser, Mai
 
         List<TileSource> tileSources = getAllTileSources();
 
-        iSourceButton = new SourceButton(this, tileSources);
+        this.showDownloadAreaButtonModel.addChangeListener(this);
+        iSourceButton = new SourceButton(this, tileSources, this.showDownloadAreaButtonModel);
         add(iSourceButton);
-        springLayout.putConstraint(SpringLayout.EAST, iSourceButton, 0, SpringLayout.EAST, this);
-        springLayout.putConstraint(SpringLayout.NORTH, iSourceButton, 30, SpringLayout.NORTH, this);
+        springLayout.putConstraint(SpringLayout.EAST, iSourceButton, -2, SpringLayout.EAST, this);
+        springLayout.putConstraint(SpringLayout.NORTH, iSourceButton, 2, SpringLayout.NORTH, this);
 
         iSizeButton = new SizeButton(this);
         add(iSizeButton);
@@ -230,7 +236,7 @@ public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser, Mai
         // and it has defined bounds. Routine is analogous to that in OsmDataLayer's paint routine (but just different
         // enough to make sharing code impractical)
         final OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
-        if (editLayer != null && Config.getPref().getBoolean("draw.data.downloaded_area", true) && !editLayer.data.getDataSources().isEmpty()) {
+        if (editLayer != null && this.showDownloadAreaButtonModel.isSelected() && !editLayer.data.getDataSources().isEmpty()) {
             // initialize area with current viewport
             Rectangle b = this.getBounds();
             // ensure we comfortably cover full area
@@ -273,6 +279,11 @@ public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser, Mai
         this.repaint();
     }
 
+    @Override
+    public void stateChanged(ChangeEvent e) {
+        this.repaint();
+    }
+
     /**
      * Enables the disk tile cache.
      * @param enabled true to enable, false to disable
@@ -341,6 +352,9 @@ public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser, Mai
         this.tileController.setTileCache(new MemoryTileCache());
         this.setTileSource(tileSource);
         PROP_MAPSTYLE.put(tileSource.getName()); // TODO Is name really unique?
+        if (this.iSourceButton.getCurrentSource() != tileSource) { // prevent infinite recursion
+            this.iSourceButton.setCurrentMap(tileSource);
+        }
     }
 
     @Override
diff --git a/src/org/openstreetmap/josm/gui/bbox/SourceButton.java b/src/org/openstreetmap/josm/gui/bbox/SourceButton.java
index 6c995a1..8b44a39 100644
--- a/src/org/openstreetmap/josm/gui/bbox/SourceButton.java
+++ b/src/org/openstreetmap/josm/gui/bbox/SourceButton.java
@@ -1,23 +1,29 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.gui.bbox;
 
-import java.awt.Color;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
 import java.awt.Dimension;
-import java.awt.Font;
-import java.awt.FontMetrics;
-import java.awt.Graphics;
-import java.awt.Graphics2D;
-import java.awt.Point;
-import java.awt.RenderingHints;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.awt.event.MouseListener;
+import java.awt.event.ActionListener;
+import java.awt.event.ActionEvent;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
 
+import javax.swing.AbstractButton;
+import javax.swing.ButtonGroup;
+import javax.swing.ButtonModel;
 import javax.swing.ImageIcon;
 import javax.swing.JComponent;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JPopupMenu;
+import javax.swing.JRadioButtonMenuItem;
+import javax.swing.JToggleButton;
 
 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
+import org.openstreetmap.josm.gui.widgets.PopupMenuButton;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
 import org.openstreetmap.josm.tools.ImageProvider;
 
@@ -25,121 +31,100 @@ import org.openstreetmap.josm.tools.ImageProvider;
  * Button that allows to choose the imagery source used for slippy map background.
  * @since 1390
  */
-public class SourceButton extends JComponent {
-
-    private static final int LAYER_HEIGHT = 20;
-    private static final int LEFT_PADDING = 5;
-    private static final int TOP_PADDING = 5;
-    private static final int BOTTOM_PADDING = 5;
-
-    private transient TileSource[] sources;
-
-    private final ImageIcon enlargeImage;
-    private final ImageIcon shrinkImage;
-    private final Dimension hiddenDimension;
-
-    // Calculated after component is added to container
-    private int barWidth;
-    private Dimension shownDimension;
-    private Font font;
+public class SourceButton extends PopupMenuButton {
+    protected class TileSourceButtonModel extends JToggleButton.ToggleButtonModel implements ActionListener {
+        protected final TileSource tileSource;
+
+        public TileSourceButtonModel(TileSource tileSource_) {
+            super();
+            this.tileSource = tileSource_;
+            this.addActionListener(this);
+        }
 
-    private boolean isEnlarged;
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            if (SourceButton.this.slippyMapBBoxChooser.getTileController().getTileSource() != this.tileSource) { // prevent infinite recursion
+                SourceButton.this.slippyMapBBoxChooser.toggleMapSource(this.tileSource);
+            }
+        }
+    }
 
-    private int currentMap;
-    private final SlippyMapBBoxChooser slippyMapBBoxChooser;
+    protected final SlippyMapBBoxChooser slippyMapBBoxChooser;
+    protected final ButtonModel showDownloadAreaButtonModel;
+    private List<TileSource> sources;
+    private ButtonGroup sourceButtonGroup;
 
     /**
      * Constructs a new {@code SourceButton}.
      * @param slippyMapBBoxChooser parent slippy map
      * @param sources list of imagery sources to display
      */
-    public SourceButton(SlippyMapBBoxChooser slippyMapBBoxChooser, Collection<TileSource> sources) {
-        this.slippyMapBBoxChooser = slippyMapBBoxChooser;
-        setSources(sources);
-        enlargeImage = ImageProvider.get("layer-switcher-maximize");
-        shrinkImage = ImageProvider.get("layer-switcher-minimize");
+    public SourceButton(
+        SlippyMapBBoxChooser slippyMapBBoxChooser_,
+        Collection<TileSource> sources_,
+        ButtonModel showDownloadAreaButtonModel_
+    ) {
+        super(new ImageProvider("dialogs/layerlist").getResource().getImageIcon(new Dimension(16, 16)));
+        this.showDownloadAreaButtonModel = showDownloadAreaButtonModel_;
+        this.slippyMapBBoxChooser = slippyMapBBoxChooser_;
+        this.setPreferredSize(new Dimension(24, 24));
+        this.setSources(sources_);
+    }
+
+    protected void generatePopupMenu() {
+        JPopupMenu pm = new JPopupMenu();
+        this.sourceButtonGroup = new ButtonGroup();
+        for (TileSource ts : this.sources) {
+            JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(ts.getName());
+            TileSourceButtonModel buttonModel = new TileSourceButtonModel(ts);
+            menuItem.setModel(buttonModel);
+            pm.add(menuItem);
+            this.sourceButtonGroup.add(menuItem);
+
+            // attempt to initialize button group matching current state of slippyMapBBoxChooser
+            buttonModel.setSelected(this.slippyMapBBoxChooser.getTileController().getTileSource() == ts);
+        }
+
+        pm.addSeparator();
 
-        hiddenDimension = new Dimension(enlargeImage.getIconWidth(), enlargeImage.getIconHeight());
-        setPreferredSize(hiddenDimension);
+        JCheckBoxMenuItem showDownloadAreaItem = new JCheckBoxMenuItem(tr("Show downloaded area"));
+        showDownloadAreaItem.setModel(this.showDownloadAreaButtonModel);
+        pm.add(showDownloadAreaItem);
 
-        addMouseListener(mouseListener);
+        this.setPopupMenu(pm);
     }
 
-    private final transient MouseListener mouseListener = new MouseAdapter() {
-        @Override
-        public void mouseReleased(MouseEvent e) {
-            if (e.getButton() == MouseEvent.BUTTON1) {
-                Point point = e.getPoint();
-                if (isEnlarged) {
-                    if (barWidth < point.x && point.y < shrinkImage.getIconHeight()) {
-                        toggle();
-                    } else {
-                        int result = (point.y - 5) / LAYER_HEIGHT;
-                        if (result >= 0 && result < SourceButton.this.sources.length) {
-                            SourceButton.this.slippyMapBBoxChooser.toggleMapSource(SourceButton.this.sources[result]);
-                            currentMap = result;
-                            toggle();
-                        }
-                    }
-                } else {
-                    toggle();
-                }
-            }
+    private void setSourceDefault() {
+        Enumeration<AbstractButton> elems = this.sourceButtonGroup.getElements();
+        if (elems.hasMoreElements()) {
+            elems.nextElement().setSelected(true);
         }
-    };
+    }
 
     /**
      * Set the tile sources.
      * @param sources The tile sources to display
      * @since 6364
      */
-    public final void setSources(Collection<TileSource> sources) {
-        CheckParameterUtil.ensureParameterNotNull(sources, "sources");
-        this.sources = sources.toArray(new TileSource[sources.size()]);
-        shownDimension = null;
+    public final void setSources(Collection<TileSource> sources_) {
+        CheckParameterUtil.ensureParameterNotNull(sources_, "sources_");
+        this.sources = new ArrayList<TileSource>(sources_);
+        this.generatePopupMenu();
+        if (this.sourceButtonGroup.getSelection() == null) {
+            this.setSourceDefault();
+        }
     }
 
-    @Override
-    protected void paintComponent(Graphics graphics) {
-        Graphics2D g = (Graphics2D) graphics.create();
-        try {
-            calculateShownDimension();
-            g.setFont(font);
-            if (isEnlarged) {
-                g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
-                int radioButtonSize = 10;
-
-                g.setColor(new Color(0, 0, 139, 179));
-                g.fillRoundRect(0, 0, barWidth + shrinkImage.getIconWidth(),
-                        sources.length * LAYER_HEIGHT + TOP_PADDING + BOTTOM_PADDING, 10, 10);
-                for (int i = 0; i < sources.length; i++) {
-                    g.setColor(Color.WHITE);
-                    g.fillOval(LEFT_PADDING, TOP_PADDING + i * LAYER_HEIGHT + 6, radioButtonSize, radioButtonSize);
-                    g.drawString(sources[i].getName(), LEFT_PADDING + radioButtonSize + LEFT_PADDING,
-                            TOP_PADDING + i * LAYER_HEIGHT + g.getFontMetrics().getHeight());
-                    if (currentMap == i) {
-                        g.setColor(Color.BLACK);
-                        g.fillOval(LEFT_PADDING + 1, TOP_PADDING + 7 + i * LAYER_HEIGHT, radioButtonSize - 2, radioButtonSize - 2);
-                    }
-                }
-
-                g.drawImage(shrinkImage.getImage(), barWidth, 0, null);
-            } else {
-                g.drawImage(enlargeImage.getImage(), 0, 0, null);
-            }
-        } finally {
-            g.dispose();
-        }
+    public final Collection<TileSource> getSources() {
+        return Collections.unmodifiableCollection(this.sources);
     }
 
-    /**
-     * Toggle the visibility of imagery source list.
-     */
-    public void toggle() {
-        this.isEnlarged = !this.isEnlarged;
-        calculateShownDimension();
-        setPreferredSize(isEnlarged ? shownDimension : hiddenDimension);
-        revalidate();
+    public final TileSource getCurrentSource() {
+        TileSourceButtonModel buttonModel = (TileSourceButtonModel) this.sourceButtonGroup.getSelection();
+        if (buttonModel != null) {
+            return buttonModel.tileSource;
+        }
+        return null;
     }
 
     /**
@@ -147,28 +132,15 @@ public class SourceButton extends JComponent {
      * @param tileSource the new imagery source to use
      */
     public void setCurrentMap(TileSource tileSource) {
-        for (int i = 0; i < sources.length; i++) {
-            if (sources[i].equals(tileSource)) {
-                currentMap = i;
+        Enumeration<AbstractButton> elems = this.sourceButtonGroup.getElements();
+        while (elems.hasMoreElements()) {
+            AbstractButton b = elems.nextElement();
+            if (((TileSourceButtonModel) b.getModel()).tileSource == tileSource) {
+                b.setSelected(true);
                 return;
             }
         }
-        currentMap = 0;
-    }
-
-    private void calculateShownDimension() {
-        if (shownDimension == null) {
-            font = getFont().deriveFont(Font.BOLD).deriveFont(15.0f);
-            int textWidth = 0;
-            FontMetrics fm = getFontMetrics(font);
-            for (TileSource source: sources) {
-                int width = fm.stringWidth(source.getName());
-                if (width > textWidth) {
-                    textWidth = width;
-                }
-            }
-            barWidth = textWidth + 50;
-            shownDimension = new Dimension(barWidth + shrinkImage.getIconWidth(), sources.length * LAYER_HEIGHT + TOP_PADDING + BOTTOM_PADDING);
-        }
+        // failed to find the correct one
+        this.setSourceDefault();
     }
 }
-- 
2.1.4

