1 /*
2  * PaneLayoutPreferencesPane.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 package org.rstudio.studio.client.workbench.prefs.views;
16 
17 import com.google.gwt.aria.client.Roles;
18 import com.google.gwt.core.client.JsArrayString;
19 import com.google.gwt.event.dom.client.ChangeEvent;
20 import com.google.gwt.event.dom.client.ChangeHandler;
21 import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
22 import com.google.gwt.event.logical.shared.ValueChangeEvent;
23 import com.google.gwt.event.logical.shared.ValueChangeHandler;
24 import com.google.gwt.event.shared.HandlerRegistration;
25 import com.google.gwt.resources.client.ImageResource;
26 import com.google.gwt.user.client.ui.CheckBox;
27 import com.google.gwt.user.client.ui.Composite;
28 import com.google.gwt.user.client.ui.FlexTable;
29 import com.google.gwt.user.client.ui.FlowPanel;
30 import com.google.gwt.user.client.ui.Label;
31 import com.google.gwt.user.client.ui.ListBox;
32 import com.google.gwt.user.client.ui.ScrollPanel;
33 import com.google.gwt.user.client.ui.VerticalPanel;
34 import com.google.inject.Inject;
35 import com.google.inject.Provider;
36 import org.rstudio.core.client.Debug;
37 import org.rstudio.core.client.StringUtil;
38 import org.rstudio.core.client.prefs.RestartRequirement;
39 import org.rstudio.core.client.resources.ImageResource2x;
40 import org.rstudio.core.client.widget.FormLabel;
41 import org.rstudio.core.client.widget.ScrollPanelWithClick;
42 import org.rstudio.core.client.widget.Toolbar;
43 import org.rstudio.core.client.widget.ToolbarButton;
44 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
45 import org.rstudio.studio.client.workbench.prefs.model.UserPrefsAccessor;
46 import org.rstudio.studio.client.workbench.ui.PaneConfig;
47 import org.rstudio.studio.client.workbench.ui.PaneManager;
48 
49 import java.util.ArrayList;
50 
51 public class PaneLayoutPreferencesPane extends PreferencesPane
52 {
53    class ExclusiveSelectionMaintainer
54    {
55       class ListChangeHandler implements ChangeHandler
56       {
ListChangeHandler(int whichList)57          ListChangeHandler(int whichList)
58          {
59             whichList_ = whichList;
60          }
61 
onChange(ChangeEvent event)62          public void onChange(ChangeEvent event)
63          {
64             int selectedIndex = lists_[whichList_].getSelectedIndex();
65 
66             for (int i = 0; i < lists_.length; i++)
67             {
68                if (i != whichList_
69                    && lists_[i].getSelectedIndex() == selectedIndex)
70                {
71                   lists_[i].setSelectedIndex(notSelectedIndex());
72                }
73             }
74 
75             updateTabSetPositions();
76          }
77 
notSelectedIndex()78          private Integer notSelectedIndex()
79          {
80             boolean[] seen = new boolean[4];
81             for (ListBox listBox : lists_)
82                seen[listBox.getSelectedIndex()] = true;
83             for (int i = 0; i < seen.length; i++)
84                if (!seen[i])
85                   return i;
86             return null;
87          }
88 
89          private final int whichList_;
90       }
91 
ExclusiveSelectionMaintainer(ListBox[] lists)92       ExclusiveSelectionMaintainer(ListBox[] lists)
93       {
94          lists_ = lists;
95          for (int i = 0; i < lists.length; i++)
96             lists[i].addChangeHandler(new ListChangeHandler(i));
97       }
98 
99       private final ListBox[] lists_;
100    }
101 
102    class ModuleList extends Composite implements ValueChangeHandler<Boolean>,
103                                                  HasValueChangeHandlers<ArrayList<Boolean>>
104    {
ModuleList(String width)105       ModuleList(String width)
106       {
107          checkBoxes_ = new ArrayList<>();
108          FlowPanel flowPanel = new FlowPanel();
109          for (String module : PaneConfig.getAllTabs())
110          {
111             CheckBox checkBox = new CheckBox(module, false);
112             checkBox.addValueChangeHandler(this);
113             checkBoxes_.add(checkBox);
114             flowPanel.add(checkBox);
115             if (module == "Presentation")
116                checkBox.setVisible(false);
117          }
118 
119          ScrollPanel scrollPanel = new ScrollPanelWithClick();
120          scrollPanel.setStyleName(res_.styles().paneLayoutTable());
121          scrollPanel.setWidth(width);
122          scrollPanel.add(flowPanel);
123          initWidget(scrollPanel);
124       }
125 
onValueChange(ValueChangeEvent<Boolean> event)126       public void onValueChange(ValueChangeEvent<Boolean> event)
127       {
128          ValueChangeEvent.fire(this, getSelectedIndices());
129       }
130 
getSelectedIndices()131       public ArrayList<Boolean> getSelectedIndices()
132       {
133          ArrayList<Boolean> results = new ArrayList<>();
134          for (CheckBox checkBox : checkBoxes_)
135             results.add(checkBox.getValue());
136          return results;
137       }
138 
setSelectedIndices(ArrayList<Boolean> selected)139       public void setSelectedIndices(ArrayList<Boolean> selected)
140       {
141          for (int i = 0; i < selected.size(); i++)
142             checkBoxes_.get(i).setValue(selected.get(i), false);
143       }
144 
getValue()145       public ArrayList<String> getValue()
146       {
147          ArrayList<String> value = new ArrayList<>();
148          for (CheckBox checkBox : checkBoxes_)
149          {
150             if (checkBox.getValue())
151                value.add(checkBox.getText());
152          }
153          return value;
154       }
155 
setValue(ArrayList<String> tabs)156       public void setValue(ArrayList<String> tabs)
157       {
158          for (CheckBox checkBox : checkBoxes_)
159             checkBox.setValue(tabs.contains(checkBox.getText()), false);
160       }
161 
presentationVisible()162       public boolean presentationVisible()
163       {
164          if (checkBoxes_.size() <= 0)
165             return false;
166 
167          CheckBox lastCheckBox = checkBoxes_.get(checkBoxes_.size() - 1);
168          return StringUtil.equals(lastCheckBox.getText(), "Presentation") &&
169                                   lastCheckBox.isVisible();
170       }
171 
addValueChangeHandler( ValueChangeHandler<ArrayList<Boolean>> handler)172       public HandlerRegistration addValueChangeHandler(
173             ValueChangeHandler<ArrayList<Boolean>> handler)
174       {
175          return addHandler(handler, ValueChangeEvent.getType());
176       }
177 
178       private final ArrayList<CheckBox> checkBoxes_;
179    }
180 
181 
182    @Inject
PaneLayoutPreferencesPane(PreferencesDialogResources res, UserPrefs userPrefs, Provider<PaneManager> pPaneManager)183    public PaneLayoutPreferencesPane(PreferencesDialogResources res,
184                                     UserPrefs userPrefs,
185                                     Provider<PaneManager> pPaneManager)
186    {
187       res_ = res;
188       userPrefs_ = userPrefs;
189       paneManager_ = pPaneManager.get();
190 
191       PaneConfig paneConfig = userPrefs.panes().getGlobalValue().cast();
192       additionalColumnCount_ = paneConfig.getAdditionalSourceColumns();
193 
194       add(new Label("Choose the layout of the panels in RStudio by selecting from the controls in" +
195          " each panel. Add up to three additional Source Columns to the left side of the layout. " +
196          "When a column is removed, all saved files within the column are closed and any unsaved " +
197          "files are moved to the main Source Pane.",
198          true));
199 
200       Toolbar columnToolbar = new Toolbar("Manage Column Display");
201       columnToolbar.setStyleName(res_.styles().newSection());
202       columnToolbar.setHeight("20px");
203 
204       ToolbarButton addButton = new ToolbarButton(
205          "Add Column",
206          "Add column",
207          res_.iconAddSourcePane());
208       if (displayColumnCount_ > PaneManager.MAX_COLUMN_COUNT - 1 ||
209          !userPrefs.allowSourceColumns().getGlobalValue())
210          addButton.setEnabled(false);
211 
212       ToolbarButton removeButton = new ToolbarButton(
213          "Remove Column",
214          "Remove column",
215          res_.iconRemoveSourcePane());
216       removeButton.setEnabled(additionalColumnCount_ > 0);
217 
218       addButton.addClickHandler(event ->
219       {
220          dirty_ = true;
221          updateTable(displayColumnCount_ + 1);
222 
223          if (displayColumnCount_ > PaneManager.MAX_COLUMN_COUNT - 1)
224             addButton.setEnabled(false);
225          if (!removeButton.isEnabled())
226             removeButton.setEnabled(true);
227       });
228 
229       removeButton.addClickHandler(event ->
230       {
231          dirty_ = true;
232          updateTable(displayColumnCount_ - 1);
233 
234          if (displayColumnCount_ < 1)
235             removeButton.setEnabled(false);
236          if (!addButton.isEnabled())
237             addButton.setEnabled(true);
238       });
239 
240       columnToolbar.addLeftWidget(addButton);
241       columnToolbar.addLeftSeparator();
242       columnToolbar.addLeftWidget(removeButton);
243       columnToolbar.addLeftSeparator();
244       add(columnToolbar);
245 
246       String[] visiblePanes = PaneConfig.getVisiblePanes();
247 
248       leftTop_ = new ListBox();
249       Roles.getListboxRole().setAriaLabelProperty(leftTop_.getElement(), "Top left panel");
250       leftBottom_ = new ListBox();
251       Roles.getListboxRole().setAriaLabelProperty(leftBottom_.getElement(), "Bottom left panel");
252       rightTop_ = new ListBox();
253       Roles.getListboxRole().setAriaLabelProperty(rightTop_.getElement(), "Top right panel");
254       rightBottom_ = new ListBox();
255       Roles.getListboxRole().setAriaLabelProperty(rightBottom_.getElement(), "Bottom right panel");
256       visiblePanes_ = new ListBox[]{leftTop_, leftBottom_, rightTop_, rightBottom_};
257       for (ListBox lb : visiblePanes_)
258       {
259          for (String value : visiblePanes)
260             lb.addItem(value);
261       }
262 
263       if (paneConfig == null || !paneConfig.validateAndAutoCorrect())
264          userPrefs.panes().setGlobalValue(PaneConfig.createDefault(), false);
265 
266       JsArrayString origPanes = userPrefs.panes().getGlobalValue().getQuadrants();
267       for (int i = 0; i < 4; i++)
268       {
269          boolean success = selectByValue(visiblePanes_[i], origPanes.get(i));
270          if (!success)
271          {
272             Debug.log("Bad config! Falling back to a reasonable default");
273             leftTop_.setSelectedIndex(0);
274             leftBottom_.setSelectedIndex(1);
275             rightTop_.setSelectedIndex(2);
276             rightBottom_.setSelectedIndex(3);
277             break;
278          }
279       }
280 
281       new ExclusiveSelectionMaintainer(visiblePanes_);
282 
283       for (ListBox lb : visiblePanes_)
284          lb.addChangeHandler(event -> dirty_ = true);
285 
286       String paneWidth = updateTable(additionalColumnCount_);
287 
288       visiblePanePanels_ = new VerticalPanel[] {leftTopPanel_, leftBottomPanel_,
289                                             rightTopPanel_, rightBottomPanel_};
290 
291       tabSet1ModuleList_ = new ModuleList(paneWidth);
292       tabSet1ModuleList_.setValue(toArrayList(userPrefs.panes().getGlobalValue().getTabSet1()));
293       tabSet2ModuleList_ = new ModuleList(paneWidth);
294       tabSet2ModuleList_.setValue(toArrayList(userPrefs.panes().getGlobalValue().getTabSet2()));
295       hiddenTabSetModuleList_ = new ModuleList(paneWidth);
296       hiddenTabSetModuleList_.setValue(toArrayList(
297                userPrefs.panes().getGlobalValue().getHiddenTabSet()));
298 
299       ValueChangeHandler<ArrayList<Boolean>> vch = new ValueChangeHandler<ArrayList<Boolean>>()
300       {
301          public void onValueChange(ValueChangeEvent<ArrayList<Boolean>> e)
302          {
303             dirty_ = true;
304 
305             ModuleList source = (ModuleList) e.getSource();
306             ModuleList other = (source == tabSet1ModuleList_)
307                                ? tabSet2ModuleList_
308                                : tabSet1ModuleList_;
309 
310             // an index should only be on for one of these lists,
311             ArrayList<Boolean> indices = source.getSelectedIndices();
312             ArrayList<Boolean> otherIndices = other.getSelectedIndices();
313             ArrayList<Boolean> hiddenIndices = hiddenTabSetModuleList_.getSelectedIndices();
314             if (!PaneConfig.isValidConfig(source.getValue()))
315             {
316                // when the configuration is invalid, we must reset sources to the prior valid
317                // configuration based on the values of the other two lists
318                for (int i = 0; i < indices.size(); i++)
319                   indices.set(i, !(otherIndices.get(i) || hiddenIndices.get(i)));
320                source.setSelectedIndices(indices);
321             }
322             else
323             {
324                for (int i = 0; i < indices.size(); i++)
325                {
326                   if (indices.get(i))
327                   {
328                      otherIndices.set(i, false);
329                      hiddenIndices.set(i, false);
330                   }
331                   else if (!otherIndices.get(i))
332                      hiddenIndices.set(i, true);
333                }
334                other.setSelectedIndices(otherIndices);
335                hiddenTabSetModuleList_.setSelectedIndices(hiddenIndices);
336 
337                updateTabSetLabels();
338             }
339          }
340       };
341       tabSet1ModuleList_.addValueChangeHandler(vch);
342       tabSet2ModuleList_.addValueChangeHandler(vch);
343 
344       updateTabSetPositions();
345       updateTabSetLabels();
346    }
347 
updateTable(int newCount)348    private String updateTable(int newCount)
349    {
350       // nothing has changed since the last update
351       if (grid_ != null && displayColumnCount_ == newCount)
352          return "";
353 
354       // cells will be twice a wide as columns to preserve space
355       double columnCount = newCount + (2 * GRID_PANE_COUNT);
356       double columnWidthValue = (double)TABLE_WIDTH / columnCount;
357       double cellWidthValue = columnWidthValue * GRID_PANE_COUNT;
358 
359       // If the column width is bigger than MAX_COLUMN_WIDTH, give space back to the panes
360       if (newCount > 0 && Math.min(columnWidthValue, MAX_COLUMN_WIDTH) != columnWidthValue)
361       {
362          double extra = (newCount * (columnWidthValue - MAX_COLUMN_WIDTH)) / GRID_PANE_COUNT;
363          cellWidthValue += extra;
364          columnWidthValue = MAX_COLUMN_WIDTH;
365       }
366       cellWidthValue -= (GRID_CELL_SPACING + GRID_CELL_PADDING);
367       columnWidthValue -= (GRID_CELL_SPACING + GRID_CELL_PADDING);
368 
369       final String columnWidth = columnWidthValue + "px";
370       final String cellWidth = cellWidthValue + "px";
371       final String selectWidth = (cellWidthValue - GRID_SELECT_PADDING) + "px";
372       leftTop_.setWidth(selectWidth);
373       leftBottom_.setWidth(selectWidth);
374       rightTop_.setWidth(selectWidth);
375       rightBottom_.setWidth(selectWidth);
376 
377       // create grid
378       if (grid_ == null)
379       {
380          grid_ = new FlexTable();
381          grid_.addStyleName(res_.styles().paneLayoutTable());
382          grid_.setCellSpacing(GRID_CELL_SPACING);
383          grid_.setCellPadding(GRID_CELL_PADDING);
384          Roles.getGridRole().setAriaLabelProperty(grid_.getElement(), "Columns and Panes Layout");
385 
386          // the two rows have a different number of columns
387          // because the source columns only use one
388          int topColumn;
389          for (topColumn = 0; topColumn < newCount; topColumn++)
390          {
391             ScrollPanel sp = createColumn();
392             grid_.setWidget(0, topColumn, sp);
393             grid_.getFlexCellFormatter().setRowSpan(0, topColumn, 2);
394             grid_.getCellFormatter().setStyleName(0, topColumn, res_.styles().paneLayoutTable());
395             grid_.getColumnFormatter().setWidth(topColumn, columnWidth);
396          }
397 
398          grid_.setWidget(0, topColumn, leftTopPanel_ = createPane(leftTop_));
399          grid_.getCellFormatter().setStyleName(0, topColumn, res_.styles().paneLayoutTable());
400 
401          grid_.setWidget(0, ++topColumn, rightTopPanel_ = createPane(rightTop_));
402          grid_.getCellFormatter().setStyleName(0, topColumn, res_.styles().paneLayoutTable());
403 
404          int bottomColumn = 0;
405          grid_.setWidget(1, bottomColumn, leftBottomPanel_ = createPane(leftBottom_));
406          grid_.getCellFormatter().setStyleName(1, bottomColumn, res_.styles().paneLayoutTable());
407 
408          grid_.setWidget(1, ++bottomColumn, rightBottomPanel_ = createPane(rightBottom_));
409          grid_.getCellFormatter().setStyleName(1, bottomColumn, res_.styles().paneLayoutTable());
410 
411          add(grid_);
412          displayColumnCount_ = newCount;
413          return cellWidth;
414       }
415 
416       // adjust existing grid
417       int difference = newCount - displayColumnCount_;
418       displayColumnCount_ = newCount;
419 
420       // when the number of columns has decreased, remove columns
421       for (int i = 0; i > difference; i--)
422          grid_.removeCell(0, i);
423 
424       // when the number of columns has increased, add columns
425       for (int i = 0; i < difference; i++)
426       {
427          ScrollPanel sp = createColumn();
428          grid_.insertCell(0, 0);
429          grid_.setWidget(0, 0, sp);
430          grid_.getFlexCellFormatter().setRowSpan(0, 0, 2);
431          grid_.getCellFormatter().setStyleName(0, 0, res_.styles().paneLayoutTable());
432       }
433 
434       // update the widths
435       for (int i = 0; i < newCount; i++)
436          grid_.getCellFormatter().setWidth(0, i, columnWidth);
437       tabSet1ModuleList_.setWidth(cellWidth);
438       tabSet2ModuleList_.setWidth(cellWidth);
439 
440       return cellWidth;
441    }
442 
createPane(ListBox listBox)443    private VerticalPanel createPane(ListBox listBox)
444    {
445       VerticalPanel vp = new VerticalPanel();
446       vp.add(listBox);
447       return vp;
448    }
449 
createColumn()450    private ScrollPanel createColumn()
451    {
452       VerticalPanel verticalPanel = new VerticalPanel();
453       FormLabel label = new FormLabel();
454       label.setText(UserPrefsAccessor.Panes.QUADRANTS_SOURCE);
455       label.setStyleName(res_.styles().label());
456       verticalPanel.add(label);
457 
458       ScrollPanel sp = new ScrollPanel();
459       sp.add(verticalPanel);
460       Roles.getTextboxRole().setAriaLabelProperty(sp.getElement(), "Additional source column");
461 
462       return sp;
463    }
464 
selectByValue(ListBox listBox, String value)465    private static boolean selectByValue(ListBox listBox, String value)
466    {
467       for (int i = 0; i < listBox.getItemCount(); i++)
468       {
469          if (listBox.getValue(i) == value)
470          {
471             listBox.setSelectedIndex(i);
472             return true;
473          }
474       }
475 
476       return false;
477    }
478 
479    @Override
getIcon()480    public ImageResource getIcon()
481    {
482       return new ImageResource2x(res_.iconPanes2x());
483    }
484 
485    @Override
initialize(UserPrefs prefs)486    protected void initialize(UserPrefs prefs)
487    {
488    }
489 
490    @Override
onApply(UserPrefs rPrefs)491    public RestartRequirement onApply(UserPrefs rPrefs)
492    {
493       RestartRequirement restartRequirement = super.onApply(rPrefs);
494 
495       if (dirty_)
496       {
497          JsArrayString panes = JsArrayString.createArray().cast();
498          panes.push(leftTop_.getValue(leftTop_.getSelectedIndex()));
499          panes.push(leftBottom_.getValue(leftBottom_.getSelectedIndex()));
500          panes.push(rightTop_.getValue(rightTop_.getSelectedIndex()));
501          panes.push(rightBottom_.getValue(rightBottom_.getSelectedIndex()));
502 
503          JsArrayString tabSet1 = JsArrayString.createArray().cast();
504          for (String tab : tabSet1ModuleList_.getValue())
505             tabSet1.push(tab);
506 
507          JsArrayString tabSet2 = JsArrayString.createArray().cast();
508          for (String tab : tabSet2ModuleList_.getValue())
509             tabSet2.push(tab);
510 
511          JsArrayString hiddenTabSet = JsArrayString.createArray().cast();
512          for (String tab : hiddenTabSetModuleList_.getValue())
513             hiddenTabSet.push(tab);
514 
515          // Determine implicit preference for console top/bottom location
516          // This needs to be saved so that when the user executes the
517          // Console on Left/Right commands we know whether to position
518          // the Console on the Top or Bottom
519          PaneConfig prevConfig = userPrefs_.panes().getGlobalValue().cast();
520          boolean consoleLeftOnTop = prevConfig.getConsoleLeftOnTop();
521          boolean consoleRightOnTop = prevConfig.getConsoleRightOnTop();
522          final String kConsole = "Console";
523          if (panes.get(0).equals(kConsole))
524             consoleLeftOnTop = true;
525          else if (panes.get(1).equals(kConsole))
526             consoleLeftOnTop = false;
527          else if (panes.get(2).equals(kConsole))
528             consoleRightOnTop = true;
529          else if (panes.get(3).equals(kConsole))
530             consoleRightOnTop = false;
531 
532          if (displayColumnCount_ != additionalColumnCount_)
533             additionalColumnCount_ =
534                paneManager_.syncAdditionalColumnCount(displayColumnCount_, true);
535 
536          userPrefs_.panes().setGlobalValue(PaneConfig.create(
537                panes, tabSet1, tabSet2, hiddenTabSet,
538                consoleLeftOnTop, consoleRightOnTop, additionalColumnCount_));
539 
540          dirty_ = false;
541       }
542 
543       return restartRequirement;
544    }
545 
546    @Override
getName()547    public String getName()
548    {
549       return "Pane Layout";
550    }
551 
updateTabSetPositions()552    private void updateTabSetPositions()
553    {
554       for (int i = 0; i < visiblePanes_.length; i++)
555       {
556          String value = visiblePanes_[i].getValue(visiblePanes_[i].getSelectedIndex());
557          if (value == "TabSet1")
558             visiblePanePanels_[i].add(tabSet1ModuleList_);
559          else if (value == "TabSet2")
560             visiblePanePanels_[i].add(tabSet2ModuleList_);
561       }
562    }
563 
updateTabSetLabels()564    private void updateTabSetLabels()
565    {
566       // If no tabs are values in a tabset pane, give the pane a generic name,
567       // otherwise the name is created from the selected values
568       String itemText1 = tabSet1ModuleList_.getValue().isEmpty() ?
569          "TabSet" : StringUtil.join(tabSet1ModuleList_.getValue(), ", ");
570       String itemText2 = tabSet2ModuleList_.getValue().isEmpty() ?
571          "TabSet" : StringUtil.join(tabSet2ModuleList_.getValue(), ", ");
572       if (StringUtil.equals(itemText1, "Presentation") && !tabSet1ModuleList_.presentationVisible())
573          itemText1 = "TabSet";
574 
575       for (ListBox pane : visiblePanes_)
576       {
577          pane.setItemText(2, itemText1);
578          pane.setItemText(3, itemText2);
579       }
580    }
581 
toArrayList(JsArrayString strings)582    private ArrayList<String> toArrayList(JsArrayString strings)
583    {
584       ArrayList<String> results = new ArrayList<>();
585       for (int i = 0; strings != null && i < strings.length(); i++)
586          results.add(strings.get(i));
587       return results;
588    }
589 
590    private final PreferencesDialogResources res_;
591    private final UserPrefs userPrefs_;
592    private final ListBox leftTop_;
593    private final ListBox leftBottom_;
594    private final ListBox rightTop_;
595    private final ListBox rightBottom_;
596    private final ListBox[] visiblePanes_;
597    private final VerticalPanel[] visiblePanePanels_;
598    private final ModuleList tabSet1ModuleList_;
599    private final ModuleList tabSet2ModuleList_;
600    private final ModuleList hiddenTabSetModuleList_;
601    private final PaneManager paneManager_;
602    private boolean dirty_ = false;
603 
604    private VerticalPanel leftTopPanel_;
605    private VerticalPanel leftBottomPanel_;
606    private VerticalPanel rightTopPanel_;
607    private VerticalPanel rightBottomPanel_;
608 
609    private int additionalColumnCount_ = 0;
610    private int displayColumnCount_ = 0;
611    private FlexTable grid_;
612 
613    private final static int GRID_CELL_SPACING = 8;
614    private final static int GRID_CELL_PADDING = 6;
615    private final static int MAX_COLUMN_WIDTH = 50 + GRID_CELL_PADDING + GRID_CELL_SPACING;
616    private final static int TABLE_WIDTH = 435;
617    private final static int GRID_PANE_COUNT = 2;
618    private final static int GRID_SELECT_PADDING = 10; // must match CSS file
619 }
620