From 9f977aaf94369dd4cf607597b44447828f8088c8 Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Thu, 23 Feb 2012 20:57:14 +0100 Subject: [PATCH] initial implementation of scrollable single-row layout for editor tabs (idea.is.internal for now) --- .../intellij/ui/tabs/impl/JBEditorTabs.java | 11 ++ .../com/intellij/ui/tabs/impl/JBTabsImpl.java | 23 +++- .../com/intellij/ui/tabs/impl/TabLabel.java | 12 +- .../singleRow/ScrollableSingleRowLayout.java | 115 ++++++++++++++++++ .../tabs/impl/singleRow/SingleRowLayout.java | 102 +++++++++++----- .../impl/singleRow/SingleRowPassInfo.java | 4 +- 6 files changed, 233 insertions(+), 34 deletions(-) create mode 100644 platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/ScrollableSingleRowLayout.java diff --git a/platform/platform-api/src/com/intellij/ui/tabs/impl/JBEditorTabs.java b/platform/platform-api/src/com/intellij/ui/tabs/impl/JBEditorTabs.java index 96a225ed6fd5..526e8cb9cc01 100644 --- a/platform/platform-api/src/com/intellij/ui/tabs/impl/JBEditorTabs.java +++ b/platform/platform-api/src/com/intellij/ui/tabs/impl/JBEditorTabs.java @@ -17,11 +17,14 @@ package com.intellij.ui.tabs.impl; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.ui.tabs.JBTabsPosition; import com.intellij.ui.tabs.TabInfo; import com.intellij.ui.tabs.TabsUtil; +import com.intellij.ui.tabs.impl.singleRow.ScrollableSingleRowLayout; +import com.intellij.ui.tabs.impl.singleRow.SingleRowLayout; import com.intellij.ui.tabs.impl.table.TableLayout; import com.intellij.util.ui.SameColor; import com.intellij.util.ui.UIUtil; @@ -40,6 +43,14 @@ public class JBEditorTabs extends JBTabsImpl { super(project, actionManager, focusManager, parent); } + @Override + protected SingleRowLayout createSingleRowLayout() { + if (ApplicationManager.getApplication().isInternal()) { + return new ScrollableSingleRowLayout(this); + } + return super.createSingleRowLayout(); + } + @Override public boolean isEditorTabs() { return true; diff --git a/platform/platform-api/src/com/intellij/ui/tabs/impl/JBTabsImpl.java b/platform/platform-api/src/com/intellij/ui/tabs/impl/JBTabsImpl.java index 4dc325e3c5e3..5f7da8ce2610 100644 --- a/platform/platform-api/src/com/intellij/ui/tabs/impl/JBTabsImpl.java +++ b/platform/platform-api/src/com/intellij/ui/tabs/impl/JBTabsImpl.java @@ -103,11 +103,11 @@ public class JBTabsImpl extends JComponent private final WeakHashMap myDeferredToRemove = new WeakHashMap(); - private final SingleRowLayout mySingleRowLayout = new SingleRowLayout(this); + private final SingleRowLayout mySingleRowLayout; private final TableLayout myTableLayout = new TableLayout(this); - private TabLayout myLayout = mySingleRowLayout; + private TabLayout myLayout; private LayoutPassInfo myLastLayoutPass; private TabInfo myLastPaintedSelection; @@ -206,6 +206,9 @@ public class JBTabsImpl extends JComponent setUiDecorator(null); + mySingleRowLayout = createSingleRowLayout(); + myLayout = mySingleRowLayout; + myPopupListener = new PopupMenuListener() { public void popupMenuWillBecomeVisible(final PopupMenuEvent e) { } @@ -228,6 +231,15 @@ public class JBTabsImpl extends JComponent } } }); + addMouseWheelListener(new MouseWheelListener() { + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (mySingleRowLayout.myLastSingRowLayout != null) { + mySingleRowLayout.scroll(e.getUnitsToScroll() * 10); + revalidateAndRepaint(false); + } + } + }); myAnimator = new Animator("JBTabs Attractions", 2, 500, true) { public void paintNow(final int frame, final int totalFrames, final int cycle) { @@ -284,6 +296,10 @@ public class JBTabsImpl extends JComponent }; } + protected SingleRowLayout createSingleRowLayout() { + return new SingleRowLayout(this); + } + public JBTabs setNavigationActionBinding(String prevActionId, String nextActionId) { if (myNextAction != null) { @@ -2491,8 +2507,6 @@ public class JBTabsImpl extends JComponent } private void updateContainer(boolean forced, final boolean layoutNow) { - final TabLabel selectedLabel = getSelectedLabel(); - for (TabInfo each : myVisibleInfos) { final JComponent eachComponent = each.getComponent(); if (getSelectedInfo() == each && getSelectedInfo() != null) { @@ -2518,6 +2532,7 @@ public class JBTabsImpl extends JComponent } } + mySingleRowLayout.scrollSelectionInView(myVisibleInfos); relayout(forced, layoutNow); } diff --git a/platform/platform-api/src/com/intellij/ui/tabs/impl/TabLabel.java b/platform/platform-api/src/com/intellij/ui/tabs/impl/TabLabel.java index 0889323cc8d7..ff7c60badee8 100644 --- a/platform/platform-api/src/com/intellij/ui/tabs/impl/TabLabel.java +++ b/platform/platform-api/src/com/intellij/ui/tabs/impl/TabLabel.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2009 JetBrains s.r.o. + * Copyright 2000-2012 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -250,6 +250,10 @@ public class TabLabel extends JPanel { @Override public Dimension getPreferredSize() { final Dimension size = super.getPreferredSize(); + if (myActionPanel != null && !myActionPanel.isVisible()) { + final Dimension actionPanelSize = myActionPanel.getPreferredSize(); + size.width += actionPanelSize.width; + } final JBTabsPosition pos = myTabs.getTabsPosition(); switch (pos) { @@ -526,6 +530,12 @@ public class TabLabel extends JPanel { } } + public void setActionPanelVisible(boolean visible) { + if (myActionPanel != null) { + myActionPanel.setVisible(visible); + } + } + @Override public String toString() { return myInfo.getText(); diff --git a/platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/ScrollableSingleRowLayout.java b/platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/ScrollableSingleRowLayout.java new file mode 100644 index 000000000000..4661611f1f2a --- /dev/null +++ b/platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/ScrollableSingleRowLayout.java @@ -0,0 +1,115 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.ui.tabs.impl.singleRow; + +import com.intellij.ui.tabs.TabInfo; +import com.intellij.ui.tabs.impl.JBTabsImpl; +import com.intellij.ui.tabs.impl.TabLabel; + +import java.util.List; + +/** + * @author yole + */ +public class ScrollableSingleRowLayout extends SingleRowLayout { + private int myScrollOffset = 0; + private boolean myScrollSelectionInViewPending = false; + + public ScrollableSingleRowLayout(final JBTabsImpl tabs) { + super(tabs); + } + + @Override + int getScrollOffset() { + return myScrollOffset; + } + + @Override + public void scroll(int units) { + myScrollOffset += units; + if (myLastSingRowLayout != null) { + clampScrollOffsetToBounds(myLastSingRowLayout); + } + } + + private void clampScrollOffsetToBounds(SingleRowPassInfo data) { + if (data.requiredLength < data.toFitLength) { + myScrollOffset = 0; + } + else { + myScrollOffset = Math.max(0, Math.min(myScrollOffset, data.requiredLength - data.toFitLength + getStrategy().getMoreRectAxisSize())); + } + } + + @Override + public void scrollSelectionInView(List visibleInfos) { + myScrollSelectionInViewPending = true; + } + + private void doScrollSelectionInView(SingleRowPassInfo passInfo) { + int offset = -myScrollOffset; + for (TabInfo info : passInfo.myVisibleInfos) { + final int length = getRequiredLength(info); + if (info == myTabs.getSelectedInfo()) { + if (offset < 0) { + scroll(offset); + } + else if (offset + length > passInfo.toFitLength - getStrategy().getMoreRectAxisSize()) { + scroll(offset + length - passInfo.toFitLength + getStrategy().getMoreRectAxisSize()); + } + break; + } + offset += length; + } + } + + @Override + protected void recomputeToLayout(SingleRowPassInfo data) { + calculateRequiredLength(data); + clampScrollOffsetToBounds(data); + if (myScrollSelectionInViewPending || !data.layoutSize.equals(myLastSingRowLayout.layoutSize)) { + myScrollSelectionInViewPending = false; + doScrollSelectionInView(data); + } + } + + protected void layoutMoreButton(SingleRowPassInfo data) { + if (data.requiredLength > data.toFitLength) { + data.moreRect = getStrategy().getMoreRect(data); + } + } + + @Override + protected void updateMoreIconVisibility(SingleRowPassInfo data) { + myMoreIcon.setPainted(data.requiredLength > data.toFitLength); + } + + @Override + protected boolean applyTabLayout(SingleRowPassInfo data, TabLabel label, int length, int deltaToFit) { + if (data.requiredLength > data.toFitLength) { + length = getStrategy().getLengthIncrement(label.getPreferredSize()); + final int moreRectSize = getStrategy().getMoreRectAxisSize(); + if (data.position + length > data.toFitLength - moreRectSize) { + super.applyTabLayout(data, label, data.toFitLength - data.position - moreRectSize - 4, deltaToFit); + label.setAlignmentToCenter(false); + label.setActionPanelVisible(false); + return false; + } + } + label.setActionPanelVisible(true); + return super.applyTabLayout(data, label, length, deltaToFit); + } +} diff --git a/platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/SingleRowLayout.java b/platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/SingleRowLayout.java index 2b82342eff49..e8e1410851de 100644 --- a/platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/SingleRowLayout.java +++ b/platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/SingleRowLayout.java @@ -63,7 +63,6 @@ public class SingleRowLayout extends TabLayout { private RowDropPolicy myRowDropPolicy = RowDropPolicy.first; - @Override public boolean isSideComponentOnTabs() { return getStrategy().isSideComponentOnTabs(); @@ -108,7 +107,8 @@ public class SingleRowLayout extends TabLayout { if (!myTabs.myForcedRelayout && myLastSingRowLayout != null && myLastSingRowLayout.contentCount == myTabs.getTabCount() && - myLastSingRowLayout.layoutSize.equals(myTabs.getSize())) { + myLastSingRowLayout.layoutSize.equals(myTabs.getSize()) && + myLastSingRowLayout.scrollOffset == getScrollOffset()) { for (TabInfo each : data.myVisibleInfos) { final TabLabel eachLabel = myTabs.myInfo2Label.get(each); if (!eachLabel.isValid()) { @@ -125,10 +125,18 @@ public class SingleRowLayout extends TabLayout { return layoutLabels; } + int getScrollOffset() { + return 0; + } + + public void scroll(int units) { + } + + public void scrollSelectionInView(List visibleInfos) { + } + public LayoutPassInfo layoutSingleRow(List visibleInfos) { SingleRowPassInfo data = new SingleRowPassInfo(this, visibleInfos); - final TabInfo selected = myTabs.getSelectedInfo(); - final JBTabsImpl.Toolbar selectedToolbar = myTabs.myInfo2Toolbar.get(selected); final boolean layoutLabels = checkLayoutLabels(data); if (!layoutLabels) { @@ -136,24 +144,20 @@ public class SingleRowLayout extends TabLayout { } - data.insets = myTabs.getLayoutInsets(); - - data.hToolbar = selectedToolbar != null && myTabs.myHorizontalSide && !selectedToolbar.isEmpty() ? selectedToolbar : null; - data.vToolbar = selectedToolbar != null && !myTabs.myHorizontalSide && !selectedToolbar.isEmpty() ? selectedToolbar : null; + final TabInfo selected = myTabs.getSelectedInfo(); + prepareLayoutPassInfo(data, selected); myTabs.resetLayout(layoutLabels || myTabs.isHideTabs()); if (layoutLabels && !myTabs.isHideTabs()) { - data.position = getStrategy().getStartPosition(data); + data.position = getStrategy().getStartPosition(data) - getScrollOffset(); recomputeToLayout(data); layoutLabelsAndGhosts(data); - if (data.toDrop.size() > 0) { - data.moreRect = getStrategy().getMoreRect(data); - } + layoutMoreButton(data); } if (selected != null) { @@ -161,7 +165,7 @@ public class SingleRowLayout extends TabLayout { getStrategy().layoutComp(data); } - myMoreIcon.setPainted(data.toLayout.size() > 0 && data.myVisibleInfos.size() > data.toLayout.size()); + updateMoreIconVisibility(data); data.tabRectangle = new Rectangle(); @@ -180,6 +184,29 @@ public class SingleRowLayout extends TabLayout { return data; } + protected void prepareLayoutPassInfo(SingleRowPassInfo data, TabInfo selected) { + data.insets = myTabs.getLayoutInsets(); + + final JBTabsImpl.Toolbar selectedToolbar = myTabs.myInfo2Toolbar.get(selected); + data.hToolbar = selectedToolbar != null && myTabs.myHorizontalSide && !selectedToolbar.isEmpty() ? selectedToolbar : null; + data.vToolbar = selectedToolbar != null && !myTabs.myHorizontalSide && !selectedToolbar.isEmpty() ? selectedToolbar : null; + data.toFitLength = getStrategy().getToFitLength(data); + + if (myTabs.isGhostsAlwaysVisible()) { + data.toFitLength -= myTabs.getGhostTabLength() * 2 + (myTabs.getInterTabSpaceLength() * 2); + } + } + + protected void updateMoreIconVisibility(SingleRowPassInfo data) { + myMoreIcon.setPainted(data.toLayout.size() > 0 && data.myVisibleInfos.size() > data.toLayout.size()); + } + + protected void layoutMoreButton(SingleRowPassInfo data) { + if (data.toDrop.size() > 0) { + data.moreRect = getStrategy().getMoreRect(data); + } + } + private void layoutLabelsAndGhosts(final SingleRowPassInfo data) { if (data.firstGhostVisible || myTabs.isGhostsAlwaysVisible()) { data.firstGhost = getStrategy().getLayoutRect(data, data.position, myTabs.getGhostTabLength()); @@ -196,8 +223,17 @@ public class SingleRowLayout extends TabLayout { int totalLength = 0; int positionStart = data.position; + boolean layoutStopped = false; for (TabInfo eachInfo : data.toLayout) { final TabLabel label = myTabs.myInfo2Label.get(eachInfo); + if (layoutStopped) { + label.setActionPanelVisible(false); + final Rectangle rec = getStrategy().getLayoutRect(data, 0, 0); + myTabs.layout(label, rec); + continue; + } + + label.setActionPanelVisible(true); final Dimension eachSize = label.getPreferredSize(); boolean isLast = data.toLayout.indexOf(eachInfo) == data.toLayout.size() - 1; @@ -209,15 +245,15 @@ public class SingleRowLayout extends TabLayout { else { length = data.toFitLength - totalLength; } - final Rectangle rec = getStrategy().getLayoutRect(data, data.position, length); - myTabs.layout(label, rec); - - label.setAlignmentToCenter((deltaToFit > 0 || myTabs.isEditorTabs()) && getStrategy().isToCenterTextWhenStretched()); + boolean continueLayout = applyTabLayout(data, label, length, deltaToFit); data.position = getStrategy().getMaxPosition(label.getBounds()); data.position += myTabs.getInterTabSpaceLength(); totalLength = getStrategy().getMaxPosition(label.getBounds()) - positionStart + myTabs.getInterTabSpaceLength(); + if (!continueLayout) { + layoutStopped = true; + } } for (TabInfo eachInfo : data.toDrop) { @@ -230,19 +266,17 @@ public class SingleRowLayout extends TabLayout { } } + protected boolean applyTabLayout(SingleRowPassInfo data, TabLabel label, int length, int deltaToFit) { + final Rectangle rec = getStrategy().getLayoutRect(data, data.position, length); + myTabs.layout(label, rec); - private void recomputeToLayout(final SingleRowPassInfo data) { - data.toFitLength = getStrategy().getToFitLength(data); + label.setAlignmentToCenter((deltaToFit > 0 || myTabs.isEditorTabs()) && getStrategy().isToCenterTextWhenStretched()); + return true; + } - if (myTabs.isGhostsAlwaysVisible()) { - data.toFitLength -= myTabs.getGhostTabLength() * 2 + (myTabs.getInterTabSpaceLength() * 2); - } - - for (TabInfo eachInfo : data.myVisibleInfos) { - data.requiredLength += getStrategy().getLengthIncrement(myTabs.myInfo2Label.get(eachInfo).getPreferredSize()) - + (myTabs.isEditorTabs() ? myTabs.getInterTabSpaceLength() : 0); - data.toLayout.add(eachInfo); - } + + protected void recomputeToLayout(final SingleRowPassInfo data) { + calculateRequiredLength(data); while (true) { if (data.requiredLength <= data.toFitLength - data.position) break; @@ -292,6 +326,18 @@ public class SingleRowLayout extends TabLayout { } + protected void calculateRequiredLength(SingleRowPassInfo data) { + for (TabInfo eachInfo : data.myVisibleInfos) { + data.requiredLength += getRequiredLength(eachInfo); + data.toLayout.add(eachInfo); + } + } + + protected int getRequiredLength(TabInfo eachInfo) { + return getStrategy().getLengthIncrement(myTabs.myInfo2Label.get(eachInfo).getPreferredSize()) + + (myTabs.isEditorTabs() ? myTabs.getInterTabSpaceLength() : 0); + } + public class GhostComponent extends JLabel { private TabInfo myInfo; diff --git a/platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/SingleRowPassInfo.java b/platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/SingleRowPassInfo.java index 911351e8e7a9..62c1b09172cb 100644 --- a/platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/SingleRowPassInfo.java +++ b/platform/platform-api/src/com/intellij/ui/tabs/impl/singleRow/SingleRowPassInfo.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2009 JetBrains s.r.o. + * Copyright 2000-2012 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,6 +51,7 @@ public class SingleRowPassInfo extends LayoutPassInfo { private final JBTabsImpl myTabs; public JComponent comp; public Rectangle tabRectangle; + int scrollOffset; public SingleRowPassInfo(SingleRowLayout layout, List visibleInfos) { @@ -61,6 +62,7 @@ public class SingleRowPassInfo extends LayoutPassInfo { toLayout = new ArrayList(); toDrop = new ArrayList(); moreRectAxisSize = layout.getStrategy().getMoreRectAxisSize(); + scrollOffset = layout.getScrollOffset(); } public TabInfo getPreviousFor(final TabInfo info) {