001/* A top-level dialog window for displaying search results.
002
003 Copyright (c) 1998-2014 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026 */
027package ptolemy.vergil.basic;
028
029import java.awt.BorderLayout;
030import java.awt.Dimension;
031import java.awt.Frame;
032import java.awt.event.KeyAdapter;
033import java.awt.event.KeyEvent;
034import java.awt.event.MouseAdapter;
035import java.awt.event.MouseEvent;
036import java.net.URL;
037import java.util.Comparator;
038import java.util.HashSet;
039import java.util.Iterator;
040import java.util.Locale;
041import java.util.Set;
042import java.util.SortedSet;
043import java.util.TreeSet;
044import java.util.regex.Matcher;
045import java.util.regex.Pattern;
046import java.util.regex.PatternSyntaxException;
047
048import javax.swing.JButton;
049import javax.swing.JPanel;
050import javax.swing.JScrollPane;
051import javax.swing.JTable;
052import javax.swing.ListSelectionModel;
053import javax.swing.event.ChangeEvent;
054import javax.swing.event.ListSelectionEvent;
055import javax.swing.event.ListSelectionListener;
056import javax.swing.table.AbstractTableModel;
057import javax.swing.table.DefaultTableCellRenderer;
058
059import ptolemy.actor.gui.ColorAttribute;
060import ptolemy.actor.gui.Configuration;
061import ptolemy.actor.gui.DialogTableau;
062import ptolemy.actor.gui.PtolemyDialog;
063import ptolemy.gui.Query;
064import ptolemy.gui.QueryListener;
065import ptolemy.kernel.Entity;
066import ptolemy.kernel.util.Attribute;
067import ptolemy.kernel.util.ChangeRequest;
068import ptolemy.kernel.util.IllegalActionException;
069import ptolemy.kernel.util.NameDuplicationException;
070import ptolemy.kernel.util.NamedObj;
071import ptolemy.kernel.util.Settable;
072import ptolemy.util.MessageHandler;
073
074///////////////////////////////////////////////////////////////////
075//// SearchResultsDialog
076
077/**
078 This class is a non-modal dialog for displaying search results.
079
080 @author Edward A. Lee
081 @version $Id$
082 @since Ptolemy II 10.0
083 @Pt.ProposedRating Yellow (eal)
084 @Pt.AcceptedRating Red (eal)
085 */
086@SuppressWarnings("serial")
087public class SearchResultsDialog extends PtolemyDialog
088        implements ListSelectionListener, QueryListener {
089
090    /** Construct a dialog for search results.
091     *  @param tableau The DialogTableau.
092     *  @param owner The frame that, per the user, is generating the dialog.
093     *  @param target The object on which the search is to be done.
094     *  @param configuration The configuration to use to open the help screen
095     *   (or null if help is not supported).
096     */
097    public SearchResultsDialog(DialogTableau tableau, Frame owner,
098            Entity target, Configuration configuration) {
099        this("Find in " + target.getName(), tableau, owner, target,
100                configuration);
101    }
102
103    /** Construct a dialog for search results.
104     *  @param title The title of the dialog
105     *  @param tableau The DialogTableau.
106     *  @param owner The frame that, per the user, is generating the dialog.
107     *  @param target The object on which the search is to be done.
108     *  @param configuration The configuration to use to open the help screen
109     *   (or null if help is not supported).
110     */
111    public SearchResultsDialog(String title, DialogTableau tableau, Frame owner,
112            Entity target, Configuration configuration) {
113        super(title, tableau, owner, target, configuration);
114
115        _owner = owner;
116        _target = target;
117
118        _query = new Query();
119
120        _initializeQuery();
121
122        getContentPane().add(_query, BorderLayout.NORTH);
123        _query.addQueryListener(this);
124
125        _resultsTableModel = new ResultsTableModel();
126        _resultsTable = new JTable(_resultsTableModel);
127        _resultsTable.setDefaultRenderer(NamedObj.class,
128                new NamedObjRenderer());
129
130        // If you change the height, then check that a few rows can be added.
131        // Also, check the setRowHeight call below.
132        _resultsTable
133                .setPreferredScrollableViewportSize(new Dimension(300, 300));
134
135        ListSelectionModel selectionModel = _resultsTable.getSelectionModel();
136        selectionModel.addListSelectionListener(this);
137
138        addKeyListener(new KeyAdapter() {
139            @Override
140            public void keyTyped(KeyEvent ke) {
141                if (ke.getKeyChar() == '\n') {
142                    _search();
143                }
144            }
145        });
146
147        // Make the contents of the table scrollable and visible.
148        JScrollPane scrollPane = new JScrollPane(_resultsTable);
149        getContentPane().add(scrollPane, BorderLayout.CENTER);
150
151        _resultsTable.addKeyListener(new KeyAdapter() {
152            @Override
153            public void keyReleased(KeyEvent event) {
154                int code = event.getKeyCode();
155                if (code == KeyEvent.VK_ENTER) {
156                    _search();
157                } else if (code == KeyEvent.VK_ESCAPE) {
158                    _cancel();
159                }
160            }
161        });
162
163        _resultsTable.addMouseListener(new MouseAdapter() {
164            @Override
165            public void mouseClicked(MouseEvent e) {
166                // TODO Auto-generated method stub
167                // super.mouseClicked(e);
168                int button = e.getButton();
169                int count = e.getClickCount();
170                if (button == MouseEvent.BUTTON1 && count == 2) {
171                    int[] selected = _resultsTable.getSelectedRows();
172                    for (int element : selected) {
173                        NamedObj selectedObject = (NamedObj) _resultsTableModel
174                                .getValueAt(element, 0);
175                        BasicGraphFrame.openComposite(_owner, selectedObject);
176                    }
177                }
178            }
179        });
180
181        pack();
182        setVisible(true);
183    }
184
185    ///////////////////////////////////////////////////////////////////
186    ////                         public methods                    ////
187
188    /** Execute the search. This is called to
189     *  notify this dialog that one of the search options has changed.
190     *  @param name The name of the query field that changed.
191     */
192    @Override
193    public void changed(String name) {
194        _search();
195    }
196
197    /** Override to clear highlights. */
198    @Override
199    public void dispose() {
200        _clearHighlights();
201        super.dispose();
202    }
203
204    /** React to notice that the selection has changed.
205     *  @param event The selection event.
206     */
207    @Override
208    public void valueChanged(ListSelectionEvent event) {
209        if (event.getValueIsAdjusting()) {
210            // Selection change is not finished. Ignore.
211            return;
212        }
213        _clearHighlights();
214
215        // Highlight new selection.
216        int[] selected = _resultsTable.getSelectedRows();
217        for (int element : selected) {
218            NamedObj selectedObject = (NamedObj) _resultsTableModel
219                    .getValueAt(element, 0);
220            _highlightResult(selectedObject);
221        }
222    }
223
224    ///////////////////////////////////////////////////////////////////
225    ////                         protected methods                 ////
226
227    /** Clear all highlights.
228     */
229    protected void _clearHighlights() {
230        // Clear previous highlights.
231        ChangeRequest request = new ChangeRequest(this, "Error Dehighlighter") {
232            @Override
233            protected void _execute() throws Exception {
234                for (Attribute highlight : _highlights) {
235                    highlight.setContainer(null);
236                }
237            }
238        };
239        request.setPersistent(false);
240        _target.requestChange(request);
241    }
242
243    /** Highlight the specified object and all its containers to
244     *  indicate that it matches the search criteria.
245     *  @param target The target.
246     */
247    protected void _highlightResult(final NamedObj target) {
248        ChangeRequest request = new ChangeRequest(this, "Error Highlighter") {
249            @Override
250            protected void _execute() throws Exception {
251                _addHighlightIfNeeded(target);
252                NamedObj container = target.getContainer();
253                while (container != null) {
254                    _addHighlightIfNeeded(container);
255                    container = container.getContainer();
256                }
257            }
258        };
259        request.setPersistent(false);
260        target.requestChange(request);
261    }
262
263    /** Initialize the query dialog.
264     *  Derived classes may change the layout of the query dialog.
265     */
266    protected void _initializeQuery() {
267        _query.addLine("text", "Find", _previousSearchTerm);
268        _query.setColumns(3);
269        _query.addCheckBox("values", "Include values", true);
270        _query.addCheckBox("names", "Include names", true);
271        _query.addCheckBox("recursive", "Recursive search", true);
272        _query.addCheckBox("case", "Case sensitive", false);
273    }
274
275    /** Perform a search and update the results table.
276     */
277    protected void _search() {
278        String findText = _query.getStringValue("text");
279        if (findText.trim().equals("")) {
280            MessageHandler.message("Please enter a search term");
281            return;
282        }
283        _previousSearchTerm = findText;
284        boolean includeValues = _query.getBooleanValue("values");
285        boolean includeNames = _query.getBooleanValue("names");
286        boolean recursiveSearch = _query.getBooleanValue("recursive");
287        boolean caseSensitive = _query.getBooleanValue("case");
288        Pattern pattern = null;
289        try {
290            pattern = Pattern.compile(findText,
291                    caseSensitive ? 0 : Pattern.CASE_INSENSITIVE);
292        } catch (PatternSyntaxException ex) {
293            BasicGraphFrame.report(_owner, "Problem with " + findText
294                    + " as a regular expression: " + ex);
295        }
296        Set<NamedObj> results = _find(_target, findText, includeValues,
297                includeNames, recursiveSearch, caseSensitive, pattern);
298        _resultsTableModel.setContents(results);
299        if (results.size() == 0) {
300            MessageHandler.message("No matches");
301        }
302    }
303
304    /** Create buttons.
305     *  @param panel The panel into which to put the buttons.
306     */
307    @Override
308    protected void _createExtendedButtons(JPanel panel) {
309        _searchButton = new JButton("Search");
310        panel.add(_searchButton);
311    }
312
313    /** Return a list of objects in the model that match the
314     *  specified search.
315     *  @param container The container within which to search.
316     *  @param text The text to find.
317     *  @param includeValues True to search values of Settable objects.
318     *  @param includeNames True to include names of objects.
319     *  @param recursive True to search within objects immediately contained.
320     *  @param caseSensitive True to match the case.
321     *  @param pattern The text compiled as a pattern, or null if the text could
322     *  not be compiled as a pattern.
323     *  @return The list of objects in the model that match the specified search.
324     */
325    protected Set<NamedObj> _find(NamedObj container, String text,
326            boolean includeValues, boolean includeNames, boolean recursive,
327            boolean caseSensitive, Pattern pattern) {
328        if (!caseSensitive) {
329            text = text.toLowerCase(Locale.getDefault());
330        }
331        SortedSet<NamedObj> result = new TreeSet<NamedObj>(
332                new NamedObjComparator());
333        Iterator<NamedObj> objects = container.containedObjectsIterator();
334        while (objects.hasNext()) {
335            NamedObj object = objects.next();
336            if (includeNames) {
337                String name = object.getName();
338                if (!caseSensitive) {
339                    name = name.toLowerCase(Locale.getDefault());
340                }
341                if (pattern != null) {
342                    Matcher matcher = pattern.matcher(name);
343                    if (name.contains(text) || matcher.matches()) {
344                        result.add(object);
345                    }
346                } else {
347                    if (name.contains(text)) {
348                        result.add(object);
349                    }
350                }
351            }
352            if (includeValues && object instanceof Settable) {
353                Settable.Visibility visible = ((Settable) object)
354                        .getVisibility();
355                if (!visible.equals(Settable.NONE)
356                        && !visible.equals(Settable.EXPERT)) {
357                    String value = ((Settable) object).getExpression();
358                    if (!caseSensitive) {
359                        value = value.toLowerCase(Locale.getDefault());
360                    }
361                    if (pattern != null) {
362                        Matcher matcher = pattern.matcher(value);
363                        if (value.contains(text) || matcher.matches()) {
364                            result.add(object);
365                        }
366                    } else {
367                        if (value.contains(text)) {
368                            result.add(object);
369                        }
370                    }
371                }
372            }
373            if (recursive) {
374                result.addAll(_find(object, text, includeValues, includeNames,
375                        recursive, caseSensitive, pattern));
376            }
377        }
378        return result;
379    }
380
381    /** Return a URL that points to the help page.
382     *  @return A URL that points to the help page
383     */
384    @Override
385    protected URL _getHelpURL() {
386        URL helpURL = getClass().getClassLoader().getResource(
387                "ptolemy/vergil/basic/doc/SearchResultsDialog.htm");
388        return helpURL;
389    }
390
391    /** Process a button press.
392     *  @param button The button.
393     */
394    @Override
395    protected void _processButtonPress(String button) {
396        // If the user has typed in a port name, but not
397        // moved the focus, we want to tell the model the
398        // data has changed.
399        if (_resultsTable.isEditing()) {
400            _resultsTable.editingStopped(new ChangeEvent(button));
401        }
402
403        // The button semantics are
404        // Add - Add a new port.
405        if (button.equals("Search")) {
406            _search();
407        } else {
408            super._processButtonPress(button);
409        }
410    }
411
412    ///////////////////////////////////////////////////////////////////
413    ////                         protected fields                  ////
414
415    /** The The frame that, per the user, is generating the dialog.
416     *  Typically a BasicGraphFrame.
417     */
418    protected Frame _owner;
419
420    /** Model for the table. */
421    protected ResultsTableModel _resultsTableModel = null;
422
423    /** The query portion of the dialog. */
424    protected Query _query;
425
426    /** Table for search results. */
427    protected JTable _resultsTable;
428
429    /** The entity on which search is performed. */
430    protected Entity _target;
431
432    ///////////////////////////////////////////////////////////////////
433    ////                         private method                    ////
434
435    /** Add a highlight color to the specified target if it is
436     *  not already present.
437     *  @param target The target to highlight.
438     *  @exception IllegalActionException If the highlight cannot be added.
439     *  @exception NameDuplicationException Should not be thrown.
440     */
441    private void _addHighlightIfNeeded(NamedObj target)
442            throws IllegalActionException, NameDuplicationException {
443        Attribute highlightColor = target.getAttribute("_highlightColor");
444        if (highlightColor instanceof ColorAttribute) {
445            // There is already a highlight. Set its color.
446            ((ColorAttribute) highlightColor).setExpression(_HIGHLIGHT_COLOR);
447            _highlights.add(highlightColor);
448        } else if (highlightColor == null) {
449            highlightColor = new ColorAttribute(target, "_highlightColor");
450            ((ColorAttribute) highlightColor).setExpression(_HIGHLIGHT_COLOR);
451            highlightColor.setPersistent(false);
452            ((ColorAttribute) highlightColor).setVisibility(Settable.EXPERT);
453            _highlights.add(highlightColor);
454        }
455    }
456
457    ///////////////////////////////////////////////////////////////////
458    ////                         private variables                 ////
459
460    /** The color to use for the highlight. */
461    private static String _HIGHLIGHT_COLOR = "{0.6, 0.6, 1.0, 1.0}";
462
463    /** Highlights that have been created. */
464    private Set<Attribute> _highlights = new HashSet<Attribute>();
465
466    /** Previous search term, if any. */
467    private String _previousSearchTerm = "";
468
469    /** The results of the latest search. */
470    private NamedObj[] _results = null;
471
472    /** The Search button. */
473    private JButton _searchButton;
474
475    ///////////////////////////////////////////////////////////////////
476    ////                         inner classes                     ////
477
478    /** Comparator for sorting named objects alphabetically by name. */
479    static class NamedObjComparator implements Comparator<NamedObj> {
480        @Override
481        public int compare(NamedObj arg0, NamedObj arg1) {
482            return arg0.getFullName().compareTo(arg1.getFullName());
483        }
484    }
485
486    /** Default renderer for results table. */
487    class NamedObjRenderer extends DefaultTableCellRenderer {
488        @Override
489        public void setValue(Object value) {
490            String fullName = ((NamedObj) value).getFullName();
491            // Strip the name of the model name and the leading and trailing period.
492            String strippedName = fullName
493                    .substring(_target.toplevel().getName().length() + 2);
494            setText(strippedName);
495        }
496    }
497
498    /** The table model for the search results table. */
499    class ResultsTableModel extends AbstractTableModel {
500
501        /** Populate the _results list.
502         */
503        public ResultsTableModel() {
504            _results = new NamedObj[0];
505        }
506
507        /** Return the number of columns, which is one.
508         *  @return the number of columns, which is 1.
509         */
510        @Override
511        public int getColumnCount() {
512            return 1;
513        }
514
515        /** Get the number of rows.
516         *  @return the number of rows.
517         */
518        @Override
519        public int getRowCount() {
520            return _results.length;
521        }
522
523        /** Get the column header name.
524         *  @return The string "Found in (select to highlight, double-click to open)".
525         *  @see javax.swing.table.TableModel#getColumnName(int)
526         */
527        @Override
528        public String getColumnName(int col) {
529            return "Found in (select to highlight, double-click to open):";
530        }
531
532        /** Get the value at a particular row and column.
533         *  @param row The row.
534         *  @param col The column.
535         *  @return the value.
536         */
537        @Override
538        public Object getValueAt(int row, int col) {
539            return _results[row];
540        }
541
542        /** Return NameObj.class.
543         *  @param column The column number.
544         *  @return Return NamedObj.class.
545         */
546        @Override
547        public Class getColumnClass(int column) {
548            return NamedObj.class;
549        }
550
551        /** Return false. Search result cells are not editable.
552         *  @param row The row number.
553         *  @param column The column number.
554         *  @return false.
555         */
556        @Override
557        public boolean isCellEditable(int row, int column) {
558            return false;
559        }
560
561        public void setContents(Set<NamedObj> results) {
562            _results = new NamedObj[results.size()];
563            int i = 0;
564            for (NamedObj result : results) {
565                _results[i++] = result;
566            }
567            fireTableDataChanged();
568        }
569    }
570}