001package jmri.jmrit.beantable;
002
003import java.awt.event.ActionEvent;
004import java.util.ArrayList;
005
006import javax.annotation.Nonnull;
007import javax.swing.JButton;
008import javax.swing.JMenu;
009import javax.swing.JMenuBar;
010import javax.swing.JMenuItem;
011import javax.swing.JPanel;
012import javax.swing.JTable;
013import javax.swing.JTextField;
014import javax.swing.MenuElement;
015
016import jmri.Audio;
017import jmri.AudioManager;
018import jmri.InstanceManager;
019import jmri.NamedBean;
020import jmri.jmrit.audio.swing.AudioBufferFrame;
021import jmri.jmrit.audio.swing.AudioListenerFrame;
022import jmri.jmrit.audio.swing.AudioSourceFrame;
023import jmri.util.swing.JmriMouseEvent;
024
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028/**
029 * Swing action to create and register an AudioTable GUI.
030 *
031 * <hr>
032 * This file is part of JMRI.
033 * <p>
034 * JMRI is free software; you can redistribute it and/or modify it under the
035 * terms of version 2 of the GNU General Public License as published by the Free
036 * Software Foundation. See the "COPYING" file for a copy of this license.
037 * <p>
038 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
039 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
040 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
041 * <p>
042 *
043 * @author Bob Jacobsen Copyright (C) 2003
044 * @author Matthew Harris copyright (c) 2009
045 */
046public class AudioTableAction extends AbstractTableAction<Audio> {
047
048    AudioTableDataModel listeners;
049    AudioTableDataModel buffers;
050    AudioTableDataModel sources;
051
052    AudioSourceFrame sourceFrame;
053    AudioBufferFrame bufferFrame;
054    AudioListenerFrame listenerFrame;
055
056    AudioTableFrame atf;
057    AudioTablePanel atp;
058
059    /**
060     * Create an action with a specific title.
061     * <p>
062     * Note that the argument is the Action title, not the title of the
063     * resulting frame. Perhaps this should be changed?
064     *
065     * @param actionName title of the action
066     */
067    public AudioTableAction(String actionName) {
068        super(actionName);
069
070        // disable ourself if there is no primary Audio manager available
071        if (!InstanceManager.getOptionalDefault(AudioManager.class).isPresent()) {
072            setEnabled(false);
073        }
074    }
075
076    /**
077     * Default constructor
078     */
079    public AudioTableAction() {
080        this(Bundle.getMessage("TitleAudioTable"));
081    }
082
083    @Override
084    public void addToFrame(@Nonnull BeanTableFrame<Audio> f) {
085        JButton addBufferButton = new JButton(Bundle.getMessage("ButtonAddAudioBuffer"));
086        atp.addToBottomBox(addBufferButton);
087        addBufferButton.addActionListener(this::addBufferPressed);
088
089        JButton addSourceButton = new JButton(Bundle.getMessage("ButtonAddAudioSource"));
090        atp.addToBottomBox(addSourceButton);
091        addSourceButton.addActionListener(this::addSourcePressed);
092    }
093
094    @Override
095    public void actionPerformed(ActionEvent e) {
096
097        // create the JTable model, with changes for specific NamedBean
098        createModel();
099
100        // create the frame
101        atf = new AudioTableFrame(atp, helpTarget()) {
102
103            /**
104             * Include "Add Source..." and "Add Buffer..." buttons
105             */
106            @Override
107            void extras() {
108                addToFrame(this);
109            }
110        };
111        setTitle();
112        atf.pack();
113        atf.setVisible(true);
114    }
115
116    /**
117     * Create the JTable DataModels, along with the changes for the specific
118     * case of Audio objects
119     */
120    @Override
121    protected void createModel() {
122        // ensure that the AudioFactory has been initialised
123        InstanceManager.getOptionalDefault(jmri.AudioManager.class).ifPresent(cm -> {
124            if (cm.getActiveAudioFactory() == null) {
125                cm.init();
126                if (cm.getActiveAudioFactory() instanceof jmri.jmrit.audio.NullAudioFactory) {
127                    InstanceManager.getDefault(jmri.UserPreferencesManager.class).
128                            showWarningMessage("Error", "NullAudioFactory initialised - no sounds will be available", getClassName(), "nullAudio", false, true);
129                }
130            }
131        });
132        listeners = new AudioListenerTableDataModel();
133        buffers = new AudioBufferTableDataModel();
134        sources = new AudioSourceTableDataModel();
135        atp = new AudioTablePanel(listeners, buffers, sources, helpTarget());
136    }
137
138    @Override
139    public JPanel getPanel() {
140        createModel();
141
142        return atp;
143    }
144
145    @Override
146    protected void setTitle() {
147        atf.setTitle(Bundle.getMessage("TitleAudioTable"));
148    }
149
150    @Override
151    protected String helpTarget() {
152        return "package.jmri.jmrit.beantable.AudioTable";
153    }
154
155    @Override
156    protected void addPressed(ActionEvent e) {
157        log.warn("This should not have happened");
158    }
159
160    void addSourcePressed(ActionEvent e) {
161        if (sourceFrame == null) {
162            sourceFrame = new AudioSourceFrame(Bundle.getMessage("TitleAddAudioSource"), sources);
163        }
164        sourceFrame.updateBufferList();
165        sourceFrame.resetFrame();
166        sourceFrame.setEscapeKeyClosesWindow(true);
167        sourceFrame.pack();
168        sourceFrame.setVisible(true);
169    }
170
171    void addBufferPressed(ActionEvent e) {
172        if (bufferFrame == null) {
173            bufferFrame = new AudioBufferFrame(Bundle.getMessage("TitleAddAudioBuffer"), buffers);
174        }
175        bufferFrame.resetFrame();
176        bufferFrame.setEscapeKeyClosesWindow(true);
177        bufferFrame.pack();
178        bufferFrame.setVisible(true);
179    }
180
181    @Override
182    public void setMenuBar(BeanTableFrame<Audio> f) {
183        JMenuBar menuBar = f.getJMenuBar();
184        MenuElement[] subElements;
185        JMenu fileMenu = null;
186        for (int i = 0; i < menuBar.getMenuCount(); i++) {
187            if (menuBar.getComponent(i) instanceof JMenu) {
188                if (((JMenu) menuBar.getComponent(i)).getText().equals(Bundle.getMessage("MenuFile"))) {
189                    fileMenu = menuBar.getMenu(i);
190                }
191            }
192        }
193        if (fileMenu == null) {
194            return;
195        }
196        subElements = fileMenu.getSubElements();
197        for (MenuElement subElement : subElements) {
198            MenuElement[] popsubElements = subElement.getSubElements();
199            for (MenuElement popsubElement : popsubElements) {
200                if (popsubElement instanceof JMenuItem) {
201                    if (((JMenuItem) popsubElement).getText().equals(Bundle.getMessage("PrintTable"))) {
202                        JMenuItem printMenu = (JMenuItem) popsubElement;
203                        fileMenu.remove(printMenu);
204                        break;
205                    }
206                }
207            }
208        }
209        fileMenu.add(atp.getPrintItem());
210    }
211
212    protected void editAudio(Audio a) {
213        Runnable t;
214        switch (a.getSubType()) {
215            case Audio.LISTENER:
216                if (listenerFrame == null) {
217                    listenerFrame = new AudioListenerFrame(Bundle.getMessage("TitleAddAudioListener"), listeners);
218                }
219                listenerFrame.populateFrame(a);
220                t = new Runnable() {
221                    @Override
222                    public void run() {
223                        listenerFrame.pack();
224                        listenerFrame.setVisible(true);
225                    }
226                };
227                javax.swing.SwingUtilities.invokeLater(t);
228                break;
229            case Audio.BUFFER:
230                if (bufferFrame == null) {
231                    bufferFrame = new AudioBufferFrame(Bundle.getMessage("TitleAddAudioBuffer"), buffers);
232                }
233                bufferFrame.populateFrame(a);
234                t = new Runnable() {
235                    @Override
236                    public void run() {
237                        bufferFrame.pack();
238                        bufferFrame.setVisible(true);
239                    }
240                };
241                javax.swing.SwingUtilities.invokeLater(t);
242                break;
243            case Audio.SOURCE:
244                if (sourceFrame == null) {
245                    sourceFrame = new AudioSourceFrame(Bundle.getMessage("TitleAddAudioBuffer"), sources);
246                }
247                sourceFrame.updateBufferList();
248                sourceFrame.populateFrame(a);
249                t = new Runnable() {
250                    @Override
251                    public void run() {
252                        sourceFrame.pack();
253                        sourceFrame.setVisible(true);
254                    }
255                };
256                javax.swing.SwingUtilities.invokeLater(t);
257                break;
258            default:
259                throw new IllegalArgumentException();
260        }
261    }
262
263    private static final Logger log = LoggerFactory.getLogger(AudioTableAction.class);
264
265    /**
266     * Define abstract AudioTableDataModel
267     */
268    abstract public class AudioTableDataModel extends BeanTableDataModel<Audio> {
269
270        char subType;
271
272        public static final int EDITCOL = NUMCOLUMN;
273
274        @SuppressWarnings({"OverridableMethodCallInConstructor", "LeakingThisInConstructor"})
275        public AudioTableDataModel(char subType) {
276            super();
277            this.subType = subType;
278            getManager().addPropertyChangeListener(this);
279            updateNameList();
280        }
281
282        @Override
283        public AudioManager getManager() {
284            return InstanceManager.getDefault(jmri.AudioManager.class);
285        }
286
287        @Override
288        protected String getMasterClassName() {
289            return getClassName();
290        }
291
292        @Override
293        public Audio getBySystemName(@Nonnull String name) {
294            return InstanceManager.getDefault(jmri.AudioManager.class).getBySystemName(name);
295        }
296
297        @Override
298        public Audio getByUserName(@Nonnull String name) {
299            return InstanceManager.getDefault(jmri.AudioManager.class).getByUserName(name);
300        }
301
302        /**
303         * Update the NamedBean list for the specific sub-type
304         *
305         * @param subType Audio sub-type to update
306         */
307        protected synchronized void updateSpecificNameList(char subType) {
308            // first, remove listeners from the individual objects
309            if (sysNameList != null) {
310                for (String sysName : sysNameList) {
311                    // if object has been deleted, it's not here; ignore it
312                    NamedBean b = getBySystemName(sysName);
313                    if (b != null) {
314                        b.removePropertyChangeListener(this);
315                    }
316                }
317            }
318
319            // recreate the list of system names
320            var tempSet = getManager().getNamedBeanSet();
321            var out = new ArrayList<String>();
322            tempSet.stream().forEach((audio) -> {
323                if (audio.getSubType() == subType) {
324                    out.add(audio.getSystemName());
325                }
326            });
327            sysNameList = out;
328
329            // and add them back in
330            sysNameList.stream().forEach((sysName) -> {
331                getBySystemName(sysName).addPropertyChangeListener(this);
332            });
333        }
334
335        @Override
336        public int getColumnCount() {
337            return EDITCOL + 1;
338        }
339
340        @Override
341        public String getColumnName(int col) {
342            switch (col) {
343                case VALUECOL:
344                    return Bundle.getMessage("LightControlDescription");
345                case EDITCOL:
346                    return "";
347                default:
348                    return super.getColumnName(col);
349            }
350        }
351
352        @Override
353        public Class<?> getColumnClass(int col) {
354            switch (col) {
355                case VALUECOL:
356                    return String.class;
357                case EDITCOL:
358                    return JButton.class;
359                case DELETECOL:
360                    return (subType != Audio.LISTENER) ? JButton.class : String.class;
361                default:
362                    return super.getColumnClass(col);
363            }
364        }
365
366        @Override
367        public String getValue(String systemName) {
368            Object m = InstanceManager.getDefault(jmri.AudioManager.class).getBySystemName(systemName);
369            if (subType == Audio.SOURCE) {
370                return (m != null) ? ((jmri.jmrit.audio.AudioSource) m).getDebugString() : "";
371            } else {
372                return (m != null) ? m.toString() : "";
373            }
374        }
375
376        @Override
377        public Object getValueAt(int row, int col) {
378            Audio a;
379            switch (col) {
380                case SYSNAMECOL:  // slot number
381                    return sysNameList.get(row);
382                case USERNAMECOL:  // return user name
383                    // sometimes, the TableSorter invokes this on rows that no longer exist, so we check
384                    a = getBySystemName(sysNameList.get(row));
385                    return (a != null) ? a.getUserName() : null;
386                case VALUECOL:
387                    a = getBySystemName(sysNameList.get(row));
388                    return (a != null) ? getValue(a.getSystemName()) : null;
389                case COMMENTCOL:
390                    a = getBySystemName(sysNameList.get(row));
391                    return (a != null) ? a.getComment() : null;
392                case DELETECOL:
393                    return (subType != Audio.LISTENER) ? Bundle.getMessage("ButtonDelete") : "";
394                case EDITCOL:
395                    return Bundle.getMessage("ButtonEdit");
396                default:
397                    log.error("internal state inconsistent with table requst for {} {}", row, col);
398                    return null;
399            }
400        }
401
402        @Override
403        public void setValueAt(Object value, int row, int col) {
404            Audio a;
405            switch (col) {
406                case EDITCOL:
407                    a = getBySystemName(sysNameList.get(row));
408                    editAudio(a);
409                    break;
410                default:
411                    super.setValueAt(value, row, col);
412            }
413        }
414
415        @Override
416        public int getPreferredWidth(int col) {
417            switch (col) {
418                case VALUECOL:
419                    return new JTextField(50).getPreferredSize().width;
420                case EDITCOL:
421                    return new JButton(Bundle.getMessage("ButtonEdit")).getPreferredSize().width;
422                default:
423                    return super.getPreferredWidth(col);
424            }
425        }
426
427        @Override
428        public boolean isCellEditable(int row, int col) {
429            switch (col) {
430                case DELETECOL:
431                    return (subType != Audio.LISTENER);
432                case VALUECOL:
433                    return false;
434                case EDITCOL:
435                    return true;
436                default:
437                    return super.isCellEditable(row, col);
438            }
439        }
440
441        @Override
442        protected void clickOn(Audio t) {
443            // Do nothing
444        }
445
446        @Override
447        protected void configValueColumn(JTable table) {
448            // Do nothing
449        }
450
451        protected void configEditColumn(JTable table) {
452            // have the edit column hold a button
453            setColumnToHoldButton(table, EDITCOL,
454                    new JButton(Bundle.getMessage("ButtonEdit")));
455        }
456
457        @Override
458        protected String getBeanType() {
459            return "Audio";
460        }
461    }
462
463    /**
464     * Specific AudioTableDataModel for Audio Listener sub-type
465     */
466    public class AudioListenerTableDataModel extends AudioTableDataModel {
467
468        AudioListenerTableDataModel() {
469            super(Audio.LISTENER);
470        }
471
472        @Override
473        protected synchronized void updateNameList() {
474            updateSpecificNameList(Audio.LISTENER);
475        }
476
477        @Override
478        protected void showPopup(JmriMouseEvent e) {
479            // Do nothing - disable pop-up menu for AudioListener
480        }
481    }
482
483    /**
484     * Specific AudioTableDataModel for Audio Buffer sub-type
485     */
486    public class AudioBufferTableDataModel extends AudioTableDataModel {
487
488        AudioBufferTableDataModel() {
489            super(Audio.BUFFER);
490        }
491
492        @Override
493        protected synchronized void updateNameList() {
494            updateSpecificNameList(Audio.BUFFER);
495        }
496    }
497
498    /**
499     * Specific AudioTableDataModel for Audio Source sub-type
500     */
501    public class AudioSourceTableDataModel extends AudioTableDataModel {
502
503        AudioSourceTableDataModel() {
504            super(Audio.SOURCE);
505        }
506
507        @Override
508        protected synchronized void updateNameList() {
509            updateSpecificNameList(Audio.SOURCE);
510        }
511    }
512
513    @Override
514    public void setMessagePreferencesDetails(){
515        jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class).setPreferenceItemDetails(getClassName(), "nullAudio", Bundle.getMessage("HideNullAudioWarningMessage"));
516        super.setMessagePreferencesDetails();
517    }
518
519    @Override
520    public String getClassDescription() {
521        return Bundle.getMessage("TitleAudioTable");
522    }
523
524    @Override
525    protected String getClassName() {
526        return AudioTableAction.class.getName();
527    }
528}