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