Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
da88a4a
add processing of multiple fluff images
SJuliez May 20, 2024
bd2d0fa
allow clicking the fluff in the unit view to advance to the next image
SJuliez May 20, 2024
f26549a
cleanup
SJuliez May 20, 2024
2c6663f
allow clan mek chassis variants, require chassis in model subdir name
SJuliez May 21, 2024
8fa601d
clean up chassis name generation
SJuliez May 22, 2024
494ca0d
Merging PR's
HammerGS May 25, 2024
4878705
show next/prev buttons for fluff images, show filename (WIP)
SJuliez Aug 3, 2024
9382bef
Merge branch 'refs/heads/master' into WIP-multi-fluff
SJuliez Aug 3, 2024
890ac0f
remove image click mouse listener
SJuliez Aug 3, 2024
6696ed3
Merge branch 'master' into multi-fluff
HammerGS Aug 3, 2024
60b69b6
Fixing
HammerGS Aug 6, 2024
0985559
Add a doc on AI usage (not Princess related) to MegaMek
HammerGS Aug 9, 2024
4f4c794
only take image files as fluff images
SJuliez Aug 10, 2024
8c253ae
add yaml parsed fluff tooltip to the MechViewPanel
SJuliez Aug 13, 2024
4d1af5c
show placeholder for units without fluff
SJuliez Aug 18, 2024
15eef9a
show info below fluff image
SJuliez Aug 23, 2024
ce52f03
Merge branch 'master' into WIP-multi-fluff
SJuliez Feb 26, 2025
d5953a3
after merge changes
SJuliez Feb 27, 2025
7033c71
Updating history.txts
HammerGS Mar 1, 2025
005179b
Merge branch 'main' into multi-fluff
SJuliez Oct 14, 2025
d72c30e
aftermerge corrections
SJuliez Oct 15, 2025
b5fafaa
Merge main into multi-fluff
HammerGS Jan 10, 2026
3218c68
Fix: Use findFluffFiles instead of undefined findFluffFile method
HammerGS Jan 10, 2026
1fd5a25
Address Copilot review feedback
HammerGS Jan 10, 2026
e5e14d7
Merge branch 'main' into multi-fluff
HammerGS Jan 23, 2026
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
155 changes: 155 additions & 0 deletions megamek/src/megamek/client/ui/FluffImageTooltip.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
*
* This file is part of MegaMek.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package megamek.client.ui;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import megamek.client.ui.clientGUI.GUIPreferences;
import megamek.client.ui.util.FluffImageHelper;
import megamek.client.ui.util.UIUtil;
import org.apache.logging.log4j.LogManager;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

/**
* This class is very specialized. It provides tooltip information for the fluff image tooltip in the
* MechViewPanel, taken from yaml files that are supplied with painted minis images.
*/
public class FluffImageTooltip {

private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());

public static String styles() {
int labelSize = UIUtil.scaleForGUI(UIUtil.FONT_SCALE1);
Color color = GUIPreferences.getInstance().getToolTipLightFGColor();
String styleColor = Integer.toHexString(color.getRGB() & 0xFFFFFF);
return "span { font-family:Noto Sans; font-size:" + labelSize + "; }"
+ ".label { color:" + styleColor + "; }";
}

/**
* Returns the tooltip text for the supplied FluffImageRecord, if any can be found, null otherwise.
*
* @param record The FluffImageRecord that is currently shown as an image
* @return A tooltip text or null if no yaml info is available
*/
public static String getTooltip(FluffImageHelper.FluffImageRecord record) {
return findYamlInfo(record).map(FluffImageTooltip::getTooltip).orElse(null);
}

private static Optional<File> findYamlInfo(FluffImageHelper.FluffImageRecord record) {
return (record.file() == null) ? Optional.empty() : getYamlFile(record.file());
}

