Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dialog for choosing scenarios #5211

Merged
merged 4 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

MMSVersion=1

Name=Instant Fame:Northwind Highlanders
Name=Instant Fame: Northwind Highlanders
Description=This scenario visits a fight of McCormack's Fusiliers in the campaign on Bellatrix against Ajax's Avengers. The retreating Avengers tried deperately to shake the Highlanders as they fled east across the southern continent of Bellatrix, but to no avail. Exhausted and bogged down by bad weather, the Avengers had no choice but to stand and fight.

Description=The campaign on Bellatrix against Ajax's Avengers brought McCormack's Fusiliers instant fame. Arriving to find themselves outnumbered three-to-one, the Fusiliers quickly used their superior grasp of tactics and their ferocity to smash the defending unit. The retreating Avengers tried deperately to shake the Highlanders as they fled east across the southern continent of Bellatrix, but to no avail. Exhausted and bogged down by bad weather, the Avengers had no choice but to stand and fight.
#FullDescription=The campaign on Bellatrix against Ajax's Avengers brought McCormack's Fusiliers instant fame. Arriving to find themselves outnumbered three-to-one, the Fusiliers quickly used their superior grasp of tactics and their ferocity to smash the defending unit. The retreating Avengers tried deperately to shake the Highlanders as they fled east across the southern continent of Bellatrix, but to no avail. Exhausted and bogged down by bad weather, the Avengers had no choice but to stand and fight.

BoardWidth=2
BoardHeight=1
Expand Down
3 changes: 3 additions & 0 deletions megamek/i18n/megamek/client/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4539,3 +4539,6 @@ CMVPanel.font=Font:
Gamemaster.Gamemaster=Gamemaster
Gamemaster.EditDamage=Edit Damage
Gamemaster.Configure=Configure

