Add RocketComponent selector in CAExportPanel

This commit is contained in:
SiboVG 2024-08-24 16:14:39 +02:00
parent 88f7c7e18d
commit 73981839e6
5 changed files with 436 additions and 61 deletions

View File

@ -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

View File

@ -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");
}
@ -121,7 +125,15 @@ public class CsvOptionPanel extends JPanel {
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);
}

View File

@ -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<CADataType> {
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<Map<RocketComponent, Boolean>> 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<RocketComponent, Boolean> componentSelectedMap;
List<RocketComponent> 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<CADataType> {
return true;
}
@Override
protected CSVExportPanel<CADataType>.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<RocketComponent, Boolean>) 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<RocketComponent, Boolean> componentMap = (Map<RocketComponent, Boolean>) 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<RocketComponent, JCheckBox> 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<RocketComponent, Boolean> 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<RocketComponent, Boolean> componentMap, GridBagConstraints gbc) {
int row = 0;
int col = 0;
for (Map.Entry<RocketComponent, Boolean> 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<RocketComponent, Boolean> newStates) {
updatingState.set(true);
try {
for (Map.Entry<RocketComponent, Boolean> entry : newStates.entrySet()) {
JCheckBox checkBox = checkBoxMap.get(entry.getKey());
if (checkBox != null) {
checkBox.setSelected(entry.getValue());
}
}
} finally {
updatingState.set(false);
}
}
public Map<RocketComponent, Boolean> 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<RocketComponent, Boolean> componentMap = (Map<RocketComponent, Boolean>) 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();
}
}
}

View File

@ -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

View File

@ -32,8 +32,8 @@ public class CSVExportPanel<T extends UnitValue> 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<T extends UnitValue> 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<T extends UnitValue> 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<T extends UnitValue> 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<T extends UnitValue> 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<T extends UnitValue> extends JPanel {
trans.get("SimExpPan.Col.Unit");
default -> throw new IndexOutOfBoundsException("column=" + column);
};
}
@Override
@ -214,14 +239,12 @@ public class CSVExportPanel<T extends UnitValue> 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<T extends UnitValue> extends JPanel {
default:
throw new IndexOutOfBoundsException("column=" + column);
}
}
@Override