/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.ide.ui.smartTree; import com.google.common.base.Predicate; import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Style; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.SimplePanel; import org.eclipse.che.ide.DelayedTask; import org.eclipse.che.ide.api.data.tree.Node; import org.eclipse.che.ide.ui.smartTree.converter.NodeConverter; import org.eclipse.che.ide.ui.smartTree.converter.impl.NodeNameConverter; import java.util.List; /** * @author Vlad Zhukovskiy */ public class SpeedSearch { private Tree tree; private NodeConverter<Node, String> nodeConverter; private DelayedTask searchTask; private StringBuilder searchRequest; private SearchPopUp searchPopUp; private static final String INITIAL_SEARCH_TEXT = "Search for: "; private static final String ID = "speedSearch"; private int searchDelay; private class SearchPopUp extends SimplePanel { private Label searchLabel; public SearchPopUp() { getElement().setId(ID); setVisible(false); //by default this.searchLabel = new Label(INITIAL_SEARCH_TEXT); add(searchLabel); } public void setSearchRequest(String request) { searchLabel.setText(INITIAL_SEARCH_TEXT + request); } } private KeyboardNavigationHandler keyNav = new KeyboardNavigationHandler() { @Override public void onBackspace(NativeEvent evt) { evt.preventDefault(); if (!Strings.isNullOrEmpty(searchRequest.toString())) { searchRequest.setLength(searchRequest.length() - 1); doSearch(); } } @Override public void onUp(NativeEvent evt) { //check if we have found nodes that matches search pattern and navigate to previous node by pressing Up key } @Override public void onDown(NativeEvent evt) { //check if we have found nodes that matches search pattern and navigate to previous node by pressing Down key } @Override public void onEnd(NativeEvent evt) { //iterate to last found node } @Override public void onHome(NativeEvent evt) { //iterate to first found node } @Override public void onEsc(NativeEvent evt) { removeSearchPopUpFromTree(); //clear search pattern and restore tree to normal mode } @Override public void onEnter(NativeEvent evt) { removeSearchPopUpFromTree(); //handle enter key, for leaf node we should check whether node is implemented by HasAction interface //and fire action performed, otherwise for non-leaf node we should expand/collapse node } @Override public void onKeyPress(NativeEvent evt) { char sChar = (char)evt.getKeyCode(); if (Character.isLetterOrDigit(sChar)) { // evt.preventDefault(); //not sure if this right decision evt.stopPropagation(); searchRequest.append(sChar); update(); } //gather key press and try to search through visible nodes to find nodes that matches search pattern } }; public SpeedSearch(Tree tree, NodeConverter<Node, String> nodeConverter) { this.tree = tree; this.nodeConverter = nodeConverter != null ? nodeConverter : new NodeNameConverter(); keyNav.bind(tree); this.searchDelay = 100; //100ms this.searchRequest = new StringBuilder(); initSearchPopUp(); } private void initSearchPopUp() { this.searchPopUp = new SearchPopUp(); Style style = this.searchPopUp.getElement().getStyle(); style.setBackgroundColor("grey"); style.setBorderStyle(Style.BorderStyle.SOLID); style.setBorderColor("#dbdbdb"); style.setBorderWidth(1, Style.Unit.PX); style.setPadding(2, Style.Unit.PX); style.setPosition(Style.Position.FIXED); style.setTop(100, Style.Unit.PX); style.setLeft(20, Style.Unit.PX); } private void addSearchPopUpToTree() { if (Document.get().getElementById(ID) == null) { searchPopUp.setVisible(true); tree.getElement().appendChild(searchPopUp.getElement()); } } private void removeSearchPopUpFromTree() { searchRequest.setLength(0); Document.get().getElementById(ID).removeFromParent(); } protected void update() { if (searchTask == null) { searchTask = new DelayedTask() { @Override public void onExecute() { doSearch(); } }; } searchTask.delay(searchDelay); } private void doSearch() { if (Strings.isNullOrEmpty(searchRequest.toString())) { cancelSearch(); return; } addSearchPopUpToTree(); searchPopUp.setSearchRequest(searchRequest.toString()); tree.getSelectionModel().deselectAll(); Iterable<Node> filter = Iterables.filter(getVisibleNodes(), matchesToSearchRequest()); boolean first = false; for (Node node : filter) { if (!first) { tree.scrollIntoView(node); first = true; } tree.getSelectionModel().select(node, true); } } private void cancelSearch() { removeSearchPopUpFromTree(); Node node = tree.getRootNodes().get(0); tree.getSelectionModel().select(node, false); tree.scrollIntoView(node); } private List<Node> getVisibleNodes() { List<Node> rootNodes = tree.getRootNodes(); return tree.getAllChildNodes(rootNodes, true); } private Predicate<Node> matchesToSearchRequest() { return new Predicate<Node>() { @Override public boolean apply(Node inputNode) { String nodeString = nodeConverter.convert(inputNode); return nodeString.toLowerCase().contains(searchRequest.toString().toLowerCase()); } }; } }