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-2025 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 private JPanel decoderPane; 077 private JPanel volumePane; 078 private JPanel decoderBlank; 079 private JButton addButton; 080 081 private VSDConfig config; 082 private VSDConfigDialog cd; 083 private List<JMenu> menuList; 084 private boolean is_auto_loading; 085 private boolean is_block_using; 086 private boolean is_viewing; 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 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 jmri.util.ThreadingUtil.runOnGUI(() -> { 172 if (is_viewing) { 173 this.runViewing(); 174 } else if (is_auto_loading) { 175 this.runAutoLoad(); 176 } 177 }); 178 } 179 180 private void runViewing() { 181 log.debug("Viewing mode"); 182 RosterEntry roster; 183 for (VSDecoder vsd : VSDecoderManager.instance().getVSDecoderList()) { 184 if (vsd.getRosterEntry() != null) { 185 // take the existing Roster entry; all is set 186 roster = vsd.getRosterEntry(); 187 } else { 188 // take a Roster entry temporarily to trigger the process 189 roster = new RosterEntry(vsd.getAddress().toString()); 190 roster.setId(vsd.getId()); 191 roster.setDccAddress(String.valueOf(vsd.getAddress().getNumber())); 192 roster.putAttribute("VSDecoder_Path", vsd.getVSDFilePath()); 193 roster.putAttribute("VSDecoder_Profile", vsd.getProfileName()); 194 roster.putAttribute("VSDecoder_LaunchThrottle", "no"); 195 } 196 addButton.doClick(); // simulate an Add-button-click 197 cd.setRosterItem(roster); // forward the roster entry 198 } 199 // change back to Edit mode 200 is_viewing = false; 201 } 202 203 private void runAutoLoad() { 204 log.debug("Auto-Loading VSDecoder"); 205 String vsdRosterGroup = "VSD"; 206 String msg = ""; 207 if (Roster.getDefault().getRosterGroupList().contains(vsdRosterGroup)) { 208 List<RosterEntry> rosterList; 209 rosterList = Roster.getDefault().getEntriesInGroup(vsdRosterGroup); 210 if (!rosterList.isEmpty()) { 211 // Allow <max_decoder> roster entries 212 int entry_counter = 1; 213 for (RosterEntry entry : rosterList) { 214 if (entry_counter <= VSDecoderManager.max_decoder) { 215 addButton.doClick(); // simulate an Add-button-click 216 cd.setRosterItem(entry); // forward the roster entry 217 entry_counter++; 218 } else { 219 msg = "Only " + VSDecoderManager.max_decoder + " Roster Entries allowed. Discarded " 220 + (rosterList.size() - VSDecoderManager.max_decoder); 221 } 222 } 223 } else { 224 msg = "No Roster Entry found in Roster Group " + vsdRosterGroup; 225 } 226 } else { 227 msg = "Roster Group \"" + vsdRosterGroup + "\" not found"; 228 } 229 if (!msg.isEmpty()) { 230 JmriJOptionPane.showMessageDialog(null, "Auto-Loading: " + msg); 231 log.warn("Auto-Loading VSDecoder aborted"); 232 } 233 } 234 235 /** 236 * Handle "Mute" button press. 237 * @param e Event that kicked this off. 238 */ 239 protected void muteButtonPressed(ActionEvent e) { 240 JToggleButton b = (JToggleButton) e.getSource(); 241 log.debug("Mute button pressed. value: {}", b.isSelected()); 242 firePropertyChange(MUTE, !b.isSelected(), b.isSelected()); 243 } 244 245 /** 246 * Handle "Add" button press 247 * @param e Event that fired this change 248 */ 249 protected void addButtonPressed(ActionEvent e) { 250 log.debug("Add button pressed"); 251 // If the maximum number of VSDecoders (Controls) is reached, don't create a new Control 252 // In Viewing Mode up to <max_decoder> existing VSDecoders are possible, so skip the check 253 if (! is_viewing && VSDecoderManager.instance().getVSDecoderList().size() >= VSDecoderManager.max_decoder) { 254 JmriJOptionPane.showMessageDialog(null, 255 "VSDecoder cannot be created. Maximal number is " + String.valueOf(VSDecoderManager.max_decoder)); 256 } else if (jmri.InstanceManager.getDefault(jmri.AudioManager.class). 257 getNamedBeanSet(jmri.Audio.BUFFER).size() == jmri.AudioManager.MAX_BUFFERS) { 258 JmriJOptionPane.showMessageDialog(null, "Decoder cannot be created! No more free buffers."); 259 } else { 260 config = new VSDConfig(); // Create a new Config for the new VSDecoder. 261 // Do something here. Create a new VSDecoder and add it to the window. 262 cd = new VSDConfigDialog(decoderPane, Bundle.getMessage("NewDecoderConfigPaneTitle"), 263 config, is_auto_loading, is_viewing); 264 cd.addPropertyChangeListener(new PropertyChangeListener() { 265 @Override 266 public void propertyChange(PropertyChangeEvent event) { 267 log.debug("property change name {}, old: {}, new: {}", event.getPropertyName(), 268 event.getOldValue(), event.getNewValue()); 269 addButtonPropertyChange(event); 270 } 271 }); 272 } 273 } 274 275 /** 276 * Callback for the Config Dialog 277 * @param event Event that fired this change 278 */ 279 protected void addButtonPropertyChange(PropertyChangeEvent event) { 280 log.debug("internal config dialog handler"); 281 // If this decoder already exists, don't create a new Control 282 // In Viewing Mode up to <max_decoder> existing VSDecoders are allowed, so skip the check 283 if (! is_viewing && VSDecoderManager.instance().getVSDecoderByAddress(config.getLocoAddress().toString()) != null) { 284 JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("MgrAddDuplicateMessage")); 285 } else { 286 VSDecoder newDecoder = VSDecoderManager.instance().getVSDecoder(config); 287 if (newDecoder != null) { 288 VSDControl newControl = new VSDControl(config); 289 // Set the Decoder to listen to PropertyChanges from the control 290 newControl.addPropertyChangeListener(newDecoder); 291 this.addPropertyChangeListener(newDecoder); 292 // Set US to listen to PropertyChanges from the control (mainly for DELETE) 293 newControl.addPropertyChangeListener(new PropertyChangeListener() { 294 @Override 295 public void propertyChange(PropertyChangeEvent event) { 296 log.debug("property change name {}, old: {}, new: {}", 297 event.getPropertyName(), event.getOldValue(), event.getNewValue()); 298 vsdControlPropertyChange(event); 299 } 300 }); 301 if (decoderPane.isAncestorOf(decoderBlank)) { 302 decoderPane.remove(decoderBlank); 303 } 304 305 decoderPane.add(newControl); 306 newControl.addSoundButtons(new ArrayList<SoundEvent>(newDecoder.getEventList())); 307 308 firePropertyChange(VOLUME_CHANGE, master_volume, null); 309 log.debug("Master volume set to {}", master_volume); 310 311 decoderPane.revalidate(); 312 decoderPane.repaint(); 313 314 this.pack(); 315 //this.setVisible(true); 316 // Do we need to make newControl a listener to newDecoder? 317 318 if (is_viewing) { 319 VSDecoderManager.instance().doResume(); 320 } else { 321 getStartBlock(newDecoder); 322 } 323 } 324 } 325 } 326 327 private void getStartBlock(VSDecoder vsd) { 328 jmri.Block start_block = null; 329 for (jmri.Block blk : jmri.InstanceManager.getDefault(jmri.BlockManager.class).getNamedBeanSet()) { 330 if (VSDecoderManager.instance().checkForPossibleStartblock(blk)) { 331 int locoAddress = VSDecoderManager.instance().getLocoAddr(blk); 332 if (locoAddress == vsd.getAddress().getNumber()) { 333 log.debug("found start block: {}, loco address: {}", blk, locoAddress); 334 Sensor s = blk.getSensor(); 335 if (s != null && is_block_using) { 336 if (s.getKnownState() == Sensor.UNKNOWN) { 337 try { 338 s.setState(Sensor.ACTIVE); 339 } catch (jmri.JmriException ex) { 340 log.debug("Exception setting sensor"); 341 } 342 } 343 } 344 start_block = blk; 345 break; // one loco address per block 346 } 347 } 348 } 349 if (start_block != null) { 350 VSDecoderManager.instance().atStart(start_block); 351 } 352 } 353 354 /** 355 * Handle property change event from one of the VSDControls 356 * @param event Event that fired this change 357 */ 358 protected void vsdControlPropertyChange(PropertyChangeEvent event) { 359 String property = event.getPropertyName(); 360 if (property.equals(VSDControl.DELETE)) { 361 String ov = (String) event.getOldValue(); 362 log.debug("vsdControlPropertyChange. ID: {}, old: {}", VSDControl.DELETE, ov); 363 VSDecoder vsd = VSDecoderManager.instance().getVSDecoderByAddress(ov); 364 if (vsd == null) { 365 log.warn("Lost context, VSDecoder is null. Quit JMRI and start over."); 366 return; 367 } 368 if (vsd.getEngineSound().isEngineStarted()) { 369 JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("MgrDeleteWhenEngineStopped")); 370 return; 371 } else { 372 this.removePropertyChangeListener(vsd); 373 log.debug("vsdControlPropertyChange. ID: {}, old: {}", REMOVE_DECODER, ov); 374 firePropertyChange(REMOVE_DECODER, ov, null); 375 decoderPane.remove((VSDControl) event.getSource()); 376 if (decoderPane.getComponentCount() == 0) { 377 decoderPane.add(decoderBlank); 378 } 379 decoderPane.revalidate(); 380 decoderPane.repaint(); 381 382 this.pack(); 383 } 384 } 385 } 386 387 /** 388 * Handle master volume slider change 389 * @param event Event that fired this change 390 */ 391 protected void volumeChange(ChangeEvent event) { 392 JSlider v = (JSlider) event.getSource(); 393 log.debug("Volume slider moved. value: {}", v.getValue()); 394 master_volume = v.getValue(); 395 firePropertyChange(VOLUME_CHANGE, master_volume, null); 396 // todo? do you want to save? 397 if (VSDecoderManager.instance().getMasterVolume() != v.getValue()) { 398 VSDecoderManager.instance().setMasterVolume(v.getValue()); 399 VSDecoderManager.instance().getVSDecoderPreferences().save(); 400 log.debug("VSD Preferences saved"); 401 } 402 } 403 404 private void buildMenu() { 405 JMenu fileMenu = new JMenu(Bundle.getMessage("MenuFile")); // uses NamedBeanBundle 406 fileMenu.setMnemonic(Mnemonics.get("FileMenu")); // OK to use this different key name for Mnemonics 407 408 fileMenu.add(new LoadVSDFileAction(Bundle.getMessage("VSDecoderFileMenuLoadVSDFile"))); 409 410 JMenu editMenu = new JMenu(Bundle.getMessage("MenuEdit")); 411 editMenu.setMnemonic(Mnemonics.get("EditMenu")); // OK to use this different key name for Mnemonics 412 editMenu.add(new VSDPreferencesAction(Bundle.getMessage("VSDecoderFileMenuPreferences"))); 413 414 menuList = new ArrayList<>(2); 415 416 menuList.add(fileMenu); 417 menuList.add(editMenu); 418 419 this.setJMenuBar(new JMenuBar()); 420 421 this.getJMenuBar().add(fileMenu); 422 this.getJMenuBar().add(editMenu); 423 424 this.addHelpMenu("package.jmri.jmrit.vsdecoder.swing.VSDManagerFrame", true); 425 } 426 427 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(VSDManagerFrame.class); 428 429}