001package jmri.jmrit.roster;
002
003import java.awt.*;
004import java.io.IOException;
005import java.util.ArrayList;
006import java.util.Enumeration;
007import java.util.Hashtable;
008import java.util.List;
009import javax.swing.BorderFactory;
010import javax.swing.BoxLayout;
011import javax.swing.ImageIcon;
012import javax.swing.JButton;
013import javax.swing.JCheckBox;
014import javax.swing.JFrame;
015import javax.swing.JLabel;
016import javax.swing.JPanel;
017import javax.swing.border.EmptyBorder;
018
019import jmri.InstanceManager;
020import jmri.jmrit.XmlFile;
021import jmri.jmrit.decoderdefn.DecoderFile;
022import jmri.jmrit.decoderdefn.DecoderIndexFile;
023import jmri.jmrit.symbolicprog.CvTableModel;
024import jmri.jmrit.symbolicprog.ResetTableModel;
025import jmri.jmrit.symbolicprog.VariableTableModel;
026import jmri.jmrit.symbolicprog.tabbedframe.PaneContainer;
027import jmri.jmrit.symbolicprog.tabbedframe.PaneProgFrame;
028import jmri.jmrit.symbolicprog.tabbedframe.PaneProgPane;
029import jmri.util.BusyGlassPane;
030import jmri.util.FileUtil;
031import jmri.util.JmriJFrame;
032import jmri.util.davidflanagan.HardcopyWriter;
033import org.jdom2.*;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037public class PrintRosterEntry implements PaneContainer {
038
039    RosterEntry _rosterEntry;
040
041    /**
042     * List of {@link jmri.jmrit.symbolicprog.tabbedframe.PaneProgPane} JPanels.
043     * Built up at line 150 or passed as argument paneList in line 188 via
044     * {link #PrintRosterEntry(RosterEntry, List, FunctionLabelPane, RosterMediaPane, JmriJFrame)}
045     */
046    List<JPanel> _paneList = new ArrayList<>();
047    FunctionLabelPane _flPane;
048    RosterMediaPane _rMPane;
049    JmriJFrame _parent;
050
051    /**
052     * Constructor for a Print roster item (programmer tabs) selection pane from an XML definition file.
053     * Includes &lt;pane&gt; elements (tabs) from Programmer (generic) as well as rosterEntry decoder.xml
054     * Called from RosterFrame &gt; PreviewAll context menu.
055     *
056     * @param rosterEntry Roster item, either as a selection or object
057     * @param parent window over which this dialog will be centered
058     * @param programmerFilename xml file name for programmer used in printing.
059     */
060    public PrintRosterEntry(RosterEntry rosterEntry, JmriJFrame parent, String programmerFilename) {
061        _rosterEntry = rosterEntry;
062        _flPane = new FunctionLabelPane(rosterEntry);
063        _rMPane = new RosterMediaPane(rosterEntry);
064        _parent = parent;
065        JLabel progStatus = new JLabel(Bundle.getMessage("StateIdle"));
066        ResetTableModel resetModel = new ResetTableModel(progStatus, null); // no programmer
067
068        log.debug("Try PrintRosterEntry {} from file {}", _rosterEntry.getDisplayName(), programmerFilename);
069        XmlFile pf = new XmlFile() {
070        };
071        Element programmerRoot;
072        Element programmerBase; // base of programmer file pane elements
073
074        try {
075            programmerRoot = pf.rootFromName(programmerFilename);
076            if (programmerRoot == null) {
077                log.error("Programmer file name incorrect {}", programmerFilename);
078                return;
079            }
080            if ((programmerBase = programmerRoot.getChild("programmer")) == null) {
081                log.error("xml file top element is not 'programmer'");
082                return;
083            }
084            log.debug("Success: xml file top element is 'programmer'");
085        } catch (JDOMException | java.io.IOException e) {
086            log.error("exception reading programmer file {}", programmerFilename, e);
087            return;
088        }
089
090        CvTableModel cvModel = new CvTableModel(progStatus, null); // no programmer
091
092        VariableTableModel variableModel = new VariableTableModel(progStatus, new String[] {"Name", "Value"}, cvModel); // NOI18N
093        
094        String decoderModel = _rosterEntry.getDecoderModel();
095        String decoderFamily = _rosterEntry.getDecoderFamily();
096
097        log.debug("selected loco uses decoder {} {}", decoderFamily, decoderModel);
098        // locate a decoder like that
099        List<DecoderFile> l = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, decoderFamily, null, null, null, decoderModel);
100        log.debug("found {} matches", l.size());
101        if (l.isEmpty()) {
102            log.debug("Loco uses {} {} decoder, but no such decoder defined", decoderFamily, decoderModel);
103            // fall back to use just the decoder name, not family
104            l = InstanceManager.getDefault(DecoderIndexFile.class).matchingDecoderList(null, null, null, null, null, decoderModel);
105            log.debug("found {} matches without family key", l.size());
106        }
107        DecoderFile decoderFile = null;
108        if (l.size() > 0) {
109            decoderFile = l.get(0);
110        } else {
111            if (decoderModel.equals("")) {
112                log.debug("blank decoderModel requested, so nothing loaded");
113            } else {
114                log.warn("no matching \"{}\" decoder found for loco, no decoder info loaded", decoderModel);
115            }
116        }
117
118        if (decoderFile == null) {
119            log.warn("no decoder file found for this loco");
120            return;
121        }
122        // save the pointer to the model element to check for include/exclude before adding to paneList
123        Element modelElem = decoderFile.getModelElement();
124
125        Element decoderRoot;
126        log.debug("Try to read decoder root from {} {}", DecoderFile.fileLocation, decoderFile.getFileName());
127
128        try {
129            decoderRoot = decoderFile.rootFromName(DecoderFile.fileLocation + decoderFile.getFileName());
130            if ((decoderRoot.getChild("decoder")) == null) {
131                log.error("xml file top element is not 'decoder'");
132                return;
133            }
134        } catch (org.jdom2.JDOMException exj) {
135            log.error("could not parse {}: {}", decoderFile.getFileName(), exj.getMessage());
136            return;
137        } catch (java.io.IOException exj) {
138            log.error("could not read {}: {}", decoderFile.getFileName(), exj.getMessage());
139            return;
140        }
141
142        // load defaults
143        decoderFile.loadVariableModel(decoderRoot.getChild("decoder"), variableModel);
144        decoderFile.loadResetModel(decoderRoot.getChild("decoder"), resetModel);
145        
146        // load the specific contents for this entry from rosterEntry file
147        rosterEntry.readFile();
148        rosterEntry.loadCvModel(variableModel, cvModel);
149
150        // add pane names from programmer
151        List<Element> rawPaneList = programmerBase.getChildren("pane");
152        log.debug("rawPaneList P size = {}", rawPaneList.size());
153        for (Element elPane : rawPaneList) {
154            // load each pane to store in _paneList and fetch its name element (i18n) to show in Select items pane
155            Element _name = elPane.getChild("name"); // multiple languages
156            // There is no name attribute of pane in Basic.xml nor (for the Comprehensive programmer) in include.parts.Basic.xml
157            // Instead, it's a separate element inside programmer.pane, fixed 4.7.2
158            String name = "Tab name"; // temporary name
159            if (_name != null) {
160                name = _name.getText(); // NOI18N
161                log.debug("Tab '{}' added from Programmer", name);
162            } else {
163                log.debug("Did not find name element in pane");
164            }
165            // include/exclude check N/A for prag panes
166            PaneProgPane p = new PaneProgPane(this, name, elPane, cvModel, variableModel, modelElem, _rosterEntry);
167            _paneList.add(p);
168        }
169
170        // compare to PaneProgFrame#loadProgrammerFile(pRosterEntry)
171        // add pane names from programmer
172        rawPaneList = decoderRoot.getChildren("pane");
173        log.debug("rawPaneList D size = {}", rawPaneList.size());
174        for (Element elPane : rawPaneList) {
175            // load each pane to store in _paneList and fetch its name element (i18n) to show in Select items pane
176            Element _name = elPane.getChild("name"); // multiple languages
177            // There is no name attribute of pane in Basic.xml nor (for the Comprehensive programmer) in include.parts.Basic.xml
178            // Instead, it's a separate element inside programmer.pane, fixed 4.7.2
179            String name = "Tab name"; // temporary name NOI18N
180            if (_name != null) {
181                name = _name.getText(); // NOI18N
182                log.debug("Tab '{}' added from Decoder", name);
183            } else {
184                log.debug("Did not find name element in pane");
185            }
186            PaneProgPane p;
187            if (PaneProgFrame.isIncludedFE(elPane, modelElem, _rosterEntry, "", "")) {
188                 p = new PaneProgPane(this, name, elPane, cvModel, variableModel, modelElem, _rosterEntry);
189                _paneList.add(p); // possible duplicates with prog pane titles handled by list
190            }
191        }
192        // check for empty panes and I18N happens in #printPanes(boolean)
193    }
194
195    @Override
196    public BusyGlassPane getBusyGlassPane() {
197        return null;
198    }
199
200    @Override
201    public void prepGlassPane(javax.swing.AbstractButton activeButton) {
202    }
203
204    @Override
205    public void enableButtons(boolean enable) {
206    }
207
208    @Override
209    public void paneFinished() {
210    }
211
212    @Override
213    public boolean isBusy() {
214        return false;
215    }
216
217    /**
218     * Configure variable fields and create a PrintRosterEntry instance while doing so.
219     * Includes all (visible) Roster Entry programmer &lt;pane&gt; elements (tabs).
220     *
221     * @param rosterEntry an item in the Roster
222     * @param paneList list of programmer tabs, including all properties
223     * @param flPane extra pane w/checkbox to select printing of "Function List"
224     * @param rMPane pane containing roster media (image)
225     * @param parent window over which this dialog will be centered
226     */
227    public PrintRosterEntry(RosterEntry rosterEntry, List<JPanel> paneList, FunctionLabelPane flPane, RosterMediaPane rMPane, JmriJFrame parent) {
228        _rosterEntry = rosterEntry;
229        _paneList = paneList;
230        _flPane = flPane;
231        _rMPane = rMPane;
232        _parent = parent;
233        log.debug("New PrintRosterEntry including a paneList of size {}", paneList.size());
234    }
235
236    /**
237     * Write a series of 'pages' to graphic output using HardcopyWriter.
238     *
239     * @param preview true if output should go to the Preview panel, false to output to a printer
240     */
241    public void doPrintPanes(boolean preview) {
242        HardcopyWriter w;
243        try {
244            w = new HardcopyWriter(_parent, _rosterEntry.getId(), 10, .8, .5, .5, .5, preview);
245        } catch (HardcopyWriter.PrintCanceledException ex) {
246            log.debug("Print cancelled");
247            return;
248        }
249        printInfoSection(w);
250
251        if (_flPane.includeInPrint()) {
252            _flPane.printPane(w);
253        }
254        log.debug("List size length: {}", _paneList.size());
255        for (int i = 0; i < _paneList.size(); i++) {
256            log.debug("start printing page {}", i + 1);
257            PaneProgPane pane = (PaneProgPane) _paneList.get(i);
258            if (pane.includeInPrint()) {
259                pane.printPane(w); // takes care of all I18N
260            }
261        }
262        w.write(w.getCurrentLineNumber(), 0, w.getCurrentLineNumber(), w.getCharactersPerLine() + 1);
263        w.close();
264    }
265
266    /**
267     * Create and display a pane to the user to select which Programmer tabs to include in printout.
268     *
269     * @param preview true if output should go to a Preview pane on screen, false to output to a printer (dialog)
270     */
271    public void printPanes(final boolean preview) {
272        final JFrame frame = new JFrame(Bundle.getMessage("TitleSelectItemsToPrint"));
273        JPanel p1 = new JPanel();
274        p1.setBorder(new EmptyBorder(5, 5, 5, 5));
275        p1.setLayout(new BoxLayout(p1, BoxLayout.PAGE_AXIS));
276
277        JPanel instruct = new JPanel();
278        instruct.setLayout(new BoxLayout(instruct, BoxLayout.PAGE_AXIS));
279        JLabel l1 = new JLabel(Bundle.getMessage("LabelSelectLine1"));
280        instruct.add(l1);
281        l1 = new JLabel(Bundle.getMessage("LabelSelectLine2"));
282        instruct.add(l1);
283        p1.add(instruct);
284
285        JPanel select = new JPanel();
286        select.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("ItemsLabel")));
287        // add checkboxes for all items
288        final Hashtable<JCheckBox, PaneProgPane> printList = new Hashtable<>();
289        select.setLayout(new BoxLayout(select, BoxLayout.PAGE_AXIS));
290        final JCheckBox funct = new JCheckBox(Bundle.getMessage("LabelFunctionList"));
291        funct.addActionListener(evt -> _flPane.includeInPrint(funct.isSelected()));
292        _flPane.includeInPrint(false);
293        select.add(funct);
294
295        log.debug("_paneList size length: {}", _paneList.size());
296        for (JPanel jPanel : _paneList) {
297            log.debug("printPanes === checking tab {}...", jPanel.getName());
298            if (jPanel instanceof PaneProgPane && !((PaneProgPane) jPanel).isEmpty()) {
299                // add a checkbox to the Preview All pane for each tab (unless empty)
300                // skip tab if empty (won't show up on printout anyway)
301                log.debug("tab {} not empty, adding", jPanel.getName());
302                final PaneProgPane pane = (PaneProgPane) jPanel;
303                pane.includeInPrint(false);
304                final JCheckBox item = new JCheckBox(jPanel.getName());
305                // Tab names _paneList.get(i).getName() show up when called from RosterFrame
306                // (are entered in line 147)
307                printList.put(item, pane);
308                item.addActionListener(evt -> pane.includeInPrint(item.isSelected()));
309                select.add(item);
310            }
311        }
312        p1.add(select);
313
314        // Add "Select All" checkbox below titled set of item boxes
315        JPanel selectAllBox = new JPanel();
316        final JCheckBox selectAll = new JCheckBox(Bundle.getMessage("SelectAll"));
317        selectAll.addActionListener(evt -> {
318            _flPane.includeInPrint(selectAll.isSelected());
319            funct.setSelected(selectAll.isSelected());
320            Enumeration<JCheckBox> en = printList.keys();
321            while (en.hasMoreElements()) {
322                JCheckBox check = en.nextElement();
323                printList.get(check).includeInPrint(selectAll.isSelected());
324                check.setSelected(selectAll.isSelected());
325            }
326        });
327        selectAllBox.add(selectAll);
328        p1.add(selectAllBox);
329
330        JButton cancel = new JButton(Bundle.getMessage("ButtonCancel"));
331        JButton ok = new JButton(Bundle.getMessage("ButtonOK"));
332        cancel.addActionListener(evt -> frame.dispose());
333        ok.addActionListener(evt -> {
334            doPrintPanes(preview);
335            frame.dispose();
336        });
337        JPanel buttons = new JPanel();
338        buttons.add(cancel);
339        buttons.add(ok);
340        p1.add(buttons);
341
342        frame.add(p1);
343        frame.pack();
344        frame.setVisible(true);
345    }
346
347    /**
348     * Write the page header to graphic output, using HardcopyWriter w.
349     * <p>
350     * Includes the DecoderPro logo image at top right.
351     *
352     * @param w the active HardcopyWriter instance to be used
353     */
354    public void printInfoSection(HardcopyWriter w) {
355        ImageIcon icon = new ImageIcon(FileUtil.findURL("resources/decoderpro.gif", FileUtil.Location.INSTALLED));
356        // we use an ImageIcon because it's guaranteed to have been loaded when ctor is complete
357        w.write(icon.getImage(), new JLabel(icon));
358        w.setFontStyle(Font.BOLD);
359        // add a number of blank lines
360        int height = icon.getImage().getHeight(null);
361        int blanks = (height - w.getLineAscent()) / w.getLineHeight();
362
363        try {
364            for (int i = 0; i < blanks; i++) {
365                String s = "\n";
366                w.write(s, 0, s.length());
367            }
368        } catch (IOException e) {
369            log.warn("error during printing: ", e);
370        }
371        _rosterEntry.printEntry(w);
372        w.setFontStyle(Font.PLAIN);
373    }
374
375    private final static Logger log = LoggerFactory.getLogger(PrintRosterEntry.class);
376
377}