001package jmri.jmrit.vsdecoder.swing; 002 003import java.awt.Dimension; 004import java.awt.event.ActionEvent; 005import java.awt.event.ActionListener; 006import java.awt.event.KeyEvent; 007import java.beans.PropertyChangeEvent; 008import java.beans.PropertyChangeListener; 009import java.util.ArrayList; 010import java.util.HashMap; 011import java.util.List; 012import java.util.Map; 013 014import javax.swing.BoxLayout; 015import javax.swing.JButton; 016import javax.swing.JLabel; 017import javax.swing.JMenu; 018import javax.swing.JMenuBar; 019import javax.swing.JPanel; 020import javax.swing.JSlider; 021import javax.swing.JToggleButton; 022import javax.swing.event.ChangeEvent; 023import javax.swing.event.ChangeListener; 024 025import jmri.Sensor; 026import jmri.jmrit.roster.Roster; 027import jmri.jmrit.roster.RosterEntry; 028import jmri.jmrit.vsdecoder.LoadVSDFileAction; 029import jmri.jmrit.vsdecoder.SoundEvent; 030import jmri.jmrit.vsdecoder.VSDConfig; 031import jmri.jmrit.vsdecoder.VSDecoder; 032import jmri.jmrit.vsdecoder.VSDecoderManager; 033import jmri.util.JmriJFrame; 034import jmri.util.swing.JmriJOptionPane; 035 036/** 037 * Main frame for the GUI VSDecoder Manager. 038 * 039 * <hr> 040 * This file is part of JMRI. 041 * <p> 042 * JMRI is free software; you can redistribute it and/or modify it under 043 * the terms of version 2 of the GNU General Public License as published 044 * by the Free Software Foundation. See the "COPYING" file for a copy 045 * of this license. 046 * <p> 047 * JMRI is distributed in the hope that it will be useful, but WITHOUT 048 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 049 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 050 * for more details. 051 * 052 * @author Mark Underwood Copyright (C) 2011 053 * @author Klaus Killinger Copyright (C) 2024 054 */ 055public class VSDManagerFrame extends JmriJFrame { 056 057 public static final String MUTE = "VSDMF:Mute"; // NOI18N 058 public static final String VOLUME_CHANGE = "VSDMF:VolumeChange"; // NOI18N 059 public static final String REMOVE_DECODER = "VSDMF:RemoveDecoder"; // NOI18N 060 public static final String CLOSE_WINDOW = "VSDMF:CloseWindow"; // NOI18N 061 062 // Map of Mnemonic KeyEvent values to GUI Components 063 private static final Map<String, Integer> Mnemonics = new HashMap<>(); 064 065 static { 066 // Menu 067 Mnemonics.put("FileMenu", KeyEvent.VK_F); 068 Mnemonics.put("EditMenu", KeyEvent.VK_E); 069 // Other GUI 070 Mnemonics.put("MuteButton", KeyEvent.VK_M); 071 Mnemonics.put("AddButton", KeyEvent.VK_A); 072 } 073 074 private int master_volume; 075 076 JPanel decoderPane; 077 JPanel volumePane; 078 JPanel decoderBlank; 079 080 private VSDConfig config; 081 private VSDConfigDialog cd; 082 private List<JMenu> menuList; 083 private boolean is_auto_loading; 084 private boolean is_block_using; 085 private boolean is_viewing; 086 private List<VSDecoder> vsdlist; 087 088 /** 089 * Constructor 090 */ 091 public VSDManagerFrame() { 092 super(true, true); 093 this.addPropertyChangeListener(VSDecoderManager.instance()); 094 is_auto_loading = VSDecoderManager.instance().getVSDecoderPreferences().isAutoLoadingVSDFile(); 095 is_block_using = VSDecoderManager.instance().getVSDecoderPreferences().getUseBlocksSetting(); 096 is_viewing = VSDecoderManager.instance().getVSDecoderList().isEmpty() ? false : true; 097 initGUI(); 098 } 099 100 @Override 101 public void initComponents() { 102 //this.initGUI(); 103 } 104 105 /** 106 * Build the GUI components 107 */ 108 private void initGUI() { 109 log.debug("initGUI"); 110 this.setTitle(Bundle.getMessage("VSDManagerFrameTitle")); 111 this.buildMenu(); 112 this.setLayout(new BoxLayout(this.getContentPane(), BoxLayout.PAGE_AXIS)); 113 114 decoderPane = new JPanel(); 115 decoderPane.setLayout(new BoxLayout(decoderPane, BoxLayout.PAGE_AXIS)); 116 decoderBlank = VSDControl.generateBlank(); 117 decoderPane.add(decoderBlank); 118 119 volumePane = new JPanel(); 120 volumePane.setLayout(new BoxLayout(volumePane, BoxLayout.LINE_AXIS)); 121 JToggleButton muteButton = new JToggleButton(Bundle.getMessage("MuteButtonLabel")); 122 JButton addButton = new JButton(Bundle.getMessage("AddButtonLabel")); 123 final JSlider volume = new JSlider(0, 100); 124 volume.setMinorTickSpacing(10); 125 volume.setPaintTicks(true); 126 master_volume = VSDecoderManager.instance().getMasterVolume(); 127 volume.setValue(master_volume); 128 volume.setPreferredSize(new Dimension(200, 20)); 129 volume.setToolTipText(Bundle.getMessage("MgrVolumeToolTip")); 130 volume.addChangeListener(new ChangeListener() { 131 @Override 132 public void stateChanged(ChangeEvent e) { 133 volumeChange(e); // slider in real time 134 } 135 }); 136 volumePane.add(new JLabel(Bundle.getMessage("VolumePaneLabel"))); 137 volumePane.add(volume); 138 volumePane.add(muteButton); 139 muteButton.setToolTipText(Bundle.getMessage("MgrMuteToolTip")); 140 muteButton.setMnemonic(Mnemonics.get("MuteButton")); 141 muteButton.addActionListener(new ActionListener() { 142 @Override 143 public void actionPerformed(ActionEvent e) { 144 muteButtonPressed(e); 145 } 146 }); 147 volumePane.add(addButton); 148 addButton.setToolTipText(Bundle.getMessage("MgrAddButtonToolTip")); 149 addButton.setMnemonic(Mnemonics.get("AddButton")); 150 addButton.addActionListener(new ActionListener() { 151 @Override 152 public void actionPerformed(ActionEvent e) { 153 addButtonPressed(e); 154 } 155 }); 156 157 this.add(decoderPane); 158 this.add(volumePane); 159 160 addWindowListener(new java.awt.event.WindowAdapter() { 161 @Override 162 public void windowClosing(java.awt.event.WindowEvent e) { 163 firePropertyChange(CLOSE_WINDOW, null, null); 164 } 165 }); 166 167 log.debug("pane size + {}", decoderPane.getPreferredSize()); 168 this.pack(); 169 this.setVisible(true); 170 171 log.debug("done..."); 172 173 // first, check Viewing Mode 174 if (is_viewing) { 175 vsdlist = new ArrayList<>(); // List of VSDecoders with uncomplete configuration (no Roster Entry reference) 176 for (VSDecoder vsd : VSDecoderManager.instance().getVSDecoderList()) { 177 if (vsd.getRosterEntry() != null) { 178 // VSDecoder configuration is complete and will be listed 179 addButton.doClick(); // simulate an Add-button-click 180 cd.setRosterItem(vsd.getRosterEntry()); // forward the roster entry 181 } else { 182 vsdlist.add(vsd); // VSDecoder with uncomplete configuration 183 } 184 } 185 // delete VSDecoder(s) with uncomplete configuration 186 for (VSDecoder v : vsdlist) { 187 VSDecoderManager.instance().deleteDecoder(v.getAddress().toString()); 188 } 189 // change back to Edit mode 190 is_viewing = false; 191 } else if (is_auto_loading) { 192 // Auto-Load 193 log.info("Auto-Loading VSDecoder"); 194 String vsdRosterGroup = "VSD"; 195 String msg = ""; 196 if (Roster.getDefault().getRosterGroupList().contains(vsdRosterGroup)) { 197 List<RosterEntry> rosterList; 198 rosterList = Roster.getDefault().getEntriesInGroup(vsdRosterGroup); 199 if (!rosterList.isEmpty()) { 200 // Allow <max_decoder> roster entries 201 int entry_counter = 1; 202 for (RosterEntry entry : rosterList) { 203 if (entry_counter <= VSDecoderManager.max_decoder) { 204 addButton.doClick(); // simulate an Add-button-click 205 cd.setRosterItem(entry); // forward the roster entry 206 entry_counter++; 207 } else { 208 msg = "Only " + VSDecoderManager.max_decoder + " Roster Entries allowed. Discarded " 209 + (rosterList.size() - VSDecoderManager.max_decoder); 210 } 211 } 212 } else { 213 msg = "No Roster Entry found in Roster Group " + vsdRosterGroup; 214 } 215 } else { 216 msg = "Roster Group \"" + vsdRosterGroup + "\" not found"; 217 } 218 if (!msg.isEmpty()) { 219 JmriJOptionPane.showMessageDialog(null, "Auto-Loading: " + msg); 220 log.warn("Auto-Loading VSDecoder aborted"); 221 } 222 } 223 } 224 225 /** 226 * Handle "Mute" button press. 227 * @param e Event that kicked this off. 228 */ 229 protected void muteButtonPressed(ActionEvent e) { 230 JToggleButton b = (JToggleButton) e.getSource(); 231 log.debug("Mute button pressed. value: {}", b.isSelected()); 232 firePropertyChange(MUTE, !b.isSelected(), b.isSelected()); 233 } 234 235 /** 236 * Handle "Add" button press 237 * @param e Event that fired this change 238 */ 239 protected void addButtonPressed(ActionEvent e) { 240 log.debug("Add button pressed"); 241 242 // If the maximum number of VSDecoders (Controls) is reached, don't create a new Control 243 // In Viewing Mode up to 4 existing VSDecoders are possible, so skip the check 244 if (! is_viewing && VSDecoderManager.instance().getVSDecoderList().size() >= VSDecoderManager.max_decoder) { 245 JmriJOptionPane.showMessageDialog(null, 246 "VSDecoder cannot be created. Maximal number is " + String.valueOf(VSDecoderManager.max_decoder)); 247 } else { 248 config = new VSDConfig(); // Create a new Config for the new VSDecoder. 249 // Do something here. Create a new VSDecoder and add it to the window. 250 cd = new VSDConfigDialog(decoderPane, Bundle.getMessage("NewDecoderConfigPaneTitle"), config, is_auto_loading, is_viewing); 251 cd.addPropertyChangeListener(new PropertyChangeListener() { 252 @Override 253 public void propertyChange(PropertyChangeEvent event) { 254 log.debug("property change name {}, old: {}, new: {}", event.getPropertyName(), 255 event.getOldValue(), event.getNewValue()); 256 addButtonPropertyChange(event); 257 } 258 }); 259 } 260 } 261 262 /** 263 * Callback for the Config Dialog 264 * @param event Event that fired this change 265 */ 266 protected void addButtonPropertyChange(PropertyChangeEvent event) { 267 log.debug("internal config dialog handler"); 268 // If this decoder already exists, don't create a new Control 269 // In Viewing Mode up to 4 existing VSDecoders are possible, so skip the check 270 VSDecoder newDecoder = null; 271 if (! is_viewing && VSDecoderManager.instance().getVSDecoderByAddress(config.getLocoAddress().toString()) != null) { 272 JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("MgrAddDuplicateMessage")); 273 } else { 274 newDecoder = VSDecoderManager.instance().getVSDecoder(config); 275 if (newDecoder == null) { 276 log.error("Lost context, VSDecoder is null. Quit JMRI and start over. No New Decoder constructed! Address: {}, profile: {}", 277 config.getLocoAddress(), config.getProfileName()); 278 return; 279 } 280 VSDControl newControl = new VSDControl(config); 281 // Set the Decoder to listen to PropertyChanges from the control 282 newControl.addPropertyChangeListener(newDecoder); 283 this.addPropertyChangeListener(newDecoder); 284 // Set US to listen to PropertyChanges from the control (mainly for DELETE) 285 newControl.addPropertyChangeListener(new PropertyChangeListener() { 286 @Override 287 public void propertyChange(PropertyChangeEvent event) { 288 log.debug("property change name {}, old: {}, new: {}", 289 event.getPropertyName(), event.getOldValue(), event.getNewValue()); 290 vsdControlPropertyChange(event); 291 } 292 }); 293 if (decoderPane.isAncestorOf(decoderBlank)) { 294 decoderPane.remove(decoderBlank); 295 } 296 297 decoderPane.add(newControl); 298 newControl.addSoundButtons(new ArrayList<SoundEvent>(newDecoder.getEventList())); 299 300 firePropertyChange(VOLUME_CHANGE, master_volume, null); 301 log.debug("Master volume set to {}", master_volume); 302 303 decoderPane.revalidate(); 304 decoderPane.repaint(); 305 306 this.pack(); 307 //this.setVisible(true); 308 // Do we need to make newControl a listener to newDecoder? 309 } 310 if (newDecoder != null) { 311 getStartBlock(newDecoder); 312 } 313 } 314 315 private void getStartBlock(VSDecoder vsd) { 316 jmri.Block start_block = null; 317 for (jmri.Block blk : jmri.InstanceManager.getDefault(jmri.BlockManager.class).getNamedBeanSet()) { 318 if (VSDecoderManager.instance().possibleStartBlocks.containsKey(blk)) { 319 int locoAddress = VSDecoderManager.instance().getLocoAddr(blk); 320 if (locoAddress == vsd.getAddress().getNumber()) { 321 log.debug("found start block: {}, loco address: {}", blk, locoAddress); 322 Sensor s = blk.getSensor(); 323 if (s != null && is_block_using) { 324 if (s.getKnownState() == Sensor.UNKNOWN) { 325 try { 326 s.setState(Sensor.ACTIVE); 327 } catch (jmri.JmriException ex) { 328 log.debug("Exception setting sensor"); 329 } 330 } 331 } 332 start_block = blk; 333 break; // one loco address per block 334 } 335 } 336 } 337 if (start_block != null) { 338 VSDecoderManager.instance().atStart(start_block); 339 } 340 } 341 342 /** 343 * Handle property change event from one of the VSDControls 344 * @param event Event that fired this change 345 */ 346 protected void vsdControlPropertyChange(PropertyChangeEvent event) { 347 String property = event.getPropertyName(); 348 if (property.equals(VSDControl.DELETE)) { 349 String ov = (String) event.getOldValue(); 350 log.debug("vsdControlPropertyChange. ID: {}, old: {}", VSDControl.DELETE, ov); 351 VSDecoder vsd = VSDecoderManager.instance().getVSDecoderByAddress(ov); 352 if (vsd == null) { 353 log.warn("Lost context, VSDecoder is null. Quit JMRI and start over."); 354 return; 355 } 356 if (vsd.getEngineSound().isEngineStarted()) { 357 JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("MgrDeleteWhenEngineStopped")); 358 return; 359 } else { 360 this.removePropertyChangeListener(vsd); 361 log.debug("vsdControlPropertyChange. ID: {}, old: {}", REMOVE_DECODER, ov); 362 firePropertyChange(REMOVE_DECODER, ov, null); 363 decoderPane.remove((VSDControl) event.getSource()); 364 if (decoderPane.getComponentCount() == 0) { 365 decoderPane.add(decoderBlank); 366 } 367 //debugPrintDecoderList(); 368 decoderPane.revalidate(); 369 decoderPane.repaint(); 370 371 this.pack(); 372 } 373 } 374 } 375 376 /** 377 * Handle master volume slider change 378 * @param event Event that fired this change 379 */ 380 protected void volumeChange(ChangeEvent event) { 381 JSlider v = (JSlider) event.getSource(); 382 log.debug("Volume slider moved. value: {}", v.getValue()); 383 master_volume = v.getValue(); 384 firePropertyChange(VOLUME_CHANGE, master_volume, null); 385 // todo? do you want to save? 386 if (VSDecoderManager.instance().getMasterVolume() != v.getValue()) { 387 VSDecoderManager.instance().setMasterVolume(v.getValue()); 388 VSDecoderManager.instance().getVSDecoderPreferences().save(); 389 log.debug("VSD Preferences saved"); 390 } 391 } 392 393 private void buildMenu() { 394 JMenu fileMenu = new JMenu(Bundle.getMessage("MenuFile")); // uses NamedBeanBundle 395 fileMenu.setMnemonic(Mnemonics.get("FileMenu")); // OK to use this different key name for Mnemonics 396 397 fileMenu.add(new LoadVSDFileAction(Bundle.getMessage("VSDecoderFileMenuLoadVSDFile"))); 398 399 JMenu editMenu = new JMenu(Bundle.getMessage("MenuEdit")); 400 editMenu.setMnemonic(Mnemonics.get("EditMenu")); // OK to use this different key name for Mnemonics 401 editMenu.add(new VSDPreferencesAction(Bundle.getMessage("VSDecoderFileMenuPreferences"))); 402 403 menuList = new ArrayList<>(2); 404 405 menuList.add(fileMenu); 406 menuList.add(editMenu); 407 408 this.setJMenuBar(new JMenuBar()); 409 410 this.getJMenuBar().add(fileMenu); 411 this.getJMenuBar().add(editMenu); 412 413 this.addHelpMenu("package.jmri.jmrit.vsdecoder.swing.VSDManagerFrame", true); 414 } 415 416 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(VSDManagerFrame.class); 417 418}