private static String getTooltip(File yamlFile) {
try {
JsonNode node = yamlMapper.readTree(yamlFile);

StringBuilder result = new StringBuilder("<HTML><HEAD><STYLE>" + styles() + "</STYLE></HEAD><BODY>");
int width = UIUtil.scaleForGUI(360);
result.append("<div width=").append(width).append(">");

if (node.has("title")) {
String unit = node.get("title").asText();
if (!unit.isBlank()) {
result.append(UIUtil.spanCSS("label", "Unit: "))
.append(UIUtil.spanCSS("value", unit));
}
}
if (node.has("author")) {
String artist = node.get("author").asText();
if (!artist.isBlank()) {
result.append(UIUtil.spanCSS("label", "<BR>Artist: "))
.append(UIUtil.spanCSS("value", artist));
}
}
if (node.has("content")) {
JsonNode contentNode = node.get("content");
String description = findInsignia(contentNode);
if (!description.isBlank()) {
result.append(UIUtil.spanCSS("label", "<BR>Insignia: "))
.append(UIUtil.spanCSS("value", description));
}
}
result.append("</div></BODY></HTML>");
return result.toString();
} catch (IOException e) {
return null;
}
}

private static String findInsignia(JsonNode contentNode) {
List<JsonNode> nodes = new ArrayList<>();
contentNode.iterator().forEachRemaining(nodes::add);
for (JsonNode node : nodes) {
if (node.has("type") && node.get("type").asText().equals("insignia")) {
return node.get("content").asText();
}
}
return "";
}

private static Optional<File> getYamlFile(File imageFile) {
File parent = imageFile.getParentFile();
if (parent == null) {
LogManager.getLogger().warn("Image file {} has no parent directory; cannot search for YAML.", imageFile);
return Optional.empty();
}
try (Stream<Path> entries = Files.walk(parent.toPath(), 1)) {
return entries.filter(p -> isSuitableYamlFile(p, imageFile)).map(Path::toFile).findFirst();
} catch (Exception e) {
LogManager.getLogger().warn("Error while reading files from {}", parent, e);
return Optional.empty();
}
}

