From c23cdb0f4134019970d80af2caf266f09884a835 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 27 Feb 2024 18:48:43 +0100 Subject: [PATCH 1/4] Add a scenario chooser to replace file choosing --- .../NWH2_InstantFame.mms | 5 +- .../i18n/megamek/client/messages.properties | 3 + .../scenario}/messages.properties | 0 .../scenario}/messages_en.properties | 0 .../scenario}/messages_es.properties | 0 megamek/src/megamek/SuiteConstants.java | 1 + .../client/ui/swing/CommonSettingsDialog.java | 4 + .../megamek/client/ui/swing/MegaMekGUI.java | 46 +---- .../ui/swing/scenario/ScenarioChooser.java | 184 ++++++++++++++++++ .../ui/swing/scenario/ScenarioInfoPane.java | 60 ++++++ .../ui/swing/scenario/ScenarioInfoPanel.java | 89 +++++++++ .../swing/scenario/ScenarioInfoRenderer.java | 45 +++++ .../client/ui/swing/util/ClickableLabel.java | 87 +++++++++ .../client/ui/swing/util/DashedSeparator.java | 102 ++++++++++ .../client/ui/swing/util/LocationBorder.java | 98 ++++++++++ .../megamek/client/ui/swing/util/UIUtil.java | 6 +- .../common/scenario/ScenarioFullInfo.java | 112 +++++++++++ .../scenario}/ScenarioLoader.java | 98 ++++------ .../src/megamek/test/ScenarioLoaderTest.java | 2 +- 19 files changed, 833 insertions(+), 109 deletions(-) rename megamek/i18n/megamek/{server => client/scenario}/messages.properties (100%) rename megamek/i18n/megamek/{server => client/scenario}/messages_en.properties (100%) rename megamek/i18n/megamek/{server => client/scenario}/messages_es.properties (100%) create mode 100644 megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java create mode 100644 megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPane.java create mode 100644 megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java create mode 100644 megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoRenderer.java create mode 100644 megamek/src/megamek/client/ui/swing/util/ClickableLabel.java create mode 100644 megamek/src/megamek/client/ui/swing/util/DashedSeparator.java create mode 100644 megamek/src/megamek/client/ui/swing/util/LocationBorder.java create mode 100644 megamek/src/megamek/common/scenario/ScenarioFullInfo.java rename megamek/src/megamek/{server => common/scenario}/ScenarioLoader.java (94%) diff --git a/megamek/data/scenarios/Northwind Highlanders/NWH2_InstantFame.mms b/megamek/data/scenarios/Northwind Highlanders/NWH2_InstantFame.mms index 285fce54bbd..029040a971e 100644 --- a/megamek/data/scenarios/Northwind Highlanders/NWH2_InstantFame.mms +++ b/megamek/data/scenarios/Northwind Highlanders/NWH2_InstantFame.mms @@ -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 diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index c1e64a58f6b..048cb5f0c12 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -4539,3 +4539,6 @@ CMVPanel.font=Font: Gamemaster.Gamemaster=Gamemaster Gamemaster.EditDamage=Edit Damage Gamemaster.Configure=Configure + +# Scenario Chooser +ScenarioChooser.title=Choose Scenario \ No newline at end of file diff --git a/megamek/i18n/megamek/server/messages.properties b/megamek/i18n/megamek/client/scenario/messages.properties similarity index 100% rename from megamek/i18n/megamek/server/messages.properties rename to megamek/i18n/megamek/client/scenario/messages.properties diff --git a/megamek/i18n/megamek/server/messages_en.properties b/megamek/i18n/megamek/client/scenario/messages_en.properties similarity index 100% rename from megamek/i18n/megamek/server/messages_en.properties rename to megamek/i18n/megamek/client/scenario/messages_en.properties diff --git a/megamek/i18n/megamek/server/messages_es.properties b/megamek/i18n/megamek/client/scenario/messages_es.properties similarity index 100% rename from megamek/i18n/megamek/server/messages_es.properties rename to megamek/i18n/megamek/client/scenario/messages_es.properties diff --git a/megamek/src/megamek/SuiteConstants.java b/megamek/src/megamek/SuiteConstants.java index f04c01b693f..cafceb57769 100644 --- a/megamek/src/megamek/SuiteConstants.java +++ b/megamek/src/megamek/SuiteConstants.java @@ -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 diff --git a/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java b/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java index 131da058a40..43993cd291f 100644 --- a/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java +++ b/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java @@ -3109,6 +3109,10 @@ public static List filteredFiles(File path, String fileEnding) { } public static List filteredFilesWithSubDirs(File path, String fileEnding) { + if (!path.exists()) { + LogManager.getLogger().warn("Path " + path + " does not exist."); + return new ArrayList<>(); + } try (Stream entries = Files.walk(path.toPath())) { return entries.map(Objects::toString).filter(name -> name.endsWith(fileEnding)).collect(toList()); } catch (IOException e) { diff --git a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java index 64e5383db87..9d9113b5fe1 100644 --- a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java +++ b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java @@ -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; @@ -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; @@ -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(); diff --git a/megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java b/megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java new file mode 100644 index 00000000000..54b8ba6921d --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java @@ -0,0 +1,184 @@ +/* + * 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 . + */ +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.ScenarioFullInfo; +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.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 scenarions 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 List scenarioInfoList = getScenarioInfos(); + private final Map> sortedScenarios = sortScenarios(scenarioInfoList); + 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() { + ScenarioInfoPane basicPane = new ScenarioInfoPane(sortedScenarios.get("")); + tabbedPane.addTab("Basic", basicPane); + + for (String directory : sortedScenarios.keySet()) { + if (!sortedScenarios.get(directory).isEmpty()) { + ScenarioInfoPane pane = new ScenarioInfoPane(sortedScenarios.get(directory)); + tabbedPane.addTab(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)); + 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 getScenarioInfos() { + List 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 and registers any truetype + * fonts from .ttf files it finds. + * + * @param directory the directory to parse + */ + public static List parseScenariosInDirectory(final File directory) { + LogManager.getLogger().info("Parsing scenarios from " + directory); + List scenarios = new ArrayList<>(); + for (String scenarioFile : CommonSettingsDialog.filteredFilesWithSubDirs(directory, MMConstants.SCENARIO_EXT)) { + try { + ScenarioFullInfo scenario = new ScenarioLoader(new File(scenarioFile)).load(); + scenarios.add(scenario); + } catch (Exception ex) { + LogManager.getLogger().error("Failed to parse scenario " + scenarioFile, ex); + } + } + return scenarios; + } + + private Map> sortScenarios(List scenarioInfos) { + return scenarioInfos.stream().collect(Collectors.groupingBy(ScenarioFullInfo::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); + } + } +} \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPane.java b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPane.java new file mode 100644 index 00000000000..86f9d029a0c --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPane.java @@ -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 . + */ +package megamek.client.ui.swing.scenario; + +import megamek.common.annotations.Nullable; +import megamek.common.scenario.ScenarioFullInfo; + +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 presets; + private final List scenarioInfoList = new ArrayList<>(); + + public ScenarioInfoPane(List scenarioInfoList) { + this.scenarioInfoList.addAll(scenarioInfoList); + setBorder(null); + getVerticalScrollBar().setUnitIncrement(16); + initialize(); + } + + public @Nullable ScenarioFullInfo getSelectedPreset() { + return presets.getSelectedValue(); + } + + protected void initialize() { + final DefaultListModel 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); + } +} \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java new file mode 100644 index 00000000000..12f380d6216 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java @@ -0,0 +1,89 @@ +/* + * 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 . + */ +package megamek.client.ui.swing.scenario; + +import megamek.client.ui.swing.util.DashedSeparator; +import megamek.client.ui.swing.util.LocationBorder; +import megamek.client.ui.swing.util.UIUtil; +import megamek.common.scenario.ScenarioFullInfo; + +import javax.swing.*; +import java.awt.*; + +/** + * This panel displays a single {@link ScenarioFullInfo} object in a well-formatted manner for display in the + * {@link ScenarioChooser}. + */ +public class ScenarioInfoPanel extends JPanel { + + static final int BASE_MINIMUM_WIDTH = 600; + static final int BASE_MINIMUM_HEIGHT = 100; + + private final JLabel lblTitle = new HeaderLabel(); + private final DescriptionLabel textDescription2 = new DescriptionLabel(); + + public ScenarioInfoPanel() { + setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(10, 5, 10, 5), + new LocationBorder(UIManager.getColor("Separator.foreground"), 3))); + setName("ScenarioInfoPanel"); + setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); + + lblTitle.setName("lblTitle"); + lblTitle.setAlignmentX(Component.CENTER_ALIGNMENT); + lblTitle.setForeground(UIUtil.uiLightGreen()); + lblTitle.setFont(new Font("Exo 2", Font.BOLD, UIUtil.FONT_SCALE1)); + add(lblTitle); + add(UIUtil.scaledVerticalSpacer(10)); + add(new DashedSeparator(UIUtil.uiLightGreen(), 0.9f, 2f)); + add(UIUtil.scaledVerticalSpacer(10)); + + textDescription2.setFont(new Font("Noto Sans", Font.PLAIN, UIUtil.FONT_SCALE1)); + textDescription2.setAlignmentX(0.5f); + textDescription2.setVerticalAlignment(SwingConstants.TOP); + textDescription2.setBorder(new UIUtil.ScaledEmptyBorder(0, 10, 0, 10)); + add(textDescription2); + } + + protected void updateFromPreset(final ScenarioFullInfo preset) { + lblTitle.setText(preset.getName()); + textDescription2.setText("" + preset.getDescription()); + } + + private static class DescriptionLabel extends JLabel { + @Override + public Dimension getMaximumSize() { + return UIUtil.scaleForGUI(BASE_MINIMUM_WIDTH, BASE_MINIMUM_HEIGHT); + } + + @Override + public Dimension getPreferredSize() { + return UIUtil.scaleForGUI(BASE_MINIMUM_WIDTH, BASE_MINIMUM_HEIGHT); + } + } + + private static class HeaderLabel extends JLabel { + @Override + public void setFont(Font font) { + // Keep a higher font size; UIUtil.adjustDialog sets everything to the same absolute font size + font = font.deriveFont(1.2f * font.getSize()); + super.setFont(font); + } + } +} \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoRenderer.java b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoRenderer.java new file mode 100644 index 00000000000..9219b105e10 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoRenderer.java @@ -0,0 +1,45 @@ +/* + * 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 . + */ +package megamek.client.ui.swing.scenario; + +import megamek.common.scenario.ScenarioFullInfo; + +import javax.swing.*; +import java.awt.*; + +/** + * This is a list renderer for {@link ScenarioInfoPanel}. + */ +public class ScenarioInfoRenderer extends ScenarioInfoPanel implements ListCellRenderer { + + @Override + public Component getListCellRendererComponent(final JList list, + final ScenarioFullInfo value, final int index, + final boolean isSelected, + final boolean cellHasFocus) { + final Color foreground = new Color((isSelected + ? list.getSelectionForeground() : list.getForeground()).getRGB()); + final Color background = new Color((isSelected + ? list.getSelectionBackground() : list.getBackground()).getRGB()); + setForeground(foreground); + setBackground(background); + updateFromPreset(value); + return this; + } +} \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/util/ClickableLabel.java b/megamek/src/megamek/client/ui/swing/util/ClickableLabel.java new file mode 100644 index 00000000000..5d1b8887523 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/util/ClickableLabel.java @@ -0,0 +1,87 @@ +/* + * 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 . + */ +package megamek.client.ui.swing.util; + +import javax.swing.*; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * This is a specialized label that acts as a replacement for a button. It underlines its text when hovered + * like a hyperlink and so indicates that it is clickable. Note that the label always uses HTML to underline + * its text internally and so does not need an additional HTML tag to show HTML content. + * This can be used in circumstances where the button's function is less important or a fallback and the + * label is used to call less attention to it than a button does. + * Another use is when a text info label should have the button functionality only as a secondary function. + * The label calls a callback method when it is clicked. + */ +public class ClickableLabel extends JLabel implements MouseListener { + + private static final String HOVERED_PREFIX = ""; + private static final String NON_HOVERED_PREFIX = ""; + private boolean isHovered = false; + private String baseText = ""; + private final Consumer clickCallback; + + /** + * Creates a new clickable label. The given callback method is called when the + * label is mouse-clicked. + * + * @param clickCallback The method to call when the label is clicked + */ + public ClickableLabel(Consumer clickCallback) { + this.clickCallback = Objects.requireNonNull(clickCallback); + addMouseListener(this); + } + + @Override + public void setText(String text) { + baseText = text; + updateText(); + } + + private void updateText() { + super.setText((isHovered ? HOVERED_PREFIX : NON_HOVERED_PREFIX) + baseText); + } + + @Override + public void mouseClicked(MouseEvent e) { + clickCallback.accept(e); + } + + @Override + public void mousePressed(MouseEvent e) { } + + @Override + public void mouseReleased(MouseEvent e) { } + + @Override + public void mouseEntered(MouseEvent e) { + isHovered = true; + updateText(); + } + + @Override + public void mouseExited(MouseEvent e) { + isHovered = false; + updateText(); + } +} \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/util/DashedSeparator.java b/megamek/src/megamek/client/ui/swing/util/DashedSeparator.java new file mode 100644 index 00000000000..102563bbf0b --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/util/DashedSeparator.java @@ -0,0 +1,102 @@ +/* + * 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 . + */ +package megamek.client.ui.swing.util; + +import javax.swing.*; +import java.awt.*; + +/** + * This component draws a dashed horizontal line, similarly to a JSeparator. It's color and line strength (stroke + * width) can be set. Also, its width (the amount of available horizontal space it covers) can be set. + */ +public class DashedSeparator extends JComponent { + + private final Color color; + private final float relativeWidth; + private final float strokeWidth; + + /** + * Creates a dashed separator that uses the UI LaF's separator color and a line width of 1. It uses the + * available horizontal space. + */ + public DashedSeparator() { + this(UIManager.getColor("Separator.foreground")); + } + + /** + * Creates a dashed separator of the given color and a line width of 1. It uses the + * available horizontal space. + */ + public DashedSeparator(Color color) { + this(color, 1, 1); + } + + /** + * Creates a dashed separator that uses the UI LaF's separator color and the given values for the + * stroke width (line thickness) and the relative horizontal length. The values for relativeWidth + * should be between 0 and 1; 1 meaning the line extends for all the horizontal width; 0.5 meaning + * only half the horizontal space is used (the line is centered). + */ + public DashedSeparator(float relativeWidth, float strokeWidth) { + this(UIManager.getColor("Separator.foreground"), relativeWidth, strokeWidth); + } + + /** + * Creates a dashed separator of the given color and the given values for the + * stroke width (line thickness) and the relative horizontal length. The values for relativeWidth + * should be between 0 and 1; 1 meaning the line extends for all the horizontal width; 0.5 meaning + * only half the horizontal space is used (the line is centered). + */ + public DashedSeparator(Color color, float relativeWidth, float strokeWidth) { + this.color = color; + this.relativeWidth = relativeWidth; + this.strokeWidth = strokeWidth; + } + + @Override + public Dimension getMaximumSize() { + return new Dimension(super.getMaximumSize().width, (int) (strokeWidth + 2)); + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(super.getPreferredSize().width, (int) (strokeWidth + 2)); + } + + @Override + public Dimension getMinimumSize() { + return new Dimension(super.getMinimumSize().width, (int) (strokeWidth + 2)); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g.create(); + try { + Stroke dashed = new BasicStroke(strokeWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, + 0, new float[]{9}, 0); + g2.setStroke(dashed); + g2.setColor(color); + g2.drawLine((int) (getWidth() * (1 - relativeWidth) / 2), 0, + (int) (getWidth() * (1 + relativeWidth) / 2), 0); + } finally { + g2.dispose(); + } + } +} \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/util/LocationBorder.java b/megamek/src/megamek/client/ui/swing/util/LocationBorder.java new file mode 100644 index 00000000000..51e599dd27c --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/util/LocationBorder.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2022 - 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 . + */ + +package megamek.client.ui.swing.util; + +import javax.swing.border.AbstractBorder; +import java.awt.*; +import java.awt.geom.Path2D; + +public class LocationBorder extends AbstractBorder { + + private final static float CL = 40; + private final static float HCL = 10; + private final static float HCH = 5; + + /** + * Thickness of the border. + */ + protected float thickness; + + /** + * Color of the border. + */ + protected Color lineColor; + + public LocationBorder(Color color, float thickness) { + this.thickness = thickness; + lineColor = color; + } + + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + if ((thickness > 0) && (lineColor != null) && (width > 0) && (height > 0) && (g instanceof Graphics2D)) { + Graphics2D g2d = (Graphics2D) g; + + Color oldColor = g2d.getColor(); + Stroke oldStroke = g2d.getStroke(); + + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setColor(this.lineColor); + g2d.setStroke(new BasicStroke(thickness)); + + Path2D.Float line = new Path2D.Float(); + float xc = x + thickness; + float xw = x + width - thickness; + float yc = y + thickness; + float yh = y + height - thickness; + + if (width < 2 * CL) { + float hcl = 0.5f * HCL / CL * width; + line.moveTo(xc, yc + HCH); + line.curveTo(xc + hcl, yc, xc + hcl, yc, width / 2.0d, yc); + line.curveTo(xw - hcl, yc, xw - hcl, yc, xw, yc + HCH); + line.lineTo(xw, yh - HCH); + line.curveTo(xw - hcl, yh, xw - hcl, yh, width / 2.0d, yh); + line.curveTo(xc + hcl, yh, xc + hcl, yh, xc, yh - HCH); + } else { + line.moveTo(xc, yc + HCH); + line.curveTo(xc + HCL, yc, xc + HCL, yc, xc + CL, yc); + line.lineTo(xw - CL, yc); + line.curveTo(xw - HCL, yc, xw - HCL, yc, xw, yc + HCH); + line.lineTo(xw, yh - HCH); + line.curveTo(xw - HCL, yh, xw - HCL, yh, xw - CL, yh); + line.lineTo(xc + CL, yh); + line.curveTo(xc + HCL, yh, xc + HCL, yh, xc, yh - HCH); + } + line.closePath(); + g2d.draw(line); + + g2d.setStroke(oldStroke); + g2d.setColor(oldColor); + } + } + + @Override + public Insets getBorderInsets(Component c, Insets insets) { + insets.set((int) (HCH + 1.5 * thickness), (int) (3 + 1.5 * thickness), + (int) (HCH + 1.5 * thickness), (int) (3 + 1.5 * thickness)); + return insets; + } + +} diff --git a/megamek/src/megamek/client/ui/swing/util/UIUtil.java b/megamek/src/megamek/client/ui/swing/util/UIUtil.java index ff184e06366..f7cbc6a90f8 100644 --- a/megamek/src/megamek/client/ui/swing/util/UIUtil.java +++ b/megamek/src/megamek/client/ui/swing/util/UIUtil.java @@ -350,8 +350,12 @@ public static float scaleForGUI(float value) { } public static Dimension scaleForGUI(Dimension dim) { + return scaleForGUI(dim.width, dim.height); + } + + public static Dimension scaleForGUI(int width, int height) { float scale = GUIPreferences.getInstance().getGUIScale(); - return new Dimension((int) (scale * dim.width), (int) (scale * dim.height)); + return new Dimension((int) (scale * width), (int) (scale * height)); } /** diff --git a/megamek/src/megamek/common/scenario/ScenarioFullInfo.java b/megamek/src/megamek/common/scenario/ScenarioFullInfo.java new file mode 100644 index 00000000000..22624139608 --- /dev/null +++ b/megamek/src/megamek/common/scenario/ScenarioFullInfo.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2022, 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 . + */ +package megamek.common.scenario; + +import megamek.common.Configuration; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +public class ScenarioFullInfo extends HashMap> { + + + public String getName() { + return getString(ScenarioLoader.NAME); + } + + public String getDescription() { + return getString(ScenarioLoader.DESCRIPTION); + } + + public String getFileName() { + return getString(ScenarioLoader.FILENAME); + } + + public String getSubDirectory() { + String scenariosDir = Configuration.scenariosDir().toString(); + if (!getFileName().contains(scenariosDir)) { + return ""; + } else { + return subDirUnderScenarios(directoriesAsList(getFileName())); + } + } + + public static List directoriesAsList(String fileName) { + Path path = Paths.get(fileName); + List result = new ArrayList<>(); + for (int i = 0; i < path.getNameCount(); i++) { + result.add(path.getName(i).toString()); + } + return result; + } + + private String subDirUnderScenarios(List 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) : ""; + } + } + + public void put(String key, String value) { + Collection values = get(key); + if (values == null) { + values = new ArrayList<>(); + put(key, values); + } + values.add(value); + } + + public String getString(String key) { + return getString(key, ScenarioLoader.SEPARATOR_COMMA); + } + + public String getString(String key, String separator) { + Collection values = get(key); + if ((values == null) || values.isEmpty()) { + return null; + } + + boolean firstElement = true; + StringBuilder sb = new StringBuilder(); + for (String val : values) { + if (firstElement) { + firstElement = false; + } else { + sb.append(separator); + } + sb.append(val); + } + return sb.toString(); + } + + /** + * @return the number of values for this key in the file + */ + public int getNumValues(String key) { + Collection values = get(key); + return (values == null) ? 0 : values.size(); + } +} diff --git a/megamek/src/megamek/server/ScenarioLoader.java b/megamek/src/megamek/common/scenario/ScenarioLoader.java similarity index 94% rename from megamek/src/megamek/server/ScenarioLoader.java rename to megamek/src/megamek/common/scenario/ScenarioLoader.java index f6da2e63ed0..949329e913e 100644 --- a/megamek/src/megamek/server/ScenarioLoader.java +++ b/megamek/src/megamek/common/scenario/ScenarioLoader.java @@ -1,19 +1,22 @@ /* - * MegaMek - Copyright (C) 2003, 2004, 2005 Ben Mazur (bmazur@sev.org) - * ScenarioLoader - Copyright (C) 2002 Josh Yockey - * Copyright © 2013 Edward Cullen (eddy@obsessedcomputers.co.uk) + * Copyright (c) 2022 - The MegaMek Team. All Rights Reserved. * - * This program 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 2 of the License, or (at your option) - * any later version. + * This file is part of MegaMek. * - * This program 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. + * 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 . */ -package megamek.server; +package megamek.common.scenario; import megamek.client.generator.RandomGenderGenerator; import megamek.common.*; @@ -26,6 +29,8 @@ import megamek.common.options.OptionsConstants; import megamek.common.util.BoardUtilities; import megamek.common.util.fileUtils.MegaMekFile; +import megamek.server.GameManager; +import megamek.server.Messages; import org.apache.logging.log4j.LogManager; import java.io.*; @@ -37,8 +42,12 @@ public class ScenarioLoader { private static final String COMMENT_MARK = "#"; + protected static final String NAME = "Name"; + protected static final String DESCRIPTION = "Description"; + protected static final String FILENAME = "FileName"; + private static final String SEPARATOR_PROPERTY = "="; - private static final String SEPARATOR_COMMA = ","; + static final String SEPARATOR_COMMA = ","; private static final String SEPARATOR_SPACE = " "; private static final String SEPARATOR_COLON = ":"; private static final String SEPARATOR_UNDERSCORE = "_"; @@ -345,7 +354,7 @@ else if (chp.entity instanceof Tank) { public Game createGame() throws Exception { LogManager.getLogger().info("Loading scenario from " + scenarioFile); - StringMultiMap p = load(); + ScenarioFullInfo p = load(); String sCheck = p.getString(PARAM_MMSVERSION); if (sCheck == null) { @@ -412,7 +421,7 @@ public Game createGame() throws Exception { return g; } - private void parsePlanetaryConditions(Game g, StringMultiMap p) { + private void parsePlanetaryConditions(Game g, ScenarioFullInfo p) { if (p.containsKey(PARAM_PLANETCOND_TEMP)) { g.getPlanetaryConditions().setTemperature(Integer.parseInt(p.getString(PARAM_PLANETCOND_TEMP))); } @@ -474,7 +483,7 @@ private void parsePlanetaryConditions(Game g, StringMultiMap p) { } } - private Collection buildFactionEntities(StringMultiMap p, Player player) throws ScenarioLoaderException { + private Collection buildFactionEntities(ScenarioFullInfo p, Player player) throws ScenarioLoaderException { String faction = player.getName(); Pattern unitPattern = Pattern.compile(String.format("^Unit_\\Q%s\\E_[^_]+$", faction)); Pattern unitDataPattern = Pattern.compile(String.format("^(Unit_\\Q%s\\E_[^_]+)_([A-Z][^_]+)$", faction)); @@ -713,7 +722,7 @@ private String getFactionParam(String faction, String param) { return param + SEPARATOR_UNDERSCORE + faction; } - private Collection createPlayers(StringMultiMap p) throws ScenarioLoaderException { + private Collection createPlayers(ScenarioFullInfo p) throws ScenarioLoaderException { String sFactions = p.getString(PARAM_FACTIONS); if ((sFactions == null) || sFactions.isEmpty()) { throw new ScenarioLoaderException("missingFactions"); @@ -780,7 +789,7 @@ private Collection createPlayers(StringMultiMap p) throws ScenarioLoader /** * Load board files and create the megaboard. */ - private Board createBoard(StringMultiMap p) throws ScenarioLoaderException { + private Board createBoard(ScenarioFullInfo p) throws ScenarioLoaderException { int mapWidth = 16, mapHeight = 17; if (p.getString(PARAM_MAP_WIDTH) == null) { LogManager.getLogger().info("No map width specified; using " + mapWidth); @@ -888,8 +897,9 @@ private Board createBoard(StringMultiMap p) throws ScenarioLoaderException { return BoardUtilities.combine(mapWidth, mapHeight, nWidth, nHeight, ba, rotateBoard, MapSettings.MEDIUM_GROUND); } - private StringMultiMap load() throws ScenarioLoaderException { - StringMultiMap props = new StringMultiMap(); + public ScenarioFullInfo load() throws ScenarioLoaderException { + ScenarioFullInfo props = new ScenarioFullInfo(); + props.put(FILENAME, List.of(scenarioFile.toString())); try (FileInputStream fis = new FileInputStream(scenarioFile); InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8); BufferedReader br = new BufferedReader(isr)) { @@ -923,7 +933,7 @@ private StringMultiMap load() throws ScenarioLoaderException { /** * Parses out the external game id from the scenario file */ - private int parseExternalGameId(StringMultiMap p) { + private int parseExternalGameId(ScenarioFullInfo p) { String sExternalId = p.getString(PARAM_GAME_EXTERNAL_ID); int ExternalGameId = 0; if (sExternalId != null) { @@ -949,7 +959,7 @@ public boolean isSinglePlayer() { * defaultValue. When the key is present, interprets "true" and "on" and "1" * as true and everything else as false. */ - private boolean parseBoolean(StringMultiMap p, String key, boolean defaultValue) { + private boolean parseBoolean(ScenarioFullInfo p, String key, boolean defaultValue) { boolean result = defaultValue; if (p.containsKey(key)) { if (p.getString(key).equalsIgnoreCase("true") @@ -1127,7 +1137,7 @@ public void addSpecificDamage(String s) { } } - private static class ScenarioLoaderException extends Exception { + public static class ScenarioLoaderException extends Exception { private static final long serialVersionUID = 8622648319531348199L; private final Object[] params; @@ -1156,46 +1166,4 @@ public String getMessage() { } } - private static class StringMultiMap extends HashMap> { - private static final long serialVersionUID = 2171662843329151622L; - - public void put(String key, String value) { - Collection values = get(key); - if (values == null) { - values = new ArrayList<>(); - put(key, values); - } - values.add(value); - } - - public String getString(String key) { - return getString(key, SEPARATOR_COMMA); - } - - public String getString(String key, String separator) { - Collection values = get(key); - if ((values == null) || values.isEmpty()) { - return null; - } - - boolean firstElement = true; - StringBuilder sb = new StringBuilder(); - for (String val : values) { - if (firstElement) { - firstElement = false; - } else { - sb.append(separator); - } - sb.append(val); - } - return sb.toString(); - } - - /** @return the number of values for this key in the file */ - public int getNumValues(String key) { - Collection values = get(key); - return (values == null) ? 0 : values.size(); - } - } - } diff --git a/megamek/src/megamek/test/ScenarioLoaderTest.java b/megamek/src/megamek/test/ScenarioLoaderTest.java index d8d7dd7f7c3..48ba3f6e1bf 100644 --- a/megamek/src/megamek/test/ScenarioLoaderTest.java +++ b/megamek/src/megamek/test/ScenarioLoaderTest.java @@ -11,7 +11,7 @@ import megamek.common.Game; import megamek.common.MechSummaryCache; import megamek.server.GameManager; -import megamek.server.ScenarioLoader; +import megamek.common.scenario.ScenarioLoader; import megamek.server.Server; public class ScenarioLoaderTest { From f023f424d4d3291c4561830ed942660d7471c7a5 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 28 Feb 2024 11:28:04 +0100 Subject: [PATCH 2/4] Increase header font size --- .../src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java index 12f380d6216..9faa17f6d2a 100644 --- a/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java +++ b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java @@ -82,7 +82,7 @@ private static class HeaderLabel extends JLabel { @Override public void setFont(Font font) { // Keep a higher font size; UIUtil.adjustDialog sets everything to the same absolute font size - font = font.deriveFont(1.2f * font.getSize()); + font = font.deriveFont(1.4f * font.getSize()); super.setFont(font); } } From d9db0ee45a7ffcacff0d3ca65370e8a39b93d667 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 28 Feb 2024 14:47:12 +0100 Subject: [PATCH 3/4] Comments and misc changes --- .../ui/swing/scenario/ScenarioChooser.java | 67 ++++++++++++++----- .../ui/swing/scenario/ScenarioInfoPane.java | 12 ++-- .../ui/swing/scenario/ScenarioInfoPanel.java | 6 +- .../swing/scenario/ScenarioInfoRenderer.java | 10 +-- .../client/ui/swing/util/ClickableLabel.java | 2 +- .../client/ui/swing/util/DashedSeparator.java | 2 +- ...cenarioFullInfo.java => ScenarioInfo.java} | 51 +++++--------- .../common/scenario/ScenarioLoader.java | 18 ++--- 8 files changed, 90 insertions(+), 78 deletions(-) rename megamek/src/megamek/common/scenario/{ScenarioFullInfo.java => ScenarioInfo.java} (64%) diff --git a/megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java b/megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java index 54b8ba6921d..a3603a08a08 100644 --- a/megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java +++ b/megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java @@ -30,7 +30,7 @@ import megamek.common.Configuration; import megamek.common.annotations.Nullable; import megamek.common.preference.PreferenceManager; -import megamek.common.scenario.ScenarioFullInfo; +import megamek.common.scenario.ScenarioInfo; import megamek.common.scenario.ScenarioLoader; import org.apache.logging.log4j.LogManager; @@ -40,6 +40,8 @@ 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; @@ -47,7 +49,7 @@ /** * This dialog lists all scenarios found in MM's scenario directory as well as the corresponding user directory. - * The scenarions are grouped by subdirectory (only the one directly below scenarios, further subdirectories + * 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. @@ -55,8 +57,7 @@ public class ScenarioChooser extends AbstractButtonDialog { private final JTabbedPane tabbedPane = new JTabbedPane(); - private final List scenarioInfoList = getScenarioInfos(); - private final Map> sortedScenarios = sortScenarios(scenarioInfoList); + private final Map> sortedScenarios = sortScenarios(getScenarioInfos()); private String scenarioFileName; public ScenarioChooser(final JFrame parentFrame) { @@ -82,13 +83,10 @@ public ScenarioChooser(final JFrame parentFrame) { @Override protected Container createCenterPane() { - ScenarioInfoPane basicPane = new ScenarioInfoPane(sortedScenarios.get("")); - tabbedPane.addTab("Basic", basicPane); - for (String directory : sortedScenarios.keySet()) { if (!sortedScenarios.get(directory).isEmpty()) { ScenarioInfoPane pane = new ScenarioInfoPane(sortedScenarios.get(directory)); - tabbedPane.addTab(directory, pane); + tabbedPane.addTab(directory.isBlank() ? "Basic" : directory, pane); } } return tabbedPane; @@ -128,8 +126,8 @@ protected JPanel createButtonPanel() { return buttonPanel; } - private static List getScenarioInfos() { - List scenarios = new ArrayList<>(parseScenariosInDirectory(Configuration.scenariosDir())); + private static List getScenarioInfos() { + List scenarios = new ArrayList<>(parseScenariosInDirectory(Configuration.scenariosDir())); String userDir = PreferenceManager.getClientPreferences().getUserDir(); if (!userDir.isBlank()) { @@ -140,17 +138,18 @@ private static List getScenarioInfos() { } /** - * Searches the provided directory and all subdirectories and registers any truetype - * fonts from .ttf files it finds. + * 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 */ - public static List parseScenariosInDirectory(final File directory) { + private static List parseScenariosInDirectory(final File directory) { LogManager.getLogger().info("Parsing scenarios from " + directory); - List scenarios = new ArrayList<>(); + List scenarios = new ArrayList<>(); for (String scenarioFile : CommonSettingsDialog.filteredFilesWithSubDirs(directory, MMConstants.SCENARIO_EXT)) { try { - ScenarioFullInfo scenario = new ScenarioLoader(new File(scenarioFile)).load(); + ScenarioInfo scenario = new ScenarioLoader(new File(scenarioFile)).load(); scenarios.add(scenario); } catch (Exception ex) { LogManager.getLogger().error("Failed to parse scenario " + scenarioFile, ex); @@ -159,8 +158,9 @@ public static List parseScenariosInDirectory(final File direct return scenarios; } - private Map> sortScenarios(List scenarioInfos) { - return scenarioInfos.stream().collect(Collectors.groupingBy(ScenarioFullInfo::getSubDirectory, Collectors.toList())); + /** Groups the given scenarios by the first subdirectory under scenarios they're in (disregards any deeper dirs) */ + private Map> sortScenarios(List scenarioInfos) { + return scenarioInfos.stream().collect(Collectors.groupingBy(this::getSubDirectory, Collectors.toList())); } @Override @@ -181,4 +181,37 @@ private void selectFromFile(MouseEvent event) { 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 directoriesAsList(String fileName) { + Path path = Paths.get(fileName); + List result = new ArrayList<>(); + for (int i = 0; i < path.getNameCount(); i++) { + result.add(path.getName(i).toString()); + } + return result; + } + + private String subDirUnderScenarios(List 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) : ""; + } + } } \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPane.java b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPane.java index 86f9d029a0c..b32447c1afd 100644 --- a/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPane.java +++ b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPane.java @@ -19,7 +19,7 @@ package megamek.client.ui.swing.scenario; import megamek.common.annotations.Nullable; -import megamek.common.scenario.ScenarioFullInfo; +import megamek.common.scenario.ScenarioInfo; import javax.swing.*; import java.util.ArrayList; @@ -30,22 +30,22 @@ */ class ScenarioInfoPane extends JScrollPane { - private JList presets; - private final List scenarioInfoList = new ArrayList<>(); + private JList presets; + private final List scenarioInfoList = new ArrayList<>(); - public ScenarioInfoPane(List scenarioInfoList) { + public ScenarioInfoPane(List scenarioInfoList) { this.scenarioInfoList.addAll(scenarioInfoList); setBorder(null); getVerticalScrollBar().setUnitIncrement(16); initialize(); } - public @Nullable ScenarioFullInfo getSelectedPreset() { + public @Nullable ScenarioInfo getSelectedPreset() { return presets.getSelectedValue(); } protected void initialize() { - final DefaultListModel listModel = new DefaultListModel<>(); + final DefaultListModel listModel = new DefaultListModel<>(); listModel.addAll(scenarioInfoList); presets = new JList<>(listModel); presets.setName("ScenarioInfoList"); diff --git a/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java index 9faa17f6d2a..2c94dd049bd 100644 --- a/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java +++ b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPanel.java @@ -21,13 +21,13 @@ import megamek.client.ui.swing.util.DashedSeparator; import megamek.client.ui.swing.util.LocationBorder; import megamek.client.ui.swing.util.UIUtil; -import megamek.common.scenario.ScenarioFullInfo; +import megamek.common.scenario.ScenarioInfo; import javax.swing.*; import java.awt.*; /** - * This panel displays a single {@link ScenarioFullInfo} object in a well-formatted manner for display in the + * This panel displays a single {@link ScenarioInfo} object in a well-formatted manner for display in the * {@link ScenarioChooser}. */ public class ScenarioInfoPanel extends JPanel { @@ -61,7 +61,7 @@ public ScenarioInfoPanel() { add(textDescription2); } - protected void updateFromPreset(final ScenarioFullInfo preset) { + protected void updateFromPreset(final ScenarioInfo preset) { lblTitle.setText(preset.getName()); textDescription2.setText("" + preset.getDescription()); } diff --git a/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoRenderer.java b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoRenderer.java index 9219b105e10..8e285d4d79d 100644 --- a/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoRenderer.java +++ b/megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoRenderer.java @@ -18,19 +18,19 @@ */ package megamek.client.ui.swing.scenario; -import megamek.common.scenario.ScenarioFullInfo; +import megamek.common.scenario.ScenarioInfo; import javax.swing.*; import java.awt.*; /** - * This is a list renderer for {@link ScenarioInfoPanel}. + * This is a JList renderer for {@link ScenarioInfoPanel}. */ -public class ScenarioInfoRenderer extends ScenarioInfoPanel implements ListCellRenderer { +public class ScenarioInfoRenderer extends ScenarioInfoPanel implements ListCellRenderer { @Override - public Component getListCellRendererComponent(final JList list, - final ScenarioFullInfo value, final int index, + public Component getListCellRendererComponent(final JList list, + final ScenarioInfo value, final int index, final boolean isSelected, final boolean cellHasFocus) { final Color foreground = new Color((isSelected diff --git a/megamek/src/megamek/client/ui/swing/util/ClickableLabel.java b/megamek/src/megamek/client/ui/swing/util/ClickableLabel.java index 5d1b8887523..445b979c8bf 100644 --- a/megamek/src/megamek/client/ui/swing/util/ClickableLabel.java +++ b/megamek/src/megamek/client/ui/swing/util/ClickableLabel.java @@ -29,7 +29,7 @@ * like a hyperlink and so indicates that it is clickable. Note that the label always uses HTML to underline * its text internally and so does not need an additional HTML tag to show HTML content. * This can be used in circumstances where the button's function is less important or a fallback and the - * label is used to call less attention to it than a button does. + * label is used to call less attention to it than a button would. * Another use is when a text info label should have the button functionality only as a secondary function. * The label calls a callback method when it is clicked. */ diff --git a/megamek/src/megamek/client/ui/swing/util/DashedSeparator.java b/megamek/src/megamek/client/ui/swing/util/DashedSeparator.java index 102563bbf0b..2c1f86fa090 100644 --- a/megamek/src/megamek/client/ui/swing/util/DashedSeparator.java +++ b/megamek/src/megamek/client/ui/swing/util/DashedSeparator.java @@ -22,7 +22,7 @@ import java.awt.*; /** - * This component draws a dashed horizontal line, similarly to a JSeparator. It's color and line strength (stroke + * This component draws a dashed horizontal line, similarly to a JSeparator. Its color and line strength (stroke * width) can be set. Also, its width (the amount of available horizontal space it covers) can be set. */ public class DashedSeparator extends JComponent { diff --git a/megamek/src/megamek/common/scenario/ScenarioFullInfo.java b/megamek/src/megamek/common/scenario/ScenarioInfo.java similarity index 64% rename from megamek/src/megamek/common/scenario/ScenarioFullInfo.java rename to megamek/src/megamek/common/scenario/ScenarioInfo.java index 22624139608..df99f4ebe4f 100644 --- a/megamek/src/megamek/common/scenario/ScenarioFullInfo.java +++ b/megamek/src/megamek/common/scenario/ScenarioInfo.java @@ -18,58 +18,37 @@ */ package megamek.common.scenario; -import megamek.common.Configuration; - -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.List; - -public class ScenarioFullInfo extends HashMap> { +/** + * This class holds all scenario info loaded from a scenario (.mms) file. It is a map of constants given in + * {@link ScenarioLoader} to a list of data for that constant. + */ +public class ScenarioInfo extends HashMap> { + /** + * @return The name of the scenario; keyword {@link ScenarioLoader#NAME} + */ public String getName() { return getString(ScenarioLoader.NAME); } + /** + * @return The description of the scenario; keyword {@link ScenarioLoader#DESCRIPTION} + */ public String getDescription() { return getString(ScenarioLoader.DESCRIPTION); } + /** + * @return The filename including directories of the scenario + */ public String getFileName() { return getString(ScenarioLoader.FILENAME); } - public String getSubDirectory() { - String scenariosDir = Configuration.scenariosDir().toString(); - if (!getFileName().contains(scenariosDir)) { - return ""; - } else { - return subDirUnderScenarios(directoriesAsList(getFileName())); - } - } - - public static List directoriesAsList(String fileName) { - Path path = Paths.get(fileName); - List result = new ArrayList<>(); - for (int i = 0; i < path.getNameCount(); i++) { - result.add(path.getName(i).toString()); - } - return result; - } - - private String subDirUnderScenarios(List 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) : ""; - } - } - public void put(String key, String value) { Collection values = get(key); if (values == null) { @@ -109,4 +88,4 @@ public int getNumValues(String key) { Collection values = get(key); return (values == null) ? 0 : values.size(); } -} +} \ No newline at end of file diff --git a/megamek/src/megamek/common/scenario/ScenarioLoader.java b/megamek/src/megamek/common/scenario/ScenarioLoader.java index 949329e913e..9fde10877f5 100644 --- a/megamek/src/megamek/common/scenario/ScenarioLoader.java +++ b/megamek/src/megamek/common/scenario/ScenarioLoader.java @@ -354,7 +354,7 @@ else if (chp.entity instanceof Tank) { public Game createGame() throws Exception { LogManager.getLogger().info("Loading scenario from " + scenarioFile); - ScenarioFullInfo p = load(); + ScenarioInfo p = load(); String sCheck = p.getString(PARAM_MMSVERSION); if (sCheck == null) { @@ -421,7 +421,7 @@ public Game createGame() throws Exception { return g; } - private void parsePlanetaryConditions(Game g, ScenarioFullInfo p) { + private void parsePlanetaryConditions(Game g, ScenarioInfo p) { if (p.containsKey(PARAM_PLANETCOND_TEMP)) { g.getPlanetaryConditions().setTemperature(Integer.parseInt(p.getString(PARAM_PLANETCOND_TEMP))); } @@ -483,7 +483,7 @@ private void parsePlanetaryConditions(Game g, ScenarioFullInfo p) { } } - private Collection buildFactionEntities(ScenarioFullInfo p, Player player) throws ScenarioLoaderException { + private Collection buildFactionEntities(ScenarioInfo p, Player player) throws ScenarioLoaderException { String faction = player.getName(); Pattern unitPattern = Pattern.compile(String.format("^Unit_\\Q%s\\E_[^_]+$", faction)); Pattern unitDataPattern = Pattern.compile(String.format("^(Unit_\\Q%s\\E_[^_]+)_([A-Z][^_]+)$", faction)); @@ -722,7 +722,7 @@ private String getFactionParam(String faction, String param) { return param + SEPARATOR_UNDERSCORE + faction; } - private Collection createPlayers(ScenarioFullInfo p) throws ScenarioLoaderException { + private Collection createPlayers(ScenarioInfo p) throws ScenarioLoaderException { String sFactions = p.getString(PARAM_FACTIONS); if ((sFactions == null) || sFactions.isEmpty()) { throw new ScenarioLoaderException("missingFactions"); @@ -789,7 +789,7 @@ private Collection createPlayers(ScenarioFullInfo p) throws ScenarioLoad /** * Load board files and create the megaboard. */ - private Board createBoard(ScenarioFullInfo p) throws ScenarioLoaderException { + private Board createBoard(ScenarioInfo p) throws ScenarioLoaderException { int mapWidth = 16, mapHeight = 17; if (p.getString(PARAM_MAP_WIDTH) == null) { LogManager.getLogger().info("No map width specified; using " + mapWidth); @@ -897,8 +897,8 @@ private Board createBoard(ScenarioFullInfo p) throws ScenarioLoaderException { return BoardUtilities.combine(mapWidth, mapHeight, nWidth, nHeight, ba, rotateBoard, MapSettings.MEDIUM_GROUND); } - public ScenarioFullInfo load() throws ScenarioLoaderException { - ScenarioFullInfo props = new ScenarioFullInfo(); + public ScenarioInfo load() throws ScenarioLoaderException { + ScenarioInfo props = new ScenarioInfo(); props.put(FILENAME, List.of(scenarioFile.toString())); try (FileInputStream fis = new FileInputStream(scenarioFile); InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8); @@ -933,7 +933,7 @@ public ScenarioFullInfo load() throws ScenarioLoaderException { /** * Parses out the external game id from the scenario file */ - private int parseExternalGameId(ScenarioFullInfo p) { + private int parseExternalGameId(ScenarioInfo p) { String sExternalId = p.getString(PARAM_GAME_EXTERNAL_ID); int ExternalGameId = 0; if (sExternalId != null) { @@ -959,7 +959,7 @@ public boolean isSinglePlayer() { * defaultValue. When the key is present, interprets "true" and "on" and "1" * as true and everything else as false. */ - private boolean parseBoolean(ScenarioFullInfo p, String key, boolean defaultValue) { + private boolean parseBoolean(ScenarioInfo p, String key, boolean defaultValue) { boolean result = defaultValue; if (p.containsKey(key)) { if (p.getString(key).equalsIgnoreCase("true") From 922a9a9d761e688185ae203b8d1ba8d98bf9a591 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 28 Feb 2024 14:56:20 +0100 Subject: [PATCH 4/4] layout correction --- .../src/megamek/client/ui/swing/scenario/ScenarioChooser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java b/megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java index a3603a08a08..b22d7260327 100644 --- a/megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java +++ b/megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java @@ -109,7 +109,7 @@ protected JPanel createButtonPanel() { Box verticalBox = Box.createVerticalBox(); verticalBox.add(Box.createVerticalGlue()); - JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); panel.add(chooseFileLabel); panel.add(Box.createHorizontalGlue()); verticalBox.add(panel);