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