// 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.code.FileSelectionController.FileOpenedEvent;
import com.google.collide.client.communication.ResourceUriUtils;
import com.google.collide.client.search.FileNameSearch;
import com.google.collide.client.ui.menu.AutoHideComponent.AutoHideHandler;
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.ui.tooltip.Tooltip;
import com.google.collide.client.ui.tooltip.Tooltip.TooltipRenderer;
import com.google.collide.client.util.Elements;
import com.google.collide.client.util.PathUtil;
import com.google.collide.client.workspace.RunButtonTargetPopup.RunTargetType;
import com.google.collide.dto.RunTarget;
import com.google.collide.dto.RunTarget.RunMode;
import com.google.collide.dto.UpdateWorkspaceRunTargets;
import com.google.collide.dto.client.DtoClientImpls.RunTargetImpl;
import com.google.collide.dto.client.DtoClientImpls.UpdateWorkspaceRunTargetsImpl;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.util.StringUtils;
import com.google.gwt.dom.client.Element;
import elemental.css.CSSStyleDeclaration;
import elemental.html.DivElement;
import elemental.html.SpanElement;
/**
* Controller for the behavior of the run button on the workspace header.
*/
public class RunButtonController {
public static RunButtonController create(AppContext context,
Element buttonElem,
Element dropdownElem,
WorkspacePlace currentPlace,
FileNameSearch fileNameSearch,
FileTreeModel fileTreeModel) {
RunButtonTargetPopup targetPopup =
RunButtonTargetPopup.create(context, buttonElem, dropdownElem, fileNameSearch);
elemental.html.Element target = Elements.asJsElement(buttonElem);
Positioner positioner = new Tooltip.TooltipPositionerBuilder().setVerticalAlign(
VerticalAlign.BOTTOM)
.setHorizontalAlign(HorizontalAlign.MIDDLE).buildAnchorPositioner(target);
Tooltip noFileSelectedTooltip = new Tooltip.Builder(
context.getResources(), target, positioner).setShouldListenToHover(false)
.setTooltipRenderer(new TooltipRenderer() {
@Override
public elemental.html.Element renderDom() {
DivElement container = Elements.createDivElement();
DivElement header = Elements.createDivElement();
header.setTextContent("No File Selected");
header.getStyle().setFontWeight(CSSStyleDeclaration.FontWeight.BOLDER);
header.getStyle().setMarginBottom(5, CSSStyleDeclaration.Unit.PX);
container.appendChild(header);
DivElement text = Elements.createDivElement();
text.setTextContent(
"Choose a file from the tree to preview it, or select a custom run target.");
container.appendChild(text);
return container;
}
}).build();
Tooltip yamlAddedTooltip = new Tooltip.Builder(
context.getResources(), target, positioner).setShouldListenToHover(false)
.setTooltipRenderer(new TooltipRenderer() {
@Override
public elemental.html.Element renderDom() {
DivElement container = Elements.createDivElement();
SpanElement text = Elements.createSpanElement();
text.setTextContent(
"The run target has been set to your newly created app.yaml file. ");
container.appendChild(text);
// TODO: We'd like to offer an option to undo the
// automatic setting of the run target, but I don't have time
// to write a coach tips class right now and tool tips can't be
// clicked.
return container;
}
}).build();
Tooltip targetResetTooltip = new Tooltip.Builder(
context.getResources(), target, positioner).setShouldListenToHover(false)
.setTooltipRenderer(new TooltipRenderer() {
@Override
public elemental.html.Element renderDom() {
DivElement container = Elements.createDivElement();
SpanElement text = Elements.createSpanElement();
text.setTextContent(
"You deleted your run target, the run button has been reset to preview "
+ "the active file.");
container.appendChild(text);
// TODO: We'd like to offer undo, but I don't have time
// to write a coach tips class right now and tool tips can't be
// clicked.
return container;
}
}).build();
return new RunButtonController(context,
dropdownElem,
currentPlace,
fileNameSearch,
targetPopup,
fileTreeModel,
noFileSelectedTooltip,
yamlAddedTooltip,
targetResetTooltip);
}
/**
* A listener which handles events in the file tree which may affect the current RunTarget.
*/
public class FileTreeChangeListener extends FileTreeModel.AbstractTreeModelChangeListener {
@Override
public void onNodeAdded(PathUtil parentDirPath, FileTreeNode newNode) {
RunTarget currentTarget = targetPopup.getRunTarget();
if (currentTarget.getRunMode() == RunMode.ALWAYS_RUN
&& currentTarget.getAlwaysRunFilename().contains("app.yaml")) {
return;
}
// did the user add an app.yaml?
if (newNode.isFile() && newNode.getName().equals("app.yaml")) {
// lets set the run target to run the app.yaml
RunTarget target = RunTargetImpl.make().setRunMode(RunMode.ALWAYS_RUN)
.setAlwaysRunFilename(newNode.getNodePath().getPathString()).setAlwaysRunUrlOrQuery("");
updateRunTarget(target);
targetPopup.setRunTarget(target);
yamlSelectedFileTooltip.show(TOOLTIP_DISPLAY_TIMEOUT_MS);
}
}
@Override
public void onNodeMoved(
PathUtil oldPath, FileTreeNode node, PathUtil newPath, FileTreeNode newNode) {
RunTarget currentTarget = targetPopup.getRunTarget();
if (currentTarget == null || currentTarget.getAlwaysRunFilename() == null) {
return;
}
PathUtil currentAlwaysRunPath = new PathUtil(currentTarget.getAlwaysRunFilename());
PathUtil relativePath = currentAlwaysRunPath.makeRelativeToParent(oldPath);
if (relativePath != null) {
// Either this node, or some ancestor node got renamed.
PathUtil newFilePath = PathUtil.concatenate(newPath, relativePath);
RunTargetImpl impl = (RunTargetImpl) currentTarget;
impl.setAlwaysRunFilename(newFilePath.getPathString());
targetPopup.setRunTarget(impl);
}
}
@Override
public void onNodesRemoved(JsonArray<FileTreeNode> oldNodes) {
RunTarget target = targetPopup.getRunTarget();
// we should clear the current file if the user deleted that one
// we should revert the run target to preview if the user deleted the
// "always run target".
// If this gets more complicated, make it a visitor type thing
boolean isAlwaysRun = target.getRunMode() == RunMode.ALWAYS_RUN;
boolean hasClearedCurrentFile = currentFilePath == null;
boolean hasClearedAlwaysRun = !isAlwaysRun;
PathUtil alwaysRunPath = isAlwaysRun ? new PathUtil(target.getAlwaysRunFilename()) : null;
for (int i = 0; (!hasClearedCurrentFile || !hasClearedAlwaysRun) && i < oldNodes.size();
i++) {
FileTreeNode node = oldNodes.get(i);
if (!hasClearedCurrentFile) {
if (node.getNodePath().containsPath(currentFilePath)) {
updateCurrentFile(null);
hasClearedCurrentFile = true;
}
}
if (!hasClearedAlwaysRun) {
if (node.getNodePath().containsPath(alwaysRunPath)) {
RunTarget newTarget = RunTargetImpl.make().setRunMode(RunMode.PREVIEW_CURRENT_FILE);
updateRunTarget(newTarget);
targetResetTooltip.show(TOOLTIP_DISPLAY_TIMEOUT_MS);
hasClearedAlwaysRun = true;
}
}
}
}
}
/**
* Timeout for tooltip messages to the user.
*/
private static final int TOOLTIP_DISPLAY_TIMEOUT_MS = 5000;
private final WorkspacePlace currentPlace;
private final AppContext appContext;
private final RunButtonTargetPopup targetPopup;
private final Element dropdownElem;
private final Tooltip noFileSelectedTooltip;
private final Tooltip yamlSelectedFileTooltip;
private final Tooltip targetResetTooltip;
private PathUtil currentFilePath;
private RunButtonController(final AppContext appContext,
Element dropdownElem,
WorkspacePlace currentPlace,
FileNameSearch fileNameSearch,
final RunButtonTargetPopup targetPopup,
FileTreeModel fileTreeModel,
Tooltip noFileSelectedTooltip,
Tooltip autoSelectedFileTooltip,
Tooltip targetResetTooltip) {
this.appContext = appContext;
this.dropdownElem = dropdownElem;
this.currentPlace = currentPlace;
this.targetPopup = targetPopup;
this.noFileSelectedTooltip = noFileSelectedTooltip;
this.yamlSelectedFileTooltip = autoSelectedFileTooltip;
this.targetResetTooltip = targetResetTooltip;
// Setup handler to keep track of current file
currentPlace.registerSimpleEventHandler(FileOpenedEvent.TYPE, new FileOpenedEvent.Handler() {
@Override
public void onFileOpened(boolean isEditable, PathUtil filePath) {
updateCurrentFile(filePath);
}
});
// Listen to the file Tree
fileTreeModel.addModelChangeListener(new FileTreeChangeListener());
this.targetPopup.setAutoHideHandler(new AutoHideHandler() {
@Override
public void onHide() {
toggleDropdownStyle(false);
// Update the runTarget for this workspace
// TODO: Consider checking to see if there has actually
// been a change
RunTarget runTarget = targetPopup.getRunTarget();
updateRunTarget(runTarget);
}
@Override
public void onShow() {
// do nothing
}
});
}
public void toggleDropdownStyle(boolean active) {
if (active) {
dropdownElem.addClassName(appContext.getResources().baseCss().buttonActive());
dropdownElem.addClassName(appContext.getResources().runButtonTargetPopupCss().stayActive());
} else {
dropdownElem.removeClassName(appContext.getResources().baseCss().buttonActive());
dropdownElem.removeClassName(
appContext.getResources().runButtonTargetPopupCss().stayActive());
}
}
public void onRunButtonClicked() {
if (targetPopup.getRunTarget() != null) {
launchRunTarget(targetPopup.getRunTarget());
} else {
launchFile(currentFilePath == null ? null : currentFilePath.getPathString(), "");
}
}
private void launchRunTarget(RunTarget target) {
if (target.getRunMode() == RunMode.PREVIEW_CURRENT_FILE) {
launchFile(currentFilePath == null ? null : currentFilePath.getPathString(), "");
} else {
launchFile(target.getAlwaysRunFilename(), target.getAlwaysRunUrlOrQuery());
}
}
/**
* Launches a file using the appropriate method
*/
private void launchFile(String file, String urlOrQuery) {
if (file == null) {
displayNoFileSelectedTooltip();
return;
}
RunTargetType appType = RunButtonTargetPopup.RunTargetType.parseTargetType(file);
launchPreview(file, StringUtils.nullToEmpty(urlOrQuery));
}
public void onRunButtonDropdownClicked() {
if (targetPopup.isShowing()) {
targetPopup.forceHide();
} else {
targetPopup.show();
// only needed on show since hiding is caught by the auto hide handler
toggleDropdownStyle(true);
}
}
private void updateRunTarget(RunTarget newTarget) {
UpdateWorkspaceRunTargets update =
UpdateWorkspaceRunTargetsImpl.make().setRunTarget(newTarget);
appContext.getFrontendApi().UPDATE_WORKSPACE_RUN_TARGETS.send(update);
targetPopup.setRunTarget(newTarget);
}
/**
* Launches a file given the base file and a query string.
*/
public void launchPreview(String baseUrl, String query) {
StringBuilder builder = new StringBuilder(ResourceUriUtils.getAbsoluteResourceBaseUri());
builder.append(StringUtils.ensureStartsWith(baseUrl, "/"));
if (!StringUtils.isNullOrEmpty(query)) {
builder.append(StringUtils.ensureStartsWith(query, "?"));
}
currentPlace.fireEvent(new RunApplicationEvent(builder.toString()));
}
private void updateCurrentFile(PathUtil filePath) {
currentFilePath = filePath;
targetPopup.updateCurrentFile(filePath == null ? null : filePath.getPathString());
}
private void displayNoFileSelectedTooltip() {
noFileSelectedTooltip.show(TOOLTIP_DISPLAY_TIMEOUT_MS);
}
}