001package jmri.jmrix.swing;
002
003import java.awt.BorderLayout;
004import java.awt.Color;
005import java.awt.Component;
006import java.awt.Dimension;
007import java.awt.FlowLayout;
008import java.awt.GridBagConstraints;
009import java.awt.GridBagLayout;
010import java.awt.Insets;
011import java.awt.event.ActionEvent;
012import java.beans.IndexedPropertyChangeEvent;
013import java.beans.PropertyChangeEvent;
014import java.text.MessageFormat;
015import java.util.ArrayList;
016import java.util.List;
017import java.util.ResourceBundle;
018
019import javax.annotation.CheckForNull;
020import javax.annotation.Nonnull;
021import javax.swing.Icon;
022import javax.swing.ImageIcon;
023import javax.swing.JButton;
024import javax.swing.JCheckBox;
025import javax.swing.JComboBox;
026import javax.swing.JComponent;
027import javax.swing.JLabel;
028import javax.swing.JPanel;
029import javax.swing.JTabbedPane;
030import javax.swing.event.ChangeEvent;
031import javax.swing.event.ChangeListener;
032
033import jmri.InstanceManager;
034import jmri.SystemConnectionMemo;
035import jmri.jmrix.ConnectionConfig;
036import jmri.jmrix.ConnectionConfigManager;
037import jmri.jmrix.ConnectionStatus;
038import jmri.jmrix.JmrixConfigPane;
039import jmri.profile.ProfileManager;
040import jmri.swing.ManagingPreferencesPanel;
041import jmri.swing.PreferencesPanel;
042import jmri.util.FileUtil;
043import jmri.util.startup.AbstractActionModel;
044import jmri.util.startup.StartupActionsManager;
045import jmri.util.swing.JmriJOptionPane;
046
047import org.openide.util.lookup.ServiceProvider;
048
049/**
050 *
051 * @author Randall Wood randall.h.wood@alexandriasoftware.com
052 */
053@ServiceProvider(service = PreferencesPanel.class)
054public class ConnectionsPreferencesPanel extends JTabbedPane implements ManagingPreferencesPanel {
055
056    private static final ResourceBundle rb = ResourceBundle.getBundle("apps.AppsConfigBundle"); // for some items // NOI18N
057
058
059    private final ImageIcon deleteIcon;
060    private final ImageIcon deleteIconRollOver;
061    private final Dimension deleteButtonSize;
062    private ImageIcon addIcon;
063    private boolean restartRequired = false;
064
065    private ArrayList<JmrixConfigPane> configPanes = new ArrayList<>();
066
067    public ConnectionsPreferencesPanel() {
068        super();
069        deleteIconRollOver = new ImageIcon(
070                FileUtil.findURL("program:resources/icons/misc/gui3/Delete16x16.png"));
071        deleteIcon = new ImageIcon(
072                FileUtil.findURL("program:resources/icons/misc/gui3/Delete-bw16x16.png"));
073        deleteButtonSize = new Dimension(
074                deleteIcon.getIconWidth() + 2,
075                deleteIcon.getIconHeight() + 2);
076        addIcon = new ImageIcon(
077                FileUtil.findURL("program:resources/icons/misc/gui3/Add16x16.png"));
078        ConnectionConfigManager ccm = InstanceManager.getDefault(ConnectionConfigManager.class);
079        ConnectionConfig[] connections = ccm.getConnections();
080        if (connections.length != 0) {
081            for (int i = 0; i < connections.length; i++) {
082                addConnection(i, JmrixConfigPane.createPanel(i));
083            }
084        } else {
085            addConnection(0, JmrixConfigPane.createNewPanel());
086        }
087        ccm.addPropertyChangeListener(ConnectionConfigManager.CONNECTIONS, (PropertyChangeEvent evt) -> {
088            int i = ((IndexedPropertyChangeEvent) evt).getIndex();
089            log.debug("PrefPanel ChangeListener of tab index i = {} of {} list", i+1, configPanes.size());
090            //if (evt.getNewValue() == null
091            //        && i < configPanes.size()
092            //        && evt.getOldValue().equals(configPanes.get(i).getCurrentObject())) {
093            //    //removeTab(null, i); // called same method removeTab again, conn tab was already removed by the user click
094            //} else
095            if ((evt.getOldValue() == null) && (evt.getNewValue() != null)) {
096                for (JmrixConfigPane pane : this.configPanes) {
097                    if (pane.getCurrentObject() == null ) {
098                        log.error("did not expect pane.getCurrentObject()==null here for {} {} {}", i, evt.getNewValue(), configPanes);
099                    } else if (pane.getCurrentObject().equals(evt.getNewValue())) {
100                        return; // don't add the connection again
101                    }
102                }
103                addConnection(i, JmrixConfigPane.createPanel(i));
104            }
105        });
106        this.addChangeListener(addTabListener);
107        newConnectionTab();
108        this.setSelectedIndex(0);
109    }
110
111    transient ChangeListener addTabListener = (ChangeEvent evt) -> {
112        // This method is called whenever the selected tab changes
113        JTabbedPane pane = (JTabbedPane) evt.getSource();
114        int sel = pane.getSelectedIndex();
115        if (sel == -1) {
116            addConnectionTab();
117            return;
118        } else {
119            Icon icon = pane.getIconAt(sel);
120            if (icon == addIcon) {
121                addConnectionTab();
122                return;
123            }
124        }
125        activeTab();
126    };
127
128    private void activeTab() {
129        for (int i = 0; i < this.getTabCount() - 1; i++) {
130            JPanel panel = (JPanel) this.getTabComponentAt(i);
131            if (panel != null) {
132                panel.invalidate();
133                Component[] comp = panel.getComponents();
134                for (Component c : comp) {
135                    if (c instanceof JButton) {
136                        if (i == this.getSelectedIndex()) {
137                            c.setVisible(true);
138                        } else {
139                            c.setVisible(false);
140                        }
141                    }
142                }
143            }
144        }
145    }
146
147    private void addConnection(int tabPosition, final JmrixConfigPane configPane) {
148        JPanel p = new JPanel();
149        p.setLayout(new BorderLayout());
150        p.add(configPane, BorderLayout.CENTER);
151
152        JButton tabCloseButton = new JButton(deleteIcon);
153        tabCloseButton.setPreferredSize(deleteButtonSize);
154        tabCloseButton.setBorderPainted(false);
155        tabCloseButton.setRolloverIcon(deleteIconRollOver);
156        tabCloseButton.setVisible(false);
157
158        JPanel c = new JPanel();
159        c.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
160        final JCheckBox disable = new JCheckBox(rb.getString("ButtonDisableConnection"));
161        disable.setSelected(configPane.getDisabled());
162        disable.addActionListener((ActionEvent e) -> {
163            configPane.setDisabled(disable.isSelected());
164        });
165        c.add(disable);
166        p.add(c, BorderLayout.SOUTH);
167        String title;
168
169        if (configPane.getConnectionName() != null) {
170            title = configPane.getConnectionName();
171        } else if ((configPane.getCurrentProtocolName() != null)
172                && (!configPane.getCurrentProtocolName().equals(
173                        JmrixConfigPane.NONE))) {
174            title = configPane.getCurrentProtocolName();
175        } else {
176            title = rb.getString("TabbedLayoutConnection") + (tabPosition + 1);
177            if (this.indexOfTab(title) != -1) {
178                for (int x = 2; x < 12; x++) {
179                    title = rb.getString("TabbedLayoutConnection")
180                            + (tabPosition + 2);
181                    if (this.indexOfTab(title) != -1) {
182                        break;
183                    }
184                }
185            }
186        }
187
188        final JPanel tabTitle = new JPanel(new BorderLayout(5, 0));
189        tabTitle.setOpaque(false);
190        p.setName(title);
191
192        if (configPane.getDisabled()) {
193            title = "(" + title + ")";
194        }
195
196        JLabel tabLabel = new JLabel(title, JLabel.LEFT);
197        tabTitle.add(tabLabel, BorderLayout.WEST);
198        tabTitle.add(tabCloseButton, BorderLayout.EAST);
199
200        this.configPanes.add(configPane);
201        this.add(p);
202        this.setTabComponentAt(tabPosition, tabTitle);
203
204        tabCloseButton.addActionListener((ActionEvent e) -> { // initial user Delete conn click
205            removeTab(e, this.indexOfTabComponent(tabTitle));
206        });
207
208        this.setToolTipTextAt(tabPosition, title);
209
210        if (configPane.getCurrentObject() != null) {
211            if (ConnectionStatus.instance().isConnectionOk(
212                    configPane.getCurrentObject().getAdapter().getSystemConnectionMemo())) {
213                tabLabel.setForeground(Color.black);
214            } else {
215                tabLabel.setForeground(Color.red);
216            }
217        }
218        if (configPane.getDisabled()) {
219            tabLabel.setForeground(Color.ORANGE);
220        }
221
222    }
223
224    void addConnectionTab() {
225        this.removeTabAt(this.indexOfTab(addIcon));
226        addConnection(configPanes.size(), JmrixConfigPane.createNewPanel());
227        newConnectionTab();
228    }
229
230    private void newConnectionTab() {
231        this.addTab(null, addIcon, null, rb.getString("ToolTipAddNewConnection"));
232        this.setSelectedIndex(this.getTabCount() - 2);
233    }
234
235    private void removeTab(ActionEvent e, int x) {
236        int i;
237        i = x; // copy x for handling
238
239        if (i != -1) {
240            // only prompt if triggered by action event
241            if (e != null && !confirmDelete(getSystemPrefix(this.configPanes.get(i)), this.getTitleAt(i))) {
242                return;
243            }
244
245            log.debug("i = {}, this.getTabCount() = {}, configPanes.size()={}", i, this.getTabCount(), configPanes.size());
246            JmrixConfigPane configPane = this.configPanes.get(i);
247            this.removeChangeListener(addTabListener);
248            this.remove(i);
249            log.debug("start of connection #{} disposal", i);
250            try {
251                JmrixConfigPane.dispose(configPane);
252            } catch (NullPointerException ex) {
253                log.error("Caught Null Pointer Exception while removing connection tab {}", i);
254            }
255            log.debug("connection tab #{} successfully disposed. configPanes.size() = {}", i+1, configPanes.size());
256            this.configPanes.remove(i); // on delete of connection a loop (listener) called this method 2x
257            restartRequired = true;
258            if (this.getTabCount() == 1) { // normally not required, stays in place while adding or deleting
259                addConnectionTab();
260            }
261            if (x != 0) {
262                this.setSelectedIndex(x - 1);
263            } else {
264                this.setSelectedIndex(0);
265            }
266            this.addChangeListener(addTabListener);
267        }
268        activeTab();
269    }
270
271    /**
272     * Get the system prefix of the connection in a pane.
273     *
274     * @param configPane the connection pane
275     * @return the system prefix, or null if the pane has no connection
276     */
277    @CheckForNull
278    static String getSystemPrefix(@CheckForNull JmrixConfigPane configPane) {
279        if (configPane == null) {
280            return null;
281        }
282        ConnectionConfig config = configPane.getCurrentObject();
283        if (config == null || config.getAdapter() == null
284                || config.getAdapter().getSystemConnectionMemo() == null) {
285            return null;
286        }
287        return config.getAdapter().getSystemConnectionMemo().getSystemPrefix();
288    }
289
290    /**
291     * Ask the user to confirm deleting a connection.
292     * <p>
293     * If the connection has Start Up actions, one dialog both confirms the
294     * delete and asks what to do with each action: remove it (the default),
295     * keep it, or move it to another connection that offers the same action
296     * and does not already have it. Otherwise the plain Yes/No question is
297     * asked. Nothing is changed unless the user confirms.
298     *
299     * @param prefix the system prefix of the connection being deleted; may be
300     *               null if it has no connection
301     * @param title  the name of the connection, for the dialog
302     * @return true to carry on deleting the connection, false if cancelled
303     */
304    boolean confirmDelete(@CheckForNull String prefix, String title) {
305        String question = MessageFormat.format(rb.getString("MessageDoDelete"), title);
306        StartupActionsManager manager = InstanceManager.getOptionalDefault(StartupActionsManager.class).orElse(null);
307        List<AbstractActionModel> models = (prefix == null || manager == null)
308                ? new ArrayList<>() : manager.getActionsForConnection(prefix);
309        if (models.isEmpty()) {
310            return JmriJOptionPane.showConfirmDialog(this, question,
311                    rb.getString("MessageDeleteConnection"),
312                    JmriJOptionPane.YES_NO_OPTION) == JmriJOptionPane.YES_OPTION;
313        }
314
315        JPanel panel = new JPanel(new GridBagLayout());
316        GridBagConstraints c = new GridBagConstraints();
317        c.gridx = 0;
318        c.gridy = 0;
319        c.gridwidth = 2;
320        c.anchor = GridBagConstraints.WEST;
321        c.insets = new Insets(2, 2, 2, 2);
322        panel.add(new JLabel(question), c);
323        c.gridy++;
324        c.insets = new Insets(8, 2, 6, 2);
325        panel.add(new JLabel(rb.getString("MessageStartupActionsForConnection")), c);
326        c.gridwidth = 1;
327        c.insets = new Insets(2, 2, 2, 8);
328        List<JComboBox<StartupActionChoice>> boxes = new ArrayList<>();
329        for (AbstractActionModel model : models) {
330            JComboBox<StartupActionChoice> box = new JComboBox<>();
331            for (StartupActionChoice choice : getChoices(manager, model, prefix)) {
332                box.addItem(choice);
333            }
334            boxes.add(box);
335            c.gridy++;
336            c.gridx = 0;
337            panel.add(new JLabel(model.getName()), c);
338            c.gridx = 1;
339            panel.add(box, c);
340        }
341
342        String[] options = {rb.getString("ButtonDeleteConnection"), Bundle.getMessage("ButtonCancel")};
343        int n = JmriJOptionPane.showOptionDialog(this, panel, rb.getString("MessageDeleteConnection"),
344                JmriJOptionPane.DEFAULT_OPTION, JmriJOptionPane.QUESTION_MESSAGE, null, options, options[1]);
345        if (n != 0) {
346            return false;
347        }
348        for (int k = 0; k < models.size(); k++) {
349            applyStartupActionChoice(manager, models.get(k), (StartupActionChoice) boxes.get(k).getSelectedItem());
350        }
351        return true;
352    }
353
354    /**
355     * Get the choices for a Start Up action of a connection being deleted:
356     * remove (first, the default), keep, and a move to each other running
357     * connection that offers the action and does not already have it.
358     *
359     * @param manager the Start Up actions manager
360     * @param model   the action
361     * @param prefix  the system prefix of the connection being deleted
362     * @return the choices, in the order shown
363     */
364    @Nonnull
365    static List<StartupActionChoice> getChoices(@Nonnull StartupActionsManager manager,
366            @Nonnull AbstractActionModel model, @CheckForNull String prefix) {
367        List<StartupActionChoice> choices = new ArrayList<>();
368        choices.add(new StartupActionChoice(StartupActionChoice.Kind.REMOVE, null,
369                rb.getString("StartupActionRemove")));
370        choices.add(new StartupActionChoice(StartupActionChoice.Kind.KEEP, null,
371                rb.getString("StartupActionKeep")));
372        for (SystemConnectionMemo memo : StartupActionsManager.getConnectionsOffering(model, prefix)) {
373            if (!hasAction(manager, model.getClassName(), memo.getSystemPrefix())) {
374                choices.add(new StartupActionChoice(StartupActionChoice.Kind.MOVE, memo.getSystemPrefix(),
375                        MessageFormat.format(rb.getString("StartupActionMoveTo"),
376                                memo.getUserName(), memo.getSystemPrefix())));
377            }
378        }
379        return choices;
380    }
381
382    private static boolean hasAction(StartupActionsManager manager, String className, String systemPrefix) {
383        return manager.getActionsForConnection(systemPrefix).stream()
384                .anyMatch(other -> className.equals(other.getClassName()));
385    }
386
387    /**
388     * Carry out the choice made for a Start Up action of a connection being
389     * deleted.
390     *
391     * @param manager the Start Up actions manager
392     * @param model   the action
393     * @param choice  what to do; null is treated as remove
394     */
395    static void applyStartupActionChoice(@Nonnull StartupActionsManager manager,
396            @Nonnull AbstractActionModel model, @CheckForNull StartupActionChoice choice) {
397        if (choice == null || choice.kind == StartupActionChoice.Kind.REMOVE) {
398            manager.removeAction(model);
399        } else if (choice.kind == StartupActionChoice.Kind.MOVE && choice.systemPrefix != null) {
400            manager.reassignAction(model, choice.systemPrefix);
401        }
402        // KEEP: left as it is; shown in red until moved or removed
403    }
404
405    /**
406     * One choice for a Start Up action of a connection being deleted.
407     */
408    static class StartupActionChoice {
409
410        enum Kind {
411            REMOVE, KEEP, MOVE
412        }
413
414        final Kind kind;
415        final String systemPrefix; // the connection to move to, for MOVE only
416        private final String text;
417
418        StartupActionChoice(@Nonnull Kind kind, @CheckForNull String systemPrefix, @Nonnull String text) {
419            this.kind = kind;
420            this.systemPrefix = systemPrefix;
421            this.text = text;
422        }
423
424        @Override
425        public String toString() {
426            return text;
427        }
428    }
429
430    @Override
431    public String getPreferencesItem() {
432        return "CONNECTIONS"; // NOI18N
433    }
434
435    @Override
436    public String getPreferencesItemText() {
437        return rb.getString("MenuConnections"); // NOI18N
438    }
439
440    @Override
441    public String getTabbedPreferencesTitle() {
442        return null;
443    }
444
445    @Override
446    public String getLabelKey() {
447        return null;
448    }
449
450    @Override
451    public JComponent getPreferencesComponent() {
452        return this;
453    }
454
455    @Override
456    public boolean isPersistant() {
457        return false;
458    }
459
460    @Override
461    public String getPreferencesTooltip() {
462        return null;
463    }
464
465    @Override
466    public void savePreferences() {
467        InstanceManager.getDefault(ConnectionConfigManager.class).savePreferences(ProfileManager.getDefault().getActiveProfile());
468    }
469
470    @Override
471    public boolean isDirty() {
472        return this.configPanes.stream().anyMatch(JmrixConfigPane::isDirty);
473    }
474
475    @Override
476    public boolean isRestartRequired() {
477        return this.restartRequired
478                || this.configPanes.stream().anyMatch(JmrixConfigPane::isRestartRequired);
479    }
480
481    @Override
482    public boolean isPreferencesValid() {
483        return this.configPanes.stream().allMatch(JmrixConfigPane::isPreferencesValid);
484    }
485
486    @Override
487    public List<PreferencesPanel> getPreferencesPanels() {
488        return new ArrayList<>(this.configPanes);
489    }
490
491    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ConnectionsPreferencesPanel.class);
492
493}