// 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.code.debugging; import com.google.collide.client.code.debugging.DebuggerApiTypes.OnEvaluateExpressionResponse; import com.google.collide.client.code.popup.EditorPopupController; import com.google.collide.client.documentparser.DocumentParser; import com.google.collide.client.editor.Editor; import com.google.collide.client.editor.MouseHoverManager; import com.google.collide.client.ui.menu.PositionController.VerticalAlign; import com.google.collide.client.util.CssUtils; 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.document.Document; import com.google.collide.shared.document.LineInfo; import com.google.collide.shared.util.JsonCollections; import com.google.collide.shared.util.ListenerRegistrar.RemoverManager; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.CssResource; import elemental.css.CSSStyleDeclaration; import elemental.html.Element; import javax.annotation.Nullable; /** * Controller for the debugger evaluation expression popup. */ public class EvaluationPopupController { public interface Css extends CssResource { String popupAnchor(); } public interface Resources extends ClientBundle, RemoteObjectTree.Resources { @Source("EvaluationPopupController.css") Css evaluationPopupControllerCss(); } static EvaluationPopupController create(Resources resources, Editor editor, EditorPopupController popupController, DebuggerState debuggerState) { return new EvaluationPopupController(resources, editor, popupController, debuggerState); } private class DebuggerListenerImpl implements DebuggerState.DebuggerStateListener, DebuggerState.EvaluateExpressionListener { private boolean isAttached; private final RemoverManager removerManager = new RemoverManager(); @Override public void onDebuggerStateChange() { handleOnDebuggerStateChange(); } @Override public void onEvaluateExpressionResponse(OnEvaluateExpressionResponse response) { handleOnEvaluateExpressionResponse(response); } @Override public void onGlobalObjectChanged() { hidePopup(); } boolean attach() { if (!isAttached) { isAttached = true; removerManager.track(debuggerState.getDebuggerStateListenerRegistrar().add(this)); removerManager.track(debuggerState.getEvaluateExpressionListenerRegistrar().add(this)); return true; } return false; } void detach() { if (isAttached) { isAttached = false; removerManager.remove(); } } } private final Css css; private final Editor editor; private final EditorPopupController popupController; private final DebuggerState debuggerState; private final DebuggerListenerImpl debuggerListener = new DebuggerListenerImpl(); private final EvaluableExpressionFinder expressionFinder = new EvaluableExpressionFinder(); private final RemoteObjectTree remoteObjectTree; private final RemoverManager removerManager = new RemoverManager(); private final Element popupRootElement; private DocumentParser documentParser; private EditorPopupController.Remover editorPopupControllerRemover; private LineInfo lastExpressionLineInfo; private EvaluableExpressionFinder.Result lastExpressionResult; private boolean awaitingExpressionEvaluation; private final EditorPopupController.PopupRenderer popupRenderer = new EditorPopupController.PopupRenderer() { @Override public Element renderDom() { RemoteObjectNode root = remoteObjectTree.getRoot(); RemoteObjectNode rootChild = (root == null ? null : root.getChildren().get(0)); // The left margin is different for expandable and non-expandable boolean expandable = (rootChild != null && rootChild.hasChildren()); if (expandable) { popupRootElement.getStyle().setMarginLeft(-7, CSSStyleDeclaration.Unit.PX); } else { popupRootElement.getStyle().setMarginLeft(-17, CSSStyleDeclaration.Unit.PX); } return popupRootElement; } }; private final MouseHoverManager.MouseHoverListener mouseHoverListener = new MouseHoverManager.MouseHoverListener() { @Override public void onMouseHover(int x, int y, LineInfo lineInfo, int column) { handleOnMouseHover(lineInfo, column); } }; private EvaluationPopupController(Resources resources, Editor editor, EditorPopupController popupController, DebuggerState debuggerState) { this.css = resources.evaluationPopupControllerCss(); this.editor = editor; this.popupController = popupController; this.debuggerState = debuggerState; this.remoteObjectTree = RemoteObjectTree.create( new RemoteObjectTree.View(resources), resources, debuggerState); this.popupRootElement = createPopupRootElement(); } private Element createPopupRootElement() { Element root = Elements.createDivElement(); CssUtils.setUserSelect(root, false); root.appendChild(remoteObjectTree.getView().getElement()); return root; } void setDocument(Document document, PathUtil path, @Nullable DocumentParser documentParser) { hidePopup(); if (path.getBaseName().endsWith(".js")) { this.documentParser = documentParser; if (debuggerListener.attach()) { // Initialize with the current debugger state. handleOnDebuggerStateChange(); } } else { this.documentParser = null; debuggerListener.detach(); removerManager.remove(); } } private void hidePopup() { if (editorPopupControllerRemover != null) { editorPopupControllerRemover.remove(); editorPopupControllerRemover = null; } lastExpressionLineInfo = null; lastExpressionResult = null; remoteObjectTree.setRoot(null); awaitingExpressionEvaluation = false; } private boolean isLastPopupVisible() { return editorPopupControllerRemover != null && editorPopupControllerRemover.isVisibleOrPending(); } private boolean registerNewFinderResult(LineInfo lineInfo, EvaluableExpressionFinder.Result result) { if (isLastPopupVisible() && lineInfo.equals(lastExpressionLineInfo) && lastExpressionResult != null && lastExpressionResult.getStartColumn() == result.getStartColumn() && lastExpressionResult.getEndColumn() == result.getEndColumn() && lastExpressionResult.getExpression().equals(result.getExpression())) { // The same result was found - no need to hide and reopen the same popup. popupController.cancelPendingHide(); return false; } hidePopup(); lastExpressionLineInfo = lineInfo; lastExpressionResult = result; awaitingExpressionEvaluation = true; return true; } private void handleOnDebuggerStateChange() { if (debuggerState.isPaused()) { removerManager.track(editor.getMouseHoverManager().addMouseHoverListener(mouseHoverListener)); } else { removerManager.remove(); hidePopup(); } } private void handleOnMouseHover(LineInfo lineInfo, int column) { EvaluableExpressionFinder.Result result = expressionFinder.find(lineInfo, column, documentParser); if (result != null) { if (registerNewFinderResult(lineInfo, result)) { debuggerState.evaluateExpression(result.getExpression()); } } else { hidePopup(); } } private void handleOnEvaluateExpressionResponse(OnEvaluateExpressionResponse response) { if (!awaitingExpressionEvaluation || isLastPopupVisible() || response.wasThrown() || lastExpressionLineInfo == null || lastExpressionResult == null || !lastExpressionResult.getExpression().equals(response.getExpression())) { return; } awaitingExpressionEvaluation = false; RemoteObjectNode newRoot = RemoteObjectNode.createRoot(); RemoteObjectNode child = new RemoteObjectNode.Builder(response.getExpression(), response.getResult()) .setDeletable(false) .build(); newRoot.addChild(child); remoteObjectTree.setRoot(newRoot); JsonArray<Element> popupPartnerElements = JsonCollections.createArray(remoteObjectTree.getContextMenuElement()); editorPopupControllerRemover = popupController.showPopup(lastExpressionLineInfo, lastExpressionResult.getStartColumn(), lastExpressionResult.getEndColumn(), css.popupAnchor(), popupRenderer, popupPartnerElements, VerticalAlign.BOTTOM, true /* shouldCaptureOutsideClickOnClose */, 0 /* delayMs */); } }