/*
* 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();
}
}