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