// 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.workspace; import com.google.collide.client.AppContext; import com.google.collide.client.common.BaseResources; import com.google.collide.client.search.FileNameSearch; import com.google.collide.client.ui.dropdown.AutocompleteController; import com.google.collide.client.ui.dropdown.DropdownController; import com.google.collide.client.ui.dropdown.DropdownWidgets; import com.google.collide.client.ui.dropdown.AutocompleteController.AutocompleteHandler; import com.google.collide.client.ui.dropdown.DropdownController.BaseListener; import com.google.collide.client.ui.dropdown.DropdownController.DropdownPositionerBuilder; import com.google.collide.client.ui.dropdown.DropdownWidgets.DropdownInput; import com.google.collide.client.ui.list.SimpleList.ListItemRenderer; import com.google.collide.client.ui.menu.AutoHideComponent; import com.google.collide.client.ui.menu.AutoHideView; import com.google.collide.client.ui.menu.PositionController; import com.google.collide.client.ui.menu.AutoHideComponent.AutoHideModel; import com.google.collide.client.ui.menu.PositionController.HorizontalAlign; import com.google.collide.client.ui.menu.PositionController.Positioner; import com.google.collide.client.ui.menu.PositionController.VerticalAlign; 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.dto.RunTarget; import com.google.collide.dto.RunTarget.RunMode; import com.google.collide.dto.client.DtoClientImpls.RunTargetImpl; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.util.RegExpUtils; import com.google.collide.shared.util.StringUtils; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.InputElement; import com.google.gwt.dom.client.LabelElement; import com.google.gwt.dom.client.SpanElement; import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.resources.client.CssResource; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.uibinder.client.UiTemplate; import com.google.gwt.user.client.ui.HTMLPanel; import elemental.dom.Node; import elemental.events.Event; import elemental.events.EventListener; import elemental.events.KeyboardEvent; /** * A popup for user selection of the run target. */ public class RunButtonTargetPopup extends AutoHideComponent<RunButtonTargetPopup.View, AutoHideModel> { public static RunButtonTargetPopup create(AppContext context, Element anchorElement, Element triggerElement, FileNameSearch searchIndex) { View view = new View(context.getResources()); return new RunButtonTargetPopup(view, anchorElement, triggerElement, searchIndex); } /** * An enum which handles the different types of files that can be parsed out of a user's file * input. Each enum value also provides the appropriate labels and formatter for the user provided * values. */ public enum RunTargetType { /** * Represents any other value typed into the dropdown. This file will be executed on the * resource serving servlet. */ FILE("Query", "?query string (optional)", new InputFormatter() { @Override public String formatUserInput(String file, String userValue) { if (userValue.isEmpty()) { return StringUtils.ensureStartsWith(file, "/"); } else { return StringUtils.ensureStartsWith(file, "/") + StringUtils.ensureStartsWith(userValue, "?"); } } }); /** * An object which formats some output based on the file name and query value provided by the * user. */ public interface InputFormatter { String formatUserInput(String file, String userValue); } /** * An object which can be used for run targets which do not need to provide a formatted output. */ public static final class NoInputFormatter implements InputFormatter { @Override public String formatUserInput(String file, String userValue) { return ""; } } private final String label; private final String placeHolder; private final InputFormatter inputFormatter; RunTargetType(String label, String placeHolder, InputFormatter inputFormatter) { this.label = label; this.placeHolder = placeHolder; this.inputFormatter = inputFormatter; } /** * Parses the user's input and determines the type of run target. */ public static RunTargetType parseTargetType(String fileInput) { return RunTargetType.FILE; } } public interface Css extends CssResource { String container(); String radio(); String stackedContainer(); String smallPreviewText(); String alwaysRunRow(); String alwaysRunInput(); String alwaysRunLabel(); String alwaysRunUrl(); String appUrlLabel(); String autocompleteFolder(); String listItem(); /* Sets the dropdown opacity to 1 while the menu is visible */ String stayActive(); } public interface Resources extends BaseResources.Resources, DropdownWidgets.Resources { @Source("RunButtonTargetPopup.css") Css runButtonTargetPopupCss(); @Source("appengine-small.png") ImageResource appengineSmall(); } public interface ViewEvents { void onAlwaysRunInputChanged(); void onPathInputChanged(); } public class ViewEventsImpl implements ViewEvents { @Override public void onAlwaysRunInputChanged() { selectRadio(RunMode.ALWAYS_RUN); getView().setDisplayForRunningApp(); } @Override public void onPathInputChanged() { selectRadio(RunMode.ALWAYS_RUN); getView().setDisplayForRunningApp(); } } private static final int MAX_AUTOCOMPLETE_RESULTS = 4; private final FileNameSearch searchIndex; private final PositionController positionController; private AutocompleteController<PathUtil> autocompleteController; private RunButtonTargetPopup( View view, Element anchorElement, Element triggerElement, FileNameSearch searchIndex) { super(view, new AutoHideModel()); view.setDelegate(new ViewEventsImpl()); this.searchIndex = searchIndex; setupAutocomplete(); // Don't eat outside clicks and only hide when the user clicks away addPartnerClickTargets( Elements.asJsElement(triggerElement), autocompleteController.getDropdown().getElement()); setCaptureOutsideClickOnClose(false); setDelay(-1); // Position Controller positionController = new PositionController(new PositionController.PositionerBuilder().setVerticalAlign( VerticalAlign.BOTTOM).setHorizontalAlign(HorizontalAlign.LEFT) .buildAnchorPositioner(Elements.asJsElement(anchorElement)), getView().getElement()); } private void setupAutocomplete() { AutocompleteHandler<PathUtil> handler = new AutocompleteHandler<PathUtil>() { @Override public JsonArray<PathUtil> doCompleteQuery(String query) { PathUtil searchPath = PathUtil.WORKSPACE_ROOT; // the PathUtil.createExcluding wasn't used here since it doesn't make // /test2/ into a path containing /test2 but instead makes it / if (query.lastIndexOf(PathUtil.SEP) != -1) { searchPath = new PathUtil(query.substring(0, query.lastIndexOf(PathUtil.SEP))); query = query.substring(query.lastIndexOf(PathUtil.SEP) + 1); } // Only the non folder part (if it exists) is supported for wildcards RegExp reQuery = RegExpUtils.createRegExpForWildcardPattern( query, ClientStringUtils.containsUppercase(query) ? "" : "i"); return searchIndex.getMatchesRelativeToPath(searchPath, reQuery, MAX_AUTOCOMPLETE_RESULTS); } @Override public void onItemSelected(PathUtil item) { getView().runAlwaysDropdown.getInput().setValue(item.getPathString()); getView().setDisplayForRunningApp(); } }; BaseListener<PathUtil> clickListener = new BaseListener<PathUtil>() { @Override public void onItemClicked(PathUtil item) { getView().runAlwaysDropdown.getInput().setValue(item.getPathString()); getView().setDisplayForRunningApp(); } }; ListItemRenderer<PathUtil> itemRenderer = new ListItemRenderer<PathUtil>() { @Override public void render(elemental.html.Element listItemBase, PathUtil itemData) { elemental.html.SpanElement fileNameElement = Elements.createSpanElement(); elemental.html.SpanElement folderNameElement = Elements.createSpanElement( getView().res.runButtonTargetPopupCss().autocompleteFolder()); listItemBase.appendChild(fileNameElement); listItemBase.appendChild(folderNameElement); int size = itemData.getPathComponentsCount(); if (size == 1) { fileNameElement.setTextContent(itemData.getPathComponent(0)); folderNameElement.setTextContent(""); } else { fileNameElement.setTextContent(itemData.getPathComponent(size - 1)); folderNameElement.setTextContent(" - " + itemData.getPathComponent(size - 2)); } } }; Positioner positioner = new DropdownPositionerBuilder().buildAnchorPositioner( getView().runAlwaysDropdown.getInput()); DropdownController<PathUtil> autocompleteDropdown = new DropdownController.Builder<PathUtil>( positioner, null, getView().res, clickListener, itemRenderer).setInputTargetElement( getView().runAlwaysDropdown.getInput()) .setShouldAutoFocusOnOpen(false).setKeyboardSelectionEnabled(true).build(); autocompleteController = AutocompleteController.create( getView().runAlwaysDropdown.getInput(), autocompleteDropdown, handler); } public void updateCurrentFile(String filePath) { getView().runPreviewCurrentFile.setInnerText( StringUtils.ensureNotEmpty(filePath, "Select a file")); } private void selectRadio(RunMode mode) { elemental.html.Element preview = Elements.asJsElement(getView().runPreviewRadio); elemental.html.Element always = Elements.asJsElement(getView().runAlwaysRadio); preview.setChecked(mode == RunMode.PREVIEW_CURRENT_FILE); always.setChecked(mode == RunMode.ALWAYS_RUN); } public void setRunTarget(RunTarget runTarget) { selectRadio(runTarget.getRunMode()); getView().runAlwaysDropdown.getInput().setValue(StringUtils.nullToEmpty( runTarget.getAlwaysRunFilename())); getView().userExtraInput.setValue(StringUtils.nullToEmpty(runTarget.getAlwaysRunUrlOrQuery())); } public RunTarget getRunTarget() { RunTargetImpl runTarget = RunTargetImpl.make(); boolean isPreviewMode = Elements.asJsElement(getView().runPreviewRadio).isChecked(); runTarget.setRunMode(isPreviewMode ? RunMode.PREVIEW_CURRENT_FILE : RunMode.ALWAYS_RUN); runTarget.setAlwaysRunFilename(getView().runAlwaysDropdown.getInput().getValue()); runTarget.setAlwaysRunUrlOrQuery(getView().userExtraInput.getValue()); return runTarget; } @Override public void show() { // Position Ourselves positionController.updateElementPosition(); // Update UI before we show getView().setDisplayForRunningApp(); super.show(); } public static class View extends AutoHideView<ViewEvents> { @UiTemplate("RunButtonTargetPopup.ui.xml") interface RunButtonDropdownUiBinder extends UiBinder<Element, View> { } private static RunButtonDropdownUiBinder uiBinder = GWT.create(RunButtonDropdownUiBinder.class); @UiField(provided = true) final Resources res; // Preview Current File Stuff @UiField InputElement runPreviewRadio; @UiField LabelElement runPreviewLabel; @UiField SpanElement runPreviewCurrentFile; // Run always stuff @UiField DivElement runAlwaysRow; @UiField InputElement runAlwaysRadio; @UiField LabelElement runAlwaysLabel; final DropdownInput runAlwaysDropdown; // Query and URL Box @UiField LabelElement userExtraLabel; @UiField InputElement userExtraInput; // Full Query URL @UiField DivElement runHintText; public View(Resources resources) { this.res = resources; Element element = uiBinder.createAndBindUi(this); setElement(Elements.asJsElement(element)); Elements.getBody().appendChild((Node) element); // Workaround for inability to set both id and ui:field in a UiBinder XML runAlwaysRadio.setId(HTMLPanel.createUniqueId()); runAlwaysLabel.setHtmlFor(runAlwaysRadio.getId()); userExtraLabel.setHtmlFor(runAlwaysRadio.getId()); runPreviewRadio.setId(HTMLPanel.createUniqueId()); runPreviewLabel.setHtmlFor(runPreviewRadio.getId()); // Create the dropdown runAlwaysDropdown = new DropdownInput(resources); runAlwaysDropdown.getInput() .addClassName(resources.runButtonTargetPopupCss().alwaysRunInput()); runAlwaysDropdown.getInput().setAttribute("placeholder", "Enter filename"); Elements.asJsElement(runAlwaysRow).appendChild(runAlwaysDropdown.getContainer()); setDisplayForRunningApp(RunTargetType.FILE); attachHandlers(); } public void attachHandlers() { runAlwaysDropdown.getInput().addEventListener(Event.INPUT, new EventListener() { @Override public void handleEvent(Event evt) { if (getDelegate() != null) { getDelegate().onAlwaysRunInputChanged(); } } }, false); Elements.asJsElement(userExtraInput).addEventListener(Event.INPUT, new EventListener() { @Override public void handleEvent(Event evt) { if (getDelegate() != null) { getDelegate().onPathInputChanged(); } } }, false); EventListener onEnterListener = new EventListener() { @Override public void handleEvent(Event evt) { if (getDelegate() != null) { KeyboardEvent keyEvent = (KeyboardEvent) evt; if (keyEvent.getKeyCode() == KeyboardEvent.KeyCode.ENTER) { getDelegate().onPathInputChanged(); evt.stopPropagation(); hide(); } } } }; runAlwaysDropdown.getInput().addEventListener(Event.KEYUP, onEnterListener, false); Elements.asJsElement(userExtraInput).addEventListener(Event.KEYUP, onEnterListener, false); } /** * Sets up the view based on the user's current file input value. */ public void setDisplayForRunningApp() { RunTargetType type = RunTargetType.parseTargetType(runAlwaysDropdown.getInput().getValue()); setDisplayForRunningApp(type); } /** * Sets up the view for the supplied app type. */ public void setDisplayForRunningApp(RunTargetType appType) { userExtraInput.setAttribute("placeholder", appType.placeHolder); userExtraLabel.setInnerText(appType.label); String fileName = runAlwaysDropdown.getInput().getValue(); String queryText = userExtraInput.getValue(); if (fileName.isEmpty()) { runHintText.setInnerText("Type a filename"); } else { runHintText.setInnerText(appType.inputFormatter.formatUserInput(fileName, queryText)); } } } }