1 /*
2  * EnvironmentObjects.java
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 
16 package org.rstudio.studio.client.workbench.views.environment.view;
17 
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.List;
21 
22 import com.google.gwt.core.client.GWT;
23 import com.google.gwt.core.client.JsArray;
24 import com.google.gwt.core.client.JsArrayString;
25 import com.google.gwt.core.client.Scheduler;
26 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
27 import com.google.gwt.event.dom.client.ScrollEvent;
28 import com.google.gwt.event.dom.client.ScrollHandler;
29 import com.google.gwt.uibinder.client.UiBinder;
30 import com.google.gwt.uibinder.client.UiField;
31 import com.google.gwt.user.client.Timer;
32 import com.google.gwt.user.client.ui.*;
33 import com.google.gwt.view.client.ListDataProvider;
34 
35 import org.rstudio.core.client.Debug;
36 import org.rstudio.core.client.ElementIds;
37 import org.rstudio.core.client.theme.res.ThemeResources;
38 import org.rstudio.core.client.theme.res.ThemeStyles;
39 import org.rstudio.core.client.widget.FontSizer;
40 import org.rstudio.core.client.widget.Operation;
41 import org.rstudio.studio.client.common.SuperDevMode;
42 import org.rstudio.studio.client.workbench.views.environment.EnvironmentPane;
43 import org.rstudio.studio.client.workbench.views.environment.model.CallFrame;
44 import org.rstudio.studio.client.workbench.views.environment.model.RObject;
45 import org.rstudio.studio.client.workbench.views.environment.view.CallFramePanel.CallFramePanelHost;
46 
47 public class EnvironmentObjects extends ResizeComposite
48    implements CallFramePanelHost,
49               EnvironmentObjectDisplay.Host
50 {
51    private class ScrollIntoViewTimer extends Timer
52    {
53       @Override
run()54       public void run()
55       {
56          try
57          {
58             if (row_ >= 0 && row_ <= objectDisplay_.getVisibleItemCount())
59                objectDisplay_.getRowElement(row_).scrollIntoView();
60          }
61          catch (Exception e)
62          {
63             // silently drop exceptions as they are noisy + not actionable
64          }
65       }
66 
setRow(int row)67       public void setRow(int row)
68       {
69          row_ = row;
70       }
71 
72       private int row_ = 0;
73    }
74 
75    // Public interfaces -------------------------------------------------------
76 
77    public interface Binder extends UiBinder<Widget, EnvironmentObjects>
78    {
79    }
80 
81    // Constructor -------------------------------------------------------------
82 
EnvironmentObjects(EnvironmentObjectsObserver observer)83    public EnvironmentObjects(EnvironmentObjectsObserver observer)
84    {
85       observer_ = observer;
86       contextDepth_ = 0;
87       environmentName_ = EnvironmentPane.GLOBAL_ENVIRONMENT_NAME;
88 
89       objectDisplayType_ = OBJECT_LIST_VIEW;
90       objectDataProvider_ = new ListDataProvider<>();
91       objectSort_ = new RObjectEntrySort();
92 
93       // timer used to scroll table element into view
94       // a timer is required as we need to wait until table elements are
95       // rendered and visible before we can scroll into view; otherwise
96       // noisy exceptions will be emitted
97       //
98       // https://github.com/rstudio/rstudio/issues/5181
99       scrollTimer_ = new ScrollIntoViewTimer();
100 
101       // set up the call frame panel
102       callFramePanel_ = new CallFramePanel(observer_, this);
103 
104       initWidget(GWT.<Binder>create(Binder.class).createAndBindUi(this));
105 
106       splitPanel.addSouth(callFramePanel_, 150);
107       splitPanel.setWidgetMinSize(callFramePanel_, style.headerRowHeight());
108 
109       setObjectDisplay(objectDisplayType_);
110 
111       FontSizer.applyNormalFontSize(this);
112    }
113 
114    // Public methods ----------------------------------------------------------
115 
116    @Override
onResize()117    public void onResize()
118    {
119       super.onResize();
120       if (pendingCallFramePanelSize_)
121       {
122          autoSizeCallFramePanel();
123       }
124    }
125 
setContextDepth(int contextDepth)126    public void setContextDepth(int contextDepth)
127    {
128       if (contextDepth > 0)
129       {
130          splitPanel.setWidgetHidden(callFramePanel_, false);
131          splitPanel.onResize();
132       }
133       else if (contextDepth == 0)
134       {
135          callFramePanel_.clearCallFrames();
136          splitPanel.setWidgetHidden(callFramePanel_, true);
137       }
138       contextDepth_ = contextDepth;
139    }
140 
addObject(RObject obj)141    public void addObject(RObject obj)
142    {
143       int idx = indexOfExistingObject(obj.getName());
144       final RObjectEntry newEntry = entryFromRObject(obj);
145       boolean added = false;
146 
147       // if the object is already in the environment, just update the value
148       if (idx >= 0)
149       {
150          final RObjectEntry oldEntry = objectDataProvider_.getList().get(idx);
151 
152          if (oldEntry.rObject.getType() == obj.getType())
153          {
154             // type hasn't changed
155             if (oldEntry.expanded &&
156                 newEntry.contentsAreDeferred)
157             {
158                // we're replacing an object that has server-deferred contents--
159                // refill it immediately. (another approach would be to push the
160                // set of currently expanded objects to the server so these
161                // objects would show up on the client already expanded)
162                fillEntryContents(newEntry, idx, false);
163             }
164             else
165             {
166                // contents aren't deferred, just use the expanded state directly
167                newEntry.expanded = oldEntry.expanded;
168             }
169             objectDataProvider_.getList().set(idx, newEntry);
170             added = true;
171          }
172          else
173          {
174             // types did change, do a full add/remove
175             objectDataProvider_.getList().remove(idx);
176          }
177 
178       }
179       if (!added)
180       {
181          RObjectEntry entry = entryFromRObject(obj);
182          idx = indexOfNewObject(entry);
183          objectDataProvider_.getList().add(idx, entry);
184       }
185       updateCategoryLeaders(true);
186 
187       // scroll into view
188       scrollTimer_.setRow(idx);
189       scrollTimer_.schedule(100);
190    }
191 
removeObject(String objName)192    public void removeObject(String objName)
193    {
194       int idx = indexOfExistingObject(objName);
195       if (idx >= 0)
196       {
197          objectDataProvider_.getList().remove(idx);
198       }
199 
200       updateCategoryLeaders(true);
201    }
202 
clearObjects()203    public void clearObjects()
204    {
205       objectDataProvider_.getList().clear();
206    }
207 
clearSelection()208    public void clearSelection()
209    {
210       objectDisplay_.clearSelection();
211    }
212 
213    // bulk add for objects--used on init or environment switch
addObjects(JsArray<RObject> objects)214    public void addObjects(JsArray<RObject> objects)
215    {
216       // create an entry for each object and sort the array
217       int numObjects = objects.length();
218       ArrayList<RObjectEntry> objectEntryList = new ArrayList<>();
219       for (int i = 0; i < numObjects; i++)
220       {
221          RObjectEntry entry = entryFromRObject(objects.get(i));
222          objectEntryList.add(entry);
223       }
224       Collections.sort(objectEntryList, objectSort_);
225 
226       // push the list into the UI and update category leaders
227       objectDataProvider_.getList().addAll(objectEntryList);
228       updateCategoryLeaders(false);
229 
230       if (useStatePersistence())
231       {
232          setDeferredState();
233       }
234    }
235 
getSelectedObjects()236    public List<String> getSelectedObjects()
237    {
238       return objectDisplay_.getSelectedObjects();
239    }
240 
setCallFrames(JsArray<CallFrame> frameList, boolean autoSize)241    public void setCallFrames(JsArray<CallFrame> frameList, boolean autoSize)
242    {
243       callFramePanel_.setCallFrames(frameList, contextDepth_);
244 
245       // if not auto-sizing we're done
246       if (!autoSize)
247          return;
248 
249       // if the parent panel has layout information, auto-size the call frame
250       // panel (let GWT go first so the call frame panel visibility has
251       // taken effect)
252       if (splitPanel.getOffsetHeight() > 0)
253       {
254          Scheduler.get().scheduleDeferred(new ScheduledCommand()
255          {
256             @Override
257             public void execute()
258             {
259                autoSizeCallFramePanel();
260             }
261          });
262       }
263       else
264       {
265          // wait until the split panel has layout information to compute the
266          // correct size of the call frame panel
267          pendingCallFramePanelSize_ = true;
268       }
269    }
270 
setEnvironmentName(String environmentName)271    public void setEnvironmentName(String environmentName)
272    {
273       environmentName_ = environmentName;
274       if (objectDisplay_ != null)
275          objectDisplay_.setEnvironmentName(environmentName);
276    }
277 
getScrollPosition()278    public int getScrollPosition()
279    {
280       return objectDisplay_.getScrollPanel().getVerticalScrollPosition();
281    }
282 
setScrollPosition(int scrollPosition)283    public void setScrollPosition(int scrollPosition)
284    {
285       deferredScrollPosition_ = scrollPosition;
286    }
287 
setExpandedObjects(JsArrayString objects)288    public void setExpandedObjects(JsArrayString objects)
289    {
290       deferredExpandedObjects_ = objects;
291    }
292 
updateLineNumber(int newLineNumber)293    public void updateLineNumber (int newLineNumber)
294    {
295       callFramePanel_.updateLineNumber(newLineNumber);
296    }
297 
setFilterText(String filterText)298    public void setFilterText (String filterText)
299    {
300       filterText_ = filterText.toLowerCase();
301 
302       // Iterate over each entry in the list, and toggle its visibility based
303       // on whether it matches the current filter text.
304       List<RObjectEntry> objects = objectDataProvider_.getList();
305       for (int i = 0; i < objects.size(); i++)
306       {
307          RObjectEntry entry = objects.get(i);
308          boolean visible = matchesFilter(entry.rObject);
309          // Redraw the object if its visibility status has changed, or if it's
310          // visible (for visible entries we need to update the search highlight)
311          if (visible != entry.visible || visible)
312          {
313             entry.visible = visible;
314             redrawRowSafely(i);
315          }
316       }
317 
318       updateCategoryLeaders(true);
319    }
320 
getObjectDisplay()321    public int getObjectDisplay()
322    {
323       return objectDisplayType_;
324    }
325 
326    // Sets the object display type. Waits for the event loop to finish because
327    // of an apparent timing bug triggered by superdevmode (see case 3745).
setObjectDisplay(int type)328    public void setObjectDisplay(int type)
329    {
330       deferredObjectDisplayType_ = type;
331       Scheduler.get().scheduleDeferred(new ScheduledCommand()
332       {
333          @Override
334          public void execute()
335          {
336             setDeferredObjectDisplay();
337          }
338       });
339    }
340 
setDeferredObjectDisplay()341    private void setDeferredObjectDisplay()
342    {
343       if (deferredObjectDisplayType_ == null)
344       {
345          return;
346       }
347 
348       final int type = deferredObjectDisplayType_;
349 
350       // if we already have an active display of this type, do nothing
351       if (type == objectDisplayType_ &&
352           objectDisplay_ != null)
353       {
354          return;
355       }
356 
357       // clean up previous object display, if we had one
358       if (objectDisplay_ != null)
359       {
360          objectDataProvider_.removeDataDisplay(objectDisplay_);
361          splitPanel.remove(objectDisplay_);
362       }
363 
364       try
365       {
366          // create the new object display and wire it to the data source
367          if (type == OBJECT_LIST_VIEW)
368          {
369             objectDisplay_ = new EnvironmentObjectList(
370                                     this, observer_, environmentName_);
371             objectSort_.setSortType(RObjectEntrySort.SORT_AUTO);
372          }
373          else if (type == OBJECT_GRID_VIEW)
374          {
375             objectDisplay_ = new EnvironmentObjectGrid(
376                                     this, observer_, environmentName_);
377             objectSort_.setSortType(RObjectEntrySort.SORT_COLUMN);
378          }
379       }
380       catch (Throwable e)
381       {
382          // For reasons that are unclear, GWT sometimes barfs when trying to
383          // create the virtual scrollbars in the DataGrid that drives the
384          // environment list (it computes, and then tries to apply, a negative
385          // height). This appears to only happen during superdevmode boot,
386          // so try again (up to 5 times) if we're in superdevmode.
387 
388          if (SuperDevMode.isActive())
389          {
390             if (gridRenderRetryCount_ >= 5)
391             {
392                Debug.log("WARNING: Failed to render environment pane data grid");
393             }
394             gridRenderRetryCount_++;
395             Debug.log("WARNING: Retrying environment data grid render (" +
396                       gridRenderRetryCount_ + ")");
397             Timer t = new Timer() {
398                @Override
399                public void run()
400                {
401                   setObjectDisplay(type);
402                }
403             };
404             t.schedule(5);
405          }
406 
407          return;
408       }
409 
410       objectDisplayType_ = type;
411       Collections.sort(objectDataProvider_.getList(), objectSort_);
412       updateCategoryLeaders(false);
413       objectDataProvider_.addDataDisplay(objectDisplay_);
414 
415       objectDisplay_.getScrollPanel().addScrollHandler(new ScrollHandler()
416       {
417          @Override
418          public void onScroll(ScrollEvent event)
419          {
420             if (useStatePersistence())
421             {
422                deferredScrollPosition_ = getScrollPosition();
423                observer_.setPersistedScrollPosition(deferredScrollPosition_);
424             }
425          }
426       });
427 
428       objectDisplay_.setEmptyTableWidget(buildEmptyGridMessage());
429       objectDisplay_.addStyleName(style.objectGrid());
430       objectDisplay_.addStyleName(style.environmentPanel());
431       splitPanel.add(objectDisplay_);
432       deferredObjectDisplayType_ = null;
433    }
434 
435    // CallFramePanelHost implementation ---------------------------------------
436 
437    @Override
minimizeCallFramePanel()438    public void minimizeCallFramePanel()
439    {
440       callFramePanelHeight_ = splitPanel.getWidgetSize(callFramePanel_).intValue();
441       splitPanel.setWidgetSize(callFramePanel_, style.headerRowHeight());
442    }
443 
444    @Override
restoreCallFramePanel()445    public void restoreCallFramePanel()
446    {
447       splitPanel.setWidgetSize(callFramePanel_, callFramePanelHeight_);
448       callFramePanel_.onResize();
449    }
450 
451    @Override
getShowInternalFunctions()452    public boolean getShowInternalFunctions()
453    {
454       return observer_.getShowInternalFunctions();
455    }
456 
457    @Override
setShowInternalFunctions(boolean show)458    public void setShowInternalFunctions(boolean show)
459    {
460       observer_.setShowInternalFunctions(show);
461    }
462 
463    // EnvironmentObjectsDisplay.Host implementation ---------------------------
464 
465    @Override
enableClickableObjects()466    public boolean enableClickableObjects()
467    {
468       return contextDepth_ < 2;
469    }
470 
471    // we currently only set and/or get persisted state at the root context
472    // level.
473    @Override
useStatePersistence()474    public boolean useStatePersistence()
475    {
476       return environmentName_ == EnvironmentPane.GLOBAL_ENVIRONMENT_NAME;
477    }
478 
479    @Override
getFilterText()480    public String getFilterText()
481    {
482       return filterText_;
483    }
484 
485    @Override
getSortColumn()486    public int getSortColumn()
487    {
488       return objectSort_.getSortColumn();
489    }
490 
491    @Override
setSortColumn(int col)492    public void setSortColumn(int col)
493    {
494       objectSort_.setSortColumn(col);
495       observer_.setViewDirty();
496       Collections.sort(objectDataProvider_.getList(), objectSort_);
497    }
498 
499    @Override
toggleAscendingSort()500    public void toggleAscendingSort()
501    {
502       setAscendingSort(!objectSort_.getAscending());
503    }
504 
505    @Override
getAscendingSort()506    public boolean getAscendingSort()
507    {
508       return objectSort_.getAscending();
509    }
510 
setAscendingSort(boolean ascending)511    public void setAscendingSort(boolean ascending)
512    {
513       objectSort_.setAscending(ascending);
514       observer_.setViewDirty();
515       Collections.sort(objectDataProvider_.getList(), objectSort_);
516    }
517 
setSort(int column, boolean ascending)518    public void setSort(int column, boolean ascending)
519    {
520       objectSort_.setSortColumn(column);
521       objectSort_.setAscending(ascending);
522       Collections.sort(objectDataProvider_.getList(), objectSort_);
523    }
524 
525    @Override
fillEntryContents(final RObjectEntry entry, final int idx, boolean drawProgress)526    public void fillEntryContents(final RObjectEntry entry,
527                                  final int idx,
528                                  boolean drawProgress)
529    {
530       entry.expanded = false;
531       entry.isExpanding = true;
532       if (drawProgress)
533          redrawRowSafely(idx);
534       observer_.fillObjectContents(entry.rObject, new Operation() {
535          public void execute()
536          {
537             entry.expanded = true;
538             entry.isExpanding = false;
539             redrawRowSafely(idx);
540          }
541       });
542    }
543 
544    // Private methods: object management --------------------------------------
545 
indexOfExistingObject(String objectName)546    private int indexOfExistingObject(String objectName)
547    {
548       List<RObjectEntry> objects = objectDataProvider_.getList();
549 
550       // find the position of the object in the list--we can't use binary
551       // search here since we're matching on names and the list isn't sorted
552       // by name (it's sorted by type, then name)
553       int index;
554       boolean foundObject = false;
555       for (index = 0; index < objects.size(); index++)
556       {
557          if (objects.get(index).rObject.getName() == objectName)
558          {
559             foundObject = true;
560             break;
561          }
562       }
563 
564       return foundObject ? index : -1;
565    }
566 
567    // returns the position a new object entry should occupy in the table
indexOfNewObject(RObjectEntry obj)568    private int indexOfNewObject(RObjectEntry obj)
569    {
570       List<RObjectEntry> objects = objectDataProvider_.getList();
571       int numObjects = objects.size();
572       int idx;
573       // consider: can we use binary search here?
574       for (idx = 0; idx < numObjects; idx++)
575       {
576          if (objectSort_.compare(obj, objects.get(idx)) < 0)
577          {
578             break;
579          }
580       }
581       return idx;
582    }
583 
584    // after adds or removes, we need to tag the new category-leading objects
updateCategoryLeaders(boolean redrawUpdatedRows)585    private void updateCategoryLeaders(boolean redrawUpdatedRows)
586    {
587       // no need to do these model updates if we're not in the mode that
588       // displays them
589       if (objectDisplayType_ != OBJECT_LIST_VIEW)
590          return;
591 
592       List<RObjectEntry> objects = objectDataProvider_.getList();
593 
594       // whether or not we've found a leader for each category
595       Boolean[] leaders = { false, false, false, false };
596       boolean foundFirstObject = false;
597 
598       for (int i = 0; i < objects.size(); i++)
599       {
600          RObjectEntry entry = objects.get(i);
601          if (!entry.visible)
602             continue;
603          if (!foundFirstObject)
604          {
605             entry.isFirstObject = true;
606             foundFirstObject = true;
607          }
608          else
609          {
610             entry.isFirstObject = false;
611          }
612          int category = entry.getCategory();
613          Boolean leader = entry.isCategoryLeader;
614          // if we haven't found a leader for this category yet, make this object
615          // the leader if it isn't already
616          if (!leaders[category])
617          {
618             leaders[category] = true;
619             if (!leader)
620             {
621                entry.isCategoryLeader = true;
622             }
623          }
624          // if this object is marked as the leader but we've already found a
625          // leader, unmark it
626          else if (leader)
627          {
628             entry.isCategoryLeader = false;
629          }
630 
631          // if we changed the leader flag, redraw the row
632          if (leader != entry.isCategoryLeader
633              && redrawUpdatedRows)
634          {
635             redrawRowSafely(i);
636          }
637       }
638    }
639 
buildEmptyGridMessage()640    private Widget buildEmptyGridMessage()
641    {
642       ThemeStyles styles = ThemeResources.INSTANCE.themeStyles();
643       HTMLPanel messagePanel = new HTMLPanel("");
644       messagePanel.setStyleName(style.emptyEnvironmentPanel());
645       environmentEmptyMessage_ = new Label(EMPTY_ENVIRONMENT_MESSAGE);
646       environmentEmptyMessage_.setStyleName(styles.subtitle());
647       environmentEmptyMessage_.setStylePrimaryName(style.emptyEnvironmentMessage());
648       ElementIds.assignElementId(environmentEmptyMessage_, ElementIds.ENV_EMPTY);
649       messagePanel.add(environmentEmptyMessage_);
650       return messagePanel;
651    }
652 
autoSizeCallFramePanel()653    private void autoSizeCallFramePanel()
654    {
655       // after setting the frames, resize the call frame panel to neatly
656       // wrap the new list, up to a maximum of 2/3 of the height of the
657       // split panel.
658       int desiredCallFramePanelSize =
659             callFramePanel_.getDesiredPanelHeight();
660 
661       if (splitPanel.getOffsetHeight() > 0)
662       {
663          desiredCallFramePanelSize = Math.min(
664                  desiredCallFramePanelSize,
665                  (int)(0.66 * splitPanel.getOffsetHeight()));
666       }
667 
668       // if the panel is minimized, just update the cached height so it'll
669       // get set to what we want when/if the panel is restored
670       if (callFramePanel_.isMinimized())
671       {
672          callFramePanelHeight_ = desiredCallFramePanelSize;
673       }
674       else
675       {
676          splitPanel.setWidgetSize(
677                callFramePanel_, desiredCallFramePanelSize);
678          callFramePanel_.onResize();
679          if (objectDisplay_ != null)
680             objectDisplay_.onResize();
681       }
682 
683       pendingCallFramePanelSize_ = false;
684    }
685 
686 
687    // Private methods: state persistence --------------------------------------
688 
setDeferredState()689    private void setDeferredState()
690    {
691       Scheduler.get().scheduleDeferred(new ScheduledCommand()
692       {
693          @Override
694          public void execute()
695          {
696             if (deferredExpandedObjects_ != null)
697             {
698                // loop through the objects in the list and check to see if each
699                // is marked expanded in the persisted list of expanded objects
700                List<RObjectEntry> objects = objectDataProvider_.getList();
701                for (int idxObj = 0; idxObj < objects.size(); idxObj++)
702                {
703                   for (int idxExpanded = 0;
704                        idxExpanded < deferredExpandedObjects_.length();
705                        idxExpanded++)
706                   {
707                      if (objects.get(idxObj).rObject.getName() ==
708                          deferredExpandedObjects_.get(idxExpanded))
709                      {
710                         objects.get(idxObj).expanded = true;
711                         redrawRowSafely(idxObj);
712                      }
713                   }
714                }
715             }
716 
717             // set the cached scroll position
718             objectDisplay_.getScrollPanel().setVerticalScrollPosition(
719                     deferredScrollPosition_);
720 
721          }
722       });
723    }
724 
matchesFilter(RObject obj)725    private boolean matchesFilter(RObject obj)
726    {
727       if (filterText_.isEmpty())
728          return true;
729       return obj.getName().toLowerCase().contains(filterText_) ||
730              obj.getValue().toLowerCase().contains(filterText_);
731    }
732 
entryFromRObject(RObject obj)733    private RObjectEntry entryFromRObject(RObject obj)
734    {
735       return new RObjectEntry(obj, matchesFilter(obj));
736    }
737 
738    // for very large environments, the number of objects may exceed the number
739    // of physical rows; avoid redrawing rows outside the bounds of the
740    // container's physical limit
redrawRowSafely(int idx)741    private void redrawRowSafely(int idx)
742    {
743       boolean oob =
744             idx >= MAX_ENVIRONMENT_OBJECTS ||
745             idx >= objectDisplay_.getRowCount();
746 
747       if (oob)
748          return;
749 
750       objectDisplay_.redrawRow(idx);
751    }
752 
753    private final static String EMPTY_ENVIRONMENT_MESSAGE =
754            "Environment is empty";
755 
756    public static final int OBJECT_LIST_VIEW = 0;
757    public static final int OBJECT_GRID_VIEW = 1;
758 
759    @UiField EnvironmentStyle style;
760    @UiField SplitLayoutPanel splitPanel;
761 
762    EnvironmentObjectDisplay objectDisplay_;
763    CallFramePanel callFramePanel_;
764    Label environmentEmptyMessage_;
765 
766    private ListDataProvider<RObjectEntry> objectDataProvider_;
767    private RObjectEntrySort objectSort_;
768 
769    private EnvironmentObjectsObserver observer_;
770    private int contextDepth_;
771    private int callFramePanelHeight_;
772    private int objectDisplayType_ = OBJECT_LIST_VIEW;
773    private String filterText_ = "";
774    private String environmentName_;
775 
776    private ScrollIntoViewTimer scrollTimer_;
777 
778    // deferred settings--set on load but not applied until we have data.
779    private int deferredScrollPosition_ = 0;
780    private JsArrayString deferredExpandedObjects_;
781    private boolean pendingCallFramePanelSize_ = false;
782    private Integer deferredObjectDisplayType_ = OBJECT_LIST_VIEW;
783    private int gridRenderRetryCount_ = 0;
784 
785    public final static int MAX_ENVIRONMENT_OBJECTS = 1024;
786 }