diff --git a/core/src/main/resources/l10n/messages.properties b/core/src/main/resources/l10n/messages.properties index 0b8d9d418..08298568c 100644 --- a/core/src/main/resources/l10n/messages.properties +++ b/core/src/main/resources/l10n/messages.properties @@ -966,6 +966,9 @@ CAPlotExportDialog.lbl.Delta.ttip = Step size (increments) for the parameter swe CAPlotExportDialog.tab.Plot = Plot CAPlotExportDialog.tab.Export = Export +! CAExportPanel +CAExportPanel.Col.Components = Components + ! CADataTypeGroup CADataTypeGroup.DOMAIN = Domain Parameter CADataTypeGroup.DRAG = Drag Characteristics diff --git a/swing/src/main/java/info/openrocket/swing/gui/components/CsvOptionPanel.java b/swing/src/main/java/info/openrocket/swing/gui/components/CsvOptionPanel.java index 65de71e9d..3fb6284c1 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/components/CsvOptionPanel.java +++ b/swing/src/main/java/info/openrocket/swing/gui/components/CsvOptionPanel.java @@ -42,7 +42,7 @@ public class CsvOptionPanel extends JPanel { * @param includeComments a list of comment inclusion options to provide; * every second item is the option name and every second the tooltip */ - public CsvOptionPanel(Class baseClass, String... includeComments) { + public CsvOptionPanel(Class baseClass, boolean placeOnNewRows, String... includeComments) { super(new MigLayout("fill, insets 0")); this.baseClassName = baseClass.getSimpleName(); @@ -88,7 +88,11 @@ public class CsvOptionPanel extends JPanel { exponentialNotationCheckbox.setSelected(Application.getPreferences().getBoolean(ApplicationPreferences.EXPORT_EXPONENTIAL_NOTATION, true)); panel.add(exponentialNotationCheckbox); - this.add(panel, "growx, wrap unrel"); + if (placeOnNewRows) { + this.add(panel, "growx, wrap unrel"); + } else { + this.add(panel, "growx, gapright unrel"); + } @@ -120,8 +124,16 @@ public class CsvOptionPanel extends JPanel { commentCharacter.setSelectedItem(Application.getPreferences().getString(ApplicationPreferences.EXPORT_COMMENT_CHARACTER, "#")); commentCharacter.setToolTipText(tip); panel.add(commentCharacter, "growx"); - - this.add(panel, "growx, wrap"); + + if (placeOnNewRows) { + this.add(panel, "growx, wrap"); + } else { + this.add(panel, "growx"); + } + } + + public CsvOptionPanel(Class baseClass, String... includeComments) { + this(baseClass, true, includeComments); } diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAExportPanel.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAExportPanel.java index 216a6f6b9..f326e1b67 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAExportPanel.java +++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/CAExportPanel.java @@ -1,6 +1,7 @@ package info.openrocket.swing.gui.dialogs.componentanalysis; import info.openrocket.core.l10n.Translator; +import info.openrocket.core.rocketcomponent.RocketComponent; import info.openrocket.core.startup.Application; import info.openrocket.core.unit.Unit; import info.openrocket.swing.gui.components.CsvOptionPanel; @@ -9,33 +10,96 @@ import info.openrocket.swing.gui.util.SwingPreferences; import info.openrocket.swing.gui.widgets.CSVExportPanel; import info.openrocket.swing.gui.widgets.SaveFileChooser; +import javax.swing.AbstractCellEditor; +import javax.swing.BorderFactory; +import javax.swing.JCheckBox; +import javax.swing.JComponent; import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import java.awt.Color; import java.awt.Component; +import java.awt.Graphics; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; public class CAExportPanel extends CSVExportPanel { private static final long serialVersionUID = 4423905472892675964L; - private static final Translator trans = Application.getTranslator(); - private static final int OPTION_FIELD_DESCRIPTIONS = 0; + private static final int FIXED_COMPONENT_COLUMN_WIDTH = 500; - private CAExportPanel(CADataType[] types, + private final List> selectedComponents; + + private CAExportPanel(ComponentAnalysisPlotExportPanel parent, CADataType[] types, boolean[] selected, CsvOptionPanel csvOptions, Component... extraComponents) { - super(types, selected, csvOptions, extraComponents); + super(types, selected, csvOptions, true, extraComponents); + + selectedComponents = new ArrayList<>(types.length); + Map componentSelectedMap; + List components; + for (CADataType type : types) { + components = parent.getComponentsForType(type); + componentSelectedMap = new HashMap<>(components.size()); + for (int i = 0; i < components.size(); i++) { + // Select the first component by default + componentSelectedMap.put(components.get(i), i == 0); + } + selectedComponents.add(componentSelectedMap); + } + + // Set row heights dynamically + for (int row = 0; row < table.getRowCount(); row++) { + int numComponents = ((Map) table.getValueAt(row, 3)).size(); + double correctNumComponents = Math.ceil(numComponents / 3.0); // 3 components per row + double height = Math.round(correctNumComponents * 25) + 10; // 25 pixels per component + 10 pixel margin + int rowHeight = Math.max(table.getRowHeight(), (int) height); + table.setRowHeight(row, rowHeight); + } } - public static CAExportPanel create(CADataType[] types) { + public static CAExportPanel create(ComponentAnalysisPlotExportPanel parent, CADataType[] types) { boolean[] selected = new boolean[types.length]; for (int i = 0; i < types.length; i++) { selected[i] = ((SwingPreferences) Application.getPreferences()).isComponentAnalysisDataTypeExportSelected(types[i]); } - CsvOptionPanel csvOptions = new CsvOptionPanel(CAExportPanel.class, + CsvOptionPanel csvOptions = new CsvOptionPanel(CAExportPanel.class, false, trans.get("SimExpPan.checkbox.Includefielddesc"), trans.get("SimExpPan.checkbox.ttip.Includefielddesc")); - return new CAExportPanel(types, selected, csvOptions); + return new CAExportPanel(parent, types, selected, csvOptions); + } + + protected void initializeTable(CADataType[] types) { + super.initializeTable(types); + + // Set custom renderers for each column + table.getColumnModel().getColumn(0).setCellRenderer(new CheckBoxRenderer()); + table.getColumnModel().getColumn(1).setCellRenderer(new LeftAlignedRenderer()); + table.getColumnModel().getColumn(2).setCellRenderer(new LeftAlignedRenderer()); + + TableColumn componentColumn = table.getColumnModel().getColumn(3); + componentColumn.setCellRenderer(new ComponentCheckBoxRenderer()); + componentColumn.setCellEditor(new ComponentCheckBoxEditor()); + componentColumn.setPreferredWidth(FIXED_COMPONENT_COLUMN_WIDTH); + + // Set specific client properties for FlatLaf + table.setShowHorizontalLines(true); } @Override @@ -98,4 +162,278 @@ public class CAExportPanel extends CSVExportPanel { return true; } + + @Override + protected CSVExportPanel.SelectionTableModel createTableModel() { + return new CASelectionTableModel(); + } + + protected class CASelectionTableModel extends SelectionTableModel { + private static final int COMPONENTS = 3; + + @Override + public int getColumnCount() { + return 4; + } + + @Override + public String getColumnName(int column) { + //// Components + if (column == COMPONENTS) { + return trans.get("CAExportPanel.Col.Components"); + } + return super.getColumnName(column); + } + + @Override + public Class getColumnClass(int column) { + //// Components + if (column == COMPONENTS) { + return Map.class; + } + return super.getColumnClass(column); + } + + @Override + public Object getValueAt(int row, int column) { + if (column == COMPONENTS) { + return selectedComponents.get(row); + } + return super.getValueAt(row, column); + } + + @Override + public void setValueAt(Object value, int row, int column) { + if (column == COMPONENTS) { + selectedComponents.set(row, (Map) value); + fireTableCellUpdated(row, column); + } else { + super.setValueAt(value, row, column); + } + } + + @Override + public boolean isCellEditable(int row, int column) { + if (column == COMPONENTS) { + return true; + } + return super.isCellEditable(row, column); + } + } + + private static class CheckBoxRenderer extends JCheckBox implements TableCellRenderer { + public CheckBoxRenderer() { + setHorizontalAlignment(JLabel.CENTER); + setVerticalAlignment(JLabel.TOP); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + setSelected((Boolean) value); + setBackground(isSelected ? table.getSelectionBackground() : table.getBackground()); + setForeground(isSelected ? table.getSelectionForeground() : table.getForeground()); + setBorder(BorderFactory.createEmptyBorder(5, 0, 0, 0)); // Add top padding + return this; + } + } + + private static class LeftAlignedRenderer extends DefaultTableCellRenderer { + public LeftAlignedRenderer() { + setHorizontalAlignment(JLabel.LEFT); + setVerticalAlignment(JLabel.TOP); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + c.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground()); + c.setForeground(isSelected ? table.getSelectionForeground() : table.getForeground()); + ((JComponent) c).setBorder(BorderFactory.createEmptyBorder(5, 5, 0, 0)); // Add top and left padding + return c; + } + } + + private static class ComponentCheckBoxRenderer implements TableCellRenderer { + private ComponentCheckBoxPanel panel; + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + if (!(value instanceof Map)) { + JLabel errorLabel = new JLabel("Invalid data"); + errorLabel.setForeground(Color.RED); + return errorLabel; + } + + @SuppressWarnings("unchecked") + Map componentMap = (Map) value; + + if (panel == null) { + panel = new ComponentCheckBoxPanel(componentMap); + } else { + panel.updateComponentStates(componentMap); + } + + panel.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground()); + panel.setGridColor(table.getGridColor()); + panel.setSelected(isSelected); + + return panel; + } + } + + private static class ComponentCheckBoxPanel extends JPanel { + private final Map checkBoxMap = new HashMap<>(); + private final ItemListener checkBoxListener; + private final AtomicBoolean updatingState = new AtomicBoolean(false); + private Color gridColor = Color.GRAY; + private boolean isSelected = false; + + public ComponentCheckBoxPanel(Map componentMap) { + setLayout(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.NORTHWEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.insets = new Insets(2, 2, 2, 2); + + checkBoxListener = e -> { + if (updatingState.get()) return; + + JCheckBox source = (JCheckBox) e.getSource(); + if (e.getStateChange() == ItemEvent.DESELECTED) { + if (checkBoxMap.values().stream().noneMatch(JCheckBox::isSelected)) { + source.setSelected(true); + } + } + + // Notify the table that the value has changed + firePropertyChange("value", null, getComponentStates()); + }; + + createCheckBoxes(componentMap, gbc); + + // Add an empty component to push everything to the top-left + gbc.gridx = 0; + gbc.gridy = (componentMap.size() + 2) / 3 + 1; + gbc.weighty = 1.0; + gbc.fill = GridBagConstraints.BOTH; + add(new JPanel(), gbc); + + // Ensure at least one checkbox is selected + if (checkBoxMap.values().stream().noneMatch(JCheckBox::isSelected) && !checkBoxMap.isEmpty()) { + checkBoxMap.values().iterator().next().setSelected(true); + } + } + + private void createCheckBoxes(Map componentMap, GridBagConstraints gbc) { + int row = 0; + int col = 0; + for (Map.Entry entry : componentMap.entrySet()) { + RocketComponent component = entry.getKey(); + Boolean isSelected = entry.getValue(); + + // Skip null components + if (component == null) { + continue; + } + + String componentName = component.getName(); + // Use a default name if getName() returns null + if (componentName == null) { + componentName = "Unnamed Component"; + } + + JCheckBox checkBox = new JCheckBox(componentName, isSelected != null && isSelected); + checkBox.setOpaque(false); + checkBox.setMargin(new Insets(0, 0, 0, 0)); + checkBox.addItemListener(checkBoxListener); + checkBoxMap.put(component, checkBox); + + gbc.gridx = col; + gbc.gridy = row; + add(checkBox, gbc); + + col++; + if (col > 2) { + col = 0; + row++; + } + } + } + + public void updateComponentStates(Map newStates) { + updatingState.set(true); + try { + for (Map.Entry entry : newStates.entrySet()) { + JCheckBox checkBox = checkBoxMap.get(entry.getKey()); + if (checkBox != null) { + checkBox.setSelected(entry.getValue()); + } + } + } finally { + updatingState.set(false); + } + } + + public Map getComponentStates() { + return checkBoxMap.entrySet().stream() + .collect(HashMap::new, + (m, e) -> m.put(e.getKey(), e.getValue().isSelected()), + HashMap::putAll); + } + + public void setGridColor(Color color) { + this.gridColor = color; + } + + public void setSelected(boolean selected) { + this.isSelected = selected; + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + // Draw the bottom border + g.setColor(gridColor); + g.drawLine(0, getHeight() - 1, getWidth(), getHeight() - 1); + + // If selected, draw a slight highlight + if (isSelected) { + g.setColor(new Color(0, 0, 255, 30)); // Semi-transparent blue + g.fillRect(0, 0, getWidth(), getHeight() - 1); + } + } + } + + private static class ComponentCheckBoxEditor extends AbstractCellEditor implements TableCellEditor { + private ComponentCheckBoxPanel panel; + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + if (!(value instanceof Map)) { + // Return a default component if value is not a Map + JLabel errorLabel = new JLabel("Invalid data"); + errorLabel.setForeground(Color.RED); + return errorLabel; + } + + @SuppressWarnings("unchecked") + Map componentMap = (Map) value; + + if (panel == null) { + panel = new ComponentCheckBoxPanel(componentMap); + } else { + panel.updateComponentStates(componentMap); + } + + panel.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground()); + return panel; + } + + @Override + public Object getCellEditorValue() { + return panel.getComponentStates(); + } + } } diff --git a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisPlotExportPanel.java b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisPlotExportPanel.java index b5039e359..2741506db 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisPlotExportPanel.java +++ b/swing/src/main/java/info/openrocket/swing/gui/dialogs/componentanalysis/ComponentAnalysisPlotExportPanel.java @@ -58,7 +58,7 @@ public class ComponentAnalysisPlotExportPanel extends JPanel { public ComponentAnalysisPlotExportPanel(ComponentAnalysisDialog parent, CAParameters parameters, AerodynamicCalculator aerodynamicCalculator, Rocket rocket) { - super(new MigLayout("fill, height 500px")); + super(new MigLayout("fill, height 700px")); this.parent = parent; this.parameters = parameters; @@ -80,7 +80,7 @@ public class ComponentAnalysisPlotExportPanel extends JPanel { this.tabbedPane.addTab(trans.get("CAPlotExportDialog.tab.Plot"), null, this.plotTab); //// Export data - this.exportTab = CAExportPanel.create(types); + this.exportTab = CAExportPanel.create(this, types); this.tabbedPane.addTab(trans.get("CAPlotExportDialog.tab.Export"), null, this.exportTab); // Create the OK button diff --git a/swing/src/main/java/info/openrocket/swing/gui/widgets/CSVExportPanel.java b/swing/src/main/java/info/openrocket/swing/gui/widgets/CSVExportPanel.java index 6e1427c96..36fb68927 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/widgets/CSVExportPanel.java +++ b/swing/src/main/java/info/openrocket/swing/gui/widgets/CSVExportPanel.java @@ -32,8 +32,8 @@ public class CSVExportPanel extends JPanel { protected static final String SPACE = "SPACE"; protected static final String TAB = "TAB"; - private final JTable table; - private final SelectionTableModel tableModel; + protected final JTable table; + protected final SelectionTableModel tableModel; private final JLabel selectedCountLabel; protected final boolean[] selected; @@ -42,7 +42,7 @@ public class CSVExportPanel extends JPanel { protected final CsvOptionPanel csvOptions; - public CSVExportPanel(T[] types, boolean[] selected, CsvOptionPanel csvOptions, Component... extraComponents) { + public CSVExportPanel(T[] types, boolean[] selected, CsvOptionPanel csvOptions, boolean separateRowForTable, Component... extraComponents) { super(new MigLayout("fill, flowy")); this.types = types; @@ -59,39 +59,9 @@ public class CSVExportPanel extends JPanel { JButton button; // Set up the variable selection table - tableModel = new SelectionTableModel(); + tableModel = createTableModel(); table = new JTable(tableModel); - table.setDefaultRenderer(Object.class, - new SelectionBackgroundCellRenderer(table.getDefaultRenderer(Object.class))); - table.setDefaultRenderer(Boolean.class, - new SelectionBackgroundCellRenderer(table.getDefaultRenderer(Boolean.class))); - table.setRowSelectionAllowed(false); - table.setColumnSelectionAllowed(false); - - table.setDefaultEditor(Unit.class, new UnitCellEditor() { - private static final long serialVersionUID = 1088570433902420935L; - - @Override - protected UnitGroup getUnitGroup(Unit value, int row, int column) { - return types[row].getUnitGroup(); - } - }); - - // Set column widths - TableColumnModel columnModel = table.getColumnModel(); - TableColumn col = columnModel.getColumn(0); - int w = table.getRowHeight(); - col.setMinWidth(w); - col.setPreferredWidth(w); - col.setMaxWidth(w); - - col = columnModel.getColumn(1); - col.setPreferredWidth(200); - - col = columnModel.getColumn(2); - col.setPreferredWidth(100); - - table.addMouseListener(new GUIUtil.BooleanTableClickListener(table)); + initializeTable(types); // Add table panel = new JPanel(new MigLayout("fill")); @@ -124,21 +94,77 @@ public class CSVExportPanel extends JPanel { updateSelectedCount(); panel.add(selectedCountLabel); - this.add(panel, "grow 100, wrap"); + if (separateRowForTable) { + this.add(panel, "spanx, grow 100, wrap"); + } else { + this.add(panel, "grow 100, wrap"); + } // Add CSV options - this.add(csvOptions, "spany, split, growx 1"); + if (separateRowForTable) { + this.add(csvOptions, "grow 1"); + } else { + this.add(csvOptions, "spany, split, growx 1"); + } //// Add extra widgets if (extraComponents != null) { for (Component c : extraComponents) { - this.add(c, "spany, split, growx 1"); + if (separateRowForTable) { + this.add(c, "grow 1"); + } else { + this.add(c, "spany, split, growx 1"); + } } } // Space-filling panel - panel = new JPanel(); - this.add(panel, "width 1, height 1, grow 1"); + if (!separateRowForTable) { + panel = new JPanel(); + this.add(panel, "width 1, height 1, grow 1"); + } + } + + public CSVExportPanel(T[] types, boolean[] selected, CsvOptionPanel csvOptions, Component... extraComponents) { + this(types, selected, csvOptions, false, extraComponents); + } + + protected SelectionTableModel createTableModel() { + return new SelectionTableModel(); + } + + protected void initializeTable(T[] types) { + table.setDefaultRenderer(Object.class, + new SelectionBackgroundCellRenderer(table.getDefaultRenderer(Object.class))); + table.setDefaultRenderer(Boolean.class, + new SelectionBackgroundCellRenderer(table.getDefaultRenderer(Boolean.class))); + table.setRowSelectionAllowed(false); + table.setColumnSelectionAllowed(false); + + table.setDefaultEditor(Unit.class, new UnitCellEditor() { + private static final long serialVersionUID = 1088570433902420935L; + + @Override + protected UnitGroup getUnitGroup(Unit value, int row, int column) { + return types[row].getUnitGroup(); + } + }); + + // Set column widths + TableColumnModel columnModel = table.getColumnModel(); + TableColumn col = columnModel.getColumn(0); + int w = table.getRowHeight(); + col.setMinWidth(w); + col.setPreferredWidth(w); + col.setMaxWidth(w); + + col = columnModel.getColumn(1); + col.setPreferredWidth(200); + + col = columnModel.getColumn(2); + col.setPreferredWidth(100); + + table.addMouseListener(new GUIUtil.BooleanTableClickListener(table)); } public boolean doExport() { @@ -171,11 +197,11 @@ public class CSVExportPanel extends JPanel { /** * The table model for the variable selection. */ - private class SelectionTableModel extends AbstractTableModel { + protected class SelectionTableModel extends AbstractTableModel { private static final long serialVersionUID = 493067422917621072L; - private static final int SELECTED = 0; - private static final int NAME = 1; - private static final int UNIT = 2; + protected static final int SELECTED = 0; + protected static final int NAME = 1; + protected static final int UNIT = 2; @Override public int getColumnCount() { @@ -199,7 +225,6 @@ public class CSVExportPanel extends JPanel { trans.get("SimExpPan.Col.Unit"); default -> throw new IndexOutOfBoundsException("column=" + column); }; - } @Override @@ -214,14 +239,12 @@ public class CSVExportPanel extends JPanel { @Override public Object getValueAt(int row, int column) { - return switch (column) { case SELECTED -> selected[row]; case NAME -> types[row]; case UNIT -> units[row]; default -> throw new IndexOutOfBoundsException("column=" + column); }; - } @Override @@ -243,7 +266,6 @@ public class CSVExportPanel extends JPanel { default: throw new IndexOutOfBoundsException("column=" + column); } - } @Override