Skip to content

Commit

Permalink
Merge pull request #6001 from IllianiCBT/immersiveDialog_glossary
Browse files Browse the repository at this point in the history
Added Glossary Functionality to `MHQDialogImmersive` with Clickable Hyperlink Support
  • Loading branch information
IllianiCBT authored Feb 8, 2025
2 parents 2b3e932 + 8d014d8 commit 890625f
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 59 deletions.
12 changes: 12 additions & 0 deletions MekHQ/resources/mekhq/resources/Glossary.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
PRISONER_CAPACITY.title=Prisoner Capacity
PRISONER_CAPACITY.definition=Prisoner Capacity is a measure of how many prisoners your Security\
\ personnel can manage without potential crisis. Generally speaking, each prisoner takes up 1\
\ capacity; however, some events may change this.\
<p>For additional information on Prisoner Capacity and how to manage it, please see the\
\ documentation in...\
<br>MekHQ/docs/Stratcon and Against the Bot/Prisoners.pdf</p>

REPUTATION.title=Reputation
REPUTATION.definition=Reputation is a measure of how well known your unit is and whether people\
\ generally believe you can get the job done. The rules for Reputation can be found in Campaign\
\ Operations.
94 changes: 74 additions & 20 deletions MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@
import mekhq.campaign.personnel.Person;
import mekhq.campaign.personnel.enums.PersonnelRole;
import mekhq.campaign.unit.Unit;
import mekhq.gui.CampaignGUI;
import mekhq.gui.dialog.GlossaryDialog;

import javax.swing.*;
import javax.swing.event.HyperlinkEvent.EventType;
import java.awt.*;
import java.util.List;
import java.util.UUID;

