001/**
002 * SelectCasesDialog -- Dialog allowing user to select a subset of a list of cases.
003 *
004 * Copyright (C) 2003-2016, by Joseph A. Huwaldt. All rights reserved.
005 *
006 * This library is free software; you can redistribute it and/or modify it under the terms
007 * of the GNU Lesser General Public License as published by the Free Software Foundation;
008 * either version 2 of the License, or (at your option) any later version.
009 *
010 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
011 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
012 * PARTICULAR PURPOSE. See the GNU Library General Public License for more details.
013 *
014 * You should have received a copy of the GNU Lesser General Public License along with
015 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place -
016 * Suite 330, Boston, MA 02111-1307, USA. Or visit: http://www.gnu.org/licenses/lgpl.html
017 */
018package jahuwaldt.js.datareader;
019
020import jahuwaldt.swing.EscapeJDialog;
021import java.awt.BorderLayout;
022import java.awt.Component;
023import java.awt.Dimension;
024import java.awt.Frame;
025import java.awt.event.*;
026import java.util.ArrayList;
027import java.util.List;
028import java.util.ResourceBundle;
029import javax.swing.*;
030import javax.swing.event.DocumentEvent;
031import javax.swing.event.DocumentListener;
032import javax.swing.table.AbstractTableModel;
033import javax.swing.table.TableCellRenderer;
034import javax.swing.table.TableColumn;
035import javax.swing.table.TableRowSorter;
036
037/**
038 * A modal dialog the allows the user to select a subset of all the available cases in a
039 * list of data sets.
040 *
041 * <p> Modified by: Joseph A. Huwaldt </p>
042 *
043 * @author Joseph A. Huwaldt, Date: March 25, 2003
044 * @version October 29, 2016
045 */
046@SuppressWarnings("serial")
047public class SelectCasesDialog extends EscapeJDialog implements ActionListener {
048
049    /**
050     * The resource bundle for this package.
051     */
052    public static final ResourceBundle RESOURCES = DataReader.RESOURCES;
053    
054    //  A list of column names to display above the table.
055    private static final String[] columnNames = {
056        padRight(RESOURCES.getString("dataSetColName"), 20),
057        padRight(RESOURCES.getString("dataCaseColName"), 20)};
058
059    //  A list of lists of strings.
060    private List<List<Object>> rowData = new ArrayList<>();
061
062    /**
063     * The full list of DataElements displayed to the user.
064     */
065    private List<DataElement> elements = new ArrayList<>();
066
067    /**
068     * The total number of input DataSets.
069     */
070    private int numSets = 0;
071
072    /**
073     * The JTable displaying the contents of the "elements" list that was input.
074     */
075    private JTable inputTable;
076
077    /**
078     * The row sorter used on the input table.
079     */
080    private TableRowSorter<AbstractTableModel> inputSorter;
081
082    /**
083     * The filter text field for filtering the input table.
084     */
085    private final JTextField filterText;
086
087    /**
088     * The JTable displaying the elements that the user has selected. This table is only
089     * used if multiple selections are possible.
090     */
091    private JTable outputTable;
092
093    //  A list of the cases that were selected by the user.
094    private DataSet selectedCases;
095
096    /**
097     * Flag indicating if the user is allowed to select multiple items or not.
098     */
099    private boolean singleSelection = false;
100
101    //  A list of lists of selected elements.
102    private ArrayList<ArrayList<Object>> selRowData = new ArrayList<>();
103    private ArrayList<DataElement> selElements = new ArrayList<>();
104
105    /**
106     * Construct a model dialog that allows the user to select a subset of all the
107     * available cases in a list of data sets.
108     *
109     * @param parent          A reference to the parent frame that this dialog belongs to.
110     * @param title           The title for the dialog window.
111     * @param message         A message to display telling the user what to do.
112     * @param data            A list of DataSet objects containing the runs to be chosen
113     *                        from.
114     * @param singleSelection Set to true to allow only a single element to be selected,
115     *                        false for multiple elements.
116     * @param showArraySizes  Append the size of ArrayParam objects to the parameter names
117     *                        if set to true. Show only the parameter names if set to
118     *                        false.
119     */
120    public SelectCasesDialog(Frame parent, String title, String message, List<? extends DataElementList<?>> data,
121            boolean singleSelection, boolean showArraySizes) {
122        super(parent, title, true);
123
124        //  Copy all the cases from the input data sets into the list.
125        numSets = data.size();
126        for (DataElementList<?> list : data) {
127
128            for (DataElement element : list) {
129                elements.add(element);
130                ArrayList<Object> colData = new ArrayList<>();
131                colData.add(list.getName().toString());
132                if (element instanceof ArrayParam) {
133                    String str = element.toString();
134                    if (showArraySizes)
135                        str += "  (" + ((ArrayParam)element).size() + ")";
136                    colData.add(str);
137                } else
138                    colData.add(element);
139                rowData.add(colData);
140            }
141        }
142
143        //  If there is only one row, simplify the dialog.
144        if (rowData.size() == 1)
145            singleSelection = true;
146        this.singleSelection = singleSelection;
147
148        //  Layout the dialog window.
149        JPanel cp = new JPanel(new BorderLayout());
150        this.getContentPane().add(cp);
151        cp.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
152
153        //  Add instructions at the top.
154        Box topPanel = Box.createHorizontalBox();
155        cp.add(topPanel, BorderLayout.NORTH);
156
157        JLabel label = new JLabel(message, SwingConstants.LEADING);
158        label.setBorder(BorderFactory.createEmptyBorder(6, 0, 6, 0));
159        topPanel.add(label);
160        topPanel.add(Box.createHorizontalStrut(10));
161        topPanel.add(Box.createGlue());
162
163        //  Create a panel to display in the center.
164        Box centerPanel = Box.createVerticalBox();
165        cp.add(centerPanel, BorderLayout.CENTER);
166        Box tablePanel = Box.createHorizontalBox();
167        centerPanel.add(tablePanel);
168
169        //  Create a table that displays the cases in all the input data sets.
170        AbstractTableModel model = new AbstractTableModel() {
171            @Override
172            public String getColumnName(int col) {
173                if (numSets == 1)
174                    col = 1;
175                return SelectCasesDialog.this.getColumnName(col);
176            }
177
178            @Override
179            public int getRowCount() {
180                return rowData.size();
181            }
182
183            @Override
184            public int getColumnCount() {
185                if (numSets == 1)
186                    return 1;
187                return columnNames.length;
188            }
189
190            @Override
191            public Object getValueAt(int row, int col) {
192                if (numSets == 1)
193                    col = 1;
194                return ((ArrayList<?>)rowData.get(row)).get(col);
195            }
196
197            @Override
198            public boolean isCellEditable(int row, int col) {
199                return false;
200            }
201        };
202        inputTable = new JTable(model);
203        inputSorter = new TableRowSorter<>(model);
204        inputTable.setRowSorter(inputSorter);
205        inputTable.setCellSelectionEnabled(false);
206        inputTable.setColumnSelectionAllowed(false);
207        inputTable.setRowSelectionAllowed(true);
208        inputTable.clearSelection();
209        inputTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);   //  Show horiz. scroll bars.
210        inputTable.addMouseListener(new MouseAdapter() {
211            @Override
212            public void mouseClicked(MouseEvent e) {
213                if (e.getClickCount() == 2) {
214                    if (!SelectCasesDialog.this.singleSelection)
215                        doAdd();
216                    else
217                        SelectCasesDialog.this.actionPerformed(null);
218                }
219            }
220        });
221        if (singleSelection)
222            inputTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
223
224        //  If only a single row exists, preselect it.
225        if (rowData.size() == 1)
226            inputTable.selectAll();
227        else {
228            ListSelectionModel selModel = inputTable.getSelectionModel();
229            selModel.setSelectionInterval(0, 0);
230        }
231
232        //  Initialize the column widths to show all the data.
233        initColumnWidths(inputTable);
234
235        //  Determine the table preferred width.
236        int width = 0;
237        int numColumns = inputTable.getColumnCount();
238        for (int i = 0; i < numColumns; ++i) {
239            TableColumn column = inputTable.getColumnModel().getColumn(i);
240            width += column.getPreferredWidth();
241        }
242        inputTable.setPreferredScrollableViewportSize(new Dimension(width, 300));
243
244        //  Put the list in a scroll pane so that we have scroll bars.
245        JScrollPane scrollPane = new JScrollPane(inputTable);
246        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
247        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
248
249        //  Add the list to the panel.
250        tablePanel.add(scrollPane);
251
252        if (!singleSelection) {
253
254            //  Create a button panel for selection buttons.
255            tablePanel.add(Box.createHorizontalStrut(10));
256            tablePanel.add(createButtonPanel());
257
258            //  Create an output table that displays selected tables.
259            outputTable = new JTable(new AbstractTableModel() {
260                @Override
261                public String getColumnName(int col) {
262                    if (numSets == 1)
263                        col = 1;
264                    return SelectCasesDialog.this.getColumnName(col);
265                }
266
267                @Override
268                public int getRowCount() {
269                    return selRowData.size();
270                }
271
272                @Override
273                public int getColumnCount() {
274                    if (numSets == 1)
275                        return 1;
276                    return columnNames.length;
277                }
278
279                @Override
280                public Object getValueAt(int row, int col) {
281                    if (numSets == 1)
282                        col = 1;
283                    return ((ArrayList<?>)selRowData.get(row)).get(col);
284                }
285
286                @Override
287                public boolean isCellEditable(int row, int col) {
288                    return false;
289                }
290            });
291            outputTable.setCellSelectionEnabled(false);
292            outputTable.setColumnSelectionAllowed(false);
293            outputTable.setRowSelectionAllowed(true);
294            outputTable.clearSelection();
295            outputTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);  //  Show hoirz. scroll bars.
296            outputTable.addMouseListener(new MouseAdapter() {
297                @Override
298                public void mouseClicked(MouseEvent e) {
299                    if (e.getClickCount() == 2) {
300                        doRemove();
301                    }
302                }
303            });
304
305            //  Initialize the column widths to show all the data.
306            initColumnWidths(outputTable);
307
308            //  Determine the table preferred width.
309            width = 0;
310            numColumns = outputTable.getColumnCount();
311            for (int i = 0; i < numColumns; ++i) {
312                TableColumn column = outputTable.getColumnModel().getColumn(i);
313                width += column.getPreferredWidth();
314            }
315            outputTable.setPreferredScrollableViewportSize(new Dimension(width, 300));
316
317            //  Put the list in a scroll pane so that we have scroll bars.
318            scrollPane = new JScrollPane(outputTable);
319            scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
320            scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
321
322            //  Add the list to the panel.
323            tablePanel.add(Box.createHorizontalStrut(10));
324            tablePanel.add(scrollPane);
325        }
326
327        //  Define the filter text field.
328        centerPanel.add(Box.createVerticalStrut(10));
329        Box form = Box.createHorizontalBox();
330        JLabel l1 = new JLabel(RESOURCES.getString("filterTextLabel"), SwingConstants.TRAILING);
331        form.add(l1);
332        filterText = new JTextField();
333        //  Whenever filterText changes, invoke newInputFilter.
334        filterText.getDocument().addDocumentListener(new DocumentListener() {
335            @Override
336            public void changedUpdate(DocumentEvent e) {
337                newInputFilter();
338            }
339
340            @Override
341            public void insertUpdate(DocumentEvent e) {
342                newInputFilter();
343            }
344
345            @Override
346            public void removeUpdate(DocumentEvent e) {
347                newInputFilter();
348            }
349        });
350        l1.setLabelFor(filterText);
351        form.add(filterText);
352        centerPanel.add(form);
353        centerPanel.add(Box.createVerticalStrut(10));
354
355        //  Define OK and Cancel buttons.
356        Box box = Box.createHorizontalBox();
357        box.add(Box.createGlue());
358
359        JButton cancelBtn = new JButton(RESOURCES.getString("cancelBtnText"));
360        cancelBtn.addActionListener(new ActionListener() {
361            @Override
362            public void actionPerformed(ActionEvent e) {
363                performEscapeAction(null);
364            }
365        });
366        box.add(cancelBtn);
367
368        box.add(Box.createHorizontalStrut(40));
369
370        //  If multiple selections allowed, change "OK" button text to "Done".
371        String okMessage = RESOURCES.getString("doneBtnText");
372        if (singleSelection)
373            okMessage = RESOURCES.getString("selectBtnText");
374        JButton okayBtn = new JButton(okMessage);
375        okayBtn.addActionListener(new ActionListener() {
376            @Override
377            public void actionPerformed(ActionEvent e) {
378                SelectCasesDialog.this.actionPerformed(null);
379            }
380        });
381        this.getRootPane().setDefaultButton(okayBtn);
382        box.add(okayBtn);
383
384        box.add(Box.createHorizontalStrut(20));
385        cp.add(box, BorderLayout.SOUTH);
386
387        this.pack();
388    }
389
390    /**
391     * Pad a string by adding spaces on the right side until the given length is reached.
392     *
393     * @param s The string to be padded.
394     * @param n The desired length of the string.
395     * @return The input string with spaces added to the right until the given length is
396     *         reached
397     */
398    protected static String padRight(String s, int n) {
399        return String.format("%1$-" + n + "s", s);
400    }
401    
402    /**
403     * This method returns a single DataSet object that contains all the cases that were
404     * selected in this dialog. If the user has not clicked on the "OK" button, null is
405     * returned.
406     *
407     * @return A single DataSet object that contains all the cases that were selected in
408     *         this dialog.
409     */
410    public DataSet getSelected() {
411        return selectedCases;
412    }
413
414    /**
415     * Method that creates a button panel containing selection buttons.
416     */
417    private Box createButtonPanel() {
418
419        //  Create selection buttons to run down the center.
420        Box btnBox = Box.createVerticalBox();
421        btnBox.add(Box.createGlue());
422
423        //  Create an "Add" button.
424        JButton button = new JButton(RESOURCES.getString("addBtnText"));
425        button.setAlignmentX(JButton.CENTER_ALIGNMENT);
426        button.addActionListener(new ActionListener() {
427            @Override
428            public void actionPerformed(ActionEvent e) {
429                doAdd();
430            }
431        });
432        btnBox.add(button);
433        btnBox.add(Box.createVerticalStrut(10));
434
435        //  Create a "Remove" button.
436        button = new JButton(RESOURCES.getString("removeBtnText"));
437        button.setAlignmentX(JButton.CENTER_ALIGNMENT);
438        button.addActionListener(new ActionListener() {
439            @Override
440            public void actionPerformed(ActionEvent e) {
441                doRemove();
442            }
443        });
444        btnBox.add(button);
445        btnBox.add(Box.createVerticalStrut(10));
446
447        //  Create an "Add All" button.
448        button = new JButton(RESOURCES.getString("addAllBtnText"));
449        button.setAlignmentX(JButton.CENTER_ALIGNMENT);
450        button.addActionListener(new ActionListener() {
451            @Override
452            public void actionPerformed(ActionEvent e) {
453                doAddAll();
454            }
455        });
456        btnBox.add(button);
457        btnBox.add(Box.createVerticalStrut(10));
458
459        //  Create a "Remove All" button.
460        button = new JButton(RESOURCES.getString("removeAllBtnText"));
461        button.setAlignmentX(JButton.CENTER_ALIGNMENT);
462        button.addActionListener(new ActionListener() {
463            @Override
464            public void actionPerformed(ActionEvent e) {
465                doRemoveAll();
466            }
467        });
468        btnBox.add(button);
469        btnBox.add(Box.createGlue());
470
471        return btnBox;
472    }
473
474    /**
475     * This method is called when the user clicks on the "Add" button.
476     */
477    private void doAdd() {
478
479        //  Copy the selected elements to the output table.
480        int[] selected = inputTable.getSelectedRows();
481        int numSelected = selected.length;
482
483        for (int i = 0; i < numSelected; ++i) {
484            int modelRow = inputTable.convertRowIndexToModel(selected[i]);  //  Convert from View to Model indexes.
485            DataElement element = elements.get(modelRow);
486            selElements.add(element);
487            ArrayList<Object> colData = new ArrayList<>();
488            colData.add(inputTable.getValueAt(selected[i], 0));
489            colData.add(inputTable.getValueAt(selected[i], 1));
490            selRowData.add(colData);
491        }
492
493        if (numSelected > 0)
494            //  Update the output table's UI.
495            outputTable.revalidate();
496
497    }
498
499    /**
500     * This method is called when the user clicks on the "Add All" button.
501     */
502    private void doAddAll() {
503
504        int numRows = inputTable.getRowCount();
505        for (int i = 0; i < numRows; ++i) {
506            int modelRow = inputTable.convertRowIndexToModel(i);    //  Convert from View to Model indexes.
507            DataElement element = elements.get(modelRow);
508            selElements.add(element);
509            ArrayList<Object> colData = new ArrayList<>();
510            colData.add(inputTable.getValueAt(i, 0));
511            colData.add(inputTable.getValueAt(i, 1));
512            selRowData.add(colData);
513        }
514
515        outputTable.revalidate();
516
517    }
518
519    /**
520     * This method is called when the user clicks on the "Remove" button.
521     */
522    private void doRemove() {
523
524        //  Get a list of all the selected rows in the table.
525        int[] selected = outputTable.getSelectedRows();
526        int numSelected = selected.length;
527
528        for (int i = 0; i < numSelected; ++i) {
529            selElements.remove(selected[i]);
530            selRowData.remove(selected[i]);
531        }
532
533        outputTable.revalidate();
534    }
535
536    /**
537     * This method is called when the user clicks on the "Remove All" button.
538     */
539    private void doRemoveAll() {
540        selElements.clear();
541        selRowData.clear();
542
543        outputTable.revalidate();
544    }
545
546    /**
547     * Method that returns a list of selected elements.
548     * 
549     * @return A list of selected elements.
550     */
551    protected DataElement[] getSelectedElements() {
552
553        DataElement[] selList;
554
555        if (singleSelection) {
556            //  Get a list of all the selected rows in the input table.
557            int[] selected = inputTable.getSelectedRows();
558            int numSelected = selected.length;
559
560            selList = new DataElement[numSelected];
561            for (int i = 0; i < numSelected; ++i) {
562                int modelRow = inputTable.convertRowIndexToModel(selected[i]);  //  Convert from View to Model indexes.
563                selList[i] = elements.get(modelRow);
564            }
565        } else {
566            //  Return a list of all the elements in the output table.
567            int numRows = outputTable.getRowCount();
568            selList = new DataElement[numRows];
569            for (int i = 0; i < numRows; ++i) {
570                selList[i] = selElements.get(i);
571            }
572
573        }
574
575        return selList;
576    }
577
578    /**
579     * This method is called when the user clicks on the "OK Button".
580     * 
581     * @param evt The event that caused this method to be called (ignored).
582     */
583    @Override
584    public void actionPerformed(ActionEvent evt) {
585
586        //  Get a list of all the selected rows in the table.
587        DataElement[] selected = getSelectedElements();
588        int numSelected = selected.length;
589
590        //  Add selected cases to the selected data set.
591        selectedCases = DataSet.newInstance(RESOURCES.getString("selectedSetName"));
592        for (int i = 0; i < numSelected; ++i) {
593            selectedCases.add((DataCase)selected[i]);
594        }
595
596        //  Hide the window.
597        this.setVisible(false);
598    }
599
600    /**
601     * Response to ENTER key pressed goes here. This has the same result as clicking on
602     * the OK or Select button.
603     */
604    @Override
605    protected void performEnterAction(KeyEvent e) {
606        actionPerformed(null);
607    }
608
609    /**
610     * Method that returns the name to place above the specified column.
611     *
612     * @param col The column to return the name for.
613     * @return The name to use for the specified column.
614     */
615    protected String getColumnName(int col) {
616        return columnNames[col];
617    }
618
619    /**
620     * Method that resets the minimum column widths.
621     */
622    private static void initColumnWidths(JTable table) {
623        int numColumns = table.getColumnCount();
624        for (int i = 0; i < numColumns; ++i) {
625            //  Get each column.
626            TableColumn column = table.getColumnModel().getColumn(i);
627
628            //  Determine the header width.
629            int headerWidth = 0;
630            try {
631                TableCellRenderer renderer = column.getHeaderRenderer();
632                if (renderer == null)
633                    renderer = table.getTableHeader().getDefaultRenderer();
634
635                Component comp = renderer.
636                        getTableCellRendererComponent(
637                                null, column.getHeaderValue(),
638                                false, false, 0, 0);
639                headerWidth = comp.getPreferredSize().width;
640            } catch (NullPointerException e) {
641                e.printStackTrace();
642            }
643
644            //  Set the minimum width to the larger of the header width
645            //  and 150 pixels.
646            column.setMinWidth(Math.max(150, headerWidth) + 10);
647        }
648    }
649
650    /**
651     * Method called whenever the filter text is edited to change the input table filter.
652     */
653    private void newInputFilter() {
654        RowFilter<AbstractTableModel, Object> rf;
655        //  If current expression doesn't parse, don't update.
656        try {
657            rf = RowFilter.regexFilter(filterText.getText(), 0);
658        } catch (java.util.regex.PatternSyntaxException e) {
659            return;
660        }
661        inputSorter.setRowFilter(rf);
662
663        //  Make sure something in the table is selected.
664        if (inputTable.getSelectedRow() < 0) {
665            ListSelectionModel selModel = inputTable.getSelectionModel();
666            selModel.setSelectionInterval(0, 0);
667        }
668    }
669}