/* * ObjectExplorerDataGrid.java * * Copyright (C) 2009-17 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.editors.explorer.view; import java.util.ArrayList; import java.util.List; import org.rstudio.core.client.Debug; import org.rstudio.core.client.JsVectorString; import org.rstudio.core.client.ListUtil; import org.rstudio.core.client.ListUtil.FilterPredicate; import org.rstudio.core.client.SafeHtmlUtil; import org.rstudio.core.client.StringUtil; import org.rstudio.core.client.dom.DomUtils; import org.rstudio.core.client.dom.DomUtils.ElementPredicate; import org.rstudio.core.client.theme.RStudioDataGridResources; import org.rstudio.core.client.theme.RStudioDataGridStyle; import org.rstudio.core.client.widget.VirtualizedDataGrid; import org.rstudio.studio.client.RStudioGinjector; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.server.ServerError; import org.rstudio.studio.client.server.ServerRequestCallback; import org.rstudio.studio.client.workbench.views.console.events.SendToConsoleEvent; import org.rstudio.studio.client.workbench.views.source.editors.explorer.ObjectExplorerServerOperations; /* * This widget provides a tabular, drill-down view into an R object. * * ## Columns * * ### Name * * The name column contains three elements: * * 1) An (optional) 'drill-down' icon, that expands the node such that * children of that object are shown and added to the table, * * 2) An icon, denoting the object's type (list, environment, etc.) * * 3) The object's name; that is, the binding through which is can be accessed * from the parent object. * * ### Type * * A text column, giving a short description of the object's type. Typically, this * will be the object's class, alongside the object's length (if relevant). * * ### Value * * A short, succinct description of the value of the object within. Icons that can * be used for interacting with this row (e.g. generating the R code that can access * that value) are drawn in this cell. */ import org.rstudio.studio.client.workbench.views.source.editors.explorer.model.ObjectExplorerHandle; import org.rstudio.studio.client.workbench.views.source.editors.explorer.model.ObjectExplorerInspectionResult; import org.rstudio.studio.client.workbench.views.source.editors.explorer.view.ObjectExplorerDataGrid.Data.ExpansionState; import org.rstudio.studio.client.workbench.views.source.model.SourceDocument; import com.google.gwt.cell.client.AbstractCell; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.JsArrayString; import com.google.gwt.dom.builder.shared.HtmlBuilderFactory; import com.google.gwt.dom.builder.shared.HtmlDivBuilder; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Style.Visibility; import com.google.gwt.dom.client.TableRowElement; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.logical.shared.AttachEvent; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.safehtml.shared.SafeHtmlUtils; import com.google.gwt.user.cellview.client.IdentityColumn; import com.google.gwt.user.cellview.client.RowHoverEvent; import com.google.gwt.user.cellview.client.TextHeader; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.Event; import com.google.gwt.view.client.CellPreviewEvent; import com.google.gwt.view.client.ListDataProvider; import com.google.inject.Inject; public class ObjectExplorerDataGrid extends VirtualizedDataGrid<ObjectExplorerDataGrid.Data> implements AttachEvent.Handler, ClickHandler, RowHoverEvent.Handler, CellPreviewEvent.Handler<ObjectExplorerDataGrid.Data> { private static interface Filter<T> { public boolean accept(T data); } public static class Data extends ObjectExplorerInspectionResult { protected Data() { } public static final native Data createMorePlaceholder(Data parent) /*-{ return { "parent": parent, "placeholder": true }; }-*/; public final native boolean isMorePlaceholder() /*-{ return !!this["placeholder"]; }-*/; public final boolean isAttributes() { return hasTag(TAG_ATTRIBUTES); } // The number of child rows that should be shown // for this node. public final native void setMaximumChildRowsShown(int limit) /*-{ this["maxChildRowsShown"] = limit; }-*/; public final int getMaximumChildRowsShown() { return getMaximumChildRowsShownImpl(DEFAULT_ROW_LIMIT); } private final native int getMaximumChildRowsShownImpl(int defaultLimit) /*-{ return this["maxChildRowsShown"] || defaultLimit; }-*/; // Whether this node is matched, according to the // current search query term. public final native void setMatched(boolean matched) /*-{ this["matched"] = matched; }-*/; public final native boolean isMatched() /*-{ return !!this["matched"]; }-*/; // The current expansion state of this row. // Rows can either be expanded (children are visible), // or not expanded (children are hidden). public final ExpansionState getExpansionState() { String state = getExpansionStateImpl(); return ExpansionState.valueOf(state.toUpperCase()); } private final native String getExpansionStateImpl() /*-{ return this["expansion_state"] || "closed"; }-*/; public final void setExpansionState(ExpansionState state) { setExpansionStateImpl(state.name()); } private final native void setExpansionStateImpl(String state) /*-{ this["expansion_state"] = state; }-*/; // Whether a particular row is visible (that is, whether // the CellTable should draw this row). private final native boolean isVisible() /*-{ var visible = this["visible"]; if (typeof visible === "undefined") return true; return visible; }-*/; public final native void setVisible(boolean visible) /*-{ this["visible"] = visible; }-*/; // The parent data associated with a node. public final native Data getParentData() /*-{ return this["parent"] || null; }-*/; public final native void setParentData(Data data) /*-{ this["parent"] = data; }-*/; // Whether this node has the parent object 'data'. public final native boolean hasParentData(Data data) /*-{ for (var parent = this["parent"]; parent != null; parent = parent["parent"]) { if (parent == data) return true; } return false; }-*/; public final native JsArray<Data> getChildrenData() /*-{ return this["children"] || null; }-*/; public final native void addChildrenData(JsArray<Data> data) /*-{ var children = this["children"] || []; for (var i = 0, n = data.length; i < n; i++) children.push(data[i]); this["children"] = children; }-*/; // Return the node's depth, or the number of parents. public final int getDepth() { int depth = 0; for (Data parent = getParentData(); parent != null; parent = parent.getParentData()) { depth++; } return depth; } // Used to update ownership of children within the tree. // This is necessary as nodes received from the server // side will not have marked ownership (no known parent). public final void updateChildOwnership() { updateChildOwnership(getChildrenData(), this); } private static final void updateChildOwnership(JsArray<Data> children, Data parent) { if (children == null) return; for (int i = 0, n = children.length(); i < n; i++) { // update parent data on this child Data child = children.get(i); child.setParentData(parent); // recurse updateChildOwnership(child.getChildrenData(), child); } } public final native void updateChildOwnershipImpl() /*-{ var children = this["children"] || []; for (var i = 0, n = children.length; i < n; i++) { var child = children[i]; child["parent"] = this; } }-*/; public enum ExpansionState { OPEN, CLOSED } } private String generateExtractingRCode(Data data, String finalReplacement) { if (data == null || data.isMorePlaceholder()) return null; // extract all access strings from data + parents List<String> accessors = new ArrayList<String>(); while (data != null && data.getObjectAccess() != null) { accessors.add(data.getObjectAccess()); data = data.getParentData(); } int n = accessors.size(); if (n == 0) return finalReplacement; // start building up access string by repeatedly // substituting in accessors String code = accessors.get(0); for (int i = 1; i < n; i++) code = code.replaceAll("#", accessors.get(i)); // finally, substitute in the original object code = code.replaceAll("#", finalReplacement); return code; } private String generateExtractingRCode(Data data) { return generateExtractingRCode(data, handle_.getTitle()); } private static final int getParentLimit(Data data) { Data parent = data.getParentData(); if (parent == null) return DEFAULT_ROW_LIMIT; return parent.getMaximumChildRowsShown(); } private class NameCell extends AbstractCell<Data> { public NameCell() { super(); String moreButtonCell = "<td><input type='button' value='More...' data-action='open'></input></td>"; moreButtonCellHtml_ = SafeHtmlUtils.fromTrustedString(moreButtonCell); } @Override public void render(Context context, Data data, SafeHtmlBuilder builder) { builder.append(TABLE_OPEN_TAG); builder.appendHtmlConstant("<tr>"); if (data == null || data.isMorePlaceholder()) { onNotExpandable(builder); addViewMoreIcon(builder); } else { addIndent(builder, data); addExpandIcon(builder, data); addName(builder, data); } builder.appendHtmlConstant("</tr>"); builder.appendHtmlConstant("</table>"); } private final void addIndent(SafeHtmlBuilder builder, Data data) { int indentPx = data.getDepth() * 10; if (indentPx == 0) return; String html = "<td style='width: " + indentPx + "px'></td>"; builder.appendHtmlConstant(html); } private final boolean onNotExpandable(SafeHtmlBuilder builder) { builder.appendHtmlConstant("<td style='width: 20px'></td>"); return false; } // Returns true if an icon was drawn; false if indent was drawn instead private final boolean addExpandIcon(final SafeHtmlBuilder builder, final Data data) { // bail if this node is not expandable if (!data.isExpandable()) return onNotExpandable(builder); // bail if we're not showing attributes, but a child attributes // node is the only thing that exists if (!showAttributes_) { // bail if the object is unnamed & atomic if (data.isAtomic() && !data.isNamed()) return onNotExpandable(builder); JsArray<Data> children = data.getChildrenData(); if (children != null && children.length() == 1) { Data child = children.get(0); if (child.hasTag(TAG_ATTRIBUTES)) return onNotExpandable(builder); } } // add expand button builder.appendHtmlConstant("<td style='width: 20px;'>"); switch (data.getExpansionState()) { case CLOSED: builder.append(SafeHtmlUtil.createOpenTag("div", "class", RES.dataGridStyle().spriteExpandIcon(), "data-action", ACTION_OPEN)); builder.appendHtmlConstant("</div>"); break; case OPEN: builder.append(SafeHtmlUtil.createOpenTag("div", "class", RES.dataGridStyle().spriteCollapseIcon(), "data-action", ACTION_CLOSE)); builder.appendHtmlConstant("</div>"); break; } builder.appendHtmlConstant("</td>"); return true; } private final void addName(SafeHtmlBuilder builder, Data data) { builder.appendHtmlConstant("<td>"); HtmlDivBuilder divBuilder = HtmlBuilderFactory.get().createDivBuilder(); divBuilder.title(data.getDisplayName()); JsVectorString classes = JsVectorString.createVector(); if (data.hasTag(TAG_VIRTUAL)) classes.push(RES.dataGridStyle().virtual()); if (!classes.isEmpty()) divBuilder.className(classes.join(" ")); String name = data.getDisplayName(); if (name == null) name = "<unknown>"; divBuilder.text(name); builder.append(divBuilder.asSafeHtml()); builder.appendHtmlConstant("</td>"); } private final void addViewMoreIcon(SafeHtmlBuilder builder) { builder.append(moreButtonCellHtml_); } private final SafeHtml moreButtonCellHtml_; } private static class TypeCell extends AbstractCell<Data> { @Override public void render(Context context, Data data, SafeHtmlBuilder builder) { if (data == null || data.isMorePlaceholder()) return; builder.append(SafeHtmlUtil.createDiv( "title", data.getDisplayType())); builder.appendEscaped(data.getDisplayType()); builder.appendHtmlConstant("</div>"); } } private static class ValueCell extends AbstractCell<Data> { @Override public void render(Context context, Data data, SafeHtmlBuilder builder) { if (data == null || data.isMorePlaceholder()) return; builder.append(TABLE_OPEN_TAG); builder.appendHtmlConstant("<tr>"); // add description builder.appendHtmlConstant("<td style='width: 100%;'>"); builder.append(SafeHtmlUtil.createDiv( "title", data.getDisplayDesc(), "class", RES.dataGridStyle().valueDesc())); builder.appendEscaped(data.getDisplayDesc()); builder.appendHtmlConstant("</div>"); builder.appendHtmlConstant("</td>"); // for data.frames and functions, add a 'View' icon JsVectorString classes = data.getObjectClass().cast(); for (String viewableClass : VIEWABLE_CLASSES) { if (classes.contains(viewableClass)) { builder.appendHtmlConstant("<td>"); addViewIcon(data, builder); builder.appendHtmlConstant("</td>"); break; } } // add extract icon builder.appendHtmlConstant("<td>"); addExtractIcon(data, builder); builder.appendHtmlConstant("</td>"); builder.appendHtmlConstant("</tr>"); builder.appendHtmlConstant("</table>"); } private void addExtractIcon(Data data, SafeHtmlBuilder builder) { SafeHtml extractTag = SafeHtmlUtil.createDiv( "class", CLASS + " " + RES.dataGridStyle().spriteExtractCodeIcon(), "style", "visibility: hidden", "data-action", ACTION_EXTRACT); builder.append(extractTag); builder.appendHtmlConstant("</div>"); } private void addViewIcon(Data data, SafeHtmlBuilder builder) { SafeHtml viewTag = SafeHtmlUtil.createDiv( "class", CLASS + " " + RES.dataGridStyle().spriteViewObjectIcon(), "style", "visibility: hidden", "data-action", ACTION_VIEW); builder.append(viewTag); builder.appendHtmlConstant("</div>"); } private static final String CLASS = RES.dataGridStyle().clickableIcon(); private static final String[] VIEWABLE_CLASSES = new String[] { "data.frame", "function" }; } public ObjectExplorerDataGrid(ObjectExplorerHandle handle, SourceDocument document) { super(RES); handle_ = handle; document_ = document; RStudioGinjector.INSTANCE.injectMembers(this); setSize("100%", "100%"); // add columns nameColumn_ = new IdentityColumn<Data>(new NameCell()); addColumn(nameColumn_, new TextHeader("Name")); setColumnWidth(nameColumn_, NAME_COLUMN_WIDTH + "px"); typeColumn_ = new IdentityColumn<Data>(new TypeCell()); addColumn(typeColumn_, new TextHeader("Type")); setColumnWidth(typeColumn_, TYPE_COLUMN_WIDTH + "px"); valueColumn_ = new IdentityColumn<Data>(new ValueCell()); addColumn(valueColumn_, new TextHeader("Value")); // set updater dataProvider_ = new ListDataProvider<Data>(); dataProvider_.setList(new ArrayList<Data>()); dataProvider_.addDataDisplay(this); // register handlers setKeyboardSelectionHandler(this); addAttachHandler(this); addRowHoverHandler(this); addDomHandler(this, ClickEvent.getType()); // populate the view once initially initializeRoot(); } @Inject private void initialize(ObjectExplorerServerOperations server, EventBus events) { server_ = server; events_ = events; } // Public methods ---- public void refresh() { initializeRoot(); } public void toggleShowAttributes(boolean showAttributes) { if (showAttributes == showAttributes_) return; showAttributes_ = showAttributes; synchronize(); } public void setFilter(String filter) { filter_ = filter; synchronize(); } // Handlers --- @Override public void onAttachOrDetach(AttachEvent event) { if (event.isAttached()) return; } @Override public void onClick(ClickEvent event) { // extract target element Element targetEl = event.getNativeEvent().getEventTarget().cast(); if (targetEl == null) return; // determine action associated with this row Element dataEl = DomUtils.findParentElement(targetEl, true, new DomUtils.ElementPredicate() { @Override public boolean test(Element el) { return el.hasAttribute("data-action"); } }); // find associated row index by looking up through the DOM Element rowEl = DomUtils.findParentElement(targetEl, new DomUtils.ElementPredicate() { @Override public boolean test(Element el) { return el.hasAttribute("__gwt_row"); } }); if (rowEl == null) return; int row = StringUtil.parseInt(rowEl.getAttribute("__gwt_row"), -1); if (row == -1) return; // if the user has clicked on the expand button, handle that if (dataEl != null) { // perform action String action = dataEl.getAttribute("data-action"); performAction(action, row); return; } // otherwise, just select the row the user clicked on setKeyboardSelectedRow(row); setKeyboardSelectedColumn(0); } @Override public void onCellPreview(CellPreviewEvent<Data> preview) { Event event = Event.getCurrentEvent(); int code = event.getKeyCode(); int type = event.getTypeInt(); int row = getKeyboardSelectedRow(); boolean isDefault = false; if (type == Event.ONKEYDOWN || type == Event.ONKEYPRESS) { switch (code) { case KeyCodes.KEY_UP: selectRowRelative(-1); break; case KeyCodes.KEY_DOWN: selectRowRelative(+1); break; case KeyCodes.KEY_PAGEUP: selectRowRelative(-10); break; case KeyCodes.KEY_PAGEDOWN: selectRowRelative(+10); break; case KeyCodes.KEY_LEFT: selectParentOrClose(row); break; case KeyCodes.KEY_RIGHT: selectChildOrOpen(row); break; default: isDefault = true; break; } } else if (type == Event.ONKEYUP) { switch (code) { case KeyCodes.KEY_ENTER: case KeyCodes.KEY_SPACE: toggleExpansion(row); break; default: isDefault = true; break; } } else { isDefault = true; } // eat any non-default handled events if (!isDefault) { preview.setCanceled(true); event.stopPropagation(); event.preventDefault(); } } @Override public void onRowHover(RowHoverEvent event) { TableRowElement rowEl = event.getHoveringRow(); Element[] buttonEls = DomUtils.getElementsByClassName(rowEl, RES.dataGridStyle().clickableIcon()); if (buttonEls == null) return; if (event.isUnHover()) { for (Element el : buttonEls) el.getStyle().setVisibility(Visibility.HIDDEN); // unset any element-specific maximum width that might've been set // on hover (see below) Element valueDescEl = DomUtils.getFirstElementWithClassName(rowEl, RES.dataGridStyle().valueDesc()); if (valueDescEl != null) valueDescEl.getParentElement().getStyle().setWidth(100, Unit.PCT); // unset hovered row hoveredRow_ = null; } else { for (Element el : buttonEls) el.getStyle().setVisibility(Visibility.VISIBLE); // set hovered row (so that we can respond to resize events) hoveredRow_ = rowEl; onResize(); } } @Override public void onResize() { super.onResize(); updateHoverRowWidth(); } private void updateHoverRowWidth() { if (hoveredRow_ == null) return; Element valueDescEl = DomUtils.getFirstElementWithClassName( hoveredRow_, RES.dataGridStyle().valueDesc()); if (valueDescEl == null) return; // iterate through other table cells to compute width of // buttons available here Element containingRowEl = DomUtils.findParentElement( valueDescEl, new ElementPredicate() { @Override public boolean test(Element el) { return el.hasTagName("tr"); } }); if (containingRowEl == null) return; // TODO: do a better job of automatically computing these widths, // rather than hard-coding them int buttonWidth = 0; int n = containingRowEl.getChildCount(); if (n == 2) buttonWidth = 20; else if (n == 3) buttonWidth = 48; int totalWidth = getOffsetWidth(); int remainingWidth = totalWidth - NAME_COLUMN_WIDTH - TYPE_COLUMN_WIDTH - buttonWidth - 20; Element parentEl = valueDescEl.getParentElement(); parentEl.getStyle().setPropertyPx("width", Math.max(0, remainingWidth)); } // Private Methods ---- private void selectRowRelative(int delta) { setKeyboardSelectedColumn(0); setKeyboardSelectedRow(getKeyboardSelectedRow() + delta); } private void selectParentOrClose(int row) { Data data = getData().get(row); // if this node has children and is currently expanded, close it if (data.isExpandable() && data.getExpansionState() == ExpansionState.OPEN) { closeRow(row); return; } // otherwise, select the parent associated with this row (if any) Data parent = data.getParentData(); if (parent == null) return; List<Data> list = getData(); for (int i = 0, n = row; i < n; i++) { if (list.get(i).equals(parent)) { setKeyboardSelectedRow(i); break; } } } private void selectChildOrOpen(int row) { Data data = getData().get(row); if (data.isMorePlaceholder()) { retrieveMore(row); return; } // if this node has children but is not expanded, expand it if (data.isExpandable() && data.getExpansionState() == ExpansionState.CLOSED) { openRow(row); return; } // otherwise, select the first child of this row (the next row) selectRowRelative(1); } private void toggleExpansion(int row) { Data data = getData().get(row); switch (data.getExpansionState()) { case OPEN: closeRow(row); break; case CLOSED: openRow(row); break; } } private void performAction(String action, int row) { if (action.equals(ACTION_OPEN)) { openRow(row); } else if (action.equals(ACTION_CLOSE)) { closeRow(row); } else if (action.equals(ACTION_EXTRACT)) { extractCode(row); } else if (action.equals(ACTION_VIEW)) { viewRow(row); } else { assert false : "Unexpected action '" + action + "' on row " + row; } } private void openRow(final int row) { final Data data = getData().get(row); if (data.isMorePlaceholder()) { retrieveMore(row); return; } // bail if we've attempted to open something non-expandable if (!data.isExpandable()) assert false: "Attempted to expand non-recursive row " + row; // toggle expansion state data.setExpansionState(ExpansionState.OPEN); // resolve children and show withChildren(data, false, new Command() { @Override public void execute() { // set all direct children as visible JsArray<Data> children = data.getChildrenData(); for (int i = 0, n = children.length(); i < n; i++) children.get(i).setVisible(true); // set attributes as visible if available Data attributes = data.getObjectAttributes().<Data>cast(); if (attributes != null) attributes.setVisible(true); // force update of data grid synchronize(); } }); } private void closeRow(int row) { final Data data = getData().get(row); // bail if we've attempted to close something non-expandable if (!data.isExpandable()) assert false: "Attempted to close non-recursive row " + row; // toggle expansion state data.setExpansionState(ExpansionState.CLOSED); // set direct children as non-visible withChildren(data, false, new Command() { @Override public void execute() { // set all direct children as invisible JsArray<Data> children = data.getChildrenData(); for (int i = 0, n = children.length(); i < n; i++) children.get(i).setVisible(false); // set attributes as invisible if available Data attributes = data.getObjectAttributes().<Data>cast(); if (attributes != null) attributes.setVisible(false); // force update of data grid synchronize(); } }); } private void extractCode(int row) { Data data = getData().get(row); String code = generateExtractingRCode(data); events_.fireEvent(new SendToConsoleEvent(code, false)); } private void viewRow(int row) { Data data = getData().get(row); String code = generateExtractingRCode(data); code = "View(" + code + ")"; events_.fireEvent(new SendToConsoleEvent(code, true)); } private void retrieveMore(int row) { Data data = getData().get(row); Data parent = data.getParentData(); if (parent == null) return; // select the previous row (so that we don't end up scrolling all over the place) selectRowRelative(row); // update the limit on the number of children we're showing parent.setMaximumChildRowsShown(parent.getMaximumChildRowsShown() + DEFAULT_ROW_LIMIT); withChildren(parent, true, new Command() { @Override public void execute() { synchronize(); } }); } private void withChildren(final Data data, final boolean forceRequest, final Command command) { // if we already have children, exit early JsArray<Data> children = data.getChildrenData(); if (!forceRequest && children != null) { if (command != null) command.execute(); return; } // no children; make a server RPC request and then call back String extractingCode = generateExtractingRCode(data, "`__OBJECT__`"); server_.explorerInspectObject( handle_.getId(), extractingCode, data.getDisplayName(), data.getObjectAccess(), data.getTags().<JsArrayString>cast(), data.getNumChildren(), new ServerRequestCallback<ObjectExplorerInspectionResult>() { @Override public void onResponseReceived(ObjectExplorerInspectionResult result) { // set parent ownership for children JsArray<Data> children = result.getChildren().cast(); data.addChildrenData(children); for (int i = 0, n = children.length(); i < n; i++) children.get(i).setParentData(data); // set parent ownership for attributes Data attributes = result.getObjectAttributes().<Data>cast(); if (attributes != null) { data.setObjectAttributes(attributes); attributes.setParentData(data); } // execute command if (command != null) command.execute(); } @Override public void onError(ServerError error) { Debug.logError(error); } }); } private void initializeRoot() { server_.explorerBeginInspect( handle_.getId(), handle_.getName(), new ServerRequestCallback<ObjectExplorerInspectionResult>() { @Override public void onResponseReceived(ObjectExplorerInspectionResult result) { root_ = result.cast(); root_.updateChildOwnership(); root_.setExpansionState(ExpansionState.OPEN); synchronize(); } @Override public void onError(ServerError error) { Debug.logError(error); } }); } private void synchronize() { final String filter = StringUtil.notNull(filter_).trim(); // only include visible data in the table // TODO: we should consider how to better handle // piecewise updates to the table, rather than // shunting a whole new list in List<Data> data = flatten(root_, new Filter<Data>() { @Override public boolean accept(Data data) { // detect if this matches the current filter if (!filter.isEmpty()) { data.setMatched(false); String[] fields = { data.getDisplayName(), data.getDisplayType(), data.getDisplayDesc() }; for (String field : fields) { int index = field.toLowerCase().indexOf(filter.toLowerCase()); if (index != -1) { data.setMatched(true); break; } } } // otherwise, check whether it's currently visible return data.isVisible(); } }); // remove entries that don't match filter if (!filter.isEmpty()) { data = ListUtil.filter(data, new FilterPredicate<Data>() { @Override public boolean test(Data object) { for (Data self = object; self != null; self = self.getParentData()) { if (self.isMatched()) return true; } return false; } }); } setData(data); redraw(); } @Override public int getRowHeight() { return 24; } @Override public int getTotalNumberOfRows() { if (dataProvider_ == null) return 0; return getData().size(); } public List<Data> getData() { return dataProvider_.getList(); } private void setData(List<Data> data) { dataProvider_.setList(data); } private final List<Data> flatten(Data data, Filter<Data> filter) { List<Data> list = new ArrayList<Data>(); flattenImpl(data, filter, list); return list; } private final void flattenImpl(Data data, Filter<Data> filter, List<Data> output) { // exit if we're not accepting data here if (filter != null && !filter.accept(data)) return; // add data output.add(data); // recurse through children JsArray<Data> children = data.getChildrenData(); if (children == null) return; // only add children within the drawing limit to this list int n = Math.min(children.length(), data.getMaximumChildRowsShown()); for (int i = 0; i < n; i++) flattenImpl(children.get(i), filter, output); // add a dummy 'More...' element boolean drawMore = data.getExpansionState() == ExpansionState.OPEN && data.isMoreAvailable(); if (drawMore) output.add(Data.createMorePlaceholder(data)); // add attributes if relevant if (showAttributes_) { Data attributes = data.getObjectAttributes().<Data>cast(); if (attributes != null) flattenImpl(data.getObjectAttributes().<Data>cast(), filter, output); } } // Members ---- private final ObjectExplorerHandle handle_; private Data root_; @SuppressWarnings("unused") private final SourceDocument document_; private final IdentityColumn<Data> nameColumn_; private final IdentityColumn<Data> typeColumn_; private final IdentityColumn<Data> valueColumn_; private final ListDataProvider<Data> dataProvider_; private TableRowElement hoveredRow_; private boolean showAttributes_; private String filter_; // Injected ---- private ObjectExplorerServerOperations server_; private EventBus events_; // Static Members ---- private static final int NAME_COLUMN_WIDTH = 180; private static final int TYPE_COLUMN_WIDTH = 180; private static final int DEFAULT_ROW_LIMIT = 200; private static final String ACTION_OPEN = "open"; private static final String ACTION_CLOSE = "close"; private static final String ACTION_EXTRACT = "extract"; private static final String ACTION_VIEW = "view"; private static final String TAG_ATTRIBUTES = "attributes"; private static final String TAG_VIRTUAL = "virtual"; // Resources, etc ---- public interface Resources extends RStudioDataGridResources { @Source({RStudioDataGridStyle.RSTUDIO_DEFAULT_CSS, "ObjectExplorerDataGrid.css"}) Styles dataGridStyle(); @Source("images/expandIcon.png") ImageResource expandIcon(); @Source("images/expandIcon_2x.png") ImageResource expandIcon2x(); @Source("images/collapseIcon.png") ImageResource collapseIcon(); @Source("images/collapseIcon_2x.png") ImageResource collapseIcon2x(); @Source("images/extractCode.png") ImageResource extractCode(); @Source("images/extractCode_2x.png") ImageResource extractCode2x(); @Source("images/viewObject.png") ImageResource viewObject(); @Source("images/viewObject_2x.png") ImageResource viewObject2x(); } public interface Styles extends RStudioDataGridStyle { String virtual(); String verticalAlignHelper(); String spriteExpandIcon(); String spriteCollapseIcon(); String spriteExtractCodeIcon(); String spriteViewObjectIcon(); String cellInnerTable(); String valueDesc(); String clickableIcon(); } private static final Resources RES = GWT.create(Resources.class); private static final SafeHtml TABLE_OPEN_TAG = SafeHtmlUtil.createOpenTag( "table", "class", RES.dataGridStyle().cellInnerTable()); static { RES.dataGridStyle().ensureInjected(); } }