// 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.popup; import com.google.collide.client.editor.Buffer; import com.google.collide.client.editor.Editor; import com.google.collide.client.editor.renderer.SingleChunkLineRenderer; import com.google.collide.client.ui.menu.AutoHideComponent; import com.google.collide.client.ui.menu.PositionController.HorizontalAlign; import com.google.collide.client.ui.menu.PositionController.Position; import com.google.collide.client.ui.menu.PositionController.PositionerBuilder; import com.google.collide.client.ui.menu.PositionController.VerticalAlign; import com.google.collide.client.ui.popup.Popup; import com.google.collide.client.util.Elements; 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.document.anchor.Anchor; import com.google.collide.shared.document.anchor.AnchorType; import elemental.css.CSSStyleDeclaration; import elemental.html.Element; import elemental.util.Timer; import javax.annotation.Nullable; /** * Controller for the editor-wide popup. */ public class EditorPopupController { private static final AnchorType START_ANCHOR_TYPE = AnchorType.create( EditorPopupController.class, "startAnchor"); private static final AnchorType END_ANCHOR_TYPE = AnchorType.create( EditorPopupController.class, "endAnchor"); /** * Interface for specifying an arbitrary renderer for the popup. */ public interface PopupRenderer { /** * @return rendered content of the popup */ Element renderDom(); } /** * Interface for controlling the popup after it has been shown * or scheduled to be shown. */ public interface Remover { /** * @return true if this popup is currently visible or timer is running * to make it visible */ public boolean isVisibleOrPending(); /** * Hides this popup, if it is currently shown or cancels pending show. */ public void remove(); } public static EditorPopupController create(Popup.Resources resources, Editor editor) { return new EditorPopupController(Popup.create(resources), editor); } private final Editor editor; private final Popup popup; /** Used to change the position of the popup each time it is shown */ private final PositionerBuilder positionerBuilder; private Remover currentPopupRemover; /** * A DIV that floats on top of the editor area. We attach the popup to this * element. */ private final Element popupDummyElement; /** * The {@link #popupDummyElement} is anchored between {@code #startAnchor} and * {@link #endAnchor} in the {@link #document} document. These variables are * tracked to properly detach the anchors from the original document. */ private Anchor startAnchor; private Anchor endAnchor; private Document document; private final Anchor.ShiftListener anchorShiftListener = new Anchor.ShiftListener() { @Override public void onAnchorShifted(Anchor anchor) { updatePopupDummyElementWidth(); } }; private final Buffer.ScrollListener scrollListener = new Buffer.ScrollListener() { @Override public void onScroll(Buffer buffer, int scrollTop) { hide(); } }; private EditorPopupController(Popup popup, Editor editor) { this.popup = popup; this.editor = editor; this.popupDummyElement = createPopupDummyElement(editor.getBuffer().getEditorLineHeight()); this.positionerBuilder = new PositionerBuilder().setPosition(Position.NO_OVERLAP) .setHorizontalAlign(HorizontalAlign.MIDDLE); popup.setAutoHideHandler(new AutoHideComponent.AutoHideHandler() { @Override public void onHide() { hide(); } public void onShow() { // do nothing } }); } public void cleanup() { hide(); } private static Element createPopupDummyElement(int lineHeight) { Element element = Elements.createDivElement(); CSSStyleDeclaration style = element.getStyle(); style.setDisplay(CSSStyleDeclaration.Display.INLINE_BLOCK); style.setPosition(CSSStyleDeclaration.Position.ABSOLUTE); style.setWidth(0, CSSStyleDeclaration.Unit.PX); style.setHeight(lineHeight, CSSStyleDeclaration.Unit.PX); style.setZIndex(1); // We do this so that custom CSS class (provided by textCssClassName in the #showPopup) // with cursor:pointer should work correctly. style.setProperty("pointer-events", "none"); return element; } private void attachPopupDummyElement(LineInfo lineInfo, int startColumn, int endColumn) { // Detach from the old document first, just in case. detachPopupDummyElement(); document = editor.getDocument(); startAnchor = document.getAnchorManager().createAnchor(START_ANCHOR_TYPE, lineInfo.line(), lineInfo.number(), startColumn); startAnchor.setRemovalStrategy(Anchor.RemovalStrategy.SHIFT); startAnchor.getShiftListenerRegistrar().add(anchorShiftListener); endAnchor = document.getAnchorManager().createAnchor(END_ANCHOR_TYPE, lineInfo.line(), lineInfo.number(), endColumn); endAnchor.setRemovalStrategy(Anchor.RemovalStrategy.SHIFT); endAnchor.getShiftListenerRegistrar().add(anchorShiftListener); editor.getBuffer().addAnchoredElement(startAnchor, popupDummyElement); updatePopupDummyElementWidth(); } private void updatePopupDummyElementWidth() { if (startAnchor != null && endAnchor != null) { Buffer buffer = editor.getBuffer(); int left = buffer.calculateColumnLeft(startAnchor.getLine(), startAnchor.getColumn()); popupDummyElement.getStyle().setWidth( buffer.calculateColumnLeft(endAnchor.getLine(), endAnchor.getColumn() + 1) - left, CSSStyleDeclaration.Unit.PX); } } private void detachPopupDummyElement() { if (startAnchor != null) { startAnchor.getShiftListenerRegistrar().remove(anchorShiftListener); editor.getBuffer().removeAnchoredElement(startAnchor, popupDummyElement); document.getAnchorManager().removeAnchor(startAnchor); startAnchor = null; } if (endAnchor != null) { endAnchor.getShiftListenerRegistrar().remove(anchorShiftListener); document.getAnchorManager().removeAnchor(endAnchor); endAnchor = null; } document = null; } /** * Shows the popup anchored to a given position in the line. * * @param lineInfo the line to anchor the popup to * @param startColumn start column in the line, inclusive * @param endColumn end column in the line, inclusive * @param textCssClassName class name to highlight the anchor, or {@code null} * if this is not needed * @param renderer the popup renderer * @param popupPartnerElements array of partner element of the popup * (i.e. those DOM elements where mouse hover will not trigger closing * the popup), or {@code null} if no additional partners should be * considered. Also see {@link AutoHideComponent#addPartner} * @param verticalAlign vertical align of the popup related to the line * @param shouldCaptureOutsideClickOnClose whether the popup should capture * and prevent clicks outside of it when it closes itself * @return an instance of {@link Remover} to control the popup */ public Remover showPopup(LineInfo lineInfo, int startColumn, int endColumn, @Nullable String textCssClassName, PopupRenderer renderer, final @Nullable JsonArray<Element> popupPartnerElements, final VerticalAlign verticalAlign, boolean shouldCaptureOutsideClickOnClose, int delayMs) { hide(); attachPopupDummyElement(lineInfo, startColumn, endColumn); final SingleChunkLineRenderer lineRenderer = textCssClassName == null ? null : SingleChunkLineRenderer.create(startAnchor, endAnchor, textCssClassName); popup.setContentElement(renderer.renderDom()); popup.setCaptureOutsideClickOnClose(shouldCaptureOutsideClickOnClose); setPopupPartnersEnabled(popupPartnerElements, true); final Timer showTimer = new Timer() { @Override public void run() { positionerBuilder.setVerticalAlign(verticalAlign); popup.show(positionerBuilder.buildAnchorPositioner(popupDummyElement)); if (lineRenderer != null) { editor.addLineRenderer(lineRenderer); requestRenderLines(lineRenderer); } } }; if (delayMs <= 0) { showTimer.run(); } else { showTimer.schedule(delayMs); } final com.google.collide.shared.util.ListenerRegistrar.Remover scrollListenerRemover = editor.getBuffer().getScrollListenerRegistrar().add(scrollListener); return (currentPopupRemover = new Remover() { @Override public boolean isVisibleOrPending() { return this == currentPopupRemover; } @Override public void remove() { showTimer.cancel(); if (isVisibleOrPending()) { currentPopupRemover = null; detachPopupDummyElement(); setPopupPartnersEnabled(popupPartnerElements, false); popup.destroy(); if (lineRenderer != null) { editor.removeLineRenderer(lineRenderer); requestRenderLines(lineRenderer); } scrollListenerRemover.remove(); } } }); } public void cancelPendingHide() { popup.cancelPendingHide(); } private void requestRenderLines(SingleChunkLineRenderer lineRenderer) { for (int i = lineRenderer.startLine(); i <= lineRenderer.endLine(); ++i) { editor.getRenderer().requestRenderLine( editor.getDocument().getLineFinder().findLine(i).line()); } } private void setPopupPartnersEnabled(@Nullable JsonArray<Element> partnerElements, boolean enable) { if (partnerElements != null) { for (int i = 0, n = partnerElements.size(); i < n; ++i) { Element element = partnerElements.get(i); if (enable) { popup.addPartner(element); popup.addPartnerClickTargets(element); } else { popup.removePartner(element); popup.removePartnerClickTargets(element); } } } } /** * Hides the popup if visible. */ public void hide() { if (currentPopupRemover != null) { currentPopupRemover.remove(); currentPopupRemover = null; } } }