001package jmri.jmrix.loconet.soundloader;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.awt.Font;
005import java.io.IOException;
006import javax.swing.JButton;
007import javax.swing.JComboBox;
008import javax.swing.JFileChooser;
009import javax.swing.JFrame;
010import javax.swing.JScrollPane;
011import javax.swing.JTable;
012import javax.swing.JTextArea;
013import javax.swing.JTextField;
014import javax.swing.table.TableCellEditor;
015import jmri.jmrix.loconet.spjfile.SpjFile;
016import jmri.util.FileUtil;
017import jmri.util.davidflanagan.HardcopyWriter;
018import jmri.util.table.ButtonEditor;
019import jmri.util.table.ButtonRenderer;
020import org.slf4j.Logger;
021import org.slf4j.LoggerFactory;
022
023/**
024 * Table data model for display of Digitrax SPJ files.
025 *
026 * @author Bob Jacobsen Copyright (C) 2003, 2006
027 * @author Dennis Miller Copyright (C) 2006
028 */
029public class EditorTableDataModel extends javax.swing.table.AbstractTableModel {
030
031    static public final int HEADERCOL = 0;
032    static public final int TYPECOL = 1;
033    static public final int MAPCOL = 2;
034    static public final int HANDLECOL = 3;
035    static public final int FILENAMECOL = 4;
036    static public final int LENGTHCOL = 5;
037    static public final int PLAYBUTTONCOL = 6;
038    static public final int REPLACEBUTTONCOL = 7;
039
040    static public final int NUMCOLUMN = 8;
041
042    SpjFile file;
043
044    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
045            justification = "cache resource at 1st start, threading OK") // NOI18N
046    public EditorTableDataModel(SpjFile file) {
047        super();
048        this.file = file;
049    }
050
051    @Override
052    public int getRowCount() {
053        // The 0th header is not displayed
054        return file.numHeaders() - 1;
055    }
056
057    @Override
058    public int getColumnCount() {
059        return NUMCOLUMN;
060    }
061
062    @Override
063    public String getColumnName(int col) {
064        switch (col) {
065            case HEADERCOL:
066                return Bundle.getMessage("HeaderHEADERCOL");
067            case TYPECOL:
068                return Bundle.getMessage("HeaderTYPECOL");
069            case HANDLECOL:
070                return Bundle.getMessage("HeaderHANDLECOL");
071            case MAPCOL:
072                return Bundle.getMessage("HeaderMAPCOL");
073            case FILENAMECOL:
074                return Bundle.getMessage("HeaderFILENAMECOL");
075            case LENGTHCOL:
076                return Bundle.getMessage("HeaderLENGTHCOL");
077            case PLAYBUTTONCOL:
078                return ""; // no title
079            case REPLACEBUTTONCOL:
080                return ""; // no title
081
082            default:
083                return "unknown";
084        }
085    }
086
087    @Override
088    public Class<?> getColumnClass(int col) {
089        switch (col) {
090            case HEADERCOL:
091            case HANDLECOL:
092                return Integer.class;
093            case LENGTHCOL:
094                return Float.class;
095            case MAPCOL:
096            case TYPECOL:
097            case FILENAMECOL:
098                return String.class;
099            case REPLACEBUTTONCOL:
100            case PLAYBUTTONCOL:
101                return JButton.class;
102            default:
103                return null;
104        }
105    }
106
107    @Override
108    public boolean isCellEditable(int row, int col) {
109        switch (col) {
110            case REPLACEBUTTONCOL:
111            case PLAYBUTTONCOL:
112                return true;
113            default:
114                return false;
115        }
116    }
117
118    @Override
119    public Object getValueAt(int row, int col) {
120        switch (col) {
121            case HEADERCOL:
122                return Integer.valueOf(row);
123            case HANDLECOL:
124                return Integer.valueOf(file.getHeader(row + 1).getHandle());
125            case MAPCOL:
126                return file.getMapEntry(file.getHeader(row + 1).getHandle());
127            case FILENAMECOL:
128                return "" + file.getHeader(row + 1).getName();
129            case TYPECOL:
130                return file.getHeader(row + 1).typeAsString();
131            case LENGTHCOL:
132                if (!file.getHeader(row + 1).isWAV()) {
133                    return null;
134                }
135                float rate = (new jmri.jmrit.sound.WavBuffer(file.getHeader(row + 1).getByteArray())).getSampleRate();
136                if (rate == 0.f) {
137                    log.error("Rate should not be zero");
138                    return null;
139                }
140                float time = file.getHeader(row + 1).getDataLength() / rate;
141                return Float.valueOf(time);
142            case PLAYBUTTONCOL:
143                if (file.getHeader(row + 1).isWAV()) {
144                    return Bundle.getMessage("ButtonPlay");
145                } else if (file.getHeader(row + 1).isTxt()) {
146                    return Bundle.getMessage("ButtonView");
147                } else if (file.getHeader(row + 1).isMap()) {
148                    return Bundle.getMessage("ButtonView");
149                } else if (file.getHeader(row + 1).isSDF()) {
150                    return Bundle.getMessage("ButtonView");
151                } else {
152                    return null;
153                }
154            case REPLACEBUTTONCOL:
155                if (file.getHeader(row + 1).isWAV()) {
156                    return Bundle.getMessage("ButtonReplace");
157                }
158                if (file.getHeader(row + 1).isSDF()) {
159                    return Bundle.getMessage("ButtonEdit");
160                } else {
161                    return null;
162                }
163            default:
164                log.error("internal state inconsistent with table requst for {} {}", row, col);
165                return null;
166        }
167    }
168
169    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "DB_DUPLICATE_SWITCH_CLAUSES",
170            justification = "better to keep cases in column order rather than to combine")
171    public int getPreferredWidth(int col) {
172        JTextField b;
173        switch (col) {
174            case TYPECOL:
175                return new JTextField(8).getPreferredSize().width;
176            case MAPCOL:
177                return new JTextField(12).getPreferredSize().width;
178            case HEADERCOL:
179            case HANDLECOL:
180                return new JTextField(3).getPreferredSize().width;
181            case FILENAMECOL:
182                return new JTextField(12).getPreferredSize().width;
183            case LENGTHCOL:
184                return new JTextField(5).getPreferredSize().width;
185            case PLAYBUTTONCOL:
186                b = new JTextField((String) getValueAt(1, PLAYBUTTONCOL));
187                return b.getPreferredSize().width + 30;
188            case REPLACEBUTTONCOL:
189                b = new JTextField((String) getValueAt(1, REPLACEBUTTONCOL));
190                return b.getPreferredSize().width + 30;
191            default:
192                log.warn("Unexpected column in getPreferredWidth: {}", col);
193                return new JTextField(8).getPreferredSize().width;
194        }
195    }
196
197    @Override
198    public void setValueAt(Object value, int row, int col) {
199        if (col == PLAYBUTTONCOL) {
200            // button fired, handle
201            if (file.getHeader(row + 1).isWAV()) {
202                playButtonPressed(value, row, col);
203                return;
204            } else if (file.getHeader(row + 1).isTxt()) {
205                viewTxtButtonPressed(value, row, col);
206                return;
207            } else if (file.getHeader(row + 1).isMap()) {
208                viewTxtButtonPressed(value, row, col);
209                return;
210            } else if (file.getHeader(row + 1).isSDF()) {
211                viewSdfButtonPressed(value, row, col);
212                return;
213            }
214        } else if (col == REPLACEBUTTONCOL) {
215            // button fired, handle
216            if (file.getHeader(row + 1).isWAV()) {
217                replWavButtonPressed(value, row, col);
218            } else if (file.getHeader(row + 1).isSDF()) {
219                editSdfButtonPressed(value, row, col);
220                return;
221            }
222        }
223    }
224
225    // should probably be abstract and put in invoking GUI
226    static JFileChooser chooser;  // shared across all uses
227
228    private synchronized static void setChooser( JFileChooser jfc ){
229        chooser = jfc;
230    }
231
232    void replWavButtonPressed(Object value, int row, int col) {
233        if (chooser == null) {
234            setChooser( new jmri.util.swing.JmriJFileChooser(FileUtil.getUserFilesPath()));
235        }
236        EditorTableDataModel.chooser.rescanCurrentDirectory();
237        int retVal = EditorTableDataModel.chooser.showOpenDialog(null);
238        if (retVal != JFileChooser.APPROVE_OPTION) {
239            return;  // give up if no file selected
240        }
241        // load file
242        jmri.jmrit.sound.WavBuffer buff;
243        try {
244            buff = new jmri.jmrit.sound.WavBuffer(chooser.getSelectedFile());
245        } catch (Exception e) {
246            log.error("Exception loading file", e);
247            return;
248        }
249        // store to memory
250        file.getHeader(row + 1).setContent(buff.getByteArray(), buff.getDataStart(), buff.getDataSize());
251        // update rest of header
252        file.getHeader(row + 1).setName(chooser.getSelectedFile().getName());
253
254        // mark table changes in other rows
255        fireTableRowsUpdated(row, row);
256    }
257
258    // should probably be abstract and put in invoking GUI
259    void playButtonPressed(Object value, int row, int col) {
260        // new jmri.jmrit.sound.WavBuffer(file.getHeader(row+1).getByteArray());
261        jmri.jmrit.sound.SoundUtil.playSoundBuffer(file.getHeader(row + 1).getByteArray());
262    }
263
264    // should probably be abstract and put in invoking GUI
265    // Also used to display the .map block
266    void viewTxtButtonPressed(Object value, int row, int col) {
267        String content = new String(file.getHeader(row + 1).getByteArray());
268        JFrame frame = new JFrame();
269        JTextArea text = new JTextArea(content);
270        text.setEditable(false);
271        text.setFont(new Font("Monospaced", Font.PLAIN, text.getFont().getSize())); // NOI18N
272        frame.getContentPane().add(new JScrollPane(text));
273        frame.pack();
274        frame.setVisible(true);
275    }
276
277    // should probably be abstract and put in invoking GUI
278    void viewSdfButtonPressed(Object value, int row, int col) {
279        jmri.jmrix.loconet.sdf.SdfBuffer buff = new jmri.jmrix.loconet.sdf.SdfBuffer(file.getHeader(row + 1).getByteArray());
280        String content = buff.toString();
281        JFrame frame = new jmri.util.JmriJFrame(Bundle.getMessage("TitleSdfView"));
282        JTextArea text = new JTextArea(content);
283        text.setEditable(false);
284        text.setFont(new Font("Monospaced", Font.PLAIN, text.getFont().getSize())); // NOI18N
285        frame.getContentPane().add(new JScrollPane(text));
286        frame.pack();
287        frame.setVisible(true);
288    }
289
290    // should probably be abstract and put in invoking GUI
291    void editSdfButtonPressed(Object value, int row, int col) {
292        jmri.jmrix.loconet.sdfeditor.EditorFrame sdfEditor
293                = new jmri.jmrix.loconet.sdfeditor.EditorFrame(file.getHeader(row + 1).getSdfBuffer());
294        sdfEditor.setVisible(true);
295    }
296
297    /**
298     * Configure a table to have our standard rows and columns.
299     * This is optional, in that other table formats can use this table model.
300     * But we put it here to help keep it consistent.
301     * @param table table to configured.
302     */
303    public void configureTable(JTable table) {
304        // allow reordering of the columns
305        table.getTableHeader().setReorderingAllowed(true);
306
307        // have to shut off autoResizeMode to get horizontal scroll to work (JavaSwing p 541)
308        table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
309
310        // resize columns as requested
311        for (int i = 0; i < table.getColumnCount(); i++) {
312            int width = getPreferredWidth(i);
313            table.getColumnModel().getColumn(i).setPreferredWidth(width);
314        }
315        //table.sizeColumnsToFit(-1);
316
317        // have the value column hold a button
318        setColumnToHoldButton(table, PLAYBUTTONCOL, largestWidthButton(PLAYBUTTONCOL));
319        setColumnToHoldButton(table, REPLACEBUTTONCOL, largestWidthButton(REPLACEBUTTONCOL));
320    }
321
322    public JButton largestWidthButton(int col) {
323        JButton retval = new JButton("TTTT");
324        if (col == PLAYBUTTONCOL) {
325            retval = checkLabelWidth(retval, "ButtonPlay");
326            retval = checkLabelWidth(retval, "ButtonView");
327        } else if (col == REPLACEBUTTONCOL) {
328            retval = checkLabelWidth(retval, "ButtonEdit");
329            retval = checkLabelWidth(retval, "ButtonReplace");
330        }
331        return retval;
332    }
333
334    private JButton checkLabelWidth(JButton now, String name) {
335        JButton b = new JButton(Bundle.getMessage(name));
336        b.revalidate();
337        if (b.getPreferredSize().width > now.getPreferredSize().width) {
338            return b;
339        } else {
340            return now;
341        }
342    }
343
344    /**
345     * Service method to set up a column so that it will hold a button for it's
346     * values.
347     *
348     * @param table The overall table, accessed for formatting
349     * @param column Which column to configure with this call
350     * @param sample Typical button, used for size
351     */
352    void setColumnToHoldButton(JTable table, int column, JButton sample) {
353        //TableColumnModel tcm = table.getColumnModel();
354        // install a button renderer & editor
355        ButtonRenderer buttonRenderer = new ButtonRenderer();
356        table.setDefaultRenderer(JButton.class, buttonRenderer);
357        TableCellEditor buttonEditor = new ButtonEditor(new JButton());
358        table.setDefaultEditor(JButton.class, buttonEditor);
359        // ensure the table rows, columns have enough room for buttons
360        table.setRowHeight(sample.getPreferredSize().height);
361        table.getColumnModel().getColumn(column)
362                .setPreferredWidth(sample.getPreferredSize().width + 30);
363    }
364
365    synchronized public void dispose() {
366    }
367
368    /**
369     * Self print - or print preview - the table.
370     * <p>
371     * Printed in equally sized
372     * columns across the page with headings and vertical lines between each
373     * column. Data is word wrapped within a column. Can handle data as strings,
374     * comboboxes or booleans.
375     *
376     * @param w the printer output to write to
377     */
378    public void printTable(HardcopyWriter w) {
379        // determine the column size - evenly sized, with space between for lines
380        int columnSize = (w.getCharactersPerLine() - this.getColumnCount() - 1) / this.getColumnCount();
381
382        // Draw horizontal dividing line
383        w.write(w.getCurrentLineNumber(), 0, w.getCurrentLineNumber(),
384                (columnSize + 1) * this.getColumnCount());
385
386        // print the column header labels
387        String[] columnStrings = new String[this.getColumnCount()];
388        // Put each column header in the array
389        for (int i = 0; i < this.getColumnCount(); i++) {
390            columnStrings[i] = this.getColumnName(i);
391        }
392        w.setFontStyle(Font.BOLD);
393        printColumns(w, columnStrings, columnSize);
394        w.setFontStyle(0);
395        w.write(w.getCurrentLineNumber(), 0, w.getCurrentLineNumber(),
396                (columnSize + 1) * this.getColumnCount());
397
398        // now print each row of data
399        // create a base string the width of the column
400        StringBuilder spaces = new StringBuilder("");
401        for (int i = 0; i < columnSize; i++) {
402            spaces.append(" ");
403        }
404        for (int i = 0; i < this.getRowCount(); i++) {
405            for (int j = 0; j < this.getColumnCount(); j++) {
406                //check for special, non string contents
407                if (this.getValueAt(i, j) == null) {
408                    columnStrings[j] = spaces.toString();
409                } else if (this.getValueAt(i, j) instanceof JComboBox) {
410                    columnStrings[j] = (String) ((JComboBox<?>) this.getValueAt(i, j)).getSelectedItem();
411                } else if (this.getValueAt(i, j) instanceof Boolean) {
412                    columnStrings[j] = (this.getValueAt(i, j)).toString();
413                } else {
414                    columnStrings[j] = (String) this.getValueAt(i, j);
415                }
416            }
417            printColumns(w, columnStrings, columnSize);
418            w.write(w.getCurrentLineNumber(), 0, w.getCurrentLineNumber(),
419                    (columnSize + 1) * this.getColumnCount());
420        }
421        w.close();
422    }
423
424    protected void printColumns(HardcopyWriter w, String columnStrings[], int columnSize) {
425        String columnString = "";
426        StringBuilder lineString = new StringBuilder("");
427        // create a base string the width of the column
428        StringBuilder spaces = new StringBuilder("");
429        for (int i = 0; i < columnSize; i++) {
430            spaces.append(" ");
431        }
432        // loop through each column
433        boolean complete = false;
434        while (!complete) {
435            complete = true;
436            for (int i = 0; i < columnStrings.length; i++) {
437                // if the column string is too wide cut it at word boundary (valid delimiters are space, - and _)
438                // use the intial part of the text,pad it with spaces and place the remainder back in the array
439                // for further processing on next line
440                // if column string isn't too wide, pad it to column width with spaces if needed
441                if (columnStrings[i].length() > columnSize) {
442                    boolean noWord = true;
443                    for (int k = columnSize; k >= 1; k--) {
444                        if (columnStrings[i].substring(k - 1, k).equals(" ")
445                                || columnStrings[i].substring(k - 1, k).equals("-")
446                                || columnStrings[i].substring(k - 1, k).equals("_")) {
447                            columnString = columnStrings[i].substring(0, k)
448                                    + spaces.substring(columnStrings[i].substring(0, k).length());
449                            columnStrings[i] = columnStrings[i].substring(k);
450                            noWord = false;
451                            complete = false;
452                            break;
453                        }
454                    }
455                    if (noWord) {
456                        columnString = columnStrings[i].substring(0, columnSize);
457                        columnStrings[i] = columnStrings[i].substring(columnSize);
458                        complete = false;
459                    }
460
461                } else {
462                    columnString = columnStrings[i] + spaces.substring(columnStrings[i].length());
463                    columnStrings[i] = "";
464                }
465                lineString.append(columnString).append(" ");
466            }
467            try {
468                w.write(lineString.toString());
469                //write vertical dividing lines
470                for (int i = 0; i < w.getCharactersPerLine(); i = i + columnSize + 1) {
471                    w.write(w.getCurrentLineNumber(), i, w.getCurrentLineNumber() + 1, i);
472                }
473                w.write("\n"); // NOI18N
474                lineString = new StringBuilder("");
475            } catch (IOException e) {
476                log.warn("error during printing:", e);
477            }
478        }
479    }
480
481    private final static Logger log = LoggerFactory.getLogger(EditorTableDataModel.class);
482
483}