diff --git a/game-app/game-core/src/main/java/games/strategy/engine/framework/map/download/DownloadMapsWindow.java b/game-app/game-core/src/main/java/games/strategy/engine/framework/map/download/DownloadMapsWindow.java index f79158f526e..54668adc505 100644 --- a/game-app/game-core/src/main/java/games/strategy/engine/framework/map/download/DownloadMapsWindow.java +++ b/game-app/game-core/src/main/java/games/strategy/engine/framework/map/download/DownloadMapsWindow.java @@ -233,11 +233,12 @@ private JTabbedPane newAvailableInstalledTabbedPanel( mapList.getOutOfDateExcluding(pendingDownloads); // For the UX, always show an available maps tab, even if it is empty final JPanel available = - newMapSelectionPanel(mapList.getAvailableExcluding(pendingDownloads), MapAction.INSTALL); + newMapSelectionPanel( + mapList.getAvailableExcluding(pendingDownloads), MapAction.INSTALL, true); tabbedPane.addTab("New Maps", available); if (!outOfDateDownloads.isEmpty()) { - final JPanel outOfDate = newMapSelectionPanel(outOfDateDownloads, MapAction.UPDATE); + final JPanel outOfDate = newMapSelectionPanel(outOfDateDownloads, MapAction.UPDATE, false); tabbedPane.addTab("Updates Available", outOfDate); } @@ -247,14 +248,17 @@ private JTabbedPane newAvailableInstalledTabbedPanel( mapList.getInstalled().keySet().stream() .sorted(Comparator.comparing(m -> m.getMapName().toUpperCase())) .collect(Collectors.toList()), - MapAction.REMOVE); + MapAction.REMOVE, + false); tabbedPane.addTab("Installed", installed); } return tabbedPane; } private JPanel newMapSelectionPanel( - final List unsortedMaps, final MapAction action) { + final List unsortedMaps, + final MapAction action, + final boolean requestFocus) { final JPanel main = new JPanelBuilder().border(30).borderLayout().build(); final JEditorPane descriptionPane = SwingComponents.newHtmlJEditorPane(); main.add(SwingComponents.newJScrollPane(descriptionPane), BorderLayout.CENTER); @@ -264,6 +268,9 @@ private JPanel newMapSelectionPanel( if (!unsortedMaps.isEmpty()) { final MapDownloadSwingTable mapDownloadSwingTable = new MapDownloadSwingTable(unsortedMaps); final JTable gamesList = mapDownloadSwingTable.getSwingComponent(); + if (requestFocus) { + SwingUtilities.invokeLater(() -> gamesList.requestFocus()); + } mapDownloadSwingTable.addMapSelectionListener( mapSelections -> newDescriptionPanelUpdatingSelectionListener( diff --git a/game-app/game-core/src/main/java/games/strategy/engine/framework/map/download/MapDownloadSwingTable.java b/game-app/game-core/src/main/java/games/strategy/engine/framework/map/download/MapDownloadSwingTable.java index 4d4749819da..141b33059ac 100644 --- a/game-app/game-core/src/main/java/games/strategy/engine/framework/map/download/MapDownloadSwingTable.java +++ b/game-app/game-core/src/main/java/games/strategy/engine/framework/map/download/MapDownloadSwingTable.java @@ -12,6 +12,7 @@ import org.triplea.http.client.maps.listing.MapDownloadItem; import org.triplea.http.client.maps.listing.MapTag; import org.triplea.swing.JTableBuilder; +import org.triplea.swing.JTableTypeAheadListener; /** * UI component representing a list of maps to download. The table is sortable and displays @@ -50,6 +51,7 @@ public MapDownloadSwingTable(final Collection maps) { .collect(Collectors.toList())) .rowMapper(mapDownloadListing -> rowMapper(mapDownloadListing, tagNames)) .build(); + table.addKeyListener(new JTableTypeAheadListener(table, 0)); } /** diff --git a/lib/swing-lib/src/main/java/org/triplea/swing/JTableTypeAheadListener.java b/lib/swing-lib/src/main/java/org/triplea/swing/JTableTypeAheadListener.java new file mode 100644 index 00000000000..83cede21dfa --- /dev/null +++ b/lib/swing-lib/src/main/java/org/triplea/swing/JTableTypeAheadListener.java @@ -0,0 +1,51 @@ +package org.triplea.swing; + +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import javax.swing.JTable; +import lombok.RequiredArgsConstructor; + +/** A KeyListener that implements row selection in a JTable by prefix-matching typed text. */ +@RequiredArgsConstructor +public class JTableTypeAheadListener extends KeyAdapter { + private static int INPUT_RESET_TIME_MS = 500; // 0.5s + + private final JTable table; + // Column that contains text data that should be matched. + private final int columnIndex; + + private String inputString = ""; + private long keyPressTime; + + @Override + public void keyPressed(KeyEvent evt) { + char ch = evt.getKeyChar(); + if (!Character.isLetterOrDigit(ch)) { + return; + } + + long time = System.currentTimeMillis(); + if (time > keyPressTime + INPUT_RESET_TIME_MS) { + inputString = ""; + } + keyPressTime = keyPressTime; + inputString += Character.toLowerCase(ch); + + final var tableModel = table.getModel(); + final int rowCount = tableModel.getRowCount(); + final int selectedRow = table.getSelectedRow(); + for (int i = 0; i < rowCount; i++) { + int row = (selectedRow + i) % rowCount; + String str = "" + tableModel.getValueAt(row, columnIndex); + if (str.toLowerCase().startsWith(inputString)) { + selectRow(row); + break; + } + } + } + + private void selectRow(int rowIndex) { + table.setRowSelectionInterval(rowIndex, rowIndex); + table.scrollRectToVisible(table.getCellRect(rowIndex, 0, true)); + } +}