private static boolean isSuitableYamlFile(Path yamlFile, File imageFile) {
if (Files.isDirectory(yamlFile)) {
return false;
}

Path yamlFileNamePath = yamlFile.getFileName();
if (yamlFileNamePath == null) {
return false;
}

String yamlFileName = yamlFileNamePath.toString();
String suffix = "data.yaml";
if (!yamlFileName.endsWith(suffix)) {
return false;
}

int baseLength = yamlFileName.length() - suffix.length();
if (baseLength <= 0) {
return false;
}

String baseName = yamlFileName.substring(0, baseLength);
return imageFile.getName().contains(baseName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,22 @@
import java.awt.BorderLayout;
import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.imageio.ImageIO;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
Expand All @@ -57,12 +63,13 @@
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLDocument;

import megamek.client.ui.FluffImageTooltip;
import megamek.client.ui.entityreadout.EntityReadout;
import megamek.client.ui.entityreadout.ReadoutSections;
import megamek.client.ui.util.FluffImageHelper;
import megamek.client.ui.util.UIUtil;
import megamek.client.ui.util.UIUtil.FixedXPanel;
import megamek.client.ui.util.ViewFormatting;
import megamek.common.Configuration;
import megamek.common.Report;
import megamek.common.preference.PreferenceManager;
import megamek.common.templates.TROView;
Expand All @@ -78,8 +85,19 @@ public class EntityReadoutPanel extends JPanel {
private final JLabel fluffImageComponent = new JLabel();
private final JScrollPane scrollPane = new JScrollPane(readoutTextComponent);

private final JLabel fluffImageLabel = new JLabel();
private final List<FluffImageHelper.FluffImageRecord> fluffImageList = new ArrayList<>();
private int fluffImageIndex = 0;
private final JButton nextImageButton = new JButton(">");
private final JButton prevImageButton = new JButton("<");
private final JLabel imageInfoLabel = new JLabel("", JLabel.CENTER);

public static final int DEFAULT_WIDTH = 360;

private static final String PLACEHOLDER_IMAGE_NAME =
new File(Configuration.fluffImagesDir(), "fluff_placeholder.png").getPath();
private static final Image PLACEHOLDER_IMAGE = readPlaceHolderImage();

public EntityReadoutPanel() {
this(-1, -1);
}
Expand Down Expand Up @@ -141,21 +159,31 @@ public void mouseMoved(MouseEvent e) {
}
textPanel.add(scrollPane);

var fluffPanel = new FixedXPanel();
if (width != -1) {
fluffPanel.setMinimumSize(new Dimension(width, height));
fluffPanel.setPreferredSize(new Dimension(width, height));
}
fluffPanel.add(fluffImageComponent);
var imageControlsPanel = new UIUtil.FixedYPanel(new FlowLayout());
imageControlsPanel.add(prevImageButton);
imageControlsPanel.add(nextImageButton);

JPanel p = new JPanel();
p.setLayout(new BoxLayout(p, BoxLayout.LINE_AXIS));
imageControlsPanel.setAlignmentX(0.5f);
fluffImageLabel.setAlignmentX(0.5f);
imageInfoLabel.setAlignmentX(0.5f);

Box fluffPanel = Box.createVerticalBox();
fluffPanel.setAlignmentY(0);
fluffPanel.add(imageControlsPanel);
fluffPanel.add(fluffImageLabel);
fluffPanel.add(Box.createVerticalStrut(10));
fluffPanel.add(imageInfoLabel);

Box p = Box.createHorizontalBox();
p.add(textPanel);
p.add(fluffPanel);
p.add(Box.createHorizontalGlue());
setLayout(new BorderLayout());
add(p);
addMouseWheelListener(wheelForwarder);

nextImageButton.addActionListener(e -> showNextFluffImage());
prevImageButton.addActionListener(e -> showPrevFluffImage());
}

public void showEntity(Entity entity, EntityReadout mekView) {
Expand Down Expand Up @@ -209,17 +237,34 @@ public void showEntity(Entity entity, boolean showDetail, boolean useAlternateCo
showEntity(entity, mekView, fontName, sections);
}

private void setFluffImage(Entity entity) {
boolean isSpritesOnly = PreferenceManager.getClientPreferences().getSpritesOnly();
Image image = isSpritesOnly ? null : FluffImageHelper.getFluffImage(entity);
private void setFluffImage(Image image) {
// Scale down to the default width if the image is wider than that
if (null != image) {
if (image.getWidth(this) > DEFAULT_WIDTH) {
image = image.getScaledInstance(DEFAULT_WIDTH, -1, Image.SCALE_SMOOTH);
}
fluffImageComponent.setIcon(new ImageIcon(image));
fluffImageLabel.setIcon(new ImageIcon(image));
} else {
fluffImageLabel.setIcon(null);
fluffImageLabel.setToolTipText(null);
}
}

private void setFluffImage(Entity entity) {
boolean isSpritesOnly = PreferenceManager.getClientPreferences().getSpritesOnly();
if (isSpritesOnly) {
setFluffImage((Image) null);
return;
}
fluffImageList.clear();
fluffImageList.addAll(FluffImageHelper.getFluffRecords(entity));
fluffImageIndex = 0;
nextImageButton.setEnabled(fluffImageList.size() > 1);
prevImageButton.setEnabled(fluffImageList.size() > 1);
if (entity != null) {
showNextFluffImage();
} else {
fluffImageComponent.setIcon(null);
setFluffImage((Image) null);
}
}

Expand All @@ -236,4 +281,44 @@ public void reset() {
listener.mouseWheelMoved(converted);
}
};

private void showNextFluffImage() {
changeFluffImageIndex(1);
}

private void showPrevFluffImage() {
changeFluffImageIndex(-1);
}

private void changeFluffImageIndex(int delta) {
fluffImageIndex += delta;
if (fluffImageIndex >= fluffImageList.size()) {
fluffImageIndex = 0;
}
if (fluffImageIndex < 0) {
fluffImageIndex = fluffImageList.size() - 1;
}
if ((fluffImageIndex >= 0) && (fluffImageIndex < fluffImageList.size())) {
try {
FluffImageHelper.FluffImageRecord record = fluffImageList.get(fluffImageIndex);
setFluffImage(record.getImage());
fluffImageLabel.setToolTipText(FluffImageTooltip.getTooltip(record));
imageInfoLabel.setText(FluffImageTooltip.getTooltip(record));
Comment thread
HammerGS marked this conversation as resolved.
} catch (IOException ex) {
setFluffImage((Image) null);
imageInfoLabel.setText("Error loading fluff image");
}
} else {
setFluffImage(PLACEHOLDER_IMAGE);
imageInfoLabel.setText("");
}
}

private static Image readPlaceHolderImage() {
try {
return ImageIO.read(new File(PLACEHOLDER_IMAGE_NAME));
} catch (IOException e) {
return null;
}
}
Comment thread
HammerGS marked this conversation as resolved.
}
Loading