diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties
index 4c96ba383f0..a76cdb07d0e 100644
--- a/megamek/i18n/megamek/client/messages.properties
+++ b/megamek/i18n/megamek/client/messages.properties
@@ -647,6 +647,10 @@ ChatLounge.butGroundMap = Ground Map
ChatLounge.butNames=Random Names...
ChatLounge.butRemoveBot=Remove Bot
ChatLounge.butSaveList=Save Unit List...
+ChatLounge.butPrintList=Print Unit List...
+ChatLounge.butPrintList.tooltip=Print the record sheets for the current player's units.\
+
This requires MegaMekLab to exist on your system.
+ChatLounge.butPrintList.printing=Loading print dialog
ChatLounge.butShrink=<
ChatLounge.butSkills=Random Skills...
ChatLounge.butShowUnitID=Show IDs
@@ -1270,6 +1274,10 @@ CommonSettingsDialog.logFileName=Game log filename:
CommonSettingsDialog.userDir=User Files Directory:
CommonSettingsDialog.userDir.tooltip=Use this directory for resources you want to share between different installs or versions of MegaMek, MegaMekLab and MekHQ. Fonts, units, camos, portraits and fluff images will also be loaded from this directory.
Note: Inside the user directory, use the directory structure of MM/MML/MHQ for camos, portraits and fluff images, i.e. data/images/camo, data/images/portraits and data/images/fluff/.
Fonts and units can be placed anywhere in the user directory.
CommonSettingsDialog.userDir.chooser.title=Choose User Data Folder
+CommonSettingsDialog.mmlPath=Path to MegaMekLab Executable:
+CommonSettingsDialog.mmlPath.tooltip=Used for printing unit lists.\
+
MegaMek will try to autodetect this when the option is blank if MM and MML are installed together.
+CommonSettingsDialog.mmlPath.chooser.title=Select MegaMekLab Executable
CommonSettingsDialog.main=Main
CommonSettingsDialog.audio=Audio
CommonSettingsDialog.miniMap=Mini Map
diff --git a/megamek/src/megamek/client/ui/swing/ClientGUI.java b/megamek/src/megamek/client/ui/swing/ClientGUI.java
index 4acc76028f5..937c6bd9dad 100644
--- a/megamek/src/megamek/client/ui/swing/ClientGUI.java
+++ b/megamek/src/megamek/client/ui/swing/ClientGUI.java
@@ -2069,6 +2069,99 @@ public void saveListFile(ArrayList unitList, String filename) {
}
}
+ /**
+ * Request MegaMekLab to print out record sheets for the current player's selected units.
+ * The method will try to find MML either automatically or based on a configured client setting.
+ *
+ * @param unitList The list of units to print
+ * @param button This should always be {@link ChatLounge#butPrintList}, if you need to trigger this method from somewhere else, override it.
+ */
+ public void printList(ArrayList unitList, JButton button) {
+ // Do nothing if there are no units to print
+ if ((unitList == null) || unitList.isEmpty()) {
+ return;
+ }
+
+ // Detect the MML executable.
+ // If the user hasn't set this manually, try to pick "MegaMakLab.exe"/".sh"
+ // from the same directory that MM is in
+ var mmlPath = CP.getMmlPath();
+ var autodetect = false;
+ if (null == mmlPath || mmlPath.isBlank()) {
+ autodetect = true;
+ if (System.getProperty("os.name").toLowerCase().contains("win")) {
+ mmlPath = "MegaMekLab.exe";
+ } else {
+ mmlPath = "MegaMekLab.sh";
+ }
+ }
+
+ var mml = new File(mmlPath);
+
+ if (!mml.canExecute()) {
+ if (autodetect) {
+ logger.error("Could not auto-detect MegaMekLab! Please configure the path to the MegaMekLab executable in the settings.", "Error printing unit list");
+ } else {
+ logger.error("%s does not appear to be an executable! Please configure the path to the MegaMekLab executable in the settings.".formatted(mml.getName()), "Error printing unit list");
+ }
+ return;
+ }
+
+ try {
+ // Save unit list to a temporary file
+ var unitFile = File.createTempFile("MegaMekPrint", ".mul");
+ EntityListFile.saveTo(unitFile, unitList);
+
+ String[] command;
+ if (mml.getName().toLowerCase().contains("gradle")) {
+ // If the executable is `gradlew`/`gradelw.bat`, assume it's the gradle wrapper
+ // which comes in the MML git repo. Compile and run MML from source in order to print units.
+ command = new String[] {
+ mml.getAbsolutePath(),
+ "run",
+ "--args=%s --no-startup".formatted(unitFile.getAbsolutePath())
+ };
+ } else {
+ // Start mml normally. "--no-startup" tells MML to exit after the user closes the
+ // print dialog (by printing or cancelling)
+ command = new String[] {
+ mml.getAbsolutePath(),
+ unitFile.getAbsolutePath(),
+ "--no-startup"
+ };
+ }
+ // It takes a while for MML to start, so we change the text of the button
+ // to let the user know that something is happening
+ button.setText(Messages.getString("ChatLounge.butPrintList.printing"));
+
+ logger.info("Running command: {}", String.join(" ", command));
+
+
+ var p = new ProcessBuilder(command)
+ .directory(mml.getAbsoluteFile().getParentFile())
+ .inheritIO()
+ .start();
+
+ // This thread's only purpose is to wait for the MML process to finish and change the button's text back to
+ // its original value.
+ new Thread(() -> {
+ try {
+ p.waitFor();
+ } catch (InterruptedException e) {
+ logger.error(e);
+ } finally {
+ button.setText(Messages.getString("ChatLounge.butPrintList"));
+ }
+ }).start();
+
+ } catch (Exception e) {
+ // If something goes wrong, probably ProcessBuild.start if anything,
+ // Make sure to set the button text back to what it started as no matter what.
+ logger.error(e, "Operation failed", "Error printing unit list");
+ button.setText(Messages.getString("ChatLounge.butPrintList"));
+ }
+ }
+
protected void saveVictoryList() {
String filename = client.getLocalPlayer().getName();
diff --git a/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java b/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java
index 431ca7b313b..ae77658b6b6 100644
--- a/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java
+++ b/megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java
@@ -212,6 +212,7 @@ private void moveElement(DefaultListModel srcModel, int srcIndex, int trg
private JTextField tfSoundMuteOthersFileName;
private JTextField userDir;
+ private JTextField mmlPath;
private final JCheckBox keepGameLog = new JCheckBox(Messages.getString("CommonSettingsDialog.keepGameLog"));
private JTextField gameLogFilename;
private final JCheckBox stampFilenames = new JCheckBox(Messages.getString("CommonSettingsDialog.stampFilenames"));
@@ -1724,6 +1725,23 @@ private JPanel getSettingsPanel() {
addLineSpacer(comps);
+ JLabel mmlPathLabel = new JLabel(Messages.getString("CommonSettingsDialog.mmlPath"));
+ mmlPathLabel.setToolTipText(Messages.getString("CommonSettingsDialog.mmlPath.tooltip"));
+ mmlPath = new JTextField(20);
+ mmlPath.setMaximumSize(new Dimension(250, 40));
+ mmlPath.setToolTipText(Messages.getString("CommonSettingsDialog.mmlPath.tooltip"));
+ JButton mmlPathChooser = new JButton("...");
+ mmlPathChooser.addActionListener(e ->
+ fileChoose(mmlPath, getFrame(), Messages.getString("CommonSettingsDialog.mmlPath.chooser.title"), false));
+ row = new ArrayList<>();
+ row.add(mmlPathLabel);
+ row.add(mmlPath);
+ row.add(Box.createHorizontalStrut(10));
+ row.add(mmlPathChooser);
+ comps.add(row);
+
+ addLineSpacer(comps);
+
// UI Theme
uiThemes = new JComboBox<>();
uiThemes.setMaximumSize(new Dimension(400, uiThemes.getMaximumSize().height));
@@ -1944,6 +1962,7 @@ public void setVisible(boolean visible) {
gameLogFilename.setEnabled(keepGameLog.isSelected());
gameLogFilename.setText(CP.getGameLogFilename());
userDir.setText(CP.getUserDir());
+ mmlPath.setText(CP.getMmlPath());
stampFilenames.setSelected(CP.stampFilenames());
stampFormat.setEnabled(stampFilenames.isSelected());
stampFormat.setText(CP.getStampFormat());
@@ -2421,6 +2440,7 @@ protected void okAction() {
CP.setKeepGameLog(keepGameLog.isSelected());
CP.setGameLogFilename(gameLogFilename.getText());
CP.setUserDir(userDir.getText());
+ CP.setMmlPath(mmlPath.getText());
CP.setStampFilenames(stampFilenames.isSelected());
CP.setStampFormat(stampFormat.getText());
CP.setReportKeywords(reportKeywordsTextPane.getText());
@@ -3452,13 +3472,19 @@ public static List filteredFilesWithSubDirs(File path, String fileEnding
* @param parent The parent JFrame of the settings dialog
*/
public static void fileChooseUserDir(JTextField userDirTextField, JFrame parent) {
- JFileChooser userDirChooser = new JFileChooser(userDirTextField.getText());
- userDirChooser.setDialogTitle(Messages.getString("CommonSettingsDialog.userDir.chooser.title"));
- userDirChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ fileChoose(userDirTextField, parent, Messages.getString("CommonSettingsDialog.userDir.chooser.title"),true);
+ }
+
+ private static void fileChoose(JTextField textField, JFrame parent, String title, boolean directories) {
+ JFileChooser userDirChooser = new JFileChooser(textField.getText());
+ userDirChooser.setDialogTitle(title);
+ if (directories) {
+ userDirChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ }
int returnVal = userDirChooser.showOpenDialog(parent);
if ((returnVal == JFileChooser.APPROVE_OPTION) && (userDirChooser.getSelectedFile() != null)
- && userDirChooser.getSelectedFile().isDirectory()) {
- userDirTextField.setText(userDirChooser.getSelectedFile().toString());
+ && (directories ? userDirChooser.getSelectedFile().isDirectory() : userDirChooser.getSelectedFile().isFile())) {
+ textField.setText(userDirChooser.getSelectedFile().toString());
}
}
}
diff --git a/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java b/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java
index 2066ee4cd5b..708c3f0d300 100644
--- a/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java
+++ b/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java
@@ -160,6 +160,7 @@ public class ChatLounge extends AbstractPhaseDisplay implements
private JButton butNames = new JButton(Messages.getString("ChatLounge.butNames"));
private JButton butLoadList = new JButton(Messages.getString("ChatLounge.butLoadList"));
private JButton butSaveList = new JButton(Messages.getString("ChatLounge.butSaveList"));
+ private JButton butPrintList = new JButton(Messages.getString("ChatLounge.butPrintList"));
/* Unit Table */
private MekTable mekTable;
@@ -276,6 +277,7 @@ public class ChatLounge extends AbstractPhaseDisplay implements
private static final String CL_ACTIONCOMMAND_LOADLIST = "load_list";
private static final String CL_ACTIONCOMMAND_SAVELIST = "save_list";
+ private static final String CL_ACTIONCOMMAND_PRINTLIST = "print_list";
private static final String CL_ACTIONCOMMAND_LOADMEK = "load_mek";
private static final String CL_ACTIONCOMMAND_ADDBOT = "add_bot";
private static final String CL_ACTIONCOMMAND_REMOVEBOT = "remove_bot";
@@ -365,6 +367,7 @@ private void setupListeners() {
butRandomMap.addActionListener(lobbyListener);
butRemoveBot.addActionListener(lobbyListener);
butSaveList.addActionListener(lobbyListener);
+ butPrintList.addActionListener(lobbyListener);
butShowUnitID.addActionListener(lobbyListener);
butSkills.addActionListener(lobbyListener);
butSpaceSize.addActionListener(lobbyListener);
@@ -546,6 +549,9 @@ private void setupUnitConfig() {
butLoadList.setEnabled(mscLoaded);
butSaveList.setActionCommand(CL_ACTIONCOMMAND_SAVELIST);
butSaveList.setEnabled(false);
+ butPrintList.setActionCommand(CL_ACTIONCOMMAND_PRINTLIST);
+ butPrintList.setEnabled(false);
+ butPrintList.setToolTipText(Messages.getString("ChatLounge.butPrintList.tooltip"));
butAdd.setEnabled(mscLoaded);
butAdd.setActionCommand(CL_ACTIONCOMMAND_LOADMEK);
butArmy.setEnabled(mscLoaded);
@@ -561,6 +567,7 @@ private void setupUnitConfig() {
panUnitInfoGrid.add(butLoadList);
panUnitInfoGrid.add(butSaveList);
panUnitInfoGrid.add(butNames);
+ panUnitInfoGrid.add(butPrintList);
panUnitInfo.add(panUnitInfoAdd);
panUnitInfo.add(panUnitInfoGrid);
@@ -1739,7 +1746,7 @@ public void actionPerformed(ActionEvent ev) {
}
clientgui.loadListFile(c.getLocalPlayer());
- } else if (ev.getSource().equals(butSaveList)) {
+ } else if (ev.getSource().equals(butSaveList) || ev.getSource().equals(butPrintList)) {
// Allow the player to save their current
// list of entities to a file.
Client c = getSelectedClient();
@@ -1752,7 +1759,11 @@ public void actionPerformed(ActionEvent ev) {
for (Entity entity : entities) {
entity.setForceString(game().getForces().forceStringFor(entity));
}
- clientgui.saveListFile(entities, c.getLocalPlayer().getName());
+ if (ev.getSource().equals(butSaveList)) {
+ clientgui.saveListFile(entities, c.getLocalPlayer().getName());
+ } else {
+ clientgui.printList(entities, (JButton) ev.getSource());
+ }
} else if (ev.getSource().equals(butAddBot)) {
configAndCreateBot(null);
@@ -2285,6 +2296,7 @@ public void removeAllListeners() {
butRandomMap.removeActionListener(lobbyListener);
butRemoveBot.removeActionListener(lobbyListener);
butSaveList.removeActionListener(lobbyListener);
+ butPrintList.removeActionListener(lobbyListener);
butShowUnitID.removeActionListener(lobbyListener);
butSkills.removeActionListener(lobbyListener);
butSpaceSize.removeActionListener(lobbyListener);
@@ -2404,10 +2416,12 @@ private void refreshPlayerConfig() {
// Disable the Remove Bot button for the "player" of a "Connect As Bot" client
butRemoveBot.setEnabled(isSingleLocalBot);
butSaveList.setEnabled(false);
+ butPrintList.setEnabled(false);
if (isSinglePlayer) {
var selPlayer = theElement(selPlayers);
var hasUnits = !game().getPlayerEntities(selPlayer, false).isEmpty();
butSaveList.setEnabled(hasUnits && unitsVisible(selPlayer));
+ butPrintList.setEnabled(hasUnits && unitsVisible(selPlayer));
setTeamSelectedItem(selPlayer.getTeam());
}
}
diff --git a/megamek/src/megamek/common/preference/ClientPreferences.java b/megamek/src/megamek/common/preference/ClientPreferences.java
index c4629e65c8c..75d3b4b7943 100644
--- a/megamek/src/megamek/common/preference/ClientPreferences.java
+++ b/megamek/src/megamek/common/preference/ClientPreferences.java
@@ -70,6 +70,8 @@ public class ClientPreferences extends PreferenceStoreProxy {
*/
public static final String USER_DIR = "UserDir";
+ public static final String MML_PATH = "MmlPath";
+
// endregion Variable Declarations
// region Constructors
@@ -103,6 +105,7 @@ public ClientPreferences(IPreferenceStore store) {
store.setDefault(IP_ADDRESSES_IN_CHAT, false);
store.setDefault(START_SEARCHLIGHTS_ON, true);
store.setDefault(USER_DIR, "");
+ store.setDefault(MML_PATH, "");
setLocale(store.getString(LOCALE));
setMekHitLocLog();
}
@@ -406,4 +409,12 @@ public void setUserDir(String userDir) {
}
store.setValue(USER_DIR, userDir);
}
+
+ public String getMmlPath() {
+ return store.getString(MML_PATH);
+ }
+
+ public void setMmlPath(String mmlPath) {
+ store.setValue(MML_PATH, mmlPath.isBlank() ? "" : new File(mmlPath).getAbsolutePath());
+ }
}