From 699bace06b01ec1b13ec7b4257f0dbb915afe226 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Mon, 6 Jan 2025 18:05:06 -0800 Subject: [PATCH 1/4] Unread indicators for tabs # Conflicts: # megameklab/src/megameklab/ui/MenuBar.java --- .../src/megameklab/EntityChangedUtil.java | 74 +++++++++++++++++++ .../src/megameklab/ui/MegaMekLabTabbedUI.java | 48 ++++++++---- megameklab/src/megameklab/ui/MenuBar.java | 1 + 3 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 megameklab/src/megameklab/EntityChangedUtil.java diff --git a/megameklab/src/megameklab/EntityChangedUtil.java b/megameklab/src/megameklab/EntityChangedUtil.java new file mode 100644 index 000000000..4d8d9fed4 --- /dev/null +++ b/megameklab/src/megameklab/EntityChangedUtil.java @@ -0,0 +1,74 @@ +package megameklab; + +import megamek.common.Entity; +import megamek.common.Mek; +import megamek.common.MekFileParser; +import megamek.common.loaders.BLKFile; +import megamek.common.loaders.EntitySavingException; +import megamek.logging.MMLogger; +import megameklab.ui.MegaMekLabMainUI; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class EntityChangedUtil { + private static MMLogger logger = MMLogger.create(EntityChangedUtil.class); + + private static Map cache = new ConcurrentHashMap<>(); + + public static boolean hasEntityChanged(MegaMekLabMainUI editor) { + var filename = editor.getFileName(); + if (filename == null || filename.isBlank()) { + return true; + } + + + if (!cache.containsKey(filename)) { + var f = new File(filename); + if (!f.exists()) { + return true; + } + + try { + var e = new MekFileParser(f).getEntity(); + cache.put(filename, encode(e)); + } catch (Exception ex) { + logger.error("Entity loading failure:", ex); + } + } + + try { + var o = cache.get(filename); + var n = encode(editor.getEntity()); + return !o.equals(n); + } catch (EntitySavingException e) { + logger.error("Entity encoding failure:", e); + return true; + } + } + + public static void editorSaved(MegaMekLabMainUI editor) { + var filename = editor.getFileName(); + if (filename == null || filename.isBlank()) { + return; + } + + try { + cache.put(editor.getFileName(), encode(editor.getEntity())); + } catch (EntitySavingException e) { + logger.error("Entity encoding failure:", e); + } + } + + private static String encode(Entity e) throws EntitySavingException { + if (e instanceof Mek m) { + return m.getMtf(); + } else { + var blk = BLKFile.getBlock(e); + return String.join("\n", blk.getAllDataAsString()); + } + } + + private EntityChangedUtil() {} +} diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index e51809157..b18f2a09b 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -23,6 +23,7 @@ import megamek.client.ui.swing.util.UIUtil; import megamek.common.Entity; import megamek.common.preference.PreferenceManager; +import megameklab.EntityChangedUtil; import megameklab.MMLConstants; import megameklab.MegaMekLab; import megameklab.ui.dialog.UiLoader; @@ -34,6 +35,7 @@ import megameklab.util.UnitUtil; import javax.swing.*; +import javax.swing.Timer; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.*; @@ -88,6 +90,8 @@ public MegaMekLabTabbedUI(MegaMekLabMainUI... entities) { // Enable opening unit and mul files by drag-and-drop setTransferHandler(new MMLFileDropTransferHandler(this)); + new Timer(500, e -> checkChanged(tabs.getSelectedIndex())).start(); + // Remember the size and position of the window from last time MML was launched pack(); restrictToScreenSize(); @@ -121,7 +125,8 @@ public MegaMekLabMainUI currentEditor() { 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()) ); + tabs.setTabComponentAt(tabs.getSelectedIndex(), new EditorTab(tabName, currentEditor()) ); + checkChanged(tabs.getSelectedIndex()); } /** @@ -147,8 +152,9 @@ private void addTab(MegaMekLabMainUI 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)); + // See EditorTab later in this file for what's going on here. + tabs.setTabComponentAt(tabs.getTabCount() - 1, new EditorTab(editor.getEntity().getDisplayName(), editor)); + checkChanged(tabs.getTabCount() - 1); if (newTab != null) { tabs.addTab("+", newTab.getContentPane()); @@ -189,7 +195,7 @@ private void newUnit(long type, boolean primitive, boolean industrial) { 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.setTabComponentAt(tabs.getSelectedIndex(), new EditorTab(newUi.getEntity().getDisplayName(), newUi)); tabs.setEnabledAt(tabs.getSelectedIndex(), true); oldUi.dispose(); } @@ -226,7 +232,7 @@ public void addUnit(Entity entity, String filename) { currentEditor().reloadTabs(); currentEditor().refreshAll(); // Set the tab name - tabs.setTabComponentAt(tabs.getSelectedIndex(), new ClosableTab(entity.getDisplayName(), currentEditor())); + tabs.setTabComponentAt(tabs.getSelectedIndex(), new EditorTab(entity.getDisplayName(), currentEditor())); } @Override @@ -268,7 +274,7 @@ public void newTab() { tabs.setSelectedIndex(tabs.getTabCount() - 1); tabs.setTabComponentAt( tabs.getTabCount() - 1, - new ClosableTab(currentEditor().getEntity().getDisplayName(), currentEditor()) + new EditorTab(currentEditor().getEntity().getDisplayName(), currentEditor()) ); addNewTabButton(); @@ -346,6 +352,15 @@ public MenuBar getMMLMenuBar() { public void stateChanged(ChangeEvent e) { if (e.getSource() == tabs) { refreshMenuBar(); + checkChanged(checkChangedTab); + checkChangedTab = tabs.getSelectedIndex(); + } + } + + private int checkChangedTab = 0; + private void checkChanged(int tabIndex) { + if (tabs.getTabCount() >= tabIndex + 1 && tabs.getTabComponentAt(tabIndex) instanceof EditorTab et) { + et.markChanged(EntityChangedUtil.hasEntityChanged(editors.get(tabIndex))); } } @@ -354,7 +369,7 @@ public void stateChanged(ChangeEvent e) { * 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}. + * the tab is replaced with a normal {@link EditorTab}. */ private class NewTabButton extends JPanel { public NewTabButton() { @@ -466,24 +481,29 @@ private JPopupMenu loadUnitMenu() { * 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; + private class EditorTab extends JPanel { + private final JLabel changesIndicator; + private final MegaMekLabMainUI editor; + + public void markChanged(boolean changed) { + changesIndicator.setText(changed ? "*" : ""); + } - public ClosableTab(String name, MegaMekLabMainUI mainUI) { - unitName = new JLabel(name); + public EditorTab(String name, MegaMekLabMainUI mainUI) { + JLabel unitName = new JLabel(name); + changesIndicator = new JLabel(); editor = mainUI; setOpaque(false); - closeButton = new JButton("❌"); + JButton 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(changesIndicator); add(closeButton); closeButton.addMouseListener(new MouseAdapter() { @Override diff --git a/megameklab/src/megameklab/ui/MenuBar.java b/megameklab/src/megameklab/ui/MenuBar.java index a6586e882..e399d5b74 100644 --- a/megameklab/src/megameklab/ui/MenuBar.java +++ b/megameklab/src/megameklab/ui/MenuBar.java @@ -44,6 +44,7 @@ import megamek.common.annotations.Nullable; import megamek.common.templates.TROView; import megamek.logging.MMLogger; +import megameklab.EntityChangedUtil; import megameklab.MMLConstants; import megameklab.ui.dialog.MMLFileChooser; import megameklab.ui.dialog.MegaMekLabUnitSelectorDialog; From ac5de3b3e6c10cfd07a2e85c96b959d7d463364f Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Mon, 6 Jan 2025 21:25:22 -0800 Subject: [PATCH 2/4] Only fire safety prompt if unit changed --- .../src/megameklab/ui/MegaMekLabMainUI.java | 3 +- .../src/megameklab/ui/MegaMekLabTabbedUI.java | 37 +++++++++++++++- megameklab/src/megameklab/ui/MenuBar.java | 4 +- .../ui/util/MegaMekLabFileSaver.java | 8 ++++ .../{ => util}/EntityChangedUtil.java | 42 +++++++++++++++---- 5 files changed, 82 insertions(+), 12 deletions(-) rename megameklab/src/megameklab/{ => util}/EntityChangedUtil.java (54%) diff --git a/megameklab/src/megameklab/ui/MegaMekLabMainUI.java b/megameklab/src/megameklab/ui/MegaMekLabMainUI.java index b2fd32fbf..237cc1e14 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabMainUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabMainUI.java @@ -20,6 +20,7 @@ import megamek.common.Entity; import megamek.common.Mounted; import megamek.common.preference.PreferenceManager; +import megameklab.util.EntityChangedUtil; import megameklab.MMLConstants; import megameklab.MegaMekLab; import megameklab.ui.util.ExitOnWindowClosingListener; @@ -83,7 +84,7 @@ public void setVisible(boolean b) { @Override public boolean safetyPrompt() { - if (CConfig.getBooleanParam(CConfig.MISC_SKIP_SAFETY_PROMPTS)) { + if (CConfig.getBooleanParam(CConfig.MISC_SKIP_SAFETY_PROMPTS) || !EntityChangedUtil.hasEntityChanged(this)) { return true; } else { int savePrompt = JOptionPane.showConfirmDialog(this, diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index b18f2a09b..c20f44d09 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -23,12 +23,13 @@ import megamek.client.ui.swing.util.UIUtil; import megamek.common.Entity; import megamek.common.preference.PreferenceManager; -import megameklab.EntityChangedUtil; +import megameklab.util.EntityChangedUtil; import megameklab.MMLConstants; import megameklab.MegaMekLab; import megameklab.ui.dialog.UiLoader; import megameklab.ui.mek.BMMainUI; import megameklab.ui.util.ExitOnWindowClosingListener; +import megameklab.ui.util.MegaMekLabFileSaver; import megameklab.ui.util.TabStateUtil; import megameklab.util.CConfig; import megameklab.util.MMLFileDropTransferHandler; @@ -235,9 +236,36 @@ public void addUnit(Entity entity, String filename) { tabs.setTabComponentAt(tabs.getSelectedIndex(), new EditorTab(entity.getDisplayName(), currentEditor())); } + private boolean exitPrompt() { + if (CConfig.getBooleanParam(CConfig.MISC_SKIP_SAFETY_PROMPTS)) { + return true; + } + if (editors.stream().limit(editors.size() - 1).noneMatch(EntityChangedUtil::hasEntityChanged)) { + return true; + } + int savePrompt = JOptionPane.showConfirmDialog(this, + "All unsaved changes to open units will be discarded. Save the units first?", + "Save Units Before Proceeding?", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE); + if (savePrompt == JOptionPane.NO_OPTION) { + return true; + } + if (savePrompt == JOptionPane.YES_OPTION) { + return editors.stream().limit(editors.size() - 1) + .filter(EntityChangedUtil::hasEntityChanged) + .noneMatch(editor -> { + tabs.setSelectedComponent(editor.getContentPane()); + tabs.paintImmediately(tabs.getBounds()); + return !editor.getMMLMenuBar().saveUnit(); + }); + } + return false; + } + @Override public boolean exit() { - if (!currentEditor().safetyPrompt()) { + if (!exitPrompt()) { return false; } @@ -348,6 +376,11 @@ public MenuBar getMMLMenuBar() { return menuBar; } + @Override + public boolean safetyPrompt() { + return currentEditor().safetyPrompt(); + } + @Override public void stateChanged(ChangeEvent e) { if (e.getSource() == tabs) { diff --git a/megameklab/src/megameklab/ui/MenuBar.java b/megameklab/src/megameklab/ui/MenuBar.java index e399d5b74..f3c76b187 100644 --- a/megameklab/src/megameklab/ui/MenuBar.java +++ b/megameklab/src/megameklab/ui/MenuBar.java @@ -44,7 +44,6 @@ import megamek.common.annotations.Nullable; import megamek.common.templates.TROView; import megamek.logging.MMLogger; -import megameklab.EntityChangedUtil; import megameklab.MMLConstants; import megameklab.ui.dialog.MMLFileChooser; import megameklab.ui.dialog.MegaMekLabUnitSelectorDialog; @@ -1171,7 +1170,8 @@ public void loadFile(File unitFile) { throw new Exception(); } - if (!owner.safetyPrompt()) { + // TabbedUi loads a unit into a new tab, no safety prompt needed. + if (!(owner instanceof MegaMekLabTabbedUI) || !owner.safetyPrompt()) { return; } diff --git a/megameklab/src/megameklab/ui/util/MegaMekLabFileSaver.java b/megameklab/src/megameklab/ui/util/MegaMekLabFileSaver.java index 1cc522747..2f25cbe3b 100644 --- a/megameklab/src/megameklab/ui/util/MegaMekLabFileSaver.java +++ b/megameklab/src/megameklab/ui/util/MegaMekLabFileSaver.java @@ -19,9 +19,11 @@ import megamek.common.loaders.BLKFile; import megamek.logging.MMLogger; import megameklab.ui.FileNameManager; +import megameklab.ui.MegaMekLabMainUI; import megameklab.ui.PopupMessages; import megameklab.ui.dialog.MMLFileChooser; import megameklab.util.CConfig; +import megameklab.util.EntityChangedUtil; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; @@ -119,6 +121,12 @@ private String saveUnitTo(JFrame ownerFrame, File file, Entity entity) { } else { BLKFile.encode(file.getPath(), entity); } + + if (ownerFrame instanceof MegaMekLabMainUI mui) { + // Since we've saved the entity, update the entity being compared against to determine if the user has unsaved work. + EntityChangedUtil.editorSaved(mui); + } + PopupMessages.showUnitSavedMessage(ownerFrame, entity, file); return file.toString(); } catch (Exception ex) { diff --git a/megameklab/src/megameklab/EntityChangedUtil.java b/megameklab/src/megameklab/util/EntityChangedUtil.java similarity index 54% rename from megameklab/src/megameklab/EntityChangedUtil.java rename to megameklab/src/megameklab/util/EntityChangedUtil.java index 4d8d9fed4..066bbceaf 100644 --- a/megameklab/src/megameklab/EntityChangedUtil.java +++ b/megameklab/src/megameklab/util/EntityChangedUtil.java @@ -1,21 +1,42 @@ -package megameklab; +/* + * 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.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.logging.MMLogger; import megameklab.ui.MegaMekLabMainUI; +import java.io.ByteArrayInputStream; import java.io.File; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class EntityChangedUtil { - private static MMLogger logger = MMLogger.create(EntityChangedUtil.class); + private static final MMLogger logger = MMLogger.create(EntityChangedUtil.class); - private static Map cache = new ConcurrentHashMap<>(); + private static final Map cache = new ConcurrentHashMap<>(); public static boolean hasEntityChanged(MegaMekLabMainUI editor) { var filename = editor.getFileName(); @@ -32,14 +53,15 @@ public static boolean hasEntityChanged(MegaMekLabMainUI editor) { try { var e = new MekFileParser(f).getEntity(); - cache.put(filename, encode(e)); + cache.put(filename, e); } catch (Exception ex) { logger.error("Entity loading failure:", ex); + cache.put(filename, null); } } try { - var o = cache.get(filename); + var o = encode(cache.get(filename)); var n = encode(editor.getEntity()); return !o.equals(n); } catch (EntitySavingException e) { @@ -55,13 +77,19 @@ public static void editorSaved(MegaMekLabMainUI editor) { } try { - cache.put(editor.getFileName(), encode(editor.getEntity())); - } catch (EntitySavingException e) { + var bis = new ByteArrayInputStream(encode(editor.getEntity()).getBytes()); + cache.put(editor.getFileName(), new MekFileParser(bis, editor.getFileName()).getEntity()); + } catch (EntitySavingException | EntityLoadingException e) { + cache.remove(filename); logger.error("Entity encoding failure:", e); } } private static String encode(Entity e) throws EntitySavingException { + if (e == null) { + return ""; + } + if (e instanceof Mek m) { return m.getMtf(); } else { From fef0bbce16f5815148fa0fca07ff9bef0b5e5d17 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Tue, 7 Jan 2025 01:29:50 -0800 Subject: [PATCH 3/4] Add comments --- .../src/megameklab/ui/MegaMekLabTabbedUI.java | 9 ++++++++- .../src/megameklab/util/EntityChangedUtil.java | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index c20f44d09..a8dcda781 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -91,6 +91,9 @@ public MegaMekLabTabbedUI(MegaMekLabMainUI... entities) { // Enable opening unit and mul files by drag-and-drop setTransferHandler(new MMLFileDropTransferHandler(this)); + // If you can think of a way to detect when the user makes a change to the entity, let me know. + // I can't, so we just poll the entity for changed to set the "unsaved work" indicator periodically + // --Pavel Braginskiy (cat /dev/random) new Timer(500, e -> checkChanged(tabs.getSelectedIndex())).start(); // Remember the size and position of the window from last time MML was launched @@ -240,6 +243,7 @@ private boolean exitPrompt() { if (CConfig.getBooleanParam(CConfig.MISC_SKIP_SAFETY_PROMPTS)) { return true; } + // No editors have changes, no need to prompt for saving if (editors.stream().limit(editors.size() - 1).noneMatch(EntityChangedUtil::hasEntityChanged)) { return true; } @@ -251,13 +255,16 @@ private boolean exitPrompt() { if (savePrompt == JOptionPane.NO_OPTION) { return true; } + // For each editor with unsaved changes, switch to that editor and save it. if (savePrompt == JOptionPane.YES_OPTION) { return editors.stream().limit(editors.size() - 1) .filter(EntityChangedUtil::hasEntityChanged) .noneMatch(editor -> { tabs.setSelectedComponent(editor.getContentPane()); + // paintImmediately means that the user can see which unit they're about to pick a file for + // because it updates the UI without waiting for this method to return tabs.paintImmediately(tabs.getBounds()); - return !editor.getMMLMenuBar().saveUnit(); + return !menuBar.saveUnit(); }); } return false; diff --git a/megameklab/src/megameklab/util/EntityChangedUtil.java b/megameklab/src/megameklab/util/EntityChangedUtil.java index 066bbceaf..3c7eb9b61 100644 --- a/megameklab/src/megameklab/util/EntityChangedUtil.java +++ b/megameklab/src/megameklab/util/EntityChangedUtil.java @@ -33,17 +33,28 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +/** + * Helps to determine if a tab should have an "Unsaved Work" indicator and if the "would you like to save" prompt should appear. + */ public class EntityChangedUtil { private static final MMLogger logger = MMLogger.create(EntityChangedUtil.class); private static final Map cache = new ConcurrentHashMap<>(); + /** + * Determines if the editor has unsaved changes. + * @param editor The MainUI to check for changes + * @return true if saving the unit would produce a different unit than the one saved already, or if there is no existing file for the unit. + */ public static boolean hasEntityChanged(MegaMekLabMainUI editor) { var filename = editor.getFileName(); if (filename == null || filename.isBlank()) { return true; } + // The idea behind how this works is to store a copy of the unit based on that unit's current file. + // By encoding both the original and current version of the entity to mtf/blk at the same time, + // We ensure that identical units will encode to identical mtf/blk strings, which indicates no unsaved work. if (!cache.containsKey(filename)) { var f = new File(filename); @@ -70,6 +81,10 @@ public static boolean hasEntityChanged(MegaMekLabMainUI editor) { } } + /** + * + * @param editor The editor containing the unit that was just saved. + */ public static void editorSaved(MegaMekLabMainUI editor) { var filename = editor.getFileName(); if (filename == null || filename.isBlank()) { From f4991abeb2255e1a35a10ae6a3c70d3922cb2636 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Tue, 7 Jan 2025 01:32:20 -0800 Subject: [PATCH 4/4] Close tabs by middle-clicking them --- megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java index a8dcda781..aeca7bdce 100644 --- a/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java +++ b/megameklab/src/megameklab/ui/MegaMekLabTabbedUI.java @@ -553,6 +553,18 @@ public void mouseClicked(MouseEvent e) { } } }); + + addMouseListener( + new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + // middle click to close tab + if (e.getButton() == MouseEvent.BUTTON2 && editor.safetyPrompt()) { + closeTabAt(editors.indexOf(editor)); + } + } + } + ); } }