001package jmri.jmrit.withrottle;
002
003import java.awt.Dimension;
004import java.awt.GridBagConstraints;
005import java.awt.GridBagLayout;
006import java.awt.event.ActionEvent;
007import java.awt.event.ActionListener;
008import java.net.InetAddress;
009import java.text.MessageFormat;
010import java.util.ArrayList;
011import java.util.Set;
012import javax.swing.AbstractAction;
013import javax.swing.Action;
014import javax.swing.ImageIcon;
015import javax.swing.JComboBox;
016import javax.swing.JLabel;
017import javax.swing.JMenu;
018import javax.swing.JMenuBar;
019import javax.swing.JMenuItem;
020import javax.swing.JPanel;
021import javax.swing.JScrollPane;
022import javax.swing.JTable;
023import javax.swing.JToolBar;
024import javax.swing.WindowConstants;
025import jmri.InstanceManager;
026import jmri.UserPreferencesManager;
027import jmri.jmrit.roster.rostergroup.RosterGroupSelector;
028import jmri.jmrit.roster.swing.RosterGroupComboBox;
029import jmri.jmrit.throttle.LargePowerManagerButton;
030import jmri.jmrit.throttle.StopAllButton;
031import jmri.util.FileUtil;
032import jmri.util.JmriJFrame;
033import jmri.util.prefs.JmriPreferencesActionFactory;
034import jmri.util.zeroconf.ZeroConfServiceManager;
035
036/**
037 * UserInterface.java Create a window for WiThrottle information and and create
038 * a FacelessServer thread to handle jmdns and device requests
039 *
040 * @author Brett Hoffman Copyright (C) 2009, 2010
041 * @author Randall Wood Copyright (C) 2013
042 * @author Paul Bender Copyright (C) 2018
043 */
044public class UserInterface extends JmriJFrame implements DeviceListener, RosterGroupSelector {
045
046    JMenuBar menuBar;
047    JMenuItem serverOnOff;
048    JPanel panel;
049    JLabel portLabel = new JLabel(Bundle.getMessage("LabelPending"));
050    JLabel manualPortLabel = new JLabel();
051    String manualPortLabelString = ""; //append IPv4 addresses as they respond to zeroconf
052    JLabel numConnected;
053    JScrollPane scrollTable;
054    JTable withrottlesList;
055    WiThrottlesListModel withrottlesListModel;
056    UserPreferencesManager userPreferences = InstanceManager.getDefault(UserPreferencesManager.class);
057    String rosterGroupSelectorPreferencesName = this.getClass().getName() + ".rosterGroupSelector";
058    RosterGroupComboBox rosterGroupSelector = new RosterGroupComboBox(userPreferences.getComboBoxLastSelection(rosterGroupSelectorPreferencesName));
059
060    //keep a reference to the actual server
061    private FacelessServer facelessServer;
062
063    // Server iVars
064    boolean isListen;
065    private final ArrayList<DeviceServer> deviceList = new ArrayList<>();
066
067    /**
068     * Save the last known size and the last known location since 4.15.4.
069     */
070    UserInterface() {
071        super(true, true);
072
073        isListen = true;
074        facelessServer = (FacelessServer) InstanceManager.getOptionalDefault(DeviceManager.class).orElseGet(() -> {
075            return InstanceManager.setDefault(DeviceManager.class, new FacelessServer());
076        });
077
078        // add ourselves as device listeners for any existing devices
079        for (DeviceServer ds : facelessServer.getDeviceList()) {
080            deviceList.add(ds);
081            ds.addDeviceListener(this);
082        }
083
084        facelessServer.addDeviceListener(this);
085
086        //update the server with the currently selected roster group
087        facelessServer.setSelectedRosterGroup(rosterGroupSelector.getSelectedItem());
088
089        //show all IPv4 addresses in window, for use by manual connections
090        addIPAddressesToUI();
091
092        createWindow();
093
094    } // End of constructor
095
096    private void addIPAddressesToUI() {
097        //get port# directly from prefs
098        int port = InstanceManager.getDefault(WiThrottlePreferences.class).getPort();
099        //list IPv4 addresses on the UI, for manual connections
100        //TODO: use some mechanism that is not tied to zeroconf networking
101        StringBuilder as = new StringBuilder(); //build multiline string of valid addresses
102        ZeroConfServiceManager manager = InstanceManager.getDefault(ZeroConfServiceManager.class);
103        Set<InetAddress> addresses = manager.getAddresses(ZeroConfServiceManager.Protocol.IPv4, false, false);
104        if (addresses.isEmpty()) {
105            // include IPv6 and link-local addresses if no non-link-local IPv4 addresses are available
106            addresses = manager.getAddresses(ZeroConfServiceManager.Protocol.All, true, false);
107        }
108        for (InetAddress ha : addresses) {
109            this.portLabel.setText(ha.getHostName());
110            as.append(ha.getHostAddress()).append(":").append(port).append("<br/>");
111        }
112        this.manualPortLabel.setText("<html>" + as + "</html>"); // NOI18N
113    }
114
115    protected void createWindow() {
116        panel = new JPanel();
117        panel.setLayout(new GridBagLayout());
118        GridBagConstraints con = new GridBagConstraints();
119        getContentPane().add(panel);
120        con.fill = GridBagConstraints.NONE;
121        con.weightx = 0.5;
122        con.weighty = 0;
123
124        JLabel label = new JLabel(MessageFormat.format(Bundle.getMessage("LabelListening"), new Object[]{DeviceServer.getWiTVersion()}));
125        con.gridx = 0;
126        con.gridy = 0;
127        con.gridwidth = 2;
128        panel.add(label, con);
129
130        con.gridx = 0;
131        con.gridy = 1;
132        con.gridwidth = 2;
133        panel.add(portLabel, con);
134
135        con.gridy = 2;
136        panel.add(manualPortLabel, con);
137
138        numConnected = new JLabel(Bundle.getMessage("LabelClients") + " " + deviceList.size());
139        con.weightx = 0;
140        con.gridx = 2;
141        con.gridy = 2;
142        con.ipadx = 5;
143        con.gridwidth = 1;
144        panel.add(numConnected, con);
145
146        JPanel rgsPanel = new JPanel();
147        rgsPanel.add(new JLabel(Bundle.getMessage("RosterGroupLabel")));
148        rgsPanel.add(rosterGroupSelector);
149        rgsPanel.setToolTipText(Bundle.getMessage("RosterGroupToolTip"));
150        JToolBar withrottleToolBar = new JToolBar();
151        withrottleToolBar.setFloatable(false);
152        withrottleToolBar.add(new StopAllButton());
153        withrottleToolBar.add(new LargePowerManagerButton());
154        withrottleToolBar.add(rgsPanel);
155        con.weightx = 0.5;
156        con.ipadx = 0;
157        con.gridx = 1;
158        con.gridy = 3;
159        con.gridwidth = 2;
160        panel.add(withrottleToolBar, con);
161
162        JLabel icon;
163        java.net.URL imageURL = FileUtil.findURL("resources/IconForWiThrottle.gif");
164
165        if (imageURL != null) {
166            ImageIcon image = new ImageIcon(imageURL);
167            icon = new JLabel(image);
168            con.weightx = 0.5;
169            con.gridx = 2;
170            con.gridy = 0;
171            con.ipady = 5;
172            con.gridheight = 2;
173            panel.add(icon, con);
174        }
175
176//  Add a list of connected devices and the address they are set to.
177        withrottlesListModel = new WiThrottlesListModel(deviceList);
178        withrottlesList = new JTable(withrottlesListModel);
179        withrottlesList.setPreferredScrollableViewportSize(new Dimension(300, 80));
180
181        withrottlesList.setRowHeight(20);
182        scrollTable = new JScrollPane(withrottlesList);
183
184        con.gridx = 0;
185        con.gridy = 4;
186        con.weighty = 1.0;
187        con.ipadx = 10;
188        con.ipady = 10;
189        con.gridheight = 3;
190        con.gridwidth = GridBagConstraints.REMAINDER;
191        con.fill = GridBagConstraints.BOTH;
192        panel.add(scrollTable, con);
193
194//  Create the menu to use with WiThrottle window. Has to be before pack() for Windows.
195        buildMenu();
196
197//  Set window size & location
198        this.setTitle("WiThrottle");
199        this.pack();
200
201        this.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
202
203        setVisible(true);
204        setMinimumSize(new Dimension(400, 250));
205        
206        rosterGroupSelector.addActionListener(new ActionListener() {
207
208            @SuppressWarnings("unchecked")
209            @Override
210            public void actionPerformed(ActionEvent e) {
211                String s = (String) ((JComboBox<String>) e.getSource()).getSelectedItem();
212                userPreferences.setComboBoxLastSelection(rosterGroupSelectorPreferencesName, s);
213                facelessServer.setSelectedRosterGroup(s);
214//              Send new selected roster group to all devices
215                for (DeviceServer device : deviceList) {
216                    device.sendPacketToDevice(device.sendRoster());
217                }
218            }
219        });
220    }
221
222    protected void buildMenu() {
223        this.setJMenuBar(new JMenuBar());
224
225        JMenu menu = new JMenu(Bundle.getMessage("MenuMenu"));
226        serverOnOff = new JMenuItem(Bundle.getMessage("MenuMenuStop"));
227        serverOnOff.addActionListener(new AbstractAction() {
228
229            @Override
230            public void actionPerformed(ActionEvent event) {
231                if (isListen) { // Stop server, remove addresses from UI
232                    disableServer();
233                    serverOnOff.setText(Bundle.getMessage("MenuMenuStart"));
234                    portLabel.setText(Bundle.getMessage("LabelNone"));
235                    manualPortLabel.setText(null);
236                } else { // Restart server
237                    enableServer();
238                    serverOnOff.setText(Bundle.getMessage("MenuMenuStop"));
239                    addIPAddressesToUI();
240                }
241            }
242        });
243
244        menu.add(serverOnOff);
245
246        menu.add(new ControllerFilterAction());
247
248        Action prefsAction = InstanceManager.getDefault(JmriPreferencesActionFactory.class).getCategorizedAction(
249                Bundle.getMessage("MenuMenuPrefs"),
250                "WITHROTTLE");
251
252        menu.add(prefsAction);
253
254        this.getJMenuBar().add(menu);
255
256        // add help menu
257        addHelpMenu("package.jmri.jmrit.withrottle.UserInterface", true);
258    }
259
260    @Override
261    public void notifyDeviceConnected(DeviceServer device) {
262
263        deviceList.add(device);
264        if (withrottlesListModel != null) {
265            withrottlesListModel.updateDeviceList(deviceList);
266        }
267        if (numConnected != null) {
268            numConnected.setText(Bundle.getMessage("LabelClients") + " " + deviceList.size());
269        }
270    }
271
272    @Override
273    public void notifyDeviceDisconnected(DeviceServer device) {
274        if (deviceList.size() < 1) {
275            return;
276        }
277        if (!deviceList.remove(device)) {
278            return;
279        }
280
281        if (numConnected != null) {
282            numConnected.setText(Bundle.getMessage("LabelClients") + " " + deviceList.size());
283        }
284        if (withrottlesListModel != null) {
285            withrottlesListModel.updateDeviceList(deviceList);
286        }
287        device.removeDeviceListener(this);
288    }
289
290    @Override
291    public void notifyDeviceAddressChanged(DeviceServer device) {
292        if (withrottlesListModel != null) {
293            withrottlesListModel.updateDeviceList(deviceList);
294        }
295    }
296
297    /**
298     * Received an UDID, update the device list
299     * @param device the device to update for
300     */
301    @Override
302    public void notifyDeviceInfoChanged(DeviceServer device) {
303        if (withrottlesListModel != null) {
304            withrottlesListModel.updateDeviceList(deviceList);
305        }
306    }
307
308    // this is package protected so tests can trigger easily.
309    void disableServer() {
310        facelessServer.disableServer();
311        isListen = false;
312    }
313
314    //tell the server thread to start listening again
315    private void enableServer() {
316        facelessServer.listen();
317        isListen = true;
318    }
319
320    @Override
321    public String getSelectedRosterGroup() {
322        return rosterGroupSelector.getSelectedRosterGroup();
323    }
324
325    // private final static Logger log = LoggerFactory.getLogger(UserInterface.class);
326}