create changelog entry
[debian/openrocket] / core / src / net / sf / openrocket / gui / preset / ComponentPresetEditor.java
1 package net.sf.openrocket.gui.preset;
2
3 import java.awt.event.ActionEvent;
4 import java.awt.event.ActionListener;
5 import java.awt.event.MouseAdapter;
6 import java.awt.event.MouseEvent;
7 import java.io.File;
8 import java.io.FileReader;
9 import java.io.IOException;
10 import java.util.ArrayList;
11 import java.util.List;
12
13 import javax.swing.AbstractAction;
14 import javax.swing.Action;
15 import javax.swing.JButton;
16 import javax.swing.JDialog;
17 import javax.swing.JFileChooser;
18 import javax.swing.JFrame;
19 import javax.swing.JLabel;
20 import javax.swing.JMenu;
21 import javax.swing.JMenuBar;
22 import javax.swing.JMenuItem;
23 import javax.swing.JOptionPane;
24 import javax.swing.JPanel;
25 import javax.swing.JScrollPane;
26 import javax.swing.JSeparator;
27 import javax.swing.JTable;
28 import javax.swing.table.DefaultTableModel;
29 import javax.xml.bind.JAXBException;
30
31 import net.miginfocom.swing.MigLayout;
32 import net.sf.openrocket.gui.util.FileHelper;
33 import net.sf.openrocket.gui.util.Icons;
34 import net.sf.openrocket.gui.util.SwingPreferences;
35 import net.sf.openrocket.l10n.ResourceBundleTranslator;
36 import net.sf.openrocket.logging.LogHelper;
37 import net.sf.openrocket.material.Material;
38 import net.sf.openrocket.preset.ComponentPreset;
39 import net.sf.openrocket.preset.loader.MaterialHolder;
40 import net.sf.openrocket.preset.loader.RocksimComponentFileTranslator;
41 import net.sf.openrocket.preset.xml.OpenRocketComponentDTO;
42 import net.sf.openrocket.preset.xml.OpenRocketComponentSaver;
43 import net.sf.openrocket.startup.Application;
44
45 /**
46  * A UI for editing component presets.  Currently this is a standalone application - run the main within this class.
47  * TODO: Full I18n TODO: Save As .csv
48  */
49 public class ComponentPresetEditor extends JPanel implements PresetResultListener {
50
51     /**
52      * The logger.
53      */
54     private static final LogHelper log = Application.getLogger();
55
56     /**
57      * The I18N translator.
58      */
59     private static ResourceBundleTranslator trans = null;
60
61     /**
62      * The table of presets.
63      */
64     private JTable table;
65
66     /**
67      * The table's data model.
68      */
69     private DataTableModel model;
70
71     /**
72      * Flag that indicates if an existing Preset is currently being edited.
73      */
74  //   private boolean editingSelected = false;
75
76     private final OpenedFileContext editContext = new OpenedFileContext();
77
78     static {
79         trans = new ResourceBundleTranslator("l10n.messages");
80         net.sf.openrocket.startup.Application.setBaseTranslator(trans);
81     }
82
83     /**
84      * Create the panel.
85      *
86      * @param frame the parent window
87      */
88     public ComponentPresetEditor(final JFrame frame) {
89         setLayout(new MigLayout("", "[82.00px, grow][168.00px, grow][84px, grow][117.00px, grow][][222px]",
90                 "[346.00px, grow][29px]"));
91
92         model = new DataTableModel(new String[]{"Manufacturer", "Type", "Part No", "Description", ""});
93
94         table = new JTable(model);
95         table.getTableHeader().setFont(new JLabel().getFont());
96         //The action never gets called because the table MouseAdapter intercepts it first.  Still need an empty
97         // instance though.
98         Action action = new AbstractAction() {
99             @Override
100             public void actionPerformed(final ActionEvent e) {
101             }
102         };
103         //Create a editor/renderer for the delete operation.  Instantiation self-registers into the table.
104         new ButtonColumn(table, action, 4);
105         table.getColumnModel().getColumn(4).setMaxWidth(Icons.EDIT_DELETE.getIconWidth());
106         table.getColumnModel().getColumn(4).setMinWidth(Icons.EDIT_DELETE.getIconWidth());
107
108         JScrollPane scrollPane = new JScrollPane(table);
109         table.setFillsViewportHeight(true);
110         table.setAutoCreateRowSorter(true);
111         add(scrollPane, "cell 0 0 6 1,grow");
112
113         table.addMouseListener(new MouseAdapter() {
114             public void mouseClicked(MouseEvent e) {
115                 JTable target = (JTable) e.getSource();
116                 int selectedColumn = table.getColumnModel().getColumnIndexAtX( target.getSelectedColumn() );
117                 int selectedRow = table.getRowSorter().convertRowIndexToModel( target.getSelectedRow() );
118                 if (selectedColumn == 4) {
119                     if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(ComponentPresetEditor.this,
120                             "Do you want to delete this preset?",
121                             "Confirm Delete", JOptionPane.YES_OPTION,
122                             JOptionPane.QUESTION_MESSAGE)) {
123                         model.removeRow(selectedRow);
124                     }
125                 }
126                 else {
127                     if (e.getClickCount() == 2) {
128                         editContext.setEditingSelected(true);
129                         new PresetEditorDialog(ComponentPresetEditor.this,
130                                 (ComponentPreset) model.getAssociatedObject(selectedRow), editContext.getMaterialsLoaded()).setVisible(true);
131                     }
132                 }
133             }
134         });
135
136
137         JMenuBar menuBar = new JMenuBar();
138         frame.setJMenuBar(menuBar);
139
140         JMenu mnFile = new JMenu("File");
141         menuBar.add(mnFile);
142
143         JMenuItem mntmOpen = new JMenuItem("Open...");
144         mnFile.add(mntmOpen);
145         mntmOpen.addActionListener(new ActionListener() {
146             @Override
147             public void actionPerformed(final ActionEvent e) {
148                 if (model.getRowCount() > 0) {
149                     /*
150                      *  If the table model already contains presets, ask the user if they a) want to discard those
151                      *  presets, b) save them before reading in another component file, or c) merge the read component file
152                      *  with the current contents of the table model.
153                      */
154                     Object[] options = {"Save",
155                             "Merge",
156                             "Discard",
157                             "Cancel"};
158                     int n = JOptionPane.showOptionDialog(frame,
159                             "The editor contains existing component presets.  What would you like to do with them?",
160                             "Existing Component Presets",
161                             JOptionPane.YES_NO_CANCEL_OPTION,
162                             JOptionPane.QUESTION_MESSAGE,
163                             null,
164                             options,
165                             options[0]);
166                     if (n == 0) { //Save.  Then remove existing rows and open.
167                         if (saveAndHandleError()) {
168                             model.removeAllRows();
169                         }
170                         else { //Save failed; bail out.
171                             return;
172                         }
173                     }
174                     else if (n == 2) { //Discard and open
175                         model.removeAllRows();
176                     }
177                     else if (n == 3) { //Cancel.  Bail out.
178                         return;
179                     }
180                 }
181                 //Open file dialog
182                 openComponentFile();
183             }
184         });
185
186         JSeparator separator = new JSeparator();
187         mnFile.add(separator);
188
189         JMenuItem mntmSave = new JMenuItem("Save As...");
190         mnFile.add(mntmSave);
191         mntmSave.addActionListener(new ActionListener() {
192             @Override
193             public void actionPerformed(final ActionEvent e) {
194                 saveAndHandleError();
195             }
196         });
197
198         JSeparator separator_1 = new JSeparator();
199         mnFile.add(separator_1);
200
201         JMenuItem mntmExit = new JMenuItem("Exit");
202         mnFile.add(mntmExit);
203         mntmExit.addActionListener(new ActionListener() {
204             public void actionPerformed(ActionEvent arg0) {
205                 System.exit(0);
206             }
207         });
208
209
210         JButton addBtn = new JButton("Add");
211         addBtn.addMouseListener(new MouseAdapter() {
212             @Override
213             public void mouseClicked(MouseEvent arg0) {
214                 editContext.setEditingSelected(false);
215                 new PresetEditorDialog(ComponentPresetEditor.this).setVisible(true);
216             }
217         });
218         add(addBtn, "cell 0 1,alignx left,aligny top");
219     }
220
221     /**
222      * Callback method from the PresetEditorDialog to notify this class when a preset has been saved.  The 'save' is
223      * really just a call back here so the preset can be added to the master table.  It's not to be confused with the
224      * save to disk.
225      *
226      * @param preset the new or modified preset
227      */
228     @Override
229     public void notifyResult(final ComponentPreset preset) {
230         if (preset != null) {
231             DataTableModel model = (DataTableModel) table.getModel();
232             //Is this a new preset?
233             String description = preset.has(ComponentPreset.DESCRIPTION) ? preset.get(ComponentPreset.DESCRIPTION) :
234                     preset.getPartNo();
235             if (!editContext.isEditingSelected()|| table.getSelectedRow() == -1) {
236                 model.addRow(new Object[]{preset.getManufacturer().getDisplayName(), preset.getType().name(),
237                         preset.getPartNo(), description, Icons.EDIT_DELETE}, preset);
238             }
239             else {
240                 //This is a modified preset; update all of the columns and the stored associated instance.
241                 int row = table.getSelectedRow();
242                 model.setValueAt(preset.getManufacturer().getDisplayName(), row, 0);
243                 model.setValueAt(preset.getType().name(), row, 1);
244                 model.setValueAt(preset.getPartNo(), row, 2);
245                 model.setValueAt(description, row, 3);
246                 model.associated.set(row, preset);
247             }
248         }
249         editContext.setEditingSelected(false);
250     }
251
252     /**
253      * Launch the test main.
254      */
255     public static void main(String[] args) {
256         try {
257             Application.setPreferences(new SwingPreferences());
258             JFrame dialog = new JFrame();
259             dialog.getContentPane().add(new ComponentPresetEditor(dialog));
260             dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
261             dialog.pack();
262             dialog.setVisible(true);
263         }
264         catch (Exception e) {
265             e.printStackTrace();
266         }
267     }
268
269     /**
270      * A table model that adds associated objects to each row, allowing for easy retrieval.
271      */
272     class DataTableModel extends DefaultTableModel {
273
274         private List<Object> associated = new ArrayList<Object>();
275
276         /**
277          * Constructs a <code>DefaultTableModel</code> with as many columns as there are elements in
278          * <code>columnNames</code> and <code>rowCount</code> of <code>null</code> object values.  Each column's name
279          * will be taken from the <code>columnNames</code> array.
280          *
281          * @param columnNames <code>array</code> containing the names of the new columns; if this is <code>null</code>
282          *                    then the model has no columns
283          *
284          * @see #setDataVector
285          * @see #setValueAt
286          */
287         DataTableModel(final Object[] columnNames) {
288             super(columnNames, 0);
289         }
290
291         public void addRow(Object[] data, Object associatedData) {
292             super.addRow(data);
293             associated.add(getRowCount() - 1, associatedData);
294         }
295
296         public void removeAllRows() {
297             for (int x = getRowCount(); x > 0; x--) {
298                 super.removeRow(x - 1);
299             }
300             associated.clear();
301         }
302
303         public void removeRow(int row) {
304             super.removeRow(row);
305             associated.remove(row);
306         }
307
308         public Object getAssociatedObject(int row) {
309             return associated.get(row);
310         }
311
312         public boolean isCellEditable(int rowIndex, int mColIndex) {
313             return false;
314         }
315     }
316
317     /**
318      * Open the component file.  Present a chooser for the user to navigate to the file.
319      *
320      * @return true if the file was successfully opened; Note: side effect, is that the ComponentPresets read from the
321      *         file are written to the table model.
322      */
323     private boolean openComponentFile() {
324         final JFileChooser chooser = new JFileChooser();
325         chooser.addChoosableFileFilter(FileHelper.OPEN_ROCKET_COMPONENT_FILTER);
326         chooser.addChoosableFileFilter(FileHelper.CSV_FILE_FILTER);
327         chooser.setFileFilter(FileHelper.OPEN_ROCKET_COMPONENT_FILTER);
328         chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
329         if (editContext.getLastDirectory() != null) {
330             chooser.setCurrentDirectory(editContext.getLastDirectory());
331         }
332         else {
333             chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory());
334         }
335
336         int option = chooser.showOpenDialog(ComponentPresetEditor.this);
337         if (option != JFileChooser.APPROVE_OPTION) {
338             editContext.setOpenedFile(null);
339             log.user("User decided not to open, option=" + option);
340             return false;
341         }
342
343         File file = chooser.getSelectedFile();
344         try {
345             if (file == null) {
346                 log.user("User did not select a file");
347                 return false;
348             }
349
350             editContext.setLastDirectory(file.getParentFile());
351             editContext.setMaterialsLoaded(null);
352             List<ComponentPreset> presets = null;
353
354             if (file.getName().toLowerCase().endsWith(".orc")) {
355                 OpenRocketComponentDTO fileContents = new OpenRocketComponentSaver().unmarshalFromOpenRocketComponent(new FileReader(file));
356                 editContext.setMaterialsLoaded( new MaterialHolder(fileContents.asMaterialList()) );
357                 presets = fileContents.asComponentPresets();
358             }
359             else {
360                 if (file.getName().toLowerCase().endsWith(".csv")) {
361                     file = file.getParentFile();
362                 }
363                 presets = new ArrayList<ComponentPreset>();
364                 MaterialHolder materialHolder = RocksimComponentFileTranslator.loadAll(presets, file);
365                 editContext.setMaterialsLoaded(materialHolder);
366             }
367             if (presets != null) {
368                 for (ComponentPreset next : presets) {
369                     notifyResult(next);
370                 }
371                 editContext.setOpenedFile(file);
372             }
373         }
374         catch (Exception e) {
375             JOptionPane.showMessageDialog(ComponentPresetEditor.this, "Unable to open OpenRocket component file: " +
376                     file.getName() + " Invalid format. " + e.getMessage());
377             editContext.setOpenedFile(null);
378             editContext.setEditingSelected(false);
379             return false;
380         }
381         return true;
382     }
383
384     private boolean saveAndHandleError() {
385         try {
386             return saveAsORC();
387         }
388         catch (Exception e1) {
389             JOptionPane.showMessageDialog(ComponentPresetEditor.this, e1.getLocalizedMessage(),
390                     "Error saving ORC file.", JOptionPane.ERROR_MESSAGE);
391             return false;
392         }
393     }
394
395     /**
396      * Save the contents of the table model as XML in .orc format.
397      *
398      * @return true if the file was written
399      *
400      * @throws JAXBException thrown if the data could not be marshaled
401      * @throws IOException   thrown if there was a problem with writing the file
402      */
403     private boolean saveAsORC() throws JAXBException, IOException {
404         File file = null;
405
406         final JFileChooser chooser = new JFileChooser();
407         chooser.addChoosableFileFilter(FileHelper.OPEN_ROCKET_COMPONENT_FILTER);
408
409         chooser.setFileFilter(FileHelper.OPEN_ROCKET_COMPONENT_FILTER);
410         if (editContext.getOpenedFile() != null) {
411             chooser.setSelectedFile(editContext.getOpenedFile());
412         }
413         else {
414             chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory());
415         }
416
417         int option = chooser.showSaveDialog(ComponentPresetEditor.this);
418         if (option != JFileChooser.APPROVE_OPTION) {
419             log.user("User decided not to save, option=" + option);
420             return false;
421         }
422
423         file = chooser.getSelectedFile();
424         if (file == null) {
425             log.user("User did not select a file");
426             return false;
427         }
428
429         ((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory());
430
431         file = FileHelper.forceExtension(file, "orc");
432
433         MaterialHolder materials = new MaterialHolder();
434         List<ComponentPreset> presets = new ArrayList<ComponentPreset>();
435
436         for (int x = 0; x < model.getRowCount(); x++) {
437             ComponentPreset preset = (ComponentPreset) model.getAssociatedObject(x);
438             // If we don't have a material already defined for saving...
439             if ( materials.getMaterial(preset.get(ComponentPreset.MATERIAL)) == null ) {
440                 // Check if we loaded a material with this name.
441                 Material m = editContext.getMaterialsLoaded().getMaterial(preset.get(ComponentPreset.MATERIAL));
442                 // If there was no material loaded with that name, use the component's material.
443                 if ( m == null ) {
444                         m = preset.get(ComponentPreset.MATERIAL);
445                 }
446                 materials.put(m);
447             }
448             presets.add(preset);
449         }
450
451         return FileHelper.confirmWrite(file, this) && new OpenRocketComponentSaver().save(file, new ArrayList<Material>(materials.values()), presets);
452     }
453
454     class OpenedFileContext {
455
456         /**
457          * State variable to keep track of which file was opened, in case it needs to be saved back to that file.
458          */
459         private File openedFile = null;
460
461         /**
462          * Last directory; file chooser is set here so user doesn't have to keep navigating to a common area.
463          */
464         private File lastDirectory = null;
465
466         private boolean editingSelected = false;
467
468         private MaterialHolder materialsLoaded = null;
469
470         OpenedFileContext() {
471         }
472
473         public File getOpenedFile() {
474             return openedFile;
475         }
476
477         public void setOpenedFile(final File theOpenedFile) {
478             openedFile = theOpenedFile;
479         }
480
481         public File getLastDirectory() {
482             return lastDirectory;
483         }
484
485         public void setLastDirectory(final File theLastDirectory) {
486             lastDirectory = theLastDirectory;
487         }
488
489         public boolean isEditingSelected() {
490             return editingSelected;
491         }
492
493         public void setEditingSelected(final boolean theEditingSelected) {
494             editingSelected = theEditingSelected;
495         }
496
497         public MaterialHolder getMaterialsLoaded() {
498             return materialsLoaded;
499         }
500
501         public void setMaterialsLoaded(final MaterialHolder theMaterialsLoaded) {
502             materialsLoaded = theMaterialsLoaded;
503         }
504     }
505 }