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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
015import java.awt.Dimension;
016import java.awt.Frame;
017import java.awt.event.ActionEvent;
018import java.awt.event.ActionListener;
019import java.awt.event.AdjustmentEvent;
020import java.awt.event.KeyAdapter;
021import java.awt.event.KeyEvent;
022import java.awt.event.MouseAdapter;
023import java.awt.event.MouseEvent;
024import java.awt.event.WindowAdapter;
025import java.awt.event.WindowEvent;
026import java.beans.PropertyChangeEvent;
027import java.io.IOException;
028import javax.swing.GroupLayout;
029import javax.swing.JButton;
030import javax.swing.JDialog;
031import javax.swing.JFileChooser;
032import javax.swing.JLabel;
033import javax.swing.JList;
034import javax.swing.JScrollPane;
035import javax.swing.LayoutStyle;
036import javax.swing.ListSelectionModel;
037import javax.swing.Timer;
038import javax.swing.WindowConstants;
039import javax.swing.event.ListSelectionEvent;
040import javax.swing.event.ListSelectionListener;
041import jmri.util.FileUtil;
042import jmri.util.swing.JmriJOptionPane;
043
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047/**
048 * Display a list of {@link Profile}s that can be selected to start a JMRI
049 * application.
050 * <p>
051 * This dialog is designed to be displayed while an application is starting. If
052 * the last profile used for the application can be found, this dialog will
053 * automatically start the application with that profile after 10 seconds unless
054 * the user intervenes.
055 *
056 * @author Randall Wood
057 */
058public class ProfileManagerDialog extends JDialog {
059
060    private Timer timer;
061    private int countDown;
062    private boolean disableTimer;
063
064    /**
065     * Creates new form ProfileManagerDialog
066     *
067     * @param parent The frame containing this dialog
068     * @param modal The modal parameter for parent JDialog
069     */
070    public ProfileManagerDialog(Frame parent, boolean modal) {
071        this(parent, modal, false);
072    }
073
074    /**
075     * Creates new form ProfileManagerDialog
076     *
077     * @param parent The frame containing this dialog
078     * @param modal The modal parameter for parent JDialog
079     * @param disableTimer true if the timer should be disabled
080     */
081    public ProfileManagerDialog(Frame parent, boolean modal, boolean disableTimer) {
082        super(parent, modal);
083        this.disableTimer = disableTimer;
084        initComponents();
085        ProfileManager.getDefault().addPropertyChangeListener(ProfileManager.ACTIVE_PROFILE, (PropertyChangeEvent evt) -> {
086            profiles.setSelectedValue(ProfileManager.getDefault().getActiveProfile(), true);
087            profiles.ensureIndexIsVisible(profiles.getSelectedIndex());
088            profiles.repaint();
089        });
090        ProfileManager.getDefault().addPropertyChangeListener(Profile.NAME, (PropertyChangeEvent evt) -> {
091            if (evt.getSource().getClass().equals(Profile.class) && evt.getPropertyName().equals(Profile.NAME)) {
092                profileNameChanged(((Profile) evt.getSource()));
093            }
094        });
095        this.jScrollPane1.getVerticalScrollBar().addAdjustmentListener((AdjustmentEvent e) -> {
096            profilesValueChanged(null);
097        });
098    }
099
100    /**
101     * This method is called from within the constructor to initialize the form.
102     * WARNING: Do NOT modify this code. The content of this method is always
103     * regenerated by the Form Editor.
104     */
105     // This uses the deprecated {@link JComponent#setNextFocusableComponent} method.
106     // Because it's autogenerated code, we leave that in place for now.
107    @SuppressWarnings( "deprecation" ) // JComponent#setNextFocusableComponent
108    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
109    private void initComponents() {
110
111        listLabel = new JLabel();
112        jScrollPane1 = new JScrollPane();
113        profiles = new JList<>();
114        btnSelect = new JButton();
115        btnCreate = new JButton();
116        btnUseExisting = new JButton();
117        countDownLbl = new JLabel();
118
119        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
120        setTitle(Bundle.getMessage("ProfileManagerDialog.title")); // NOI18N
121        setMinimumSize(new Dimension(310, 110));
122        addMouseListener(new MouseAdapter() {
123            @Override
124            public void mousePressed(MouseEvent evt) {
125                formMousePressed(evt);
126            }
127        });
128        addWindowListener(new WindowAdapter() {
129            @Override
130            public void windowOpened(WindowEvent evt) {
131                formWindowOpened(evt);
132            }
133            @Override
134            public void windowClosed(WindowEvent evt) {
135                formWindowClosed(evt);
136            }
137        });
138
139        listLabel.setText(Bundle.getMessage("ProfileManagerDialog.listLabel.text")); // NOI18N
140
141        profiles.setModel(new ProfileListModel());
142        profiles.setSelectedValue(ProfileManager.getDefault().getActiveProfile(), true);
143        profiles.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
144        profiles.setToolTipText(Bundle.getMessage("ProfileManagerDialog.profiles.toolTipText")); // NOI18N
145        profiles.setCellRenderer(new ProfileListCellRenderer());
146
147
148        profiles.setNextFocusableComponent(btnSelect);
149
150
151        profiles.addKeyListener(new KeyAdapter() {
152            @Override
153            public void keyPressed(KeyEvent evt) {
154                profilesKeyPressed(evt);
155            }
156        });
157        profiles.addListSelectionListener(new ListSelectionListener() {
158            @Override
159            public void valueChanged(ListSelectionEvent evt) {
160                profilesValueChanged(evt);
161            }
162        });
163        jScrollPane1.setViewportView(profiles);
164        profiles.ensureIndexIsVisible(profiles.getSelectedIndex());
165        profiles.getAccessibleContext().setAccessibleName(Bundle.getMessage("ProfileManagerDialog.profiles.AccessibleContext.accessibleName")); // NOI18N
166        profiles.getAccessibleContext().setAccessibleDescription(Bundle.getMessage("ProfileManagerDialog.profiles.toolTipText")); // NOI18N
167
168        btnSelect.setText(Bundle.getMessage("ProfileManagerDialog.btnSelect.text")); // NOI18N
169        btnSelect.addActionListener(new ActionListener() {
170            @Override
171            public void actionPerformed(ActionEvent evt) {
172                btnSelectActionPerformed(evt);
173            }
174        });
175
176        btnCreate.setText(Bundle.getMessage("ProfileManagerDialog.btnCreate.text")); // NOI18N
177        btnCreate.setToolTipText(Bundle.getMessage("ProfilePreferencesPanel.btnCreateNewProfile.toolTipText")); // NOI18N
178        btnCreate.addActionListener(new ActionListener() {
179            @Override
180            public void actionPerformed(ActionEvent evt) {
181                btnCreateActionPerformed(evt);
182            }
183        });
184
185        btnUseExisting.setText(Bundle.getMessage("ProfileManagerDialog.btnUseExisting.text")); // NOI18N
186        btnUseExisting.addActionListener(new ActionListener() {
187            @Override
188            public void actionPerformed(ActionEvent evt) {
189                btnUseExistingActionPerformed(evt);
190            }
191        });
192
193        countDownLbl.setText(Bundle.getMessage("ProfileManagerDialog.countDownLbl.text")); // NOI18N
194        countDownLbl.setToolTipText(Bundle.getMessage("ProfileManagerDialog.countDownLbl.toolTipText")); // NOI18N
195
196        GroupLayout layout = new GroupLayout(getContentPane());
197        getContentPane().setLayout(layout);
198        layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
199            .addGroup(layout.createSequentialGroup()
200                .addContainerGap()
201                .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
202                    .addComponent(listLabel)
203                    .addComponent(countDownLbl))
204                .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
205                .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
206                    .addGroup(layout.createSequentialGroup()
207                        .addGap(0, 0, Short.MAX_VALUE)
208                        .addComponent(btnUseExisting)
209                        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
210                        .addComponent(btnCreate)
211                        .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED)
212                        .addComponent(btnSelect))
213                    .addComponent(jScrollPane1))
214                .addContainerGap())
215        );
216        layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
217            .addGroup(layout.createSequentialGroup()
218                .addContainerGap()
219                .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
220                    .addComponent(jScrollPane1, GroupLayout.DEFAULT_SIZE, 168, Short.MAX_VALUE)
221                    .addComponent(listLabel))
222                .addPreferredGap(LayoutStyle.ComponentPlacement.UNRELATED)
223                .addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
224                    .addComponent(btnSelect)
225                    .addComponent(btnCreate)
226                    .addComponent(btnUseExisting)
227                    .addComponent(countDownLbl))
228                .addContainerGap())
229        );
230
231        listLabel.getAccessibleContext().setAccessibleName(Bundle.getMessage("ProfileManagerDialog.listLabel.text")); // NOI18N
232
233        pack();
234    }// </editor-fold>//GEN-END:initComponents
235
236    private void btnSelectActionPerformed(ActionEvent evt) {//GEN-FIRST:event_btnSelectActionPerformed
237        timer.stop();
238        countDown = -1;
239        countDownLbl.setVisible(false);
240        if (profiles.getSelectedValue() != null) {
241            ProfileManager.getDefault().setActiveProfile(profiles.getSelectedValue());
242            dispose();
243        }
244    }//GEN-LAST:event_btnSelectActionPerformed
245
246    private void btnCreateActionPerformed(ActionEvent evt) {//GEN-FIRST:event_btnCreateActionPerformed
247        timer.stop();
248        countDownLbl.setVisible(false);
249        AddProfileDialog apd = new AddProfileDialog(this, true, false);
250        apd.setLocationRelativeTo(this);
251        apd.setVisible(true);
252    }//GEN-LAST:event_btnCreateActionPerformed
253
254    private void btnUseExistingActionPerformed(ActionEvent evt) {//GEN-FIRST:event_btnUseExistingActionPerformed
255        timer.stop();
256        countDownLbl.setVisible(false);
257        JFileChooser chooser = new jmri.util.swing.JmriJFileChooser(FileUtil.getHomePath());
258        chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
259        chooser.setFileFilter(new ProfileFileFilter());
260        chooser.setFileView(new ProfileFileView());
261        // TODO: Use NetBeans OpenDialog if its available
262        if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
263            try {
264                if (!Profile.isProfile(chooser.getSelectedFile())) {
265                    log.warn("{} is not a profile directory", chooser.getSelectedFile());
266                    JmriJOptionPane.showMessageDialog(this,
267                            Bundle.getMessage("addExistingNotAProfile",chooser.getSelectedFile()),
268                            Bundle.getMessage("addExistingNotAProfile"),
269                            JmriJOptionPane.ERROR_MESSAGE);
270                    return;
271                } else {
272                    Profile p = new Profile(chooser.getSelectedFile());
273                    ProfileManager.getDefault().addProfile(p);
274                    profiles.setSelectedValue(p, true);
275                }
276            } catch (IOException ex) {
277                // new Profile can throw an exception, but not for when a directory is not a profile directory
278                // it just creates an invalid null profile
279                // which when subsequently deleted removes the entire directory tree.
280                log.warn("Unexpected error in new Profile({})", chooser.getSelectedFile(),ex);
281                return;
282            }
283        }
284    }//GEN-LAST:event_btnUseExistingActionPerformed
285
286    private void formWindowOpened(WindowEvent evt) {//GEN-FIRST:event_formWindowOpened
287        countDown = ProfileManager.getDefault().getAutoStartActiveProfileTimeout();
288        if (disableTimer) {
289            countDownLbl.setText("");
290        } else {
291            countDownLbl.setText(Integer.toString(countDown));
292        }
293        timer = new Timer(1000, (ActionEvent e) -> {
294            if (disableTimer) {
295                return;
296            }
297            if (countDown > 0) {
298                countDown--;
299                countDownLbl.setText(Integer.toString(countDown));
300            } else {
301                setVisible(false);
302                Profile profile = profiles.getSelectedValue();
303                ProfileManager.getDefault().setActiveProfile(profile);
304                if (profile != null) {
305                    log.info("Automatically starting with profile {} after timeout.", profile.getId());
306                } else {
307                    log.info("Automatically starting without a profile");
308                }
309                timer.stop();
310                countDown = -1;
311                dispose();
312            }
313        });
314        timer.setRepeats(true);
315        if (profiles.getModel().getSize() > 0
316                && null != ProfileManager.getDefault().getActiveProfile()
317                && countDown > 0) {
318            timer.start();
319        } else {
320            countDownLbl.setVisible(false);
321            btnSelect.setEnabled(false);
322        }
323    }//GEN-LAST:event_formWindowOpened
324
325    /**
326     * Get the active profile or display a dialog to prompt the user for it.
327     *
328     * @param f  The {@link java.awt.Frame} to display the dialog over
329     * @return the active or selected {@link Profile}
330     * @throws java.io.IOException if unable to read or set the starting Profile
331     * @see ProfileManager#getStartingProfile()
332     */
333    public static Profile getStartingProfile(Frame f) throws IOException {
334        ProfileManager manager = ProfileManager.getDefault();
335        if (ProfileManager.getStartingProfile() == null
336                || (System.getProperty(ProfileManager.SYSTEM_PROPERTY) == null
337                && !manager.isAutoStartActiveProfile())) {
338            Profile last = manager.getActiveProfile();
339            ProfileManagerDialog pmd = new ProfileManagerDialog(f, true);
340            pmd.setLocationRelativeTo(f);
341            pmd.setVisible(true);
342            if (last == null || !last.equals(manager.getActiveProfile())) {
343                manager.saveActiveProfile();
344            }
345        }
346        return manager.getActiveProfile();
347    }
348
349    private void profileNameChanged(Profile p) {
350        p.save();
351        log.info("Saving profile {}", p.getId());
352    }
353
354    private void profilesValueChanged(ListSelectionEvent evt) {//GEN-FIRST:event_profilesValueChanged
355        timer.stop();
356        countDownLbl.setVisible(false);
357        btnSelect.setEnabled(true);
358    }//GEN-LAST:event_profilesValueChanged
359
360    private void formMousePressed(MouseEvent evt) {//GEN-FIRST:event_formMousePressed
361        this.profilesValueChanged(null);
362    }//GEN-LAST:event_formMousePressed
363
364    private void profilesKeyPressed(KeyEvent evt) {//GEN-FIRST:event_profilesKeyPressed
365        if (evt.getKeyCode() == KeyEvent.VK_ENTER) {
366            this.btnSelect.doClick();
367        }
368    }//GEN-LAST:event_profilesKeyPressed
369
370    @SuppressFBWarnings(value = "DM_EXIT", justification = "This exit ensures launch is aborted if a profile is not selected or autostarted")
371    private void formWindowClosed(WindowEvent evt) {//GEN-FIRST:event_formWindowClosed
372        if (countDown != -1) {
373            // prevent an attempt to reclose this window from blocking application exit
374            countDown = -1;
375            // exit with an error code to indicate abnormal exit
376            System.exit(255);
377        }
378    }//GEN-LAST:event_formWindowClosed
379
380    // Variables declaration - do not modify//GEN-BEGIN:variables
381    private JButton btnCreate;
382    private JButton btnSelect;
383    private JButton btnUseExisting;
384    private JLabel countDownLbl;
385    private JScrollPane jScrollPane1;
386    private JLabel listLabel;
387    private JList<Profile> profiles;
388    // End of variables declaration//GEN-END:variables
389    private static final Logger log = LoggerFactory.getLogger(ProfileManagerDialog.class);
390}