Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow printing unit list from MM #6190

Merged
merged 2 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions megamek/i18n/megamek/client/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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=<html>Print the record sheets for the current player's units.\
<br>This requires MegaMekLab to exist on your system.</html>
ChatLounge.butPrintList.printing=Loading print dialog
ChatLounge.butShrink=<
ChatLounge.butSkills=Random Skills...
ChatLounge.butShowUnitID=Show IDs
Expand Down Expand Up @@ -1270,6 +1274,10 @@ CommonSettingsDialog.logFileName=Game log filename:
CommonSettingsDialog.userDir=User Files Directory:
CommonSettingsDialog.userDir.tooltip=<html>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.<BR>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/. <BR>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=<html>Used for printing unit lists.\
<br>MegaMek will try to autodetect this when the option is blank if MM and MML are installed together.</html>
CommonSettingsDialog.mmlPath.chooser.title=Select MegaMekLab Executable
CommonSettingsDialog.main=Main
CommonSettingsDialog.audio=Audio
CommonSettingsDialog.miniMap=Mini Map
Expand Down
93 changes: 93 additions & 0 deletions megamek/src/megamek/client/ui/swing/ClientGUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -2069,6 +2069,99 @@
}
}

/**
* 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<Entity> 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");
Fixed Show fixed Hide fixed

Check warning

Code scanning / CodeQL

Local information disclosure in a temporary directory Medium

Local information disclosure vulnerability due to use of file readable by other local users.
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();

Expand Down
36 changes: 31 additions & 5 deletions megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ private <T> void moveElement(DefaultListModel<T> 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"));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -3452,13 +3472,19 @@ public static List<String> 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());
}
}
}
18 changes: 16 additions & 2 deletions megamek/src/megamek/client/ui/swing/lobby/ChatLounge.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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());
}
}
Expand Down
11 changes: 11 additions & 0 deletions megamek/src/megamek/common/preference/ClientPreferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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());
}
}