org.rstudio.studio.client.workbench.views.source.DocumentOutlineWidget.java Source code

Java tutorial

Introduction

Here is the source code for org.rstudio.studio.client.workbench.views.source.DocumentOutlineWidget.java

Source

/*
 * DocumentOutlineWidget.java
 *
 * Copyright (C) 2009-12 by RStudio, Inc.
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */
package org.rstudio.studio.client.workbench.views.source;

import org.rstudio.core.client.CommandWithArg;
import org.rstudio.core.client.Counter;
import org.rstudio.core.client.HandlerRegistrations;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.dom.DomUtils;
import org.rstudio.core.client.theme.res.ThemeStyles;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.common.filetypes.TextFileType;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefsAccessor;
import org.rstudio.studio.client.workbench.views.source.editors.text.Scope;
import org.rstudio.studio.client.workbench.views.source.editors.text.ScopeFunction;
import org.rstudio.studio.client.workbench.views.source.editors.text.TextEditingTarget;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.CursorChangedEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.CursorChangedHandler;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.EditorThemeStyleChangedEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.ScopeTreeReadyEvent;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DockLayoutPanel;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Tree;
import com.google.gwt.user.client.ui.TreeItem;
import com.google.gwt.user.client.ui.Widget;
import com.google.inject.Inject;

public class DocumentOutlineWidget extends Composite implements EditorThemeStyleChangedEvent.Handler {
    public class VerticalSeparator extends Composite {
        public VerticalSeparator() {
            panel_ = new FlowPanel();
            panel_.addStyleName(RES.styles().leftSeparator());
            initWidget(panel_);
        }

        private final FlowPanel panel_;
    }

    private class DocumentOutlineTreeEntry extends Composite {
        public DocumentOutlineTreeEntry(Scope node, int depth) {
            node_ = node;
            FlowPanel panel = new FlowPanel();

            setIndent(depth);
            setLabel(node);

            panel.add(indent_);
            panel.add(label_);

            panel.addDomHandler(new ClickHandler() {
                @Override
                public void onClick(ClickEvent event) {
                    target_.setCursorPosition(node_.getPreamble());
                    target_.getDocDisplay().alignCursor(node_.getPreamble(), 0.1);

                    // Defer focus so it occurs after click has been fully handled
                    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
                        @Override
                        public void execute() {
                            target_.focus();
                        }
                    });
                }
            }, ClickEvent.getType());

