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