Index: src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java	(revision 15890)
+++ src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java	(working copy)
@@ -54,6 +54,7 @@
 import org.openstreetmap.josm.gui.MapView;
 import org.openstreetmap.josm.gui.SideButton;
 import org.openstreetmap.josm.gui.dialogs.layer.ActivateLayerAction;
+import org.openstreetmap.josm.gui.dialogs.layer.CycleLayerAction;
 import org.openstreetmap.josm.gui.dialogs.layer.DeleteLayerAction;
 import org.openstreetmap.josm.gui.dialogs.layer.DuplicateAction;
 import org.openstreetmap.josm.gui.dialogs.layer.LayerListTransferHandler;
@@ -141,6 +142,8 @@
     private final ActivateLayerAction activateLayerAction;
     private final ShowHideLayerAction showHideLayerAction;
 
+    private final CycleLayerAction cycleLayerAction;
+
     //TODO This duplicates ShowHide actions functionality
     /** stores which layer index to toggle and executes the ShowHide action if the layer is present */
     private final class ToggleLayerIndexVisibility extends AbstractAction {
@@ -329,6 +332,9 @@
         // Show/Activate layer on Enter key press
         InputMapUtils.addSpacebarAction(layerList, showHideLayerAction);
 
+        // Cycle layer action
+        cycleLayerAction = new CycleLayerAction();
+
         createLayout(layerList, true, Arrays.asList(
                 new SideButton(moveUpAction, false),
                 new SideButton(moveDownAction, false),
@@ -388,6 +394,7 @@
         DISPLAY_NUMBERS.removeListener(visibilityWidthListener);
         ExpertToggleAction.removeExpertModeChangeListener(visibilityWidthListener);
         layerManager.removeLayerChangeListener(visibilityWidthListener);
+        cycleLayerAction.destroy();
         super.destroy();
         instance = null;
     }
Index: src/org/openstreetmap/josm/gui/dialogs/layer/CycleLayerAction.java
===================================================================
--- src/org/openstreetmap/josm/gui/dialogs/layer/CycleLayerAction.java	(nonexistent)
+++ src/org/openstreetmap/josm/gui/dialogs/layer/CycleLayerAction.java	(working copy)
@@ -0,0 +1,84 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.layer;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.openstreetmap.josm.actions.JosmAction;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.gui.layer.MainLayerManager;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.openstreetmap.josm.tools.Shortcut;
+
+/**
+ * Allow users to cycle between adjacent layers easily
+ *
+ * @author Taylor Smock
+ * @since xxx
+ */
+public class CycleLayerAction extends JosmAction {
+    private static final Shortcut cycleUp =
+            Shortcut.registerShortcut("core:cyclelayerup", tr("Cycle layers up"), KeyEvent.VK_OPEN_BRACKET, Shortcut.SHIFT);
+    private static final Shortcut cycleDown =
+            Shortcut.registerShortcut("core:cyclelayerdown", tr("Cycle layers down"), KeyEvent.VK_CLOSE_BRACKET, Shortcut.SHIFT);
+
+    /**
+     * Create a CycleLayerAction that cycles through layers that are in the model
+     */
+    public CycleLayerAction() {
+        super(tr("Cycle layers"), "dialogs/next", tr("Cycle through layers"), cycleDown, true,
+                "cycle-layer", false);
+        MainApplication.registerActionShortcut(this, cycleUp); // cycleDown gets registered by JosmAction
+        new ImageProvider("dialogs", "next").getResource().attachImageIcon(this, true);
+        putValue(SHORT_DESCRIPTION, tr("Cycle through visible layers."));
+        putValue(NAME, tr("Cycle layers"));
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        MainLayerManager manager = MainApplication.getLayerManager();
+        List<Layer> managerLayers = manager.getLayers().stream().filter(layer -> !(layer instanceof ImageryLayer))
+                .collect(Collectors.toList());
+        if (managerLayers.isEmpty())
+            return;
+        int key = KeyEvent.getExtendedKeyCodeForChar(e.getActionCommand().charAt(0));
+        char character = KeyEvent.getKeyText(key).charAt(0);
+        int realKey = KeyEvent.getExtendedKeyCodeForChar(character);
+        Shortcut shortcut = Shortcut
+                .findShortcut(realKey, cycleDown.getAssignedModifier())
+                .orElse(null);
+
+        List<Layer> layers;
+        if (cycleUp.equals(shortcut)) {
+            int index = managerLayers.indexOf(manager.getActiveLayer());
+            int sublist = index < managerLayers.size() ? index + 1 : index;
+            if (index >= managerLayers.size() - 1) {
+                sublist = 0;
+            }
+            layers = managerLayers.subList(sublist, managerLayers.size());
+        } else {
+            layers = new ArrayList<>(managerLayers);
+            Collections.reverse(layers);
+            int index = layers.indexOf(manager.getActiveLayer());
+            int sublist = index < managerLayers.size() - 1 ? index + 1 : 0;
+            layers = layers.subList(sublist, layers.size());
+        }
+        manager.setActiveLayer(layers.stream().filter(Layer::isVisible).filter(tlayer -> !(tlayer instanceof ImageryLayer))
+                .findFirst()
+                .orElse(manager.getActiveLayer()));
+    }
+
+    @Override
+    public void destroy() {
+        super.destroy();
+        MainApplication.unregisterActionShortcut(this, cycleUp);
+    }
+}

Property changes on: src\org\openstreetmap\josm\gui\dialogs\layer\CycleLayerAction.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
Index: test/unit/org/openstreetmap/josm/gui/dialogs/layer/CycleLayerActionTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/dialogs/layer/CycleLayerActionTest.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/gui/dialogs/layer/CycleLayerActionTest.java	(working copy)
@@ -0,0 +1,126 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.gui.dialogs.layer;
+
+import static org.junit.Assert.assertEquals;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
+import org.openstreetmap.josm.gui.layer.MainLayerManager;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.tools.Shortcut;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Test class for {@link CycleLayerAction}
+ *
+ * @author Taylor Smock
+ */
+public class CycleLayerActionTest {
+    /** Layers need a projection */
+    @Rule
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules test = new JOSMTestRules().main().preferences().projection().fakeImagery();
+
+    CycleLayerAction cycle;
+    MainLayerManager manager;
+
+    static final int MODIFIER = Shortcut.SHIFT;
+    static final int KEYUP = KeyEvent.VK_OPEN_BRACKET;
+    static final int KEYDOWN = KeyEvent.VK_CLOSE_BRACKET;
+    ActionEvent down;
+    ActionEvent up;
+
+    /**
+     * Set up common items (make layers, etc.)
+     */
+    @Before
+    public void setUp() {
+        cycle = new CycleLayerAction();
+        manager = MainApplication.getLayerManager();
+        for (int i = 0; i < 10; i++) {
+            manager.addLayer(new OsmDataLayer(new DataSet(), tr("Layer {0}", i), null));
+        }
+        up = new ActionEvent(this, ActionEvent.ACTION_FIRST, KeyEvent.getKeyText(KEYUP), MODIFIER);
+        down = new ActionEvent(this, ActionEvent.ACTION_FIRST, KeyEvent.getKeyText(KEYDOWN), MODIFIER);
+    }
+
+    /**
+     * Test going down from the bottom
+     */
+    @Test
+    public void testDownBottom() {
+        manager.setActiveLayer(manager.getLayers().get(0));
+        cycle.actionPerformed(down);
+        assertEquals(manager.getLayers().size() - 1, manager.getLayers().indexOf(manager.getActiveLayer()));
+    }
+
+    /**
+     * Check going up from the top
+     */
+    @Test
+    public void testUpTop() {
+        manager.setActiveLayer(manager.getLayers().get(manager.getLayers().size() - 1));
+        cycle.actionPerformed(up);
+        assertEquals(0, manager.getLayers().indexOf(manager.getActiveLayer()));
+    }
+
+    /**
+     * Check going down
+     */
+    @Test
+    public void testDown() {
+        manager.setActiveLayer(manager.getLayers().get(3));
+        cycle.actionPerformed(down);
+        assertEquals(2, manager.getLayers().indexOf(manager.getActiveLayer()));
+    }
+
+    /**
+     * Check going up
+     */
+    @Test
+    public void testUp() {
+        manager.setActiveLayer(manager.getLayers().get(3));
+        cycle.actionPerformed(up);
+        assertEquals(4, manager.getLayers().indexOf(manager.getActiveLayer()));
+    }
+
+    /**
+     * Test no layers
+     */
+    @Test
+    public void testNoLayers() {
+        manager.getLayers().forEach(manager::removeLayer);
+        cycle.actionPerformed(up);
+        cycle.actionPerformed(down);
+        assertEquals(0, manager.getLayers().size());
+    }
+
+    /**
+     * Test with an aerial imagery layer
+     */
+    @Test
+    public void testWithAerialImagery() {
+        final ImageryInfo magentaTilesInfo = ImageryLayerInfo.instance.getLayers().stream()
+                .filter(i -> i.getName().equals("Magenta Tiles")).findAny().get();
+        ImageryLayer imageryLayer = ImageryLayer.create(magentaTilesInfo);
+        manager.addLayer(imageryLayer);
+        manager.moveLayer(imageryLayer, 5);
+        manager.setActiveLayer(manager.getLayers().get(4));
+        cycle.actionPerformed(up);
+        assertEquals(6, manager.getLayers().indexOf(manager.getActiveLayer()));
+        cycle.actionPerformed(down);
+        assertEquals(4, manager.getLayers().indexOf(manager.getActiveLayer()));
+    }
+}

Property changes on: test\unit\org\openstreetmap\josm\gui\dialogs\layer\CycleLayerActionTest.java
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
