001package jmri.jmrix.loconet.swing;
002
003import javax.swing.Action;
004import javax.swing.JMenu;
005import javax.swing.JSeparator;
006
007import java.util.ArrayList;
008
009import jmri.jmrix.loconet.locomon.LocoMonPane;
010import jmri.jmrix.loconet.slotmon.SlotMonPane;
011import jmri.jmrix.loconet.clockmon.ClockMonPane;
012import jmri.jmrix.loconet.locostats.swing.LocoStatsPanel;
013import jmri.jmrix.loconet.bdl16.BDL16Panel;
014import jmri.jmrix.loconet.pm4.PM4Panel;
015import jmri.jmrix.loconet.se8.SE8Panel;
016import jmri.jmrix.loconet.ds64.Ds64TabbedPanel;
017import jmri.jmrix.loconet.cmdstnconfig.CmdStnConfigPane;
018import jmri.jmrix.loconet.locoid.LocoIdPanel;
019import jmri.jmrix.loconet.duplexgroup.swing.DuplexGroupTabbedPanel;
020import jmri.jmrix.loconet.swing.throttlemsg.MessagePanel;
021import jmri.jmrix.loconet.locogen.LocoGenPanel;
022import jmri.jmrix.loconet.swing.lncvprog.LncvProgPane;
023import jmri.jmrix.loconet.swing.lnsv1prog.Lnsv1ProgPane;
024import jmri.jmrix.loconet.pr3.swing.Pr3SelectPane;
025import jmri.jmrix.loconet.soundloader.LoaderPane;
026import jmri.jmrix.loconet.soundloader.EditorPane;
027import jmri.jmrix.loconet.loconetovertcp.LnTcpServerAction;
028import jmri.jmrix.loconet.LocoNetSystemConnectionMemo;
029import jmri.jmrix.loconet.LnCommandStationType;
030import jmri.jmrix.loconet.swing.menuitemspi.MenuItemsService;
031
032import jmri.util.swing.WindowInterface;
033import jmri.util.swing.sdi.JmriJFrameInterface;
034
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038
039/**
040 * Create a "Systems" menu containing the Jmri LocoNet-specific tools.
041 *
042 * @author Bob Jacobsen Copyright 2003, 2010
043 * @author B. Milhaupt Copyright 2021, 2022
044 */
045public class LocoNetMenu extends JMenu {
046    private boolean lastWasSeparator;
047
048
049
050    /**
051     * Create a LocoNet menu.
052     * <br>
053     * Adds menu items for JMRI code's LocoNet menu items, as defined in
054     * the initialization of <code>panelItems</code> here, and appends those
055     * menu items from SPI extensions which implement
056     * {@link jmri.jmrix.loconet.swing.menuitemspi.spi.MenuItemsInterface}
057     * to report additional menu items for inclusion on the LocoNet menu.
058     * <br>
059     * This method pre-loads the TrafficController to certain actions.
060     * <br>
061     * Actions will open new windows.
062     *<br>
063     * @param memo      {@link jmri.jmrix.loconet.LocoNetSystemConnectionMemo} to
064     *                  be used by this object
065     * @see jmri.jmrix.loconet.swing.menuitemspi.spi.MenuItemsInterface
066     * @see jmri.jmrix.loconet.swing.menuitemspi.MenuItemsService
067     */
068    public LocoNetMenu(LocoNetSystemConnectionMemo memo) {
069        super();
070        ArrayList<LocoNetMenuItem> panelItems;
071        panelItems = new ArrayList<>();
072
073        // Define the common allExtensionItems in the LocoNet menu.  Note that
074        // LnMessageServer and LnTcpServer are special-cased because they have no
075        // GUI interface and are handled slightly differently by processItems().
076
077        panelItems.add(new LocoNetMenuItem("MenuItemLocoNetMonitor", LocoMonPane.class, false, true)); // NOI18N
078        panelItems.add(new LocoNetMenuItem("MenuItemSlotMonitor", SlotMonPane.class, false, true)); // NOI18N
079        panelItems.add(new LocoNetMenuItem("MenuItemClockMon", ClockMonPane.class, true, true)); // NOI18N
080        panelItems.add(new LocoNetMenuItem("MenuItemLocoStats", LocoStatsPanel.class, false, true)); // NOI18N
081        panelItems.add(null); // direct Ln CS/hardware tools
082        panelItems.add(new LocoNetMenuItem("MenuItemBDL16Programmer", BDL16Panel.class, true, true)); // NOI18N
083        panelItems.add(new LocoNetMenuItem("MenuItemPM4Programmer", PM4Panel.class, true, true)); // NOI18N
084        panelItems.add(new LocoNetMenuItem("MenuItemSE8cProgrammer", SE8Panel.class, true, true)); // NOI18N
085        panelItems.add(new LocoNetMenuItem("MenuItemDS64Programmer", Ds64TabbedPanel.class, true, true)); // NOI18N
086        panelItems.add(new LocoNetMenuItem("MenuItemCmdStnConfig", CmdStnConfigPane.class,true, true)); // NOI18N
087        panelItems.add(new LocoNetMenuItem("MenuItemSetID", LocoIdPanel.class, true, true)); // NOI18N
088        panelItems.add(new LocoNetMenuItem("MenuItemDuplex", DuplexGroupTabbedPanel.class, true, true)); // NOI18N
089        panelItems.add(null); // listing panes for Roster programming
090        panelItems.add(new LocoNetMenuItem("MenuItemLnsv1Prog", Lnsv1ProgPane.class, true, true)); // NOI18N
091        // Lnsv2?
092        panelItems.add(new LocoNetMenuItem("MenuItemLncvProg", LncvProgPane.class, true, true)); // NOI18N
093        panelItems.add(null); // message/packet tools
094        panelItems.add(new LocoNetMenuItem("MenuItemThrottleMessages", MessagePanel.class, true, true)); // NOI18N
095        panelItems.add(new LocoNetMenuItem("MenuItemSendPacket", LocoGenPanel.class, false, true)); // NOI18N
096        panelItems.add(new LocoNetMenuItem("MenuItemPr3ModeSelect", Pr3SelectPane.class, false, true)); // NOI18N
097        panelItems.add(null); // upload/download tools
098        panelItems.add(new LocoNetMenuItem("MenuItemDownload", jmri.jmrix.loconet.downloader.LoaderPane.class, false, true)); // NOI18N
099        panelItems.add(new LocoNetMenuItem("MenuItemSoundload", LoaderPane.class, false, true)); // NOI18N
100        panelItems.add(new LocoNetMenuItem("MenuItemSoundEditor", EditorPane.class, false, true)); // NOI18N
101        panelItems.add(null); // servers
102        panelItems.add(new LocoNetMenuItem("MenuItemLocoNetOverTCPServer", LnTcpServerAction.class, false, false));
103
104        LnCommandStationType cmdStation = null;
105        if (memo != null) {
106            setText(memo.getUserName());
107            cmdStation = memo.getSlotManager().getCommandStationType();
108        } else {
109            setText(Bundle.getMessage("MenuLocoNet"));
110        }
111
112        WindowInterface wi = new JmriJFrameInterface();
113
114        boolean isLocoNetInterface;
115        isLocoNetInterface = (cmdStation == null) ||
116                (!cmdStation.equals(LnCommandStationType.COMMAND_STATION_PR2_ALONE) &&
117                !cmdStation.equals(LnCommandStationType.COMMAND_STATION_PR3_ALONE) &&
118                !cmdStation.equals(LnCommandStationType.COMMAND_STATION_PR4_ALONE) &&
119                !cmdStation.equals(LnCommandStationType.COMMAND_STATION_USB_DCS240_ALONE) &&
120                !cmdStation.equals(LnCommandStationType.COMMAND_STATION_USB_DCS52_ALONE));
121
122        JMenu hostMenu = this;
123        lastWasSeparator = true;  // to prevent an initial separator in menu
124        processItems(hostMenu, panelItems, isLocoNetInterface, wi, memo);
125
126        lastWasSeparator = false;
127        add(hostMenu);
128        panelItems.clear();
129        // Deal with menu item tasks from SPI extensions
130        ArrayList<JMenu> extensionMenus  = getExtensionMenuItems(isLocoNetInterface,
131             wi,  memo);
132        log.trace("number of extension items is {}.", panelItems.size());
133        while ((!extensionMenus.isEmpty()) && (extensionMenus.get(extensionMenus.size()-1) == null)) {
134            // remove any dangling separators at end of list of menu allExtensionItems
135            extensionMenus.remove(panelItems.size()-1);
136        }
137        if (!extensionMenus.isEmpty()) {
138            add(new JSeparator());  // ensure placement of a horizontal bar above
139                                    // extension menu
140            log.debug("number of items {}", panelItems.size());
141            while (!extensionMenus.isEmpty()) {
142                JMenu menu = extensionMenus.get(0);
143                add(menu);
144                log.trace("Added extension menu {}", menu.getName());
145                extensionMenus.remove(0);
146            }
147        }
148    }
149
150    /**
151     * Create an Action suitable for inclusion as a menu item on a LocoNet menu.
152     *
153     * @param item a LocoetMenuItem object which defines the menu item's
154     *      characteristics, and which will be the basis for the returned Action
155     *      object.
156     * @param isLocoNetInterface is true if the LocoNet connection has a physical
157     *      interface to LocoNet, else false.
158     * @param wi the WindowInterface associated with the JMRI instance and LocoNetMenu.
159     * @param memo the LocoNetSystemConnectionMemo associated with the LocoNet
160     *          connection.
161     * @return an Action which may be added to a local JMenu for inclusion in a
162     * LocoNet connection's menu; the action's object may make use of the LocoNet
163     * memo and associate its GUI objects with the JMRI WindowInterface.  If the
164     * item requires a physical LocoNet interface but the connection does not have
165     * such an interface, then null is returned.
166     */
167    public Action processExternalItem(LocoNetMenuItem item, boolean isLocoNetInterface,
168            WindowInterface wi, LocoNetSystemConnectionMemo memo) {
169        if (item == null) {
170            return null;
171        }
172        if (item.hasGui()  ) {
173            if (isLocoNetInterface || (!item.isInterfaceOnly())) {
174                log.trace("created GUI menu item {}.", item.getName());
175                return createGuiAction(item, wi, memo);
176            } else {
177                log.trace("not displaying item {} ({}) account requires "
178                        + "interface which is not present in current "
179                        + "configuration.",
180                        item.getName(), item.getClassToLoad().getCanonicalName()
181                        );
182                return null;
183            }
184        } else {
185            log.trace("created non-GUI menu item {}.", item.getName());
186            return createNonGuiAction(item);
187        }
188    }
189
190    /**
191     * Create an Action object from a LocoNetMenuItem, linked to the appropriate
192     * WindowInterface, for use as a menu item on a LocoNet menu.
193     *
194     * Depending on whether the item needs a gui and/or a physical LocoNet
195     * interface, this method returns null or an Action which is suitable for
196     * use as a menu item on a LocoNet menu.
197     *<br>
198     * If the item's name is found as a key the Bundle associated with this object,
199     * then the I18N'd string will be used as the Action's text.
200     *
201     * @param item LocoNetMenuItem which defines the menu item's requirements.
202     * @param wi WindowInterface to which the item's GUI object will be linked.
203     * @param memo LocoNetSystemConnectionMemo with which the item will be linked.
204     * @return null if the item's requirements are not met by the current
205     *      connection, or an Action which may be used as a JMenuItem.
206     */
207    public Action createGuiAction(LocoNetMenuItem item, WindowInterface wi,
208            LocoNetSystemConnectionMemo memo) {
209        String translatedMenuItemName;
210        try {
211            translatedMenuItemName = Bundle.getMessage(item.getName());
212        } catch (java.util.MissingResourceException e) {
213            // skip internationalization if name is not present as a "key"
214            translatedMenuItemName = item.getName();
215        }
216
217        return new LnNamedPaneAction(translatedMenuItemName, wi,
218                item.getClassToLoad().getCanonicalName(), memo);
219    }
220
221    /**
222     * Create an Action object from a LocoNetMenuItem, for use as a menu item on
223     * a LocoNet menu, without linkage to the WindowInterface associated with the
224     * LocoNet menu.
225     *
226     * This method returns an Action which is suitable for use as a menu item on
227     * a LocoNet menu.
228     *<br>
229     * If the item's name is found as a key the Bundle associated with this object,
230     * then the I18N'd string will be used as the Action's text.
231     *
232     * @param item LocoNetMenuItem which defines the menu item's requirements.
233     * @return an Action which may be used as a JMenuItem.
234     */
235    public Action createNonGuiAction(LocoNetMenuItem item) {
236        Action menuItem = null;
237        try {
238            menuItem = (Action) item.getClassToLoad()
239                            .getDeclaredConstructor().newInstance();
240            menuItem.putValue("NAME", item.getName()); // set the menu item name // NOI18N
241        } catch (IllegalAccessException | InstantiationException | NoSuchMethodException | java.lang.reflect.InvocationTargetException ex ) {
242            log.warn("could not load menu item {} ({})",
243                    item.getName(), item.getClassToLoad().getCanonicalName(), ex);
244        }
245        return menuItem;
246    }
247
248    /**
249     * Get an ArrayList of JMenu objects as provided via the SPI "extension"
250     * mechanisms.
251     * @param isConnectionWithHardwareInterface informs whether the connection
252     *      has actual hardware
253     * @param wi allows the extension menu items to be associated with the
254     *      JAVA WindowInterface which relates to the connection's menu
255     * @param memo the LocoNetSystemConnectionMemo associated with the menu to
256     *      which the extension's MenuItem(s) are to be attached.
257     * @return an ArrayList of JMenu objects, as populated from the menu items
258     *      reported by any available SPI extensions.  May be an empty ArrayList
259     *      if none of the SPI extensions provide menu items for this menu.
260     * <br>
261     * @see jmri.jmrix.loconet.swing.menuitemspi.spi.MenuItemsInterface
262     * @see jmri.jmrix.loconet.swing.menuitemspi.MenuItemsService
263     */
264    public final java.util.ArrayList<JMenu> getExtensionMenuItems(
265            boolean isConnectionWithHardwareInterface, WindowInterface wi,
266            LocoNetSystemConnectionMemo memo) {
267        ArrayList<JMenu> locoNetMenuItems = new ArrayList<>();
268        log.trace("searching for extensions for the canonical name {}",
269                this.getClass().getCanonicalName());
270        MenuItemsService lnMenuItemServiceInstance;
271        lnMenuItemServiceInstance = MenuItemsService.getInstance();
272        locoNetMenuItems.addAll(
273                lnMenuItemServiceInstance.getMenuExtensionsItems(
274                        isConnectionWithHardwareInterface, wi, memo));
275        log.trace("LocoNetItems size is {}", locoNetMenuItems.size());
276        return locoNetMenuItems;
277    }
278
279    private void processItems(JMenu menu, ArrayList<LocoNetMenuItem> items, boolean isLocoNetInterface,
280            WindowInterface wi, LocoNetSystemConnectionMemo memo) {
281        items.forEach(item -> {
282            processAnItem(menu, item, isLocoNetInterface, wi, memo);
283        });
284    }
285
286    private void processAnItem(JMenu menu, LocoNetMenuItem item, boolean isLocoNetInterface,
287            WindowInterface wi, LocoNetSystemConnectionMemo memo) {
288        if (item == null) {
289            if (!lastWasSeparator) {
290                menu.add(new JSeparator());
291                log.trace("Added new JSeparator");
292
293                lastWasSeparator = true;
294            }
295        } else if (item.hasGui()  ) {
296            if (isLocoNetInterface || (!item.isInterfaceOnly())) {
297                addGuiItem(menu, item, wi, memo);
298                log.trace("added GUI item {}", item.getName());
299            } else {
300                log.trace("not displaying item {} ({}) account requires "
301                        + "interface which is not present in current "
302                        + "configuration.",
303                        item.getName(), item.getClassToLoad().getCanonicalName());
304            }
305        } else {
306            addNonGuiItem(menu, item);
307                log.trace("added non-GUI item {}", item.getName());
308        }
309    }
310
311    private void addNonGuiItem(JMenu menu, LocoNetMenuItem item) {
312        if (item != null) {
313            Action menuItem = createNonGuiAction(item);
314            menu.add(menuItem);
315            log.debug("Adding (non-gui) item {} ({})",
316                    item.getName(), item.getClassToLoad());
317            lastWasSeparator = false;
318        } else {
319            menu.add(new JSeparator());
320            lastWasSeparator = true;
321            log.trace("adding a JSeparator account null item");
322        }
323    }
324    private void addGuiItem(JMenu menu, LocoNetMenuItem item, WindowInterface wi,
325            LocoNetSystemConnectionMemo memo) {
326        Action a = createGuiAction(item, wi, memo);
327        menu.add(a);
328        lastWasSeparator = false;
329        log.debug("Added new GUI-based item for {} ({}).",
330                item.getName(), item.getClassToLoad().getCanonicalName());
331    }
332
333    private static final Logger log = LoggerFactory.getLogger(LocoNetMenu.class);
334
335}