001/*============================================================================*
002 * WARNING      This class contains automatically modified code.      WARNING *
003 *                                                                            *
004 * The method initComponents() and the variable declarations between the      *
005 * "// Variables declaration - do not modify" and                             *
006 * "// End of variables declaration" comments will be overwritten if modified *
007 * by hand. Using the NetBeans IDE to edit this file is strongly recommended. *
008 *                                                                            *
009 * See http://jmri.org/help/en/html/doc/Technical/NetBeansGUIEditor.shtml for *
010 * more information.                                                          *
011 *============================================================================*/
012package jmri.profile;
013
014import java.awt.Cursor;
015import java.awt.Frame;
016import java.awt.event.ActionEvent;
017import java.awt.event.ActionListener;
018import java.awt.event.MouseEvent;
019import java.beans.PropertyChangeListener;
020import java.io.File;
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.ResourceBundle;
024
025import javax.swing.ButtonGroup;
026import javax.swing.GroupLayout;
027import javax.swing.JButton;
028import javax.swing.JComponent;
029import javax.swing.JFileChooser;
030import javax.swing.JLabel;
031import javax.swing.JMenuItem;
032import javax.swing.JPanel;
033import javax.swing.JPopupMenu;
034import javax.swing.JRadioButton;
035import javax.swing.JScrollPane;
036import javax.swing.JSpinner;
037import javax.swing.JTabbedPane;
038import javax.swing.JTable;
039import javax.swing.LayoutStyle;
040import javax.swing.SpinnerNumberModel;
041import javax.swing.SwingUtilities;
042import javax.swing.event.ChangeEvent;
043import javax.swing.event.ChangeListener;
044import javax.swing.event.ListSelectionEvent;
045import javax.swing.event.ListSelectionListener;
046import javax.swing.event.PopupMenuEvent;
047import javax.swing.event.PopupMenuListener;
048import javax.swing.filechooser.FileNameExtensionFilter;
049
050import jmri.jmrit.roster.Roster;
051import jmri.swing.PreferencesPanel;
052import jmri.util.FileUtil;
053import jmri.util.prefs.InitializationException;
054import jmri.util.swing.JmriJOptionPane;
055
056import org.jdom2.JDOMException;
057import org.openide.util.lookup.ServiceProvider;
058
059/**
060 * A JPanel suitable for managing {@link jmri.profile.Profile}s within a
061 * preferences window.
062 *
063 * @author Randall Wood
064 */
065@ServiceProvider(service = PreferencesPanel.class)
066public final class ProfilePreferencesPanel extends JPanel implements PreferencesPanel {
067
068    /**
069     * Creates new form ProfilePreferencesPanel
070     */
071    public ProfilePreferencesPanel() {
072        initComponents();
073        this.spinnerTimeout.setValue(ProfileManager.getDefault().getAutoStartActiveProfileTimeout());
074        this.profilesTblValueChanged(null);
075        this.searchPathsTblValueChanged(null);
076        int index = ProfileManager.getDefault().getAllProfiles().indexOf(ProfileManager.getDefault().getActiveProfile());
077        if (index != -1) {
078            this.profilesTbl.setRowSelectionInterval(index, index);
079        }
080    }
081
082    /**
083     * This method is called from within the constructor to initialize the form.
084     * WARNING: Do NOT modify this code. The content of this method is always
085     * regenerated by the Form Editor.
086     */
087    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
088    private void initComponents() {
089
090        profilesPopupMenu = new JPopupMenu();
091        renameMI = new JMenuItem();
092        jSeparator1 = new JPopupMenu.Separator();
093        copyMI = new JMenuItem();
094        deleteMI = new JMenuItem();
095        grpStartWithSelectors = new ButtonGroup();
096        jTabbedPane1 = new JTabbedPane();
097        enabledPanel = new JPanel();
098        jScrollPane1 = new JScrollPane();
099        profilesTbl = new JTable() {
100            //Implement table cell tool tips.
101            @Override
102            public String getToolTipText(MouseEvent e) {
103                try {
104                    return getValueAt(rowAtPoint(e.getPoint()), -1).toString();
105                } catch (RuntimeException e1) {
106                    //catch null pointer exception if mouse is over an empty line
107                }
108                return null;
109            }};
110            btnOpenExistingProfile = new JButton();
111            btnDeleteProfile = new JButton();
112            btnCreateNewProfile = new JButton();
113            btnActivateProfile = new JButton();
114            btnExportProfile = new JButton();
115            btnCopyProfile = new JButton();
116            spinnerTimeout = new JSpinner();
117            jLabel1 = new JLabel();
118            rdoStartWithActiveProfile = new JRadioButton();
119            rdoStartWithProfileSelector = new JRadioButton();
120            searchPathsPanel = new JPanel();
121            btnRemoveSearchPath = new JButton();
122            btnAddSearchPath = new JButton();
123            jScrollPane3 = new JScrollPane();
124            searchPathsTbl = new JTable() {
125                //Implement table cell tool tips.
126                @Override
127                public String getToolTipText(MouseEvent e) {
128                    try {
129                        return getValueAt(rowAtPoint(e.getPoint()), -1).toString();
130                    } catch (RuntimeException e1) {
131                        //catch null pointer exception if mouse is over an empty line
132                    }
133                    return null;
134                }};
135
136                profilesPopupMenu.addPopupMenuListener(new PopupMenuListener() {
137                    @Override
138                    public void popupMenuWillBecomeVisible(PopupMenuEvent evt) {
139                        profilesPopupMenuPopupMenuWillBecomeVisible(evt);
140                    }
141                    @Override
142                    public void popupMenuWillBecomeInvisible(PopupMenuEvent evt) {
143                    }
144                    @Override
145                    public void popupMenuCanceled(PopupMenuEvent evt) {
146                    }
147                });
148
149                ResourceBundle bundle = ResourceBundle.getBundle("jmri/profile/Bundle"); // NOI18N
150                renameMI.setText(bundle.getString("ProfilePreferencesPanel.renameMI.text")); // NOI18N
151                renameMI.addActionListener(new ActionListener() {
152                    @Override
153                    public void actionPerformed(ActionEvent evt) {
154                        renameMIActionPerformed(evt);
155                    }
156                });
157                profilesPopupMenu.add(renameMI);
158                profilesPopupMenu.add(jSeparator1);
159
160                copyMI.setText(bundle.getString("ProfilePreferencesPanel.copyMI.text")); // NOI18N
161                profilesPopupMenu.add(copyMI);
162
163                deleteMI.setText(bundle.getString("ProfilePreferencesPanel.deleteMI.text")); // NOI18N
164                profilesPopupMenu.add(deleteMI);
165
166                if (ProfileManager.getDefault().isAutoStartActiveProfile()) {
167                    this.rdoStartWithActiveProfile.setSelected(true);
168                } else {
169                    this.rdoStartWithProfileSelector.setSelected(true);
170                }
171
172                profilesTbl.setModel(new ProfileTableModel());
173                profilesTbl.getSelectionModel().addListSelectionListener(new ProfilesSelectionListener());
174                profilesTbl.getTableHeader().setReorderingAllowed(false);
175                jScrollPane1.setViewportView(profilesTbl);
176
177                btnOpenExistingProfile.setText(bundle.getString("ProfilePreferencesPanel.btnOpenExistingProfile.text")); // NOI18N
178                btnOpenExistingProfile.setToolTipText(bundle.getString("ProfilePreferencesPanel.btnOpenExistingProfile.toolTipText")); // NOI18N
179                btnOpenExistingProfile.addActionListener(new ActionListener() {
180                    @Override
181                    public void actionPerformed(ActionEvent evt) {
182                        btnOpenExistingProfileActionPerformed(evt);
183                    }
184                });
185
186                btnDeleteProfile.setText(bundle.getString("ProfilePreferencesPanel.btnDeleteProfile.text")); // NOI18N
187                btnDeleteProfile.setToolTipText(bundle.getString("ProfilePreferencesPanel.btnDeleteProfile.toolTipText")); // NOI18N
188                btnDeleteProfile.addActionListener(new ActionListener() {
189                    @Override
190                    public void actionPerformed(ActionEvent evt) {
191                        btnDeleteProfileActionPerformed(evt);
192                    }
193                });
194
195                btnCreateNewProfile.setText(bundle.getString("ProfilePreferencesPanel.btnCreateNewProfile.text")); // NOI18N
196                btnCreateNewProfile.setToolTipText(bundle.getString("ProfilePreferencesPanel.btnCreateNewProfile.toolTipText")); // NOI18N
197                btnCreateNewProfile.addActionListener(new ActionListener() {
198                    @Override
199                    public void actionPerformed(ActionEvent evt) {
200                        btnCreateNewProfileActionPerformed(evt);
201                    }
202                });
203
204                btnActivateProfile.setText(bundle.getString("ProfilePreferencesPanel.btnActivateProfile.text")); // NOI18N
205                btnActivateProfile.setToolTipText(bundle.getString("ProfilePreferencesPanel.btnActivateProfile.toolTipText")); // NOI18N
206                btnActivateProfile.addActionListener(new ActionListener() {
207                    @Override
208                    public void actionPerformed(ActionEvent evt) {
209                        btnActivateProfileActionPerformed(evt);
210                    }
211                });
212
213                btnExportProfile.setText(bundle.getString("ProfilePreferencesPanel.btnExportProfile.text")); // NOI18N
214                btnExportProfile.setToolTipText(bundle.getString("ProfilePreferencesPanel.btnExportProfile.toolTipText")); // NOI18N
215                btnExportProfile.addActionListener(new ActionListener() {
216                    @Override
217                    public void actionPerformed(ActionEvent evt) {
218                        btnExportProfileActionPerformed(evt);
219                    }
220                });
221
222                btnCopyProfile.setText(bundle.getString("ProfilePreferencesPanel.btnCopyProfile.text")); // NOI18N
223                btnCopyProfile.addActionListener(new ActionListener() {
224                    @Override
225                    public void actionPerformed(ActionEvent evt) {
226                        btnCopyProfileActionPerformed(evt);
227                    }
228                });
229
230                spinnerTimeout.setModel(new SpinnerNumberModel(10, 0, 500, 1));
231                spinnerTimeout.addChangeListener(new ChangeListener() {
232                    @Override
233                    public void stateChanged(ChangeEvent evt) {
234                        spinnerTimeoutStateChanged(evt);
235                    }
236                });
237
238                jLabel1.setText(bundle.getString("ProfilePreferencesPanel.jLabel1.text")); // NOI18N
239
240                grpStartWithSelectors.add(rdoStartWithActiveProfile);
241                rdoStartWithActiveProfile.setText(bundle.getString("ProfilePreferencesPanel.rdoStartWithActiveProfile.text")); // NOI18N
242                rdoStartWithActiveProfile.addActionListener(new ActionListener() {
243                    @Override
244                    public void actionPerformed(ActionEvent evt) {
245                        rdoStartWithActiveProfileActionPerformed(evt);
246                    }
247                });
248
249                grpStartWithSelectors.add(rdoStartWithProfileSelector);
250                rdoStartWithProfileSelector.setText(bundle.getString("ProfilePreferencesPanel.rdoStartWithProfileSelector.text")); // NOI18N
251                rdoStartWithProfileSelector.addActionListener(new ActionListener() {
252                    @Override
253                    public void actionPerformed(ActionEvent evt) {
254                        rdoStartWithProfileSelectorActionPerformed(evt);
255                    }
256                });
257
258                GroupLayout enabledPanelLayout = new GroupLayout(enabledPanel);
259                enabledPanel.setLayout(enabledPanelLayout);
260                enabledPanelLayout.setHorizontalGroup(enabledPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
261                    .addGroup(enabledPanelLayout.createSequentialGroup()
262                        .addContainerGap()
263                        .addGroup(enabledPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
264                            .addComponent(jScrollPane1)
265                            .addGroup(enabledPanelLayout.createSequentialGroup()
266                                .addComponent(btnActivateProfile)
267                                .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
268                                .addComponent(btnOpenExistingProfile)
269                                .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
270                                .addComponent(btnCreateNewProfile)
271                                .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
272                                .addComponent(btnCopyProfile)
273                                .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
274                                .addComponent(btnExportProfile)
275                                .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, 60, Short.MAX_VALUE)
276                                .addComponent(btnDeleteProfile))
277                            .addGroup(enabledPanelLayout.createSequentialGroup()
278                                .addGroup(enabledPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
279                                    .addComponent(rdoStartWithActiveProfile)
280                                    .addGroup(enabledPanelLayout.createSequentialGroup()
281                                        .addComponent(rdoStartWithProfileSelector)
282                                        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
283                                        .addComponent(spinnerTimeout, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
284                                        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
285                                        .addComponent(jLabel1)))
286                                .addGap(0, 0, Short.MAX_VALUE)))
287                        .addContainerGap())
288                );
289                enabledPanelLayout.setVerticalGroup(enabledPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
290                    .addGroup(enabledPanelLayout.createSequentialGroup()
291                        .addContainerGap()
292                        .addComponent(jScrollPane1, GroupLayout.DEFAULT_SIZE, 190, Short.MAX_VALUE)
293                        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
294                        .addGroup(enabledPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
295                            .addComponent(btnOpenExistingProfile)
296                            .addComponent(btnCreateNewProfile)
297                            .addComponent(btnActivateProfile)
298                            .addComponent(btnExportProfile)
299                            .addComponent(btnDeleteProfile)
300                            .addComponent(btnCopyProfile))
301                        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
302                        .addComponent(rdoStartWithActiveProfile)
303                        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
304                        .addGroup(enabledPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
305                            .addComponent(rdoStartWithProfileSelector)
306                            .addComponent(spinnerTimeout, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
307                            .addComponent(jLabel1)))
308                );
309
310                jTabbedPane1.addTab(bundle.getString("ProfilePreferencesPanel.enabledPanel.TabConstraints.tabTitle"), enabledPanel); // NOI18N
311
312                btnRemoveSearchPath.setText(bundle.getString("ProfilePreferencesPanel.btnRemoveSearchPath.text")); // NOI18N
313                btnRemoveSearchPath.setToolTipText(bundle.getString("ProfilePreferencesPanel.btnRemoveSearchPath.toolTipText")); // NOI18N
314                btnRemoveSearchPath.addActionListener(new ActionListener() {
315                    @Override
316                    public void actionPerformed(ActionEvent evt) {
317                        btnRemoveSearchPathActionPerformed(evt);
318                    }
319                });
320
321                btnAddSearchPath.setText(bundle.getString("ProfilePreferencesPanel.btnAddSearchPath.text")); // NOI18N
322                btnAddSearchPath.setToolTipText(bundle.getString("ProfilePreferencesPanel.btnAddSearchPath.toolTipText")); // NOI18N
323                btnAddSearchPath.addActionListener(new ActionListener() {
324                    @Override
325                    public void actionPerformed(ActionEvent evt) {
326                        btnAddSearchPathActionPerformed(evt);
327                    }
328                });
329
330                searchPathsTbl.setModel(new SearchPathTableModel());
331                searchPathsTbl.getSelectionModel().addListSelectionListener(new SearchPathSelectionListener());
332                searchPathsTbl.getTableHeader().setReorderingAllowed(false);
333                jScrollPane3.setViewportView(searchPathsTbl);
334
335                GroupLayout searchPathsPanelLayout = new GroupLayout(searchPathsPanel);
336                searchPathsPanel.setLayout(searchPathsPanelLayout);
337                searchPathsPanelLayout.setHorizontalGroup(searchPathsPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
338                    .addGroup(searchPathsPanelLayout.createSequentialGroup()
339                        .addContainerGap()
340                        .addGroup(searchPathsPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
341                            .addComponent(jScrollPane3, GroupLayout.DEFAULT_SIZE, 667, Short.MAX_VALUE)
342                            .addGroup(searchPathsPanelLayout.createSequentialGroup()
343                                .addComponent(btnAddSearchPath)
344                                .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
345                                .addComponent(btnRemoveSearchPath)))
346                        .addContainerGap())
347                );
348                searchPathsPanelLayout.setVerticalGroup(searchPathsPanelLayout.createParallelGroup(GroupLayout.Alignment.LEADING)
349                    .addGroup(searchPathsPanelLayout.createSequentialGroup()
350                        .addContainerGap()
351                        .addComponent(jScrollPane3, GroupLayout.DEFAULT_SIZE, 247, Short.MAX_VALUE)
352                        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
353                        .addGroup(searchPathsPanelLayout.createParallelGroup(GroupLayout.Alignment.BASELINE)
354                            .addComponent(btnAddSearchPath)
355                            .addComponent(btnRemoveSearchPath))
356                        .addContainerGap())
357                );
358
359                jTabbedPane1.addTab(bundle.getString("ProfilePreferencesPanel.searchPathsPanel.TabConstraints.tabTitle_1"), searchPathsPanel); // NOI18N
360
361                GroupLayout layout = new GroupLayout(this);
362                this.setLayout(layout);
363                layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
364                    .addComponent(jTabbedPane1, GroupLayout.Alignment.TRAILING)
365                );
366                layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
367                    .addComponent(jTabbedPane1)
368                );
369
370                jTabbedPane1.getAccessibleContext().setAccessibleName(bundle.getString("ProfilePreferencesPanel.enabledPanel.TabConstraints.tabTitle")); // NOI18N
371            }// </editor-fold>//GEN-END:initComponents
372
373    private void renameMIActionPerformed(ActionEvent evt) {//GEN-FIRST:event_renameMIActionPerformed
374        // TODO add your handling code here:
375    }//GEN-LAST:event_renameMIActionPerformed
376
377    private void profilesPopupMenuPopupMenuWillBecomeVisible(PopupMenuEvent evt) {//GEN-FIRST:event_profilesPopupMenuPopupMenuWillBecomeVisible
378        if (profilesTbl.getSelectedRowCount() == 1) {
379            this.renameMI.setEnabled(true);
380        }
381    }//GEN-LAST:event_profilesPopupMenuPopupMenuWillBecomeVisible
382
383    private void btnAddSearchPathActionPerformed(ActionEvent evt) {//GEN-FIRST:event_btnAddSearchPathActionPerformed
384        JFileChooser chooser = new jmri.util.swing.JmriJFileChooser(FileUtil.getHomePath());
385        chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
386        chooser.setFileFilter(new ProfileFileFilter());
387        chooser.setFileView(new ProfileFileView());
388        // TODO: Use NetBeans OpenDialog if its availble
389        if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
390            try {
391                ProfileManager.getDefault().addSearchPath(chooser.getSelectedFile());
392                int index = ProfileManager.getDefault().getAllSearchPaths().indexOf(chooser.getSelectedFile());
393                this.searchPathsTbl.setRowSelectionInterval(index, index);
394            } catch (IOException ex) {
395                log.warn("Unable to write profiles while adding search path {}", chooser.getSelectedFile().getPath(), ex);
396                JmriJOptionPane.showMessageDialog(this,
397                        Bundle.getMessage("ProfilePreferencesPanel.btnAddSearchPath.errorMessage",
398                                chooser.getSelectedFile().getPath(),
399                                ex.getLocalizedMessage()),
400                        Bundle.getMessage("ProfilePreferencesPanel.btnAddSearchPath.errorTitle"),
401                        JmriJOptionPane.ERROR_MESSAGE);
402            }
403        }
404    }//GEN-LAST:event_btnAddSearchPathActionPerformed
405
406    private void btnRemoveSearchPathActionPerformed(ActionEvent evt) {//GEN-FIRST:event_btnRemoveSearchPathActionPerformed
407        ArrayList<File> paths = new ArrayList<>(this.searchPathsTbl.getSelectedRowCount());
408        for (int row : this.searchPathsTbl.getSelectedRows()) {
409            paths.add(ProfileManager.getDefault().getSearchPaths(row));
410        }
411        for (File path : paths) {
412            try {
413                ProfileManager.getDefault().removeSearchPath(path);
414            } catch (IOException ex) {
415                log.warn("Unable to write profiles while removing search path {}", path.getPath(), ex);
416                JmriJOptionPane.showMessageDialog(this,
417                        Bundle.getMessage("ProfilePreferencesPanel.btnRemoveSearchPath.errorMessage", path.getPath(), ex.getLocalizedMessage()),
418                        Bundle.getMessage("ProfilePreferencesPanel.btnRemoveSearchPath.errorTitle"),
419                        JmriJOptionPane.ERROR_MESSAGE);
420            }
421        }
422    }//GEN-LAST:event_btnRemoveSearchPathActionPerformed
423
424    private void btnExportProfileActionPerformed(ActionEvent evt) {//GEN-FIRST:event_btnExportProfileActionPerformed
425        Profile p = ProfileManager.getDefault().getProfiles(profilesTbl.getSelectedRow());
426        if (p == null) {
427            // abort if selection does not match an existing profile
428            return;
429        }
430        JFileChooser chooser = new jmri.util.swing.JmriJFileChooser();
431        chooser.setFileFilter(new FileNameExtensionFilter("ZIP Archives", "zip"));
432        chooser.setFileView(new ProfileFileView());
433        chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
434        chooser.setSelectedFile(new File(p.getName() + ".zip"));
435        if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
436            try {
437                if (chooser.getSelectedFile().exists()) {
438                    int result = JmriJOptionPane.showConfirmDialog(this,
439                            Bundle.getMessage("ProfilePreferencesPanel.btnExportProfile.overwriteMessage",
440                                    chooser.getSelectedFile().getName(),
441                                    chooser.getSelectedFile().getParentFile().getName()),
442                            Bundle.getMessage("ProfilePreferencesPanel.btnExportProfile.overwriteTitle"),
443                            JmriJOptionPane.YES_NO_OPTION,
444                            JmriJOptionPane.WARNING_MESSAGE);
445                    if (result == JmriJOptionPane.YES_OPTION) {
446                        if (!chooser.getSelectedFile().delete()) {
447                            JmriJOptionPane.showMessageDialog(this,
448                                    Bundle.getMessage("ProfilePreferencesPanel.btnExportProfile.failureToDeleteMessage",
449                                            chooser.getSelectedFile().getName(),
450                                            chooser.getSelectedFile().getParentFile().getName()),
451                                    Bundle.getMessage("ProfilePreferencesPanel.btnExportProfile.failureToDeleteTitle"),
452                                    JmriJOptionPane.ERROR_MESSAGE);
453                        }
454                    } else {
455                        this.btnExportProfileActionPerformed(evt);
456                        return;
457                    }
458                }
459                boolean exportExternalUserFiles = false;
460                boolean exportExternalRoster = false;
461                if (!(new File(FileUtil.getUserFilesPath())).getCanonicalPath().startsWith(p.getPath().getCanonicalPath())) {
462                    int result = JmriJOptionPane.showConfirmDialog(this,
463                            Bundle.getMessage("ProfilePreferencesPanel.btnExportProfile.externalUserFilesMessage"),
464                            Bundle.getMessage("ProfilePreferencesPanel.btnExportProfile.externalUserFilesTitle"),
465                            JmriJOptionPane.YES_NO_OPTION,
466                            JmriJOptionPane.QUESTION_MESSAGE);
467                    if (result == JmriJOptionPane.YES_OPTION) {
468                        exportExternalUserFiles = true;
469                    }
470                }
471                if (!(new File(Roster.getDefault().getRosterLocation())).getCanonicalPath().startsWith(p.getPath().getCanonicalPath())
472                        && !Roster.getDefault().getRosterLocation().startsWith(FileUtil.getUserFilesPath())) {
473                    int result = JmriJOptionPane.showConfirmDialog(this,
474                            Bundle.getMessage("ProfilePreferencesPanel.btnExportProfile.externalRosterMessage"),
475                            Bundle.getMessage("ProfilePreferencesPanel.btnExportProfile.externalRosterTitle"),
476                            JmriJOptionPane.YES_NO_OPTION,
477                            JmriJOptionPane.QUESTION_MESSAGE);
478                    if (result == JmriJOptionPane.YES_OPTION) {
479                        exportExternalRoster = true;
480                    }
481                }
482                //if (ProfileManager.getDefault().getActiveProfile() == p) {
483                //    // TODO: save roster, panels, operations if needed and safe to do so
484                //}
485                ProfileManager.getDefault().export(p, chooser.getSelectedFile(), exportExternalUserFiles, exportExternalRoster);
486                log.info("Profile \"{}\" exported to \"{}\"", p.getName(), chooser.getSelectedFile().getName());
487                JmriJOptionPane.showMessageDialog(this,
488                        Bundle.getMessage("ProfilePreferencesPanel.btnExportProfile.successMessage",
489                                p.getName(), chooser.getSelectedFile().getName()),
490                        Bundle.getMessage("ProfilePreferencesPanel.btnExportProfile.successTitle"),
491                        JmriJOptionPane.INFORMATION_MESSAGE);
492            } catch (IOException | JDOMException | InitializationException ex) {
493                log.warn("Unable to export profile \"{}\" to {}", p.getName(), chooser.getSelectedFile().getPath(), ex);
494                JmriJOptionPane.showMessageDialog(this,
495                        Bundle.getMessage("ProfilePreferencesPanel.btnExportProfile.errorMessage",
496                                p.getName(),
497                                chooser.getSelectedFile().getPath(),
498                                ex.getLocalizedMessage()),
499                        Bundle.getMessage("ProfilePreferencesPanel.btnExportProfile.errorTitle"),
500                        JmriJOptionPane.ERROR_MESSAGE);
501            }
502        }
503    }//GEN-LAST:event_btnExportProfileActionPerformed
504
505    private void btnActivateProfileActionPerformed(ActionEvent evt) {//GEN-FIRST:event_btnActivateProfileActionPerformed
506        try {
507            Profile p = ProfileManager.getDefault().getProfiles(profilesTbl.getSelectedRow());
508            ProfileManager.getDefault().setNextActiveProfile(p);
509            ProfileManager.getDefault().saveActiveProfile(p, ProfileManager.getDefault().isAutoStartActiveProfile());
510        } catch (IOException ex) {
511            log.error("Unable to save profile preferences", ex);
512            JmriJOptionPane.showMessageDialog(this, "Usable to save profile preferences.\n" + ex.getLocalizedMessage(), "Error", JmriJOptionPane.ERROR_MESSAGE);
513        }
514    }//GEN-LAST:event_btnActivateProfileActionPerformed
515
516    private void btnCreateNewProfileActionPerformed(ActionEvent evt) {//GEN-FIRST:event_btnCreateNewProfileActionPerformed
517        AddProfileDialog apd = new AddProfileDialog((Frame) SwingUtilities.getWindowAncestor(this), true, true);
518        apd.setLocationRelativeTo(this);
519        apd.setVisible(true);
520    }//GEN-LAST:event_btnCreateNewProfileActionPerformed
521
522    private void btnDeleteProfileActionPerformed(ActionEvent evt) {//GEN-FIRST:event_btnDeleteProfileActionPerformed
523        ArrayList<Profile> profiles = new ArrayList<>(this.profilesTbl.getSelectedRowCount());
524        for (int row : this.profilesTbl.getSelectedRows()) {
525            profiles.add(ProfileManager.getDefault().getAllProfiles().get(row));
526        }
527        for (Profile deletedProfile : profiles) {
528            if (!Profile.isProfile(deletedProfile.getPath())) {
529                int result = JmriJOptionPane.showOptionDialog(this,
530                        Bundle.getMessage("ProfilePreferencesPanel.btnDeleteProfile.dlgMessageNotAProfile", deletedProfile.getName(),deletedProfile.getPath()), // NOI18N
531                        Bundle.getMessage("ProfilePreferencesPanel.btnDeleteProfile.dlgTitle", deletedProfile.getName()), // NOI18N
532                        JmriJOptionPane.OK_CANCEL_OPTION,
533                        JmriJOptionPane.QUESTION_MESSAGE,
534                        null, // use default icon
535                        new String[]{
536                            Bundle.getMessage("ProfilePreferencesPanel.btnDeleteProfile.text"), // NOI18N
537                            Bundle.getMessage("AddProfileDialog.btnCancel.text") // NOI18N
538                        },
539                        JmriJOptionPane.CANCEL_OPTION
540                );
541                if (result == JmriJOptionPane.OK_OPTION) {
542                    ProfileManager.getDefault().removeProfile(deletedProfile);
543                    log.info("Removed profile \"{}\" left directory {}", deletedProfile.getName(), deletedProfile.getPath());
544                    this.setCursor(Cursor.getDefaultCursor());
545                    profilesTbl.repaint();
546                }
547            } else {
548                int result = JmriJOptionPane.showOptionDialog(this,
549                        Bundle.getMessage("ProfilePreferencesPanel.btnDeleteProfile.dlgMessage", deletedProfile.getName(), deletedProfile.getPath(), "AAAAA" ), // NOI18N
550                        Bundle.getMessage("ProfilePreferencesPanel.btnDeleteProfile.dlgTitle", deletedProfile.getName()), // NOI18N
551                        JmriJOptionPane.OK_CANCEL_OPTION,
552                        JmriJOptionPane.QUESTION_MESSAGE,
553                        null, // use default icon
554                        new String[]{
555                            Bundle.getMessage("ProfilePreferencesPanel.btnDeleteProfile.text"), // NOI18N
556                            Bundle.getMessage("AddProfileDialog.btnCancel.text") // NOI18N
557                        },
558                        JmriJOptionPane.CANCEL_OPTION
559                );
560                if (result == JmriJOptionPane.OK_OPTION) {
561                    this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
562                    if (!FileUtil.delete(deletedProfile.getPath())) {
563                        log.warn("Unable to delete profile directory {}", deletedProfile.getPath());
564                        this.setCursor(Cursor.getDefaultCursor());
565                        JmriJOptionPane.showMessageDialog(this,
566                                Bundle.getMessage("ProfilePreferencesPanel.btnDeleteProfile.errorMessage", deletedProfile.getPath()),
567                                Bundle.getMessage("ProfilePreferencesPanel.btnDeleteProfile.errorMessage"),
568                                JmriJOptionPane.ERROR_MESSAGE);
569                        this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
570                    }
571                    ProfileManager.getDefault().removeProfile(deletedProfile);
572                    log.info("Removed profile \"{}\" from {}", deletedProfile.getName(), deletedProfile.getPath());
573                }
574            }
575            this.setCursor(Cursor.getDefaultCursor());
576            profilesTbl.repaint();
577        }
578    }//GEN-LAST:event_btnDeleteProfileActionPerformed
579
580    private void btnOpenExistingProfileActionPerformed(ActionEvent evt) {//GEN-FIRST:event_btnOpenExistingProfileActionPerformed
581        JFileChooser chooser = new jmri.util.swing.JmriJFileChooser(FileUtil.getHomePath());
582        chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
583        chooser.setFileFilter(new ProfileFileFilter());
584        chooser.setFileView(new ProfileFileView());
585        // TODO: Use NetBeans OpenDialog if its availble
586        if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
587            try {
588                if (!Profile.isProfile(chooser.getSelectedFile())) {
589                    log.warn("{} is not a profile directory", chooser.getSelectedFile());
590                    JmriJOptionPane.showMessageDialog(this,
591                            Bundle.getMessage("addExistingNotAProfile",chooser.getSelectedFile()),
592                            Bundle.getMessage("addExistingNotAProfile"),
593                            JmriJOptionPane.ERROR_MESSAGE);
594                    return;
595                } else {
596                    Profile p = new Profile(chooser.getSelectedFile());
597                    ProfileManager.getDefault().addProfile(p);
598                    int index = ProfileManager.getDefault().getAllProfiles().indexOf(p);
599                    profilesTbl.setRowSelectionInterval(index, index);
600                }
601            } catch (IOException ex) {
602                // new Profile can throw an exception, but not for directory is not a profile directory
603                // it just creates and invalid null profile
604                // which when subsequently deleted removes the entire directory tree.
605                log.warn("Unexpected error in new Profile({})", chooser.getSelectedFile(),ex);
606                return;
607            }
608        }
609    }//GEN-LAST:event_btnOpenExistingProfileActionPerformed
610
611    private void btnCopyProfileActionPerformed(ActionEvent evt) {//GEN-FIRST:event_btnCopyProfileActionPerformed
612        AddProfileDialog apd = new AddProfileDialog((Frame) SwingUtilities.getWindowAncestor(this), true, true);
613        apd.setSourceProfile(ProfileManager.getDefault().getAllProfiles().get(profilesTbl.getSelectedRow()));
614        apd.setLocationRelativeTo(this);
615        apd.setVisible(true);
616    }//GEN-LAST:event_btnCopyProfileActionPerformed
617
618    private void spinnerTimeoutStateChanged(ChangeEvent evt) {//GEN-FIRST:event_spinnerTimeoutStateChanged
619        ProfileManager.getDefault().setAutoStartActiveProfileTimeout((Integer) this.spinnerTimeout.getValue());
620        try {
621            ProfileManager.getDefault().saveActiveProfile();
622        } catch (IOException ex) {
623            log.error("Unable to save active profile.", ex);
624        }
625    }//GEN-LAST:event_spinnerTimeoutStateChanged
626
627    private void rdoStartWithActiveProfileActionPerformed(ActionEvent evt) {//GEN-FIRST:event_rdoStartWithActiveProfileActionPerformed
628        this.setAutoStartActiveProfile(true);
629        this.spinnerTimeout.setEnabled(false);
630    }//GEN-LAST:event_rdoStartWithActiveProfileActionPerformed
631
632    private void rdoStartWithProfileSelectorActionPerformed(ActionEvent evt) {//GEN-FIRST:event_rdoStartWithProfileSelectorActionPerformed
633        this.setAutoStartActiveProfile(false);
634        this.spinnerTimeout.setEnabled(true);
635    }//GEN-LAST:event_rdoStartWithProfileSelectorActionPerformed
636
637    private void setAutoStartActiveProfile(boolean automatic) {
638        ProfileManager.getDefault().setAutoStartActiveProfile(automatic);
639        try {
640            ProfileManager.getDefault().saveActiveProfile();
641        } catch (IOException ex) {
642            log.error("Unable to save active profile.", ex);
643        }
644    }
645
646    // Variables declaration - do not modify//GEN-BEGIN:variables
647    private JButton btnActivateProfile;
648    private JButton btnAddSearchPath;
649    private JButton btnCopyProfile;
650    private JButton btnCreateNewProfile;
651    private JButton btnDeleteProfile;
652    private JButton btnExportProfile;
653    private JButton btnOpenExistingProfile;
654    private JButton btnRemoveSearchPath;
655    private JMenuItem copyMI;
656    private JMenuItem deleteMI;
657    private JPanel enabledPanel;
658    private ButtonGroup grpStartWithSelectors;
659    private JLabel jLabel1;
660    private JScrollPane jScrollPane1;
661    private JScrollPane jScrollPane3;
662    private JPopupMenu.Separator jSeparator1;
663    private JTabbedPane jTabbedPane1;
664    private JPopupMenu profilesPopupMenu;
665    private JTable profilesTbl;
666    private JRadioButton rdoStartWithActiveProfile;
667    private JRadioButton rdoStartWithProfileSelector;
668    private JMenuItem renameMI;
669    private JPanel searchPathsPanel;
670    private JTable searchPathsTbl;
671    private JSpinner spinnerTimeout;
672    // End of variables declaration//GEN-END:variables
673
674    @Override
675    public String getPreferencesItem() {
676        return "Profiles"; // NOI18N
677    }
678
679    @Override
680    public String getPreferencesItemText() {
681        return Bundle.getMessage("ProfilePreferencesPanel.enabledPanel.TabConstraints.tabTitle");
682    }
683
684    @Override
685    public String getTabbedPreferencesTitle() {
686        return null;
687    }
688
689    @Override
690    public String getLabelKey() {
691        return null;
692    }
693
694    @Override
695    public JComponent getPreferencesComponent() {
696        return this;
697    }
698
699    @Override
700    public boolean isPersistant() {
701        return false;
702    }
703
704    @Override
705    public String getPreferencesTooltip() {
706        return null;
707    }
708
709    @Override
710    public void savePreferences() {
711        // Nothing to do since ProfileManager preferences are saved immediately
712    }
713
714    @Override
715    public int getSortOrder() {
716        return 1000;
717    }
718
719    public void dispose() {
720        ProfileManager.getDefault().removePropertyChangeListener((PropertyChangeListener) profilesTbl.getModel());
721    }
722
723    private void profilesTblValueChanged(ListSelectionEvent e) {
724        if (profilesTbl.getSelectedRowCount() == 1 && profilesTbl.getSelectedRow() < ProfileManager.getDefault().getAllProfiles().size()) {
725            Profile p = ProfileManager.getDefault().getAllProfiles().get(profilesTbl.getSelectedRow());
726            this.btnDeleteProfile.setEnabled(!p.equals(ProfileManager.getDefault().getActiveProfile()));
727            if (ProfileManager.getDefault().getNextActiveProfile() != null) {
728                this.btnActivateProfile.setEnabled(!p.equals(ProfileManager.getDefault().getNextActiveProfile()));
729            } else {
730                this.btnActivateProfile.setEnabled(!p.equals(ProfileManager.getDefault().getActiveProfile()));
731            }
732            this.btnCopyProfile.setEnabled(true);
733            this.btnExportProfile.setEnabled(true);
734        } else if (this.profilesTbl.getSelectedRowCount() > 1) {
735            this.btnDeleteProfile.setEnabled(true);
736            for (int row : this.profilesTbl.getSelectedRows()) {
737                Profile p = ProfileManager.getDefault().getAllProfiles().get(row);
738                if (p.equals(ProfileManager.getDefault().getActiveProfile())) {
739                    this.btnDeleteProfile.setEnabled(false);
740                    break;
741                }
742            }
743            this.btnCopyProfile.setEnabled(false);
744            this.btnExportProfile.setEnabled(false);
745            this.btnActivateProfile.setEnabled(false);
746        } else {
747            this.btnDeleteProfile.setEnabled(false);
748            this.btnCopyProfile.setEnabled(false);
749            this.btnExportProfile.setEnabled(false);
750            this.btnActivateProfile.setEnabled(false);
751        }
752    }
753
754    private void searchPathsTblValueChanged(ListSelectionEvent e) {
755        if (this.searchPathsTbl.getSelectedRowCount() == 1 && this.searchPathsTbl.getSelectedRow() < ProfileManager.getDefault().getAllSearchPaths().size()) {
756            File sp = ProfileManager.getDefault().getSearchPaths(this.searchPathsTbl.getSelectedRow());
757            if (sp == null || sp.equals(new File(FileUtil.getPreferencesPath()))) {
758                this.btnRemoveSearchPath.setEnabled(false);
759            } else {
760                this.btnRemoveSearchPath.setEnabled(true);
761            }
762        } else if (this.searchPathsTbl.getSelectedRowCount() > 1) {
763            this.btnRemoveSearchPath.setEnabled(true);
764        } else {
765            this.btnRemoveSearchPath.setEnabled(false);
766        }
767    }
768
769    @Override
770    public boolean isDirty() {
771        return false; // ProfileManager preferences are saved immediately, so this is always false
772    }
773
774    /**
775     * {@inheritDoc}
776     * @return if the next profile to use has changed; false otherwise
777     */
778    @Override
779    public boolean isRestartRequired() {
780        // Since next profile defaults to null when application starts, restart
781        // is required only if next profile is not null and is not the same
782        // profile as the current profile
783        Profile ap = ProfileManager.getDefault().getActiveProfile();
784        Profile np = ProfileManager.getDefault().getNextActiveProfile();
785        return np != null && ap != null && !ap.equals(np);
786    }
787
788    @Override
789    public boolean isPreferencesValid() {
790        return true; // no validity checking performed
791    }
792
793    /* Comment out until I get around to utilizing this, so Jenkins does not throw warnings.
794     private static class ZipFileFilter extends FileFilter {
795
796     public ZipFileFilter() {
797     }
798
799     @Override
800     public boolean accept(File f) {
801     if (!f.isDirectory()) {
802     int i = f.getName().lastIndexOf('.');
803     if (i > 0 && i < f.getName().length() - 1) {
804     return f.getName().substring(i + 1).toLowerCase().equalsIgnoreCase("zip"); // NOI18N
805     }
806     return false;
807     }
808     return true;
809     }
810
811     @Override
812     public String getDescription() {
813     return "Zip archives (.zip)";
814     }
815     }
816     */
817    private class SearchPathSelectionListener implements ListSelectionListener {
818
819        @Override
820        public void valueChanged(ListSelectionEvent e) {
821            ProfilePreferencesPanel.this.searchPathsTblValueChanged(e);
822        }
823    }
824
825    private class ProfilesSelectionListener implements ListSelectionListener {
826
827        @Override
828        public void valueChanged(ListSelectionEvent e) {
829            ProfilePreferencesPanel.this.profilesTblValueChanged(e);
830        }
831    }
832
833    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ProfilePreferencesPanel.class);
834
835}