From 8d8ecf5240c7da5952b6e721aa86af261b151421 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Wed, 1 Jan 2025 04:26:57 -0800 Subject: [PATCH] Restore tabs --- .gitignore | 1 + .../megameklab/resources/Dialogs.properties | 2 +- .../megameklab/resources/Menu.properties | 1 + megameklab/src/megameklab/MegaMekLab.java | 1 + megameklab/src/megameklab/ui/MMLStartUp.java | 3 +- .../src/megameklab/ui/MegaMekLabTabbedUI.java | 15 +- .../src/megameklab/ui/dialog/UiLoader.java | 56 +++++- .../src/megameklab/ui/util/TabStateUtil.java | 172 ++++++++++++++++++ 8 files changed, 234 insertions(+), 17 deletions(-) create mode 100644 megameklab/src/megameklab/ui/util/TabStateUtil.java diff --git a/.gitignore b/.gitignore index 4782e40dc..33c7019af 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ /megameklab/docs/mml-revision.txt /megameklab/MegaMekLab.l4j.ini units.cache +.mml_tmp diff --git a/megameklab/resources/megameklab/resources/Dialogs.properties b/megameklab/resources/megameklab/resources/Dialogs.properties index 62efaf624..2a696eef5 100644 --- a/megameklab/resources/megameklab/resources/Dialogs.properties +++ b/megameklab/resources/megameklab/resources/Dialogs.properties @@ -63,7 +63,7 @@ ConfigurationDialog.chkSummaryFormatTRO.tooltip=When checked, text exports are f ConfigurationDialog.chkSkipSavePrompts.text=Disable save prompts. Use at your own risk! ConfigurationDialog.chkSkipSavePrompts.tooltip=When checked, no safety dialogs warning about losing changes when switching unit or unit type will be shown. ConfigurationDialog.startup.text=MML Startup: -ConfigurationDialog.startup.tooltip=Depending on the startup type selected, MML will start in the main menu or, alternatively, directly load the most recent unit or start with a new unit instead of the main menu. +ConfigurationDialog.startup.tooltip=Depending on the startup type selected, MML will start in the main menu or, alternatively, directly load the most recent unit or start with a new unit instead of the main menu, or restore all of your opened tabs.
Restoring tabs restores the state of the entities as they were in the editor, not as you saved them! ConfigurationDialog.mekChassis.text=Mek Chassis Arrangement: ConfigurationDialog.mekChassis.tooltip=Meks with a Clan and an IS chassis name will print their chassis in the selected arrangement. Meks with no clan chassis name will always just print their chassis. ConfigurationDialog.cbMulOpenBehaviour.text=MUL file open behaviour: diff --git a/megameklab/resources/megameklab/resources/Menu.properties b/megameklab/resources/megameklab/resources/Menu.properties index 5b8a40a5e..96ece9e91 100644 --- a/megameklab/resources/megameklab/resources/Menu.properties +++ b/megameklab/resources/megameklab/resources/Menu.properties @@ -107,6 +107,7 @@ message.exportingInvalidUnit.text=Warning: Exporting an invalid unit! # The following values are used programatically by MMLStartUp MMLStartUp.SPLASH_SCREEN=MML Main UI MMLStartUp.RECENT_UNIT=Last Unit +MMLStartUp.RESTORE_TABS=Restore Tabs MMLStartUp.NEW_MEK=New Mek MMLStartUp.NEW_TANK=New Combat Vehicle MMLStartUp.NEW_BATTLEARMOR=New BattleArmor diff --git a/megameklab/src/megameklab/MegaMekLab.java b/megameklab/src/megameklab/MegaMekLab.java index 7267ea4f6..50a65f8ee 100644 --- a/megameklab/src/megameklab/MegaMekLab.java +++ b/megameklab/src/megameklab/MegaMekLab.java @@ -154,6 +154,7 @@ private static void startup(String[] args) { new StartupGUI().setVisible(true); } } + case RESTORE_TABS -> UiLoader.restoreTabbedUi(); default -> { new StartupGUI().setVisible(true); } diff --git a/megameklab/src/megameklab/ui/MMLStartUp.java b/megameklab/src/megameklab/ui/MMLStartUp.java index fdb9d8853..d413c6885 100644 --- a/megameklab/src/megameklab/ui/MMLStartUp.java +++ b/megameklab/src/megameklab/ui/MMLStartUp.java @@ -31,6 +31,7 @@ public enum MMLStartUp { SPLASH_SCREEN, RECENT_UNIT, + RESTORE_TABS, NEW_MEK, NEW_TANK, NEW_BATTLEARMOR, @@ -62,4 +63,4 @@ public static MMLStartUp parse(String startUpName) { return SPLASH_SCREEN; } } -} \ No newline at end of file +} diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index 2bb323648..8e77a1083 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -28,6 +28,7 @@ import megameklab.ui.dialog.UiLoader; import megameklab.ui.mek.BMMainUI; import megameklab.ui.util.ExitOnWindowClosingListener; +import megameklab.ui.util.TabStateUtil; import megameklab.util.CConfig; import megameklab.util.MMLFileDropTransferHandler; import megameklab.util.UnitUtil; @@ -37,6 +38,7 @@ import javax.swing.event.ChangeListener; import java.awt.*; import java.awt.event.*; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -63,11 +65,6 @@ public class MegaMekLabTabbedUI extends JFrame implements MenuBarOwner, ChangeLi public MegaMekLabTabbedUI(MegaMekLabMainUI... entities) { super("MegaMekLab"); - // Create a blank Mek by default - if (entities.length == 0) { - entities = new MegaMekLabMainUI[] { new BMMainUI(false, false) }; - } - // If there are more tabs than can fit, show a scroll bar instead of stacking tabs in multiple rows // This is a matter of preference, I could be convinced to switch this. tabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); @@ -241,6 +238,14 @@ public boolean exit() { PreferenceManager.getInstance().save(); MegaMek.getMMPreferences().saveToFile(MMLConstants.MM_PREFERENCES_FILE); MegaMekLab.getMMLPreferences().saveToFile(MMLConstants.MML_PREFERENCES_FILE); + + try { + TabStateUtil.saveTabState(editors.stream().limit(editors.size() - 1).toList()); + } catch (IOException e) { + // todo real error handling? + throw new RuntimeException(e); + } + return true; } diff --git a/megameklab/src/megameklab/ui/dialog/UiLoader.java b/megameklab/src/megameklab/ui/dialog/UiLoader.java index 4c6e111fc..bacc395a3 100644 --- a/megameklab/src/megameklab/ui/dialog/UiLoader.java +++ b/megameklab/src/megameklab/ui/dialog/UiLoader.java @@ -19,6 +19,7 @@ package megameklab.ui.dialog; import java.awt.BorderLayout; +import java.io.IOException; import java.util.Map; import java.util.Objects; import java.util.ResourceBundle; @@ -33,6 +34,7 @@ import megameklab.ui.MegaMekLabMainUI; import megameklab.ui.MegaMekLabTabbedUI; import megameklab.ui.PopupMessages; +import megameklab.ui.StartupGUI; import megameklab.ui.battleArmor.BAMainUI; import megameklab.ui.combatVehicle.CVMainUI; import megameklab.ui.fighterAero.ASMainUI; @@ -42,6 +44,7 @@ import megameklab.ui.mek.BMMainUI; import megameklab.ui.protoMek.PMMainUI; import megameklab.ui.supportVehicle.SVMainUI; +import megameklab.ui.util.TabStateUtil; import megameklab.util.UnitUtil; /** @@ -69,6 +72,7 @@ public class UiLoader { private final boolean industrial; private final Entity newUnit; private final String fileName; + private boolean restore = false; public static void loadUi(Entity newUnit, String fileName) { new UiLoader(UnitUtil.getEditorTypeForEntity(newUnit), newUnit.isPrimitive(), newUnit.isIndustrialMek(), newUnit, fileName).show(); @@ -78,6 +82,10 @@ public static void loadUi(long type, boolean primitive, boolean industrial) { new UiLoader(type, primitive, industrial, null, "").show(); } + public static void restoreTabbedUi() { + new UiLoader(true).show(); + } + /** * @param type - the unit type to load the mainUI from, based on the types * in StartupGUI.java @@ -102,6 +110,16 @@ private UiLoader(long type, boolean primitive, boolean industrial, Entity newUni splashImage.setLocationRelativeTo(null); } + private UiLoader(boolean restore) { + this(0, false, false, null, null); + + if (!restore) { + throw new IllegalArgumentException("Impossible!"); + } + + this.restore = true; + } + /** * Shows the splash image, hides the calling frame and starts loading the new * unit's UI. @@ -112,17 +130,35 @@ public void show() { } private void loadNewUi() { - MegaMekLabMainUI newUI = getUI(type, primitive, industrial); - if (newUnit != null) { - UnitUtil.updateLoadedUnit(newUnit); - newUI.setEntity(newUnit, fileName); - newUI.reloadTabs(); - newUI.refreshAll(); + try { + MegaMekLabTabbedUI tabbedUi; + if (!restore) { + MegaMekLabMainUI newUI = getUI(type, primitive, industrial); + if (newUnit != null) { + UnitUtil.updateLoadedUnit(newUnit); + newUI.setEntity(newUnit, fileName); + newUI.reloadTabs(); + newUI.refreshAll(); + } + tabbedUi = new MegaMekLabTabbedUI(newUI); + tabbedUi.setVisible(true); + } else { + try { + var editors = TabStateUtil.loadTabState().toArray(new MegaMekLabMainUI[0]); + if (editors.length == 0) { + throw new IllegalStateException("Could not restore tabs"); + } + tabbedUi = new MegaMekLabTabbedUI(editors); + tabbedUi.setVisible(true); + } catch (IOException | IllegalStateException e) { + new StartupGUI().setVisible(true); + } + } + + } finally { + splashImage.setVisible(false); + splashImage.dispose(); } - var tabbedUi = new MegaMekLabTabbedUI(newUI); - tabbedUi.setVisible(true); - splashImage.setVisible(false); - splashImage.dispose(); } /** diff --git a/megameklab/src/megameklab/ui/util/TabStateUtil.java b/megameklab/src/megameklab/ui/util/TabStateUtil.java new file mode 100644 index 000000000..55ba6a99c --- /dev/null +++ b/megameklab/src/megameklab/ui/util/TabStateUtil.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMekLab. + * + * 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 megameklab.ui.util; + +import megamek.common.Entity; +import megamek.common.Mek; +import megamek.common.MekFileParser; +import megamek.common.loaders.BLKFile; +import megamek.common.loaders.EntityLoadingException; +import megamek.common.loaders.EntitySavingException; +import megamek.common.preference.PreferenceManager; +import megameklab.ui.MegaMekLabMainUI; +import megameklab.ui.dialog.UiLoader; +import megameklab.util.UnitUtil; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Pattern; + +public class TabStateUtil { + private final static String TAB_STATE_DIRECTORY = ".mml_tmp"; + private final static String TAB_STATE_CLEAN = "clean"; + private final static String FILENAME_ASSOCIATIONS = "filenames.db"; + + public static void saveTabState(List editors) throws IOException { + var dir = getTabStateDirectory(); + + var clean = new File(dir, TAB_STATE_CLEAN); + if (clean.exists()) { + if (!clean.delete()) { + throw new IOException("Could not delete " + clean); + } + } + + FileUtils.cleanDirectory(dir); + + Map filenameAssociations = new LinkedHashMap<>(); + + for (var editor : editors) { + File unitFile; + if (editor.getEntity() instanceof Mek) { + unitFile = File.createTempFile("mml_unit_", ".mtf.tmp", dir); + try ( + var fos = new FileOutputStream(unitFile); + var ps = new PrintStream(fos) + ) { + ps.println(((Mek) editor.getEntity()).getMtf()); + } catch (Exception e) { + continue; + } + } else { + unitFile = File.createTempFile("mml_unit_", ".blk.tmp", dir); + try { + BLKFile.encode(unitFile.getPath(), editor.getEntity()); + } catch (EntitySavingException e) { + continue; + } + } + + var fileName = editor.getFileName(); + if (fileName == null || fileName.isBlank()) { + fileName = " "; + } + + filenameAssociations.put(unitFile, fileName); + } + + File filenameAssociationsFile = new File(dir, FILENAME_ASSOCIATIONS); + try ( + var fos = new FileOutputStream(filenameAssociationsFile); + var ps = new PrintStream(fos) + ) { + for (var entry : filenameAssociations.entrySet()) { + ps.print(entry.getKey().getPath() + '\0' + entry.getValue() + '\0'); + } + } + + clean.createNewFile(); + } + + public static List loadTabState() throws IOException { + var dir = getTabStateDirectory(); + List editors = new ArrayList<>(); + + var clean = new File(dir, TAB_STATE_CLEAN); + if (!clean.exists()) { + return editors; + } + + var db = new File(dir, FILENAME_ASSOCIATIONS); + if (!db.exists()) { + return editors; + } + + var parts = Files.readString(Paths.get(db.getAbsolutePath())).split(Pattern.quote("\0")); + for (int i = 0; i < parts.length; i += 2) { + var entityFile = new File(parts[i]); + + var newFile = new File(entityFile.getAbsolutePath().replaceFirst("\\.tmp$", "")); + FileUtils.copyFile(entityFile, newFile); + + var fileName = parts[i + 1]; + if (fileName.isBlank()) { + fileName = ""; + } + + try { + Entity loadedUnit = new MekFileParser(newFile).getEntity(); + newFile.delete(); + var editor = UiLoader.getUI(UnitUtil.getEditorTypeForEntity(loadedUnit), loadedUnit.isPrimitive(), loadedUnit.isIndustrialMek()); + editor.setEntity(loadedUnit); + editor.setFileName(fileName); + editor.reloadTabs(); + editor.refreshAll(); + editors.add(editor); + } catch (EntityLoadingException ignored) {} + } + + clean.delete(); + + return editors; + } + + private static File getTabStateDirectory() throws IOException { + var userDirString = PreferenceManager.getClientPreferences().getUserDir(); + if (userDirString == null || userDirString.isBlank()) { + userDirString = "."; + } + + var userDir = new File(userDirString); + + if (!userDir.isDirectory()) { + throw new IOException("User dir is not a directory: " + userDirString); + } + + var tabStateDir = new File(userDir, TAB_STATE_DIRECTORY); + if (!tabStateDir.isDirectory()) { + if (!tabStateDir.mkdir()) { + throw new IOException("Could not create tab state directory: " + tabStateDir); + } else { + Files.setAttribute(Paths.get(tabStateDir.getAbsolutePath()), "dos:hidden", true); + } + } + + return tabStateDir; + } + + private TabStateUtil() {} +}