diff --git a/.gitignore b/.gitignore index 45eefc794a4..a531dfba94d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ build/ #MegaMek Data Folders # mmconf /megamek/mmconf/clientsettings.xml +/megamek/mmconf/recent_boards.yml /megamek/mmconf/*gameoptions.xml /megamek/mmconf/*.properties !/megamek/mmconf/shared.properties diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index d4700b60815..dab39108c61 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -171,6 +171,7 @@ AdvancedOptions.ShowFPS.name=Show drawtime #Board Editor BoardEditor.BridgeBuildingElevError=Bridge/Building Elevation is an offset from the surface of a hex and hence must be a positive value! BoardEditor.OpenFileError=Could not open file {0}. +BoardEditor.loadBoardError=Could not load the board file. BoardEditor.butAddTerrain=Add/Set Terrain BoardEditor.butBoardOpen=Open... BoardEditor.butBoardNew=New... @@ -954,6 +955,7 @@ CommonMenuBar.boardRemoveRoads=Remove Roads and Bridges CommonMenuBar.boardRemoveBuildings=Remove Buildings and Fuel Tanks CommonMenuBar.fileBoardNew=New CommonMenuBar.fileBoardOpen=Open... +CommonMenuBar.fileBoardRecent=Recent Boards CommonMenuBar.fileBoardSave=Save... CommonMenuBar.fileBoardSaveAs=Save As... CommonMenuBar.fileBoardSaveAsImage=Save As Image... diff --git a/megamek/i18n/megamek/client/messages_de.properties b/megamek/i18n/megamek/client/messages_de.properties index 46aba222ada..570f0dc158c 100644 --- a/megamek/i18n/megamek/client/messages_de.properties +++ b/megamek/i18n/megamek/client/messages_de.properties @@ -1240,4 +1240,6 @@ CASCardPanel.printCard=Drucken CASCardPanel.MUL=MUL öffnen CASCardPanel.conversionReport=Umrechnungs-Bericht CASCardPanel.font=Font: -CASCardPanel.cardSize=Kartengröße: \ No newline at end of file +CASCardPanel.cardSize=Kartengröße: +Error=Fehler +BoardEditor.loadBoardError=Karte konnte nicht geladen werden. \ No newline at end of file diff --git a/megamek/src/megamek/client/ui/swing/BoardEditor.java b/megamek/src/megamek/client/ui/swing/BoardEditor.java index 07b62df322c..6aff02cf1f4 100644 --- a/megamek/src/megamek/client/ui/swing/BoardEditor.java +++ b/megamek/src/megamek/client/ui/swing/BoardEditor.java @@ -25,6 +25,7 @@ import megamek.client.ui.Messages; import megamek.client.ui.dialogs.helpDialogs.AbstractHelpDialog; import megamek.client.ui.dialogs.helpDialogs.BoardEditorHelpDialog; +import megamek.client.ui.enums.DialogResult; import megamek.client.ui.swing.boardview.*; import megamek.client.ui.swing.dialog.FloodDialog; import megamek.client.ui.swing.dialog.LevelChangeDialog; @@ -32,10 +33,7 @@ import megamek.client.ui.swing.minimap.Minimap; import megamek.client.ui.swing.tileset.HexTileset; import megamek.client.ui.swing.tileset.TilesetManager; -import megamek.client.ui.swing.util.FontHandler; -import megamek.client.ui.swing.util.MegaMekController; -import megamek.client.ui.swing.util.StringDrawer; -import megamek.client.ui.swing.util.UIUtil; +import megamek.client.ui.swing.util.*; import megamek.client.ui.swing.util.UIUtil.FixedYPanel; import megamek.common.*; import megamek.common.annotations.Nullable; @@ -503,22 +501,9 @@ private void setupFrame() { @Override public void windowClosing(WindowEvent e) { // When the board has changes, ask the user - if (hasChanges) { - ignoreHotKeys = true; - int savePrompt = JOptionPane.showConfirmDialog(null, - Messages.getString("BoardEditor.exitprompt"), - Messages.getString("BoardEditor.exittitle"), - JOptionPane.YES_NO_CANCEL_OPTION, - JOptionPane.WARNING_MESSAGE); - ignoreHotKeys = false; - - // When the user cancels or did not actually save the board, don't close - if (((savePrompt == JOptionPane.YES_OPTION) && !boardSave(false)) || - (savePrompt == JOptionPane.CANCEL_OPTION)) { - return; - } + if (hasChanges && (showSavePrompt() == DialogResult.CANCELLED)) { + return; } - // otherwise: exit the Map Editor minimapW.setVisible(false); if (controller != null) { @@ -535,6 +520,32 @@ public void windowClosed(WindowEvent e) { }); } + /** + * Shows a prompt to save the current board. When the board is actually saved or the user presses + * "No" (don't want to save), returns DialogResult.CONFIRMED. In this case, the action (loading a board + * or leaving the board editor) that led to this prompt may be continued. + * In all other cases, returns DialogResult.CANCELLED, meaning the action should not be continued. + * + * @return DialogResult.CANCELLED (cancel action) or CONFIRMED (continue action) + */ + private DialogResult showSavePrompt() { + ignoreHotKeys = true; + int savePrompt = JOptionPane.showConfirmDialog(null, + Messages.getString("BoardEditor.exitprompt"), + Messages.getString("BoardEditor.exittitle"), + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE); + ignoreHotKeys = false; + // When the user cancels or did not actually save the board, don't load anything + if (((savePrompt == JOptionPane.YES_OPTION) && !boardSave(false)) + || (savePrompt == JOptionPane.CANCEL_OPTION) + || (savePrompt == JOptionPane.CLOSED_OPTION)) { + return DialogResult.CANCELLED; + } else { + return DialogResult.CONFIRMED; + } + } + /** * Sets up Scaling Icon Buttons */ @@ -1009,10 +1020,9 @@ private void setupEditorPanel() { addManyActionListeners(butBoardOpen, butExpandMap, butBoardNew); addManyActionListeners(butDelTerrain, butAddTerrain, butSourceFile); - JPanel panButtons = new JPanel(new GridLayout(3, 2, 2, 2)); + JPanel panButtons = new JPanel(new GridLayout(3, 3, 2, 2)); addManyButtons(panButtons, List.of(butBoardNew, butBoardSave, butBoardOpen, - butExpandMap, butBoardSaveAs, butBoardSaveAsImage)); - panButtons.add(butBoardValidate); + butExpandMap, butBoardSaveAs, butBoardSaveAsImage, butBoardValidate)); if (Desktop.isDesktopSupported()) { panButtons.add(butSourceFile); } @@ -1443,7 +1453,7 @@ public void updateMapSettings(MapSettings newSettings) { mapSettings = newSettings; } - public void boardLoad() { + public void loadBoard() { JFileChooser fc = new JFileChooser(loadPath); setDialogSize(fc); fc.setDialogTitle(Messages.getString("BoardEditor.loadBoard")); @@ -1454,10 +1464,11 @@ public void boardLoad() { // I want a file, y'know! return; } - curBoardFile = fc.getSelectedFile(); - loadPath = curBoardFile.getParentFile(); - // load! - try (InputStream is = new FileInputStream(fc.getSelectedFile())) { + loadBoard(fc.getSelectedFile()); + } + + public void loadBoard(File file) { + try (InputStream is = new FileInputStream(file)) { // tell the board to load! board.load(is, null, true); Set boardTags = board.getTags(); @@ -1474,15 +1485,31 @@ public void boardLoad() { } cheRoadsAutoExit.setSelected(board.getRoadsAutoExit()); mapSettings.setBoardSize(board.getWidth(), board.getHeight()); + curBoardFile = file; + RecentBoardList.addBoard(curBoardFile); + loadPath = curBoardFile.getParentFile(); // Now, *after* initialization of the board which will correct some errors, // do a board validation validateBoard(false); - refreshTerrainList(); setupUiFreshBoard(); } catch (IOException ex) { LogManager.getLogger().error("", ex); + showBoardLoadError(ex); + initializeBoardIfEmpty(); + } + } + + private void showBoardLoadError(Exception ex) { + String message = Messages.getString("BoardEditor.loadBoardError") + System.lineSeparator() + ex.getMessage(); + String title = Messages.getString("Error"); + JOptionPane.showMessageDialog(frame, message, title, JOptionPane.ERROR_MESSAGE); + } + + private void initializeBoardIfEmpty() { + if ((board == null) || (board.getWidth() == 0) || (board.getHeight() == 0)) { + boardNew(false); } } @@ -1554,6 +1581,7 @@ private boolean boardSave(boolean saveAs) { butSourceFile.setEnabled(true); savedUndoStackSize = undoStack.size(); hasChanges = false; + RecentBoardList.addBoard(curBoardFile); setFrameTitle(); return true; } catch (IOException e) { @@ -1772,7 +1800,13 @@ private void showBoardValidationReport(List errors) { // @Override public void actionPerformed(ActionEvent ae) { - if (ae.getActionCommand().equals(ClientGUI.BOARD_NEW)) { + if (ae.getActionCommand().startsWith(ClientGUI.BOARD_RECENT)) { + if (hasChanges && (showSavePrompt() == DialogResult.CANCELLED)) { + return; + } + String recentBoard = ae.getActionCommand().substring(ClientGUI.BOARD_RECENT.length() + 1); + loadBoard(new File(recentBoard)); + } else if (ae.getActionCommand().equals(ClientGUI.BOARD_NEW)) { ignoreHotKeys = true; boardNew(true); ignoreHotKeys = false; @@ -1782,7 +1816,7 @@ public void actionPerformed(ActionEvent ae) { ignoreHotKeys = false; } else if (ae.getActionCommand().equals(ClientGUI.BOARD_OPEN)) { ignoreHotKeys = true; - boardLoad(); + loadBoard(); ignoreHotKeys = false; } else if (ae.getActionCommand().equals(ClientGUI.BOARD_SAVE)) { ignoreHotKeys = true; diff --git a/megamek/src/megamek/client/ui/swing/ClientGUI.java b/megamek/src/megamek/client/ui/swing/ClientGUI.java index 23236a68b79..7161247c7b7 100644 --- a/megamek/src/megamek/client/ui/swing/ClientGUI.java +++ b/megamek/src/megamek/client/ui/swing/ClientGUI.java @@ -108,6 +108,7 @@ public class ClientGUI extends AbstractClientGUI implements BoardViewListener, // board submenu public static final String BOARD_NEW = "fileBoardNew"; public static final String BOARD_OPEN = "fileBoardOpen"; + public static final String BOARD_RECENT = "recent"; public static final String BOARD_SAVE = "fileBoardSave"; public static final String BOARD_SAVE_AS = "fileBoardSaveAs"; public static final String BOARD_SAVE_AS_IMAGE = "fileBoardSaveAsImage"; diff --git a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java index 00376de025c..33cfe6fa4ee 100644 --- a/megamek/src/megamek/client/ui/swing/CommonMenuBar.java +++ b/megamek/src/megamek/client/ui/swing/CommonMenuBar.java @@ -35,6 +35,7 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; +import java.io.File; import java.util.*; import static java.awt.event.KeyEvent.*; @@ -87,6 +88,7 @@ public class CommonMenuBar extends JMenuBar implements ActionListener, IPreferen // The Board menu private final JMenuItem boardNew = new JMenuItem(getString("CommonMenuBar.fileBoardNew")); private final JMenuItem boardOpen = new JMenuItem(getString("CommonMenuBar.fileBoardOpen")); + private final JMenu boardRecent = new JMenu(getString("CommonMenuBar.fileBoardRecent")); private final JMenuItem boardSave = new JMenuItem(getString("CommonMenuBar.fileBoardSave")); private final JMenuItem boardSaveAs = new JMenuItem(getString("CommonMenuBar.fileBoardSaveAs")); private final JMenuItem boardSaveAsImage = new JMenuItem(getString("CommonMenuBar.fileBoardSaveAsImage")); @@ -216,6 +218,8 @@ public CommonMenuBar() { add(menu); initMenuItem(boardNew, menu, BOARD_NEW); initMenuItem(boardOpen, menu, BOARD_OPEN, VK_O); + initMenuItem(boardRecent, menu, BOARD_OPEN, VK_O); + initializeRecentBoardsMenu(); initMenuItem(boardSave, menu, BOARD_SAVE); initMenuItem(boardSaveAs, menu, BOARD_SAVE_AS); initMenuItem(boardValidate, menu, BOARD_VALIDATE); @@ -335,6 +339,7 @@ public CommonMenuBar() { setKeyBinds(); GUIP.addPreferenceChangeListener(this); KeyBindParser.addPreferenceChangeListener(this); + RecentBoardList.addListener(this); } /** Sets/updates the accelerators from the KeyCommandBinds preferences. */ @@ -479,6 +484,7 @@ private synchronized void updateEnabledStates() { boardSaveAsImage.setEnabled(isBoardEditor || isInGame); // TODO: should work in the lobby boardNew.setEnabled(isBoardEditor || isMainMenu); boardOpen.setEnabled(isBoardEditor || isMainMenu); + boardRecent.setEnabled((isBoardEditor || isMainMenu) && !RecentBoardList.getRecentBoards().isEmpty()); fileUnitsPaste.setEnabled(isLobby); fileUnitsCopy.setEnabled(isLobby); fileUnitsReinforce.setEnabled((isLobby || isInGame) && isNotVictory); @@ -568,6 +574,8 @@ public void preferenceChange(PreferenceChangeEvent e) { gameRoundReport.setSelected(GUIP.getMiniReportEnabled()); } else if (e.getName().equals(GUIPreferences.PLAYER_LIST_ENABLED)) { gamePlayerList.setSelected(GUIP.getPlayerListEnabled()); + } else if (e.getName().equals(RecentBoardList.RECENT_BOARDS_UPDATED)) { + initializeRecentBoardsMenu(); } } @@ -580,6 +588,7 @@ private void adaptToGUIScale() { public void die() { GUIP.removePreferenceChangeListener(this); KeyBindParser.removePreferenceChangeListener(this); + RecentBoardList.removeListener(this); } private void initMenuItem(JMenuItem item, JMenu menu, String command) { @@ -593,4 +602,19 @@ private void initMenuItem(JMenuItem item, JMenu menu, String command, int mnemon initMenuItem(item, menu, command); item.setMnemonic(mnemonic); } + + /** + * Updates the Recent Boards submenu with the current list of recent boards + */ + private void initializeRecentBoardsMenu() { + List recentBoards = RecentBoardList.getRecentBoards(); + boardRecent.removeAll(); + for (String recentBoard : recentBoards) { + File boardFile = new File(recentBoard); + JMenuItem item = new JMenuItem(boardFile.getName()); + initMenuItem(item, boardRecent, BOARD_RECENT + "|" + recentBoard); + } + boardRecent.setEnabled(!recentBoards.isEmpty()); + adaptToGUIScale(); + } } diff --git a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java index 5d64025b5de..c81f2677aa7 100644 --- a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java +++ b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java @@ -346,6 +346,16 @@ void showEditor() { editor.boardNew(GUIPreferences.getInstance().getBoardEdRndStart()); } + /** + * Display the board editor and load the given board + */ + void showEditor(String boardFile) { + BoardEditor editor = new BoardEditor(controller); + controller.boardEditor = editor; + launch(editor.getFrame()); + editor.loadBoard(new File(boardFile)); + } + void showSkinEditor() { int response = JOptionPane.showConfirmDialog(frame, "The skin editor is currently " @@ -368,7 +378,7 @@ void showEditorOpen() { BoardEditor editor = new BoardEditor(controller); controller.boardEditor = editor; launch(editor.getFrame()); - editor.boardLoad(); + editor.loadBoard(); } /** @@ -1016,6 +1026,10 @@ void unlaunch() { } private final ActionListener actionListener = ev -> { + if (ev.getActionCommand().startsWith(ClientGUI.BOARD_RECENT)) { + String recentBoard = ev.getActionCommand().substring(ClientGUI.BOARD_RECENT.length() + 1); + showEditor(recentBoard); + } switch (ev.getActionCommand()) { case ClientGUI.BOARD_NEW: showEditor(); diff --git a/megamek/src/megamek/client/ui/swing/RecentBoardList.java b/megamek/src/megamek/client/ui/swing/RecentBoardList.java new file mode 100644 index 00000000000..fab37f030d3 --- /dev/null +++ b/megamek/src/megamek/client/ui/swing/RecentBoardList.java @@ -0,0 +1,138 @@ +/* + * 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; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import megamek.common.Configuration; +import megamek.common.preference.IPreferenceChangeListener; +import megamek.common.preference.PreferenceChangeEvent; +import megamek.logging.MMLogger; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This class keeps a list of recently opened board files and makes it available statically. It automatically + * writes the list to a file in the MM's mmconf directory. + */ +public final class RecentBoardList { + + public static final String RECENT_BOARDS_UPDATED = "recent_board_event"; + + private static final MMLogger LOGGER = MMLogger.create(RecentBoardList.class); + private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory() + .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER) + .disable(YAMLGenerator.Feature.SPLIT_LINES) + ); + private static final int MAX_RECENT_BOARDS = 10; + private static final String RECENT_BOARD_FILENAME = "recent_boards.yml"; + private static final File RECENT_BOARD_FILE = new File(Configuration.configDir(), RECENT_BOARD_FILENAME); + + private static final RecentBoardList INSTANCE = new RecentBoardList(); + private static final List LISTENERS = new CopyOnWriteArrayList<>(); + + private List recentBoards = null; + + /** + * @return A list of the most recently opened board files. Can be empty. + */ + public static List getRecentBoards() { + INSTANCE.initialize(); + return INSTANCE.recentBoards; + } + + /** + * Adds a new board to the recent board files, replacing the oldest if the list is full. Also + * saves the list to file. + * + * @param board The board filename (full path) + */ + public static void addBoard(String board) { + INSTANCE.addBoardImpl(board); + } + + /** + * Adds a new board to the recent board files, replacing the oldest if the list is full. Also + * saves the list to file. + * + * @param board The board file + */ + public static void addBoard(File board) { + addBoard(board.toString()); + } + + /** + * Adds a listener for recent board changes. The event will have the name {@link #RECENT_BOARDS_UPDATED}. + */ + public static void addListener(IPreferenceChangeListener listener) { + if (!LISTENERS.contains(listener)) { + LISTENERS.add(listener); + } + } + + public static void removeListener(IPreferenceChangeListener listener) { + LISTENERS.remove(listener); + } + + private void addBoardImpl(String board) { + initialize(); + // remove and add so there is only one copy of each and the new board is at the end of the list + recentBoards.remove(board); + recentBoards.add(board); + while (recentBoards.size() > MAX_RECENT_BOARDS) { + recentBoards.remove(0); + } + saveRecentBoards(); + LISTENERS.forEach(l -> l.preferenceChange( + new PreferenceChangeEvent(board, RECENT_BOARDS_UPDATED, null, null))); + } + + private void saveRecentBoards() { + try { + YAML_MAPPER.writeValue(RECENT_BOARD_FILE, INSTANCE.recentBoards); + } catch (IOException e) { + LOGGER.error("Could not save recent board list", e); + } + } + + private void initialize() { + if (INSTANCE.recentBoards == null) { + try { + TypeReference> typeRef = new TypeReference<>() { }; + INSTANCE.recentBoards = YAML_MAPPER.readValue(RECENT_BOARD_FILE, typeRef); + } catch (FileNotFoundException e) { + // ignore, this happens when no list has been saved yet + } catch (IOException e) { + LOGGER.error("Could not load recent board list", e); + } + if (INSTANCE.recentBoards == null) { + INSTANCE.recentBoards = new ArrayList<>(); + } + } + } + + private RecentBoardList() { } +}