diff --git a/.github/getting-started.png b/.github/getting-started.png new file mode 100644 index 000000000..0c64d1aa8 Binary files /dev/null and b/.github/getting-started.png differ diff --git a/README.md b/README.md index e2c1a7c90..d089d495f 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 8c2302ac8..e7e38f845 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -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) diff --git a/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java index 0239958b8..b11871fb0 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java @@ -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"); diff --git a/swing/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java index 0354815ad..befa087ba 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java @@ -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); diff --git a/swing/src/net/sf/openrocket/gui/dialogs/preferences/GeneralPreferencesPanel.java b/swing/src/net/sf/openrocket/gui/dialogs/preferences/GeneralPreferencesPanel.java index 4f3571519..5506779e5 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/preferences/GeneralPreferencesPanel.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/preferences/GeneralPreferencesPanel.java @@ -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 } }); } diff --git a/swing/src/net/sf/openrocket/gui/dialogs/preset/ComponentPresetChooserDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/preset/ComponentPresetChooserDialog.java index 8a6203ca9..e991f7c26 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/preset/ComponentPresetChooserDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/preset/ComponentPresetChooserDialog.java @@ -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(); } diff --git a/swing/src/net/sf/openrocket/gui/dialogs/preset/ComponentPresetTable.java b/swing/src/net/sf/openrocket/gui/dialogs/preset/ComponentPresetTable.java index 6b32bd009..1451892cd 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/preset/ComponentPresetTable.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/preset/ComponentPresetTable.java @@ -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(); diff --git a/swing/src/net/sf/openrocket/gui/dialogs/preset/XTableColumnModel.java b/swing/src/net/sf/openrocket/gui/dialogs/preset/XTableColumnModel.java index 6155fe091..a19455359 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/preset/XTableColumnModel.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/preset/XTableColumnModel.java @@ -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 getAllColumns() { + return allTableColumns.elements(); + } } diff --git a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java index df6d60d2b..2f4f9b84a 100644 --- a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java @@ -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); diff --git a/swing/src/net/sf/openrocket/gui/util/GUIUtil.java b/swing/src/net/sf/openrocket/gui/util/GUIUtil.java index 2becea7c8..5a3541888 100644 --- a/swing/src/net/sf/openrocket/gui/util/GUIUtil.java +++ b/swing/src/net/sf/openrocket/gui/util/GUIUtil.java @@ -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 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(); + } } diff --git a/swing/src/net/sf/openrocket/gui/util/PreferencesExporter.java b/swing/src/net/sf/openrocket/gui/util/PreferencesExporter.java index a69257c2d..b9a113bc1 100644 --- a/swing/src/net/sf/openrocket/gui/util/PreferencesExporter.java +++ b/swing/src/net/sf/openrocket/gui/util/PreferencesExporter.java @@ -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); diff --git a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java index f0aa48b88..e544fcd8d 100644 --- a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java +++ b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java @@ -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(); } diff --git a/swing/src/net/sf/openrocket/gui/util/TableUIPreferences.java b/swing/src/net/sf/openrocket/gui/util/TableUIPreferences.java new file mode 100644 index 000000000..67f1d9cdb --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/util/TableUIPreferences.java @@ -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 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 allColumns = table.getColumnModel().getColumns(); + List 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 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); + } + } +} \ No newline at end of file