Merge remote-tracking branch 'origin/unstable' into unstable

This commit is contained in:
SiboVG 2023-10-30 13:29:06 +01:00
commit 48d9532e2f
14 changed files with 294 additions and 115 deletions

BIN
.github/getting-started.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

102
README.md
View File

@ -1,65 +1,76 @@
OpenRocket
==========
# OpenRocket 🚀
OpenRocket is a free, fully featured model rocket simulator that allows you to design and simulate your rockets before actually building and flying them.
![Build Status](https://github.com/openrocket/openrocket/actions/workflows/build.yml/badge.svg)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
![GitHub release](https://img.shields.io/github/release/openrocket/openrocket.svg)
[![Github Releases (by release)](https://img.shields.io/github/downloads/openrocket/openrocket/latest/total.svg)](https://GitHub.com/openrocket/openrocket/releases/)
[![Join our Discord server!](https://img.shields.io/discord/1073297014814691328?logo=discord)](https://discord.gg/qD2G5v2FAw)
--------
OpenRocket is a free, fully featured model rocket simulator that allows you to design and simulate your rockets before actually building and flying them.
## 🛠️ Design, Visualize, and Analyze
![Three-stage rocket - 2D](.github/OpenRocket_home_2D.png)
![Three-stage rocket - 3D](.github/OpenRocket_home_3D.png)
![Three-stage rocket - Simulation plot](.github/OpenRocket_sim.png)
1. **Design** your rockets using a rich selection of built-in components:
![Three-stage rocket - 2D](.github/OpenRocket_home_2D.png)
The main features include:
2. **Visualize** your masterpiece in 3D:
![Three-stage rocket - 3D](.github/OpenRocket_home_3D.png)
* Six-degree-of-freedom flight simulation
* Automatic design optimization
* Realtime simulated altitude, velocity and acceleration display
* Staging and clustering support
* Cross-platform (Java-based)
3. **Plot & Analyze** your simulation results for precision and improvements:
![Three-stage rocket - Simulation plot](.github/OpenRocket_sim.png)
Read more about it on the [OpenRocket Wiki](http://wiki.openrocket.info).
## 🌟 Features
Installers
----------
OpenRocket maintains an installer for installing the software and Java runtime. You can find the installers on [our
website](https://openrocket.info/downloads.html).
- **Six-degree-of-freedom flight simulation**
- **Automatic design optimization**
- **Realtime simulated altitude, velocity, and acceleration display**
- **Staging and clustering support**
- **Cross-platform (Java-based)**
... plus many more
📖 Read more on the [OpenRocket Wiki](http://wiki.openrocket.info).
## 💾 Installers
Find the OpenRocket installers [here](https://openrocket.info/downloads.html)
## 📝 Release Notes
Release Notes
-------------
Release notes are available on each [release's page](https://github.com/openrocket/openrocket/releases) or on [our website](https://openrocket.info/release_notes.html).
License
-------
## 🚀 Getting started
OpenRocket is an Open Source project licensed under the [GNU GPL](https://www.gnu.org/licenses/gpl-3.0.en.html). This means that the software is free to use for whatever purposes, and the source code is also available for studying and extending.
The easiest way to get started is to open one of our in-program example designs:
Contributing
------------
OpenRocket needs help to become even better. Implementing features, writing documentation and creating example designs are just a few ways of helping. If you are interested in helping make OpenRocket the best rocket simulator out there, please [click here for information on how to get involved](http://openrocket.sourceforge.net/getinvolved.html) and [read the practicalities of contributing here](CONTRIBUTING.md).
![Get started with the example designs](.github/getting-started.png)
**Contributors**
- Sampo Niskanen, main developer
- Doug Pedrick, support for RockSim designs, printing
- Kevin Ruland, Android version
- Bill Kuker, 3D visualization
- Richard Graham, geodetic computations
- Jason Blood, freeform fin set import
- Boris du Reau, internationalization
- Daniel Williams, pod support, maintainer
- Joe Pfeiffer (maintainer)
- Billy Olsen (maintainer)
- Sibo Van Gool (RASAero file format, 3D OBJ export, dark theme, maintainer)
- Neil Weinstock (tester, icons, forum support)
- H. Craig Miller (tester)
Dive into the essentials: adjust component dimensions, plot a simulation, swap out motors, ... Explore the impact of your changes and, most importantly, enjoy the process! 😊
## 💪 Contribute
**Translators**
Help us soar higher! Whether it's implementing features, writing documentation, or creating design examples, every contribution matters. Interested? Check out [how to get involved](http://openrocket.sourceforge.net/getinvolved.html) and the [practicalities of contributing](CONTRIBUTING.md).
### ✨ Contributors
- [Sampo Niskanen](https://github.com/plaa) - Original developer
- [Doug Pedrick](https://github.com/rodinia814) - RockSim designs, printing
- [Kevin Ruland](https://github.com/kruland2607) - Android version
- [Bill Kuker](https://github.com/bkuker) - 3D visualization
- [Richard Graham](https://github.com/rdgraham) - Geodetic computations
- Jason Blood - Freeform fin set import
- [Boris du Reau](https://github.com/bdureau) - Internationalization
- [Daniel Williams](https://github.com/teyrana) - Pod support, maintainer
- [Joe Pfeiffer](https://github.com/JoePfeiffer) - Maintainer
- [Billy Olsen](https://github.com/wolsen) - Maintainer
- [Sibo Van Gool](https://github.com/SiboVG) - RASAero file format, 3D OBJ export, dark theme, maintainer
- [Neil Weinstock](https://github.com/neilweinstock) - Tester, icons, forum support
- [H. Craig Miller](https://github.com/hcraigmiller) - Tester
You can view the full list of contributors [here](https://github.com/openrocket/openrocket/graphs/contributors).
### 🌍Translators
- Tripoli France
- Tripoli Spain
- Stefan Lobas / ERIG
@ -69,3 +80,12 @@ OpenRocket needs help to become even better. Implementing features, writing docu
- Polish Rocketry Society / Łukasz & Alex Kazanski
- Sibo Van Gool
- Mohamed Amin Elkebsi
## 📜 License
OpenRocket is proudly open-source under the [GNU GPL](https://www.gnu.org/licenses/gpl-3.0.en.html) license. Feel free to use, study, and extend.
---
⭐ Please give us a star if you find OpenRocket useful, and spread the word! ⭐
[![Star History Chart](https://api.star-history.com/svg?repos=openrocket/openrocket&type=Date)](https://star-history.com/#openrocket/openrocket&Date)

View File

@ -75,7 +75,7 @@ This is the first beta release of version 23.09.
* Show calculated values in override tab (fixes #1629)
* Decrease minimum FoV to 10 degrees in Photo Studio
* Increase resolution of launch temperature and pressure to 2 decimal places (fixes #2003)
* Display Cd oerride with 3 decimal places
* Display Cd override with 3 decimal places
* Add wiki button to help menu (fixes #2046)
* Eliminate option to save "some" sim data (fixes #2024)
* Add OK/Cancel buttons when editing simulations (fixes #2158)

View File

@ -303,14 +303,21 @@ public class RocketComponentConfig extends JPanel {
}
// Yes/No dialog: Are you sure you want to discard your changes?
JPanel msg = createCancelOperationContent();
int resultYesNo = JOptionPane.showConfirmDialog(RocketComponentConfig.this, msg,
trans.get("RocketCompCfg.CancelOperation.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if (resultYesNo == JOptionPane.YES_OPTION) {
ComponentConfigDialog.clearConfigListeners = false; // Undo action => config listeners of new component will be cleared
disposeDialog();
document.undo();
}
SwingUtilities.invokeLater(() -> {
JPanel msg = createCancelOperationContent();
int resultYesNo = JOptionPane.showConfirmDialog(RocketComponentConfig.this, msg,
trans.get("RocketCompCfg.CancelOperation.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
if (resultYesNo == JOptionPane.YES_OPTION) {
ComponentConfigDialog.clearConfigListeners = false;
// Need to execute after delay, otherwise the dialog will not be disposed
GUIUtil.executeAfterDelay(100, () -> {
disposeDialog();
document.undo();
});
}
});
}
});
buttonPanel.add(cancelButton, "split 2, right, gapleft 30lp");

View File

@ -52,6 +52,7 @@ import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import net.sf.openrocket.arch.SystemInfo;
import net.sf.openrocket.gui.util.UITheme;
import net.sf.openrocket.gui.widgets.SaveFileChooser;
import net.sf.openrocket.rocketcomponent.FlightConfiguration;
import org.slf4j.Logger;
@ -1162,7 +1163,7 @@ public class GeneralOptimizationDialog extends JDialog {
// TODO: update this dynamically instead of hard-coded values
// The macOS file chooser has an issue where it does not update its size when the accessory is added.
if (SystemInfo.getPlatform() == SystemInfo.Platform.MAC_OS) {
if (SystemInfo.getPlatform() == SystemInfo.Platform.MAC_OS && UITheme.isLightTheme(GUIUtil.getUITheme())) {
Dimension currentSize = chooser.getPreferredSize();
Dimension newSize = new Dimension((int) (1.5 * currentSize.width), (int) (1.3 * currentSize.height));
chooser.setPreferredSize(newSize);

View File

@ -354,15 +354,10 @@ public class GeneralPreferencesPanel extends PreferencesPanel {
trans.get("generalprefs.ImportWarning.title"),
JOptionPane.WARNING_MESSAGE);
// Introduce a small delay before showing the PreferencesDialog
Timer timer = new Timer(100, new ActionListener() { // 100ms delay
@Override
public void actionPerformed(ActionEvent arg0) {
PreferencesDialog.showPreferences(parent.getParentFrame()); // Refresh the preferences dialog
}
// Need to execute after delay, otherwise the dialog will not be disposed
GUIUtil.executeAfterDelay(100, () -> {
PreferencesDialog.showPreferences(parent.getParentFrame()); // Refresh the preferences dialog
});
timer.setRepeats(false); // Only execute once
timer.start(); // Start the timer
}
});
}

View File

@ -36,6 +36,7 @@ import net.sf.openrocket.gui.adaptors.PresetModel;
import net.sf.openrocket.gui.components.StyledLabel;
import net.sf.openrocket.gui.util.GUIUtil;
import net.sf.openrocket.gui.util.SwingPreferences;
import net.sf.openrocket.gui.util.TableUIPreferences;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.preset.ComponentPreset;
import net.sf.openrocket.preset.TypedKey;
@ -52,6 +53,7 @@ import net.sf.openrocket.utils.TableRowTraversalPolicy;
*/
@SuppressWarnings("serial")
public class ComponentPresetChooserDialog extends JDialog {
private static final String TABLE_ID = "CmpPrst.";
private static final Translator trans = Application.getTranslator();
@ -154,10 +156,10 @@ public class ComponentPresetChooserDialog extends JDialog {
// need to create componentSelectionTable before filter checkboxes,
// but add to panel after
componentSelectionTable = new ComponentPresetTable(presetType, presets, displayedColumnKeys);
GUIUtil.setAutomaticColumnTableWidths(componentSelectionTable, 20);
// Make the first column (the favorite column) as small as possible
int w = componentSelectionTable.getRowHeight() + 4;
XTableColumnModel tm = componentSelectionTable.getXColumnModel();
//TableColumn tc = componentSelectionTable.getColumnModel().getColumn(0);
TableColumn tc = tm.getColumn(0);
tc.setPreferredWidth(w);
tc.setMaxWidth(w);
@ -166,7 +168,14 @@ public class ComponentPresetChooserDialog extends JDialog {
// The normal left/right and tab/shift-tab key action traverses each cell/column of the table instead of going to the next row.
TableRowTraversalPolicy.setTableRowTraversalPolicy(componentSelectionTable);
// Add checkboxes (legacy, match fore diameter, ...)
panel.add(getFilterCheckboxes(tm, legacyColumnIndex), "wrap para");
// Load the table UI settings from the preferences
boolean legacySelected = showLegacyCheckBox.isSelected();
TableUIPreferences.loadTableUIPreferences(componentSelectionTable, TABLE_ID + component.getComponentName(),
preferences.getTablePreferences());
showLegacyCheckBox.setSelected(legacySelected); // Restore legacy state (may change during UI preference loading)
JScrollPane scrollpane = new JScrollPane();
scrollpane.setViewportView(componentSelectionTable);
@ -202,6 +211,8 @@ public class ComponentPresetChooserDialog extends JDialog {
closeButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
TableUIPreferences.storeTableUIPreferences(componentSelectionTable, TABLE_ID + component.getComponentName(),
preferences.getTablePreferences());
ComponentPresetChooserDialog.this.setVisible(false);
applySelectedPreset();
}
@ -214,7 +225,6 @@ public class ComponentPresetChooserDialog extends JDialog {
GUIUtil.rememberWindowSize(this);
this.setLocationByPlatform(true);
GUIUtil.rememberWindowPosition(this);
GUIUtil.rememberTableColumnWidths(componentSelectionTable, "Presets" + component.getClass().getCanonicalName());
updateFilters();
}

View File

@ -86,7 +86,10 @@ public class ComponentPresetTable extends JTable {
ComponentPreset preset = ComponentPresetTable.this.presets.get(rowIndex);
Application.getComponentPresetDao().setFavorite(preset, presetType, (Boolean) aValue);
ComponentPresetTable.this.updateFavorites();
ComponentPresetTable.this.setRowSelectionInterval(rowIndex, rowIndex);
int viewIndex = ComponentPresetTable.this.convertRowIndexToView(rowIndex);
if (viewIndex != -1) {
ComponentPresetTable.this.setRowSelectionInterval(viewIndex, viewIndex);
}
}
@Override
@ -163,7 +166,7 @@ public class ComponentPresetTable extends JTable {
this.sorter.toggleSortOrder(2); // Sort by the first column (manufacturer) by default
for (TableColumn hiddenColumn : this.hiddenColumns) {
this.tableColumnModel.setColumnVisible(hiddenColumn, false);
this.tableColumnModel.removeColumn(hiddenColumn);
}
JTableHeader header = this.getTableHeader();

View File

@ -239,4 +239,13 @@ public class XTableColumnModel extends DefaultTableColumnModel {
public TableColumn getColumn(int columnIndex, boolean onlyVisible) {
return tableColumns.elementAt(columnIndex);
}
/**
* Returns an Enumeration of all columns, regardless of their visibility.
*
* @return an Enumeration of all TableColumn objects in this model
*/
public Enumeration<TableColumn> getAllColumns() {
return allTableColumns.elements();
}
}

View File

@ -44,7 +44,9 @@ import javax.swing.table.DefaultTableCellRenderer;
import net.sf.openrocket.arch.SystemInfo;
import net.sf.openrocket.gui.components.CsvOptionPanel;
import net.sf.openrocket.gui.util.FileHelper;
import net.sf.openrocket.gui.util.GUIUtil;
import net.sf.openrocket.gui.util.SwingPreferences;
import net.sf.openrocket.gui.util.UITheme;
import net.sf.openrocket.gui.widgets.SaveFileChooser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -534,7 +536,7 @@ public class SimulationPanel extends JPanel {
// TODO: update this dynamically instead of hard-coded values
// The macOS file chooser has an issue where it does not update its size when the accessory is added.
if (SystemInfo.getPlatform() == SystemInfo.Platform.MAC_OS) {
if (SystemInfo.getPlatform() == SystemInfo.Platform.MAC_OS && UITheme.isLightTheme(GUIUtil.getUITheme())) {
Dimension currentSize = chooser.getPreferredSize();
Dimension newSize = new Dimension((int) (1.5 * currentSize.width), (int) (1.3 * currentSize.height));
chooser.setPreferredSize(newSize);

View File

@ -51,12 +51,14 @@ import javax.swing.RootPaneContainer;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
@ -404,36 +406,6 @@ public class GUIUtil {
}
}
public static void rememberTableColumnWidths(final JTable table, String keyName) {
final String key = keyName == null ? table.getClass().getName() : keyName;
Enumeration<TableColumn> columns = table.getColumnModel().getColumns();
while (columns.hasMoreElements()) {
TableColumn column = columns.nextElement();
column.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals("width")) {
log.debug("Storing width of " + table.getName() + "-" + column + ": " + column.getWidth());
((SwingPreferences) Application.getPreferences()).setTableColumnWidth(
key, column.getModelIndex(), column.getWidth());
}
}
});
final Integer width = ((SwingPreferences) Application.getPreferences()).getTableColumnWidth(
key, column.getModelIndex());
if (width != null) {
column.setPreferredWidth(width);
} else {
column.setPreferredWidth(getOptimalColumnWidth(table, column.getModelIndex()));
}
}
}
public static void rememberTableColumnWidths(final JTable table) {
rememberTableColumnWidths(table, null);
}
public static int getOptimalColumnWidth(JTable table, int columnIndex) {
if (columnIndex >= table.getColumnModel().getColumnCount()) {
return -1;
@ -476,26 +448,46 @@ public class GUIUtil {
window.setLocation(position);
}
}
public static void setAutomaticColumnTableWidths(JTable table, int max) {
/**
* Computes the optimal column widths for the specified table, based on the content in all rows for that column,
* and optionally also the column header content.
* @param table the table
* @param max the maximum width for a column
* @param includeHeaderWidth whether to include the width of the column header
* @return an array of column widths
*/
public static int[] computeOptimalColumnWidths(JTable table, int max, boolean includeHeaderWidth) {
int columns = table.getColumnCount();
int widths[] = new int[columns];
int[] widths = new int[columns];
Arrays.fill(widths, 1);
// Consider the width required by the header text if includeHeaderWidth is true
if (includeHeaderWidth) {
TableCellRenderer headerRenderer = table.getTableHeader().getDefaultRenderer();
for (int col = 0; col < columns; col++) {
TableColumn column = table.getColumnModel().getColumn(col);
Component headerComp = headerRenderer.getTableCellRendererComponent(table, column.getHeaderValue(), false, false, 0, col);
widths[col] = headerComp.getPreferredSize().width;
}
}
// Compare header width to the width required by the cell data
for (int row = 0; row < table.getRowCount(); row++) {
for (int col = 0; col < columns; col++) {
Object value = table.getValueAt(row, col);
//System.out.println("row=" + row + " col=" + col + " : " + value);
widths[col] = Math.max(widths[col], value == null ? 0 : value.toString().length());
TableCellRenderer cellRenderer = table.getCellRenderer(row, col);
Component cellComp = cellRenderer.getTableCellRendererComponent(table, value, false, false, row, col);
int cellWidth = cellComp.getPreferredSize().width;
widths[col] = Math.max(widths[col], cellWidth);
}
}
for (int col = 0; col < columns; col++) {
//System.err.println("Setting column " + col + " to width " + widths[col]);
table.getColumnModel().getColumn(col).setPreferredWidth(Math.min(widths[col], max) * 100);
widths[col] = Math.min(widths[col], max * 100); // Adjusting for your max value and scaling
}
return widths;
}
public static Window getWindowAncestor(Component c) {
@ -761,5 +753,22 @@ public class GUIUtil {
}
}
/**
* Executes the given code after a specified delay.
*
* @param delayMillis the delay in milliseconds.
* @param runnable the code to be executed after the delay.
*/
public static void executeAfterDelay(int delayMillis, Runnable runnable) {
Timer timer = new Timer(delayMillis, new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
runnable.run();
}
});
timer.setRepeats(false);
timer.start();
}
}

View File

@ -55,7 +55,7 @@ public abstract class PreferencesExporter {
// TODO: update this dynamically instead of hard-coded values
// The macOS file chooser has an issue where it does not update its size when the accessory is added.
if (SystemInfo.getPlatform() == SystemInfo.Platform.MAC_OS) {
if (SystemInfo.getPlatform() == SystemInfo.Platform.MAC_OS && UITheme.isLightTheme(GUIUtil.getUITheme())) {
Dimension currentSize = chooser.getPreferredSize();
Dimension newSize = new Dimension((int) (1.35 * currentSize.width), (int) (1.2 * currentSize.height));
chooser.setPreferredSize(newSize);

View File

@ -136,6 +136,22 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences {
public Preferences getPreferences() {
return PREFNODE;
}
/**
* Returns the preference node responsible for saving UI window information (position, size...)
* @return the preference node for window information
*/
public Preferences getWindowsPreferences() {
return PREFNODE.node(NODE_WINDOWS);
}
/**
* Returns the preference node responsible for saving table information (column widths, order...)
* @return the preference node for table information
*/
public Preferences getTablePreferences() {
return PREFNODE.node(NODE_TABLES);
}
public void clearPreferences() {
try {
@ -550,7 +566,7 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences {
public Point getWindowPosition(Class<?> c) {
int x, y;
String pref = PREFNODE.node(NODE_WINDOWS).get("position." + c.getCanonicalName(), null);
String pref = getWindowsPreferences().get("position." + c.getCanonicalName(), null);
if (pref == null)
return null;
@ -575,7 +591,7 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences {
}
public void setWindowPosition(Class<?> c, Point p) {
PREFNODE.node(NODE_WINDOWS).put("position." + c.getCanonicalName(), "" + p.x + "," + p.y);
getWindowsPreferences().put("position." + c.getCanonicalName(), "" + p.x + "," + p.y);
storeVersion();
}
@ -603,7 +619,7 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences {
public Dimension getWindowSize(Class<?> c) {
int x, y;
String pref = PREFNODE.node(NODE_WINDOWS).get("size." + c.getCanonicalName(), null);
String pref = getWindowsPreferences().get("size." + c.getCanonicalName(), null);
if (pref == null)
return null;
@ -622,22 +638,22 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences {
public boolean isWindowMaximized(Class<?> c) {
String pref = PREFNODE.node(NODE_WINDOWS).get("size." + c.getCanonicalName(), null);
String pref = getWindowsPreferences().get("size." + c.getCanonicalName(), null);
return "max".equals(pref);
}
public void setWindowSize(Class<?> c, Dimension d) {
PREFNODE.node(NODE_WINDOWS).put("size." + c.getCanonicalName(), "" + d.width + "," + d.height);
getWindowsPreferences().put("size." + c.getCanonicalName(), "" + d.width + "," + d.height);
storeVersion();
}
public void setWindowMaximized(Class<?> c) {
PREFNODE.node(NODE_WINDOWS).put("size." + c.getCanonicalName(), "max");
getWindowsPreferences().put("size." + c.getCanonicalName(), "max");
storeVersion();
}
public Integer getTableColumnWidth(String keyName, int columnIdx) {
String pref = PREFNODE.node(NODE_TABLES).get(
String pref = getTablePreferences().get(
"cw." + keyName + "." + columnIdx, null);
if (pref == null)
return null;
@ -655,7 +671,7 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences {
}
public void setTableColumnWidth(String keyName, int columnIdx, Integer width) {
PREFNODE.node(NODE_TABLES).put(
getTablePreferences().put(
"cw." + keyName + "." + columnIdx, width.toString());
storeVersion();
}

View File

@ -0,0 +1,107 @@
package net.sf.openrocket.gui.util;
import net.sf.openrocket.gui.dialogs.preset.XTableColumnModel;
import javax.swing.JTable;
import javax.swing.table.TableColumn;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.prefs.Preferences;
/**
* Utility class for storing and loading the following table UI preferences:
* - column width
* - column order
* - column visibility
*/
public class TableUIPreferences {
private static final String TABLE_COLUMN_WIDTH_PREFIX = ".cw.";
private static final String TABLE_COLUMN_ORDER_PREFIX = ".co.";
private static final String TABLE_COLUMN_VISIBILITY_PREFIX = ".cv.";
public static void storeTableUIPreferences(JTable table, String tableName, Preferences preferences) {
// Store column widths
for (int i = 0; i < table.getColumnCount(); i++) {
TableColumn column = table.getColumnModel().getColumn(i);
int width = column.getWidth();
preferences.putInt(tableName + TABLE_COLUMN_WIDTH_PREFIX + column.getIdentifier(), width);
}
// Store column order
for (int i = 0; i < table.getColumnCount(); i++) {
TableColumn column = table.getColumnModel().getColumn(i);
preferences.putInt(tableName + TABLE_COLUMN_ORDER_PREFIX + column.getIdentifier(), i);
}
// Store column visibility
if (table.getColumnModel() instanceof XTableColumnModel customModel) {
Enumeration<TableColumn> columns = customModel.getAllColumns();
while (columns.hasMoreElements()) {
TableColumn column = columns.nextElement();
boolean isVisible = customModel.isColumnVisible(column);
preferences.putBoolean(tableName + TABLE_COLUMN_VISIBILITY_PREFIX + column.getIdentifier(), isVisible);
}
}
}
public static void loadTableUIPreferences(JTable table, String tableName, Preferences preferences) {
// Ensure all columns are visible
Enumeration<TableColumn> allColumns = table.getColumnModel().getColumns();
List<TableColumn> removedColumns = new ArrayList<>();
while (allColumns.hasMoreElements()) {
TableColumn column = allColumns.nextElement();
if (table.convertColumnIndexToView(column.getModelIndex()) == -1) {
removedColumns.add(column);
}
}
for (TableColumn col : removedColumns) {
table.addColumn(col);
}
// Get all columns from the table's column model and restore visibility
if (table.getColumnModel() instanceof XTableColumnModel customModel) {
Enumeration<TableColumn> columns = customModel.getAllColumns(); // Use getAllColumns to get all columns, including invisible ones
while (columns.hasMoreElements()) {
TableColumn column = columns.nextElement();
String identifier = column.getIdentifier().toString();
// Default to true if the preference is not found
boolean isVisible = preferences.getBoolean(tableName + TABLE_COLUMN_VISIBILITY_PREFIX + identifier, true);
customModel.setColumnVisible(column, isVisible);
}
}
// Now, restore column order
for (int i = 0; i < table.getColumnCount(); i++) {
TableColumn column = table.getColumnModel().getColumn(i);
int storedOrder = preferences.getInt(tableName + TABLE_COLUMN_ORDER_PREFIX + column.getIdentifier(), i);
if (storedOrder != i && storedOrder < table.getColumnCount()) {
table.moveColumn(table.convertColumnIndexToView(column.getModelIndex()), storedOrder);
}
}
// Check if any column width is missing from preferences
boolean computeOptimalWidths = false;
for (int i = 0; i < table.getColumnCount() && !computeOptimalWidths; i++) {
TableColumn column = table.getColumnModel().getColumn(i);
if (preferences.get(tableName + TABLE_COLUMN_WIDTH_PREFIX + column.getIdentifier(), null) == null) {
computeOptimalWidths = true;
}
}
// If any column width is missing, compute optimal widths for all columns
int[] optimalWidths = null;
if (computeOptimalWidths) {
optimalWidths = GUIUtil.computeOptimalColumnWidths(table, 20, true);
}
// Restore column widths
for (int i = 0; i < table.getColumnCount(); i++) {
TableColumn column = table.getColumnModel().getColumn(i);
int defaultWidth = (optimalWidths != null) ? optimalWidths[i] : column.getWidth();
int width = preferences.getInt(tableName + TABLE_COLUMN_WIDTH_PREFIX + column.getIdentifier(), defaultWidth);
column.setPreferredWidth(width);
}
}
}