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