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/docs/UserDirHelp.html b/megameklab/docs/UserDirHelp.html index 895448139..1ccb31946 100644 --- a/megameklab/docs/UserDirHelp.html +++ b/megameklab/docs/UserDirHelp.html @@ -1,8 +1,10 @@
Use this directory for resources you want to share between different installs or versions of MegaMek, MegaMekLab and MekHQ. Fonts, units, camos, portraits and unit fluff images will also be loaded from this directory (in addition to what is loaded from MegaMek's own data). The directory should be an absolute path such as D:/MyBTStuff (in other words, not relative to your MegaMek directory).
+Use this directory for resources you want to share between different installs or versions of MegaMek, MegaMekLab and MekHQ. The files listed below will also be loaded from this directory (in addition to what is loaded from MegaMek's own data). The directory should be an absolute path such as D:/MyBTStuff (in other words, not relative to your MegaMek directory).
-How to place files within the user data directory: +
Generally, all content from the user directory is added to the pre-defined content. In some cases, added content may replace pre-defined content when it has the same name or file path.
+ +How to place files within the user data directory:
+ Additionally, the user directory is used to restore your most recent session in MegaMekLab. + If you set the startup mode of MML to "Restore Tabs", a hidden folder called ".mml_tmp" will be created in your user directory. + You should not touch this folder. Changing the contents of this folder may cause MML to fail to start. + If you have "Restore Tabs" set and MML is failing to start, try deleting the ".mml_tmp" folder, in case it was corrupted somehow. +
+This is an example of a suitable directory structure with a few example files:
-
-D:/myBTStuff
- Oxanium.ttf
- Exo.ttf
- /campaign_units
- Atlas AS8-XT.mtf
- /data
- Jura.ttf
- /images
- /camo
- myForceCamo.png
- /oldcamo
- camo1.png
- camo2.png
- /portraits
- myPortrait1.png
- /fluff
- /Mek
- Atlas.png
- /DropShip
- Colossus.png
+
+ D:/myBTStuff
+ Oxanium.ttf
+ Exo.ttf
+ campaign_units/
+ Atlas AS8-XT.mtf
+ data/
+ Jura.ttf
+ MyMMSkin.xml
+ images/
+ camo/
+ myForceCamo.png
+ oldcamo/
+ camo1.png
+ camo2.png
+ portraits/
+ minscandboo.png
+ fluff/
+ Mek/
+ Atlas.png
+ DropShip/
+ Colossus.png
+ universe/
+ ranks.xml
+ awards/
+ MyAwards.xml
+ AuriganAwards.xml
+ .mml_tmp/
+ Here be dragons!
+
+ * The JTabbedPane doesn't come with any functionality for the user adding/removing tabs out of the box, + * so this is how we fake it. + */ + private void addNewTabButton() { + var editor = new BMMainUI(false, false); + editors.add(editor); + editor.refreshAll(); + editor.setOwner(this); + tabs.addTab("➕", editor.getContentPane()); + tabs.setTabComponentAt(tabs.getTabCount() - 1, new NewTabButton()); + tabs.setEnabledAt(tabs.getTabCount() - 1, false); + } + + /** + * The name is misleading, this is actually the Switch Unit Type operation! + * Replaces the current editor with a new blank one of the given unit type. + * Disposes of the old editor UI after the new one is initialized. + * + * @param type the type of unit to load for the new editor UI + * @param primitive whether the unit is primitive + * @param industrial whether the unit is an IndustrialMek + */ + private void newUnit(long type, boolean primitive, boolean industrial) { + var oldUi = editors.get(tabs.getSelectedIndex()); + var newUi = UiLoader.getUI(type, primitive, industrial); + editors.set(tabs.getSelectedIndex(), newUi); + tabs.setComponentAt(tabs.getSelectedIndex(), newUi.getContentPane()); + tabs.setTabComponentAt(tabs.getSelectedIndex(), new ClosableTab(newUi.getEntity().getDisplayName(), newUi)); + tabs.setEnabledAt(tabs.getSelectedIndex(), true); + oldUi.dispose(); + } + + /** + * The name is misleading, this is actually the Switch Unit Type operation! + * Replaces the current editor with a new blank one of the given unit type. + * Disposes of the old editor UI after the new one is initialized. + * + * @param type the type of unit to load for the new editor UI + * @param primitive whether the unit is primitive + */ + @Override + public void newUnit(long type, boolean primitive) { + newUnit(type, primitive, false); + } + + + /** + * Adds a new tab with the given unit to the tabbed user interface. + * + * @param entity The Entity object representing the unit to be added. + * @param filename The name of the file associated with the unit being added. + */ + public void addUnit(Entity entity, String filename) { + // Create a new "new tab" button, since we're about to replace the existing one + addNewTabButton(); + // Select the old "new tab" button... + tabs.setSelectedIndex(tabs.getTabCount() - 2); + // ...and replace it, since newUnit is actually the Switch Unit Type operation. + newUnit(UnitUtil.getEditorTypeForEntity(entity), entity.isPrimitive(), entity.isIndustrialMek()); + + currentEditor().setEntity(entity, filename); + currentEditor().reloadTabs(); + currentEditor().refreshAll(); + // Set the tab name + tabs.setTabComponentAt(tabs.getSelectedIndex(), new ClosableTab(entity.getDisplayName(), currentEditor())); + } + + @Override + public boolean exit() { + if (!currentEditor().safetyPrompt()) { + return false; + } + + CConfig.setParam(CConfig.GUI_FULLSCREEN, Integer.toString(getExtendedState())); + CConfig.setParam(CConfig.GUI_PLAF, UIManager.getLookAndFeel().getClass().getName()); + CConfig.writeMainUiWindowSettings(this); + CConfig.saveConfig(); + PreferenceManager.getInstance().save(); + MegaMek.getMMPreferences().saveToFile(MMLConstants.MM_PREFERENCES_FILE); + MegaMekLab.getMMLPreferences().saveToFile(MMLConstants.MML_PREFERENCES_FILE); + + if (CConfig.getStartUpType() == MMLStartUp.RESTORE_TABS) { + try { + TabStateUtil.saveTabState(editors.stream().limit(editors.size() - 1).toList()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + return true; + } + + private void restrictToScreenSize() { + DisplayMode currentMonitor = getGraphicsConfiguration().getDevice().getDisplayMode(); + int scaledMonitorW = UIUtil.getScaledScreenWidth(currentMonitor); + int scaledMonitorH = UIUtil.getScaledScreenHeight(currentMonitor); + int w = Math.min(getSize().width, scaledMonitorW); + int h = Math.min(getSize().height, scaledMonitorH); + setSize(new Dimension(w, h)); + } + + public void newTab() { + tabs.setEnabledAt(tabs.getTabCount() - 1, true); + tabs.setSelectedIndex(tabs.getTabCount() - 1); + tabs.setTabComponentAt( + tabs.getTabCount() - 1, + new ClosableTab(currentEditor().getEntity().getDisplayName(), currentEditor()) + ); + + addNewTabButton(); + } + + /** + * Deletes the current tab. + * This does not issue the safety prompt, it is up to the caller to do so! + */ + public void closeCurrentTab() { + closeTabAt(tabs.getSelectedIndex()); + } + + private void closeTabAt(int position) { + if (tabs.getTabCount() <= 2) { + MegaMekLabTabbedUI.this.dispatchEvent(new WindowEvent(MegaMekLabTabbedUI.this, WindowEvent.WINDOW_CLOSING)); + } + + var editor = editors.get(position); + + tabs.remove(editor.getContentPane()); + if (tabs.getSelectedIndex() == tabs.getTabCount() - 1) { + tabs.setSelectedIndex(tabs.getSelectedIndex() - 1); + } + editors.remove(editor); + closedEditors.push(editor); + // Tell the menu bar to enable the "reopen tab" shortcut + refreshMenuBar(); + } + + public void reopenTab() { + var editor = closedEditors.pop(); + if (editor != null) { + addTab(editor); + tabs.setSelectedIndex(tabs.getTabCount() - 2); + refreshMenuBar(); + } + } + + public boolean hasClosedTabs() { + return !closedEditors.isEmpty(); + } + + @Override + public JFrame getFrame() { + return this; + } + + @Override + public Entity getEntity() { + return currentEditor().getEntity(); + } + + @Override + public String getFileName() { + return currentEditor().getFileName(); + } + + @Override + public boolean hasEntityNameChanged() { + return currentEditor().hasEntityNameChanged(); + } + + @Override + public void refreshMenuBar() { + menuBar.refreshMenuBar(); + } + + @Override + public MenuBar getMMLMenuBar() { + return menuBar; + } + + + /** + * Represents a button used for creating new tabs in the MegaMekLabTabbedUI interface. + * Used to mimic functionality for adding new tabs in a tabbed user interface. + * Normally this tab should be disabled so it can't be navigated to, then when the + button is clicked + * the tab is replaced with a normal {@link ClosableTab}. + */ + private class NewTabButton extends JPanel { + public NewTabButton() { + setOpaque(false); + var button = new JButton("➕"); + button.setForeground(Color.GREEN); + button.setFont(Font.getFont("Symbola")); + button.setFocusable(false); + button.setBorder(BorderFactory.createEmptyBorder()); + + button.addActionListener(e -> { + newTab(); + }); + + add(button); + } + } + + /** + * Represents a custom tab component for use in a tabbed user interface, designed to display + * the name of a unit and provide a close button for removing the associated tab. + * The close button can be shift-clicked to skip the editor's safety prompt. + * This class extends JPanel and is initialized with a unit name and its associated editor instance. + */ + private class ClosableTab extends JPanel { + JLabel unitName; + JButton closeButton; + MegaMekLabMainUI editor; + + public ClosableTab(String name, MegaMekLabMainUI mainUI) { + unitName = new JLabel(name); + editor = mainUI; + + setOpaque(false); + + closeButton = new JButton("❌"); + closeButton.setFont(Font.getFont("Symbola")); + closeButton.setForeground(Color.RED); + closeButton.setFocusable(false); + closeButton.setBorder(BorderFactory.createEmptyBorder()); + closeButton.setToolTipText("Shift-click to skip the save confirmation dialog"); + add(unitName); + add(closeButton); + closeButton.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.isShiftDown() || editor.safetyPrompt()) { + closeTabAt(editors.indexOf(editor)); + } + } + }); + } + } + + /** + * ReopenTabStack is a utility class that manages a fixed-capacity stack of closed + * MegaMekLabMainUI editors. It allows for storing references to recently closed + * editors and retrieving them in reverse order of their closure, resembling + * a "reopen tab" functionality. + *
+ * This stack maintains a circular buffer of references with a maximum capacity
+ * defined by the constant STACK_CAPACITY. If the capacity is exceeded, the oldest
+ * editor will be disposed of and removed to make room for new entries.
+ */
+ private static class ReopenTabStack {
+ public static final int STACK_CAPACITY = 20;
+
+ private final MegaMekLabMainUI[] closedEditors = new MegaMekLabMainUI[STACK_CAPACITY];
+ private int size = 0;
+ private int start = 0;
+
+ public void push(MegaMekLabMainUI editor) {
+ int pos = start + size % closedEditors.length;
+ if (size == closedEditors.length) {
+ closedEditors[pos].dispose();
+ start++;
+ start %= closedEditors.length;
+ } else {
+ size++;
+ }
+ closedEditors[pos] = editor;
+ }
+
+ public MegaMekLabMainUI pop() {
+ if (size == 0) {
+ return null;
+ }
+ int pos = start + size - 1 % closedEditors.length;
+ var ret = closedEditors[pos];
+
+ closedEditors[pos] = null;
+ size--;
+
+ return ret;
+ }
+
+ public boolean isEmpty() {
+ return size == 0;
+ }
+ }
+}
diff --git a/megameklab/src/megameklab/ui/MenuBar.java b/megameklab/src/megameklab/ui/MenuBar.java
index 12462e116..4ff5dd196 100644
--- a/megameklab/src/megameklab/ui/MenuBar.java
+++ b/megameklab/src/megameklab/ui/MenuBar.java
@@ -77,14 +77,16 @@ public MenuBar(MenuBarOwner owner) {
/**
* Returns the unit main UI, if this menubar is attached to one (instead of the
- * StartupGUI
- * aka splash screen), null otherwise.
+ * StartupGUI aka splash screen), null otherwise.
+ * Under the Tabbed UI, returns the main UI for the currently selected tab.
*
* @return The unit main UI of this menubar or null
*/
public @Nullable MegaMekLabMainUI getUnitMainUi() {
if (owner instanceof MegaMekLabMainUI) {
return (MegaMekLabMainUI) owner;
+ } else if (owner instanceof MegaMekLabTabbedUI tabbedUI) {
+ return tabbedUI.currentEditor();
} else {
return null;
}
@@ -124,6 +126,33 @@ private JMenu createFileMenu() {
fileMenu.setName("fileMenu");
fileMenu.setMnemonic(KeyEvent.VK_F);
+ if (owner instanceof MegaMekLabTabbedUI tabbedUI) {
+ final JMenuItem miNewTab = new JMenuItem(resources.getString("miNewTab.text"));
+ miNewTab.setName("miNewTab");
+ miNewTab.setMnemonic(KeyEvent.VK_N);
+ miNewTab.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK));
+ miNewTab.addActionListener(e -> tabbedUI.newTab());
+ fileMenu.add(miNewTab);
+
+ final JMenuItem miCloseTab = new JMenuItem(resources.getString("miCloseTab.text"));
+ miCloseTab.setName("miCloseTab");
+ miCloseTab.setMnemonic(KeyEvent.VK_W);
+ miCloseTab.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK));
+ miCloseTab.addActionListener(e -> {
+ if (tabbedUI.safetyPrompt()) {
+ tabbedUI.closeCurrentTab();
+ }
+ });
+ fileMenu.add(miCloseTab);
+
+ final JMenuItem miReopenTab = new JMenuItem(resources.getString("miReopenTab.text"));
+ miReopenTab.setName("miReopenTab");
+ miReopenTab.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK));
+ miReopenTab.addActionListener(e -> tabbedUI.reopenTab());
+ miReopenTab.setEnabled(tabbedUI.hasClosedTabs());
+ fileMenu.add(miReopenTab);
+ }
+
final JMenuItem miResetCurrentUnit = new JMenuItem(resources.getString("miResetCurrentUnit.text"));
miResetCurrentUnit.setName("miResetCurrentUnit");
miResetCurrentUnit.setMnemonic(KeyEvent.VK_R);
@@ -166,7 +195,7 @@ private JMenu createFileMenu() {
private JMenu createSwitchUnitTypeMenu() {
final JMenu switchUnitTypeMenu = new JMenu(resources.getString("switchUnitTypeMenu.text"));
switchUnitTypeMenu.setName("switchUnitTypeMenu");
- switchUnitTypeMenu.setMnemonic(KeyEvent.VK_W);
+ switchUnitTypeMenu.setMnemonic(KeyEvent.VK_U);
final Entity entity = owner.getEntity();
@@ -652,7 +681,7 @@ private JMenu createThemesMenu() {
private JMenu createUnitValidationMenu() {
final JMenu unitValidationMenu = new JMenu(resources.getString("unitValidationMenu.text"));
unitValidationMenu.setName("unitValidationMenu");
- unitValidationMenu.setMnemonic(KeyEvent.VK_U);
+ unitValidationMenu.setMnemonic(KeyEvent.VK_V);
final JMenuItem miValidateCurrentUnit = new JMenuItem(resources.getString("CurrentUnit.text"));
miValidateCurrentUnit.setName("miValidateCurrentUnit");
@@ -1220,11 +1249,14 @@ public void loadFile(File unitFile) {
warnOnInvalid(loadedUnit);
newRecentUnit(unitFile.toString());
- if (isStartupGui() || (loadedUnit.getEntityType() != owner.getEntity().getEntityType())) {
+ if (owner instanceof MegaMekLabTabbedUI tabbedUi) {
+ tabbedUi.addUnit(loadedUnit, unitFile.toString());
+ refresh();
+ } else if (isStartupGui() || (loadedUnit.getEntityType() != owner.getEntity().getEntityType())) {
owner.getFrame().setVisible(false);
owner.getFrame().dispose();
UiLoader.loadUi(loadedUnit, unitFile.toString());
- } else {
+ } else if (owner instanceof MegaMekLabMainUI ){
getUnitMainUi().setEntity(loadedUnit, unitFile.toString());
UnitUtil.updateLoadedUnit(getUnitMainUi().getEntity());
reload();
diff --git a/megameklab/src/megameklab/ui/MenuBarOwner.java b/megameklab/src/megameklab/ui/MenuBarOwner.java
index 7f6699984..e4902f3ee 100644
--- a/megameklab/src/megameklab/ui/MenuBarOwner.java
+++ b/megameklab/src/megameklab/ui/MenuBarOwner.java
@@ -117,6 +117,7 @@ default void changeTheme(UIManager.LookAndFeelInfo lookAndFeelInfo) {
changeTheme(lookAndFeelInfo.getClassName());
}
+
/**
* Sets the look and feel for the application and lets Swing update the current
* components.
diff --git a/megameklab/src/megameklab/ui/StartupGUI.java b/megameklab/src/megameklab/ui/StartupGUI.java
index 2b14cc6da..88b0b5f13 100644
--- a/megameklab/src/megameklab/ui/StartupGUI.java
+++ b/megameklab/src/megameklab/ui/StartupGUI.java
@@ -277,7 +277,9 @@ public static void selectAndLoadUnitFromCache(MenuBarOwner previousFrame) {
}
CConfig.setMostRecentFile(fileName);
- if (!(previousFrame instanceof MegaMekLabMainUI)
+ if (previousFrame instanceof MegaMekLabTabbedUI tabbedUi) {
+ tabbedUi.addUnit(newUnit, fileName);
+ } else if (!(previousFrame instanceof MegaMekLabMainUI)
|| (newUnit.getEntityType() != previousFrame.getEntity().getEntityType())) {
previousFrame.getFrame().setVisible(false);
previousFrame.getFrame().dispose();
diff --git a/megameklab/src/megameklab/ui/dialog/UiLoader.java b/megameklab/src/megameklab/ui/dialog/UiLoader.java
index 7a774eff0..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;
@@ -31,7 +32,9 @@
import megamek.client.ui.swing.util.UIUtil;
import megamek.common.*;
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;
@@ -41,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;
/**
@@ -68,36 +72,20 @@ 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) {
- if ((newUnit == null) || (newUnit instanceof Mek)) {
- new UiLoader(Entity.ETYPE_MEK, false, false, newUnit, fileName).show();
- } else if (newUnit.isSupportVehicle()) {
- new UiLoader(Entity.ETYPE_SUPPORT_TANK, false, false, newUnit, fileName).show();
- } else if (newUnit.hasETypeFlag(Entity.ETYPE_SMALL_CRAFT)) {
- new UiLoader(Entity.ETYPE_DROPSHIP, newUnit.isPrimitive(), false, newUnit, fileName).show();
- } else if (newUnit.hasETypeFlag(Entity.ETYPE_JUMPSHIP)) {
- new UiLoader(Entity.ETYPE_JUMPSHIP, newUnit.isPrimitive(), false, newUnit, fileName).show();
- } else if ((newUnit instanceof Aero) && !(newUnit instanceof FixedWingSupport)) {
- new UiLoader(Entity.ETYPE_AERO, newUnit.isPrimitive(), false, newUnit, fileName).show();
- } else if (newUnit instanceof BattleArmor) {
- new UiLoader(Entity.ETYPE_BATTLEARMOR, false, false, newUnit, fileName).show();
- } else if (newUnit instanceof Infantry) {
- new UiLoader(Entity.ETYPE_INFANTRY, false, false, newUnit, fileName).show();
- } else if (newUnit instanceof ProtoMek) {
- new UiLoader(Entity.ETYPE_PROTOMEK, false, false, newUnit, fileName).show();
- } else if ((newUnit instanceof Tank) && !(newUnit instanceof GunEmplacement)) {
- new UiLoader(Entity.ETYPE_TANK, false, false, newUnit, fileName).show();
- } else {
- PopupMessages.showUiLoadError(null);
- new UiLoader(Entity.ETYPE_MEK, false, false, null, "").show();
- }
+ new UiLoader(UnitUtil.getEditorTypeForEntity(newUnit), newUnit.isPrimitive(), newUnit.isIndustrialMek(), newUnit, fileName).show();
}
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
@@ -122,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.
@@ -132,16 +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();
}
- newUI.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..ecce9602b
--- /dev/null
+++ b/megameklab/src/megameklab/ui/util/TabStateUtil.java
@@ -0,0 +1,194 @@
+/*
+ * 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