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}