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 1d0a5342e28a..b7408a7409de 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 @@ -31,6 +31,7 @@ import com.intellij.ui.tabs.impl.singleRow.ScrollableSingleRowLayout; import com.intellij.ui.tabs.impl.singleRow.SingleRowLayout; import com.intellij.ui.tabs.impl.singleRow.SingleRowPassInfo; import com.intellij.ui.tabs.impl.table.TableLayout; +import com.intellij.ui.tabs.impl.table.TablePassInfo; import com.intellij.ui.tabs.impl.tabsLayout.TabsLayout; import com.intellij.ui.tabs.impl.tabsLayout.TabsLayoutCallback; import com.intellij.ui.tabs.impl.tabsLayout.TabsLayoutInfo; @@ -199,6 +200,7 @@ public class JBTabsImpl extends JComponent private MouseListener myTabsLayoutMouseListener; private MouseMotionListener myTabsLayoutMouseMotionListener; private MouseWheelListener myTabsLayoutMouseWheelListener; + private boolean mySingleRow = true; protected JBTabsBorder createTabBorder() { return new JBDefaultTabsBorder(this); @@ -405,7 +407,7 @@ public class JBTabsImpl extends JComponent setUiDecorator(null); mySingleRowLayout = createSingleRowLayout(); - myLayout = mySingleRowLayout; + setLayout(mySingleRowLayout); mySplitter.getDivider().setOpaque(false); @@ -457,19 +459,28 @@ public class JBTabsImpl extends JComponent if (mySingleRowLayout.myLastSingRowLayout != null) { mySingleRowLayout.scroll((int)Math.round(units * mySingleRowLayout.getScrollUnitIncrement())); revalidateAndRepaint(false); + } else if (myTableLayout.myLastTableLayout != null) { + myTableLayout.scroll((int)Math.round(units * myTableLayout.getScrollUnitIncrement())); + revalidateAndRepaint(false); } }); AWTEventListener listener = new AWTEventListener() { final Alarm afterScroll = new Alarm(parentDisposable); @Override public void eventDispatched(AWTEvent event) { - if (mySingleRowLayout.myLastSingRowLayout == null) return; + Rectangle tabRectangle = null; + if (mySingleRowLayout.myLastSingRowLayout != null) { + tabRectangle = mySingleRowLayout.myLastSingRowLayout.tabRectangle; + } else if (myTableLayout.myLastTableLayout != null) { + tabRectangle = myTableLayout.myLastTableLayout.tabRectangle; + } + if (tabRectangle == null) return; MouseEvent me = (MouseEvent)event; Point point = me.getPoint(); SwingUtilities.convertPointToScreen(point, me.getComponent()); Rectangle rect = getVisibleRect(); - rect = rect.intersection(mySingleRowLayout.myLastSingRowLayout.tabRectangle); + rect = rect.intersection(tabRectangle); Point p = rect.getLocation(); SwingUtilities.convertPointToScreen(p, JBTabsImpl.this); rect.setLocation(p); @@ -552,12 +563,21 @@ public class JBTabsImpl extends JComponent @Override protected void paintChildren(Graphics g) { super.paintChildren(g); - if (Registry.is("ui.no.bangs.and.whistles", false)) { + if (Registry.is("ui.no.bangs.and.whistles", false) || !isSingleRow() || !UISettings.getInstance().getHideTabsIfNeeded()) { return; } - if (Registry.is("ide.editor.tabs.show.fadeout") && !getTabsPosition().isSide() && myMoreToolbar.getComponent().isShowing()) { + JComponent more = myMoreToolbar.getComponent(); + + if (Registry.is("ide.editor.tabs.show.fadeout") && !getTabsPosition().isSide() && more.isShowing()) { + TabInfo selectedInfo = getSelectedInfo(); + final JBTabsImpl.Toolbar selectedToolbar = selectedInfo != null ? myInfo2Toolbar.get(selectedInfo) : null; int width = JBUI.scale(MathUtil.clamp(Registry.intValue("ide.editor.tabs.fadeout.width", 10), 1, 200)); + Rectangle moreRect = getMoreRect(); Rectangle labelsArea = null; + + int moreY = 0; + int moreHeight = 0; + boolean showRightFadeout = false; boolean showLeftFadeout = false; for (TabLabel label : myInfo2Label.values()) { @@ -567,18 +587,25 @@ public class JBTabsImpl extends JComponent labelsArea = labelsArea.union(label.getBounds()); } showLeftFadeout |= label.getX() < 0; - showRightFadeout |= label.getWidth() < label.getPreferredSize().width - 1; + boolean needShowRightFadeout = moreRect != null + && label.getX() + label.getPreferredSize().width > moreRect.x + && label.getY() == moreRect.y; + if (needShowRightFadeout && !showRightFadeout) { + moreY = label.getY(); + moreHeight = label.getHeight(); + } + showRightFadeout |= needShowRightFadeout; } Color transparent = ColorUtil.withAlpha(UIUtil.getPanelBackground(), 0); if (showLeftFadeout) { - Rectangle leftSide = new Rectangle(0, labelsArea.y, width, labelsArea.height); + Rectangle leftSide = new Rectangle(0, more.getY() - 1, width, more.getHeight() + 1); ((Graphics2D)g).setPaint( new GradientPaint(leftSide.x, leftSide.y, UIUtil.getPanelBackground(), leftSide.x + leftSide.width, leftSide.y, transparent)); ((Graphics2D)g).fill(leftSide); } if (showRightFadeout) { - Rectangle rightSide = new Rectangle(myMoreToolbar.getComponent().getX() - 1 - width, labelsArea.y, width, labelsArea.height); + Rectangle rightSide = new Rectangle(myMoreToolbar.getComponent().getX() - 1 - width, moreY, width, moreHeight); ((Graphics2D)g).setPaint( new GradientPaint(rightSide.x, rightSide.y, transparent, rightSide.x + rightSide.width, rightSide.y, UIUtil.getPanelBackground())); @@ -605,20 +632,23 @@ public class JBTabsImpl extends JComponent info.revalidate(); label.setTabActions(info.getTabLabelActions()); } - boolean oldHideTabsIfNeeded = mySingleRowLayout instanceof ScrollableSingleRowLayout; - boolean newHideTabsIfNeeded = uiSettings.getHideTabsIfNeeded(); - if (oldHideTabsIfNeeded != newHideTabsIfNeeded) { - updateRowLayout(); - } + updateRowLayout(); } private void updateRowLayout() { - boolean wasSingleRow = isSingleRow(); mySingleRowLayout = createSingleRowLayout(); - if (wasSingleRow) { - myLayout = mySingleRowLayout; + boolean useTableLayout = !isSingleRow(); + useTableLayout |= getTabsPosition() == JBTabsPosition.top + && supportsTableLayoutAsSingleRow() + && UISettings.getInstance().getState().getShowPinnedTabsInASeparateRow(); + TabLayout layout = useTableLayout ? myTableLayout : mySingleRowLayout; + if (setLayout(layout)) { + relayout(true, true); } - relayout(true, true); + } + + protected boolean supportsTableLayoutAsSingleRow() { + return false; } protected SingleRowLayout createSingleRowLayout() { @@ -692,7 +722,7 @@ public class JBTabsImpl extends JComponent } public boolean isDragOut(TabLabel label, int deltaX, int deltaY) { - if (NEW_TABS) { + if (!NEW_TABS) { return getEffectiveLayout().isDragOut(label, deltaX, deltaY); } else { return myTabsLayout.isDragOut(label, deltaX, deltaY); @@ -703,7 +733,7 @@ public class JBTabsImpl extends JComponent if (NEW_TABS) { return myTabsLayout != null && myTabsLayout.ignoreTabLabelLimitedWidthWhenPaint(); } else { - return getEffectiveLayout() instanceof ScrollableSingleRowLayout; + return myLayout instanceof ScrollableSingleRowLayout /*|| myLayout instanceof TableLayout*/; } } @@ -955,13 +985,27 @@ public class JBTabsImpl extends JComponent } private Rectangle getMoreRect() { - SingleRowPassInfo lastLayout = mySingleRowLayout.myLastSingRowLayout; - return lastLayout != null ? lastLayout.moreRect : null; + if (myLayout instanceof SingleRowLayout) { + SingleRowPassInfo lastLayout = mySingleRowLayout.myLastSingRowLayout; + return lastLayout != null ? lastLayout.moreRect : null; + } + if (myLayout instanceof TableLayout) { + TablePassInfo lastLayout = myTableLayout.myLastTableLayout; + return lastLayout != null ? lastLayout.moreRect : null; + } + return null; } private Rectangle getTitleRect() { - SingleRowPassInfo lastLayout = mySingleRowLayout.myLastSingRowLayout; - return lastLayout != null ? lastLayout.titleRect : null; + if (myLayout instanceof SingleRowLayout) { + SingleRowPassInfo lastLayout = mySingleRowLayout.myLastSingRowLayout; + return lastLayout != null ? lastLayout.titleRect : null; + } + if (myLayout instanceof TableLayout) { + TablePassInfo lastLayout = myTableLayout.myLastTableLayout; + return lastLayout != null ? lastLayout.titleRect : null; + } + return null; } @Override @@ -977,18 +1021,16 @@ public class JBTabsImpl extends JComponent @Override public boolean canShowMorePopup() { - if (NEW_TABS) { - return myTabsLayout instanceof MorePopupAware && ((MorePopupAware)myTabsLayout).canShowMorePopup(); - } - return getMoreRect() != null; + if (myLayout instanceof MorePopupAware) + return ((MorePopupAware)myLayout).canShowMorePopup(); + Rectangle moreRect = getMoreRect(); + return moreRect != null; } @Override public void showMorePopup() { - if (NEW_TABS) { - if (myTabsLayout instanceof MorePopupAware) { - ((MorePopupAware)myTabsLayout).showMorePopup(); - } + if (myLayout instanceof MorePopupAware) { + ((MorePopupAware)myLayout).showMorePopup(); return; } @@ -1775,12 +1817,13 @@ public class JBTabsImpl extends JComponent } } - if (isSingleRow()) { + if (myLayout instanceof SingleRowLayout) { mySingleRowLayout.scrollSelectionInView(); myLastLayoutPass = mySingleRowLayout.layoutSingleRow(visible); Rectangle moreRect = getMoreRect(); - if (moreRect != null) { - Dimension preferredSize = myMoreToolbar.getComponent().getPreferredSize(); + JComponent mComponent = myMoreToolbar.getComponent(); + if (moreRect != null && !moreRect.isEmpty()) { + Dimension preferredSize = mComponent.getPreferredSize(); Rectangle bounds = new Rectangle(moreRect); int xDiff = (bounds.width - preferredSize.width) / 2; int yDiff = (bounds.height - preferredSize.height) / 2; @@ -1788,12 +1831,12 @@ public class JBTabsImpl extends JComponent bounds.width -= 2 * xDiff; bounds.y += yDiff; bounds.height -= 2* yDiff; - myMoreToolbar.getComponent().setBounds(bounds); + mComponent.setBounds(bounds); } else { - myMoreToolbar.getComponent().setBounds(new Rectangle()); + mComponent.setBounds(new Rectangle()); } Rectangle titleRect = getTitleRect(); - if (titleRect != null) { + if (titleRect != null && !titleRect.isEmpty()) { Dimension preferredSize = myTitleWrapper.getPreferredSize(); Rectangle bounds = new Rectangle(titleRect); JBInsets.removeFrom(bounds, getLayoutInsets()); @@ -1817,7 +1860,9 @@ public class JBTabsImpl extends JComponent } } else { - myLastLayoutPass = myTableLayout.layoutTable(visible); + //TableLayout does layout 'Title' and 'More' by itself + myTableLayout.scrollSelectionInView(); + myLastLayoutPass = myTableLayout.layoutTable(visible, myTitleWrapper, myMoreToolbar.getComponent()); mySingleRowLayout.myLastSingRowLayout = null; } @@ -2346,8 +2391,11 @@ public class JBTabsImpl extends JComponent } } } - - mySingleRowLayout.scrollSelectionInView(); + if (myLayout == mySingleRowLayout) { + mySingleRowLayout.scrollSelectionInView(); + } else if (myLayout == myTableLayout) { + myTableLayout.scrollSelectionInView(); + } relayout(forced, layoutNow); } @@ -2372,7 +2420,8 @@ public class JBTabsImpl extends JComponent myForcedRelayout = forced; } if (myMoreToolbar != null) { - myMoreToolbar.getComponent().setVisible(getEffectiveLayout() instanceof ScrollableSingleRowLayout); + myMoreToolbar.getComponent().setVisible(getEffectiveLayout() instanceof ScrollableSingleRowLayout || + getEffectiveLayout() instanceof TableLayout); } revalidateAndRepaint(layoutNow); } @@ -2752,13 +2801,17 @@ public class JBTabsImpl extends JComponent @Override public JBTabsPresentation setSingleRow(boolean singleRow) { - myLayout = singleRow ? mySingleRowLayout : myTableLayout; - - relayout(true, false); - + mySingleRow = singleRow; + updateRowLayout(); return this; } + private boolean setLayout(TabLayout layout) { + if (myLayout == layout) return false; + myLayout = layout; + return true; + } + public int getSeparatorWidth() { return mySeparatorWidth; } @@ -2769,11 +2822,7 @@ public class JBTabsImpl extends JComponent @Override public boolean isSingleRow() { - if (!NEW_TABS) { - return getEffectiveLayout() == mySingleRowLayout; - } else { - return myTabsLayout.isSingleRow(); - } + return mySingleRow; } public boolean isSideComponentVertical() { @@ -2789,8 +2838,7 @@ public class JBTabsImpl extends JComponent } public TabLayout getEffectiveLayout() { - if (myLayout == myTableLayout && getTabsPosition() == JBTabsPosition.top) return myTableLayout; - return mySingleRowLayout; + return myLayout; } @Override diff --git a/platform/platform-api/src/com/intellij/ui/tabs/impl/table/TableLayout.java b/platform/platform-api/src/com/intellij/ui/tabs/impl/table/TableLayout.java index 368de50352a6..e92fc21208b7 100644 --- a/platform/platform-api/src/com/intellij/ui/tabs/impl/table/TableLayout.java +++ b/platform/platform-api/src/com/intellij/ui/tabs/impl/table/TableLayout.java @@ -2,22 +2,30 @@ package com.intellij.ui.tabs.impl.table; import com.intellij.ide.ui.UISettings; +import com.intellij.openapi.ui.JBMenuItem; +import com.intellij.openapi.ui.JBPopupMenu; import com.intellij.ui.tabs.TabInfo; import com.intellij.ui.tabs.TabsUtil; -import com.intellij.ui.tabs.impl.JBTabsImpl; -import com.intellij.ui.tabs.impl.LayoutPassInfo; -import com.intellij.ui.tabs.impl.TabLabel; -import com.intellij.ui.tabs.impl.TabLayout; +import com.intellij.ui.tabs.impl.*; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.ui.GraphicsUtil; +import com.intellij.util.ui.JBFont; +import com.intellij.util.ui.JBUI; import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; +import java.util.Arrays; +import java.util.Iterator; import java.util.List; -public class TableLayout extends TabLayout { +public class TableLayout extends TabLayout implements MorePopupAware { + private int myScrollOffset = 0; + private boolean myScrollSelectionInViewPending = false; - private final JBTabsImpl myTabs; + final JBTabsImpl myTabs; public TablePassInfo myLastTableLayout; @@ -26,142 +34,260 @@ public class TableLayout extends TabLayout { } private TablePassInfo computeLayoutTable(List visibleInfos) { - final TablePassInfo data = new TablePassInfo(myTabs, visibleInfos); + final TablePassInfo data = new TablePassInfo(this, visibleInfos); + if (myTabs.isHideTabs()) { + return data; + } + boolean singleRow = myTabs.isSingleRow(); + boolean showPinnedTabsSeparately = UISettings.getInstance().getState().getShowPinnedTabsInASeparateRow(); + boolean scrollable = UISettings.getInstance().getHideTabsIfNeeded() && singleRow; + int titleWidth = myTabs.myTitleWrapper.getPreferredSize().width; - int eachXPinned = data.toFitRec.x; - int eachXUnpinned = data.toFitRec.x; + data.titleRect.setBounds(data.toFitRec.x, data.toFitRec.y, titleWidth, myTabs.myHeaderFitSize.height); + data.moreRect.setBounds(data.toFitRec.x + data.toFitRec.width, data.toFitRec.y, 0, myTabs.myHeaderFitSize.height); + calculateLengths(data); + + int eachX = data.titleRect.x + data.titleRect.width; + Insets insets = myTabs.getLayoutInsets(); + int eachY = insets.top; TableRow eachTableRow = new TableRow(data); data.table.add(eachTableRow); int requiredRowsPinned = 0; int requiredRowsUnpinned = 0; - - final int maxX = data.toFitRec.x + data.toFitRec.width; + final int maxX = data.moreRect.x - 1; int hGap = myTabs.getTabHGap(); - boolean showPinnedTabsSeparately = UISettings.getInstance().getState().getShowPinnedTabsInASeparateRow(); for (TabInfo eachInfo : data.myVisibleInfos) { - final TabLabel eachLabel = myTabs.myInfo2Label.get(eachInfo); + TabLabel eachLabel = myTabs.getTabLabel(eachInfo); boolean pinned = eachLabel.isPinned(); - final Dimension size = eachLabel.getNotStrictPreferredSize(); - int width = size.width + hGap; + int width = data.lengths.get(eachInfo); + if (!pinned || !showPinnedTabsSeparately) { + data.requiredLength += width; + } if (pinned && showPinnedTabsSeparately) { - if (eachXPinned + width >= maxX) { - requiredRowsPinned++; - eachXPinned = data.toFitRec.x; - } - else if (requiredRowsPinned == 0) { + if (requiredRowsPinned == 0) { requiredRowsPinned = 1; } - myTabs.layout(eachLabel, eachXPinned, 0, size.width, 1); - eachXPinned += width; + myTabs.layout(eachLabel, eachX, eachY, width, myTabs.myHeaderFitSize.height); + data.bounds.put(eachInfo, eachLabel.getBounds()); + eachX += width + hGap; } else { - if (eachXUnpinned + size.width + hGap >= maxX) { + if ((!scrollable && eachX + width + hGap > maxX && !singleRow) || (showPinnedTabsSeparately && eachLabel.isNextToLastPinned())) { requiredRowsUnpinned++; - eachXUnpinned = data.toFitRec.x; + eachY += myTabs.myHeaderFitSize.height; + eachX = data.toFitRec.x; } else if (requiredRowsUnpinned == 0) { requiredRowsUnpinned = 1; } - myTabs.layout(eachLabel, eachXUnpinned, 0, size.width, 1); - eachXUnpinned += width; + if (scrollable) { + if (eachX - getScrollOffset() + width + hGap > maxX) { + width = Math.max(0, maxX - eachX + getScrollOffset()); + data.invisible.add(eachInfo); + } + } + + myTabs.layout(eachLabel, eachX - getScrollOffset(), eachY, width == 1 ? 0 : width, myTabs.myHeaderFitSize.height); + Rectangle rectangle = new Rectangle(myTabs.myHeaderFitSize); + data.bounds.put(eachInfo, eachLabel.getBounds()); + int intersection = eachLabel.getBounds().intersection(rectangle).width; + if (scrollable && intersection < eachLabel.getBounds().width) { + data.invisible.add(eachInfo); + } + eachX += width + hGap; } } + if (requiredRowsPinned > 0 && requiredRowsUnpinned > 0) data.moreRect.y += myTabs.myHeaderFitSize.height /*+ myTabs.getSeparatorWidth()*/; - eachXPinned = data.toFitRec.x; - eachXUnpinned = data.toFitRec.x; + if (data.invisible.isEmpty()) { + data.moreRect.setBounds(0, 0, 0, 0); + } + + eachX = data.toFitRec.x + titleWidth; for (TabInfo eachInfo : data.myVisibleInfos) { - final TabLabel eachLabel = myTabs.myInfo2Label.get(eachInfo); - final Dimension size = eachLabel.getNotStrictPreferredSize(); + final TabLabel eachLabel = myTabs.getTabLabel(eachInfo); boolean pinned = eachLabel.isPinned(); - int width = size.width + hGap; + int width = data.lengths.get(eachInfo); if (pinned && showPinnedTabsSeparately) { - if (eachXPinned + width <= maxX) { - eachTableRow.add(eachInfo, width); - eachXPinned += width; - } else { - eachTableRow = new TableRow(data); - data.table.add(eachTableRow); - eachXPinned = data.toFitRec.x + width; - eachTableRow.add(eachInfo, width); + eachTableRow.add(eachInfo, width); + eachX += width; + } + else { + boolean useSameRow = singleRow || eachX + /*size.*/width + hGap <= maxX; + if (showPinnedTabsSeparately && eachLabel.isNextToLastPinned()) { + useSameRow = false; } - } else { - if (eachXUnpinned + size.width + hGap <= maxX && (!showPinnedTabsSeparately || !eachLabel.isNextToLastPinned())) { + if (useSameRow) { eachTableRow.add(eachInfo, width); - eachXUnpinned += width; - } else { + eachX += width; + } + else { eachTableRow = new TableRow(data); data.table.add(eachTableRow); - eachXUnpinned = data.toFitRec.x + width; + eachX = data.toFitRec.x + titleWidth + width; eachTableRow.add(eachInfo, width); } } } - + if (myScrollSelectionInViewPending) { + myScrollSelectionInViewPending = false; + doScrollSelectionInView(data); + } + clampScrollOffsetToBounds(data); return data; } - - public boolean isLastRow(TabInfo info) { - if (info == null) return false; - List rows = myLastTableLayout.table; - if (rows.size() > 0) { - for (TabInfo tabInfo : rows.get(rows.size() - 1).myColumns) { - if (tabInfo == info) return true; - } - } - - return false; - } - public LayoutPassInfo layoutTable(List visibleInfos) { - myTabs.resetLayout(true); - Insets insets = myTabs.getLayoutInsets(); - int eachY = insets.top; - TablePassInfo data = new TablePassInfo(myTabs, visibleInfos); + private void calculateLengths(TablePassInfo data) { + boolean compressible = isCompressible(); boolean showPinnedTabsSeparately = UISettings.getInstance().getState().getShowPinnedTabsInASeparateRow(); - if (!myTabs.isHideTabs()) { - data = computeLayoutTable(visibleInfos); - insets = myTabs.getLayoutInsets(); - eachY = insets.top; - int eachX; - - for (TableRow eachRow : data.table) { - eachX = insets.left; - - for (int i = 0; i < eachRow.myColumns.size(); i++) { - TabInfo tabInfo = eachRow.myColumns.get(i); - final TabLabel label = myTabs.myInfo2Label.get(tabInfo); - - int width; - if (label.isPinned() && showPinnedTabsSeparately) { - width = label.getNotStrictPreferredSize().width; - } - else { - width = label.getPreferredSize().width; - } - - myTabs.layout(label, eachX, eachY, width, myTabs.myHeaderFitSize.height); - label.setAlignmentToCenter(false); - - boolean lastCell = i == eachRow.myColumns.size() - 1; - eachX += width + (lastCell ? 0 : myTabs.getTabHGap()); + int standardLengthToFit = data.moreRect.x - (data.titleRect.x + data.titleRect.width); + if (compressible || showPinnedTabsSeparately) { + if (showPinnedTabsSeparately) { + List pinned = ContainerUtil.filter(data.myVisibleInfos, info -> info.isPinned()); + calculateCompressibleLengths(pinned, data, standardLengthToFit + data.moreRect.width); + List unpinned = ContainerUtil.filter(data.myVisibleInfos, info -> !info.isPinned()); + if (compressible) { + calculateCompressibleLengths(unpinned, data, pinned.isEmpty() + ? standardLengthToFit + data.moreRect.width + : standardLengthToFit + data.titleRect.width + data.moreRect.width); } - eachY += myTabs.myHeaderFitSize.height; + else { + calculateRawLengths(unpinned, data); + if (getTotalLength(unpinned, data) > standardLengthToFit) { + int moreWidth = myTabs.isSingleRow() ? myTabs.myMoreToolbar.getComponent().getPreferredSize().width : 0; + data.moreRect.setBounds(data.toFitRec.x + data.toFitRec.width - moreWidth, /*data.toFitRec.y*/myTabs.getLayoutInsets().top, moreWidth, myTabs.myHeaderFitSize.height /*- myTabs.getSeparatorWidth()*/); + calculateRawLengths(unpinned, data); + } + } + } + else { + calculateCompressibleLengths(data.myVisibleInfos, data, standardLengthToFit); + } + } + else {//both scrollable and multi-row + calculateRawLengths(data.myVisibleInfos, data); + if (getTotalLength(data.myVisibleInfos, data) > standardLengthToFit) { + int moreWidth = myTabs.isSingleRow() ? myTabs.myMoreToolbar.getComponent().getPreferredSize().width : 0; + data.moreRect.setBounds(data.toFitRec.x + data.toFitRec.width - moreWidth, data.toFitRec.y, moreWidth, myTabs.myHeaderFitSize.height); + calculateRawLengths(data.myVisibleInfos, data); + } + } + } + + private static int getTotalLength(@NotNull List list, @NotNull TablePassInfo data) { + int total = 0; + for (TabInfo info : list) { + total += data.lengths.get(info); + } + return total; + } + + private boolean isCompressible() { + return myTabs.isSingleRow() && !UISettings.getInstance().getHideTabsIfNeeded() && myTabs.supportsCompression(); + } + + private void calculateCompressibleLengths(List list, TablePassInfo data, int toFitLength) { + if (list.isEmpty()) return; + int hGap = myTabs.getTabHGap(); + int maxGridSize = 0; + int spentLength = 0; + int lengthEstimation = 0; + + int[] lengths = new int[list.size()]; + for (int i = 0; i < list.size(); i++) { + TabInfo tabInfo = list.get(i); + final TabLabel label = myTabs.myInfo2Label.get(tabInfo); + if (maxGridSize == 0) { + Font font = label.getLabelComponent().getFont(); + maxGridSize = GraphicsUtil.stringWidth("m", font == null ? JBFont.label() : font) * myTabs.tabMSize(); + } + int lengthIncrement = Math.max(JBUI.scale(50), label.getPreferredSize().width); + lengths[i] = lengthIncrement; + lengthEstimation += lengthIncrement; + } + + final int extraWidth = toFitLength - lengthEstimation; + + Arrays.sort(lengths); + double acc = 0; + int actualGridSize = 0; + for (int i = 0; i < lengths.length; i++) { + int length = lengths[i]; + acc += length; + actualGridSize = (int)Math.min(maxGridSize, (acc + extraWidth) / (i + 1)); + if (i < lengths.length - 1 && actualGridSize < lengths[i + 1]) break; + } + + + for (Iterator iterator = list.iterator(); iterator.hasNext(); ) { + TabInfo tabInfo = iterator.next(); + final TabLabel label = myTabs.myInfo2Label.get(tabInfo); + + int length; + int lengthIncrement = label.getPreferredSize().width; + if (!iterator.hasNext()) { + length = Math.min(toFitLength - spentLength, Math.max(actualGridSize, lengthIncrement)); + } + else if (extraWidth <= 0) {//need compress + length = (int)(lengthIncrement * (float)toFitLength / lengthEstimation); + } + else { + length = Math.max(lengthIncrement, actualGridSize); + } + if (tabInfo.isPinned()) { + length = Math.min(getMaxPinnedTabWidth(), length); + } + length = Math.max(JBUI.scale(50), length); + data.lengths.put(tabInfo, length); + spentLength += length + hGap; + } + } + + private void calculateRawLengths(List list, TablePassInfo data) { + boolean showPinnedTabsSeparately = UISettings.getInstance().getState().getShowPinnedTabsInASeparateRow(); + for (TabInfo info : list) { + TabLabel eachLabel = myTabs.getTabLabel(info); + Dimension size = + eachLabel.isPinned() && showPinnedTabsSeparately ? eachLabel.getNotStrictPreferredSize() : eachLabel.getPreferredSize(); + data.lengths.put(info, Math.max(JBUI.scale(50), size.width + myTabs.getTabHGap())); + } + } + + public LayoutPassInfo layoutTable(List visibleInfos, JComponent title, JComponent moreToolbar) { + myTabs.resetLayout(true); + Rectangle unitedTabArea = null; + TablePassInfo data = computeLayoutTable(visibleInfos); + Rectangle rect = new Rectangle(data.moreRect); + rect.y += myTabs.getBorderThickness(); + moreToolbar.setBounds(rect); + title.setBounds(data.titleRect); + Insets insets = myTabs.getLayoutInsets(); + int eachY = insets.top; + for (TabInfo info : visibleInfos) { + Rectangle bounds = data.bounds.get(info); + if (unitedTabArea == null) { + unitedTabArea = bounds; + } + else { + unitedTabArea = unitedTabArea.union(bounds); } } if (myTabs.getSelectedInfo() != null) { final JBTabsImpl.Toolbar selectedToolbar = myTabs.myInfo2Toolbar.get(myTabs.getSelectedInfo()); - final int componentY = eachY + (myTabs.isEditorTabs() ? 0 : 2) - myTabs.getLayoutInsets().top; + final int componentY = (unitedTabArea != null ? unitedTabArea.y + unitedTabArea.height : eachY) + (myTabs.isEditorTabs() ? 0 : 2) - + myTabs.getLayoutInsets().top; if (!myTabs.myHorizontalSide && selectedToolbar != null && !selectedToolbar.isEmpty()) { final int toolbarWidth = selectedToolbar.getPreferredSize().width; final int vSeparatorWidth = toolbarWidth > 0 ? myTabs.getSeparatorWidth() : 0; if (myTabs.isSideComponentBefore()) { - Rectangle compRect = myTabs.layoutComp(toolbarWidth + vSeparatorWidth, componentY, myTabs.getSelectedInfo().getComponent(), 0, 0); + Rectangle compRect = + myTabs.layoutComp(toolbarWidth + vSeparatorWidth, componentY, myTabs.getSelectedInfo().getComponent(), 0, 0); myTabs.layout(selectedToolbar, compRect.x - toolbarWidth - vSeparatorWidth, compRect.y, toolbarWidth, compRect.height); } else { @@ -175,7 +301,9 @@ public class TableLayout extends TabLayout { myTabs.layoutComp(0, componentY, myTabs.getSelectedInfo().getComponent(), 0, 0); } } - + if (unitedTabArea != null) { + data.tabRectangle.setBounds(unitedTabArea); + } myLastTableLayout = data; return data; } @@ -209,9 +337,9 @@ public class TableLayout extends TabLayout { Rectangle secondBounds = second.getBounds(); final boolean between = firstBounds.getMaxX() < point.x - && secondBounds.getX() > point.x - && firstBounds.y < point.y - && secondBounds.getMaxY() > point.y; + && secondBounds.getX() > point.x + && firstBounds.y < point.y + && secondBounds.getMaxY() > point.y; if (between) { c = first; @@ -232,7 +360,8 @@ public class TableLayout extends TabLayout { } } result = index; - } else if (index < myLastTableLayout.myVisibleInfos.size()) { + } + else if (index < myLastTableLayout.myVisibleInfos.size()) { result = index; } } @@ -240,8 +369,111 @@ public class TableLayout extends TabLayout { } @Override - @MagicConstant(intValues = {SwingConstants.CENTER, SwingConstants.TOP, SwingConstants.LEFT, SwingConstants.BOTTOM, SwingConstants.RIGHT, -1}) + @MagicConstant(intValues = { + SwingConstants.CENTER, + SwingConstants.TOP, + SwingConstants.LEFT, + SwingConstants.BOTTOM, + SwingConstants.RIGHT, + -1 + }) public int getDropSideFor(@NotNull Point point) { return TabsUtil.getDropSideFor(point, myTabs); } + + int getScrollOffset() { + return myScrollOffset; + } + + public void scroll(int units) { + if (!myTabs.isSingleRow()) { + myScrollOffset = 0; + return; + } + myScrollOffset += units; + + clampScrollOffsetToBounds(myLastTableLayout); + } + + private void clampScrollOffsetToBounds(@Nullable TablePassInfo data) { + if (data == null) { + return; + } + if (data.requiredLength < data.toFitRec.width) { + myScrollOffset = 0; + } + else { + myScrollOffset = Math.max(0, Math.min(myScrollOffset, + data.requiredLength - data.toFitRec.width + data.moreRect.width /*+ (1 + myTabs.getIndexOf(myTabs.getSelectedInfo())) * myTabs.getBorderThickness()*/+ data.titleRect.width)); + } + } + + public int getScrollUnitIncrement() { + return 10; + } + + public void scrollSelectionInView() { + myScrollSelectionInViewPending = true; + doScrollSelectionInView(myLastTableLayout); + } + + @Override + public boolean canShowMorePopup() { + return myLastTableLayout != null && !myLastTableLayout.moreRect.isEmpty(); + } + + @Override + public void showMorePopup() { + Rectangle rect = myLastTableLayout != null ? myLastTableLayout.moreRect : null; + if (rect == null || rect.isEmpty()) return; + + JBPopupMenu menu = new JBPopupMenu(); + for (final TabInfo each : myLastTableLayout.invisible) { + menu.add(createMenuItem(each)); + } + menu.show(myTabs, rect.x, rect.y + rect.height); + } + + private JBMenuItem createMenuItem(@NotNull TabInfo tabInfo) { + final JBMenuItem item = new JBMenuItem(tabInfo.getText(), tabInfo.getIcon()); + item.setForeground(tabInfo.getDefaultForeground()); + item.setBackground(tabInfo.getTabColor()); + item.addActionListener(__ -> myTabs.select(tabInfo, true)); + return item; + } + + private void doScrollSelectionInView(TablePassInfo data) { + if (myTabs.isMouseInsideTabsArea() + || data == null + || data.lengths.isEmpty() + || myTabs.isHideTabs() + || !UISettings.getInstance().getState().getShowPinnedTabsInASeparateRow()) { + return; + } + + int offset = -myScrollOffset; + for (TabInfo info : data.myVisibleInfos) { + if (info.isPinned()) continue; + final int length = data.lengths.get(info); + if (info == myTabs.getSelectedInfo()) { + if (offset < 0) { + scroll(offset); + } + else { + final int maxLength = data.moreRect.x; + if (offset + length > maxLength) { + // left side should be always visible + if (length < maxLength) { + scroll(offset + length - maxLength); + } + else { + scroll(offset); + } + } + } + break; + } + offset += length; + } + } } diff --git a/platform/platform-api/src/com/intellij/ui/tabs/impl/table/TablePassInfo.java b/platform/platform-api/src/com/intellij/ui/tabs/impl/table/TablePassInfo.java index c26149aaf091..ecb1b6dbf399 100644 --- a/platform/platform-api/src/com/intellij/ui/tabs/impl/table/TablePassInfo.java +++ b/platform/platform-api/src/com/intellij/ui/tabs/impl/table/TablePassInfo.java @@ -4,22 +4,28 @@ package com.intellij.ui.tabs.impl.table; import com.intellij.ui.tabs.TabInfo; import com.intellij.ui.tabs.impl.JBTabsImpl; import com.intellij.ui.tabs.impl.LayoutPassInfo; +import org.jetbrains.annotations.NotNull; import java.awt.*; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.*; -class TablePassInfo extends LayoutPassInfo { +public class TablePassInfo extends LayoutPassInfo { final List table = new ArrayList<>(); public final Rectangle toFitRec; + public final Rectangle tabRectangle = new Rectangle(); final Map myInfo2Row = new HashMap<>(); final JBTabsImpl myTabs; + @NotNull public final Rectangle moreRect = new Rectangle(); + @NotNull public final Rectangle titleRect = new Rectangle(); + public final List invisible = new ArrayList<>(); + final Map lengths = new LinkedHashMap<>(); + final Map bounds = new HashMap<>(); + int requiredLength = 0; - TablePassInfo(final JBTabsImpl tabs, List visibleInfos) { + TablePassInfo(TableLayout layout, List visibleInfos) { super(visibleInfos); - myTabs = tabs; + myTabs = layout.myTabs; final Insets insets = myTabs.getLayoutInsets(); toFitRec = new Rectangle(insets.left, insets.top, myTabs.getWidth() - insets.left - insets.right, myTabs.getHeight() - insets.top - insets.bottom); diff --git a/platform/platform-impl/src/com/intellij/application/options/editor/EditorTabsConfigurable.kt b/platform/platform-impl/src/com/intellij/application/options/editor/EditorTabsConfigurable.kt index cb1b2f31552d..0d009921b5cf 100644 --- a/platform/platform-impl/src/com/intellij/application/options/editor/EditorTabsConfigurable.kt +++ b/platform/platform-impl/src/com/intellij/application/options/editor/EditorTabsConfigurable.kt @@ -78,8 +78,7 @@ class EditorTabsConfigurable : BoundSearchableConfigurable( myMultipleRowsRadio = radioButton(showTabsInMultipleRows) .enableIf(myEditorTabPlacement.selectedValueIs(SwingConstants.TOP)).component checkBox(showPinnedTabsInASeparateRow).enableIf( - myEditorTabPlacement.selectedValueIs(SwingConstants.TOP) - and myMultipleRowsRadio.selected).withLargeLeftGap().component + myEditorTabPlacement.selectedValueIs(SwingConstants.TOP)).withLargeLeftGap().component }) } val group = ButtonGroup() diff --git a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorTabbedContainer.java b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorTabbedContainer.java index c75532319b75..a94d031320ab 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorTabbedContainer.java +++ b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorTabbedContainer.java @@ -654,6 +654,11 @@ public final class EditorTabbedContainer implements CloseAction.CloseTarget { }); } + @Override + protected boolean supportsTableLayoutAsSingleRow() { + return true; + } + @Override protected void paintChildren(Graphics g) { super.paintChildren(g);