            initWidget(panel);
        }

        private void setLabel(Scope node) {
            String text = "";
            if (node.isChunk()) {
                text = node.getChunkLabel();
                if (StringUtil.isNullOrEmpty(text))
                    text = "(" + node.getLabel().toLowerCase() + ")";
            } else if (node.isFunction()) {
                ScopeFunction asFunctionNode = (ScopeFunction) node;
                text = asFunctionNode.getFunctionName();
            } else if (node.isYaml()) {
                text = "Title";
            } else {
                text = node.getLabel();
            }

            if (label_ == null)
                label_ = new Label(text);
            else
                label_.setText(text);

            label_.addStyleName(RES.styles().nodeLabel());
            label_.addStyleName(ThemeStyles.INSTANCE.handCursor());

            label_.removeStyleName(RES.styles().nodeLabelChunk());
            label_.removeStyleName(RES.styles().nodeLabelSection());
            label_.removeStyleName(RES.styles().nodeLabelFunction());

            if (node.isChunk())
                label_.addStyleName(RES.styles().nodeLabelChunk());
            else if (node.isSection() && !node.isMarkdownHeader() && !node.isYaml())
                label_.addStyleName(RES.styles().nodeLabelSection());
            else if (node.isFunction())
                label_.addStyleName(RES.styles().nodeLabelFunction());
        }

        private void setIndent(int depth) {
            depth = Math.max(0, depth);
            String text = StringUtil.repeat(" ", depth * 2);
            if (indent_ == null)
                indent_ = new HTML(text);
            else
                indent_.setHTML(text);

            indent_.addStyleName(RES.styles().nodeLabel());
            indent_.getElement().getStyle().setFloat(Style.Float.LEFT);
        }

        public void update(Scope node, int depth) {
            node_ = node;
            setLabel(node);
            setIndent(depth);
        }

        public Scope getScopeNode() {
            return node_;
        }

        private Scope node_;
        private HTML indent_;
        private Label label_;
    }

    private class DocumentOutlineTreeItem extends TreeItem {
        public DocumentOutlineTreeItem(DocumentOutlineTreeEntry entry) {
            super(entry);
            entry_ = entry;
        }

        public DocumentOutlineTreeEntry getEntry() {
            return entry_;
        }

        private final DocumentOutlineTreeEntry entry_;
    }

    @Inject
    private void initialize(UIPrefs uiPrefs) {
        uiPrefs_ = uiPrefs;
    }

    public DocumentOutlineWidget(TextEditingTarget target) {
        RStudioGinjector.INSTANCE.injectMembers(this);

        emptyPlaceholder_ = new FlowPanel();
        emptyPlaceholder_.add(new Label("No outline available"));
        emptyPlaceholder_.addStyleName(RES.styles().emptyPlaceholder());

        container_ = new DockLayoutPanel(Unit.PX);
        container_.addStyleName(RES.styles().container());
        target_ = target;

        separator_ = new VerticalSeparator();
        container_.addWest(separator_, 4);

        // This is a somewhat hacky way of allowing the separator to 'fit'
        // to a size of 4px, but overflow an extra 4px (to provide extra
        // space for a mouse cursor to drag or resize)
        Element parent = separator_.getElement().getParentElement();
        parent.getStyle().setPaddingRight(4, Unit.PX);

        tree_ = new Tree();
        tree_.addStyleName(RES.styles().tree());

        panel_ = new FlowPanel();
        panel_.addStyleName(RES.styles().panel());
        panel_.add(tree_);

        container_.add(panel_);
        handlers_ = new HandlerRegistrations();
        initHandlers();

        initWidget(container_);
    }

    public Widget getLeftSeparator() {
        return separator_;
    }

    @Override
    public void onEditorThemeStyleChanged(EditorThemeStyleChangedEvent event) {
        updateStyles(container_, event.getStyle());
        updateStyles(emptyPlaceholder_, event.getStyle());
    }

    private void initHandlers() {
        handlers_.add(target_.getDocDisplay().addScopeTreeReadyHandler(new ScopeTreeReadyEvent.Handler() {
            @Override
            public void onScopeTreeReady(ScopeTreeReadyEvent event) {
                rebuildScopeTree(event.getScopeTree(), event.getCurrentScope());
                resetTreeStyles();
            }
        }));

        handlers_.add(target_.getDocDisplay().addCursorChangedHandler(new CursorChangedHandler() {
            @Override
            public void onCursorChanged(CursorChangedEvent event) {
                if (target_.getDocDisplay().isScopeTreeReady(event.getPosition().getRow())) {
                    currentScope_ = target_.getDocDisplay().getCurrentScope();
                    currentVisibleScope_ = getCurrentVisibleScope(currentScope_);
                    resetTreeStyles();
                }
            }
        }));

        handlers_.add(target_.addEditorThemeStyleChangedHandler(this));

        handlers_.add(uiPrefs_.shownSectionsInDocumentOutline().bind(new CommandWithArg<String>() {
            @Override
            public void execute(String prefValue) {
                rebuildScopeTreeOnPrefChange();
            }
        }));

    }

    private void updateStyles(Widget widget, Style computed) {
        Style outlineStyles = widget.getElement().getStyle();
        outlineStyles.setBackgroundColor(computed.getBackgroundColor());
        outlineStyles.setColor(computed.getColor());
    }

    private void addOrSetItem(Scope node, int depth, int index) {
        int treeSize = tree_.getItemCount();
        if (index < treeSize) {
            DocumentOutlineTreeItem item = (DocumentOutlineTreeItem) tree_.getItem(index);

            item.getEntry().update(node, depth);
        } else {
            tree_.addItem(createEntry(node, depth));
        }
    }

    private void setActiveWidget(Widget widget) {
        panel_.clear();
        panel_.add(widget);
    }

    private void rebuildScopeTreeOnPrefChange() {
        if (scopeTree_ == null || currentScope_ == null)
            return;

        rebuildScopeTree(scopeTree_, currentScope_);
    }

    private void rebuildScopeTree(JsArray<Scope> scopeTree, Scope currentScope) {
        scopeTree_ = scopeTree;
        currentScope_ = currentScope;
        currentVisibleScope_ = getCurrentVisibleScope(currentScope_);

        if (scopeTree_.length() == 0) {
            setActiveWidget(emptyPlaceholder_);
            return;
        }

        setActiveWidget(tree_);

        int h1Count = 0;
        for (int i = 0; i < scopeTree_.length(); i++) {
            Scope node = scopeTree_.get(i);
            if (node.isMarkdownHeader()) {
                if (node.getDepth() == 1)
                    h1Count++;
            }
        }

        int initialDepth = h1Count == 1 ? -1 : 0;

        Counter counter = new Counter(-1);
        for (int i = 0; i < scopeTree_.length(); i++)
            buildScopeTreeImpl(scopeTree_.get(i), initialDepth, counter);

        // Clean up leftovers in the tree. 
        int oldTreeSize = tree_.getItemCount();
        int newTreeSize = counter.increment();

        for (int i = oldTreeSize - 1; i >= newTreeSize; i--) {
            TreeItem item = tree_.getItem(i);
            if (item != null)
                item.remove();
        }
    }

    private void buildScopeTreeImpl(Scope node, int depth, Counter counter) {
        if (shouldDisplayNode(node))
            addOrSetItem(node, depth, counter.increment());

        JsArray<Scope> children = node.getChildren();
        for (int i = 0; i < children.length(); i++) {
            int newDepth = depth + 1;

            // Don't add extra indentation for items within namespaces
            if (node.isNamespace())
                newDepth--;

            buildScopeTreeImpl(children.get(i), newDepth, counter);
        }
    }

    private boolean isUnnamedNode(Scope node) {
        if (node.isChunk())
            return StringUtil.isNullOrEmpty(node.getChunkLabel());
        return StringUtil.isNullOrEmpty(node.getLabel());
    }

    private boolean shouldDisplayNode(Scope node) {
        String shownSectionsPref = uiPrefs_.shownSectionsInDocumentOutline().getGlobalValue();
        if (node.isChunk() && shownSectionsPref.equals(UIPrefsAccessor.DOC_OUTLINE_SHOW_SECTIONS_ONLY))
            return false;

        if (isUnnamedNode(node) && !shownSectionsPref.equals(UIPrefsAccessor.DOC_OUTLINE_SHOW_ALL))
            return false;

        // NOTE: the 'is*' items are not mutually exclusive
        if (node.isAnon() || node.isLambda() || node.isTopLevel())
            return false;

        // Don't show namespaces in the scope tree
        if (node.isNamespace())
            return false;

        // don't show R functions or R sections in .Rmd unless requested
        TextFileType fileType = target_.getDocDisplay().getFileType();
        if (!shownSectionsPref.equals(UIPrefsAccessor.DOC_OUTLINE_SHOW_ALL) && fileType.isRmd()) {
            if (node.isFunction())
                return false;

            if (node.isSection() && !node.isMarkdownHeader())
                return false;
        }

        // filter out anonymous functions
        // TODO: Annotate scope tree in such a way that this isn't necessary
        if (node.getLabel() != null && node.getLabel().startsWith("<function>"))
            return false;

        return node.isChunk() || node.isClass() || node.isFunction() || node.isNamespace() || node.isSection();
    }

    private void resetTreeStyles() {
        for (int i = 0; i < tree_.getItemCount(); i++)
            setTreeItemStyles((DocumentOutlineTreeItem) tree_.getItem(i));
    }

    private DocumentOutlineTreeItem createEntry(Scope node, int depth) {
        DocumentOutlineTreeEntry entry = new DocumentOutlineTreeEntry(node, depth);
        DocumentOutlineTreeItem item = new DocumentOutlineTreeItem(entry);
        setTreeItemStyles(item);
        return item;
    }

    private void setTreeItemStyles(DocumentOutlineTreeItem item) {
        Scope node = item.getEntry().getScopeNode();
        item.addStyleName(RES.styles().node());
        DomUtils.toggleClass(item.getElement(), RES.styles().activeNode(), isActiveNode(node));
    }

    private Scope getCurrentVisibleScope(Scope node) {
        for (; node != null && !node.isTopLevel(); node = node.getParentScope())
            if (shouldDisplayNode(node))
                return node;
        return null;
    }

    private boolean isActiveNode(Scope node) {
        return node != null && node.equals(currentVisibleScope_);
    }

    private final DockLayoutPanel container_;
    private final FlowPanel panel_;
    private final VerticalSeparator separator_;
    private final Tree tree_;
    private final FlowPanel emptyPlaceholder_;

    private final TextEditingTarget target_;
    private final HandlerRegistrations handlers_;

    private JsArray<Scope> scopeTree_;
    private Scope currentScope_;
    private Scope currentVisibleScope_;

    private UIPrefs uiPrefs_;

    // Styles, Resources etc. ----
    public interface Styles extends CssResource {
        String panel();

        String container();

        String leftSeparator();

        String emptyPlaceholder();

        String tree();

        String node();

        String activeNode();

        String activeParentNode();

        String nodeLabel();

        String nodeLabelChunk();

        String nodeLabelSection();

        String nodeLabelFunction();
    }

    public interface Resources extends ClientBundle {
        @Source("DocumentOutlineWidget.css")
        Styles styles();
    }

    private static Resources RES = GWT.create(Resources.class);
    static {
        RES.styles().ensureInjected();
    }

}