import static java.lang.Math.max;
import static megamek.client.ui.WrapLayout.wordWrap;
Expand All @@ -52,12 +55,14 @@
*/
public class MHQDialogImmersive extends JDialog {
private final String RESOURCE_BUNDLE = "mekhq.resources.GUI";
public final static String GLOSSARY_COMMAND_STRING = "GLOSSARY";
public final static String PERSON_COMMAND_STRING = "PERSON";

private Campaign campaign;

private int CENTER_WIDTH = UIUtil.scaleForGUI(400);

private final int INSERT_SIZE = UIUtil.scaleForGUI(5);
private final int PADDING = UIUtil.scaleForGUI(5);
protected final int IMAGE_WIDTH = 125; // This is scaled to GUI by 'scaleImageIconToWidth'

private JPanel northPanel;
Expand Down Expand Up @@ -115,7 +120,7 @@ public MHQDialogImmersive(Campaign campaign, @Nullable Person leftSpeaker,
// Main Panel to hold all boxes
JPanel mainPanel = new JPanel(new GridBagLayout());
GridBagConstraints constraints = new GridBagConstraints();
constraints.insets = new Insets(INSERT_SIZE, INSERT_SIZE, INSERT_SIZE, INSERT_SIZE);
constraints.insets = new Insets(PADDING, PADDING, PADDING, PADDING);
constraints.fill = GridBagConstraints.BOTH;
constraints.weighty = 1;

Expand Down Expand Up @@ -222,7 +227,7 @@ private JPanel createCenterBox(String centerMessage, List<ButtonLabelTooltipPair
// Add a HyperlinkListener to capture hyperlink clicks
editorPane.addHyperlinkListener(evt -> {
if (evt.getEventType() == EventType.ACTIVATED) {
handleHyperlinkClick(campaign, evt.getDescription());
handleImmersiveHyperlinkClick(this, campaign, evt.getDescription());
}
});

Expand All @@ -233,7 +238,7 @@ private JPanel createCenterBox(String centerMessage, List<ButtonLabelTooltipPair

// Create a container with a border for the padding
JPanel scrollPaneContainer = new JPanel(new BorderLayout());
scrollPaneContainer.setBorder(BorderFactory.createEmptyBorder(INSERT_SIZE, 0, INSERT_SIZE, 0));
scrollPaneContainer.setBorder(BorderFactory.createEmptyBorder(PADDING, 0, PADDING, 0));
scrollPaneContainer.add(scrollPane, BorderLayout.CENTER);

// Add the scrollPane with padding to the northPanel
Expand All @@ -248,18 +253,46 @@ private JPanel createCenterBox(String centerMessage, List<ButtonLabelTooltipPair
}

/**
* Handles hyperlink clicks from HTML content.
* Handles hyperlink clicks from HTML content dialog.
*
* <p>
* This method processes the provided hyperlink reference to determine the type of command
* and executes the appropriate action. It supports commands for displaying a glossary
* entry or focusing on a specific person in the campaign.
* </p>
*
* <b>Supported Commands:</b>
* <ul>
* <li>{@code GLOSSARY_COMMAND_STRING}: Opens a new {@link GlossaryDialog} to display the
* referenced glossary entry.</li>
* <li>{@code PERSON_COMMAND_STRING}: Focuses on a specific person in the campaign using
* their unique identifier (UUID). If using this, you will need to ensure your dialog has
* modal set to {@code false}</li>
* </ul>
*
* <p>
* <b>Usage</b><br>
* This method provides a default implementation that does nothing. Subclasses should
* override this to provide specific behavior when hyperlinks are clicked.
* If the command is not recognized, no action is performed.
* </p>
*
* @param campaign The {@link Campaign} instance that contains relevant data.
* @param href The hyperlink reference (e.g., a URL or a specific identifier).
* @param parent The parent {@link JDialog} instance to associate with the new dialog, if created.
* @param campaign The {@link Campaign} instance that contains application and campaign data.
* @param reference The hyperlink reference used to determine the command and additional
* information (e.g., a specific glossary term key or a person's UUID).
*/
protected void handleHyperlinkClick(Campaign campaign, String href) {
logger.error("handleHyperlinkClick() was not overridden in the subclass.");
public static void handleImmersiveHyperlinkClick(JDialog parent, Campaign campaign, String reference) {
String[] splitReference = reference.split(":");

String commandKey = splitReference[0];
String entryKey = splitReference[1];

if (commandKey.equals(GLOSSARY_COMMAND_STRING)) {
new GlossaryDialog(parent, campaign, entryKey);
} else if (commandKey.equals(PERSON_COMMAND_STRING)) {
CampaignGUI campaignGUI = campaign.getApp().getCampaigngui();

final UUID id = UUID.fromString(reference.split(":")[1]);
campaignGUI.focusOnPerson(id);
}
}

/**
Expand All @@ -272,16 +305,37 @@ protected void handleHyperlinkClick(Campaign campaign, String href) {
*/
private void populateOutOfCharacterPanel(String outOfCharacterMessage) {
JPanel pnlOutOfCharacter = new JPanel(new GridBagLayout());
pnlOutOfCharacter.setBorder(BorderFactory.createEtchedBorder());

JLabel lblOutOfCharacter = new JLabel(
String.format("<html><div style='width: %dpx'>%s</div></html>",
CENTER_WIDTH, outOfCharacterMessage));
lblOutOfCharacter.setBorder(BorderFactory.createEmptyBorder(INSERT_SIZE, INSERT_SIZE,
INSERT_SIZE, INSERT_SIZE));
// Create a compound border with an etched border and padding (empty border)
pnlOutOfCharacter.setBorder(
BorderFactory.createEtchedBorder()
);

// Create a JEditorPane for the message
JEditorPane editorPane = new JEditorPane();
editorPane.setContentType("text/html");
editorPane.setEditable(false);
editorPane.setFocusable(false);

int width = CENTER_WIDTH;
width += leftSpeaker != null ? IMAGE_WIDTH + PADDING : 0;
width += rightSpeaker != null ? IMAGE_WIDTH + PADDING : 0;

// Use inline CSS to set font family, size, and other style properties
editorPane.setText(String.format("<div style='width: %s'>%s</div>", width, outOfCharacterMessage));
setFontScaling(editorPane, false, 1);

// Add a HyperlinkListener to capture hyperlink clicks
editorPane.addHyperlinkListener(evt -> {
if (evt.getEventType() == EventType.ACTIVATED) {
handleImmersiveHyperlinkClick(this, campaign, evt.getDescription());
}
});

pnlOutOfCharacter.add(lblOutOfCharacter);
// Add the editor pane to the panel
pnlOutOfCharacter.add(editorPane);

// Add the panel to the southPanel
southPanel.add(pnlOutOfCharacter, BorderLayout.SOUTH);
}

Expand All @@ -299,7 +353,7 @@ private void populateButtonPanel(List<ButtonLabelTooltipPair> buttons) {
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.insets = new Insets(INSERT_SIZE, INSERT_SIZE, INSERT_SIZE, INSERT_SIZE);
gbc.insets = new Insets(PADDING, PADDING, PADDING, PADDING);
gbc.anchor = GridBagConstraints.WEST;
gbc.fill = GridBagConstraints.NONE;

Expand Down
169 changes: 169 additions & 0 deletions MekHQ/src/mekhq/gui/dialog/GlossaryDialog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* Copyright (c) 2025 - The MegaMek Team. All Rights Reserved.
*
* This file is part of MekHQ.
*
* MekHQ 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.
*
* MekHQ 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 MekHQ. If not, see <http://www.gnu.org/licenses/>.
*/
package mekhq.gui.dialog;

import megamek.client.ui.swing.util.UIUtil;
import megamek.logging.MMLogger;
import mekhq.campaign.Campaign;

import javax.swing.*;
import javax.swing.event.HyperlinkEvent.EventType;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import static java.lang.Math.round;
import static javax.swing.BorderFactory.createEmptyBorder;
import static megamek.client.ui.swing.util.FlatLafStyleBuilder.setFontScaling;
import static mekhq.gui.baseComponents.MHQDialogImmersive.handleImmersiveHyperlinkClick;
import static mekhq.utilities.MHQInternationalization.getFormattedTextAt;
import static mekhq.utilities.MHQInternationalization.isResourceKeyValid;

/**
* The {@code GlossaryDialog} class represents a dialog window for displaying glossary entries.
* It displays detailed information about a glossary term, including its title and description,
* in a styled HTML format.
*
* <p>
* This class uses a {@link JEditorPane} to render glossary entry content and supports hyperlink
* interactions for related glossary entries. If a related term is clicked, a new {@code GlossaryDialog}
* is opened to show its details.
* </p>
*/
public class GlossaryDialog extends JDialog {
private static final MMLogger logger = MMLogger.create(GlossaryDialog.class);

private final JDialog parent;
private final Campaign campaign;

private int CENTER_WIDTH = UIUtil.scaleForGUI(400);
private int CENTER_HEIGHT = UIUtil.scaleForGUI(300);
private int PADDING = UIUtil.scaleForGUI(10);

private final String GLOSSARY_BUNDLE = "mekhq.resources.Glossary";

/**
* Creates a new {@code GlossaryDialog} instance to display information about a glossary term.
*
* <p>
* The dialog retrieves the glossary term's title and description using the provided key
* and displays the content in a styled format. During its construction, the parent dialog
* is hidden to ensure that only this dialog is visible to the user.
* </p>
*
* @param parent The parent {@link JDialog} that is temporarily hidden while this dialog is displayed.
* @param campaign The {@link Campaign} object containing resources and glossary entries.
* @param key The unique identifier for the glossary term to be displayed.
*/
public GlossaryDialog(JDialog parent, Campaign campaign, String key) {
this.parent = parent;
this.campaign = campaign;

parent.setVisible(false);
buildDialog(key);
}

/**
* Builds the Glossary Dialog by setting its title and definition based on the key provided.
*
* <p>
* This method fetches the title and definition strings for the glossary term from the
* resource bundle. If the title is invalid (i.e., the resource key is not found),
* it logs an error and terminates the dialog building process.
* </p>
*
* @param key The resource key used to retrieve the glossary term's title and definition.
*/
private void buildDialog(String key) {
String title = getFormattedTextAt(GLOSSARY_BUNDLE, key + ".title");
if (!isResourceKeyValid(title)) {
logger.error("No valid title for {}", key);
return;
}

String description = getFormattedTextAt(GLOSSARY_BUNDLE, key + ".definition");
if (!isResourceKeyValid(description)) {
logger.error("No valid definition for {}", key);
return;
}

setTitle(title);

// Create a JEditorPane for the message
JEditorPane editorPane = new JEditorPane();
editorPane.setContentType("text/html");
editorPane.setEditable(false);
editorPane.setFocusable(false);

// Use inline CSS to set font family, size, and other style properties
String fontStyle = "font-family: Noto Sans;";
editorPane.setText(String.format(
"<div style='width: %s; %s'>"
+ "<h1 style='text-align: center;'>%s</h1>"
+ "%s</div>",
CENTER_WIDTH, fontStyle, title, description
));
setFontScaling(editorPane, false, 1.1);

// Add a HyperlinkListener to capture hyperlink clicks
editorPane.addHyperlinkListener(evt -> {
if (evt.getEventType() == EventType.ACTIVATED) {
handleImmersiveHyperlinkClick(parent, campaign, evt.getDescription());
}
});

// Wrap the JEditorPane in a JScrollPane
JScrollPane scrollPane = new JScrollPane(editorPane);
scrollPane.setMinimumSize(new Dimension(CENTER_WIDTH, scrollPane.getHeight()));

// Create a JPanel with padding
JPanel paddedPanel = new JPanel(new BorderLayout());
paddedPanel.setBorder(createEmptyBorder(PADDING, PADDING, PADDING, PADDING));
paddedPanel.add(scrollPane, BorderLayout.CENTER);
add(paddedPanel);

// Assign close action
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
onCloseAction();
}
});

// Set dialog properties
setSize((int) round((CENTER_WIDTH + (PADDING * 2)) * 1.1), CENTER_HEIGHT);
setLocationRelativeTo(null);
setModal(true);
setVisible(true);
}

/**
* Handles user interactions when the dialog is closed.
*
* <p>
* This method ensures the parent dialog is made visible again after the glossary
* dialog is closed.
* </p>
*/
private void onCloseAction() {
dispose();
parent.setVisible(true);
}
}
21 changes: 2 additions & 19 deletions MekHQ/src/mekhq/gui/dialog/VocationalExperienceAwardDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,9 @@
import mekhq.campaign.CampaignOptions;
import mekhq.campaign.mission.AtBContract;
import mekhq.campaign.personnel.Person;
import mekhq.gui.CampaignGUI;
import mekhq.gui.baseComponents.MHQDialogImmersive;

import java.util.List;
import java.util.UUID;

import static mekhq.campaign.Campaign.AdministratorSpecialization.HR;
import static mekhq.utilities.MHQInternationalization.getFormattedTextAt;
Expand All @@ -42,6 +40,8 @@
* personnel records via hyperlinks.</p>
*/
public class VocationalExperienceAwardDialog extends MHQDialogImmersive {
private static final String PERSON_COMMAND_STRING = "PERSON";

private static final String RESOURCE_BUNDLE = "mekhq.resources.VocationalExperienceAwardDialog";

/**
Expand All @@ -61,23 +61,6 @@ public VocationalExperienceAwardDialog(Campaign campaign) {
setAlwaysOnTop(true);
}

/**
* Handles the hyperlink click event in the dialog.
*
* <p>This method parses the hyperlink reference to focus on the personnel record identified by
* the provided UUID in the campaign's graphical user interface.</p>
*
* @param campaign the {@link Campaign} containing relevant personnel data
* @param hyperlinkReference the hyperlink reference containing the UUID of the selected character
*/
@Override
protected void handleHyperlinkClick(Campaign campaign, String hyperlinkReference) {
CampaignGUI campaignGUI = campaign.getApp().getCampaigngui();

final UUID id = UUID.fromString(hyperlinkReference.split(":")[1]);
campaignGUI.focusOnPerson(id);
}

/**
* Creates the list of buttons to be displayed in the dialog.
*
Expand Down
Loading

0 comments on commit 890625f

Please sign in to comment.