From c4372d6fa17d5453e7ce28fb101418b971fa0473 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Fri, 8 Nov 2024 23:05:13 -0800 Subject: [PATCH 1/2] Allow printing unit list from MM --- .../i18n/megamek/client/messages.properties | 6 ++ .../megamek/client/ui/swing/ClientGUI.java | 65 +++++++++++++++++++ .../client/ui/swing/CommonSettingsDialog.java | 36 ++++++++-- .../client/ui/swing/lobby/ChatLounge.java | 17 ++++- .../common/preference/ClientPreferences.java | 11 ++++ 5 files changed, 128 insertions(+), 7 deletions(-) diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 4c96ba383f0..c283ac15e35 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -647,6 +647,8 @@ ChatLounge.butGroundMap = Ground Map ChatLounge.butNames=Random Names... ChatLounge.butRemoveBot=Remove Bot ChatLounge.butSaveList=Save Unit List... +ChatLounge.butPrintList=Print Unit List... +ChatLounge.butPrintList.printing=Loading print dialog ChatLounge.butShrink=< ChatLounge.butSkills=Random Skills... ChatLounge.butShowUnitID=Show IDs @@ -1270,6 +1272,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..3a6d459c4b3 100644 --- a/megamek/src/megamek/client/ui/swing/ClientGUI.java +++ b/megamek/src/megamek/client/ui/swing/ClientGUI.java @@ -2069,6 +2069,71 @@ public void saveListFile(ArrayList unitList, String filename) { } } + public void printList(ArrayList unitList, JButton button) { + if ((unitList == null) || unitList.isEmpty()) { + return; + } + + 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 { + var unitFile = File.createTempFile("MegaMekPrint", ".mul"); + EntityListFile.saveTo(unitFile, unitList); + String[] command; + if (mml.getName().toLowerCase().contains("gradle")) { + command = new String[] { + mml.getAbsolutePath(), + "run", + "--args=%s --no-startup".formatted(unitFile.getAbsolutePath()) + }; + } else { + command = new String[] { + mml.getAbsolutePath(), + unitFile.getAbsolutePath(), + "--no-startup" + }; + } + 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(); + new Thread(() -> { + try { + p.waitFor(); + } catch (InterruptedException e) { + logger.error(e); + } finally { + button.setText(Messages.getString("ChatLounge.butPrintList")); + } + }).start(); + + } catch (Exception e) { + logger.error(e, "Operation failed", "Error printing unit list"); + } + } + 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..40285e77c44 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,8 @@ private void setupUnitConfig() { butLoadList.setEnabled(mscLoaded); butSaveList.setActionCommand(CL_ACTIONCOMMAND_SAVELIST); butSaveList.setEnabled(false); + butPrintList.setActionCommand(CL_ACTIONCOMMAND_PRINTLIST); + butPrintList.setEnabled(false); butAdd.setEnabled(mscLoaded); butAdd.setActionCommand(CL_ACTIONCOMMAND_LOADMEK); butArmy.setEnabled(mscLoaded); @@ -561,6 +566,7 @@ private void setupUnitConfig() { panUnitInfoGrid.add(butLoadList); panUnitInfoGrid.add(butSaveList); panUnitInfoGrid.add(butNames); + panUnitInfoGrid.add(butPrintList); panUnitInfo.add(panUnitInfoAdd); panUnitInfo.add(panUnitInfoGrid); @@ -1739,7 +1745,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 +1758,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 +2295,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 +2415,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()); + } } From 91a30a4864efd520d1e71a82096b3e4599a257f9 Mon Sep 17 00:00:00 2001 From: Pavel Braginskiy Date: Sun, 10 Nov 2024 10:40:59 -0800 Subject: [PATCH 2/2] Add comments and tooltip --- .../i18n/megamek/client/messages.properties | 2 ++ .../megamek/client/ui/swing/ClientGUI.java | 28 +++++++++++++++++++ .../client/ui/swing/lobby/ChatLounge.java | 1 + 3 files changed, 31 insertions(+) diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index c283ac15e35..a76cdb07d0e 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -648,6 +648,8 @@ 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... diff --git a/megamek/src/megamek/client/ui/swing/ClientGUI.java b/megamek/src/megamek/client/ui/swing/ClientGUI.java index 3a6d459c4b3..937c6bd9dad 100644 --- a/megamek/src/megamek/client/ui/swing/ClientGUI.java +++ b/megamek/src/megamek/client/ui/swing/ClientGUI.java @@ -2069,11 +2069,22 @@ 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()) { @@ -2097,28 +2108,42 @@ public void printList(ArrayList unitList, JButton button) { } 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(); @@ -2130,7 +2155,10 @@ public void printList(ArrayList unitList, JButton button) { }).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")); } } diff --git a/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java b/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java index 40285e77c44..708c3f0d300 100644 --- a/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java +++ b/megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java @@ -551,6 +551,7 @@ private void setupUnitConfig() { 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);