Skip to content

Commit

Permalink
Restore tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelbraginskiy committed Jan 1, 2025
1 parent b62d04c commit 8d8ecf5
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@
/megameklab/docs/mml-revision.txt
/megameklab/MegaMekLab.l4j.ini
units.cache
.mml_tmp
Original file line number Diff line number Diff line change
Expand Up @@ -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=<html>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.<br>Restoring tabs restores the state of the entities as they were in the editor, not as you saved them!</html>
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:
Expand Down
1 change: 1 addition & 0 deletions megameklab/resources/megameklab/resources/Menu.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions megameklab/src/megameklab/MegaMekLab.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ private static void startup(String[] args) {
new StartupGUI().setVisible(true);
}
}
case RESTORE_TABS -> UiLoader.restoreTabbedUi();
default -> {
new StartupGUI().setVisible(true);
}
Expand Down
3 changes: 2 additions & 1 deletion megameklab/src/megameklab/ui/MMLStartUp.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public enum MMLStartUp {

SPLASH_SCREEN,
RECENT_UNIT,
RESTORE_TABS,
NEW_MEK,
NEW_TANK,
NEW_BATTLEARMOR,
Expand Down Expand Up @@ -62,4 +63,4 @@ public static MMLStartUp parse(String startUpName) {
return SPLASH_SCREEN;
}
}
}
}
15 changes: 10 additions & 5 deletions megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down
56 changes: 46 additions & 10 deletions megameklab/src/megameklab/ui/dialog/UiLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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();
}

/**
Expand Down
172 changes: 172 additions & 0 deletions megameklab/src/megameklab/ui/util/TabStateUtil.java
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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<MegaMekLabMainUI> 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<File, String> 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();

Check notice

Code scanning / CodeQL

Ignored error status of call Note

Method saveTabState ignores exceptional return value of File.createNewFile.
}

public static List<MegaMekLabMainUI> loadTabState() throws IOException {
var dir = getTabStateDirectory();
List<MegaMekLabMainUI> 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];

Check failure

Code scanning / CodeQL

Array index out of bounds Error

This array access might be out of bounds, as the index might be equal to the array length.
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() {}
}

0 comments on commit 8d8ecf5

Please sign in to comment.