// Copyright 2012 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.collide.client.search.awesomebox; import com.google.collide.client.code.FileSelectedPlace; import com.google.collide.client.code.FileSelectionController.FileOpenedEvent; import com.google.collide.client.history.Place; import com.google.collide.client.search.FileNameSearch; import com.google.collide.client.search.awesomebox.AwesomeBox.Resources; import com.google.collide.client.search.awesomebox.FileNameNavigationSection.FileNavItem; import com.google.collide.client.util.ClientStringUtils; import com.google.collide.client.util.Elements; import com.google.collide.client.util.PathUtil; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.util.JsonCollections; import com.google.collide.shared.util.RegExpUtils; import com.google.collide.shared.util.StringUtils; import com.google.common.base.Preconditions; import com.google.gwt.regexp.shared.RegExp; import elemental.html.SpanElement; /** * Section that performs navigation via file names in the AwesomeBox */ public class FileNameNavigationSection extends AbstractAwesomeBoxSection<FileNavItem> { /* * Maximum number of recently opened files to display in the dropdown list. * The list contains at most MAX_RECENT_FILES+1 since the most recent file * will be the currently opened file and thus not displayed. */ private static final int MAX_RECENT_FILES = 3; private final FileNameSearch searchIndex; private final JsonArray<PathUtil> recentFiles; // TODO: When code place is gone look into compile time injection. private Place currentPlace; public FileNameNavigationSection(AwesomeBox.Resources res, FileNameSearch searchIndex) { super(res); this.searchIndex = searchIndex; recentFiles = JsonCollections.createArray(); createDom(); } public void registerOnFileOpenedHandler(Place currentPlace) { this.currentPlace = currentPlace; // Subscribe to file notifications for our recent file list currentPlace.registerSimpleEventHandler(FileOpenedEvent.TYPE, new FileOpenedEvent.Handler() { @Override public void onFileOpened(boolean isEditable, PathUtil filePath) { // in case it is already there, remove it. recentFiles.remove(filePath); // check to ensure we aren't over our file limit and then insert if (recentFiles.size() > MAX_RECENT_FILES) { recentFiles.pop(); } recentFiles.splice(0, 0, filePath); } }); } /** * Encapsulates a file path that is displayed in the awesome box. * */ static class FileNavItem extends AbstractAwesomeBoxSection.ActionItem { private final SpanElement fileNameElement; private final SpanElement folderNameElement; private PathUtil filePath; private final Place place; public FileNavItem(Resources res, Place place) { super(res, AwesomeBoxUtils.createSectionItem(res)); Preconditions.checkNotNull(place, "Place cannot be null"); this.place = place; element.addClassName(res.awesomeBoxSectionCss().fileItem()); fileNameElement = Elements.createSpanElement(); folderNameElement = Elements.createSpanElement(res.awesomeBoxSectionCss().folder()); element.appendChild(fileNameElement); element.appendChild(folderNameElement); } public void setPath(PathUtil path) { int size = path.getPathComponentsCount(); if (size == 1) { fileNameElement.setTextContent(path.getPathComponent(0)); folderNameElement.setTextContent(""); } else { fileNameElement.setTextContent(path.getPathComponent(size - 1)); folderNameElement.setTextContent(" - " + path.getPathComponent(size - 2)); } filePath = path; } /** * Navigates to this file. */ @Override public ActionResult doAction(ActionSource source) { place.fireChildPlaceNavigation(FileSelectedPlace.PLACE.createNavigationEvent(filePath)); return ActionResult.CLOSE; } /** * @return The filename portion of the path */ @Override public String completeQuery() { return filePath.getBaseName(); } } /** * Creates the basic DOM for this section */ private void createDom() { sectionElement = AwesomeBoxUtils.createSectionContainer(res); } @Override public boolean onQueryChanged(String query) { // the most recent file in our list is the current opened one, don't show it JsonArray<PathUtil> files = recentFiles.slice(1, MAX_RECENT_FILES+1); if (searchIndex != null && !StringUtils.isNullOrEmpty(query)) { // TODO: Results come back in the order they appear in the tree // there needs to be some sort of twiddler that re-ranks the results so // that filenames that are better matches appear higher. RegExp reQuery = RegExpUtils.createRegExpForWildcardPattern( query, ClientStringUtils.containsUppercase(query) ? "" : "i"); files = searchIndex.getMatches(reQuery, 5); } // we don't have anything to display if (files.size() == 0) { return false; } showFiles(files); return true; } private void showFiles(JsonArray<PathUtil> files) { // Reuse any fileNavItems that are currently out there, don't worry about // selection, clearSelection will be called by the AwesomeBox after this int i = reuseAnyExistingElements(files); for (; i < files.size(); i++) { FileNavItem item = new FileNavItem(res, currentPlace); item.setPath(files.get(i)); listItems.add(item); sectionElement.appendChild(item.getElement()); } } /** * Reuses any existing FileNavItem elements in the dropdown section for speed. * If the number of existing elements is greater than the number of new * elements then the remaining items are removed from the dropdown and our * array. * * @param newItems */ private int reuseAnyExistingElements(JsonArray<PathUtil> newItems) { int i = 0; for (; i < newItems.size() && i < listItems.size(); i++) { listItems.get(i).setPath(newItems.get(i)); } JsonArray<FileNavItem> removed = listItems.asJsonArray().splice(i, listItems.size() - i); for (int r = 0; r < removed.size(); r++) { removed.get(r).remove(); } return i; } /** * The file name navigation section is never shown initially. */ @Override public boolean onShowing(AwesomeBox awesomeBox) { // Never show the first file in the list, since it's the current file. if (recentFiles.size() > 1) { showFiles(recentFiles.slice(1, MAX_RECENT_FILES+1)); return true; } return false; } }