1 /* 2 * Copyright (c) 2008, 2017, Oracle and/or its affiliates. 3 * All rights reserved. Use is subject to license terms. 4 * 5 * This file is available and licensed under the following license: 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * - Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * - Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in 15 * the documentation and/or other materials provided with the distribution. 16 * - Neither the name of Oracle Corporation nor the names of its 17 * contributors may be used to endorse or promote products derived 18 * from this software without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 package ensemble; 33 34 35 import ensemble.control.Popover; 36 import ensemble.control.SearchBox; 37 import ensemble.search.DocumentType; 38 import ensemble.search.IndexSearcher; 39 import ensemble.search.SearchResult; 40 import java.util.EnumMap; 41 import java.util.List; 42 import java.util.Map; 43 import javafx.animation.KeyFrame; 44 import javafx.animation.Timeline; 45 import javafx.beans.value.ObservableValue; 46 import javafx.event.ActionEvent; 47 import javafx.geometry.Point2D; 48 import javafx.scene.control.Tooltip; 49 import javafx.scene.input.KeyCode; 50 import javafx.scene.input.KeyEvent; 51 import javafx.util.Duration; 52 import org.apache.lucene.queryparser.classic.ParseException; 53 54 55 /** 56 * Implementation of popover to show search results 57 */ 58 public class SearchPopover extends Popover { 59 private final SearchBox searchBox; 60 private final PageBrowser pageBrowser; 61 private IndexSearcher indexSearcher; 62 private Tooltip searchErrorTooltip = null; 63 private Timeline searchErrorTooltipHidder = null; 64 private SearchResultPopoverList searchResultPopoverList; 65 66 public SearchPopover(final SearchBox searchBox, PageBrowser pageBrowser) { 67 super(); 68 this.searchBox = searchBox; 69 this.pageBrowser = pageBrowser; 70 getStyleClass().add("right-tooth"); 71 setPrefWidth(600); 72 73 searchBox.textProperty().addListener((ObservableValue<? extends String> ov, String t, String t1) -> { 74 updateResults(); 75 }); 76 77 searchBox.addEventFilter(KeyEvent.ANY, (KeyEvent t) -> { 78 if (t.getCode() == KeyCode.DOWN 79 || t.getCode() == KeyCode.UP 80 || t.getCode() == KeyCode.PAGE_DOWN 81 || (t.getCode() == KeyCode.HOME && (t.isControlDown() || t.isMetaDown())) 82 || (t.getCode() == KeyCode.END && (t.isControlDown() || t.isMetaDown())) 83 || t.getCode() == KeyCode.PAGE_UP) { 84 searchResultPopoverList.fireEvent(t); 85 t.consume(); 86 } else if (t.getCode() == KeyCode.ENTER) { 87 t.consume(); 88 if (t.getEventType() == KeyEvent.KEY_PRESSED) { 89 SearchResult selectedItem = searchResultPopoverList.getSelectionModel().getSelectedItem(); 90 if (selectedItem != null) searchResultPopoverList.itemClicked(selectedItem); 91 } 92 } 93 }); 94 searchResultPopoverList = new SearchResultPopoverList(pageBrowser); 95 // if list gets focus then send back to search box 96 searchResultPopoverList.focusedProperty().addListener((ObservableValue<? extends Boolean> ov, Boolean t, Boolean hasFocus) -> { 97 if (hasFocus) { 98 searchBox.requestFocus(); 99 searchBox.selectPositionCaret(searchBox.getText().length()); 100 } 101 }); 102 } 103 104 private void updateResults() { 105 if (searchBox.getText() == null || searchBox.getText().isEmpty()) { 106 populateMenu(new EnumMap<DocumentType, List<SearchResult>>(DocumentType.class)); 107 return; 108 } 109 boolean haveResults = false; 110 Map<DocumentType, List<SearchResult>> results = null; 111 try { 112 if (indexSearcher == null) indexSearcher = new IndexSearcher(); 113 results = indexSearcher.search( 114 searchBox.getText() + (searchBox.getText().matches("\\w+") ? "*" : "") 115 ); 116 // check if we have any results 117 for (List<SearchResult> categoryResults: results.values()) { 118 if (categoryResults.size() > 0) { 119 haveResults = true; 120 break; 121 } 122 } 123 } catch (ParseException e) { 124 showError(e.getMessage().substring("Cannot parse ".length())); 125 } 126 if (haveResults) { 127 showError(null); 128 populateMenu(results); 129 show(); 130 } else { 131 if (searchErrorTooltip==null || searchErrorTooltip.getText()==null) showError("No matches"); 132 hide(); 133 } 134 } 135 136 private void showError(String message) { 137 if (searchErrorTooltip == null) { 138 searchErrorTooltip = new Tooltip(); 139 } 140 searchErrorTooltip.setText(message); 141 if (searchErrorTooltipHidder != null) searchErrorTooltipHidder.stop(); 142 if (message != null) { 143 Point2D toolTipPos = searchBox.localToScene(0, searchBox.getLayoutBounds().getHeight()); 144 double x = toolTipPos.getX() + searchBox.getScene().getX() + searchBox.getScene().getWindow().getX(); 145 double y = toolTipPos.getY() + searchBox.getScene().getY() + searchBox.getScene().getWindow().getY(); 146 searchErrorTooltip.show( searchBox.getScene().getWindow(),x, y); 147 searchErrorTooltipHidder = new Timeline(); 148 searchErrorTooltipHidder.getKeyFrames().add( 149 new KeyFrame(Duration.seconds(3), (ActionEvent t) -> { 150 searchErrorTooltip.hide(); 151 searchErrorTooltip.setText(null); 152 }) 153 ); 154 searchErrorTooltipHidder.play(); 155 } else { 156 searchErrorTooltip.hide(); 157 } 158 } 159 160 private void populateMenu(Map<DocumentType, List<SearchResult>> results) { 161 searchResultPopoverList.getItems().clear(); 162 for(Map.Entry<DocumentType, List<SearchResult>> entry: results.entrySet()) { 163 searchResultPopoverList.getItems().addAll(entry.getValue()); 164 } 165 clearPages(); 166 pushPage(searchResultPopoverList); 167 } 168 }