# Scenario Chooser
ScenarioChooser.title=Choose Scenario
1 change: 1 addition & 0 deletions megamek/src/megamek/SuiteConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public abstract class SuiteConstants {

//region File Formats
public static final String TRUETYPE_FONT = ".ttf";
public static final String SCENARIO_EXT = ".mms";
//endregion File Formats

//region File Paths
Expand Down
4 changes: 4 additions & 0 deletions megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -3109,6 +3109,10 @@ public static List<String> filteredFiles(File path, String fileEnding) {
}

public static List<String> filteredFilesWithSubDirs(File path, String fileEnding) {
if (!path.exists()) {
LogManager.getLogger().warn("Path " + path + " does not exist.");
return new ArrayList<>();
}
try (Stream<Path> entries = Files.walk(path.toPath())) {
return entries.map(Objects::toString).filter(name -> name.endsWith(fileEnding)).collect(toList());
} catch (IOException e) {
Expand Down
46 changes: 6 additions & 40 deletions megamek/src/megamek/client/ui/swing/MegaMekGUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import megamek.client.ui.swing.dialog.MainMenuUnitBrowserDialog;
import megamek.client.ui.swing.gameConnectionDialogs.ConnectDialog;
import megamek.client.ui.swing.gameConnectionDialogs.HostDialog;
import megamek.client.ui.swing.scenario.ScenarioChooser;
import megamek.client.ui.swing.skinEditor.SkinEditorMainGUI;
import megamek.client.ui.swing.tooltip.PilotToolTip;
import megamek.client.ui.swing.util.MegaMekController;
Expand All @@ -52,7 +53,7 @@
import megamek.common.util.ImageUtil;
import megamek.common.util.fileUtils.MegaMekFile;
import megamek.server.GameManager;
import megamek.server.ScenarioLoader;
import megamek.common.scenario.ScenarioLoader;
import megamek.server.Server;
import megamek.utilities.xml.MMXMLUtility;
import org.apache.logging.log4j.LogManager;
Expand Down Expand Up @@ -687,48 +688,13 @@ public void quickLoadGame() {
* Host a game constructed from a scenario file
*/
void scenario() {
JFileChooser fc = new JFileChooser("data" + File.separatorChar + "scenarios");
fc.setLocation(frame.getLocation().x + 150, frame.getLocation().y + 100);
fc.setDialogTitle(Messages.getString("MegaMek.SelectScenarioDialog.title"));

FileFilter filter = new FileFilter() {

@Override
public boolean accept(File f) {
if (f.isDirectory()) {
return true;
}

String ext = null;
String s = f.getName();
int i = s.lastIndexOf('.');

if ((i > 0) && (i < (s.length() - 1))) {
ext = s.substring(i + 1).toLowerCase();
}

if (ext != null) {
return ext.equalsIgnoreCase("mms");
}

return false;
}

@Override
public String getDescription() {
return "MegaMek Scenario Files";
}

};
fc.setFileFilter(filter);

int returnVal = fc.showOpenDialog(frame);
if ((returnVal != JFileChooser.APPROVE_OPTION) || (fc.getSelectedFile() == null)) {
// I want a file, y'know!
ScenarioChooser scenarioChooser = new ScenarioChooser(frame);
scenarioChooser.setVisible(true);
if (scenarioChooser.getSelectedScenarioFilename() == null) {
return;
}

ScenarioLoader sl = new ScenarioLoader(fc.getSelectedFile());
ScenarioLoader sl = new ScenarioLoader(new File(scenarioChooser.getSelectedScenarioFilename()));
Game g;
try {
g = sl.createGame();
Expand Down
217 changes: 217 additions & 0 deletions megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
* Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
*
* This file is part of MegaMek.
*
* MegaMek is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MegaMek is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MegaMek. If not, see <http://www.gnu.org/licenses/>.
*/
package megamek.client.ui.swing.scenario;

import megamek.MMConstants;
import megamek.client.ui.Messages;
import megamek.client.ui.baseComponents.AbstractButtonDialog;
import megamek.client.ui.swing.ButtonEsc;
import megamek.client.ui.swing.CloseAction;
import megamek.client.ui.swing.CommonSettingsDialog;
import megamek.client.ui.swing.dialog.DialogButton;
import megamek.client.ui.swing.util.ClickableLabel;
import megamek.client.ui.swing.util.UIUtil;
import megamek.common.Configuration;
import megamek.common.annotations.Nullable;
import megamek.common.preference.PreferenceManager;
import megamek.common.scenario.ScenarioInfo;
import megamek.common.scenario.ScenarioLoader;
import org.apache.logging.log4j.LogManager;

import javax.swing.*;
import javax.swing.border.MatteBorder;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* This dialog lists all scenarios found in MM's scenario directory as well as the corresponding user directory.
* The scenarios are grouped by subdirectory (only the one directly below scenarios, further subdirectories
* are scanned for scenarios but not used for grouping). After closing, a chosen scenario can be retrieved
* by calling {@link #getSelectedScenarioFilename()}. As a fallback, the dialog also allows choosing a
* scenario by file.
*/
public class ScenarioChooser extends AbstractButtonDialog {

private final JTabbedPane tabbedPane = new JTabbedPane();
private final Map<String, List<ScenarioInfo>> sortedScenarios = sortScenarios(getScenarioInfos());
private String scenarioFileName;

public ScenarioChooser(final JFrame parentFrame) {
super(parentFrame, "ScenarioChooser", "ScenarioChooser.title");
initialize();
setMinimumSize(UIUtil.scaleForGUI(ScenarioInfoPanel.BASE_MINIMUM_WIDTH, ScenarioInfoPanel.BASE_MINIMUM_HEIGHT * 3));
}

/**
* @return the selected preset, or null if the dialog was cancelled or no preset was selected
*/
public @Nullable String getSelectedScenarioFilename() {
if (scenarioFileName != null) {
return scenarioFileName;
}
Component selectedTab = tabbedPane.getSelectedComponent();
if (!(selectedTab instanceof ScenarioInfoPane) || !getResult().isConfirmed()) {
return null;
} else {
return ((ScenarioInfoPane) selectedTab).getSelectedPreset().getFileName();
}
}

@Override
protected Container createCenterPane() {
for (String directory : sortedScenarios.keySet()) {
if (!sortedScenarios.get(directory).isEmpty()) {
ScenarioInfoPane pane = new ScenarioInfoPane(sortedScenarios.get(directory));
tabbedPane.addTab(directory.isBlank() ? "Basic" : directory, pane);
}
}
return tabbedPane;
}

@Override
protected JPanel createButtonPanel() {
JButton cancelButton = new ButtonEsc(new CloseAction(this));
JButton okButton = new DialogButton(Messages.getString("Ok.text"));
okButton.addActionListener(this::okButtonActionPerformed);

ClickableLabel chooseFileLabel = new ClickableLabel(this::selectFromFile);
chooseFileLabel.setText("Select from File...");

JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.LINE_AXIS));
buttonPanel.setBorder(BorderFactory.createCompoundBorder(
new MatteBorder(1, 0, 0, 0, UIManager.getColor("Separator.foreground")),
new UIUtil.ScaledEmptyBorder(10, 0, 10, 0)));

Box verticalBox = Box.createVerticalBox();
verticalBox.add(Box.createVerticalGlue());
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
panel.add(chooseFileLabel);
panel.add(Box.createHorizontalGlue());
verticalBox.add(panel);
verticalBox.add(Box.createVerticalGlue());

JPanel rightButtonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 20, 0));
rightButtonPanel.add(okButton);
rightButtonPanel.add(cancelButton);
getRootPane().setDefaultButton(okButton);

buttonPanel.add(Box.createHorizontalStrut(20));
buttonPanel.add(verticalBox);
buttonPanel.add(rightButtonPanel);
return buttonPanel;
}

private static List<ScenarioInfo> getScenarioInfos() {
List<ScenarioInfo> scenarios = new ArrayList<>(parseScenariosInDirectory(Configuration.scenariosDir()));

String userDir = PreferenceManager.getClientPreferences().getUserDir();
if (!userDir.isBlank()) {
File subDir = new File(userDir, Configuration.scenariosDir().toString());
parseScenariosInDirectory(subDir);
}
return scenarios;
}

/**
* Searches the provided directory and all subdirectories for scenario files and returns a list of
* them.
*
* @param directory the directory to parse
* @return a List of scenarios
*/
private static List<ScenarioInfo> parseScenariosInDirectory(final File directory) {
LogManager.getLogger().info("Parsing scenarios from " + directory);
List<ScenarioInfo> scenarios = new ArrayList<>();
for (String scenarioFile : CommonSettingsDialog.filteredFilesWithSubDirs(directory, MMConstants.SCENARIO_EXT)) {
try {
ScenarioInfo scenario = new ScenarioLoader(new File(scenarioFile)).load();
scenarios.add(scenario);
} catch (Exception ex) {
LogManager.getLogger().error("Failed to parse scenario " + scenarioFile, ex);
}
}
return scenarios;
}

/** Groups the given scenarios by the first subdirectory under scenarios they're in (disregards any deeper dirs) */
private Map<String, List<ScenarioInfo>> sortScenarios(List<ScenarioInfo> scenarioInfos) {
return scenarioInfos.stream().collect(Collectors.groupingBy(this::getSubDirectory, Collectors.toList()));
}

@Override
public void setVisible(boolean b) {
if (b) {
UIUtil.adjustDialog(this, UIUtil.FONT_SCALE1);
}
super.setVisible(b);
}

private void selectFromFile(MouseEvent event) {
JFileChooser fc = new JFileChooser(Configuration.scenariosDir());
fc.setFileFilter(new FileNameExtensionFilter("Scenario files", "mms"));
fc.setDialogTitle(Messages.getString("MegaMek.SelectScenarioDialog.title"));
int returnVal = fc.showOpenDialog(this);
if ((returnVal == JFileChooser.APPROVE_OPTION) && (fc.getSelectedFile() != null)) {
scenarioFileName = fc.getSelectedFile().toString();
okButtonActionPerformed(null);
}
}

/**
* @return The first subdirectory under the scenarios directory that the scenario is in; a scenario
* in scenarios/tukkayid/secondencounter/ would return "tukkayid". This is used for grouping
*/
private String getSubDirectory(ScenarioInfo scenarioInfo) {
String scenariosDir = Configuration.scenariosDir().toString();
if (!scenarioInfo.getFileName().contains(scenariosDir)) {
return "";
} else {
return subDirUnderScenarios(directoriesAsList(scenarioInfo.getFileName()));
}
}

/** @return The directories (and filename) present in the given full fileName as a list. */
private static List<String> directoriesAsList(String fileName) {
Path path = Paths.get(fileName);
List<String> result = new ArrayList<>();
for (int i = 0; i < path.getNameCount(); i++) {
result.add(path.getName(i).toString());
}
return result;
}

private String subDirUnderScenarios(List<String> directoryList) {
if (!directoryList.contains("scenarios")) {
return "";
} else {
int index = directoryList.indexOf("scenarios");
// The next entry must not be the last, as the last entry is the scenario filename
return (index + 2 < directoryList.size()) ? directoryList.get(index + 1) : "";
}
}
}
60 changes: 60 additions & 0 deletions megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPane.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
*
* This file is part of MegaMek.
*
* MegaMek is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MegaMek is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MegaMek. If not, see <http://www.gnu.org/licenses/>.
*/
package megamek.client.ui.swing.scenario;

import megamek.common.annotations.Nullable;
import megamek.common.scenario.ScenarioInfo;

import javax.swing.*;
import java.util.ArrayList;
import java.util.List;

/**
* This panel displays a scrollable vertical list of ScenarioInfo panels.
*/
class ScenarioInfoPane extends JScrollPane {

private JList<ScenarioInfo> presets;
private final List<ScenarioInfo> scenarioInfoList = new ArrayList<>();

public ScenarioInfoPane(List<ScenarioInfo> scenarioInfoList) {
this.scenarioInfoList.addAll(scenarioInfoList);
setBorder(null);
getVerticalScrollBar().setUnitIncrement(16);
initialize();
}

public @Nullable ScenarioInfo getSelectedPreset() {
return presets.getSelectedValue();
}

protected void initialize() {
final DefaultListModel<ScenarioInfo> listModel = new DefaultListModel<>();
listModel.addAll(scenarioInfoList);
presets = new JList<>(listModel);
presets.setName("ScenarioInfoList");
presets.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
presets.setLayoutOrientation(JList.VERTICAL);
presets.setCellRenderer(new ScenarioInfoRenderer());

var panel = new JPanel();
panel.add(presets);
setViewportView(panel);
}
}
Loading
Loading