001package jmri.jmrit.display; 002 003import java.awt.event.ActionEvent; 004import java.awt.event.ActionListener; 005import java.util.HashMap; 006import java.util.Map; 007 008import javax.annotation.CheckForNull; 009import javax.annotation.Nonnull; 010import javax.swing.AbstractAction; 011import javax.swing.ButtonGroup; 012import javax.swing.JCheckBoxMenuItem; 013import javax.swing.JMenu; 014import javax.swing.JPopupMenu; 015import javax.swing.JRadioButtonMenuItem; 016 017import jmri.*; 018import jmri.jmrit.audio.AudioSource; 019import jmri.jmrit.catalog.NamedIcon; 020import jmri.util.swing.JmriMouseEvent; 021 022/** 023 * An icon that plays an audio on a web panel. 024 * 025 * @author Daniel Bergqvist (C) 2023 026 */ 027public class AudioIcon extends PositionableLabel { 028 029 public static final String PROPERTY_COMMAND = "Command"; 030 public static final String PROPERTY_COMMAND_PLAY = "Play"; 031 public static final String PROPERTY_COMMAND_STOP = "Stop"; 032 033 public static final IdentityManager IDENTITY_MANAGER = new IdentityManager(); 034 035 private final int _identity; 036 private NamedIcon _originalIcon = new NamedIcon("resources/icons/audio_icon.gif", "resources/icons/audio_icon.gif"); 037 private String _originalText = Bundle.getMessage("AudioIcon_Text"); 038 private OnClickOperation _onClickOperation = OnClickOperation.DoNothing; 039 private boolean _playSoundWhenJmriPlays = true; 040 private boolean _stopSoundWhenJmriStops = false; 041 042 // the associated Audio object 043 private NamedBeanHandle<Audio> _namedAudio; 044 045 public AudioIcon(String s, @Nonnull Editor editor) { 046 super(s, editor); 047 _identity = IDENTITY_MANAGER.getIdentity(this); 048 _originalText = s; 049 } 050 051 public AudioIcon(int identity, String s, @Nonnull Editor editor) { 052 super(s, editor); 053 _identity = IDENTITY_MANAGER.getIdentity(identity, this); 054 _originalText = s; 055 } 056 057 public AudioIcon(@CheckForNull NamedIcon s, @Nonnull Editor editor) { 058 super(s, editor); 059 _identity = IDENTITY_MANAGER.getIdentity(this); 060 _originalIcon = _namedIcon; 061 062 // Please retain the line below. It's used to create the resources/icons/audio_icon.gif icon 063 // createAudioIconImage(); 064 } 065 066 public AudioIcon(int identity, @CheckForNull NamedIcon s, @Nonnull Editor editor) { 067 super(s, editor); 068 _identity = IDENTITY_MANAGER.getIdentity(identity, this); 069 _originalIcon = _namedIcon; 070 071 // Please retain the line below. It's used to create the resources/icons/audio_icon.gif icon 072 // createAudioIconImage(); 073 } 074 075 @Override 076 public Positionable deepClone() { 077 AudioIcon pos = new AudioIcon(getText(), _editor); 078 pos._originalIcon = new NamedIcon(_originalIcon); 079 pos._originalText = _originalText; 080 pos.setAudio(getNamedAudio().getName()); 081 pos._onClickOperation = _onClickOperation; 082 pos._playSoundWhenJmriPlays = _playSoundWhenJmriPlays; 083 pos._stopSoundWhenJmriStops = _stopSoundWhenJmriStops; 084 085 return super.finishClone(pos); 086 } 087 088 @Override 089 @Nonnull 090 public String getTypeString() { 091 return Bundle.getMessage("PositionableType_AudioIcon"); 092 } 093 094 @Override 095 @Nonnull 096 public String getNameString() { 097 String name; 098 if (_namedAudio == null) { 099 name = Bundle.getMessage("NotConnected"); 100 } else { 101 name = _namedAudio.getBean().getDisplayName( 102 NamedBean.DisplayOptions.USERNAME_SYSTEMNAME); 103 } 104 return name; 105 } 106 107 public int getIdentity() { 108 return _identity; 109 } 110 111 /** 112 * Attached a named audio to this display item 113 * 114 * @param pName System/user name to lookup the audio object 115 */ 116 public void setAudio(String pName) { 117 if (InstanceManager.getNullableDefault(jmri.AudioManager.class) != null) { 118 try { 119 Audio audio = InstanceManager.getDefault(AudioManager.class).provideAudio(pName); 120 setAudio(jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).getNamedBeanHandle(pName, audio)); 121 } catch (AudioException | IllegalArgumentException ex) { 122 log.error("Audio '{}' not available, icon won't see changes", pName); 123 } 124 } else { 125 log.error("No AudioManager for this protocol, icon won't see changes"); 126 } 127 } 128 129 /** 130 * Attached a named audio to this display item 131 * 132 * @param s the Audio 133 */ 134 public void setAudio(NamedBeanHandle<Audio> s) { 135 _namedAudio = s; 136 if (_namedAudio != null) { 137 setName(_namedAudio.getName()); // Swing name for e.g. tests 138 } 139 } 140 141 public Audio getAudio() { 142 if (_namedAudio == null) { 143 return null; 144 } 145 return _namedAudio.getBean(); 146 } 147 148 @Override 149 public jmri.NamedBean getNamedBean() { 150 return getAudio(); 151 } 152 153 public NamedBeanHandle<Audio> getNamedAudio() { 154 return _namedAudio; 155 } 156 157 public void setOnClickOperation(OnClickOperation operation) { 158 _onClickOperation = operation; 159 } 160 161 public OnClickOperation getOnClickOperation() { 162 return _onClickOperation; 163 } 164 165 public void setPlaySoundWhenJmriPlays(boolean value) { 166 _playSoundWhenJmriPlays = value; 167 } 168 169 public boolean getPlaySoundWhenJmriPlays() { 170 return _playSoundWhenJmriPlays; 171 } 172 173 public void setStopSoundWhenJmriStops(boolean value) { 174 _stopSoundWhenJmriStops = value; 175 } 176 177 public boolean getStopSoundWhenJmriStops() { 178 return _stopSoundWhenJmriStops; 179 } 180 181 public void play() { 182 log.debug("AudioIcon.play()"); 183 firePropertyChange(PROPERTY_COMMAND, null, PROPERTY_COMMAND_PLAY); 184 } 185 186 public void stop() { 187 log.debug("AudioIcon.stop()"); 188 firePropertyChange(PROPERTY_COMMAND, null, PROPERTY_COMMAND_STOP); 189 } 190 191 @Override 192 protected void edit() { 193 makeIconEditorFrame(this, "Audio", true, null); 194 _iconEditor.setPickList(jmri.jmrit.picker.PickListModel.audioPickModelInstance()); 195 _iconEditor.setIcon(0, "plainIcon", _namedIcon); 196 _iconEditor.makeIconPanel(false); 197 198 // set default icons, then override with this turnout's icons 199 ActionListener addIconAction = (ActionEvent a) -> updateAudio(); 200 _iconEditor.complete(addIconAction, true, true, true); 201 _iconEditor.setSelection(getAudio()); 202 } 203 204 void updateAudio() { 205 setAudio(_iconEditor.getTableSelection().getDisplayName()); 206 var iconMap = _iconEditor.getIconMap(); 207 NamedIcon newIcon = iconMap.get("plainIcon"); 208 setIcon(newIcon); 209 _iconEditorFrame.dispose(); 210 _iconEditorFrame = null; 211 _iconEditor = null; 212 invalidate(); 213 } 214 215 @Override 216 protected void editIcon() { 217 super.editIcon(); 218 // If the icon is changed, we must remember that in case the user 219 // switches between icon -> text -> icon 220 _originalIcon = _namedIcon; 221 } 222 223 @Override 224 public void doMousePressed(JmriMouseEvent e) { 225 log.debug("doMousePressed"); 226 if (!e.isMetaDown() && !e.isAltDown()) { 227 if (_onClickOperation != OnClickOperation.DoNothing && _namedAudio != null) { 228 Audio audio = _namedAudio.getBean(); 229 if (audio.getSubType() == Audio.SOURCE && (audio instanceof AudioSource)) { 230 AudioSource source = (AudioSource)audio; 231 if (source.getState() == Audio.STATE_PLAYING) { 232 source.stop(); 233 } else { 234 source.play(); 235 } 236 } 237 } 238 } 239 super.doMousePressed(e); 240 } 241 242 private void changeAudioIconType() { 243 _unRotatedText = null; 244 if (isIcon()) { 245 _icon = false; 246 _text = true; 247 setText(_originalText); 248 setIcon(null); 249 setOpaque(true); 250 } else if (isText()) { 251 _icon = true; 252 if (getText() != null) { 253 _originalText = getText(); 254 } 255 _text = false; 256 setText(null); 257 setUnRotatedText(null); 258 setOpaque(false); 259 setIcon(_originalIcon); 260 } 261 int deg = getDegrees(); 262 rotate(deg); 263 } 264 265 /** 266 * Pop-up just displays the audio name. 267 * 268 * @param popup the menu to display 269 * @return always true 270 */ 271 @Override 272 public boolean showPopUp(JPopupMenu popup) { 273 if (isEditable()) { 274 if (isIcon()) { 275 popup.add(new AbstractAction(Bundle.getMessage("ChangeToText")) { 276 @Override 277 public void actionPerformed(ActionEvent e) { 278 changeAudioIconType(); 279 } 280 }); 281 } else { 282 popup.add(new AbstractAction(Bundle.getMessage("ChangeToIcon")) { 283 @Override 284 public void actionPerformed(ActionEvent e) { 285 changeAudioIconType(); 286 } 287 }); 288 } 289 290 JMenu menu = new JMenu(Bundle.getMessage("AudioIcon_WebPanelMenu")); 291 ButtonGroup buttonGroup = new ButtonGroup(); 292 293 JRadioButtonMenuItem rbMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("AudioIcon_WebPanelMenu_OnClickPlaySoundGlobally")); 294 rbMenuItem.addActionListener((ActionEvent event) -> { 295 _onClickOperation = OnClickOperation.PlaySoundGlobally; 296 }); 297 rbMenuItem.setSelected(_onClickOperation == OnClickOperation.PlaySoundGlobally); 298 menu.add(rbMenuItem); 299 buttonGroup.add(rbMenuItem); 300 301 rbMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("AudioIcon_WebPanelMenu_OnClickPlaySoundLocally")); 302 rbMenuItem.addActionListener((ActionEvent event) -> { 303 _onClickOperation = OnClickOperation.PlaySoundLocally; 304 }); 305 rbMenuItem.setSelected(_onClickOperation == OnClickOperation.PlaySoundLocally); 306 menu.add(rbMenuItem); 307 buttonGroup.add(rbMenuItem); 308 309 rbMenuItem = new JRadioButtonMenuItem(Bundle.getMessage("AudioIcon_WebPanelMenu_OnClickDoNothing")); 310 rbMenuItem.addActionListener((ActionEvent event) -> { 311 _onClickOperation = OnClickOperation.DoNothing; 312 }); 313 rbMenuItem.setSelected(_onClickOperation == OnClickOperation.DoNothing); 314 menu.add(rbMenuItem); 315 buttonGroup.add(rbMenuItem); 316 317 JCheckBoxMenuItem cbMenuItem2 = new JCheckBoxMenuItem(Bundle.getMessage("AudioIcon_WebPanelMenu_PlaySoundWhenJmriPlays")); 318 cbMenuItem2.addActionListener((ActionEvent event) -> { 319 _playSoundWhenJmriPlays = cbMenuItem2.isSelected(); 320 }); 321 cbMenuItem2.setSelected(_playSoundWhenJmriPlays); 322 menu.add(cbMenuItem2); 323 324 JCheckBoxMenuItem cbMenuItem3 = new JCheckBoxMenuItem(Bundle.getMessage("AudioIcon_WebPanelMenu_StopSoundWhenJmriStops")); 325 cbMenuItem3.addActionListener((ActionEvent event) -> { 326 _stopSoundWhenJmriStops = cbMenuItem3.isSelected(); 327 }); 328 cbMenuItem3.setSelected(_stopSoundWhenJmriStops); 329 menu.add(cbMenuItem3); 330 331 popup.add(menu); 332 } 333 return true; 334 } 335 336 337/* 338 // Please retain this commented method. It's used to create the resources/icons/logixng/logixng_icon.gif icon 339 340 private void createAudioIconImage() { 341 342 try { 343 int width = 69, height = 39; 344 345 // TYPE_INT_ARGB specifies the image format: 8-bit RGBA packed into integer pixels 346 java.awt.image.BufferedImage bi = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB); 347 348 java.awt.Graphics2D ig2 = bi.createGraphics(); 349 350 ig2.setColor(java.awt.Color.WHITE); 351 ig2.fillRect(0, 0, width-1, height-1); 352 ig2.setColor(java.awt.Color.BLACK); 353 ig2.drawRect(0, 0, width-1, height-1); 354 355 java.awt.Font font = new java.awt.Font("Verdana", java.awt.Font.BOLD, 15); 356 ig2.setFont(font); 357 ig2.setPaint(java.awt.Color.black); 358 359 // Draw string twice to get more bold 360 ig2.drawString("Audio", 11, 24); 361 ig2.drawString("Audio", 12, 24); 362 363 javax.imageio.ImageIO.write(bi, "gif", new java.io.File(jmri.util.FileUtil.getExternalFilename("resources/icons/audio_icon.gif"))); 364 } catch (java.io.IOException ie) { 365 throw new RuntimeException(ie); 366 } 367 } 368*/ 369 370 371 public enum OnClickOperation { 372 PlaySoundGlobally(Bundle.getMessage("AudioIcon_WebPanelMenu_OnClickPlaySoundGlobally")), 373 PlaySoundLocally(Bundle.getMessage("AudioIcon_WebPanelMenu_OnClickPlaySoundLocally")), 374 DoNothing(Bundle.getMessage("AudioIcon_WebPanelMenu_OnClickDoNothing")); 375 376 private final String _text; 377 378 private OnClickOperation(String text) { 379 this._text = text; 380 } 381 382 @Override 383 public String toString() { 384 return _text; 385 } 386 387 } 388 389 390 public static class IdentityManager { 391 392 Map<Integer, AudioIcon> _identities = new HashMap<>(); 393 int _lastIdentity = -1; 394 395 private IdentityManager() { 396 // Private constructor to keep it as a singleton 397 } 398 399 public int getIdentity(AudioIcon audioIcon) { 400 _lastIdentity++; 401 _identities.put(_lastIdentity, audioIcon); 402 return _lastIdentity; 403 } 404 405 public int getIdentity(int identity, AudioIcon audioIcon) { 406 if (_identities.containsKey(identity)) { 407 log.error("Identity {} already exists", identity); 408 return getIdentity(audioIcon); 409 } 410 _identities.put(identity, audioIcon); 411 if (identity > _lastIdentity) { 412 _lastIdentity = identity; 413 } 414 return identity; 415 } 416 417 public AudioIcon getAudioIcon(int identity) { 418 return _identities.get(identity); 419 } 420 421 } 422 423 424 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AudioIcon.class); 425}