From 05a9996341063c345d032608a93fd05ac0c485f3 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Mon, 30 Dec 2024 18:01:32 -0800 Subject: [PATCH 01/15] Open multiple units in tabs --- .../src/megameklab/ui/MegaMekLabMainUI.java | 13 ++ .../src/megameklab/ui/MegaMekLabTabbedUI.java | 171 ++++++++++++++++++ megameklab/src/megameklab/ui/MenuBar.java | 5 +- .../src/megameklab/ui/MenuBarOwner.java | 1 + megameklab/src/megameklab/ui/StartupGUI.java | 4 +- .../src/megameklab/ui/dialog/UiLoader.java | 4 +- megameklab/src/megameklab/util/CConfig.java | 22 ++- 7 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java diff --git a/megameklab/src/megameklab/ui/MegaMekLabMainUI.java b/megameklab/src/megameklab/ui/MegaMekLabMainUI.java index 8bee05b13..64a4393b5 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabMainUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabMainUI.java @@ -35,6 +35,7 @@ public abstract class MegaMekLabMainUI extends JFrame implements RefreshListener protected MenuBar mmlMenuBar; protected boolean refreshRequired = false; private String originalName = ""; + private MegaMekLabTabbedUI owner = null; public MegaMekLabMainUI() { setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); @@ -203,4 +204,16 @@ public boolean hasEntityNameChanged() { public MenuBar getMMLMenuBar() { return mmlMenuBar; } + + public void setOwner(MegaMekLabTabbedUI owner) { + this.owner = owner; + } + + @Override + public void setTitle(String title) { + super.setTitle(title); + if (owner != null) { + owner.setTabName(title); + } + } } diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java new file mode 100644 index 000000000..b68a2341d --- /dev/null +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -0,0 +1,171 @@ +package megameklab.ui; + +import megamek.MegaMek; +import megamek.client.ui.swing.util.UIUtil; +import megamek.common.Entity; +import megamek.common.preference.PreferenceManager; +import megameklab.MMLConstants; +import megameklab.MegaMekLab; +import megameklab.ui.dialog.UiLoader; +import megameklab.ui.mek.BMMainUI; +import megameklab.ui.util.ExitOnWindowClosingListener; +import megameklab.util.CConfig; +import megameklab.util.MMLFileDropTransferHandler; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +public class MegaMekLabTabbedUI extends JFrame implements MenuBarOwner { + private List editors = new ArrayList<>(); + + private JTabbedPane tabs = new JTabbedPane(); + + private MenuBar menuBar; + + public MegaMekLabTabbedUI(MegaMekLabMainUI... entities) { + super("MegaMekLab"); + + if (entities.length == 0) { + throw new IllegalArgumentException("At least one entity must be provided"); + } + + for (MegaMekLabMainUI e : entities) { + addTab(e); + + } + addTab(new BMMainUI(false, false), "+"); + tabs.addChangeListener(new TabsChangedListener()); + setContentPane(tabs); + + menuBar = new MenuBar(this); + setJMenuBar(menuBar); + + setTransferHandler(new MMLFileDropTransferHandler(this)); + + + pack(); + setSize(800, 600); // todo remember window size + restrictToScrenSize(); + setLocationRelativeTo(null); + + CConfig.getMainUiWindowSize(this).ifPresent(this::setSize); + CConfig.getMainUiWindowPosition(this).ifPresent(this::setLocation); + + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + addWindowListener(new ExitOnWindowClosingListener(this)); + setExtendedState(CConfig.getIntParam(CConfig.GUI_FULLSCREEN)); + } + + private MegaMekLabMainUI currentEditor() { + return editors.get(tabs.getSelectedIndex()); + } + + @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; + } + + private class TabsChangedListener implements ChangeListener { + @Override + public void stateChanged(ChangeEvent e) { + if (tabs.getSelectedIndex() == editors.size() - 1) { + tabs.setTitleAt(tabs.getSelectedIndex(), editors.get(tabs.getSelectedIndex()).getEntity().getDisplayName()); + + addTab(new BMMainUI(false, false), "+"); + } + } + } + + public void setTabName(String tabName) { + tabs.setTitleAt(tabs.getSelectedIndex(), tabName); + } + + private void addTab(MegaMekLabMainUI editor) { + addTab(editor, editor.getEntity().getDisplayName()); + } + + private void addTab(MegaMekLabMainUI editor, String name) { + editors.add(editor); + editor.refreshAll(); + editor.setOwner(this); + tabs.addTab(name, editor.getContentPane()); + } + + @Override + public void newUnit(long type, boolean primitive) { + var oldUi = editors.get(tabs.getSelectedIndex()); + var newUi = UiLoader.getUI(type, primitive, true); + editors.set(tabs.getSelectedIndex(), newUi); + tabs.setComponentAt(tabs.getSelectedIndex(), newUi.getContentPane()); + tabs.setTitleAt(tabs.getSelectedIndex(), newUi.getEntity().getDisplayName()); + + oldUi.dispose(); + } + + public void addEditor(Entity entity, String filename) { + var newUi = UiLoader.getUI(entity.getUnitType(), entity.isPrimitive(), entity.isIndustrialMek()); + newUi.setEntity(entity, filename); + newUi.reloadTabs(); + newUi.refreshAll(); + tabs.setSelectedIndex(tabs.getTabCount() - 1); + var oldUi = editors.get(tabs.getSelectedIndex()); + editors.set(tabs.getSelectedIndex(), newUi); + tabs.setComponentAt(tabs.getSelectedIndex(), newUi.getContentPane()); + tabs.setTitleAt(tabs.getSelectedIndex(), newUi.getEntity().getDisplayName()); + oldUi.dispose(); + } + + @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); + return true; + } + + private void restrictToScrenSize() { + 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)); + } +} diff --git a/megameklab/src/megameklab/ui/MenuBar.java b/megameklab/src/megameklab/ui/MenuBar.java index 12462e116..020a6f001 100644 --- a/megameklab/src/megameklab/ui/MenuBar.java +++ b/megameklab/src/megameklab/ui/MenuBar.java @@ -1224,11 +1224,14 @@ public void loadFile(File unitFile) { 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(); refresh(); + } else if (owner instanceof MegaMekLabTabbedUI tabbedUi) { + tabbedUi.addEditor(loadedUnit, unitFile.toString()); + refresh(); } } catch (Exception ex) { PopupMessages.showFileReadError(owner.getFrame(), unitFile.toString(), ex.getMessage()); 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..81737817c 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.addEditor(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..78ac579fb 100644 --- a/megameklab/src/megameklab/ui/dialog/UiLoader.java +++ b/megameklab/src/megameklab/ui/dialog/UiLoader.java @@ -31,6 +31,7 @@ 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.battleArmor.BAMainUI; import megameklab.ui.combatVehicle.CVMainUI; @@ -139,7 +140,8 @@ private void loadNewUi() { newUI.reloadTabs(); newUI.refreshAll(); } - newUI.setVisible(true); + var tabbedUi = new MegaMekLabTabbedUI(newUI); + tabbedUi.setVisible(true); splashImage.setVisible(false); splashImage.dispose(); } diff --git a/megameklab/src/megameklab/util/CConfig.java b/megameklab/src/megameklab/util/CConfig.java index 018d1df4e..c7c736ad5 100644 --- a/megameklab/src/megameklab/util/CConfig.java +++ b/megameklab/src/megameklab/util/CConfig.java @@ -36,15 +36,13 @@ import megamek.common.Configuration; import megamek.logging.MMLogger; import megameklab.printing.MekChassisArrangement; -import megameklab.ui.MMLStartUp; -import megameklab.ui.MegaMekLabMainUI; -import megameklab.ui.MenuBarOwner; -import megameklab.ui.PopupMessages; +import megameklab.ui.*; import megameklab.ui.battleArmor.BAMainUI; import megameklab.ui.combatVehicle.CVMainUI; import megameklab.ui.fighterAero.ASMainUI; import megameklab.ui.infantry.CIMainUI; import megameklab.ui.largeAero.DSMainUI; +import megameklab.ui.largeAero.WSMainUI; import megameklab.ui.mek.BMMainUI; import megameklab.ui.protoMek.PMMainUI; import megameklab.ui.supportVehicle.SVMainUI; @@ -84,6 +82,7 @@ public final class CConfig { public static final String GUI_CI_MAINUI_WINDOW = "CIWindow"; public static final String GUI_DS_MAINUI_WINDOW = "DSWindow"; public static final String GUI_WS_MAINUI_WINDOW = "WSWindow"; + public static final String GUI_TABBED_WINDOW = "TabbedWindow"; public static final int RECENT_FILE_COUNT = 10; public static final String FILE_RECENT_PREFIX = "Save_File_"; @@ -431,16 +430,16 @@ public static void writeFileChooserSettings(JDialog dialog) { writeWindowSettings(FILE_CHOOSER_WINDOW, dialog); } - public static Optional getMainUiWindowSize(MegaMekLabMainUI mainUi) { + public static Optional getMainUiWindowSize(MenuBarOwner mainUi) { return getWindowSize(settingForMainUi(mainUi)); } - public static Optional getMainUiWindowPosition(MegaMekLabMainUI mainUi) { + public static Optional getMainUiWindowPosition(MenuBarOwner mainUi) { return getWindowPosition(settingForMainUi(mainUi)); } - public static void writeMainUiWindowSettings(MegaMekLabMainUI mainUi) { - writeWindowSettings(settingForMainUi(mainUi), mainUi); + public static void writeMainUiWindowSettings(MenuBarOwner mainUi) { + writeWindowSettings(settingForMainUi(mainUi), (Component) mainUi); } public static String getRecentFile(int recentFileNumber) { @@ -501,7 +500,7 @@ private static void writeWindowSettings(String cconfigSetting, Component compone saveConfig(); } - private static String settingForMainUi(MegaMekLabMainUI ui) { + private static String settingForMainUi(MenuBarOwner ui) { if (ui instanceof BMMainUI) { return GUI_BM_MAINUI_WINDOW; } else if (ui instanceof CVMainUI) { @@ -518,9 +517,12 @@ private static String settingForMainUi(MegaMekLabMainUI ui) { return GUI_CI_MAINUI_WINDOW; } else if (ui instanceof SVMainUI) { return GUI_SV_MAINUI_WINDOW; - } else { + } else if (ui instanceof WSMainUI) { return GUI_WS_MAINUI_WINDOW; + } else if (ui instanceof MegaMekLabTabbedUI) { + return GUI_TABBED_WINDOW; } + throw new IllegalArgumentException("Unknown MenuBarOwner: " + ui.getClass().getName()); } private static void applyImportedSettings(MenuBarOwner menuBarOwner) { From 1c05c5798db3dfdb1e0e976ae71c24a1e4f698b5 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Mon, 30 Dec 2024 20:06:31 -0800 Subject: [PATCH 02/15] Make tabs closable, and set tab name when unit name changes --- .../src/megameklab/ui/MegaMekLabMainUI.java | 12 +- .../src/megameklab/ui/MegaMekLabTabbedUI.java | 112 +++++++++++++----- .../src/megameklab/ui/dialog/UiLoader.java | 23 +--- megameklab/src/megameklab/util/UnitUtil.java | 25 ++++ 4 files changed, 113 insertions(+), 59 deletions(-) diff --git a/megameklab/src/megameklab/ui/MegaMekLabMainUI.java b/megameklab/src/megameklab/ui/MegaMekLabMainUI.java index 64a4393b5..6c7c99777 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabMainUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabMainUI.java @@ -129,6 +129,10 @@ public boolean exit() { public void refreshHeader() { String fileInfo = fileName.isBlank() ? "" : " (" + fileName + ")"; setTitle(getEntity().getFullChassis() + " " + getEntity().getModel() + fileInfo); + if (owner != null) { + getEntity().generateDisplayName(); + owner.setTabName(getEntity().getDisplayName()); + } } @Override @@ -208,12 +212,4 @@ public MenuBar getMMLMenuBar() { public void setOwner(MegaMekLabTabbedUI owner) { this.owner = owner; } - - @Override - public void setTitle(String title) { - super.setTitle(title); - if (owner != null) { - owner.setTabName(title); - } - } } diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index b68a2341d..3c6f693d9 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -11,15 +11,17 @@ import megameklab.ui.util.ExitOnWindowClosingListener; import megameklab.util.CConfig; import megameklab.util.MMLFileDropTransferHandler; +import megameklab.util.UnitUtil; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.*; +import java.awt.event.*; import java.util.ArrayList; import java.util.List; -public class MegaMekLabTabbedUI extends JFrame implements MenuBarOwner { +public class MegaMekLabTabbedUI extends JFrame implements MenuBarOwner, ChangeListener { private List editors = new ArrayList<>(); private JTabbedPane tabs = new JTabbedPane(); @@ -33,12 +35,14 @@ public MegaMekLabTabbedUI(MegaMekLabMainUI... entities) { throw new IllegalArgumentException("At least one entity must be provided"); } + tabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); + for (MegaMekLabMainUI e : entities) { addTab(e); } - addTab(new BMMainUI(false, false), "+"); - tabs.addChangeListener(new TabsChangedListener()); + addNewButton(); + tabs.addChangeListener(this); setContentPane(tabs); menuBar = new MenuBar(this); @@ -94,54 +98,61 @@ public MenuBar getMMLMenuBar() { return menuBar; } - private class TabsChangedListener implements ChangeListener { - @Override - public void stateChanged(ChangeEvent e) { - if (tabs.getSelectedIndex() == editors.size() - 1) { - tabs.setTitleAt(tabs.getSelectedIndex(), editors.get(tabs.getSelectedIndex()).getEntity().getDisplayName()); - - addTab(new BMMainUI(false, false), "+"); - } + @Override + public void stateChanged(ChangeEvent e) { + if (tabs.getSelectedIndex() == editors.size() - 1) { + tabs.setTabComponentAt( + tabs.getSelectedIndex(), + new ClosableTab(currentEditor().getEntity().getDisplayName(), currentEditor()) + ); + + addNewButton(); } } public void setTabName(String tabName) { - tabs.setTitleAt(tabs.getSelectedIndex(), tabName); + tabs.setTabComponentAt(tabs.getSelectedIndex(), new ClosableTab(tabName, currentEditor()) ); } private void addTab(MegaMekLabMainUI editor) { - addTab(editor, editor.getEntity().getDisplayName()); + editors.add(editor); + editor.refreshAll(); + editor.setOwner(this); + tabs.addTab(editor.getEntity().getDisplayName(), editor.getContentPane()); + tabs.setTabComponentAt(tabs.getTabCount() - 1, new ClosableTab(editor.getEntity().getDisplayName(), editor)); } - private void addTab(MegaMekLabMainUI editor, String name) { + private void addNewButton() { + var editor = new BMMainUI(false, false); editors.add(editor); editor.refreshAll(); editor.setOwner(this); - tabs.addTab(name, editor.getContentPane()); + tabs.addTab("➕", editor.getContentPane()); + tabs.setTabComponentAt(tabs.getTabCount() - 1, new NewTabButton()); } - @Override - public void newUnit(long type, boolean primitive) { + private void newUnit(long type, boolean primitive, boolean industrial) { var oldUi = editors.get(tabs.getSelectedIndex()); - var newUi = UiLoader.getUI(type, primitive, true); + var newUi = UiLoader.getUI(type, primitive, industrial); editors.set(tabs.getSelectedIndex(), newUi); tabs.setComponentAt(tabs.getSelectedIndex(), newUi.getContentPane()); - tabs.setTitleAt(tabs.getSelectedIndex(), newUi.getEntity().getDisplayName()); + tabs.setTabComponentAt(tabs.getSelectedIndex(), new ClosableTab(newUi.getEntity().getDisplayName(), newUi)); oldUi.dispose(); } + @Override + public void newUnit(long type, boolean primitive) { + newUnit(type, primitive, false); + } + public void addEditor(Entity entity, String filename) { - var newUi = UiLoader.getUI(entity.getUnitType(), entity.isPrimitive(), entity.isIndustrialMek()); - newUi.setEntity(entity, filename); - newUi.reloadTabs(); - newUi.refreshAll(); - tabs.setSelectedIndex(tabs.getTabCount() - 1); - var oldUi = editors.get(tabs.getSelectedIndex()); - editors.set(tabs.getSelectedIndex(), newUi); - tabs.setComponentAt(tabs.getSelectedIndex(), newUi.getContentPane()); - tabs.setTitleAt(tabs.getSelectedIndex(), newUi.getEntity().getDisplayName()); - oldUi.dispose(); + addNewButton(); + tabs.setSelectedIndex(tabs.getTabCount() - 2); + newUnit(UnitUtil.getEditorTypeForEntity(entity), entity.isPrimitive(), entity.isIndustrialMek()); + currentEditor().setEntity(entity, filename); + currentEditor().reloadTabs(); + currentEditor().refreshAll(); } @Override @@ -168,4 +179,47 @@ private void restrictToScrenSize() { int h = Math.min(getSize().height, scaledMonitorH); setSize(new Dimension(w, h)); } + + private static class NewTabButton extends JPanel { + public NewTabButton() { + setOpaque(false); + var label = new JLabel("➕"); + label.setForeground(Color.GREEN); + add(label); + } + } + + 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.setForeground(Color.RED); + closeButton.setFocusable(false); + closeButton.setBorder(BorderFactory.createEmptyBorder()); + add(unitName); + add(closeButton); + closeButton.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.isShiftDown() || editor.safetyPrompt()) { + tabs.remove(editor.getContentPane()); + if (tabs.getSelectedIndex() == tabs.getTabCount() - 1 && tabs.getTabCount() > 1) { + tabs.setSelectedIndex(tabs.getSelectedIndex() - 1); + } + editors.remove(editor); + stateChanged(new ChangeEvent(tabs)); + editor.dispose(); + } + } + }); + } + } } diff --git a/megameklab/src/megameklab/ui/dialog/UiLoader.java b/megameklab/src/megameklab/ui/dialog/UiLoader.java index 78ac579fb..4c6e111fc 100644 --- a/megameklab/src/megameklab/ui/dialog/UiLoader.java +++ b/megameklab/src/megameklab/ui/dialog/UiLoader.java @@ -71,28 +71,7 @@ public class UiLoader { private final String fileName; 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) { diff --git a/megameklab/src/megameklab/util/UnitUtil.java b/megameklab/src/megameklab/util/UnitUtil.java index 61e2502d7..f51502766 100644 --- a/megameklab/src/megameklab/util/UnitUtil.java +++ b/megameklab/src/megameklab/util/UnitUtil.java @@ -64,6 +64,7 @@ import megamek.common.weapons.srms.StreakSRMWeapon; import megamek.logging.MMLogger; import megameklab.ui.PopupMessages; +import megameklab.ui.dialog.UiLoader; public class UnitUtil { private static final MMLogger logger = MMLogger.create(UnitUtil.class); @@ -2140,4 +2141,28 @@ static boolean isNonMekOrTankWeapon(Entity unit, WeaponType weapon) { } return false; } + + public static long getEditorTypeForEntity(Entity newUnit) { + if ((newUnit == null) || (newUnit instanceof Mek)) { + return Entity.ETYPE_MEK; + } else if (newUnit.isSupportVehicle()) { + return Entity.ETYPE_SUPPORT_TANK; + } else if (newUnit.hasETypeFlag(Entity.ETYPE_SMALL_CRAFT)) { + return Entity.ETYPE_DROPSHIP; + } else if (newUnit.hasETypeFlag(Entity.ETYPE_JUMPSHIP)) { + return Entity.ETYPE_JUMPSHIP; + } else if ((newUnit instanceof Aero) && !(newUnit instanceof FixedWingSupport)) { + return Entity.ETYPE_AERO; + } else if (newUnit instanceof BattleArmor) { + return Entity.ETYPE_BATTLEARMOR; + } else if (newUnit instanceof Infantry) { + return Entity.ETYPE_INFANTRY; + } else if (newUnit instanceof ProtoMek) { + return Entity.ETYPE_PROTOMEK; + } else if ((newUnit instanceof Tank) && !(newUnit instanceof GunEmplacement)) { + return Entity.ETYPE_TANK; + } else { + throw new IllegalArgumentException("Cannot open this entity in an editor"); + } + } } From 87518552b356d469a86686ad50c50a2ef5736013 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Mon, 30 Dec 2024 20:08:41 -0800 Subject: [PATCH 03/15] Missing license file --- .../src/megameklab/ui/MegaMekLabTabbedUI.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index 3c6f693d9..e650f0e06 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -1,3 +1,22 @@ +/* + * 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; import megamek.MegaMek; From f369abd050e6c633141d4ee29a75a3dd987abf6c Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Mon, 30 Dec 2024 22:17:53 -0800 Subject: [PATCH 04/15] tooltip --- megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java | 1 + 1 file changed, 1 insertion(+) diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index e650f0e06..5dbce8f01 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -223,6 +223,7 @@ public ClosableTab(String name, MegaMekLabMainUI mainUI) { 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() { From d550d44d7f6329a6356cba54460deff9f5259555 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Tue, 31 Dec 2024 12:31:16 -0800 Subject: [PATCH 05/15] Properly rename tab --- megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java | 1 + 1 file changed, 1 insertion(+) diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index 5dbce8f01..35014078c 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -172,6 +172,7 @@ public void addEditor(Entity entity, String filename) { currentEditor().setEntity(entity, filename); currentEditor().reloadTabs(); currentEditor().refreshAll(); + tabs.setTabComponentAt(tabs.getSelectedIndex(), new ClosableTab(entity.getDisplayName(), currentEditor())); } @Override From 546a44452af563e893f8ac4b77ce8cadaae89f4e Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Tue, 31 Dec 2024 12:51:21 -0800 Subject: [PATCH 06/15] Redirect certain menu bar operations (e.g. saving) to the currently selected tab --- megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java | 3 +-- megameklab/src/megameklab/ui/MenuBar.java | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index 35014078c..7252f1e5d 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -71,7 +71,6 @@ public MegaMekLabTabbedUI(MegaMekLabMainUI... entities) { pack(); - setSize(800, 600); // todo remember window size restrictToScrenSize(); setLocationRelativeTo(null); @@ -83,7 +82,7 @@ public MegaMekLabTabbedUI(MegaMekLabMainUI... entities) { setExtendedState(CConfig.getIntParam(CConfig.GUI_FULLSCREEN)); } - private MegaMekLabMainUI currentEditor() { + public MegaMekLabMainUI currentEditor() { return editors.get(tabs.getSelectedIndex()); } diff --git a/megameklab/src/megameklab/ui/MenuBar.java b/megameklab/src/megameklab/ui/MenuBar.java index 020a6f001..4b215eee0 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; } From b62d04c4691fa94f4a4c4f5680f91a5ef1334875 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Tue, 31 Dec 2024 18:20:59 -0800 Subject: [PATCH 07/15] Add comments to TabbedUI --- .../src/megameklab/ui/MegaMekLabTabbedUI.java | 169 ++++++++++++++---- megameklab/src/megameklab/ui/MenuBar.java | 2 +- megameklab/src/megameklab/ui/StartupGUI.java | 2 +- 3 files changed, 137 insertions(+), 36 deletions(-) diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index 7252f1e5d..2bb323648 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -40,6 +40,10 @@ import java.util.ArrayList; import java.util.List; +/** + * Replaces {@link MegaMekLabMainUI} as the top-level window for MML. + * Holds several {@link MegaMekLabMainUI}s as tabs, allowing many units to be open at once. + */ public class MegaMekLabTabbedUI extends JFrame implements MenuBarOwner, ChangeListener { private List editors = new ArrayList<>(); @@ -47,77 +51,75 @@ public class MegaMekLabTabbedUI extends JFrame implements MenuBarOwner, ChangeLi private MenuBar menuBar; + /** + * Constructs a new MegaMekLabTabbedUI instance, which serves as the main tabbed UI + * for managing multiple MegaMekLabMainUI editors. Automatically initializes a default + * BMMainUI instance if no entities are provided. + * + * @param entities A variable number of MegaMekLabMainUI instances that will be added + * as tabs to the UI. If no entities are provided, a default BMMainUI + * instance will be created and added. + */ public MegaMekLabTabbedUI(MegaMekLabMainUI... entities) { super("MegaMekLab"); - if (entities.length == 0) { - throw new IllegalArgumentException("At least one entity must be provided"); + // 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); + + // Add the given editors as tabs, then add the New Button. + // The New Button is actually just another blank Mek with the name "+", + // There is nothing special about it for (MegaMekLabMainUI e : entities) { addTab(e); - } addNewButton(); + + tabs.addChangeListener(this); setContentPane(tabs); menuBar = new MenuBar(this); setJMenuBar(menuBar); + // Enable opening unit and mul files by drag-and-drop setTransferHandler(new MMLFileDropTransferHandler(this)); - + // Remember the size and position of the window from last time MML was launched pack(); restrictToScrenSize(); setLocationRelativeTo(null); - CConfig.getMainUiWindowSize(this).ifPresent(this::setSize); CConfig.getMainUiWindowPosition(this).ifPresent(this::setLocation); + // ...and save that size nad position on exit setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); addWindowListener(new ExitOnWindowClosingListener(this)); setExtendedState(CConfig.getIntParam(CConfig.GUI_FULLSCREEN)); } + /** + * Retrieves the currently selected editor from the tabbed user interface. + * + * @return The currently selected MegaMekLabMainUI instance, which represents the + * active editor in the tabbed UI. + */ public MegaMekLabMainUI currentEditor() { return editors.get(tabs.getSelectedIndex()); } - @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; - } @Override public void stateChanged(ChangeEvent e) { + // This watches for the user selecting the New Tab button, which is actually a normal tab. + // When they select it, we quickly rename the tab to "New Mek" and add a new "New Tab" button onto the end. if (tabs.getSelectedIndex() == editors.size() - 1) { tabs.setTabComponentAt( tabs.getSelectedIndex(), @@ -128,18 +130,41 @@ public void stateChanged(ChangeEvent e) { } } + /** + * Updates the name of the currently selected tab in the tabbed user interface. + * Should typically be called when the name of the unit being edited changes. + * + * @param tabName The new name to be set for the currently selected tab. + */ public void setTabName(String tabName) { + // ClosableTab is a label with the unit name, and a close button. + // If we didn't need that close button, this could be tabs.setTitleAt tabs.setTabComponentAt(tabs.getSelectedIndex(), new ClosableTab(tabName, currentEditor()) ); } + /** + * Adds a new editor tab to the tabbed UI. This includes adding the editor + * to the internal editor collection, refreshing it, setting the ownership, + * and adding the tab to the tabs UI. + * + * @param editor The MegaMekLabMainUI instance to be added as a new tab. + */ private void addTab(MegaMekLabMainUI editor) { editors.add(editor); editor.refreshAll(); editor.setOwner(this); tabs.addTab(editor.getEntity().getDisplayName(), editor.getContentPane()); + // See ClosableTab later in this file for what's going on here. tabs.setTabComponentAt(tabs.getTabCount() - 1, new ClosableTab(editor.getEntity().getDisplayName(), editor)); } + /** + * Similar to addTab above, this adds a blank Mek editor, but with the name "➕" + * so that it looks like a button for creating a new tab. + *

+ * 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 addNewButton() { var editor = new BMMainUI(false, false); editors.add(editor); @@ -149,6 +174,15 @@ private void addNewButton() { tabs.setTabComponentAt(tabs.getTabCount() - 1, new NewTabButton()); } + /** + * 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); @@ -159,18 +193,38 @@ private void newUnit(long type, boolean primitive, boolean industrial) { 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); } - public void addEditor(Entity entity, String filename) { + + /** + * 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 addNewButton(); + // 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())); } @@ -199,6 +253,47 @@ private void restrictToScrenSize() { setSize(new Dimension(w, h)); } + + // + @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. + * This class extends JPanel and is rendered as a non-opaque panel containing a "+" symbol. + * Used to mimic functionality for adding new tabs in a tabbed user interface. + *

+ * The only reason this is a separate class instead of just the text for a tab is so that it can be green. + */ private static class NewTabButton extends JPanel { public NewTabButton() { setOpaque(false); @@ -208,6 +303,12 @@ public NewTabButton() { } } + /** + * 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; diff --git a/megameklab/src/megameklab/ui/MenuBar.java b/megameklab/src/megameklab/ui/MenuBar.java index 4b215eee0..a69246852 100644 --- a/megameklab/src/megameklab/ui/MenuBar.java +++ b/megameklab/src/megameklab/ui/MenuBar.java @@ -1232,7 +1232,7 @@ public void loadFile(File unitFile) { reload(); refresh(); } else if (owner instanceof MegaMekLabTabbedUI tabbedUi) { - tabbedUi.addEditor(loadedUnit, unitFile.toString()); + tabbedUi.addUnit(loadedUnit, unitFile.toString()); refresh(); } } catch (Exception ex) { diff --git a/megameklab/src/megameklab/ui/StartupGUI.java b/megameklab/src/megameklab/ui/StartupGUI.java index 81737817c..88b0b5f13 100644 --- a/megameklab/src/megameklab/ui/StartupGUI.java +++ b/megameklab/src/megameklab/ui/StartupGUI.java @@ -278,7 +278,7 @@ public static void selectAndLoadUnitFromCache(MenuBarOwner previousFrame) { CConfig.setMostRecentFile(fileName); if (previousFrame instanceof MegaMekLabTabbedUI tabbedUi) { - tabbedUi.addEditor(newUnit, fileName); + tabbedUi.addUnit(newUnit, fileName); } else if (!(previousFrame instanceof MegaMekLabMainUI) || (newUnit.getEntityType() != previousFrame.getEntity().getEntityType())) { previousFrame.getFrame().setVisible(false); From 8d8ecf5240c7da5952b6e721aa86af261b151421 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Wed, 1 Jan 2025 04:26:57 -0800 Subject: [PATCH 08/15] 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() {} +} From fdf0bcd2032d28704acb8b48a327d05068153f76 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Wed, 1 Jan 2025 04:32:41 -0800 Subject: [PATCH 09/15] Fix load from file to not destroy all tabs --- megameklab/src/megameklab/ui/MenuBar.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/megameklab/src/megameklab/ui/MenuBar.java b/megameklab/src/megameklab/ui/MenuBar.java index a69246852..bd543b854 100644 --- a/megameklab/src/megameklab/ui/MenuBar.java +++ b/megameklab/src/megameklab/ui/MenuBar.java @@ -1222,7 +1222,10 @@ 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()); @@ -1231,9 +1234,6 @@ public void loadFile(File unitFile) { UnitUtil.updateLoadedUnit(getUnitMainUi().getEntity()); reload(); refresh(); - } else if (owner instanceof MegaMekLabTabbedUI tabbedUi) { - tabbedUi.addUnit(loadedUnit, unitFile.toString()); - refresh(); } } catch (Exception ex) { PopupMessages.showFileReadError(owner.getFrame(), unitFile.toString(), ex.getMessage()); From 4f1ec425dd81911d6453d6feefe721ff254ebfda Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Wed, 1 Jan 2025 04:49:31 -0800 Subject: [PATCH 10/15] Don't save tab state when startup behaviour isn't Restore Tabs --- megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index 8e77a1083..93148bb5a 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -239,11 +239,12 @@ public boolean exit() { 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); + 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; From fd82a32c52c399c14e1150f6dec91b972751536e Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Wed, 1 Jan 2025 12:37:02 -0800 Subject: [PATCH 11/15] Set font to Symbola for symbols --- megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index 93148bb5a..9ca7d2145 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -305,6 +305,7 @@ public NewTabButton() { setOpaque(false); var label = new JLabel("➕"); label.setForeground(Color.GREEN); + label.setFont(Font.getFont("Symbola")); add(label); } } @@ -327,6 +328,7 @@ public ClosableTab(String name, MegaMekLabMainUI mainUI) { setOpaque(false); closeButton = new JButton("❌"); + closeButton.setFont(Font.getFont("Symbola")); closeButton.setForeground(Color.RED); closeButton.setFocusable(false); closeButton.setBorder(BorderFactory.createEmptyBorder()); From 93b1b611dbfffee3c1817d2e84cc7de01eab5760 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Wed, 1 Jan 2025 13:47:07 -0800 Subject: [PATCH 12/15] Prevent keyboard navigation to "new tab" button --- megameklab/docs/UserDirHelp.html | 71 +++++++++++++------ .../src/megameklab/ui/MegaMekLabTabbedUI.java | 70 +++++++++--------- 2 files changed, 82 insertions(+), 59 deletions(-) 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 @@

How to Use the User Data Directory

-

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:

  • Fonts can be placed anywhere in the user data directory (i.e., in the directory itself or in any subfolder) @@ -13,27 +15,50 @@
  • Portrait images must be placed in <user data directory>/data/images/portraits/. In this subfolder, further subfolders can be used (optional).
  • Unit fluff images must be placed in <user data directory>/data/images/fluff/<unit type>/. To find the exact subfolders to use, go to your megamek folder and open /data/images/fluff/. + +
  • Skin definition files (.xml) can be placed anywhere in the user data directory + +
  • Rank Systems may be defined in <user data directory>/data/universe/ranks.xml + +
  • Award definition files (.xml) can be placed in <user data directory>/data/universe/awards
+

+ 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!
+

+ diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index 9ca7d2145..bf40359ac 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -35,7 +35,6 @@ import javax.swing.*; import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; import java.awt.*; import java.awt.event.*; import java.io.IOException; @@ -46,7 +45,7 @@ * Replaces {@link MegaMekLabMainUI} as the top-level window for MML. * Holds several {@link MegaMekLabMainUI}s as tabs, allowing many units to be open at once. */ -public class MegaMekLabTabbedUI extends JFrame implements MenuBarOwner, ChangeListener { +public class MegaMekLabTabbedUI extends JFrame implements MenuBarOwner { private List editors = new ArrayList<>(); private JTabbedPane tabs = new JTabbedPane(); @@ -70,16 +69,12 @@ public MegaMekLabTabbedUI(MegaMekLabMainUI... entities) { tabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); - // Add the given editors as tabs, then add the New Button. - // The New Button is actually just another blank Mek with the name "+", - // There is nothing special about it + // Add the given editors as tabs, then add the New Tab Button. for (MegaMekLabMainUI e : entities) { addTab(e); } - addNewButton(); + addNewTabButton(); - - tabs.addChangeListener(this); setContentPane(tabs); menuBar = new MenuBar(this); @@ -112,21 +107,6 @@ public MegaMekLabMainUI currentEditor() { } - - @Override - public void stateChanged(ChangeEvent e) { - // This watches for the user selecting the New Tab button, which is actually a normal tab. - // When they select it, we quickly rename the tab to "New Mek" and add a new "New Tab" button onto the end. - if (tabs.getSelectedIndex() == editors.size() - 1) { - tabs.setTabComponentAt( - tabs.getSelectedIndex(), - new ClosableTab(currentEditor().getEntity().getDisplayName(), currentEditor()) - ); - - addNewButton(); - } - } - /** * Updates the name of the currently selected tab in the tabbed user interface. * Should typically be called when the name of the unit being edited changes. @@ -162,13 +142,14 @@ private void addTab(MegaMekLabMainUI editor) { * 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 addNewButton() { + 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); } /** @@ -186,7 +167,7 @@ private void newUnit(long type, boolean primitive, boolean 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(); } @@ -212,7 +193,7 @@ public void newUnit(long type, boolean primitive) { */ public void addUnit(Entity entity, String filename) { // Create a new "new tab" button, since we're about to replace the existing one - addNewButton(); + addNewTabButton(); // Select the old "new tab" button... tabs.setSelectedIndex(tabs.getTabCount() - 2); // ...and replace it, since newUnit is actually the Switch Unit Type operation. @@ -295,18 +276,32 @@ public MenuBar getMMLMenuBar() { /** * Represents a button used for creating new tabs in the MegaMekLabTabbedUI interface. - * This class extends JPanel and is rendered as a non-opaque panel containing a "+" symbol. * Used to mimic functionality for adding new tabs in a tabbed user interface. - *

- * The only reason this is a separate class instead of just the text for a tab is so that it can be green. + * 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 static class NewTabButton extends JPanel { + private class NewTabButton extends JPanel { public NewTabButton() { setOpaque(false); - var label = new JLabel("➕"); - label.setForeground(Color.GREEN); - label.setFont(Font.getFont("Symbola")); - add(label); + var button = new JButton("➕"); + button.setForeground(Color.GREEN); + button.setFont(Font.getFont("Symbola")); + button.setFocusable(false); + button.setBorder(BorderFactory.createEmptyBorder()); + + button.addActionListener(e -> { + tabs.setEnabledAt(tabs.getTabCount() - 1, true); + tabs.setSelectedIndex(tabs.getTabCount() - 1); + tabs.setTabComponentAt( + tabs.getTabCount() - 1, + new ClosableTab(currentEditor().getEntity().getDisplayName(), currentEditor()) + ); + + + addNewTabButton(); + }); + + add(button); } } @@ -339,12 +334,15 @@ public ClosableTab(String name, MegaMekLabMainUI mainUI) { @Override public void mouseClicked(MouseEvent e) { if (e.isShiftDown() || editor.safetyPrompt()) { + if (tabs.getTabCount() <= 2) { + MegaMekLabTabbedUI.this.dispatchEvent(new WindowEvent(MegaMekLabTabbedUI.this, WindowEvent.WINDOW_CLOSING)); + } + tabs.remove(editor.getContentPane()); - if (tabs.getSelectedIndex() == tabs.getTabCount() - 1 && tabs.getTabCount() > 1) { + if (tabs.getSelectedIndex() == tabs.getTabCount() - 1) { tabs.setSelectedIndex(tabs.getSelectedIndex() - 1); } editors.remove(editor); - stateChanged(new ChangeEvent(tabs)); editor.dispose(); } } From a6a39dc61e3cedb7b7858ceed10a327fae1dd116 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Wed, 1 Jan 2025 14:04:17 -0800 Subject: [PATCH 13/15] File menu actions and keyboard shortcuts for new/close tab --- .../megameklab/resources/Menu.properties | 2 + .../src/megameklab/ui/MegaMekLabTabbedUI.java | 50 ++++++++++++------- megameklab/src/megameklab/ui/MenuBar.java | 24 ++++++++- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/megameklab/resources/megameklab/resources/Menu.properties b/megameklab/resources/megameklab/resources/Menu.properties index 96ece9e91..07c247809 100644 --- a/megameklab/resources/megameklab/resources/Menu.properties +++ b/megameklab/resources/megameklab/resources/Menu.properties @@ -15,6 +15,8 @@ MenuBar.accessibleName=Main Menu Bar ### File Menu fileMenu.text=File miResetCurrentUnit.text=Reset Current Unit +miNewTab.text=New Tab +miCloseTab.text=Close Tab ## Switch Unit Type Menu switchUnitTypeMenu.text=Switch Unit Type miSwitchToMek.text=Mek diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index bf40359ac..b1b82fdfc 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -240,6 +240,35 @@ private void restrictToScrenSize() { 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() { + if (tabs.getTabCount() <= 2) { + MegaMekLabTabbedUI.this.dispatchEvent(new WindowEvent(MegaMekLabTabbedUI.this, WindowEvent.WINDOW_CLOSING)); + } + + var editor = currentEditor(); + + tabs.remove(editor.getContentPane()); + if (tabs.getSelectedIndex() == tabs.getTabCount() - 1) { + tabs.setSelectedIndex(tabs.getSelectedIndex() - 1); + } + editors.remove(editor); + editor.dispose(); + } // @Override @@ -290,15 +319,7 @@ public NewTabButton() { button.setBorder(BorderFactory.createEmptyBorder()); button.addActionListener(e -> { - tabs.setEnabledAt(tabs.getTabCount() - 1, true); - tabs.setSelectedIndex(tabs.getTabCount() - 1); - tabs.setTabComponentAt( - tabs.getTabCount() - 1, - new ClosableTab(currentEditor().getEntity().getDisplayName(), currentEditor()) - ); - - - addNewTabButton(); + newTab(); }); add(button); @@ -334,16 +355,7 @@ public ClosableTab(String name, MegaMekLabMainUI mainUI) { @Override public void mouseClicked(MouseEvent e) { if (e.isShiftDown() || editor.safetyPrompt()) { - if (tabs.getTabCount() <= 2) { - MegaMekLabTabbedUI.this.dispatchEvent(new WindowEvent(MegaMekLabTabbedUI.this, WindowEvent.WINDOW_CLOSING)); - } - - tabs.remove(editor.getContentPane()); - if (tabs.getSelectedIndex() == tabs.getTabCount() - 1) { - tabs.setSelectedIndex(tabs.getSelectedIndex() - 1); - } - editors.remove(editor); - editor.dispose(); + closeCurrentTab(); } } }); diff --git a/megameklab/src/megameklab/ui/MenuBar.java b/megameklab/src/megameklab/ui/MenuBar.java index bd543b854..184b498a7 100644 --- a/megameklab/src/megameklab/ui/MenuBar.java +++ b/megameklab/src/megameklab/ui/MenuBar.java @@ -126,6 +126,26 @@ 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 miResetCurrentUnit = new JMenuItem(resources.getString("miResetCurrentUnit.text")); miResetCurrentUnit.setName("miResetCurrentUnit"); miResetCurrentUnit.setMnemonic(KeyEvent.VK_R); @@ -168,7 +188,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(); @@ -654,7 +674,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"); From 2e5970b3a5d664c4e9e60e932ad32f23fbc93065 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Wed, 1 Jan 2025 14:27:18 -0800 Subject: [PATCH 14/15] Address review comments --- .../src/megameklab/ui/MegaMekLabMainUI.java | 4 +- .../src/megameklab/ui/MegaMekLabTabbedUI.java | 9 ++--- .../src/megameklab/ui/util/TabStateUtil.java | 40 ++++++++++++++----- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/megameklab/src/megameklab/ui/MegaMekLabMainUI.java b/megameklab/src/megameklab/ui/MegaMekLabMainUI.java index 6c7c99777..a39cf3bb8 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabMainUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabMainUI.java @@ -53,13 +53,13 @@ protected void finishSetup() { protected void setSizeAndLocation() { pack(); - restrictToScrenSize(); + restrictToScreenSize(); setLocationRelativeTo(null); CConfig.getMainUiWindowSize(this).ifPresent(this::setSize); CConfig.getMainUiWindowPosition(this).ifPresent(this::setLocation); } - private void restrictToScrenSize() { + private void restrictToScreenSize() { DisplayMode currentMonitor = getGraphicsConfiguration().getDevice().getDisplayMode(); int scaledMonitorW = UIUtil.getScaledScreenWidth(currentMonitor); int scaledMonitorH = UIUtil.getScaledScreenHeight(currentMonitor); diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index b1b82fdfc..591f1e09e 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -34,7 +34,6 @@ import megameklab.util.UnitUtil; import javax.swing.*; -import javax.swing.event.ChangeEvent; import java.awt.*; import java.awt.event.*; import java.io.IOException; @@ -85,12 +84,12 @@ public MegaMekLabTabbedUI(MegaMekLabMainUI... entities) { // Remember the size and position of the window from last time MML was launched pack(); - restrictToScrenSize(); + restrictToScreenSize(); setLocationRelativeTo(null); CConfig.getMainUiWindowSize(this).ifPresent(this::setSize); CConfig.getMainUiWindowPosition(this).ifPresent(this::setLocation); - // ...and save that size nad position on exit + // ...and save that size and position on exit setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); addWindowListener(new ExitOnWindowClosingListener(this)); setExtendedState(CConfig.getIntParam(CConfig.GUI_FULLSCREEN)); @@ -231,7 +230,7 @@ public boolean exit() { return true; } - private void restrictToScrenSize() { + private void restrictToScreenSize() { DisplayMode currentMonitor = getGraphicsConfiguration().getDevice().getDisplayMode(); int scaledMonitorW = UIUtil.getScaledScreenWidth(currentMonitor); int scaledMonitorH = UIUtil.getScaledScreenHeight(currentMonitor); @@ -270,7 +269,6 @@ public void closeCurrentTab() { editor.dispose(); } - // @Override public JFrame getFrame() { return this; @@ -300,7 +298,6 @@ public void refreshMenuBar() { public MenuBar getMMLMenuBar() { return menuBar; } - // /** diff --git a/megameklab/src/megameklab/ui/util/TabStateUtil.java b/megameklab/src/megameklab/ui/util/TabStateUtil.java index 55ba6a99c..ecce9602b 100644 --- a/megameklab/src/megameklab/ui/util/TabStateUtil.java +++ b/megameklab/src/megameklab/ui/util/TabStateUtil.java @@ -26,6 +26,7 @@ import megamek.common.loaders.EntityLoadingException; import megamek.common.loaders.EntitySavingException; import megamek.common.preference.PreferenceManager; +import megamek.logging.MMLogger; import megameklab.ui.MegaMekLabMainUI; import megameklab.ui.dialog.UiLoader; import megameklab.util.UnitUtil; @@ -41,12 +42,14 @@ import java.util.regex.Pattern; public class TabStateUtil { + private final static MMLogger logger = MMLogger.create(TabStateUtil.class); + 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 dir = getTabStateDirectory(true); var clean = new File(dir, TAB_STATE_CLEAN); if (clean.exists()) { @@ -69,14 +72,16 @@ public static void saveTabState(List editors) throws IOExcepti ) { ps.println(((Mek) editor.getEntity()).getMtf()); } catch (Exception e) { - continue; + logger.fatal("Failed to write unit while saving tab state.", e); + return; } } else { unitFile = File.createTempFile("mml_unit_", ".blk.tmp", dir); try { BLKFile.encode(unitFile.getPath(), editor.getEntity()); } catch (EntitySavingException e) { - continue; + logger.fatal("Failed to write unit while saving tab state.", e); + return; } } @@ -98,13 +103,20 @@ public static void saveTabState(List editors) throws IOExcepti } } - clean.createNewFile(); + if (!clean.createNewFile()) { + throw new IOException("Could not create " + clean); + } } public static List loadTabState() throws IOException { - var dir = getTabStateDirectory(); + var dir = getTabStateDirectory(false); + List editors = new ArrayList<>(); + if (dir == null) { + return editors; + } + var clean = new File(dir, TAB_STATE_CLEAN); if (!clean.exists()) { return editors; @@ -129,22 +141,29 @@ public static List loadTabState() throws IOException { 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) {} + } catch (EntityLoadingException e) { + logger.warn("Could not restore tab for entity file %s:%s".formatted(entityFile, fileName), e); + } finally { + if (!newFile.delete()) { + logger.warn("Could not delete temporary file %s".formatted(newFile)); + } + } } - clean.delete(); + if (!clean.delete()) { + logger.error("Could not mark tab state as dirty on load!"); + } return editors; } - private static File getTabStateDirectory() throws IOException { + private static File getTabStateDirectory(boolean create) throws IOException { var userDirString = PreferenceManager.getClientPreferences().getUserDir(); if (userDirString == null || userDirString.isBlank()) { userDirString = "."; @@ -158,6 +177,9 @@ private static File getTabStateDirectory() throws IOException { var tabStateDir = new File(userDir, TAB_STATE_DIRECTORY); if (!tabStateDir.isDirectory()) { + if (!create) { + return null; + } if (!tabStateDir.mkdir()) { throw new IOException("Could not create tab state directory: " + tabStateDir); } else { From a2abcdd43b0b99e9e7733679b33cefd640c955f8 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Wed, 1 Jan 2025 15:42:20 -0800 Subject: [PATCH 15/15] reopen closed tab functionality --- .../megameklab/resources/Menu.properties | 1 + .../src/megameklab/ui/MegaMekLabTabbedUI.java | 100 ++++++++++++++++-- megameklab/src/megameklab/ui/MenuBar.java | 7 ++ 3 files changed, 101 insertions(+), 7 deletions(-) diff --git a/megameklab/resources/megameklab/resources/Menu.properties b/megameklab/resources/megameklab/resources/Menu.properties index 07c247809..fd834eea0 100644 --- a/megameklab/resources/megameklab/resources/Menu.properties +++ b/megameklab/resources/megameklab/resources/Menu.properties @@ -17,6 +17,7 @@ fileMenu.text=File miResetCurrentUnit.text=Reset Current Unit miNewTab.text=New Tab miCloseTab.text=Close Tab +miReopenTab.text=Reopen Closed Tab ## Switch Unit Type Menu switchUnitTypeMenu.text=Switch Unit Type miSwitchToMek.text=Mek diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index 591f1e09e..0d8fae8da 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -37,7 +37,7 @@ import java.awt.*; import java.awt.event.*; import java.io.IOException; -import java.util.ArrayList; +import java.util.*; import java.util.List; /** @@ -45,11 +45,13 @@ * Holds several {@link MegaMekLabMainUI}s as tabs, allowing many units to be open at once. */ public class MegaMekLabTabbedUI extends JFrame implements MenuBarOwner { - private List editors = new ArrayList<>(); + private final List editors = new ArrayList<>(); - private JTabbedPane tabs = new JTabbedPane(); + private final ReopenTabStack closedEditors = new ReopenTabStack(); - private MenuBar menuBar; + private final JTabbedPane tabs = new JTabbedPane(); + + private final MenuBar menuBar; /** * Constructs a new MegaMekLabTabbedUI instance, which serves as the main tabbed UI @@ -123,15 +125,33 @@ public void setTabName(String tabName) { * to the internal editor collection, refreshing it, setting the ownership, * and adding the tab to the tabs UI. * + * If there already exists a New Tab button, the new tab is placed before it. + * * @param editor The MegaMekLabMainUI instance to be added as a new tab. */ private void addTab(MegaMekLabMainUI editor) { + MegaMekLabMainUI newTab = null; + NewTabButton newTabButton = null; + if (tabs.getTabCount() > 0 && tabs.getTabComponentAt(tabs.getTabCount() - 1) instanceof NewTabButton ntb) { + newTabButton = ntb; + newTab = editors.get(editors.size() - 1); + tabs.removeTabAt(tabs.getTabCount() - 1); + editors.remove(editors.size() - 1); + } + editors.add(editor); editor.refreshAll(); editor.setOwner(this); tabs.addTab(editor.getEntity().getDisplayName(), editor.getContentPane()); // See ClosableTab later in this file for what's going on here. tabs.setTabComponentAt(tabs.getTabCount() - 1, new ClosableTab(editor.getEntity().getDisplayName(), editor)); + + if (newTab != null) { + tabs.addTab("+", newTab.getContentPane()); + tabs.setTabComponentAt(tabs.getTabCount() - 1, newTabButton); + tabs.setEnabledAt(tabs.getTabCount() - 1, false); + editors.add(newTab); + } } /** @@ -255,18 +275,37 @@ public void newTab() { * 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 = currentEditor(); + var editor = editors.get(position); tabs.remove(editor.getContentPane()); if (tabs.getSelectedIndex() == tabs.getTabCount() - 1) { tabs.setSelectedIndex(tabs.getSelectedIndex() - 1); } editors.remove(editor); - editor.dispose(); + 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 @@ -352,10 +391,57 @@ public ClosableTab(String name, MegaMekLabMainUI mainUI) { @Override public void mouseClicked(MouseEvent e) { if (e.isShiftDown() || editor.safetyPrompt()) { - closeCurrentTab(); + 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 184b498a7..4ff5dd196 100644 --- a/megameklab/src/megameklab/ui/MenuBar.java +++ b/megameklab/src/megameklab/ui/MenuBar.java @@ -144,6 +144,13 @@ private JMenu createFileMenu() { } }); 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"));