diff --git a/.gitignore b/.gitignore index 9bcd7463ee3..ef8ec634051 100644 --- a/.gitignore +++ b/.gitignore @@ -54,5 +54,6 @@ venv/ .venv/ CLAUDE.md +AGENTS.md .claude/ spec/ diff --git a/src/cpp/session/include/session/prefs/UserPrefValues.hpp b/src/cpp/session/include/session/prefs/UserPrefValues.hpp index 90f85d6a44e..2bff968b79e 100644 --- a/src/cpp/session/include/session/prefs/UserPrefValues.hpp +++ b/src/cpp/session/include/session/prefs/UserPrefValues.hpp @@ -78,9 +78,12 @@ namespace prefs { #define kPanesTabSet1 "tabSet1" #define kPanesTabSet2 "tabSet2" #define kPanesHiddenTabSet "hiddenTabSet" +#define kPanesSidebar "sidebar" #define kPanesConsoleLeftOnTop "console_left_on_top" #define kPanesConsoleRightOnTop "console_right_on_top" #define kPanesAdditionalSourceColumns "additional_source_columns" +#define kPanesSidebarVisible "sidebar_visible" +#define kPanesSidebarLocation "sidebar_location" #define kAllowSourceColumns "allow_source_columns" #define kUseSpacesForTab "use_spaces_for_tab" #define kNumSpacesForTab "num_spaces_for_tab" diff --git a/src/cpp/session/resources/schema/user-prefs-schema.json b/src/cpp/session/resources/schema/user-prefs-schema.json index 9916d232423..fc372b5c6f5 100644 --- a/src/cpp/session/resources/schema/user-prefs-schema.json +++ b/src/cpp/session/resources/schema/user-prefs-schema.json @@ -162,7 +162,7 @@ "description": "Allocation of panes to quadrants.", "items": { "type": "string", - "enum": ["Source", "Console", "TabSet1", "TabSet2", "HiddenTabSet"] + "enum": ["Source", "Console", "TabSet1", "TabSet2", "HiddenTabSet", "Sidebar"] } }, "tabSet1": { @@ -186,6 +186,13 @@ "type": "string" } }, + "sidebar": { + "type": "array", + "description": "The panes included in the sidebar.", + "items": { + "type": "string" + } + }, "console_left_on_top": { "type": "boolean", "description": "Whether the console is on top when it is on the left side of the workbench." @@ -198,6 +205,16 @@ "type": "integer", "default": 0, "description": "The number of columns of full source columns." + }, + "sidebar_visible": { + "type": "boolean", + "default": false, + "description": "Whether the sidebar is visible." + }, + "sidebar_location": { + "type": "string", + "enum": ["left", "right", "satellite"], + "description": "The location of the sidebar." } }, "default": { @@ -206,13 +223,17 @@ "Console", "TabSet1", "TabSet2", - "HiddenTabSet"], - "tabSet1": ["Environment", "History", "Connections", "Build", "VCS", "Tutorial", "Presentation"], - "tabSet2": ["Files", "Plots", "Packages", "Help", "Viewer", "Presentations"], + "HiddenTabSet", + "Sidebar"], + "tabSet1": ["Environment", "Connections", "Build", "VCS", "Tutorial", "Presentation"], + "tabSet2": ["Plots", "Packages", "Help", "Viewer", "Presentations"], "hiddenTabSet": [], + "sidebar": ["Files", "History"], "console_left_on_top": false, "console_right_on_top": true, - "additional_source_columns": 0 + "additional_source_columns": 0, + "sidebar_visible": false, + "sidebar_location": "right" }, "description": "Layout of panes in the RStudio workbench." }, diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/commands/CmdConstants.java b/src/gwt/src/org/rstudio/studio/client/workbench/commands/CmdConstants.java index 9f4dab36f96..c7f63e24686 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/commands/CmdConstants.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/commands/CmdConstants.java @@ -536,6 +536,24 @@ public interface CmdConstants extends Constants { @DefaultStringValue("") // $NON-NLS-1$ String projectSweaveOptionsDesc(); + // showSidebar + @DefaultStringValue("Show Si_debar") // $NON-NLS-1$ + String showSidebarMenuLabel(); + + // hideSidebar + @DefaultStringValue("Hide Sideb_ar") // $NON-NLS-1$ + String hideSidebarMenuLabel(); + + // toggleSidebar + @DefaultStringValue("Toggle Visibility of Sidebar") // $NON-NLS-1$ + String toggleSidebarLabel(); + + // moveSidebar + @DefaultStringValue("Move Sidebar Left or Right") // $NON-NLS-1$ + String moveSidebarLabel(); + @DefaultStringValue("Move Si_debar Left or Right") // $NON-NLS-1$ + String moveSidebarMenuLabel(); + // showToolbar @DefaultStringValue("Show _Toolbar") // $NON-NLS-1$ String showToolbarMenuLabel(); diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/commands/CmdConstants_en.properties b/src/gwt/src/org/rstudio/studio/client/workbench/commands/CmdConstants_en.properties index 1d629915dda..ee27e1c6e8f 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/commands/CmdConstants_en.properties +++ b/src/gwt/src/org/rstudio/studio/client/workbench/commands/CmdConstants_en.properties @@ -350,6 +350,19 @@ projectSweaveOptionsButtonLabel = projectSweaveOptionsMenuLabel = projectSweaveOptionsDesc = +# showSidebar +showSidebarMenuLabel = Show Si_debar + +# hideSidebar +hideSidebarMenuLabel = Hide Sideb_ar + +# toggleSidebar +toggleSidebarLabel = Toggle Visibility of Sidebar + +# moveSidebar +moveSidebarLabel = Move Sidebar Left or Right +moveSidebarMenuLabel = Move Si_debar Left or Right + # showToolbar showToolbarMenuLabel = Show _Toolbar diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/commands/CmdConstants_fr.properties b/src/gwt/src/org/rstudio/studio/client/workbench/commands/CmdConstants_fr.properties index 0f0d4b2acba..34d0c2146cb 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/commands/CmdConstants_fr.properties +++ b/src/gwt/src/org/rstudio/studio/client/workbench/commands/CmdConstants_fr.properties @@ -350,6 +350,20 @@ projectSweaveOptionsButtonLabel = projectSweaveOptionsMenuLabel = projectSweaveOptionsDesc = +# showSidebar +showSidebarMenuLabel = Afficher la barre latérale + +# hideSidebar +hideSidebarMenuLabel = Cacher la barre latérale + +# toggleSidebar +toggleSidebarLabel = Basculer la visibilité de la barre latérale +toggleSidebarMenuLabel = Basculer la barre latérale + +# moveSidebar +moveSidebarLabel = Déplacer la barre latérale à gauche ou à droite +moveSidebarMenuLabel = Déplacer la barre latérale à gauche ou à droite + # showToolbar showToolbarMenuLabel = Afficher la barre d\u0027ou_tils diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/commands/Commands.cmd.xml b/src/gwt/src/org/rstudio/studio/client/workbench/commands/Commands.cmd.xml index 7b14251545c..05716b92d8a 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/commands/Commands.cmd.xml +++ b/src/gwt/src/org/rstudio/studio/client/workbench/commands/Commands.cmd.xml @@ -280,6 +280,8 @@ See `/src/gwt/tools/i18n-helpers/README.md`. + + @@ -288,6 +290,7 @@ See `/src/gwt/tools/i18n-helpers/README.md`. + @@ -1373,6 +1376,21 @@ See `/src/gwt/tools/i18n-helpers/README.md`. buttonLabel="" desc=""/> + + + + + + + + diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/commands/Commands.cmd.xml.MD5 b/src/gwt/src/org/rstudio/studio/client/workbench/commands/Commands.cmd.xml.MD5 index 557a4400644..e3c6accab1f 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/commands/Commands.cmd.xml.MD5 +++ b/src/gwt/src/org/rstudio/studio/client/workbench/commands/Commands.cmd.xml.MD5 @@ -1 +1 @@ -4b58c8a119104c4b6f014c5c2f96b35a +086eb47d92ed036786c1ea5ded9ba210 diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/commands/Commands.java b/src/gwt/src/org/rstudio/studio/client/workbench/commands/Commands.java index 1261a5aedfb..57c7d9ae871 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/commands/Commands.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/commands/Commands.java @@ -356,6 +356,10 @@ public abstract AppCommand showToolbar(); public abstract AppCommand hideToolbar(); public abstract AppCommand toggleToolbar(); + public abstract AppCommand showSidebar(); + public abstract AppCommand hideSidebar(); + public abstract AppCommand toggleSidebar(); + public abstract AppCommand moveSidebar(); public abstract AppCommand zoomActualSize(); public abstract AppCommand zoomIn(); public abstract AppCommand zoomOut(); diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/prefs/model/UserPrefsAccessor.java b/src/gwt/src/org/rstudio/studio/client/workbench/prefs/model/UserPrefsAccessor.java index 026be3afb5e..0d76c25b9ad 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/prefs/model/UserPrefsAccessor.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/prefs/model/UserPrefsAccessor.java @@ -389,23 +389,28 @@ protected Panes() {} public final static String QUADRANTS_TABSET1 = "TabSet1"; public final static String QUADRANTS_TABSET2 = "TabSet2"; public final static String QUADRANTS_HIDDENTABSET = "HiddenTabSet"; + public final static String QUADRANTS_SIDEBAR = "Sidebar"; public final native JsArrayString getQuadrants() /*-{ - return this && this.quadrants || ["Source","Console","TabSet1","TabSet2","HiddenTabSet"]; + return this && this.quadrants || ["Source","Console","TabSet1","TabSet2","HiddenTabSet","Sidebar"]; }-*/; public final native JsArrayString getTabSet1() /*-{ - return this && this.tabSet1 || ["Environment","History","Connections","Build","VCS","Tutorial","Presentation"]; + return this && this.tabSet1 || ["Environment","Connections","Build","VCS","Tutorial","Presentation"]; }-*/; public final native JsArrayString getTabSet2() /*-{ - return this && this.tabSet2 || ["Files","Plots","Packages","Help","Viewer","Presentations"]; + return this && this.tabSet2 || ["Plots","Packages","Help","Viewer","Presentations"]; }-*/; public final native JsArrayString getHiddenTabSet() /*-{ return this && this.hiddenTabSet || []; }-*/; + public final native JsArrayString getSidebar() /*-{ + return this && this.sidebar || ["Files","History"]; + }-*/; + public final native boolean getConsoleLeftOnTop() /*-{ return this && this.console_left_on_top || false; }-*/; @@ -418,6 +423,14 @@ public final native int getAdditionalSourceColumns() /*-{ return this && this.additional_source_columns || 0; }-*/; + public final native boolean getSidebarVisible() /*-{ + return this && this.sidebar_visible || false; + }-*/; + + public final native String getSidebarLocation() /*-{ + return this && this.sidebar_location || "right"; + }-*/; + } /** diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/prefs/views/PaneLayoutPreferencesPane.java b/src/gwt/src/org/rstudio/studio/client/workbench/prefs/views/PaneLayoutPreferencesPane.java index bee5275b321..ebf003310cd 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/prefs/views/PaneLayoutPreferencesPane.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/prefs/views/PaneLayoutPreferencesPane.java @@ -79,7 +79,7 @@ public void onChange(ChangeEvent event) private Integer notSelectedIndex() { - boolean[] seen = new boolean[4]; + boolean[] seen = new boolean[5]; // Updated to 5 to include Sidebar for (ListBox listBox : lists_) seen[listBox.getSelectedIndex()] = true; for (int i = 0; i < seen.length; i++) @@ -242,6 +242,10 @@ public PaneLayoutPreferencesPane(PreferencesDialogResources res, add(columnToolbar); String[] visiblePanes = PaneConfig.getVisiblePanes(); + // Add Sidebar to the list of visible panes + String[] visiblePanesWithSidebar = new String[visiblePanes.length + 1]; + System.arraycopy(visiblePanes, 0, visiblePanesWithSidebar, 0, visiblePanes.length); + visiblePanesWithSidebar[visiblePanes.length] = "Sidebar"; leftTop_ = new ListBox(); Roles.getListboxRole().setAriaLabelProperty(leftTop_.getElement(), "Top left panel"); @@ -254,7 +258,7 @@ public PaneLayoutPreferencesPane(PreferencesDialogResources res, visiblePanes_ = new ListBox[]{leftTop_, leftBottom_, rightTop_, rightBottom_}; for (ListBox lb : visiblePanes_) { - for (String value : visiblePanes) + for (String value : visiblePanesWithSidebar) lb.addItem(value); } @@ -290,6 +294,8 @@ public PaneLayoutPreferencesPane(PreferencesDialogResources res, tabSet1ModuleList_.setValue(toArrayList(userPrefs.panes().getGlobalValue().getTabSet1())); tabSet2ModuleList_ = new ModuleList(paneWidth); tabSet2ModuleList_.setValue(toArrayList(userPrefs.panes().getGlobalValue().getTabSet2())); + sidebarModuleList_ = new ModuleList(paneWidth); + sidebarModuleList_.setValue(toArrayList(userPrefs.panes().getGlobalValue().getSidebar())); hiddenTabSetModuleList_ = new ModuleList(paneWidth); hiddenTabSetModuleList_.setValue(toArrayList( userPrefs.panes().getGlobalValue().getHiddenTabSet())); @@ -301,35 +307,57 @@ public void onValueChange(ValueChangeEvent> e) dirty_ = true; ModuleList source = (ModuleList) e.getSource(); - ModuleList other = (source == tabSet1ModuleList_) - ? tabSet2ModuleList_ - : tabSet1ModuleList_; - - // an index should only be on for one of these lists, ArrayList indices = source.getSelectedIndices(); - ArrayList otherIndices = other.getSelectedIndices(); + ArrayList tabSet1Indices = tabSet1ModuleList_.getSelectedIndices(); + ArrayList tabSet2Indices = tabSet2ModuleList_.getSelectedIndices(); + ArrayList sidebarIndices = sidebarModuleList_.getSelectedIndices(); ArrayList hiddenIndices = hiddenTabSetModuleList_.getSelectedIndices(); + if (!PaneConfig.isValidConfig(source.getValue())) { // when the configuration is invalid, we must reset sources to the prior valid - // configuration based on the values of the other two lists + // configuration based on the values of the other lists for (int i = 0; i < indices.size(); i++) - indices.set(i, !(otherIndices.get(i) || hiddenIndices.get(i))); + { + if (source == tabSet1ModuleList_) + indices.set(i, !(tabSet2Indices.get(i) || sidebarIndices.get(i) || hiddenIndices.get(i))); + else if (source == tabSet2ModuleList_) + indices.set(i, !(tabSet1Indices.get(i) || sidebarIndices.get(i) || hiddenIndices.get(i))); + else if (source == sidebarModuleList_) + indices.set(i, !(tabSet1Indices.get(i) || tabSet2Indices.get(i) || hiddenIndices.get(i))); + } source.setSelectedIndices(indices); } else { + // Ensure 4-way exclusivity: a tab can only be in one of the four lists for (int i = 0; i < indices.size(); i++) { if (indices.get(i)) { - otherIndices.set(i, false); + // Clear this index from all other lists + if (source != tabSet1ModuleList_) + tabSet1Indices.set(i, false); + if (source != tabSet2ModuleList_) + tabSet2Indices.set(i, false); + if (source != sidebarModuleList_) + sidebarIndices.set(i, false); hiddenIndices.set(i, false); } - else if (!otherIndices.get(i)) - hiddenIndices.set(i, true); + else if (source != hiddenTabSetModuleList_) + { + // If unchecked and not in any other list, put in hidden + if (!tabSet1Indices.get(i) && !tabSet2Indices.get(i) && !sidebarIndices.get(i)) + hiddenIndices.set(i, true); + } } - other.setSelectedIndices(otherIndices); + + if (source != tabSet1ModuleList_) + tabSet1ModuleList_.setSelectedIndices(tabSet1Indices); + if (source != tabSet2ModuleList_) + tabSet2ModuleList_.setSelectedIndices(tabSet2Indices); + if (source != sidebarModuleList_) + sidebarModuleList_.setSelectedIndices(sidebarIndices); hiddenTabSetModuleList_.setSelectedIndices(hiddenIndices); updateTabSetLabels(); @@ -338,6 +366,7 @@ else if (!otherIndices.get(i)) }; tabSet1ModuleList_.addValueChangeHandler(vch); tabSet2ModuleList_.addValueChangeHandler(vch); + sidebarModuleList_.addValueChangeHandler(vch); updateTabSetPositions(); updateTabSetLabels(); @@ -434,6 +463,7 @@ private String updateTable(int newCount) grid_.getCellFormatter().setWidth(0, i, columnWidth); tabSet1ModuleList_.setWidth(cellWidth); tabSet2ModuleList_.setWidth(cellWidth); + sidebarModuleList_.setWidth(cellWidth); return cellWidth; } @@ -506,6 +536,10 @@ public RestartRequirement onApply(UserPrefs rPrefs) for (String tab : tabSet2ModuleList_.getValue()) tabSet2.push(tab); + JsArrayString sidebar = JsArrayString.createArray().cast(); + for (String tab : sidebarModuleList_.getValue()) + sidebar.push(tab); + JsArrayString hiddenTabSet = JsArrayString.createArray().cast(); for (String tab : hiddenTabSetModuleList_.getValue()) hiddenTabSet.push(tab); @@ -530,9 +564,15 @@ else if (panes.get(3).equals(PaneManager.CONSOLE_PANE)) additionalColumnCount_ = paneManager_.syncAdditionalColumnCount(displayColumnCount_, true); + // Get current sidebar visibility and location settings + PaneConfig currentConfig = userPrefs_.panes().getGlobalValue().cast(); + boolean sidebarVisible = currentConfig.getSidebarVisible(); + String sidebarLocation = currentConfig.getSidebarLocation(); + userPrefs_.panes().setGlobalValue(PaneConfig.create( panes, tabSet1, tabSet2, hiddenTabSet, - consoleLeftOnTop, consoleRightOnTop, additionalColumnCount_)); + consoleLeftOnTop, consoleRightOnTop, additionalColumnCount_, + sidebar, sidebarVisible, sidebarLocation)); dirty_ = false; } @@ -555,6 +595,8 @@ private void updateTabSetPositions() visiblePanePanels_[i].add(tabSet1ModuleList_); else if (StringUtil.equals(value, UserPrefsAccessor.Panes.QUADRANTS_TABSET2)) visiblePanePanels_[i].add(tabSet2ModuleList_); + else if (StringUtil.equals(value, "Sidebar")) + visiblePanePanels_[i].add(sidebarModuleList_); } } @@ -565,7 +607,9 @@ private void updateTabSetLabels() String itemText1 = tabSet1ModuleList_.getValue().isEmpty() ? "TabSet" : StringUtil.join(tabSet1ModuleList_.getValue(), ", "); String itemText2 = tabSet2ModuleList_.getValue().isEmpty() ? - "TabSet" : StringUtil.join(tabSet2ModuleList_.getValue(), ", "); + "TabSet" : StringUtil.join(tabSet2ModuleList_.getValue(), ", "); + String itemTextSidebar = sidebarModuleList_.getValue().isEmpty() ? + "Sidebar" : StringUtil.join(sidebarModuleList_.getValue(), ", "); if (StringUtil.equals(itemText1, "Presentation") && !tabSet1ModuleList_.presentationVisible()) itemText1 = "TabSet"; @@ -573,6 +617,7 @@ private void updateTabSetLabels() { pane.setItemText(2, itemText1); pane.setItemText(3, itemText2); + pane.setItemText(4, itemTextSidebar); } } @@ -594,6 +639,7 @@ private ArrayList toArrayList(JsArrayString strings) private final VerticalPanel[] visiblePanePanels_; private final ModuleList tabSet1ModuleList_; private final ModuleList tabSet2ModuleList_; + private final ModuleList sidebarModuleList_; private final ModuleList hiddenTabSetModuleList_; private final PaneManager paneManager_; private boolean dirty_ = false; diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/ui/MainSplitPanel.java b/src/gwt/src/org/rstudio/studio/client/workbench/ui/MainSplitPanel.java index 4459e179531..3673c25d2f9 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/ui/MainSplitPanel.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/ui/MainSplitPanel.java @@ -134,10 +134,22 @@ public MainSplitPanel(EventBus events, } public void initialize(ArrayList leftList, Widget center, Widget right) + { + initialize(leftList, center, right, null, "right"); + } + + public void initialize(ArrayList leftList, Widget center, Widget right, Widget sidebar) + { + initialize(leftList, center, right, sidebar, "right"); + } + + public void initialize(ArrayList leftList, Widget center, Widget right, Widget sidebar, String sidebarLocation) { leftList_ = leftList; center_ = center; right_ = right; + sidebar_ = sidebar; + sidebarLocation_ = sidebarLocation; new JSObjectStateValue(GROUP_WORKBENCH, KEY_RIGHTPANESIZE, @@ -150,31 +162,114 @@ protected void onInit(JsObject value) { // If we already have a set state, with the correct number of columns use that State state = value == null ? null : (State)value.cast(); + int expectedCount = leftList_.size() + 1 + (sidebar_ != null ? 1 : 0); if (state != null && state.validate() && state.hasSplitterPos() && - state.getSplitterCount() == leftList_.size() + 1) + state.getSplitterCount() == expectedCount) { if (state.hasPanelWidth() && state.hasWindowWidth() && state.getWindowWidth() != Window.getClientWidth()) { int delta = state.getWindowWidth() - state.getPanelWidth(); int offsetWidth = Window.getClientWidth() - delta; - double pct = (double)state.getSplitterPos()[0] - / state.getPanelWidth(); - addEast(right_, pct * offsetWidth); + int idx = 0; + double pct; + // Add sidebar if on left + if (sidebar_ != null && "left".equals(sidebarLocation_)) + { + pct = (double)state.getSplitterPos()[idx++] + / state.getPanelWidth(); + addWest(sidebar_, pct * offsetWidth); + } + // Add left widgets for (int i = 0; i < leftList_.size(); i++) { - pct = (double)state.getSplitterPos()[i + 1] + pct = (double)state.getSplitterPos()[idx++] / state.getPanelWidth(); addWest(leftList_.get(i), pct * offsetWidth); } + // Handle right-side sidebar case differently for proper resizing + if (sidebar_ != null && !"left".equals(sidebarLocation_)) + { + // Add sidebar first using addEast (rightmost position) + double sidebarPct = (double)state.getSplitterPos()[idx++] + / state.getPanelWidth(); + addEast(sidebar_, sidebarPct * offsetWidth); + + // Get right widget width + double rightPct = (double)state.getSplitterPos()[idx++] + / state.getPanelWidth(); + + // Calculate center width: total - left widgets - sidebar - right + double centerWidth = offsetWidth; + // Subtract all left widget widths that were already added + int leftStartIdx = sidebar_ != null && "left".equals(sidebarLocation_) ? 1 : 0; + for (int i = 0; i < leftList_.size(); i++) + { + double leftPct = (double)state.getSplitterPos()[leftStartIdx + i] / state.getPanelWidth(); + centerWidth -= leftPct * offsetWidth; + } + // Subtract sidebar and right widths + centerWidth -= sidebarPct * offsetWidth; + centerWidth -= rightPct * offsetWidth; + + // Add center using addWest (last addWest call) + addWest(center_, centerWidth); + + // Add right using add() (last thing added) + add(right_); + } + else + { + // No sidebar on right - use original logic + pct = (double)state.getSplitterPos()[idx++] + / state.getPanelWidth(); + addEast(right_, pct * offsetWidth); + add(center_); + } } else { - addEast(right_, state.getSplitterPos()[0]); + int idx = 0; + // Add sidebar if on left + if (sidebar_ != null && "left".equals(sidebarLocation_)) + addWest(sidebar_, state.getSplitterPos()[idx++]); + // Add left widgets for (int i = 0; i < leftList_.size(); i++) - addWest(leftList_.get(i), state.getSplitterPos()[i + 1]); + addWest(leftList_.get(i), state.getSplitterPos()[idx++]); + // Handle right-side sidebar case differently for proper resizing + if (sidebar_ != null && !"left".equals(sidebarLocation_)) + { + // Add sidebar first using addEast (rightmost position) + int sidebarWidth = state.getSplitterPos()[idx++]; + addEast(sidebar_, sidebarWidth); + + // Get right widget width + int rightWidth = state.getSplitterPos()[idx++]; + + // Calculate remaining width for center (total - left widgets - sidebar - right) + int centerWidth = state.getPanelWidth(); + // Subtract all left widget widths that were already added + int leftStartIdx = sidebar_ != null && "left".equals(sidebarLocation_) ? 1 : 0; + for (int i = 0; i < leftList_.size(); i++) + centerWidth -= state.getSplitterPos()[leftStartIdx + i]; + // Subtract sidebar and right widths + centerWidth -= sidebarWidth; + centerWidth -= rightWidth; + + // Add center using addWest (last addWest call) + addWest(center_, centerWidth); + + // Add right using add() (last thing added) + add(right_); + } + else + { + // No sidebar on right - use original logic + addEast(right_, state.getSplitterPos()[idx++]); + add(center_); + } } } else @@ -182,10 +277,33 @@ protected void onInit(JsObject value) // When there are only two panels, make the left side slightly larger than the right, // otherwise divide the space equally. double splitWidth = getDefaultSplitterWidth(); - addEast(right_, splitWidth); - + + // Add sidebar if on left + if (sidebar_ != null && "left".equals(sidebarLocation_)) + addWest(sidebar_, splitWidth * 0.8); // Sidebar slightly narrower + + // Add left widgets for (Widget w : leftList_) addWest(w, splitWidth); + + // Handle right-side sidebar case differently for proper resizing + if (sidebar_ != null && !"left".equals(sidebarLocation_)) + { + // Add sidebar first using addEast (rightmost position) + addEast(sidebar_, splitWidth * 0.8); // Sidebar slightly narrower + + // Add center using addWest (last addWest call) + addWest(center_, splitWidth); + + // Add right using add() (last thing added) + add(right_); + } + else + { + // No sidebar on right - use original logic + addEast(right_, splitWidth); + add(center_); + } } Scheduler.get().scheduleDeferred(new ScheduledCommand() @@ -207,13 +325,27 @@ protected JsObject getValue() // The widget's code determines the splitter positions from the width of each widget // so these value represent that width rather than the actual coordinates of the // splitter. - int[] splitterArray = new int[leftList_.size() + 1]; - splitterArray[0] = right_.getOffsetWidth(); + int sidebarCount = sidebar_ != null ? 1 : 0; + int[] splitterArray = new int[leftList_.size() + 1 + sidebarCount]; + int idx = 0; + + // Store sidebar width if on left + if (sidebar_ != null && "left".equals(sidebarLocation_)) + splitterArray[idx++] = sidebar_.getOffsetWidth(); + + // Store left widget widths if (!leftList_.isEmpty()) { for (int i = 0; i < leftList_.size(); i++) - splitterArray[i + 1] = leftList_.get(i).getOffsetWidth(); + splitterArray[idx++] = leftList_.get(i).getOffsetWidth(); } + + // Store sidebar width if on right (before right widget in the array) + if (sidebar_ != null && !"left".equals(sidebarLocation_)) + splitterArray[idx++] = sidebar_.getOffsetWidth(); + + // Store right widget width + splitterArray[idx++] = right_.getOffsetWidth(); state.setSplitterPos(splitterArray); return state.cast(); } @@ -233,7 +365,6 @@ protected boolean hasChanged() private State lastKnownValue_; }; - add(center_); setWidgetMinSize(right_, 0); } @@ -248,14 +379,15 @@ public void addLeftWidget(Widget widget) { clearForRefresh(); leftList_.add(0, widget); - initialize(leftList_, center_, right_); + initialize(leftList_, center_, right_, sidebar_, sidebarLocation_); } public double getDefaultSplitterWidth() { - return leftList_.isEmpty() ? + int columnCount = 2 + leftList_.size() + (sidebar_ != null ? 1 : 0); + return leftList_.isEmpty() && sidebar_ == null ? Window.getClientWidth() * 0.45 : - Window.getClientWidth() / (2 + leftList_.size()); + Window.getClientWidth() / columnCount; } public double getLeftSize() @@ -278,7 +410,32 @@ public void removeLeftWidget(Widget widget) { clearForRefresh(); leftList_.remove(widget); - initialize(leftList_, center_, right_); + initialize(leftList_, center_, right_, sidebar_, sidebarLocation_); + } + + public void setSidebarWidget(Widget widget) + { + setSidebarWidget(widget, "right"); + } + + public void setSidebarWidget(Widget widget, String location) + { + clearForRefresh(); + sidebar_ = widget; + sidebarLocation_ = location; + initialize(leftList_, center_, right_, sidebar_, sidebarLocation_); + } + + public void removeSidebarWidget() + { + clearForRefresh(); + sidebar_ = null; + initialize(leftList_, center_, right_, null); + } + + public boolean hasSidebarWidget() + { + return sidebar_ != null; } public void onSplitterResized(SplitterResizedEvent event) @@ -298,6 +455,8 @@ private void clearForRefresh() { remove(center_); remove(right_); + if (sidebar_ != null) + remove(sidebar_); for (Widget w : leftList_) remove(w); } @@ -368,6 +527,8 @@ public void execute() { private ArrayList leftList_; private Widget center_; private Widget right_; + private Widget sidebar_; + private String sidebarLocation_ = "right"; private static final String GROUP_WORKBENCH = "workbenchp"; private static final String KEY_RIGHTPANESIZE = "rightpanesize"; private Command layoutCommand_; diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/ui/PaneConfig.java b/src/gwt/src/org/rstudio/studio/client/workbench/ui/PaneConfig.java index 51f8308aea7..5d55d3a1703 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/ui/PaneConfig.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/ui/PaneConfig.java @@ -44,7 +44,34 @@ public native static PaneConfig create(JsArrayString panes, hiddenTabSet: hiddenTabSet, console_left_on_top: consoleLeftOnTop, console_right_on_top: consoleRightOnTop, - additional_source_columns: additionalSources + additional_source_columns: additionalSources, + sidebar: [], + sidebar_visible: false, + sidebar_location: "right" + }; + }-*/; + + public native static PaneConfig create(JsArrayString panes, + JsArrayString tabSet1, + JsArrayString tabSet2, + JsArrayString hiddenTabSet, + boolean consoleLeftOnTop, + boolean consoleRightOnTop, + int additionalSources, + JsArrayString sidebarTabs, + boolean sidebarVisible, + String sidebarLocation) /*-{ + return { + quadrants: panes, + tabSet1: tabSet1, + tabSet2: tabSet2, + hiddenTabSet: hiddenTabSet, + console_left_on_top: consoleLeftOnTop, + console_right_on_top: consoleRightOnTop, + additional_source_columns: additionalSources, + sidebar: sidebarTabs, + sidebar_visible: sidebarVisible, + sidebar_location: sidebarLocation }; }-*/; @@ -91,7 +118,8 @@ public static String[] getAllPanes() UserPrefsAccessor.Panes.QUADRANTS_CONSOLE, UserPrefsAccessor.Panes.QUADRANTS_TABSET1, UserPrefsAccessor.Panes.QUADRANTS_TABSET2, - UserPrefsAccessor.Panes.QUADRANTS_HIDDENTABSET + UserPrefsAccessor.Panes.QUADRANTS_HIDDENTABSET, + UserPrefsAccessor.Panes.QUADRANTS_SIDEBAR }; } @@ -101,7 +129,8 @@ public static String[] getVisiblePanes() UserPrefsAccessor.Panes.QUADRANTS_SOURCE, UserPrefsAccessor.Panes.QUADRANTS_CONSOLE, UserPrefsAccessor.Panes.QUADRANTS_TABSET1, - UserPrefsAccessor.Panes.QUADRANTS_TABSET2 + UserPrefsAccessor.Panes.QUADRANTS_TABSET2, + UserPrefsAccessor.Panes.QUADRANTS_SIDEBAR }; } @@ -282,7 +311,10 @@ public final PaneConfig copy() copy(getHiddenTabSet()), getConsoleLeftOnTop(), getConsoleRightOnTop(), - getAdditionalSourceColumns()); + getAdditionalSourceColumns(), + copy(getSidebar()), + getSidebarVisible(), + getSidebarLocation()); } public final native boolean isEqualTo(PaneConfig other) /*-{ @@ -290,7 +322,8 @@ public final native boolean isEqualTo(PaneConfig other) /*-{ this.panes.toString() == other.panes.toString() && this.tabSet1.toString() == other.tabSet1.toString() && this.tabSet2.toString() == other.tabSet2.toString() && - this.hiddenTabSet.toString() == other.hiddenTabSet.toString(); + this.hiddenTabSet.toString() == other.hiddenTabSet.toString() && + this.sidebar.toString() == other.sidebar.toString(); }-*/; private boolean sameElements(JsArrayString a, String[] b) diff --git a/src/gwt/src/org/rstudio/studio/client/workbench/ui/PaneManager.java b/src/gwt/src/org/rstudio/studio/client/workbench/ui/PaneManager.java index 18935c00ec0..b79e5782af6 100644 --- a/src/gwt/src/org/rstudio/studio/client/workbench/ui/PaneManager.java +++ b/src/gwt/src/org/rstudio/studio/client/workbench/ui/PaneManager.java @@ -103,6 +103,7 @@ public enum Tab { public static final String LEFT_COLUMN = "left"; public static final String RIGHT_COLUMN = "right"; + public static final String SIDEBAR_COLUMN = "sidebar"; class SelectedTabStateValue extends IntStateValue { @@ -330,7 +331,25 @@ public PaneManager(Provider pSplitPanel, 0).cast()); } } - panel_.initialize(leftList_, center_, right_); + // Initialize sidebar if configured + Widget sidebarWidget = null; + String sidebarLocation = config.getSidebarLocation(); + if (config.getSidebarVisible()) + { + LogicalWindow sidebarWindow = panesByName_.get(UserPrefsAccessor.Panes.QUADRANTS_SIDEBAR); + if (sidebarWindow != null) + { + // For sidebar, we use just the WindowFrame directly (no vertical split) + sidebarWindow.transitionToState(WindowState.NORMAL); + sidebarWidget = sidebarWindow.getNormal(); + sidebar_ = sidebarWidget; + } + showSidebar(true); + } else { + showSidebar(false); + } + + panel_.initialize(leftList_, center_, right_, sidebarWidget, sidebarLocation); for (LogicalWindow window : sourceLogicalWindows_) { @@ -376,6 +395,7 @@ else if (numDocs > 0 && window.getState() == WindowState.HIDE) } source_.loadDisplay(); + userPrefs.panes().addValueChangeHandler(evt -> { ArrayList newPanes = createPanes( @@ -419,6 +439,9 @@ else if (center_.getOffsetWidth() == 0 || right_.getOffsetWidth() == 0) hiddenTabs_ = tabNamesToTabs(evt.getValue().getHiddenTabSet()); populateTabPanel(hiddenTabs_, hiddenTabSetTabPanel_, hiddenTabSetMinPanel_); + // manage sidebar + showSidebar(evt.getValue().getSidebarVisible()); + // manage source column commands boolean visible = userPrefs.allowSourceColumns().getValue() && (userPrefs.panes().getValue().getAdditionalSourceColumns() < MAX_COLUMN_COUNT); @@ -937,7 +960,158 @@ public void onPaneLayout() { optionsLoader_.showOptions(PaneLayoutPreferencesPane.class, true); } + + private void setSidebarPref(boolean showSidebar) + { + PaneConfig paneConfig = userPrefs_.panes().getValue().cast(); + if (showSidebar == paneConfig.getSidebarVisible()) + return; + + // Update the preference + userPrefs_.panes().setGlobalValue(PaneConfig.create( + JsArrayUtil.copy(paneConfig.getQuadrants()), + paneConfig.getTabSet1(), + paneConfig.getTabSet2(), + paneConfig.getHiddenTabSet(), + paneConfig.getConsoleLeftOnTop(), + paneConfig.getConsoleRightOnTop(), + paneConfig.getAdditionalSourceColumns(), + paneConfig.getSidebar(), + showSidebar, + paneConfig.getSidebarLocation())); + + userPrefs_.writeUserPrefs(); + } + + @Handler + public void onShowSidebar() + { + setSidebarPref(true); + } + + @Handler + public void onHideSidebar() + { + setSidebarPref(false); + } + + @Handler + public void onToggleSidebar() + { + // Toggle the preference value and update UI + PaneConfig paneConfig = userPrefs_.panes().getValue().cast(); + boolean newVisibility = !paneConfig.getSidebarVisible(); + + setSidebarPref(newVisibility); + } + + @Handler + public void onMoveSidebar() + { + // Toggle the sidebar location between left and right + PaneConfig paneConfig = userPrefs_.panes().getValue().cast(); + String currentLocation = paneConfig.getSidebarLocation(); + String newLocation = "left".equals(currentLocation) ? "right" : "left"; + + // Update the preference + userPrefs_.panes().setGlobalValue(PaneConfig.create( + JsArrayUtil.copy(paneConfig.getQuadrants()), + paneConfig.getTabSet1(), + paneConfig.getTabSet2(), + paneConfig.getHiddenTabSet(), + paneConfig.getConsoleLeftOnTop(), + paneConfig.getConsoleRightOnTop(), + paneConfig.getAdditionalSourceColumns(), + paneConfig.getSidebar(), + paneConfig.getSidebarVisible(), + newLocation)); + + // Persist the preference change + userPrefs_.writeUserPrefs(); + + // Update the UI by hiding and re-showing the sidebar in the new location + refreshSidebar(); + } + + public void showSidebar(boolean showSidebar) + { + if (showSidebar && sidebar_ == null) + { + // Create sidebar configuration + PaneConfig config = userPrefs_.panes().getValue().cast(); + JsArrayString sidebarTabs = config.getSidebar(); + + // Create sidebar tabset if not already created + LogicalWindow sidebarWindow = panesByName_.get(UserPrefsAccessor.Panes.QUADRANTS_SIDEBAR); + if (sidebarWindow == null) + { + Triad sidebar = createTabSet( + UserPrefsAccessor.Panes.QUADRANTS_SIDEBAR, + tabNamesToTabs(sidebarTabs)); + panesByName_.put(UserPrefsAccessor.Panes.QUADRANTS_SIDEBAR, sidebar.first); + sidebarTabPanel_ = sidebar.second; + sidebarMinPanel_ = sidebar.third; + sidebarTabs_ = tabNamesToTabs(sidebarTabs); + sidebarWindow = sidebar.first; + } + + if (sidebarWindow != null) + { + // For sidebar, we use just the WindowFrame directly (no vertical split) + sidebarWindow.transitionToState(WindowState.NORMAL); + sidebar_ = sidebarWindow.getNormal(); + String location = config.getSidebarLocation(); + panel_.setSidebarWidget(sidebar_, location); + } + } + else if (!showSidebar && sidebar_ != null) + { + panel_.removeSidebarWidget(); + sidebar_ = null; + } + + // manage commands + commands_.showSidebar().setVisible(!showSidebar); + commands_.hideSidebar().setVisible(showSidebar); + } + + public void refreshSidebar() + { + // If sidebar is visible, refresh it (e.g. if the sidebar location has changed) + if (sidebar_ != null) + { + showSidebar(false); + showSidebar(true); + } + } + public void setSidebarLocation(String location) + { + // Update preference and refresh the sidebar if visible + PaneConfig paneConfig = userPrefs_.panes().getValue().cast(); + + // Only update if location has changed + if (!location.equals(paneConfig.getSidebarLocation())) + { + userPrefs_.panes().setGlobalValue(PaneConfig.create( + JsArrayUtil.copy(paneConfig.getQuadrants()), + paneConfig.getTabSet1(), + paneConfig.getTabSet2(), + paneConfig.getHiddenTabSet(), + paneConfig.getConsoleLeftOnTop(), + paneConfig.getConsoleRightOnTop(), + paneConfig.getAdditionalSourceColumns(), + paneConfig.getSidebar(), + paneConfig.getSidebarVisible(), + location)); + + userPrefs_.writeUserPrefs(); + + // If sidebar is visible, refresh it in the new location + refreshSidebar(); + } + } + private boolean equals(T lhs, T rhs) { if (lhs == null) @@ -1016,6 +1190,8 @@ else if (sourceLogicalWindows_.contains(window)) DomUtils.contains(right_.getElement(), window.getActiveWidget().getElement()); boolean isCenterWidget = DomUtils.contains(center_.getElement(), window.getActiveWidget().getElement()); + boolean isSidebarWidget = sidebar_ != null && + DomUtils.contains(sidebar_.getElement(), window.getActiveWidget().getElement()); window.onWindowStateChange(new WindowStateChangeEvent(WindowState.EXCLUSIVE)); @@ -1299,6 +1475,14 @@ private void initPanes(PaneConfig config) hiddenTabSetTabPanel_ = tsHide.second; hiddenTabSetTabPanel_.setNeverVisible(true); hiddenTabSetMinPanel_ = tsHide.third; + + // Initialize sidebar (always create it, even if not initially visible) + Triad sidebar = createTabSet( + UserPrefsAccessor.Panes.QUADRANTS_SIDEBAR, + tabNamesToTabs(config.getSidebar())); + panesByName_.put(UserPrefsAccessor.Panes.QUADRANTS_SIDEBAR, sidebar.first); + sidebarTabPanel_ = sidebar.second; + sidebarMinPanel_ = sidebar.third; } private ArrayList tabNamesToTabs(JsArrayString tabNames) @@ -2096,6 +2280,10 @@ private void clearFocusIndicator() private MinimizedModuleTabLayoutPanel tabSet2MinPanel_; private WorkbenchTabPanel hiddenTabSetTabPanel_; private MinimizedModuleTabLayoutPanel hiddenTabSetMinPanel_; + private WorkbenchTabPanel sidebarTabPanel_; + private MinimizedModuleTabLayoutPanel sidebarMinPanel_; + private Widget sidebar_; + private ArrayList sidebarTabs_; // Zoom-related members ---- private Tab lastSelectedTab_ = null; diff --git a/src/gwt/test/org/rstudio/studio/client/workbench/commands/DummyCommands.java b/src/gwt/test/org/rstudio/studio/client/workbench/commands/DummyCommands.java index 50c4bf07a17..0f807136f72 100644 --- a/src/gwt/test/org/rstudio/studio/client/workbench/commands/DummyCommands.java +++ b/src/gwt/test/org/rstudio/studio/client/workbench/commands/DummyCommands.java @@ -1564,6 +1564,26 @@ public AppCommand toggleToolbar() { return null; } + @Override + public AppCommand showSidebar() { + return null; + } + + @Override + public AppCommand hideSidebar() { + return null; + } + + @Override + public AppCommand toggleSidebar() { + return null; + } + + @Override + public AppCommand moveSidebar() { + return null; + } + @Override public AppCommand zoomActualSize() { return null;