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}