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 }