diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index f42b3c968..65080c7f0 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -614,6 +614,7 @@ simpanel.dlg.lbl.DeleteSim1 = Delete the selected simulations? simpanel.dlg.lbl.DeleteSim2 = This operation cannot be undone. simpanel.dlg.lbl.DeleteSim3 = Delete simulations simpanel.col.Status = Status +simpanel.col.Warnings = Warnings simpanel.col.Name = Name simpanel.col.Motors = Motors simpanel.col.Configuration = Configuration diff --git a/core/resources/pix/icons/warning_high.png b/core/resources/pix/icons/warning_high.png new file mode 100644 index 000000000..c37bd062e Binary files /dev/null and b/core/resources/pix/icons/warning_high.png differ diff --git a/core/resources/pix/icons/warning_low.png b/core/resources/pix/icons/warning_low.png new file mode 100644 index 000000000..12cd1aef9 Binary files /dev/null and b/core/resources/pix/icons/warning_low.png differ diff --git a/core/resources/pix/icons/warning_normal.png b/core/resources/pix/icons/warning_normal.png new file mode 100644 index 000000000..628cf2dae Binary files /dev/null and b/core/resources/pix/icons/warning_normal.png differ diff --git a/core/src/net/sf/openrocket/logging/MessageSet.java b/core/src/net/sf/openrocket/logging/MessageSet.java index adf490edb..3e7ace376 100644 --- a/core/src/net/sf/openrocket/logging/MessageSet.java +++ b/core/src/net/sf/openrocket/logging/MessageSet.java @@ -116,6 +116,20 @@ public abstract class MessageSet extends AbstractSet imple return messages.size(); } + /** + * Returns the number of messages with the specified priority. + * @param priority the priority + * @return the number of messages with the specified priority. + */ + public int getNrOfMessagesWithPriority(MessagePriority priority) { + int count = 0; + for (E m : messages) { + if (m.getPriority() == priority) { + count++; + } + } + return count; + } public void immute() { mutable.immute(); diff --git a/core/src/net/sf/openrocket/logging/WarningSet.java b/core/src/net/sf/openrocket/logging/WarningSet.java index d93a20d0e..e4688f1c0 100644 --- a/core/src/net/sf/openrocket/logging/WarningSet.java +++ b/core/src/net/sf/openrocket/logging/WarningSet.java @@ -27,6 +27,18 @@ public class WarningSet extends MessageSet { return add(Warning.fromString(s)); } + public int getNrOfCriticalWarnings() { + return getNrOfMessagesWithPriority(MessagePriority.HIGH); + } + + public int getNrOfNormalWarnings() { + return getNrOfMessagesWithPriority(MessagePriority.NORMAL); + } + + public int getNrOfInformativeWarnings() { + return getNrOfMessagesWithPriority(MessagePriority.LOW); + } + @Override public WarningSet clone() { try { diff --git a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java index b5e248cba..682b4d4f5 100644 --- a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java @@ -1,7 +1,7 @@ package net.sf.openrocket.gui.main; -import java.awt.Color; +import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; @@ -21,13 +21,14 @@ import java.io.Serial; import java.util.Comparator; import javax.swing.AbstractAction; +import javax.swing.Box; +import javax.swing.BoxLayout; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JLabel; -import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; @@ -94,13 +95,6 @@ public class SimulationPanel extends JPanel { private static final Translator trans = Application.getTranslator(); - private static final Color WARNING_COLOR = Color.RED; - private static final String WARNING_TEXT = "\uFF01"; // Fullwidth exclamation mark - - private static final Color OK_COLOR = new Color(60, 150, 0); - private static final String OK_TEXT = "\u2714"; // Heavy check mark - - private RocketDescriptor descriptor = Application.getInjector().getInstance(RocketDescriptor.class); @@ -195,6 +189,7 @@ public class SimulationPanel extends JPanel { simulationTable.setRowSorter(simulationTableSorter); simulationTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); simulationTable.setDefaultRenderer(Object.class, new JLabelRenderer()); + simulationTable.setDefaultRenderer(WarningsBox.class, new WarningsBoxRenderer()); simulationTableModel.setColumnWidths(simulationTable.getColumnModel()); simulationTable.setFillsViewportHeight(true); @@ -239,6 +234,7 @@ public class SimulationPanel extends JPanel { if (e.getButton() == MouseEvent.BUTTON1) { if (e.getClickCount() == 1) { + // Rerun the simulation if (column == 0) { int selected = simulationTable.convertRowIndexToModel(selectedRow); Simulation sim = document.getSimulations().get(selected); @@ -248,14 +244,15 @@ public class SimulationPanel extends JPanel { runSimulation(); } } - } else if (e.getClickCount() == 2) { - int selected = simulationTable.convertRowIndexToModel(selectedRow); // Show the warnings for the simulation - if (column == 0) { + else if (column == 1) { + int selected = simulationTable.convertRowIndexToModel(selectedRow); SimulationWarningDialog.showWarningDialog(SimulationPanel.this, document.getSimulations().get(selected)); } + } else if (e.getClickCount() == 2) { + int selected = simulationTable.convertRowIndexToModel(selectedRow); // Edit the simulation or plot/export - else { + if (column > 1) { simulationTable.clearSelection(); simulationTable.addRowSelectionInterval(selectedRow, selectedRow); @@ -756,67 +753,83 @@ public class SimulationPanel extends JPanel { return simulationTable.getSelectionModel(); } - private String getSimulationToolTip(Simulation sim, boolean includeSimName) { - String tip; + private static String getSimulationStatusToolTip(Simulation sim, boolean includeSimName) { + StringBuilder tip; FlightData data = sim.getSimulatedData(); - tip = ""; + tip = new StringBuilder(""); if (includeSimName) { - tip += "" + sim.getName() + "
"; + tip.append("").append(sim.getName()).append("
"); } switch (sim.getStatus()) { case CANT_RUN: - tip += trans.get("simpanel.ttip.noData")+"
"; + tip.append(trans.get("simpanel.ttip.noData")).append("
"); break; case LOADED: - tip += trans.get("simpanel.ttip.loaded") + "
"; + tip.append(trans.get("simpanel.ttip.loaded")).append("
"); break; case UPTODATE: - tip += trans.get("simpanel.ttip.uptodate") + "
"; + tip.append(trans.get("simpanel.ttip.uptodate")).append("
"); break; case OUTDATED: - tip += trans.get("simpanel.ttip.outdated") + "
"; + tip.append(trans.get("simpanel.ttip.outdated")).append("
"); break; case EXTERNAL: - tip += trans.get("simpanel.ttip.external") + "
"; - return tip; + tip.append(trans.get("simpanel.ttip.external")).append("
"); + return tip.toString(); case NOT_SIMULATED: - tip += trans.get("simpanel.ttip.notSimulated"); - return tip; + tip.append(trans.get("simpanel.ttip.notSimulated")); + return tip.toString(); } if (data == null) { - tip += trans.get("simpanel.ttip.noData"); - return tip; + tip.append(trans.get("simpanel.ttip.noData")); + return tip.toString(); } for (int b = 0; b < data.getBranchCount(); b++) { FlightEvent abortEvent = data.getBranch(b).getFirstEvent(FlightEvent.Type.SIM_ABORT); - if ( abortEvent != null) { - tip += "" + trans.get("simpanel.ttip.simAbort") + ": " + - ((SimulationAbort)(abortEvent.getData())).toString() + "
"; + if (abortEvent != null) { + tip.append("").append(trans.get("simpanel.ttip.simAbort")).append(": ").append((abortEvent.getData()).toString()).append("
"); } } - - WarningSet warnings = data.getWarningSet(); - if (warnings.isEmpty()) { - tip += trans.get("simpanel.ttip.noWarnings"); - return tip; - } - tip += trans.get("simpanel.ttip.warnings"); - for (Warning w : warnings) { - tip += "
" + w.toString(); - } - - return tip; + return tip.toString(); } - private String getSimulationToolTip(Simulation sim) { - return getSimulationToolTip(sim, true); + private static String getSimulationWarningsToolTip(Simulation sim, boolean includeSimName) { + StringBuilder tip; + FlightData data = sim.getSimulatedData(); + + tip = new StringBuilder(""); + if (includeSimName) { + tip.append("").append(sim.getName()).append("
"); + } + + if (data == null) { + tip.append(trans.get("simpanel.ttip.noData")); + return tip.toString(); + } + + WarningSet warnings = data.getWarningSet(); + if (warnings.isEmpty()) { + tip.append(trans.get("simpanel.ttip.noWarnings")); + return tip.toString(); + } + + tip.append(trans.get("simpanel.ttip.warnings")); + for (Warning w : warnings) { + tip.append("
").append(w.toString()); + } + + return tip.toString(); + } + + private String getSimulationStatusToolTip(Simulation sim) { + return getSimulationStatusToolTip(sim, true); } private void openDialog(boolean plotMode, boolean isNewSimulation, final Simulation... sims) { @@ -1138,7 +1151,7 @@ public class SimulationPanel extends JPanel { label.setBackground(table.getBackground()); label.setOpaque(true); - label.setToolTipText(getSimulationToolTip(document.getSimulation(row))); + label.setToolTipText(getSimulationStatusToolTip(document.getSimulation(row))); return label; } @@ -1146,7 +1159,7 @@ public class SimulationPanel extends JPanel { isSelected, hasFocus, row, column); if (component instanceof JComponent) { - ((JComponent) component).setToolTipText(getSimulationToolTip( + ((JComponent) component).setToolTipText(getSimulationStatusToolTip( document.getSimulation(row))); } return component; @@ -1167,17 +1180,97 @@ public class SimulationPanel extends JPanel { @Override public String toString() { - String text = getSimulationToolTip(simulation, false); + String text = getSimulationStatusToolTip(simulation, false); return text.replace("
", "-").replaceAll("<[^>]*>",""); } } + private static class WarningsBox extends Box { + private Simulation simulation; + + public WarningsBox(Simulation simulation) { + super(BoxLayout.X_AXIS); // Horizontal box + this.simulation = simulation; + updateContent(); + } + + public void replaceSimulation(Simulation simulation) { + this.simulation = simulation; + updateContent(); + } + + private void updateContent() { + removeAll(); // Clear existing content before update + setToolTipText(""); + + if (simulation == null) { + revalidate(); + return; + } + + // Update the tooltip text + String ttip = getSimulationWarningsToolTip(simulation, true); + setToolTipText(ttip); + + WarningSet warnings = simulation.getSimulatedWarnings(); + + if (warnings == null || warnings.isEmpty()) { + revalidate(); + return; + } + + int nrOfCriticalWarnings = warnings.getNrOfCriticalWarnings(); + int nrOfNormalWarnings = warnings.getNrOfNormalWarnings(); + int nrOfInfoWarnings = warnings.getNrOfInformativeWarnings(); + + if (nrOfCriticalWarnings > 0) { + add(new JLabel(nrOfCriticalWarnings + " ")); + add(new JLabel(Icons.WARNING_HIGH)); + } + + if (nrOfCriticalWarnings > 0 && nrOfNormalWarnings > 0) { + add(new JLabel(", ")); + } + + if (nrOfNormalWarnings > 0) { + add(new JLabel(nrOfNormalWarnings + " ")); + add(new JLabel(Icons.WARNING_NORMAL)); + } + + if ((nrOfCriticalWarnings > 0 || nrOfNormalWarnings > 0) && nrOfInfoWarnings > 0) { + add(new JLabel(", ")); + } + + if (nrOfInfoWarnings > 0) { + add(new JLabel(nrOfInfoWarnings + " ")); + add(new JLabel(Icons.WARNING_LOW)); + } + + revalidate(); // Notify layout manager of changes + } + } + + public static class WarningsBoxRenderer extends DefaultTableCellRenderer { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + if (value instanceof WarningsBox box) { + // Wrap the box in a panel with BorderLayout to allow alignment + JPanel panel = new JPanel(new BorderLayout()); + panel.setToolTipText(box.getToolTipText()); + panel.add(box, BorderLayout.EAST); // Align to the right within the panel + + return panel; + } + return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + } + } + private class SimulationTableModel extends ColumnTableModel { private static final long serialVersionUID = 8686456963492628476L; public SimulationTableModel() { super( - //// Status and warning column + //// Status column new Column("") { private StatusLabel label = null; @@ -1201,31 +1294,12 @@ public class SimulationPanel extends JPanel { Simulation.Status status = simulation.getStatus(); label.setIcon(Icons.SIMULATION_STATUS_ICON_MAP.get(status)); - - // Set warning marker - if (status == Simulation.Status.NOT_SIMULATED || - status == Simulation.Status.EXTERNAL) { - label.setText(""); - } else { - - WarningSet w = document.getSimulation(row).getSimulatedWarnings(); - if (w == null) { - label.setText(""); - } else if (w.isEmpty()) { - label.setForeground(OK_COLOR); - label.setText(OK_TEXT); - } else { - label.setForeground(WARNING_COLOR); - label.setText(WARNING_TEXT); - } - } - return label; } @Override public int getExactWidth() { - return 36; + return 26; } @Override @@ -1234,6 +1308,38 @@ public class SimulationPanel extends JPanel { } }, + //// Warnings column + new Column(trans.get("simpanel.col.Warnings")) { + private WarningsBox box = null; + + @Override + public Object getValueAt(int row) { + if (row < 0 || row >= document.getSimulationCount()) + return null; + + Simulation simulation = document.getSimulation(row); + + // Initialize the box + if (box == null) { + box = new WarningsBox(simulation); + } else { + box.replaceSimulation(simulation); + } + + return box; + } + + @Override + public int getDefaultWidth() { + return 70; + } + + @Override + public Class getColumnClass() { + return WarningsBox.class; + } + }, + //// Simulation name //// Name new Column(trans.get("simpanel.col.Name")) { diff --git a/swing/src/net/sf/openrocket/gui/util/Icons.java b/swing/src/net/sf/openrocket/gui/util/Icons.java index cdc7c997e..afc53f907 100644 --- a/swing/src/net/sf/openrocket/gui/util/Icons.java +++ b/swing/src/net/sf/openrocket/gui/util/Icons.java @@ -99,6 +99,10 @@ public class Icons { public static final Icon NOT_FAVORITE = loadImageIcon("pix/icons/star_silver.png", "Not favorite"); public static final Icon FAVORITE = loadImageIcon("pix/icons/star_gold.png", "Favorite"); + public static final Icon WARNING_LOW = loadImageIcon("pix/icons/warning_low.png", "Informative Warning"); + public static final Icon WARNING_NORMAL = loadImageIcon("pix/icons/warning_normal.png", "Warning"); + public static final Icon WARNING_HIGH = loadImageIcon("pix/icons/warning_high.png", "Critical Warning"); + public static final Icon MASS_OVERRIDE_LIGHT = loadImageIcon("pix/icons/mass-override_light.png", "Mass Override"); public static final Icon MASS_OVERRIDE_DARK = loadImageIcon("pix/icons/mass-override_dark.png", "Mass Override"); public static final Icon MASS_OVERRIDE_SUBCOMPONENT_LIGHT = loadImageIcon("pix/icons/mass-override-subcomponent_light.png", "Mass Override Subcomponent"); @@ -143,17 +147,6 @@ public class Icons { return new ImageIcon(url, name); } - /** - * Loads an ImageIcon with a new name. - * - * @param icon the original ImageIcon to load. - * @param newName the new name for the ImageIcon. - * @return the loaded ImageIcon with the new name. - */ - public static ImageIcon loadImageIconWithNewName(ImageIcon icon, String newName) { - return new ImageIcon(icon.getImage(), newName); - } - /** * Scales an ImageIcon to the specified scale. * @param icon icon to scale