/* * Copyright (C) 2016 MegaMek team * * 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 2 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 java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.awt.FontFormatException; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Image; import java.awt.Insets; import java.awt.Point; import java.awt.Window; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.ResourceBundle; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.UIManager; import org.joda.time.chrono.GJChronology; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import megamek.common.util.EncodeControl; import mekhq.IconPackage; import mekhq.MekHQ; import mekhq.Utilities; import mekhq.campaign.Campaign; import mekhq.campaign.ExtraData; import mekhq.campaign.LogEntry; import mekhq.campaign.force.Force; import mekhq.campaign.personnel.BodyLocation; import mekhq.campaign.personnel.Injury; import mekhq.campaign.personnel.InjuryLevel; import mekhq.campaign.personnel.InjuryType; import mekhq.campaign.personnel.Person; import mekhq.gui.view.Paperdoll; public class MedicalViewDialog extends JDialog { private static final long serialVersionUID = 6178230374580087883L; private static final String MENU_CMD_SEPARATOR = ","; //$NON-NLS-1$ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); //$NON-NLS-1$ private final static DateTimeFormatter DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd").withChronology(GJChronology.getInstanceUTC()); //$NON-NLS-1$ private static final ExtraData.Key<String> DOCTOR_NOTES = new ExtraData.StringKey("doctor_notes"); //$NON-NLS-1$ // TODO: Custom paper dolls @SuppressWarnings("unused") private static final ExtraData.Key<String> PAPERDOLL = new ExtraData.StringKey("paperdoll_xml_file"); //$NON-NLS-1$ private final Campaign campaign; private final Person person; private ResourceBundle resourceMap = null; private Paperdoll defaultMaleDoll; private Paperdoll defaultFemaleDoll; private JPanel dollWrapper; private Paperdoll doll; private JPanel injuryPanel; private JTextArea notesArea; private ActionListener dollActionListener; private transient Font labelFont; private transient Font handwritingFont; private transient Color labelColor; private transient ImageIcon healImageIcon; private boolean gmMode; public MedicalViewDialog(Window parent, Campaign c, Person p, IconPackage ip) { super(); this.campaign = Objects.requireNonNull(c); this.person = Objects.requireNonNull(p); //this.iconPackage = Objects.requireNonNull(ip); resourceMap = ResourceBundle.getBundle("mekhq.resources.MedicalViewDialog", new EncodeControl()); //$NON-NLS-1$ // Preload default paperdolls try(InputStream fis = new FileInputStream(ip.getGuiElement("default_male_paperdoll"))) { //$NON-NLS-1$ defaultMaleDoll = new Paperdoll(fis); } catch(IOException e) { MekHQ.logError(e); } try(InputStream fis = new FileInputStream(ip.getGuiElement("default_female_paperdoll"))) { //$NON-NLS-1$ defaultFemaleDoll = new Paperdoll(fis); } catch(IOException e) { MekHQ.logError(e); } setPreferredSize(new Dimension(1024, 840)); setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); setLocationRelativeTo(parent); labelFont = UIManager.getDefaults().getFont("Menu.font").deriveFont(Font.PLAIN, 16); //$NON-NLS-1$ try(InputStream fis = new FileInputStream("data/fonts/angelina.TTF")) { //$NON-NLS-1$ handwritingFont = Font.createFont(Font.TRUETYPE_FONT, fis).deriveFont(Font.PLAIN, 22); } catch (FontFormatException | IOException e) { handwritingFont = null; } labelColor = new Color(170, 170, 170); healImageIcon = new ImageIcon(new ImageIcon("data/images/misc/medical.png").getImage().getScaledInstance(16, 16, Image.SCALE_DEFAULT)); //$NON-NLS-1$ dollActionListener = ae -> { final BodyLocation loc = BodyLocation.of(ae.getActionCommand()); final boolean locationPicked = !loc.readableName.isEmpty(); Point mousePos = doll.getMousePosition(); JPopupMenu popup = new JPopupMenu(); if(locationPicked) { JLabel header = new JLabel(Utilities.capitalize(loc.readableName)); header.setFont(UIManager.getDefaults().getFont("Menu.font").deriveFont(Font.BOLD)); //$NON-NLS-1$ popup.add(header); popup.addSeparator(); } if(locationPicked) { ActionListener addActionListener = addEvent -> { String[] commands = addEvent.getActionCommand().split(MENU_CMD_SEPARATOR, 2); InjuryType addIType = InjuryType.byKey(commands[0]); int severity = Integer.valueOf(commands[1]); person.addInjury(addIType.newInjury(campaign, person, loc, severity)); revalidate(); }; JMenu addMenu = new JMenu(resourceMap.getString("menuAdd.text")); //$NON-NLS-1$ InjuryType.getAllTypes().stream().filter(it -> it.isValidInLocation(loc)) .sorted((it1, it2) -> it1.getSimpleName().compareToIgnoreCase(it2.getSimpleName())) .forEach(it -> { IntStream.range(1, it.getMaxSeverity() + 1).forEach(severity -> { JMenuItem add = new JMenuItem(resourceMap.getString("menuMore.text") + it.getSimpleName(severity)); //$NON-NLS-1$ add.setActionCommand(it.getKey() + MENU_CMD_SEPARATOR + severity); add.addActionListener(addActionListener); addMenu.add(add); }); }); popup.add(addMenu); } else { JMenuItem edit = new JMenuItem(resourceMap.getString("menuNewInjury.text"), //$NON-NLS-1$ UIManager.getIcon("FileView.fileIcon")); //$NON-NLS-1$ popup.add(edit); } JMenuItem remove = new JMenuItem(loc.readableName.isEmpty() ? resourceMap.getString("menuHealAll.text") : resourceMap.getString("menuHeal.text"), healImageIcon); //$NON-NLS-1$ //$NON-NLS-2$ if(locationPicked && p.getInjuriesByLocation(loc).isEmpty()) { remove.setEnabled(false); } else { remove.addActionListener(rae -> { person.getInjuries().stream().filter(inj -> !locationPicked || (inj.getLocation() == loc)) .forEach(inj -> person.removeInjury(inj)); revalidate(); }); } popup.add(remove); Dimension popupSize = popup.getPreferredSize(); popup.show(doll, (int) (mousePos.getX() - popupSize.getWidth()) + 10, (int) mousePos.getY() - 10); }; setBackground(Color.WHITE); getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); Container scrollPanel = new JPanel(); getContentPane().add(new JScrollPane(scrollPanel, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER)); initComponents(scrollPanel); JButton okayButton = new JButton(resourceMap.getString("buttonDone.text")); //$NON-NLS-1$ okayButton.addActionListener(ae -> { if(!notesArea.getText().isEmpty()) { p.getExtraData().set(DOCTOR_NOTES, notesArea.getText()); } setVisible(false); }); okayButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, 20)); getContentPane().add(okayButton); pack(); } private void initComponents(Container cont) { cont.setBackground(Color.WHITE); cont.setLayout(new GridBagLayout()); GridBagConstraints gbc; gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = 0; gbc.gridheight = 6; gbc.weighty = 1.0; gbc.fill = GridBagConstraints.VERTICAL; dollWrapper = new JPanel(null); dollWrapper.setLayout(new BoxLayout(dollWrapper, BoxLayout.Y_AXIS)); dollWrapper.setMinimumSize(new Dimension(256, 768)); dollWrapper.setMaximumSize(new Dimension(256, Integer.MAX_VALUE)); dollWrapper.setOpaque(false); dollWrapper.setAlignmentY(Component.TOP_ALIGNMENT); cont.add(dollWrapper, gbc); gbc.gridx = 1; gbc.gridheight = 1; gbc.weightx = 1.0; gbc.weighty = 0.0; gbc.insets = new Insets(10, 10, 10, 10); gbc.fill = GridBagConstraints.BOTH; cont.add(genBaseData(campaign, person), gbc); gbc.gridy = 1; cont.add(genMedicalHistory(campaign, person), gbc); gbc.gridy = 2; cont.add(genAllergies(campaign, person), gbc); gbc.gridy = 3; cont.add(genIllnesses(campaign, person), gbc); gbc.gridy = 4; cont.add(injuryPanel = new JPanel(), gbc); gbc.gridy = 5; cont.add(genNotes(campaign, person), gbc); } @Override public void validate() { dollWrapper.setVisible(false); fillDoll(dollWrapper, campaign, person); dollWrapper.setVisible(true); injuryPanel.setVisible(false); fillInjuries(injuryPanel, campaign, person); injuryPanel.setVisible(true); super.validate(); } private JPanel fillDoll(JPanel panel, Campaign c, Person p) { panel.removeAll(); if(null != doll) { doll.removeActionListener(dollActionListener); } doll = person.isMale() ? defaultMaleDoll : defaultFemaleDoll; doll.clearLocColors(); doll.clearLocTags(); doll.setHighlightColor(new Color(170, 170, 255)); Arrays.stream(BodyLocation.values()) .filter(bl -> p.hasInjury(bl)) .forEach(bl -> { if(person.isLocationMissing(bl) && !person.isLocationMissing(bl.parent)) { doll.setLocTag(bl, "lost"); //$NON-NLS-1$ } else if(!person.isLocationMissing(bl)) { InjuryLevel level = getMaxInjuryLevel(person, bl); Color col; switch(level) { case CHRONIC: col = new Color(255, 204, 255); break; case DEADLY: col = Color.RED; break; case MAJOR: col = Color.ORANGE; break; case MINOR: col = Color.YELLOW; break; case NONE: col = Color.WHITE; break; default: col = Color.WHITE; break; } doll.setLocColor(bl, col); } }); if(isGMMode()) { doll.addActionListener(dollActionListener); } panel.add(doll); panel.add(Box.createVerticalGlue()); return panel; } private JPanel genBaseData(Campaign c, Person p) { JPanel panel = new JPanel(); panel.setOpaque(false); panel.setLayout(new GridLayout(10, 2)); panel.setBorder(BorderFactory.createMatteBorder(3, 3, 0, 3, Color.BLACK)); panel.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); String name = p.getFullName(); String[] nameParts = name.split(" ", -1); //$NON-NLS-1$ String familyName = "-"; //$NON-NLS-1$ String givenNames = ""; //$NON-NLS-1$ if(nameParts.length < 2) { givenNames = nameParts[0]; } else { familyName = nameParts[nameParts.length - 1]; givenNames = String.join(" ", Arrays.copyOf(nameParts, nameParts.length - 1)); //$NON-NLS-1$ } GregorianCalendar birthday = (GregorianCalendar) p.getBirthday().clone(); DATE_FORMAT.setCalendar(birthday); String birthdayString = DATE_FORMAT.format(birthday.getTime()); GregorianCalendar now = (GregorianCalendar) c.getCalendar().clone(); int ageInMonths = (now.get(Calendar.YEAR) - birthday.get(Calendar.YEAR)) * 12 + now.get(Calendar.MONTH) - birthday.get(Calendar.MONTH); String phenotype = (p.getPhenotype() != Person.PHENOTYPE_NONE) ? p.getPhenotypeName() : resourceMap.getString("baselinePhenotype.text"); //$NON-NLS-1$ Force f = c.getForceFor(p); String force = (null != f) ? f.getFullName() : "-"; //$NON-NLS-1$ Person doc = c.getPerson(p.getDoctorId()); String doctor = resourceMap.getString("none.text"); //$NON-NLS-1$ if((null != doc) && doc.isActive()) { doctor = doc.getFullTitle(); } panel.add(genLabel(resourceMap.getString("familyName.text"))); //$NON-NLS-1$ panel.add(genLabel(resourceMap.getString("givenNames.text"))); //$NON-NLS-1$ panel.add(genWrittenPanel(familyName)); panel.add(genWrittenPanel(givenNames)); panel.add(genLabel(resourceMap.getString("birthDate.text"))); //$NON-NLS-1$ panel.add(genLabel(resourceMap.getString("age.text"))); //$NON-NLS-1$ panel.add(genWrittenPanel(birthdayString)); panel.add(genWrittenPanel(String.format(resourceMap.getString("age.format"), ageInMonths / 12, ageInMonths % 12))); //$NON-NLS-1$ panel.add(genLabel(resourceMap.getString("gender.text"))); //$NON-NLS-1$ panel.add(genLabel(resourceMap.getString("phenotype.text"))); //$NON-NLS-1$ panel.add(genWrittenPanel(p.isMale() ? resourceMap.getString("genderMale.text") : resourceMap.getString("genderFemale.text"))); //$NON-NLS-1$ //$NON-NLS-2$ panel.add(genWrittenPanel(phenotype)); panel.add(genLabel(resourceMap.getString("assignedTo.text"))); //$NON-NLS-1$ panel.add(genLabel("")); //$NON-NLS-1$ panel.add(genWrittenPanel(force)); panel.add(genWrittenPanel("")); //$NON-NLS-1$ panel.add(genLabel(resourceMap.getString("assignedDoctor.text"))); //$NON-NLS-1$ panel.add(genLabel(resourceMap.getString("lastCheckup.text"))); //$NON-NLS-1$ panel.add(genWrittenPanel(doctor)); panel.add(genWrittenPanel("")); //$NON-NLS-1$ return panel; } private JPanel genMedicalHistory(Campaign c, Person p) { JPanel panel = new JPanel(); panel.setOpaque(false); panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); panel.add(genLabel(resourceMap.getString("medicalHistory.text"))); //$NON-NLS-1$ Map<String, List<LogEntry>> groupedEntries = p.getPersonnelLog().stream() .filter(entry -> entry.isType(Person.LOGTYPE_MEDICAL)) .sorted((entry1, entry2) -> entry1.getDate().compareTo(entry2.getDate())) .collect(Collectors.groupingBy(entry -> DATE_FORMAT.format(entry.getDate()))); groupedEntries.entrySet().stream() .filter(e -> !e.getValue().isEmpty()) .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey())) .forEachOrdered(e -> { if(e.getValue().size() > 1) { panel.add(genWrittenText(e.getKey())); e.getValue().forEach(entry -> { JPanel wrapper = new JPanel(); wrapper.setLayout(new BoxLayout(wrapper, BoxLayout.X_AXIS)); wrapper.setOpaque(false); wrapper.setAlignmentX(Component.LEFT_ALIGNMENT); wrapper.add(Box.createHorizontalStrut(60)); wrapper.add(genWrittenText(String.format(resourceMap.getString("historyText.format"), entry.getDesc()))); //$NON-NLS-1$ panel.add(wrapper); }); } else { panel.add(genWrittenText(String.format(resourceMap.getString("historyDateAndText.format"), e.getKey(), e.getValue().get(0).getDesc()))); //$NON-NLS-1$ } }); return panel; } private JPanel genAllergies(Campaign c, Person p) { JPanel panel = new JPanel(); panel.setOpaque(false); panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); panel.add(genLabel(resourceMap.getString("allergies.text"))); //$NON-NLS-1$ panel.add(genWrittenText("")); //$NON-NLS-1$ return panel; } private JPanel genIllnesses(Campaign c, Person p) { JPanel panel = new JPanel(); panel.setOpaque(false); panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); panel.add(genLabel(resourceMap.getString("illnesses.text"))); //$NON-NLS-1$ panel.add(genWrittenText("")); //$NON-NLS-1$ return panel; } /** Get the maximum injury level in the specified location */ private InjuryLevel getMaxInjuryLevel(Person p, BodyLocation loc) { return p.getInjuries().stream().filter(inj -> !inj.isHidden() && (inj.getLocation() == loc)) .sorted((inj1, inj2) -> Integer.compare(inj2.getLevel().ordinal(), inj1.getLevel().ordinal())) .findFirst().map(Injury::getLevel).orElse(InjuryLevel.NONE); } /** Compiles a list of body locations stream ordered by the maximum injury level in that location */ private Stream<BodyLocation> maxInjurylevelLocationStream(Person p) { Map<BodyLocation, InjuryLevel> levelMap = new HashMap<>(); Arrays.stream(BodyLocation.values()) .filter(bl -> p.hasInjury(bl)) .forEach(bl -> { levelMap.put(bl, getMaxInjuryLevel(p, bl)); }); return levelMap.entrySet().stream().sorted((entry1, entry2) -> Integer.compare(entry2.getValue().ordinal(), entry1.getValue().ordinal()) ).map(Map.Entry::getKey); } private JPanel fillInjuries(JPanel panel, Campaign c, Person p) { panel.removeAll(); panel.setOpaque(false); panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); panel.add(genLabel(resourceMap.getString("injuries.text"))); //$NON-NLS-1$ maxInjurylevelLocationStream(p).forEachOrdered(bl -> { JPanel blWrapper = new JPanel(); blWrapper.setLayout(new BoxLayout(blWrapper, BoxLayout.X_AXIS)); blWrapper.setOpaque(false); blWrapper.setAlignmentX(Component.LEFT_ALIGNMENT); blWrapper.add(Box.createHorizontalStrut(30)); blWrapper.add(genWrittenText(Utilities.capitalize(bl.readableName))); panel.add(blWrapper); p.getInjuriesByLocation(bl).stream() .sorted((inj1, inj2) -> Integer.compare(inj2.getLevel().ordinal(), inj1.getLevel().ordinal())) .forEachOrdered(inj -> { JLabel injLabel = null; if(inj.getType().isPermanent()) { injLabel = genWrittenText(String.format(resourceMap.getString("injuriesText.format"), //$NON-NLS-1$ inj.getType().getSimpleName(), inj.getStart().toString(DATE_FORMATTER))); } else if(inj.isPermanent() || (inj.getTime() <= 0)) { injLabel = genWrittenText(String.format(resourceMap.getString("injuriesPermanent.format"), //$NON-NLS-1$ inj.getType().getSimpleName(), inj.getStart().toString(DATE_FORMATTER))); } else { injLabel = genWrittenText(String.format(resourceMap.getString("injuriesTextAndDuration.format"), //$NON-NLS-1$ inj.getType().getSimpleName(), inj.getStart().toString(DATE_FORMATTER), genTimePeriod(inj.getTime()))); } if(isGMMode()) { injLabel.addMouseListener(new InjuryLabelMouseAdapter(injLabel, p, inj)); } JPanel wrapper = new JPanel(); wrapper.setLayout(new BoxLayout(wrapper, BoxLayout.X_AXIS)); wrapper.setOpaque(false); wrapper.setAlignmentX(Component.LEFT_ALIGNMENT); wrapper.add(Box.createHorizontalStrut(60)); wrapper.add(injLabel); panel.add(wrapper); }); }); return panel; } private JPanel genNotes(Campaign c, Person p) { JPanel panel = new JPanel(); panel.setOpaque(false); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); panel.add(genLabel(resourceMap.getString("doctorsNotes.text"))); //$NON-NLS-1$ String notes = p.getExtraData().get(DOCTOR_NOTES, ""); //$NON-NLS-1$ notesArea = new JTextArea(notes); notesArea.setEditable(true); notesArea.setAlignmentX(Component.LEFT_ALIGNMENT); notesArea.setFont(handwritingFont); notesArea.setLineWrap(true); notesArea.setWrapStyleWord(true); JPanel notesPanel = new JPanel(); notesPanel.setLayout(new BoxLayout(notesPanel, BoxLayout.X_AXIS)); notesPanel.setOpaque(false); notesPanel.setAlignmentX(Component.LEFT_ALIGNMENT); notesPanel.add(Box.createHorizontalStrut(30)); notesPanel.add(notesArea); panel.add(notesPanel); return panel; } private JLabel genLabel(String text) { JLabel label = new JLabel(text); label.setFont(labelFont); label.setAlignmentX(Component.LEFT_ALIGNMENT); label.setForeground(labelColor); return label; } private JPanel genWrittenPanel(String text) { JLabel label = genWrittenText(text); JPanel wrapper = new JPanel(); wrapper.setLayout(new BoxLayout(wrapper, BoxLayout.X_AXIS)); wrapper.setOpaque(false); wrapper.setAlignmentX(Component.LEFT_ALIGNMENT); wrapper.add(Box.createHorizontalStrut(30)); wrapper.add(label); wrapper.setBorder(BorderFactory.createMatteBorder(0, 0, 3, 0, Color.BLACK)); return wrapper; } private JLabel genWrittenText(String text) { JLabel label = new JLabel(text); label.setFont(handwritingFont); return label; } private String genTimePeriod(int days) { if(days <= 1) { return resourceMap.getString("durationOneDay.text"); //$NON-NLS-1$ } else if(days < 21) { return String.format(resourceMap.getString("durationDays.format"), days); //$NON-NLS-1$ } else if(days <= 12 * 7) { return String.format(resourceMap.getString("durationWeeks.format"), days * 1.0 / 7.0); //$NON-NLS-1$ } else if(days <= 2 * 365) { return String.format(resourceMap.getString("durationMonths.format"), days * 12.0 / 365.0); //$NON-NLS-1$ } else { return String.format(resourceMap.getString("durationYears.format"), days * 1.0 / 365.0); //$NON-NLS-1$ } } public boolean isGMMode() { return gmMode; } public void setGMMode(boolean gmMode) { this.gmMode = gmMode; } private static class InjuryLabelMouseAdapter extends MouseAdapter { private final JLabel label; private final Person person; private final Injury injury; private ImageIcon healImageIcon; private ResourceBundle resourceMap = null; public InjuryLabelMouseAdapter(JLabel label, Person person, Injury injury) { this.label = label; this.person = person; this.injury = injury; this.healImageIcon = new ImageIcon(new ImageIcon("data/images/misc/medical.png").getImage().getScaledInstance(16, 16, Image.SCALE_DEFAULT)); //$NON-NLS-1$ this.resourceMap = ResourceBundle.getBundle("mekhq.resources.MedicalViewDialog", new EncodeControl()); //$NON-NLS-1$ } @Override public void mouseEntered(MouseEvent e) { label.setBackground(Color.LIGHT_GRAY); label.setOpaque(true); label.invalidate(); } @Override public void mouseExited(MouseEvent e) { label.setBackground(Color.WHITE); label.setOpaque(false); label.invalidate(); } @Override public void mouseClicked(MouseEvent e) { if(e.getButton() == MouseEvent.BUTTON1) { JPopupMenu popup = new JPopupMenu(); JLabel header = new JLabel(injury.getFluff()); header.setFont(UIManager.getDefaults().getFont("Menu.font").deriveFont(Font.BOLD)); //$NON-NLS-1$ popup.add(header); popup.addSeparator(); JMenuItem edit = new JMenuItem(resourceMap.getString("menuEdit.text"), //$NON-NLS-1$ UIManager.getIcon("FileView.fileIcon")); //$NON-NLS-1$ popup.add(edit); JMenuItem remove = new JMenuItem(resourceMap.getString("menuRemove.text"), healImageIcon); //$NON-NLS-1$ remove.addActionListener(ae -> { person.removeInjury(injury); label.getRootPane().getParent().revalidate(); }); popup.add(remove); Dimension popupSize = popup.getPreferredSize(); popup.show(e.getComponent(), e.getX() - (int) popupSize.getWidth() + 10, e.getY() - 10); } } } }